diff --git a/.github/TEAM_MEMBERS b/.github/TEAM_MEMBERS index 5eb465e0dd..ee2e26f452 100644 --- a/.github/TEAM_MEMBERS +++ b/.github/TEAM_MEMBERS @@ -18,4 +18,4 @@ simonklee Slickstef11 usrnk1 vimtor -starptech +StarpTech diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 8981aad49a..1c41a66faa 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -6,7 +6,6 @@ on: branches: - ci - dev - - v2 - beta - fix/npm-native-binary-install - snapshot-* @@ -32,9 +31,6 @@ permissions: contents: write packages: write -env: - OPENCODE_CHANNEL: ${{ (github.ref_name == 'v2' && 'next') || '' }} - jobs: version: runs-on: blacksmith-4vcpu-ubuntu-2404 @@ -126,7 +122,7 @@ jobs: - build-cli - version runs-on: blacksmith-4vcpu-windows-2025 - if: github.repository == 'anomalyco/opencode' && github.ref_name != 'v2' + if: github.repository == 'anomalyco/opencode' env: AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} @@ -223,9 +219,8 @@ jobs: build-electron: needs: - - build-cli - version - if: github.repository == 'anomalyco/opencode' && github.ref_name != 'v2' + if: github.repository == 'anomalyco/opencode' continue-on-error: false env: AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} @@ -320,10 +315,7 @@ jobs: env: OPENCODE_VERSION: ${{ needs.version.outputs.version }} OPENCODE_CHANNEL: ${{ (github.ref_name == 'beta' && 'beta') || 'prod' }} - OPENCODE_CLI_ARTIFACT: ${{ (runner.os == 'Windows' && 'opencode-cli-windows') || 'opencode-cli' }} RUST_TARGET: ${{ matrix.settings.target }} - GH_TOKEN: ${{ github.token }} - GITHUB_RUN_ID: ${{ github.run_id }} - name: Build run: bun run build @@ -347,8 +339,7 @@ jobs: env: OPENCODE_CHANNEL: ${{ (github.ref_name == 'beta' && 'beta') || 'prod' }} GH_TOKEN: ${{ steps.committer.outputs.token }} - CSC_LINK: ${{ secrets.APPLE_CERTIFICATE }} - CSC_KEY_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} + CSC_KEYCHAIN: build.keychain APPLE_API_KEY: ${{ runner.temp }}/apple-api-key.p8 APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY }} APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }} @@ -451,7 +442,6 @@ jobs: path: packages/opencode/dist - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 - if: github.ref_name != 'v2' with: name: opencode-cli-signed-windows path: packages/opencode/dist diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 534a8d78a7..c69de1d93b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -4,7 +4,6 @@ on: push: branches: - dev - - v2 pull_request: workflow_dispatch: @@ -75,9 +74,13 @@ jobs: working-directory: packages/client run: bun run check:generated + - name: Run HttpApi exerciser gates + if: runner.os == 'Linux' + working-directory: packages/opencode + run: bun run test:httpapi + e2e: name: e2e (${{ matrix.settings.name }}) - if: github.ref_name != 'v2' && github.head_ref != 'v2' strategy: fail-fast: false matrix: diff --git a/.github/workflows/typecheck.yml b/.github/workflows/typecheck.yml index 5c83a8e691..fc9a52797c 100644 --- a/.github/workflows/typecheck.yml +++ b/.github/workflows/typecheck.yml @@ -2,9 +2,9 @@ name: typecheck on: push: - branches: [dev, v2] + branches: [dev] pull_request: - branches: [dev, v2] + branches: [dev] workflow_dispatch: jobs: diff --git a/.gitignore b/.gitignore index ba80d79b90..006cab8c27 100644 --- a/.gitignore +++ b/.gitignore @@ -13,7 +13,6 @@ tmp dist ts-dist .turbo -.typecheck-profiles **/.serena .serena/ **/.omo diff --git a/.opencode/command/translate.md b/.opencode/command/translate.md index 8d493f4a81..de18ae2ee8 100644 --- a/.opencode/command/translate.md +++ b/.opencode/command/translate.md @@ -1,6 +1,6 @@ --- description: translate English to other languages -model: opencode/claude-opus-4-8 +model: opencode/gpt-5.6-sol --- run git diff and translate changed english doc and UI copy files to other international languages. Translate all languages in parallel to save time. diff --git a/.opencode/skills/opencode-drive/SKILL.md b/.opencode/skills/opencode-drive/SKILL.md deleted file mode 100644 index 6633526b10..0000000000 --- a/.opencode/skills/opencode-drive/SKILL.md +++ /dev/null @@ -1,254 +0,0 @@ ---- -name: opencode-drive -description: Use when an agent needs drive OpenCode via a script or interact with an isolated instance ---- - -# OpenCode Drive - -Use `opencode-drive` to launch an isolated OpenCode instance and control it via commands or a script. - -There are two modes. Always default to using a script unless specifically directed to be interactive (connect -to an existing running instance, or start a new one, and make a few changes to the UI and read it, and iterate -on changes). - -Scripts allow you to run a full walkthrough in one run. When the script is done opencode-drive exits, -stops all processes, and cleans up all artifacts. - -# Prepare The Environment - -Use `init` when files must be added to the isolated home or project before OpenCode starts. It prints the artifact directory without launching OpenCode. A later `start` with the same name reuses it. - -```bash -artifacts=$(opencode-drive init --name demo) -cp -R ./fixtures/home/. "$artifacts/" -cp -R ./fixtures/project/. "$artifacts/files/" -opencode-drive start --name demo --dev ~/projects/opencode -``` - -The simulated project is under `$artifacts/files`. Running `start` without a prior `init` initializes the artifacts automatically. - -# Scripted usage - -You can write scripts that walk through entire flows, and gives you full access to controlling -the backend too. See examples of the script API at the bottom of this file. - -After creating or editing a script, always typecheck it before running. Never skip this step: - -```bash -opencode-drive check ./reproduce-stale-exploring-empty.ts -``` - -Run it by passing `--script` to start: - -```bash -opencode-drive start --name auto-stop-reproduction --script ./reproduce-stale-exploring-empty.ts -``` - -It will output information about the run, including paths to log files which you can read -to inspect what happened. If you need to dig into failures that aren't clear, read those log -files. If the script is unsuccessful, automatically fix the script and run it again. - -Scripts use one typed definition object. `setup` runs before OpenCode starts, -and `fs.writeFile` always writes inside the simulated project. - -You can read the full typed API here: https://raw.githubusercontent.com/jlongster/opencode-drive/refs/heads/main/src/script/types.ts - -```ts -import { defineScript } from "opencode-drive" - -export default defineScript({ - async setup({ fs, config }) { - config.autoupdate = false - await fs.writeFile("src/example.ts", "export const value = 1\n") - }, - - async run({ ui, llm }) { - await ui.submit("Open src/example.ts") - await llm.send(llm.text("The file exports `value`.")) - await ui.waitFor("The file exports `value`.") - }, -}) -``` - -`setup` receives the current OpenCode config object, which starts from the -default drive config unless the prepared instance already has one. When a script -needs custom config, mutate this `config` parameter instead of generating and -writing a new config object from scratch, so the script keeps the default -provider/model settings unless it intentionally changes them. - -Note that the simulated model is a GPT model type, and opencode uses the `patch` tool for working with files Do not use a `edit` or `write` tool to edit files. - -Use `launch: "manual"` when the script needs to launch the server and every TUI -itself (this is extremely rare, do not use this unless explicitly asked). In this -mode `ui` is typed as `null`; call `server.launch()` exactly -once before launching clients. Each `clients.launch(name)` result provides the -same UI methods as the automatic client. You can see an example of this API -here: https://raw.githubusercontent.com/jlongster/opencode-drive/refs/heads/main/examples/multiple-clients.ts - -Use the exported `wait(milliseconds)` utility for an unconditional delay. - -`await llm.send(...)` waits for the next request and resolves after OpenCode -acknowledges its complete response. `llm.queue(...)` declares responses in -advance. Chunks may be built with `text`, `reasoning`, `toolCall`, `raw`, -`finish`, and `disconnect`. A normal response receives `finish("stop")` -automatically unless it yields or queues an explicit terminal event. - -`llm.text(text, { delay, chunkSize })` defaults to a 2 ms delay and a -15-character target varied by plus or minus 5 per chunk. - -`llm.reasoning` accepts the same options, and `llm.pause(milliseconds)` adds a -delay between any two outputs. - -Use `llm.serve` for an ongoing typed response generator: - -```ts -llm.serve(async function* (request, index) { - yield llm.reasoning(`Handling request ${index + 1}`) - yield llm.text(`Received ${request.id}`) - yield llm.finish("stop") -}) -``` - -The backend connection, response cleanup, cancellation, and recording -completion are automatic. - -You can see some example scripts here: - -- https://raw.githubusercontent.com/jlongster/opencode-drive/refs/heads/main/examples/simple.ts -- https://raw.githubusercontent.com/jlongster/opencode-drive/refs/heads/main/examples/serve.ts - -## Prune - -- `prune` removes artifact directories. These are always cleaned up after running a script - successfully, but leftover on failed runs. Always call this if a script fails. - -```bash -opencode-drive prune --name demo - -// --force cleans up all artifcat directories -opencode-dirve prune --force -``` - -# Live interaction usage - -- Always give headless instances a unique `--name`. Visible instances may omit it. -- A normal headless `start` detaches automatically and returns after the instance is ready. -- Do not add `&`; the long-running owner already runs in the background. -- Configure simulated model responses after startup when needed. -- Send ordered UI commands with `send`. -- Always stop the instance when finished. - -```bash -opencode-drive start --name demo - -opencode-drive send --name demo \ - --command.ui.type '{"text":"Explain this project"}' \ - --command.ui.enter - -opencode-drive stop --name demo -``` - -## Send UI Commands - -- Every `send` opens a connection to the named instance, runs its commands in order, and exits. -- Combine typing and Enter in one command when submitting a prompt. -- JSON-valued commands require one JSON argument. -- Multiple command flags execute from left to right. - -Commands: - -- `--command.ui.type ` types into the focused editor. Arguments: `text` string. -- `--command.ui.press ` presses a key. Arguments: `key` string; optional `modifiers` object with boolean `ctrl`, `shift`, `meta`, `super`, or `hyper`. -- `--command.ui.enter` presses Enter. Arguments: none. -- `--command.ui.arrow ` presses an arrow key. Arguments: `direction` is `up`, `down`, `left`, or `right`. -- `--command.ui.focus ` focuses an element. Arguments: `target` is the numeric element `num` returned by `ui.state`. -- `--command.ui.click ` clicks an element. Arguments: numeric `target`, `x`, and `y`; use the element `num` returned by `ui.state` as `target`. -- `--command.ui.state` prints focus and interactive element metadata as JSON. Arguments: none. -- `--command.ui.matches ` prints whether literal, case-sensitive text appears on screen. Arguments: `text` string. - -```bash -opencode-drive send --name demo \ - --command.ui.type '{"text":"Find the relevant code and explain it"}' \ - --command.ui.enter - -opencode-drive send --name demo \ - --command.ui.press '{"key":"p","modifiers":{"ctrl":true}}' - -opencode-drive send --name demo \ - --command.ui.arrow '{"direction":"down"}' - -opencode-drive send --name demo \ - --command.ui.focus '{"target":12}' - -opencode-drive send --name demo \ - --command.ui.click '{"target":12,"x":4,"y":1}' - -opencode-drive send --name demo \ - --command.ui.matches '{"text":"OpenCode"}' -``` - -To read the UI state and see information about interactable elements, use the `ui.state` command: - -```bash -opencode-drive send --name demo --command.ui.state -``` - -## Configure LLM Responses - -- `responses` controls what the LLM responds with -- Only use this if you are wanting to reproduce an exact type of response -- Defaults are `text,reasoning,diff,tool` with `write,apply_patch`. -- Supported types are `text`, `reasoning`, `diff`, and `tool`. -- `--tools` limits generated tool calls to names offered by OpenCode. - -```bash -opencode-drive responses --name demo \ - --types text,reasoning,diff,tool \ - --tools write,apply_patch - -opencode-drive responses --name demo \ - --types tool \ - --tools read,glob,grep -``` - -## Inspect The UI - -- `ui.state` prints focus and interactive element metadata as JSON. -- `ui.matches` checks for literal, case-sensitive screen text. -- `screenshot` prints the generated image path. - -```bash -opencode-drive screenshot --name demo -``` - -## Lifecycle - -- `stop` waits for recording export and owner cleanup before returning. - -```bash -opencode-drive stop --name demo -``` - -# Record The UI - -- Start with `--record` to capture a headless instance from its first rendered frame. -- `stop` finishes the recording, exports an MP4, and prints its path. - -```bash -opencode-drive start --name demo --record - -opencode-drive send --name demo \ - --command.ui.type '{"text":"Show me the current architecture"}' \ - --command.ui.enter - -opencode-drive stop --name demo -``` - -# Artifacts dir - -- `dir` prints the artifact directory for the instance. - -```bash -opencode-drive dir --name demo -``` - diff --git a/.opencode/skills/sample-skill/SKILL.md b/.opencode/skills/sample-skill/SKILL.md deleted file mode 100644 index 87b9e96907..0000000000 --- a/.opencode/skills/sample-skill/SKILL.md +++ /dev/null @@ -1,31 +0,0 @@ ---- -name: sample-skill -description: Use when the user says sample skill, skill demo, or asks how an opencode SKILL.md should be structured; demonstrates a tiny project-local skill with practical assistant workflow guidance. ---- - -# Sample Skill - -This is a minimal project-local opencode skill. It exists as a reference for how a skill is structured and as a tiny reusable workflow the assistant can load when the user asks for a skill example. - -## When To Use - -- Use when the user asks for a sample skill or skill template. -- Use when demonstrating the required `SKILL.md` frontmatter and body format. -- Do not use for unrelated coding tasks just because a skill exists. - -## Workflow - -- Confirm the specific outcome the user wants if the request is ambiguous. -- Inspect the relevant files before changing anything. -- Make the smallest correct change. -- Verify the result with a focused read, typecheck, test, or other lightweight check when available. -- Summarize the changed files and any required restart or reload step. - -## Example Response Style - -When this skill is relevant, keep responses direct and actionable: - -```text -I created a project-local skill at .opencode/skills/sample-skill/SKILL.md. -Restart opencode for the new skill to be discovered by future sessions. -``` diff --git a/.opencode/tool/github-triage.ts b/.opencode/tool/github-triage.ts index f25ca48b38..e861e1e467 100644 --- a/.opencode/tool/github-triage.ts +++ b/.opencode/tool/github-triage.ts @@ -4,7 +4,7 @@ import { tool } from "@opencode-ai/plugin" const TEAM = { tui: ["kommander", "simonklee"], desktop_web: ["Hona", "Brendonovich"], - core: ["jlongster", "rekram1-node", "nexxeln", "kitlangton", "starptech"], + core: ["jlongster", "rekram1-node", "nexxeln", "kitlangton"], inference: ["fwang", "MrMushrooooom", "starptech"], windows: ["Hona"], } as const diff --git a/AGENTS.md b/AGENTS.md index f6f3c970e8..cd2327e888 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,7 +1,6 @@ - To regenerate the legacy JavaScript SDK, run `./packages/sdk/js/script/build.ts`. - After changing the public Protocol or Server `HttpApi`, run `bun run generate` from `packages/client`. Do not edit `src/generated` or `src/generated-effect` directly. - Keep runtime dependencies directed from Schema to Core and Protocol, then from Core and Protocol to Server. Client runtime code may depend on Schema and Protocol but never Core or Server; `sdk-next` composes Client, Core, and Server. -- Do not modify `packages/opencode` unless the user explicitly asks for V1 work. `packages/opencode` is the V1 implementation and is present for reference only. New implementation changes should land in the V2 package set: `packages/core`, `packages/cli`, `packages/server`, `packages/protocol`, `packages/schema`, and related generated client surfaces when required. - The default branch in this repo is `dev`. - Local `main` ref may not exist; use `dev` or `origin/dev` for diffs. @@ -19,15 +18,12 @@ Valid types are `feat`, `fix`, `docs`, `chore`, `refactor`, and `test`. Scopes a Examples: `fix(tui): simplify thinking toggle styling`, `docs: update contributing guide`, `chore(sdk): regenerate types`. -Never bypass Git hooks. Do not use `--no-verify` or otherwise disable, skip, or circumvent commit or push hooks. If a hook fails, fix the failure or stop and report it to the user. - ## Style Guide ### General Principles - Keep things in one function unless composable or reusable - Do not extract single-use helpers preemptively. Inline the logic at the call site unless the helper is reused, hides a genuinely complex boundary, or has a clear independent name that improves the caller. -- Before adding complexity for a speculative or vanishingly unlikely race or security edge case, explain the concrete failure mode, likelihood, and complexity cost to the user and get their buy-in. Do not silently expand scope for theoretical robustness. - Avoid `try`/`catch` where possible - Avoid using the `any` type - Use Bun APIs when possible, like `Bun.file()` @@ -154,15 +150,12 @@ const table = sqliteTable("session", { ## V2 Session Core -- Keep durable events minimal: record irreducible new facts and do not repeat state derivable by folding the ordered aggregate history. Enrich projections and read models with previous or derived state when consumers need self-contained views. -- Keep durable prompt admission separate from model execution. `SessionV2.prompt(...)` admits one durable `session_pending` row before scheduling advisory `SessionExecution.wake(sessionID)` unless `resume: false` requests admit-only behavior. The serialized runner promotes admitted inputs into visible user messages at safe boundaries, consuming the pending row in the same event transaction; `session_pending` stores only unconsumed work. -- Reusing a Session ID adopts the existing Session. Reusing a prompt message ID reconciles an exact retry only when Session, prompt, and delivery mode match; conflicting reuse fails. Retry of an already-promoted input reconciles against the projected message and the durable admitted event rather than a retained row. -- Keep `SessionExecution` process-global and Session-ID based. Its local implementation owns the process-local Session coordinator and discovers placement through `SessionStore` plus `LocationServiceMap.get(session.location)` only when a drain starts; no layer should take a Session ID. V2 interruption targets the active process-local ownership chain for that Session; interruption of a known but idle or locally unowned Session is a no-op, while the public API rejects an unknown Session. +- Keep durable prompt admission separate from model execution. `SessionV2.prompt(...)` admits one durable `session_input` row before scheduling advisory `SessionExecution.wake(sessionID)` unless `resume: false` requests admit-only behavior. The serialized runner promotes admitted inputs into visible user messages at safe boundaries. +- Reusing a Session ID adopts the existing Session. Reusing a prompt message ID reconciles an exact retry only when Session, prompt, and delivery mode match; conflicting reuse fails. Historical projected prompts lazily synthesize promoted inbox records during exact retry. +- Keep `SessionExecution` process-global and Session-ID based. Its local implementation owns the process-local Session coordinator and discovers placement through `SessionStore` plus `LocationServiceMap.get(session.location)` only when a drain starts; no layer should take a Session ID. V2 interruption targets the active process-local ownership chain for that Session; idle or missing interruption is a no-op. - Keep `SessionRunner`, model resolution, tool registry, permissions, and filesystem Location-scoped. Omitted `Location.workspaceID` means implicit-local placement; explicit workspace identity remains reserved for future placement semantics. -- Preserve one explicit `llm.stream(request)` call per Physical Attempt and reload projected history before durable continuation. Most Steps have one Physical Attempt; overflow-triggered compaction recovery may rebuild one Step for a second attempt. Do not bridge through legacy `SessionPrompt.loop(...)` or delegate orchestration to an in-memory tool loop. +- Preserve one explicit `llm.stream(request)` call per provider turn and reload projected history before durable continuation. Do not bridge through legacy `SessionPrompt.loop(...)` or delegate orchestration to an in-memory tool loop. - Keep local Session drains process-local until clustering is implemented. `SessionRunCoordinator` joins explicit same-Session resumes, coalesces prompt wakeups, and allows different Sessions to run concurrently. Advisory wakes drain eligible durable inbox rows only; post-crash continuation recovery requires a separate explicit design before it may retry provider work. A drain has no durable identity or transcript boundary. -- Keep delivery vocabulary explicit. Prompts steer by default and promote at the next safe step boundary while the current drain requires continuation. An explicit `queue` input remains pending until the Session would otherwise become idle; promote one queued input at that boundary, then reevaluate continuation before promoting another. Promoting any new user input resets the selected agent's step allowance; a batch of steers resets it once. -- One step is one logical LLM call; its durable record covers only the model-visible span. Do not write "provider turn", and do not use bare "turn" for a single call: "turn" is reserved for the future assistant-turn unit containing all steps from prompt promotion until the session would go idle. +- Keep delivery vocabulary explicit. Prompts steer by default and promote at the next safe provider-turn boundary while the current drain requires continuation. An explicit `queue` input remains pending until the Session would otherwise become idle; promote one queued input at that boundary, then reevaluate continuation before promoting another. Promoting any new user input resets the selected agent's provider-turn allowance; a batch of steers resets it once. - Keep EventV2 replay owner claims separate from clustered Session execution ownership. -- Keep the Instructions algebra and built-ins in `src/instructions`; keep instruction producers with their observed domains, and keep Session History selection plus `InstructionState` and `InstructionEntry` persistence Session-owned. `InstructionDiscovery` observes ambient global and upward-project instructions. The runner composes built-ins, discovery, guidance, and entries explicitly in `loadInstructions`; there is no instruction registry. -- `session.instructions.updated` stores only changed source keys and content hashes. Blob values live once in `instruction_blob`; `instruction_state` is a rebuildable fold cache, never primary state. Render initial instructions and chronological updates from values during request assembly. Completed compaction moves the instruction epoch; Session movement and committed revert clear it. Unavailable sources retain the last value and block only the initial complete delta. +- Keep the System Context algebra, registry, and built-ins in `src/system-context`; keep Context Source producers with their observed domains, and keep Session History selection plus Context Epoch persistence Session-owned. diff --git a/CONTEXT.md b/CONTEXT.md index 79611f30f5..5e5955d344 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -4,50 +4,40 @@ OpenCode sessions preserve durable conversational history while assembling the r ## Language -**Model Context**: -The complete model-visible input assembled for one **Step**, including system instructions, **Session History**, tool definitions, and step-local additions. **Instructions** are one component of Model Context, not a synonym for it. -_Avoid_: System Context - -**Instructions**: -The opaque algebra of independently refreshable typed instruction sources that render the durable instruction baseline and chronological updates shown to the model. -_Avoid_: Model Context, System Context +**System Context**: +The structured collection of contextual facts presented to the model as initial instructions and chronological updates. +_Avoid_: System prompt **Session History**: -The projected chronological conversation selected for a **Step** after applying the active compaction boundary and interleaving derived **Instruction Updates** from the current **Instruction Epoch**. +The projected chronological conversation selected for a provider turn after applying the active compaction and **Context Epoch** cutoffs. _Avoid_: Session Context -**Instruction Source**: -One independently read typed value within **Instructions**, represented by a stable namespaced key, canonical JSON codec, pure first/changed renderers, and an optional removal renderer. +**Context Source**: +One independently observed typed value within the **System Context**, represented by a stable key, JSON codec, infallible loader, pure baseline/update renderers, and an optional removal renderer for dynamic sources. _Avoid_: Prompt fragment -**InstructionEntry**: -One API-managed, durable, per-Session instruction value. Its slash-free client key maps to the `api/` **Instruction Source** key. Entries deliberately render to the model as mechanism-neutral `` blocks: the model sees session context, not how it was attached. +**System Context Registry**: +The Location-scoped registry of ordered, scoped producers that contribute to the current **System Context**. -**InstructionDiscovery**: -The Location-scoped service that observes ambient global and upward-project `AGENTS.md` files as one ordered aggregate **Instruction Source**. +**Mid-Conversation System Message**: +A durable chronological instruction that tells the model the newly effective state of a changed **Context Source**. +_Avoid_: System update, system notification, raw text diff -**Instruction State**: -The Session-owned projection cache of one instruction log fold: epoch start, values at that start, current values, and the last folded sequence. It is rebuilt from durable events and never authors model-visible facts. +**Context Epoch**: +The span during which one initially rendered **System Context** remains the immutable provider-cache baseline, ending at completed compaction, Session movement, or an incompatible context transition that requires a fresh baseline. -**Instruction Update**: -A durable `session.instructions.updated` value delta admitted at a **Safe Step Boundary**. Its model-visible System text is rendered from stored values at request assembly and is never persisted verbatim. -_Avoid_: Correction, stored prose, raw text diff - -**Initial Instructions**: -The deterministic instruction text rendered from values at the current **Instruction Epoch** start and sent as provider-cache prefix state until completed compaction moves the epoch or Session movement or committed revert resets it. +**Baseline System Context**: +The full **System Context** rendered at the start of a **Context Epoch**. _Avoid_: Live system prompt -**Instruction Epoch**: -The span between completed compactions. Its start is the last `session.compaction.ended` sequence, or the initial complete instruction delta when no prior epoch exists. +**Context Snapshot**: +The overwriteable model-hidden JSON state used to compare each **Context Source** with the value last admitted to a provider turn. -**Instruction Values**: -The key-to-hash map produced by folding instruction deltas in durable sequence order. Hash bodies live once in the content-addressed instruction blob store. +**Unavailable Context**: +An expected temporary inability to observe a **Context Source** value; the runtime retains its prior effective state and emits no update, or omits it until first successfully loaded. -**Unavailable Instruction Source**: -An expected temporary inability to read an **Instruction Source** value; the runtime retains its prior effective value and emits no update, while an unavailable source blocks the initial complete delta. - -**Safe Step Boundary**: -The point during Step preparation, after prior tool settlement and before durable input promotion, where instruction changes may be admitted chronologically. +**Safe Provider-Turn Boundary**: +The point immediately before a provider call, after durable input promotion and any required tool settlement, where context changes may be admitted chronologically. **Admitted Prompt**: A durable user input accepted into the Session inbox but not yet included in **Session History**. @@ -55,24 +45,11 @@ A durable user input accepted into the Session inbox but not yet included in **S **Prompt Promotion**: The durable transition that removes an **Admitted Prompt** from pending input and appends its user message to **Session History**. -**Step**: -One logical LLM call spanning pre-flight instruction synchronization, input promotion, request build, and compaction check; the provider stream; and tool settlement. -_Avoid_: provider turn, turn (unqualified) - -**Physical Attempt**: -One actual provider request on the wire in service of a **Step**; most Steps have one Physical Attempt, while overflow-triggered compaction recovery may give one Step two. - -**Assistant Turn**: -A reserved name for the not-yet-modeled unit containing all **Steps** from prompt promotion until the assistant yields the floor; do not reify it until something durable needs it. - -**Settlement**: -The terminal transition for a unit of work: Step and tool settlement are durable, while drain and execution settlement are coordinator-observed. - -**Execution**: -One session-scoped coordinator busy period from first wake until idle. An Execution is process-local coordination rather than a durable domain entity. +**Provider Turn**: +One request to a model provider and the response projected from that request. **Session Drain**: -One process-local execution span that promotes eligible input and runs required **Steps** until no immediate continuation remains. A Session Drain has no durable identity or transcript boundary. +One process-local execution span that promotes eligible input and runs required **Provider Turns** until no immediate continuation remains. A Session Drain has no durable identity or transcript boundary. **Model Tool Output**: The bounded projection of a Core-executed tool result persisted in Session history and replayed to the model. A tool may shape this projection semantically, but the Tool Registry enforces the final size limit. @@ -110,49 +87,52 @@ _Avoid_: Response envelope ## Relationships -- **Instructions** is an opaque carrier composed from zero or more **Instruction Sources**. -- **Model Context** is broader than **Instructions**. For each **Step**, the runner assembles the selected agent or provider system text, **Initial Instructions**, **Session History**, available tools, and step-local additions into one model request. -- **Session History** persists conversational messages. The runner derives model-facing **Instruction Update** messages from value deltas and interleaves them by durable sequence; **Initial Instructions** remain separate provider-request state. -- The runner explicitly loads and combines instruction built-ins, **InstructionDiscovery**, selected-agent skill guidance, reference guidance, MCP guidance, and **InstructionEntry** values. There is no instruction registry. -- `Instructions.combine(...)` preserves caller order and rejects duplicate stable namespaced source keys. The runner loads its producers concurrently, then combines them in its fixed declared order. -- Each **Instruction Source** read returns one coherent typed value, explicit removal, or temporary unavailability. `Instructions.make(...)` hides the value type so differently typed sources compose uniformly; its canonical codec defines storage and hash equivalence, while pure renderers produce first, changed, and optional removal text. -- `Instructions.read(...)` reads every composed source concurrently and exactly once at the boundary. `Instructions.diff(...)` compares encoded-value hashes with current **Instruction Values** and returns one delta plus new blob bodies. -- `Instructions.renderInitial(...)` renders values at the **Instruction Epoch** start. `Instructions.renderUpdate(...)` renders one hydrated delta against the values immediately before it. -- A changed **Instruction Source** contributes its hash to one **Instruction Update**; explicit removal contributes the `"removed"` sentinel. -- An **Instruction Update** persists only its value delta. Rendered text is derived during request assembly and excluded from compaction summaries. -- The instruction blob insert, durable delta, and **Instruction State** advance commit atomically. -- Changes from multiple **Instruction Sources** admitted at one safe boundary combine into one **Instruction Update**. -- Instruction changes are sampled and admitted lazily at a **Safe Step Boundary**, never pushed asynchronously when their source changes. -- At a **Safe Step Boundary**, prior tool results are already settled; instruction preparation completes before newly admitted user input promotes. +- A **System Context** is an opaque carrier composed from zero or more **Context Sources**. +- **Session History** contains projected conversational messages and admitted **Mid-Conversation System Messages**; the active **Baseline System Context** remains separate provider-request state. +- The **System Context Registry** uses stable-keyed scoped contributions to assemble the current **System Context**; contributor removal naturally removes its sources at the next **Safe Provider-Turn Boundary**. +- A changed **Context Source** may produce one **Mid-Conversation System Message** containing its newly effective state. +- A **Mid-Conversation System Message** persists the exact combined rendered text sent to the model. +- The current **Context Snapshot** advances atomically with the corresponding durable **Mid-Conversation System Message**. +- A **Context Snapshot** stores one codec-encoded JSON value and, for removable dynamic sources, a pre-rendered removal message per stable **Context Source** key. +- Changes from multiple **Context Sources** admitted at one safe boundary combine into one **Mid-Conversation System Message**. +- Context changes are sampled and admitted lazily at a **Safe Provider-Turn Boundary**, never pushed asynchronously when their source changes. +- At a **Safe Provider-Turn Boundary**, newly promoted user input or settled tool results precede any combined **Mid-Conversation System Message**. - An **Admitted Prompt** is replayable pending input, not yet model-visible **Session History**. - **Prompt Promotion** atomically consumes the pending inbox entry and appends its model-visible user message. -- Steering prompts promote at the next **Safe Step Boundary** while the current **Session Drain** still requires continuation. Promoting any newly admitted user input resets the selected agent's step allowance; multiple prompts promoted at one boundary reset it once. +- Steering prompts promote at the next **Safe Provider-Turn Boundary** while the current **Session Drain** still requires continuation. Promoting any newly admitted user input resets the selected agent's provider-turn allowance; multiple prompts promoted at one boundary reset it once. - A queued prompt does not promote while the current **Session Drain** requires continuation. The runner promotes one queued prompt when the Session would otherwise become idle, then reevaluates continuation before promoting another. -- A **Session Drain** is process-local coordination rather than a durable domain entity. Durable recovery must reason from prompts, projected history, physical attempts, and tool state rather than inventing an enclosing execution identity. -- An **Execution** contains one or more **Session Drains**; a **Session Drain** contains one reserved assistant-turn span at a time; that span contains **Steps**; and each **Step** contains one or more **Physical Attempts** plus any tool calls it requires. -- A **Step** record covers only the model-visible span from first assistant output through tool settlement; pre-flight leaves no record, and one Step settles at most one record. -- The first **Step** admits one complete delta and renders **Initial Instructions** without narrating that delta in history; an unavailable initial source blocks the Step instead of persisting incomplete values. -- Instruction preparation precedes durable input promotion on every Step so an unavailable first baseline leaves pending input untouched and later updates enter history before newly promoted input. -- Completed compaction moves the **Instruction Epoch** to the exact `session.compaction.ended` sequence and copies current hashes to the epoch's initial values. Earlier updates leave active model history while durable deltas remain. -- A newly composed **Instruction Source** absent from current **Instruction Values** emits its first rendering once at the next **Safe Step Boundary**. -- **Unavailable Instruction Source** uses stale-while-revalidate semantics and is distinct from a successfully loaded absence, which may emit removal text. -- **InstructionDiscovery** observes ambient instructions as one ordered aggregate **Instruction Source**. -- Ambient discovery reads global and upward-project `AGENTS.md` files and honors `OPENCODE_DISABLE_PROJECT_CONFIG` for project files. -- After a successful internal file or directory read, nearby `AGENTS.md` files toward the Location root are injected once per Session as durable synthetic instruction messages. -- **InstructionEntry** stores API-managed per-Session JSON values. Each entry contributes one `api/` **Instruction Source**, so adding, replacing, or removing an entry is reconciled at the next **Safe Step Boundary**. -- Location-scoped instruction producers naturally re-resolve when a moved Session next runs in its destination Location. -- Moving a Session clears its **Instruction State**, so the destination must admit a complete delta before another prompt can promote. Committed revert does the same; replay derives both resets from their durable events. -- Selected-agent available-skill guidance is an **Instruction Source** composed explicitly by the runner. It lists only names and descriptions permitted for that agent; skill bodies and locations are exposed only through the permission-checked `skill` tool. -- The selected agent and model are sampled when a **Step** starts. Changes admitted after that boundary apply to the next Step and do not restart the current Step. -- An agent switch that changes selected-agent guidance produces an **Instruction Update** while preserving the current baseline. -- Local tool authorization and pending permission requests retain the effective agent of the **Step** that issued the call; a later agent switch cannot change that call's policy. -- Instruction source changes never wake idle Sessions; the next naturally scheduled **Safe Step Boundary** loads and compares current values lazily. -- Once admitted, an **Instruction Update** remains durable even if the following **Physical Attempt** fails and is replayed unchanged on retry. -- **Instruction Updates** remain durable value history but are not `session_message` rows. Clients display changed keys rather than model-facing prose. -- The date **Instruction Source** initially preserves host-local calendar-date behavior; a configured user timezone may replace that default later. -- **Initial Instructions** are recomputed deterministically from durable values for every request; rendered bytes are not stored. -- A model/provider switch preserves current **Instruction Values**, the **Instruction Epoch**, and chronological conversation history; the new selection applies to the next **Step**. -- **Native Continuation Metadata** remains in durable history. Step projection includes it only for a successful exact originating provider/model match; failed Steps and incompatible models omit opaque metadata, while non-empty visible reasoning lowers to ordinary assistant text after a model switch. This conservative relation may widen only when recorded provider tests establish compatibility. +- A **Session Drain** is process-local coordination rather than a durable domain entity. Durable recovery must reason from prompts, projected history, provider attempts, and tool state rather than inventing an enclosing execution identity. +- The first provider turn renders the latest complete **Baseline System Context** and initializes its **Context Snapshot** without emitting a redundant **Mid-Conversation System Message**; unavailable initial context blocks the turn instead of persisting an incomplete baseline. +- Initial **System Context** preparation precedes the first durable input promotion so an unavailable baseline leaves that input pending and retryable; ordinary reconciliation remains after promotion. +- Compaction starts a new **Context Epoch** with a freshly rendered **Baseline System Context** and **Context Snapshot**; prior **Mid-Conversation System Messages** remain durable audit history but leave projected model history. +- A newly registered core or plugin-defined **Context Source** absent from the current snapshot emits its baseline rendering once at the next **Safe Provider-Turn Boundary**. +- **Context Source** keys are stable and namespaced; duplicate keys fail composition. `SystemContext.combine(...)` preserves caller order; the **System Context Registry** evaluates producers concurrently and combines them in stable contribution-key order so rendered context remains deterministic. +- Each **Context Source** loader returns one coherent typed value. `SystemContext.make(...)` hides that value type so differently typed sources compose uniformly. Its codec compares and stores that value; its pure renderers produce model-visible baseline, update, and removal text only when needed. +- `SystemContext.initialize(...)` observes a composed **System Context** once and produces a fresh **Baseline System Context** with its **Context Snapshot**. +- `SystemContext.reconcile(...)` observes a composed **System Context** once and returns exactly one next action: unchanged, updated, replacement ready, or replacement blocked. +- `SystemContext.replace(...)` renders a fresh generation after completed compaction or another baseline-replacing transition; it reports replacement blocked while previously admitted context is unavailable. +- **Unavailable Context** uses stale-while-revalidate semantics and is distinct from a successfully loaded absence, which may emit removal text. +- Ordinary **Context Source** loaders return values directly; loaders that intentionally use stale-while-revalidate may explicitly return **Unavailable Context**. +- Nested project instruction discovery after successful reads remains a follow-up; when implemented, discovered instructions must be admitted durably at the next **Safe Provider-Turn Boundary**. +- Location-scoped services naturally re-resolve effective context when a moved session next runs in its destination location. +- Moving a Session clears its active **Context Epoch**, so the destination must initialize a complete baseline before another prompt can promote. +- Instruction discovery, source identity, persistence, and file loading belong to the instruction service; the **System Context** abstraction only composes effectful producers and renders loaded values. +- The first instruction-service slice observes global and upward project `AGENTS.md` files as one ordered aggregate **Context Source** at each **Safe Provider-Turn Boundary**. +- Built-in and instruction context producers register through the **System Context Registry** with stable contribution keys. Plugin-defined context registration and hot-reload lifecycle remain a follow-up built on the same scoped registry seam. +- Selected-agent available-skill guidance is a **Context Source** composed with Location-wide registry sources immediately before Context Epoch admission. It lists only names and descriptions permitted for that agent; skill bodies and locations are exposed only through the permission-checked `skill` tool. +- The selected agent and model are sampled when a provider turn starts. Changes admitted after that boundary apply to the next provider turn and do not restart the current turn. +- Selected-agent available-skill guidance remains a **Context Source**. An agent switch that changes that guidance produces a **Mid-Conversation System Message** while preserving the current baseline. +- Local tool authorization and pending permission requests retain the effective agent of the provider turn that issued the call; a later agent switch cannot change that call's policy. +- Context source changes never wake idle sessions; the next naturally scheduled **Safe Provider-Turn Boundary** loads and compares current values lazily. +- Once admitted, a **Mid-Conversation System Message** remains durable even if the following provider attempt fails and is replayed unchanged on retry. +- **Mid-Conversation System Messages** remain durable Session-message history; normal user-facing transcript surfaces may hide them. +- The date **Context Source** initially preserves host-local calendar-date behavior; a configured user timezone may replace that default later. +- A **Context Epoch** begins with one immutable **Baseline System Context**. +- A **Baseline System Context** is stored durably and reused verbatim across process restarts within its **Context Epoch**. +- A **Baseline System Context** durably preserves the exact joined text used for the active provider-cache prefix. +- Completed compaction starts a new **Context Epoch** on the next provider attempt, folding the current complete **System Context** into a fresh baseline and removing earlier **Mid-Conversation System Messages** from active model history. +- A model/provider switch preserves the current **Context Epoch** and chronological conversation history; the new selection applies to the next provider turn. +- **Native Continuation Metadata** remains in durable history. Provider-turn projection includes it only for a successful exact originating provider/model match; failed turns and incompatible models omit opaque metadata, while non-empty visible reasoning lowers to ordinary assistant text after a model switch. This conservative relation may widen only when recorded provider tests establish compatibility. - **Model Request Options** remain provider-semantic through Catalog resolution. The Session runner maps them into the LLM package's provider-option namespace; the selected protocol adapter alone owns provider wire encoding. - **Generation Controls**, protocol-semantic **Model Request Options**, and compatibility request body fields are separate Catalog domains. A shared ingestion adapter partitions legacy and models.dev AI-SDK-shaped options before routing. - The **PTY Environment** is a server concern rather than a Core PTY concern. PTY creation merges caller values, then the host overlay, then Core-forced terminal invariants such as `TERM` and `OPENCODE_TERMINAL`. @@ -182,12 +162,12 @@ _Avoid_: Response envelope - SDK executes Server's assembled `HttpRouter` in memory. It opens no listener and performs no network I/O, while preserving Server routing, middleware, codecs, handlers, and errors. - The Effect Client and SDK re-export their decoded datatype facade from Schema so callers do not depend on internal package locations or Core's versioned names. - A capability intended for both networked and **Embedded OpenCode** belongs in the authoritative public `HttpApi`; embedded-only same-process capabilities extend **Embedded OpenCode** separately. -- `sessions.log({ sessionID, after, follow })` is the public durable Session event stream. It verifies the Session, replays durable events after the optional aggregate sequence, optionally continues with newly committed durable events, excludes live-only fragments, and is transported as SSE in both networked and embedded modes. +- `sessions.events({ sessionID, after })` is a public durable Session event stream. It verifies the Session, replays durable events after the optional aggregate sequence, continues with newly committed durable events, excludes live-only fragments, and is transported as SSE in both networked and embedded modes. - `events.subscribe()` is a distinct public instance-wide live stream for Session and non-Session activity. It has no replay guarantee and includes connection, heartbeat, and instance-disposal lifecycle events; consumers recover from disconnection by refreshing authoritative state. - A Session ID is not an optional filter on `events.subscribe()`: instance-wide live events and durable Session events have different schemas, replay guarantees, cursors, lifecycle events, and failure behavior. - The initial common OpenCode Client does not expose server-global event aggregation. `events.subscribe()` is bounded to the connected OpenCode instance or workspace; any future cross-instance administrative stream requires a separately designed API. - `events.subscribe()` does not automatically reconnect after transport loss. The live-only stream fails with `ClientError`; consumers refresh authoritative state before explicitly opening a new subscription because events missed during disconnection cannot be replayed. -- `sessions.log({ sessionID, after, follow })` returns the generated HTTP client's cold durable event stream and does not build reconnection policy into the endpoint or client constructor. Transport loss fails the stream with `ClientError`. Callers may compose an explicit resuming stream above it by retaining the last observed durable sequence and opening a new subscription with `after`; any reusable resume helper remains a separate API design question. +- `sessions.events({ sessionID, after })` returns the generated HTTP client's cold durable event stream and does not build reconnection policy into the endpoint or client constructor. Transport loss fails the stream with `ClientError`. Callers may compose an explicit resuming stream above it by retaining the last observed durable sequence and opening a new subscription with `after`; any reusable resume helper remains a separate API design question. - The stable `sessions.list(...)` design returns a **Page** in both networked and **Embedded OpenCode**; embedded execution does not define a separate unbounded array-returning list operation. The beta client currently preserves the existing HTTP `{ data, cursor }` envelope until emitter-level Page projection is implemented. - Session list cursors are opaque branded values carrying continuation query and ordering state. Consumers pass them back unchanged and do not inspect storage anchors or encoded filter fields. - A Session list continuation accepts only its opaque cursor. Scope, filters, ordering, and page size are fixed by the initial query and carried by that cursor. @@ -195,27 +175,27 @@ _Avoid_: Response envelope - `sessions.message({ sessionID, messageID })` is a required resource lookup. An unknown Session fails with `SessionNotFoundError`; a known Session with an absent or differently owned message fails with `MessageNotFoundError` without disclosing cross-Session ownership. Absence is not represented as `undefined` across the public HTTP boundary. - `sessions.interrupt({ sessionID })` first verifies that the durable Session exists, failing with `SessionNotFoundError` otherwise. For a known Session, interruption is idempotent: idle, already-settled, or locally unowned execution is a no-op. - `sessions.active()` snapshots the current process's foreground Session drain registry as a record of Session IDs to `{ type: "running" }`. Missing IDs are inactive; background subagents and tasks do not make their parent Session active, and process restart clears the registry. -- `sessions.context({ sessionID })` preserves the existing message-only operation. It returns projected **Session History**; it does not include or represent the complete **Model Context**, whose agent system text, **Initial Instructions**, tools, and step-local additions remain separate. -- **Open question**: Should a future, separately named operation expose complete **Model Context**, including the instruction baseline, applied instruction metadata, tools, and step-local additions? +- `sessions.context({ sessionID })` preserves the existing message-only operation. It returns projected conversational messages selected as Session context; it does not include or represent the complete provider request context, whose baseline system context and other contributions remain separate. +- **Open question**: Should a future, separately named operation expose the complete provider request context, including baseline system context, selected source contributions, and context-epoch metadata? - `sessions.prompt(...)` exposes `resume?: boolean`. Omitting it preserves durable admission followed by an advisory execution wake; `resume: false` requests durable admit-only behavior. -- The public operation remains `sessions.prompt(...)`; `SessionPending.admit` is the internal primitive, while the public `Admission` result and `resume` option express its durable admission semantics. +- The public operation remains `sessions.prompt(...)`; `SessionInput.admit` is the internal primitive, while the public `Admission` result and `resume` option express its durable admission semantics. - `sessions.create(...)` accepts an optional `location`. Omission resolves through the connected OpenCode instance's default or current location; an explicit value selects a known location. Networked and embedded transports use the same handler semantics. - `sessions.switchAgent({ sessionID, agent })` is part of the common client alongside `sessions.switchModel(...)`. It affects subsequent Session activity and fails with `SessionNotFoundError` for an unknown Session. - The **Embedded OpenCode** Layer delegates to the same scoped creation path; it does not define a second implementation. - A **PTY Environment** adapter observes plugins in the request Location while passing the resolved PTY working directory to the hook; standalone servers use an empty adapter. -- An **Instruction Update** lowers to the provider's native chronological instruction role when supported and to a wrapped chronological fallback otherwise. -- When the aggregate discovered instruction set changes, its **Instruction Update** includes the complete current ordered set and supersedes the prior aggregate value; when no discovered instructions remain, the message states that previously loaded instructions no longer apply. +- A **Mid-Conversation System Message** lowers to the provider's native chronological instruction role when supported and to a wrapped chronological fallback otherwise. +- When the effective aggregate instruction set changes, its **Mid-Conversation System Message** includes the complete current ordered set and supersedes the prior aggregate value; when no ambient instructions remain, the message states that previously loaded instructions no longer apply. - Ambient project instruction discovery honors `OPENCODE_DISABLE_PROJECT_CONFIG`; global instructions remain eligible. - Oversized textual **Model Tool Output** retains a bounded preview in Session history while its complete text moves to managed tool-output storage. Arbitrary structured-result size is a separate concern. - One tool settlement receives one aggregate textual limit, using the configured maximum lines or UTF-8 bytes, whichever is reached first. The limit is provider-independent; token pressure belongs to context assembly and compaction. - Generic truncation preserves the beginning and end of textual output. Tools may apply a more meaningful strategy before the Tool Registry enforces the final limit. -- A truncated **Model Tool Output** identifies its complete text in the bounded model-visible preview. The Tool Registry also supplies managed paths as internal metadata to tool hooks; Session events do not expose a typed `outputPaths` field. +- A truncated **Model Tool Output** identifies its complete text both in the bounded model-visible preview and as a typed managed output path. Managed output paths do not modify the tool's validated structured result. - A **Managed Tool Output File** is temporary and may expire after its retention period. The bounded **Model Tool Output**, not the file, is the durable replayable record. -- Failure to retain a **Managed Tool Output File** fails settlement operationally. The Session never publishes a successful result whose complete output was lost during generic bounding. +- Failure to retain a **Managed Tool Output File** does not change a successful tool operation into a failed one. The Session records an explicitly lossy bounded output without a path, while operators receive diagnostics for the storage failure. - Once a tool operation succeeds, bounding its **Model Tool Output** and publishing its one durable settlement form an interruption-safe completion region. Raw oversized success is never published before a later correction. - When a structured-only result would exceed the **Model Tool Output** limit, its validated structured value remains unchanged for Session consumers while model replay uses a bounded textual JSON preview and optional managed output path. - Existing tool-managed output paths survive generic bounding. A fallback file retains exactly the complete projected text received by the Tool Registry and never claims to reconstruct output already discarded by tool-specific shaping. -- **Managed Tool Output Files** use globally unique names in one shared flat directory. They receive no special filesystem authority; each tool applies its ordinary external-path policy. +- **Managed Tool Output Files** use globally unique names in one shared flat directory. Their absolute paths are readable and searchable by ordinary tools; other absolute paths remain outside Location-scoped filesystem authority. - Provider-executed tool results remain provider-native transcript facts outside generic Tool Registry bounding. Their context control requires provider-aware pruning or compaction because some providers require exact structured round-trip payloads. ## Client contract architecture @@ -237,9 +217,9 @@ Before stabilizing the client API: ## Example dialogue -> **Dev:** "The date changed while the session was active. Should the **Instruction Update** say what the old date was?" -> **Domain expert:** "No. Emit the newly effective date so the agent can act on the current instructions." +> **Dev:** "The date changed while the session was active. Should the **Mid-Conversation System Message** say what the old date was?" +> **Domain expert:** "No. Emit the newly effective date so the agent can act on the current **System Context**." ## Flagged ambiguities -- Legacy `experimental.chat.system.transform` can mutate assembled system text arbitrarily, but V2 plugins do not yet expose an equivalent hook. Decide separately whether to port it, model dynamic uses as explicit **Instruction Sources**, or narrow its semantics. +- Legacy `experimental.chat.system.transform` can mutate the assembled baseline system prompt arbitrarily, but V2 plugins do not yet expose an equivalent hook. Decide separately whether to port it, replace dynamic uses with plugin-defined **Context Sources**, or narrow its semantics. diff --git a/bun.lock b/bun.lock index 4eb9d477ed..e905ada8c5 100644 --- a/bun.lock +++ b/bun.lock @@ -14,7 +14,6 @@ }, "devDependencies": { "@actions/artifact": "5.0.1", - "@ast-grep/cli": "0.44.0", "@tsconfig/bun": "catalog:", "@types/mime-types": "3.0.1", "@typescript/native-preview": "catalog:", @@ -30,7 +29,7 @@ }, "packages/app": { "name": "@opencode-ai/app", - "version": "1.17.18", + "version": "1.18.11", "dependencies": { "@corvu/drawer": "catalog:", "@dnd-kit/abstract": "0.5.0", @@ -38,6 +37,7 @@ "@dnd-kit/helpers": "0.5.0", "@dnd-kit/solid": "0.5.0", "@kobalte/core": "catalog:", + "@opencode-ai/client": "file:vendor/opencode-ai-client-1.17.13-v2.tgz", "@opencode-ai/core": "workspace:*", "@opencode-ai/schema": "workspace:*", "@opencode-ai/sdk": "workspace:*", @@ -66,7 +66,7 @@ "diff": "catalog:", "effect": "catalog:", "fuzzysort": "catalog:", - "ghostty-web": "github:anomalyco/ghostty-web#513463a6f1190253057e8a3f0dac8f6ee8393553", + "ghostty-web": "github:anomalyco/ghostty-web#83c0a07b8628b748aed073b232cb4b52a6ca11c1", "luxon": "catalog:", "marked": "catalog:", "marked-shiki": "catalog:", @@ -96,52 +96,40 @@ }, "packages/cli": { "name": "@opencode-ai/cli", - "version": "1.17.18", + "version": "1.18.11", "bin": { - "opencode2": "./bin/opencode2.cjs", + "lildax": "./bin/lildax.cjs", }, "dependencies": { "@effect/platform-node": "catalog:", - "@opencode-ai/client": "workspace:*", "@opencode-ai/core": "workspace:*", - "@opencode-ai/plugin": "workspace:*", - "@opencode-ai/schema": "workspace:*", "@opencode-ai/sdk": "workspace:*", "@opencode-ai/server": "workspace:*", "@opencode-ai/tui": "workspace:*", "@opentui/core": "catalog:", - "@opentui/keymap": "catalog:", "@opentui/solid": "catalog:", "@parcel/watcher": "2.5.1", "effect": "catalog:", - "fuzzysort": "catalog:", - "immer": "11.1.4", - "jsonc-parser": "3.3.1", - "open": "10.1.2", - "opentui-spinner": "catalog:", - "semver": "catalog:", "solid-js": "catalog:", - "strip-ansi": "7.1.2", - "uqr": "0.1.3", }, "devDependencies": { "@opencode-ai/script": "workspace:*", "@tsconfig/bun": "catalog:", "@types/bun": "catalog:", - "@types/semver": "catalog:", "@typescript/native-preview": "catalog:", }, }, "packages/client": { "name": "@opencode-ai/client", - "version": "1.17.13", "dependencies": { "@opencode-ai/protocol": "workspace:*", "@opencode-ai/schema": "workspace:*", }, "devDependencies": { "@effect/platform-node": "catalog:", + "@opencode-ai/core": "workspace:*", "@opencode-ai/httpapi-codegen": "workspace:*", + "@opencode-ai/server": "workspace:*", "@tsconfig/bun": "catalog:", "@types/bun": "catalog:", "@typescript/native-preview": "catalog:", @@ -156,7 +144,7 @@ }, "packages/codemode": { "name": "@opencode-ai/codemode", - "version": "1.17.18", + "version": "1.18.11", "dependencies": { "acorn": "8.15.0", "effect": "catalog:", @@ -170,7 +158,7 @@ }, "packages/console/app": { "name": "@opencode-ai/console-app", - "version": "1.17.18", + "version": "1.18.11", "dependencies": { "@cloudflare/vite-plugin": "1.15.2", "@ibm/plex": "6.4.1", @@ -206,7 +194,7 @@ }, "packages/console/core": { "name": "@opencode-ai/console-core", - "version": "1.17.18", + "version": "1.18.11", "dependencies": { "@aws-sdk/client-sts": "3.782.0", "@jsx-email/render": "1.1.1", @@ -233,7 +221,7 @@ }, "packages/console/function": { "name": "@opencode-ai/console-function", - "version": "1.17.18", + "version": "1.18.11", "dependencies": { "@ai-sdk/anthropic": "3.0.82", "@ai-sdk/openai": "3.0.48", @@ -255,7 +243,7 @@ }, "packages/console/mail": { "name": "@opencode-ai/console-mail", - "version": "1.17.18", + "version": "1.18.11", "dependencies": { "@jsx-email/all": "2.2.3", "@jsx-email/cli": "1.4.3", @@ -279,7 +267,7 @@ }, "packages/console/support": { "name": "@opencode-ai/console-support", - "version": "1.17.18", + "version": "1.18.11", "dependencies": { "@cloudflare/vite-plugin": "1.15.2", "@opencode-ai/console-core": "workspace:*", @@ -299,7 +287,7 @@ }, "packages/core": { "name": "@opencode-ai/core", - "version": "1.17.18", + "version": "1.18.11", "bin": { "opencode": "./bin/opencode", }, @@ -307,7 +295,7 @@ "@ai-sdk/alibaba": "1.0.17", "@ai-sdk/amazon-bedrock": "4.0.112", "@ai-sdk/anthropic": "3.0.82", - "@ai-sdk/azure": "3.0.49", + "@ai-sdk/azure": "3.0.88", "@ai-sdk/cerebras": "2.0.41", "@ai-sdk/cohere": "3.0.27", "@ai-sdk/deepinfra": "2.0.41", @@ -315,8 +303,8 @@ "@ai-sdk/google": "3.0.73", "@ai-sdk/google-vertex": "4.0.128", "@ai-sdk/groq": "3.0.31", - "@ai-sdk/mistral": "3.0.27", - "@ai-sdk/openai": "3.0.53", + "@ai-sdk/mistral": "3.0.51", + "@ai-sdk/openai": "3.0.84", "@ai-sdk/openai-compatible": "2.0.41", "@ai-sdk/perplexity": "3.0.26", "@ai-sdk/provider": "3.0.8", @@ -330,10 +318,8 @@ "@effect/sql-sqlite-bun": "catalog:", "@ff-labs/fff-bun": "0.9.4", "@lydell/node-pty": "catalog:", - "@modelcontextprotocol/sdk": "1.29.0", "@npmcli/arborist": "9.4.0", "@npmcli/config": "10.8.1", - "@opencode-ai/codemode": "workspace:*", "@opencode-ai/effect-drizzle-sqlite": "workspace:*", "@opencode-ai/effect-sqlite-node": "workspace:*", "@opencode-ai/llm": "workspace:*", @@ -353,7 +339,7 @@ "drizzle-orm": "catalog:", "effect": "catalog:", "fuzzysort": "3.1.0", - "gitlab-ai-provider": "6.10.0", + "gitlab-ai-provider": "6.12.1", "glob": "13.0.5", "google-auth-library": "10.5.0", "gray-matter": "4.0.3", @@ -395,16 +381,16 @@ }, "packages/desktop": { "name": "@opencode-ai/desktop", - "version": "1.17.18", + "version": "1.18.11", "dependencies": { "@zip.js/zip.js": "2.7.62", + "drizzle-orm": "catalog:", "effect": "catalog:", "electron-context-menu": "4.1.2", "electron-log": "^5", "electron-store": "11.0.2", "electron-updater": "6.8.9", "electron-window-state": "^5.0.3", - "marked": "^15", }, "devDependencies": { "@actions/artifact": "4.0.0", @@ -447,15 +433,9 @@ "@parcel/watcher-win32-x64": "2.5.1", }, }, - "packages/docs": { - "name": "@opencode-ai/docs", - "devDependencies": { - "mint": "4.2.666", - }, - }, "packages/effect-drizzle-sqlite": { "name": "@opencode-ai/effect-drizzle-sqlite", - "version": "1.17.18", + "version": "1.18.11", "dependencies": { "drizzle-orm": "catalog:", "effect": "catalog:", @@ -469,7 +449,7 @@ }, "packages/effect-sqlite-node": { "name": "@opencode-ai/effect-sqlite-node", - "version": "1.17.18", + "version": "1.18.11", "dependencies": { "effect": "catalog:", }, @@ -481,7 +461,7 @@ }, "packages/enterprise": { "name": "@opencode-ai/enterprise", - "version": "1.17.18", + "version": "1.18.11", "dependencies": { "@hono/standard-validator": "catalog:", "@opencode-ai/core": "workspace:*", @@ -513,7 +493,7 @@ }, "packages/function": { "name": "@opencode-ai/function", - "version": "1.17.18", + "version": "1.18.11", "dependencies": { "@octokit/auth-app": "8.0.1", "@octokit/rest": "catalog:", @@ -529,12 +509,12 @@ }, "packages/http-recorder": { "name": "@opencode-ai/http-recorder", - "version": "1.17.18", + "version": "1.18.11", "dependencies": { + "@effect/platform-node": "4.0.0-beta.83", "@effect/platform-node-shared": "4.0.0-beta.83", }, "devDependencies": { - "@effect/platform-node": "catalog:", "@tsconfig/node22": "catalog:", "@types/bun": "catalog:", "@types/node": "catalog:", @@ -543,12 +523,11 @@ "typescript": "catalog:", }, "peerDependencies": { - "effect": "catalog:", + "effect": "4.0.0-beta.83", }, }, "packages/httpapi-codegen": { "name": "@opencode-ai/httpapi-codegen", - "version": "0.0.0", "dependencies": { "effect": "catalog:", "prettier": "3.6.2", @@ -561,7 +540,7 @@ }, "packages/llm": { "name": "@opencode-ai/llm", - "version": "1.17.18", + "version": "1.18.11", "dependencies": { "@opencode-ai/schema": "workspace:*", "@smithy/eventstream-codec": "4.2.14", @@ -580,7 +559,7 @@ }, "packages/opencode": { "name": "opencode", - "version": "1.17.18", + "version": "1.18.11", "bin": { "opencode": "./bin/opencode", }, @@ -591,7 +570,7 @@ "@ai-sdk/alibaba": "1.0.17", "@ai-sdk/amazon-bedrock": "4.0.112", "@ai-sdk/anthropic": "3.0.82", - "@ai-sdk/azure": "3.0.49", + "@ai-sdk/azure": "3.0.88", "@ai-sdk/cerebras": "2.0.60", "@ai-sdk/cohere": "3.0.27", "@ai-sdk/deepinfra": "2.0.41", @@ -599,8 +578,8 @@ "@ai-sdk/google": "3.0.73", "@ai-sdk/google-vertex": "4.0.128", "@ai-sdk/groq": "3.0.31", - "@ai-sdk/mistral": "3.0.27", - "@ai-sdk/openai": "3.0.53", + "@ai-sdk/mistral": "3.0.51", + "@ai-sdk/openai": "3.0.84", "@ai-sdk/openai-compatible": "2.0.41", "@ai-sdk/perplexity": "3.0.26", "@ai-sdk/provider": "3.0.8", @@ -617,8 +596,6 @@ "@octokit/graphql": "9.0.2", "@octokit/rest": "catalog:", "@openauthjs/openauth": "catalog:", - "@opencode-ai/cli": "workspace:*", - "@opencode-ai/client": "workspace:*", "@opencode-ai/codemode": "workspace:*", "@opencode-ai/llm": "workspace:*", "@opencode-ai/plugin": "workspace:*", @@ -655,7 +632,7 @@ "drizzle-orm": "catalog:", "effect": "catalog:", "fuzzysort": "3.1.0", - "gitlab-ai-provider": "6.10.0", + "gitlab-ai-provider": "6.12.1", "glob": "13.0.5", "google-auth-library": "10.5.0", "gray-matter": "4.0.3", @@ -713,11 +690,9 @@ }, "packages/plugin": { "name": "@opencode-ai/plugin", - "version": "1.17.18", + "version": "1.18.11", "dependencies": { "@ai-sdk/provider": "3.0.8", - "@opencode-ai/client": "workspace:*", - "@opencode-ai/schema": "workspace:*", "@opencode-ai/sdk": "workspace:*", "effect": "catalog:", "zod": "catalog:", @@ -726,16 +701,15 @@ "@opentui/core": "catalog:", "@opentui/keymap": "catalog:", "@opentui/solid": "catalog:", - "@tsconfig/bun": "catalog:", "@tsconfig/node22": "catalog:", "@types/node": "catalog:", "@typescript/native-preview": "catalog:", "typescript": "catalog:", }, "peerDependencies": { - "@opentui/core": ">=0.4.3", - "@opentui/keymap": ">=0.4.3", - "@opentui/solid": ">=0.4.3", + "@opentui/core": ">=0.4.5", + "@opentui/keymap": ">=0.4.5", + "@opentui/solid": ">=0.4.5", }, "optionalPeers": [ "@opentui/core", @@ -745,7 +719,6 @@ }, "packages/protocol": { "name": "@opencode-ai/protocol", - "version": "1.17.11", "dependencies": { "@opencode-ai/schema": "workspace:*", "effect": "catalog:", @@ -754,12 +727,10 @@ "@tsconfig/bun": "catalog:", "@types/bun": "catalog:", "@typescript/native-preview": "catalog:", - "typescript": "catalog:", }, }, "packages/schema": { "name": "@opencode-ai/schema", - "version": "1.17.11", "dependencies": { "effect": "catalog:", }, @@ -767,7 +738,6 @@ "@tsconfig/bun": "catalog:", "@types/bun": "catalog:", "@typescript/native-preview": "catalog:", - "typescript": "catalog:", }, }, "packages/script": { @@ -785,14 +755,10 @@ "dependencies": { "@opencode-ai/client": "workspace:*", "@opencode-ai/core": "workspace:*", - "@opencode-ai/plugin": "workspace:*", - "@opencode-ai/schema": "workspace:*", "@opencode-ai/server": "workspace:*", "effect": "catalog:", }, "devDependencies": { - "@opencode-ai/httpapi-codegen": "workspace:*", - "@opencode-ai/protocol": "workspace:*", "@tsconfig/bun": "catalog:", "@types/bun": "catalog:", "@typescript/native-preview": "catalog:", @@ -800,7 +766,7 @@ }, "packages/sdk/js": { "name": "@opencode-ai/sdk", - "version": "1.17.18", + "version": "1.18.11", "dependencies": { "cross-spawn": "catalog:", }, @@ -815,12 +781,10 @@ }, "packages/server": { "name": "@opencode-ai/server", - "version": "1.17.18", + "version": "1.18.11", "dependencies": { - "@effect/platform-node": "catalog:", "@opencode-ai/core": "workspace:*", "@opencode-ai/protocol": "workspace:*", - "@opencode-ai/simulation": "workspace:*", "drizzle-orm": "catalog:", "effect": "catalog:", }, @@ -832,9 +796,10 @@ }, "packages/session-ui": { "name": "@opencode-ai/session-ui", - "version": "1.17.18", + "version": "1.18.11", "dependencies": { "@kobalte/core": "catalog:", + "@opencode-ai/client": "file:../app/vendor/opencode-ai-client-1.17.13-v2.tgz", "@opencode-ai/core": "workspace:*", "@opencode-ai/sdk": "workspace:*", "@opencode-ai/ui": "workspace:*", @@ -846,7 +811,6 @@ "@solid-primitives/media": "2.3.3", "@solid-primitives/resize-observer": "2.1.3", "@solidjs/meta": "catalog:", - "@solidjs/router": "catalog:", "diff": "catalog:", "dompurify": "3.3.1", "fuzzysort": "catalog:", @@ -874,26 +838,9 @@ "vite": "catalog:", }, }, - "packages/simulation": { - "name": "@opencode-ai/simulation", - "version": "1.17.13", - "dependencies": { - "@fontsource/adwaita-mono": "5.2.1", - "@napi-rs/canvas": "1.0.2", - "@opencode-ai/core": "workspace:*", - "@opencode-ai/llm": "workspace:*", - "@opentui/core": "catalog:", - "effect": "catalog:", - }, - "devDependencies": { - "@tsconfig/bun": "catalog:", - "@types/bun": "catalog:", - "@typescript/native-preview": "catalog:", - }, - }, "packages/slack": { "name": "@opencode-ai/slack", - "version": "1.17.18", + "version": "1.18.11", "dependencies": { "@opencode-ai/sdk": "workspace:*", "@slack/bolt": "^3.17.1", @@ -906,9 +853,10 @@ }, "packages/stats/app": { "name": "@opencode-ai/stats-app", - "version": "1.17.18", + "version": "1.18.11", "dependencies": { "@ibm/plex": "6.4.1", + "@kobalte/core": "catalog:", "@opencode-ai/stats-core": "workspace:*", "@opencode-ai/ui": "workspace:*", "@solidjs/meta": "catalog:", @@ -939,7 +887,7 @@ }, "packages/stats/core": { "name": "@opencode-ai/stats-core", - "version": "1.17.18", + "version": "1.18.11", "dependencies": { "@aws-sdk/client-athena": "3.933.0", "@planetscale/database": "1.19.0", @@ -958,7 +906,7 @@ }, "packages/stats/server": { "name": "@opencode-ai/stats-server", - "version": "1.17.18", + "version": "1.18.11", "dependencies": { "@aws-sdk/client-firehose": "3.933.0", "@effect/platform-node": "catalog:", @@ -990,6 +938,7 @@ "@types/node": "catalog:", "@types/react": "18.0.25", "react": "18.2.0", + "react-dom": "18.2.0", "solid-js": "catalog:", "storybook": "^10.2.13", "storybook-solidjs-vite": "^10.0.9", @@ -999,18 +948,15 @@ }, "packages/tui": { "name": "@opencode-ai/tui", - "version": "1.17.18", + "version": "1.18.11", "dependencies": { - "@opencode-ai/client": "workspace:*", "@opencode-ai/core": "workspace:*", "@opencode-ai/plugin": "workspace:*", "@opencode-ai/sdk": "workspace:*", - "@opencode-ai/simulation": "workspace:*", "@opencode-ai/ui": "workspace:*", "@opentui/core": "catalog:", "@opentui/keymap": "catalog:", "@opentui/solid": "catalog:", - "@solid-primitives/event-bus": "1.1.2", "clipboardy": "4.0.0", "diff": "catalog:", "effect": "catalog:", @@ -1020,7 +966,6 @@ "remeda": "catalog:", "solid-js": "catalog:", "strip-ansi": "7.1.2", - "uqr": "0.1.3", }, "devDependencies": { "@tsconfig/bun": "catalog:", @@ -1030,7 +975,7 @@ }, "packages/ui": { "name": "@opencode-ai/ui", - "version": "1.17.18", + "version": "1.18.11", "dependencies": { "@kobalte/core": "catalog:", "@pierre/diffs": "catalog:", @@ -1056,6 +1001,7 @@ "remend": "catalog:", "shiki": "catalog:", "solid-list": "catalog:", + "solid-sonner": "catalog:", "strip-ansi": "7.1.2", }, "devDependencies": { @@ -1081,7 +1027,7 @@ }, "packages/web": { "name": "@opencode-ai/web", - "version": "1.17.18", + "version": "1.18.11", "dependencies": { "@astrojs/cloudflare": "12.6.3", "@astrojs/markdown-remark": "6.3.1", @@ -1124,18 +1070,19 @@ ], "patchedDependencies": { "@pierre/trees@1.0.0-beta.4": "patches/@pierre%2Ftrees@1.0.0-beta.4.patch", + "@tanstack/virtual-core@3.17.3": "patches/@tanstack%2Fvirtual-core@3.17.3.patch", "@ai-sdk/xai@3.0.102": "patches/@ai-sdk%2Fxai@3.0.102.patch", "@modelcontextprotocol/sdk@1.29.0": "patches/@modelcontextprotocol%2Fsdk@1.29.0.patch", "gcp-metadata@8.1.2": "patches/gcp-metadata@8.1.2.patch", "@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch", "effect@4.0.0-beta.83": "patches/effect@4.0.0-beta.83.patch", - "@npmcli/agent@4.0.2": "patches/@npmcli%2Fagent@4.0.2.patch", + "@ai-sdk/mistral@3.0.51": "patches/@ai-sdk%2Fmistral@3.0.51.patch", "@silvia-odwyer/photon-node@0.3.4": "patches/@silvia-odwyer%2Fphoton-node@0.3.4.patch", - "@tanstack/solid-virtual@3.13.28": "patches/@tanstack%2Fsolid-virtual@3.13.28.patch", + "@npmcli/agent@4.0.2": "patches/@npmcli%2Fagent@4.0.2.patch", "solid-js@1.9.10": "patches/solid-js@1.9.10.patch", "@ai-sdk/google@3.0.73": "patches/@ai-sdk%2Fgoogle@3.0.73.patch", - "@tanstack/virtual-core@3.17.0": "patches/@tanstack%2Fvirtual-core@3.17.0.patch", "pacote@21.5.0": "patches/pacote@21.5.0.patch", + "@dnd-kit/dom@0.5.0": "patches/@dnd-kit%2Fdom@0.5.0.patch", }, "overrides": { "@opentui/core": "catalog:", @@ -1157,9 +1104,9 @@ "@npmcli/arborist": "9.4.0", "@octokit/rest": "22.0.0", "@openauthjs/openauth": "0.0.0-20250322224806", - "@opentui/core": "0.4.3", - "@opentui/keymap": "0.4.3", - "@opentui/solid": "0.4.3", + "@opentui/core": "0.4.5", + "@opentui/keymap": "0.4.5", + "@opentui/solid": "0.4.5", "@pierre/diffs": "1.2.10", "@playwright/test": "1.59.1", "@sentry/solid": "10.36.0", @@ -1170,7 +1117,7 @@ "@solidjs/router": "0.15.4", "@solidjs/start": "https://pkg.pr.new/@solidjs/start@dfb2020", "@tailwindcss/vite": "4.1.11", - "@tanstack/solid-virtual": "3.13.28", + "@tanstack/solid-virtual": "3.13.32", "@tsconfig/bun": "1.0.9", "@tsconfig/node22": "22.0.2", "@types/bun": "1.3.13", @@ -1190,7 +1137,7 @@ "hono": "4.10.7", "hono-openapi": "1.1.2", "luxon": "3.6.1", - "marked": "17.0.1", + "marked": "17.0.6", "marked-shiki": "1.2.1", "opentui-spinner": "0.0.7", "remeda": "2.26.0", @@ -1199,6 +1146,7 @@ "shiki": "4.2.0", "solid-js": "1.9.10", "solid-list": "0.3.0", + "solid-sonner": "0.3.1", "sst": "4.13.1", "tailwindcss": "4.1.11", "typescript": "5.8.2", @@ -1232,7 +1180,7 @@ "@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.82", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-WKKou2wbhGGYV8PSALAPyV2YY4nfCqCPkyBzYtJtDA9yCcIFwsbtkTNgg7bqtLCVzeEsY7wwxRoCWy+EMfrw/A=="], - "@ai-sdk/azure": ["@ai-sdk/azure@3.0.49", "", { "dependencies": { "@ai-sdk/openai": "3.0.48", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-wskgAL+OmrHG7by/iWIxEBQCEdc1mDudha/UZav46i0auzdFfsDB/k2rXZaC4/3nWSgMZkxr0W3ncyouEGX/eg=="], + "@ai-sdk/azure": ["@ai-sdk/azure@3.0.88", "", { "dependencies": { "@ai-sdk/deepseek": "2.0.47", "@ai-sdk/openai": "3.0.84", "@ai-sdk/provider": "3.0.14", "@ai-sdk/provider-utils": "4.0.38" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-RRjZkB1lYplh8dpBarnvkl1j7sYLHsyXua7erL3oNcMK7fHcl4bPO5C7iQhD1O/DqD/zCceDifnege1s+8yEvw=="], "@ai-sdk/cerebras": ["@ai-sdk/cerebras@2.0.41", "", { "dependencies": { "@ai-sdk/openai-compatible": "2.0.37", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-kDMEpjaRdRXIUi1EH8WHwLRahyDTYv9SAJnP6VCCeq8X+tVqZbMLCqqxSG5dRknrI65ucjvzQt+FiDKTAa7AHg=="], @@ -1242,7 +1190,7 @@ "@ai-sdk/deepinfra": ["@ai-sdk/deepinfra@2.0.41", "", { "dependencies": { "@ai-sdk/openai-compatible": "2.0.37", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-y6RoOP7DGWmDSiSxrUSt5p18sbz+Ixe5lMVPmdE7x+Tr5rlrzvftyHhjWHfqlAtoYERZTGFbP6tPW1OfQcrb4A=="], - "@ai-sdk/deepseek": ["@ai-sdk/deepseek@2.0.35", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-9DhYurbAvcurOEGN6u2myYDybrrzGfcrkG8hwmFjwTrePW6KCMggm0YxP7e8RkLYcQKqCEMgFlyEB4BM6EmiKg=="], + "@ai-sdk/deepseek": ["@ai-sdk/deepseek@2.0.47", "", { "dependencies": { "@ai-sdk/provider": "3.0.14", "@ai-sdk/provider-utils": "4.0.38" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MzcQ321JO8OY+TVLFI81A7cIIuoeLLxrLCDD+8C1E3Ro6UFyfMtRXo9bw9OhTMRSDMo6hgSDOo4Fekz8aJtQYQ=="], "@ai-sdk/elevenlabs": ["@ai-sdk/elevenlabs@2.0.33", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-EtvsWfGrqx3OhzJdoi82qH+4yzEPPKZr2utyQ+w8cHKoFeg0+8Lou9Z3uixy73WEwz8Z1+AR8QT9fZ64AWGYPA=="], @@ -1256,7 +1204,7 @@ "@ai-sdk/groq": ["@ai-sdk/groq@3.0.31", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-XbbugpnFmXGu2TlXiq8KUJskP6/VVbuFcnFIGDzDIB/Chg6XHsNnqrTF80Zxkh0Pd3+NvbM+2Uqrtsndk6bDAg=="], - "@ai-sdk/mistral": ["@ai-sdk/mistral@3.0.27", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ZXe7nZQgliDdjz5ufH5RKpHWxbN72AzmzzKGbF/z+0K9GN5tUCnftrQRvTRFHA5jAzTapcm2BEevmGLVbMkW+A=="], + "@ai-sdk/mistral": ["@ai-sdk/mistral@3.0.51", "", { "dependencies": { "@ai-sdk/provider": "3.0.14", "@ai-sdk/provider-utils": "4.0.40" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-83eXY6p0lUFhSuMvNDmTKDuMciK5XDAWDlNh5c0L80tKjmtCFRItA1MZHp4IKe1r7eK8Rb5nN7qtxqMLUFRIRw=="], "@ai-sdk/openai": ["@ai-sdk/openai@3.0.48", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ALmj/53EXpcRqMbGpPJPP4UOSWw0q4VGpnDo7YctvsynjkrKDmoneDG/1a7VQnSPYHnJp6tTRMf5ZdxZ5whulg=="], @@ -1274,8 +1222,6 @@ "@ai-sdk/xai": ["@ai-sdk/xai@3.0.102", "", { "dependencies": { "@ai-sdk/openai-compatible": "2.0.56", "@ai-sdk/provider": "3.0.13", "@ai-sdk/provider-utils": "4.0.35" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-NeQyOR7OCqDMgaLS4uNX/ep/HrwUzzFYLzXQSRoqLy2jsnqxAJhsgltRwAwf+ADjyPBIAKEOestWnIQA+LrLrQ=="], - "@alcalzone/ansi-tokenize": ["@alcalzone/ansi-tokenize@0.2.5", "", { "dependencies": { "ansi-styles": "^6.2.1", "is-fullwidth-code-point": "^5.0.0" } }, "sha512-3NX/MpTdroi0aKz134A6RC2Gb2iXVECN4QaAXnvCIxxIm3C3AVB1mkUe8NaaiyvOpDfsrqWhYtj+Q6a62RrTsw=="], - "@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="], "@ampproject/remapping": ["@ampproject/remapping@2.3.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw=="], @@ -1284,26 +1230,6 @@ "@anycable/core": ["@anycable/core@0.9.2", "", { "dependencies": { "nanoevents": "^7.0.1" } }, "sha512-x5ZXDcW/N4cxWl93CnbHs/u7qq4793jS2kNPWm+duPrXlrva+ml2ZGT7X9tuOBKzyIHf60zWCdIK7TUgMPAwXA=="], - "@ark/schema": ["@ark/schema@0.55.0", "", { "dependencies": { "@ark/util": "0.55.0" } }, "sha512-IlSIc0FmLKTDGr4I/FzNHauMn0MADA6bCjT1wauu4k6MyxhC1R9gz0olNpIRvK7lGGDwtc/VO0RUDNvVQW5WFg=="], - - "@ark/util": ["@ark/util@0.55.0", "", {}, "sha512-aWFNK7aqSvqFtVsl1xmbTjGbg91uqtJV7Za76YGNEwIO4qLjMfyY8flmmbhooYMuqPCO2jyxu8hve943D+w3bA=="], - - "@ast-grep/cli": ["@ast-grep/cli@0.44.0", "", { "dependencies": { "detect-libc": "2.1.2" }, "optionalDependencies": { "@ast-grep/cli-darwin-arm64": "0.44.0", "@ast-grep/cli-darwin-x64": "0.44.0", "@ast-grep/cli-linux-arm64-gnu": "0.44.0", "@ast-grep/cli-linux-x64-gnu": "0.44.0", "@ast-grep/cli-win32-arm64-msvc": "0.44.0", "@ast-grep/cli-win32-ia32-msvc": "0.44.0", "@ast-grep/cli-win32-x64-msvc": "0.44.0" }, "bin": { "sg": "sg", "ast-grep": "ast-grep" } }, "sha512-Jf4PuP7XjzsMa3m9gYxmzV8KyWZc4w1ZzKe/t0+90wWxmSasQJe6AtMkJxHEi98MGgfAF1nWziqjDd0/6EsBjA=="], - - "@ast-grep/cli-darwin-arm64": ["@ast-grep/cli-darwin-arm64@0.44.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-bF7euu/hF/cYg4510z8110vh60rrqfrBdsfRqVGd6xqNSPENu7CJnTVN/Z4Nk5U1NM8YKzUD+dYx1ySUJ0CUNQ=="], - - "@ast-grep/cli-darwin-x64": ["@ast-grep/cli-darwin-x64@0.44.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-0fI9caQGp1dFcmBATNlVytIRdAeYb91v1D2xjMIi1bSX+l8Uj846JUiaimUGBuBZmyFq+BScoWM4RnprEmZMpQ=="], - - "@ast-grep/cli-linux-arm64-gnu": ["@ast-grep/cli-linux-arm64-gnu@0.44.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-JB6EUnqEtGGtyg1GqNquld/++1CvaWD7r84IwwhddX1qx0NmDoHyn2mKd8vnQ24Z0RkV3g7y7foMLakELbGtDw=="], - - "@ast-grep/cli-linux-x64-gnu": ["@ast-grep/cli-linux-x64-gnu@0.44.0", "", { "os": "linux", "cpu": "x64" }, "sha512-rNL0LsI682D9EMzfaGVEtZa1xaqTtGb2I+Zk4ZzidX6u+fF7f79wdqyKahKjXzoIrGkuhkoL3gcyLKAtQd9+qg=="], - - "@ast-grep/cli-win32-arm64-msvc": ["@ast-grep/cli-win32-arm64-msvc@0.44.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-lqD0MhGQAddh2YoV/brKQ6GVcFLmRiTBwIElutwedaUvRCdasTGukFPYuSWk/iI8Kv19xom6s7l+mGuZ7v+xwQ=="], - - "@ast-grep/cli-win32-ia32-msvc": ["@ast-grep/cli-win32-ia32-msvc@0.44.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-ZJrnS+2OkNfwyr6yrN69glP67uybBxDvl9mqZvh1J44vB3OFn9U9c+cVAoZIAo7JD5F4rZNxwyu3gcy4+xuwEA=="], - - "@ast-grep/cli-win32-x64-msvc": ["@ast-grep/cli-win32-x64-msvc@0.44.0", "", { "os": "win32", "cpu": "x64" }, "sha512-OJEo7f95YYaSuS1byUB7ZctbzxoA7/wCoAol+pt6pvfdW/8Wq+L1qU28glwx7dQ0HgTsnPZbWpXQwmZpCBHhZg=="], - "@astrojs/check": ["@astrojs/check@0.9.6", "", { "dependencies": { "@astrojs/language-server": "^2.16.1", "chokidar": "^4.0.1", "kleur": "^4.1.5", "yargs": "^17.7.2" }, "peerDependencies": { "typescript": "^5.0.0" }, "bin": { "astro-check": "bin/astro-check.js" } }, "sha512-jlaEu5SxvSgmfGIFfNgcn5/f+29H61NJzEMfAZ82Xopr4XBchXB1GVlcJsE+elUlsYSbXlptZLX+JMG3b/wZEA=="], "@astrojs/cloudflare": ["@astrojs/cloudflare@12.6.3", "", { "dependencies": { "@astrojs/internal-helpers": "0.7.1", "@astrojs/underscore-redirects": "1.0.0", "@cloudflare/workers-types": "^4.20250507.0", "tinyglobby": "^0.2.13", "vite": "^6.3.5", "wrangler": "^4.14.1" }, "peerDependencies": { "astro": "^5.0.0" } }, "sha512-xhJptF5tU2k5eo70nIMyL1Udma0CqmUEnGSlGyFflLqSY82CRQI6nWZ/xZt0ZvmXuErUjIx0YYQNfZsz5CNjLQ=="], @@ -1332,10 +1258,6 @@ "@astrojs/yaml2ts": ["@astrojs/yaml2ts@0.2.4", "", { "dependencies": { "yaml": "^2.8.3" } }, "sha512-8oddpOae35pJsXPQXhTkM0ypfKPskVsh2bCxRtbf7e+/Epw2nReakFYpLKjZMEr75CsoF203PMnCocpfz0s69A=="], - "@asyncapi/parser": ["@asyncapi/parser@3.4.0", "", { "dependencies": { "@asyncapi/specs": "^6.8.0", "@openapi-contrib/openapi-schema-to-json-schema": "~3.2.0", "@stoplight/json": "3.21.0", "@stoplight/json-ref-readers": "^1.2.2", "@stoplight/json-ref-resolver": "^3.1.5", "@stoplight/spectral-core": "^1.18.3", "@stoplight/spectral-functions": "^1.7.2", "@stoplight/spectral-parsers": "^1.0.2", "@stoplight/spectral-ref-resolver": "^1.0.3", "@stoplight/types": "^13.12.0", "@types/json-schema": "^7.0.11", "@types/urijs": "^1.19.19", "ajv": "^8.17.1", "ajv-errors": "^3.0.0", "ajv-formats": "^2.1.1", "avsc": "^5.7.5", "js-yaml": "^4.1.0", "jsonpath-plus": "^10.0.0", "node-fetch": "2.6.7" } }, "sha512-Sxn74oHiZSU6+cVeZy62iPZMFMvKp4jupMFHelSICCMw1qELmUHPvuZSr+ZHDmNGgHcEpzJM5HN02kR7T4g+PQ=="], - - "@asyncapi/specs": ["@asyncapi/specs@6.8.1", "", { "dependencies": { "@types/json-schema": "^7.0.11" } }, "sha512-czHoAk3PeXTLR+X8IUaD+IpT+g+zUvkcgMDJVothBsan+oHN3jfcFcFUNdOPAAFoUCQN1hXF1dWuphWy05THlA=="], - "@aws-crypto/crc32": ["@aws-crypto/crc32@5.2.0", "", { "dependencies": { "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "tslib": "^2.6.2" } }, "sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg=="], "@aws-crypto/crc32c": ["@aws-crypto/crc32c@5.2.0", "", { "dependencies": { "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "tslib": "^2.6.2" } }, "sha512-+iWb8qaHLYKrNvGRbiYRHSdKRWhto5XlZUEBwDjYNf+ly5SVYG6zEoYIdxvf5R3zyeP16w4PLBn3rH1xc74Rag=="], @@ -1530,8 +1452,6 @@ "@bufbuild/protoplugin": ["@bufbuild/protoplugin@2.12.0", "", { "dependencies": { "@bufbuild/protobuf": "2.12.0", "@typescript/vfs": "^1.6.2", "typescript": "5.4.5" } }, "sha512-ORlDITp8AFUXzIhLRoMCG+ud+D3MPKWb5HQXBoskMMnjeyEjE1H1qLonVNPyOr8lkx3xSfYUo8a0dvOZJVAzow=="], - "@canvas/image-data": ["@canvas/image-data@1.1.0", "", {}, "sha512-QdObRRjRbcXGmM1tmJ+MrHcaz1MftF2+W7YI+MsphnsCrmtyfS0d5qJbk0MeSbUeyM/jCb0hmnkXPsy026L7dA=="], - "@capsizecss/unpack": ["@capsizecss/unpack@2.4.0", "", { "dependencies": { "blob-to-buffer": "^1.2.8", "cross-fetch": "^3.0.4", "fontkit": "^2.0.2" } }, "sha512-GrSU71meACqcmIUxPYOJvGKF0yryjN/L1aCuE9DViCTJI7bfkjgYDPD1zbNDcINJwSSP6UaBZY9GAbYDO7re0Q=="], "@clack/core": ["@clack/core@1.0.0-alpha.1", "", { "dependencies": { "picocolors": "^1.0.0", "sisteransi": "^1.0.5" } }, "sha512-rFbCU83JnN7l3W1nfgCqqme4ZZvTTgsiKQ6FM0l+r0P+o2eJpExcocBUWUIwnDzL76Aca9VhUdWmB2MbUv+Qyg=="], @@ -1624,11 +1544,11 @@ "@emmetio/stream-reader-utils": ["@emmetio/stream-reader-utils@0.1.0", "", {}, "sha512-ZsZ2I9Vzso3Ho/pjZFsmmZ++FWeEd/txqybHTm4OgaZzdS8V9V/YYWQwg5TC38Z7uLWUV1vavpLLbjJtKubR1A=="], - "@emnapi/core": ["@emnapi/core@1.11.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.2", "tslib": "^2.4.0" } }, "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ=="], + "@emnapi/core": ["@emnapi/core@1.9.2", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" } }, "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA=="], - "@emnapi/runtime": ["@emnapi/runtime@1.11.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw=="], + "@emnapi/runtime": ["@emnapi/runtime@1.9.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw=="], - "@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA=="], + "@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w=="], "@emotion/is-prop-valid": ["@emotion/is-prop-valid@0.8.8", "", { "dependencies": { "@emotion/memoize": "0.7.4" } }, "sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA=="], @@ -1736,8 +1656,6 @@ "@floating-ui/utils": ["@floating-ui/utils@0.2.11", "", {}, "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg=="], - "@fontsource/adwaita-mono": ["@fontsource/adwaita-mono@5.2.1", "", {}, "sha512-6+Q1UIvklJ9REijs6kv7YlRNt6yktRj0iW8H69YIugdD9P2h3eIX1AB8/9ICMfpVyVeywlsrCXg82y/LfRrjyg=="], - "@fontsource/ibm-plex-mono": ["@fontsource/ibm-plex-mono@5.2.5", "", {}, "sha512-G09N3GfuT9qj3Ax2FDZvKqZttzM3v+cco2l8uXamhKyXLdmlaUDH5o88/C3vtTHj2oT7yRKsvxz9F+BXbWKMYA=="], "@fontsource/inter": ["@fontsource/inter@5.2.8", "", {}, "sha512-P6r5WnJoKiNVV+zvW2xM13gNdFhAEpQ9dQJHt3naLvfg+LkF2ldgSLiF4T41lf1SQCM9QmkqPTn4TH568IRagg=="], @@ -1804,38 +1722,6 @@ "@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.33.5", "", { "os": "win32", "cpu": "x64" }, "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg=="], - "@inquirer/ansi": ["@inquirer/ansi@1.0.2", "", {}, "sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ=="], - - "@inquirer/checkbox": ["@inquirer/checkbox@4.3.2", "", { "dependencies": { "@inquirer/ansi": "^1.0.2", "@inquirer/core": "^10.3.2", "@inquirer/figures": "^1.0.15", "@inquirer/type": "^3.0.10", "yoctocolors-cjs": "^2.1.3" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-VXukHf0RR1doGe6Sm4F0Em7SWYLTHSsbGfJdS9Ja2bX5/D5uwVOEjr07cncLROdBvmnvCATYEWlHqYmXv2IlQA=="], - - "@inquirer/confirm": ["@inquirer/confirm@5.1.21", "", { "dependencies": { "@inquirer/core": "^10.3.2", "@inquirer/type": "^3.0.10" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ=="], - - "@inquirer/core": ["@inquirer/core@10.3.2", "", { "dependencies": { "@inquirer/ansi": "^1.0.2", "@inquirer/figures": "^1.0.15", "@inquirer/type": "^3.0.10", "cli-width": "^4.1.0", "mute-stream": "^2.0.0", "signal-exit": "^4.1.0", "wrap-ansi": "^6.2.0", "yoctocolors-cjs": "^2.1.3" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A=="], - - "@inquirer/editor": ["@inquirer/editor@4.2.23", "", { "dependencies": { "@inquirer/core": "^10.3.2", "@inquirer/external-editor": "^1.0.3", "@inquirer/type": "^3.0.10" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-aLSROkEwirotxZ1pBaP8tugXRFCxW94gwrQLxXfrZsKkfjOYC1aRvAZuhpJOb5cu4IBTJdsCigUlf2iCOu4ZDQ=="], - - "@inquirer/expand": ["@inquirer/expand@4.0.23", "", { "dependencies": { "@inquirer/core": "^10.3.2", "@inquirer/type": "^3.0.10", "yoctocolors-cjs": "^2.1.3" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-nRzdOyFYnpeYTTR2qFwEVmIWypzdAx/sIkCMeTNTcflFOovfqUk+HcFhQQVBftAh9gmGrpFj6QcGEqrDMDOiew=="], - - "@inquirer/external-editor": ["@inquirer/external-editor@1.0.3", "", { "dependencies": { "chardet": "^2.1.1", "iconv-lite": "^0.7.0" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA=="], - - "@inquirer/figures": ["@inquirer/figures@1.0.15", "", {}, "sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g=="], - - "@inquirer/input": ["@inquirer/input@4.3.1", "", { "dependencies": { "@inquirer/core": "^10.3.2", "@inquirer/type": "^3.0.10" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-kN0pAM4yPrLjJ1XJBjDxyfDduXOuQHrBB8aLDMueuwUGn+vNpF7Gq7TvyVxx8u4SHlFFj4trmj+a2cbpG4Jn1g=="], - - "@inquirer/number": ["@inquirer/number@3.0.23", "", { "dependencies": { "@inquirer/core": "^10.3.2", "@inquirer/type": "^3.0.10" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-5Smv0OK7K0KUzUfYUXDXQc9jrf8OHo4ktlEayFlelCjwMXz0299Y8OrI+lj7i4gCBY15UObk76q0QtxjzFcFcg=="], - - "@inquirer/password": ["@inquirer/password@4.0.23", "", { "dependencies": { "@inquirer/ansi": "^1.0.2", "@inquirer/core": "^10.3.2", "@inquirer/type": "^3.0.10" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-zREJHjhT5vJBMZX/IUbyI9zVtVfOLiTO66MrF/3GFZYZ7T4YILW5MSkEYHceSii/KtRk+4i3RE7E1CUXA2jHcA=="], - - "@inquirer/prompts": ["@inquirer/prompts@7.9.0", "", { "dependencies": { "@inquirer/checkbox": "^4.3.0", "@inquirer/confirm": "^5.1.19", "@inquirer/editor": "^4.2.21", "@inquirer/expand": "^4.0.21", "@inquirer/input": "^4.2.5", "@inquirer/number": "^3.0.21", "@inquirer/password": "^4.0.21", "@inquirer/rawlist": "^4.1.9", "@inquirer/search": "^3.2.0", "@inquirer/select": "^4.4.0" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-X7/+dG9SLpSzRkwgG5/xiIzW0oMrV3C0HOa7YHG1WnrLK+vCQHfte4k/T80059YBdei29RBC3s+pSMvPJDU9/A=="], - - "@inquirer/rawlist": ["@inquirer/rawlist@4.1.11", "", { "dependencies": { "@inquirer/core": "^10.3.2", "@inquirer/type": "^3.0.10", "yoctocolors-cjs": "^2.1.3" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-+LLQB8XGr3I5LZN/GuAHo+GpDJegQwuPARLChlMICNdwW7OwV2izlCSCxN6cqpL0sMXmbKbFcItJgdQq5EBXTw=="], - - "@inquirer/search": ["@inquirer/search@3.2.2", "", { "dependencies": { "@inquirer/core": "^10.3.2", "@inquirer/figures": "^1.0.15", "@inquirer/type": "^3.0.10", "yoctocolors-cjs": "^2.1.3" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-p2bvRfENXCZdWF/U2BXvnSI9h+tuA8iNqtUKb9UWbmLYCRQxd8WkvwWvYn+3NgYaNwdUkHytJMGG4MMLucI1kA=="], - - "@inquirer/select": ["@inquirer/select@4.4.2", "", { "dependencies": { "@inquirer/ansi": "^1.0.2", "@inquirer/core": "^10.3.2", "@inquirer/figures": "^1.0.15", "@inquirer/type": "^3.0.10", "yoctocolors-cjs": "^2.1.3" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-l4xMuJo55MAe+N7Qr4rX90vypFwCajSakx59qe/tMaC1aEHWLyw68wF4o0A4SLAY4E0nd+Vt+EyskeDIqu1M6w=="], - - "@inquirer/type": ["@inquirer/type@3.0.10", "", { "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA=="], - "@internationalized/date": ["@internationalized/date@3.12.2", "", { "dependencies": { "@swc/helpers": "^0.5.0" } }, "sha512-FY1Y+H64NDs+HAF6omlnWxm3mEpfgaCSWtL5l551ZZfImA+kGjPFgrnJrGjH6lfmLL0g8Z/mBu1R3kufeCp6Jw=="], "@internationalized/number": ["@internationalized/number@3.6.7", "", { "dependencies": { "@swc/helpers": "^0.5.0" } }, "sha512-3ji1fcrT+FPAK86UqEhB/psHixYo6niWPJtt7+qRaYFynt/BaJG8GhAPimtWUpEiVSTq8ZM8L5psMxGquiB/Vg=="], @@ -1870,12 +1756,6 @@ "@jsdevtools/ono": ["@jsdevtools/ono@7.1.3", "", {}, "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg=="], - "@jsep-plugin/assignment": ["@jsep-plugin/assignment@1.3.0", "", { "peerDependencies": { "jsep": "^0.4.0||^1.0.0" } }, "sha512-VVgV+CXrhbMI3aSusQyclHkenWSAm95WaiKrMxRFam3JSUiIaQjoMIw2sEs/OX4XifnqeQUN4DYbJjlA8EfktQ=="], - - "@jsep-plugin/regex": ["@jsep-plugin/regex@1.0.4", "", { "peerDependencies": { "jsep": "^0.4.0||^1.0.0" } }, "sha512-q7qL4Mgjs1vByCaTnDFcBnV9HS7GVPJX5vyVoCgZHNSC9rjwIlmbXG5sUuorR5ndfHAIlJ8pVStxvjXHbNvtUg=="], - - "@jsep-plugin/ternary": ["@jsep-plugin/ternary@1.1.4", "", { "peerDependencies": { "jsep": "^0.4.0||^1.0.0" } }, "sha512-ck5wiqIbqdMX6WRQztBL7ASDty9YLgJ3sSAK5ZpBzXeySvFGCzIvM6UiAI4hTZ22fEcYQVV/zhUbNscggW+Ukg=="], - "@jsx-email/all": ["@jsx-email/all@2.2.3", "", { "dependencies": { "@jsx-email/body": "1.0.2", "@jsx-email/button": "1.0.4", "@jsx-email/column": "1.0.3", "@jsx-email/container": "1.0.2", "@jsx-email/font": "1.0.3", "@jsx-email/head": "1.0.2", "@jsx-email/heading": "1.0.2", "@jsx-email/hr": "1.0.2", "@jsx-email/html": "1.0.2", "@jsx-email/img": "1.0.2", "@jsx-email/link": "1.0.2", "@jsx-email/markdown": "2.0.4", "@jsx-email/preview": "1.0.2", "@jsx-email/render": "1.1.1", "@jsx-email/row": "1.0.2", "@jsx-email/section": "1.0.2", "@jsx-email/tailwind": "2.4.4", "@jsx-email/text": "1.0.2" }, "peerDependencies": { "react": "^18.2.0" } }, "sha512-OBvLe/hVSQc0LlMSTJnkjFoqs3bmxcC4zpy/5pT5agPCSKMvAKQjzmsc2xJ2wO73jSpRV1K/g38GmvdCfrhSoQ=="], "@jsx-email/body": ["@jsx-email/body@1.0.2", "", { "peerDependencies": { "react": "^18.2.0" } }, "sha512-NjR2tgLH4XGfGkm+O8kcVwi9MBqZsXZCLlmk3HlMux3/n/+a5zB+yhJqXWZBJl2i+6cSF+E2O6hK11ekyK9WWQ=="], @@ -1950,26 +1830,6 @@ "@mdx-js/react": ["@mdx-js/react@3.1.1", "", { "dependencies": { "@types/mdx": "^2.0.0" }, "peerDependencies": { "@types/react": ">=16", "react": ">=16" } }, "sha512-f++rKLQgUVYDAtECQ6fn/is15GkEH9+nZPM3MS0RcxVqoTfawHvDlSCH7JbMhAM6uJ32v3eXLvLmLvjGu7PTQw=="], - "@mintlify/cli": ["@mintlify/cli@4.0.1269", "", { "dependencies": { "@inquirer/prompts": "7.9.0", "@mintlify/common": "1.0.985", "@mintlify/link-rot": "3.0.1172", "@mintlify/models": "0.0.333", "@mintlify/prebuild": "1.0.1131", "@mintlify/previewing": "4.0.1197", "@mintlify/validation": "0.1.769", "adm-zip": "0.5.16", "chalk": "5.2.0", "color": "4.2.3", "detect-port": "1.5.1", "front-matter": "4.0.2", "fs-extra": "11.2.0", "ink": "6.3.0", "inquirer": "12.3.0", "js-yaml": "4.1.1", "mdast-util-mdx-jsx": "3.2.0", "open": "8.4.2", "openid-client": "6.8.2", "posthog-node": "5.17.2", "react": "19.2.3", "semver": "7.7.2", "unist-util-visit": "5.0.0", "yargs": "17.7.1", "zod": "4.3.6" }, "optionalDependencies": { "keytar": "7.9.0" }, "bin": { "mint": "bin/index.js", "mintlify": "bin/index.js" } }, "sha512-l9b7InT55JWXV7TU7Jr4Wrijv4/gMFHLQyWJ7fcjqpSxAetR+xNyeEARRlcf7OicGTjZuvmRGGfj75kp3O4p7A=="], - - "@mintlify/common": ["@mintlify/common@1.0.985", "", { "dependencies": { "@asyncapi/parser": "3.4.0", "@asyncapi/specs": "6.8.1", "@mintlify/mdx": "3.0.4", "@mintlify/models": "0.0.333", "@mintlify/openapi-parser": "0.0.8", "@mintlify/validation": "0.1.769", "@sindresorhus/slugify": "2.2.0", "@types/mdast": "4.0.4", "acorn": "8.11.2", "acorn-jsx": "5.3.2", "color-blend": "4.0.0", "estree-util-to-js": "2.0.0", "estree-walker": "3.0.3", "front-matter": "4.0.2", "hast-util-from-html": "2.0.3", "hast-util-to-html": "9.0.4", "hast-util-to-text": "4.0.2", "hex-rgb": "5.0.0", "ignore": "7.0.5", "js-yaml": "4.1.1", "lodash": "4.18.1", "mdast-util-from-markdown": "2.0.2", "mdast-util-gfm": "3.0.0", "mdast-util-mdx": "3.0.0", "mdast-util-mdx-jsx": "3.1.3", "micromark-extension-gfm": "3.0.0", "micromark-extension-mdx-jsx": "3.0.1", "micromark-extension-mdxjs": "3.0.0", "openapi-types": "12.1.3", "postcss": "8.5.14", "rehype-stringify": "10.0.1", "remark": "15.0.1", "remark-frontmatter": "5.0.0", "remark-gfm": "4.0.0", "remark-math": "6.0.0", "remark-mdx": "3.1.0", "remark-parse": "11.0.0", "remark-rehype": "11.1.1", "remark-stringify": "11.0.0", "sucrase": "3.34.0", "tailwindcss": "3.4.17", "unified": "11.0.5", "unist-builder": "4.0.0", "unist-util-map": "4.0.0", "unist-util-remove": "4.0.0", "unist-util-remove-position": "5.0.0", "unist-util-visit": "5.0.0", "unist-util-visit-parents": "6.0.1", "vfile": "6.0.3", "xss": "1.0.15" } }, "sha512-eJPeR99AKgVifXLdiA2hhfNy2+CmZ3zqQAscRXmFbJFST8SgsUj6rU3D2fx0XYt38b7T3LqEMD7DH5ipSC+Zfg=="], - - "@mintlify/link-rot": ["@mintlify/link-rot@3.0.1172", "", { "dependencies": { "@mintlify/common": "1.0.985", "@mintlify/models": "0.0.333", "@mintlify/prebuild": "1.0.1131", "@mintlify/previewing": "4.0.1197", "@mintlify/scraping": "4.0.849", "@mintlify/validation": "0.1.769", "fs-extra": "11.1.0", "unist-util-visit": "4.1.2" } }, "sha512-8962sk/WO/0YcSkHTiaVZ2DvrCb8TC4BPgp21a9qW6nsPKKNixMhsC2uUB90D+gOzPTejJl0NNd4gLk4fA3SKw=="], - - "@mintlify/mdx": ["@mintlify/mdx@3.0.4", "", { "dependencies": { "@shikijs/transformers": "^3.11.0", "@shikijs/twoslash": "^3.12.2", "arktype": "^2.1.26", "hast-util-to-string": "^3.0.1", "mdast-util-from-markdown": "^2.0.2", "mdast-util-gfm": "^3.1.0", "mdast-util-mdx-jsx": "^3.2.0", "mdast-util-to-hast": "^13.2.0", "next-mdx-remote-client": "^1.0.3", "rehype-katex": "^7.0.1", "remark-gfm": "^4.0.0", "remark-math": "^6.0.0", "remark-smartypants": "^3.0.2", "shiki": "^3.11.0", "unified": "^11.0.0", "unist-util-visit": "^5.0.0" }, "peerDependencies": { "@radix-ui/react-popover": "^1.1.15", "react": "^18.3.1", "react-dom": "^18.3.1" } }, "sha512-tJhdpnM5ReJLNJ2fuDRIEr0zgVd6id7/oAIfs26V46QlygiLsc8qx4Rz3LWIX51rUXW/cfakjj0EATxIciIw+g=="], - - "@mintlify/models": ["@mintlify/models@0.0.333", "", { "dependencies": { "axios": "1.16.1", "openapi-types": "12.1.3" } }, "sha512-0uAsuTsV8gYCDpv4aA0MWilQu+a/mrK+G6q5FVcgunbEZ3meeCXfrQFTviaEw7A+0cR/7Pc2KLA66cPDm+3Qdg=="], - - "@mintlify/openapi-parser": ["@mintlify/openapi-parser@0.0.8", "", { "dependencies": { "ajv": "^8.17.1", "ajv-draft-04": "^1.0.0", "ajv-formats": "^3.0.1", "jsonpointer": "^5.0.1", "leven": "^4.0.0", "yaml": "^2.4.5" } }, "sha512-9MBRq9lS4l4HITYCrqCL7T61MOb20q9IdU7HWhqYMNMM1jGO1nHjXasFy61yZ8V6gMZyyKQARGVoZ0ZrYN48Og=="], - - "@mintlify/prebuild": ["@mintlify/prebuild@1.0.1131", "", { "dependencies": { "@mintlify/common": "1.0.985", "@mintlify/openapi-parser": "0.0.8", "@mintlify/scraping": "4.0.849", "@mintlify/validation": "0.1.769", "chalk": "5.3.0", "favicons": "7.2.0", "front-matter": "4.0.2", "fs-extra": "11.1.0", "js-yaml": "4.1.1", "openapi-types": "12.1.3", "sharp": "0.33.5", "sharp-ico": "0.1.5", "unist-util-visit": "4.1.2", "uuid": "11.1.1" } }, "sha512-EbPf1/z1m8K/Jl4qXggiMQwfdqXLF25sj+d5SHaGl6T0auTOYMayN578Rb/qM4fjhhlBQwub919Zsq9syYeYUA=="], - - "@mintlify/previewing": ["@mintlify/previewing@4.0.1197", "", { "dependencies": { "@mintlify/common": "1.0.985", "@mintlify/prebuild": "1.0.1131", "@mintlify/validation": "0.1.769", "adm-zip": "0.5.16", "better-opn": "3.0.2", "chalk": "5.2.0", "chokidar": "3.5.3", "express": "4.22.0", "front-matter": "4.0.2", "fs-extra": "11.1.0", "got": "13.0.0", "ink": "6.3.0", "ink-spinner": "5.0.0", "is-online": "10.0.0", "js-yaml": "4.1.1", "openapi-types": "12.1.3", "react": "19.2.3", "socket.io": "4.8.0", "tar": "7.5.15", "unist-util-visit": "4.1.2", "yargs": "17.7.1" } }, "sha512-q4TunK8KjE1k9ve5nOAQTvBpsE7bX+RJN/ozx7RdIyQSCSexpkywmSM6nE3ECJyjUWFy8pg6yrYYbUQLHN/oww=="], - - "@mintlify/scraping": ["@mintlify/scraping@4.0.849", "", { "dependencies": { "@mintlify/common": "1.0.985", "@mintlify/openapi-parser": "0.0.8", "fs-extra": "11.1.1", "hast-util-to-mdast": "10.1.0", "js-yaml": "4.1.1", "mdast-util-mdx-jsx": "3.1.3", "neotraverse": "0.6.18", "puppeteer": "22.14.0", "rehype-parse": "9.0.1", "remark-gfm": "4.0.0", "remark-mdx": "3.0.1", "remark-parse": "11.0.0", "remark-stringify": "11.0.0", "unified": "11.0.5", "unist-util-visit": "5.0.0", "yargs": "17.7.1", "zod": "3.24.0" }, "bin": { "mintlify-scrape": "bin/cli.js" } }, "sha512-4aMltLtfSU5rkUJt2SCVaYIbuggiEnx7lMWKM8NW93SaZUeoL0iKNbo0K24SmOqS7kYATRRH8zI7gefPX2ZLDw=="], - - "@mintlify/validation": ["@mintlify/validation@0.1.769", "", { "dependencies": { "@mintlify/mdx": "3.0.4", "@mintlify/models": "0.0.333", "arktype": "2.1.27", "fractional-indexing": "3.2.0", "js-yaml": "4.1.1", "lcm": "0.0.3", "lodash": "4.18.1", "neotraverse": "0.6.18", "object-hash": "3.0.0", "openapi-types": "12.1.3", "uuid": "11.1.1", "zod": "3.24.0", "zod-to-json-schema": "3.20.4" } }, "sha512-8Sg6DCdQ7RSc3NIyaSSWy7G6KaAuw0NfUSBCYLwGhtp6dYPh1jEsPn6YU8hqUMNZn1OQHsA52rA6lZQxjnyLHQ=="], - "@mixmark-io/domino": ["@mixmark-io/domino@2.2.0", "", {}, "sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw=="], "@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.29.0", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.2.1", "express-rate-limit": "^8.2.1", "hono": "^4.11.4", "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ=="], @@ -1998,30 +1858,6 @@ "@msgpackr-extract/msgpackr-extract-win32-x64": ["@msgpackr-extract/msgpackr-extract-win32-x64@3.0.4", "", { "os": "win32", "cpu": "x64" }, "sha512-CmCXPQrkbwExx3j946/PtHWHbYJiCRBRDl4BlkRQcJB/YOwQxJRTpoo7aTsortjgoJ1x7opzTSxn7C+ASSLVjQ=="], - "@napi-rs/canvas": ["@napi-rs/canvas@1.0.2", "", { "optionalDependencies": { "@napi-rs/canvas-android-arm64": "1.0.2", "@napi-rs/canvas-darwin-arm64": "1.0.2", "@napi-rs/canvas-darwin-x64": "1.0.2", "@napi-rs/canvas-linux-arm-gnueabihf": "1.0.2", "@napi-rs/canvas-linux-arm64-gnu": "1.0.2", "@napi-rs/canvas-linux-arm64-musl": "1.0.2", "@napi-rs/canvas-linux-riscv64-gnu": "1.0.2", "@napi-rs/canvas-linux-x64-gnu": "1.0.2", "@napi-rs/canvas-linux-x64-musl": "1.0.2", "@napi-rs/canvas-win32-arm64-msvc": "1.0.2", "@napi-rs/canvas-win32-x64-msvc": "1.0.2" } }, "sha512-EYEqlMYaCbpZDz+IgDH5xp9MTd3ui4dmGqbQYryhMLnSRxrhHKq5KQWHHKxFUcEP4Hp8/BWgvqXocX4j7iSbOQ=="], - - "@napi-rs/canvas-android-arm64": ["@napi-rs/canvas-android-arm64@1.0.2", "", { "os": "android", "cpu": "arm64" }, "sha512-IMXKVQod0ol4vt3gmClUfXz4JAgHYESGPCUqmH3lQxBoL0K/2greJaQE1HVBVxWWFKfLc4OLZVdxg7kXVyXv+g=="], - - "@napi-rs/canvas-darwin-arm64": ["@napi-rs/canvas-darwin-arm64@1.0.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Sc8tPi6cF+5lqOzCCKFALJHhDiRwyMzTPYm3bbhdXsOunU0lQO5f05ucyOzN2r55I23Hg5bsjH63uSCvWp3EgQ=="], - - "@napi-rs/canvas-darwin-x64": ["@napi-rs/canvas-darwin-x64@1.0.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-niDXZ9LhKB1zLrUdYB64RHQFDGz9rr0eGx061qtJJU3U20EMMIx28ADF5fVYbhtOgkWQrBjFicfaye1yM0U62A=="], - - "@napi-rs/canvas-linux-arm-gnueabihf": ["@napi-rs/canvas-linux-arm-gnueabihf@1.0.2", "", { "os": "linux", "cpu": "arm" }, "sha512-sgatQL9JxGRH/Amzcvu0P3t8Am3duou74CisfuJ41Dwt8cWy723z/9KZ8LlgmxfypEwEZxSTNFJtU8d281lmhQ=="], - - "@napi-rs/canvas-linux-arm64-gnu": ["@napi-rs/canvas-linux-arm64-gnu@1.0.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-dgKuX0peF3xwY6ZF5QxGS4wbfDqpoFAJYXiLSp+guZKARQUKMkRqZSDrXKj7nfrec3UCMzC0PFCPte0ES98AiA=="], - - "@napi-rs/canvas-linux-arm64-musl": ["@napi-rs/canvas-linux-arm64-musl@1.0.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-qwROoDIC9upfvDoRLuPn2aNg9CGW1x0Ygr4k2Or+8paA9d0qBLwk87U+g8KQpoOviKoPoiwl97kvBYuYD7qZoA=="], - - "@napi-rs/canvas-linux-riscv64-gnu": ["@napi-rs/canvas-linux-riscv64-gnu@1.0.2", "", { "os": "linux", "cpu": "none" }, "sha512-fXRjnPihdnbO6qy1QQOgxAonb68A0TCEG7rj1x7v7rxNElsE8EVIKIEUTvyDtU+sthYSbX+8e7g3oZiLGnOmxw=="], - - "@napi-rs/canvas-linux-x64-gnu": ["@napi-rs/canvas-linux-x64-gnu@1.0.2", "", { "os": "linux", "cpu": "x64" }, "sha512-nPR97DXhbWIAy7yazF3jc06kEPMqYMLmPzFOVNlwKPfIoSChnI+x7dc0hTLaihz3jxrjL6j4BbA7earxfx4X3g=="], - - "@napi-rs/canvas-linux-x64-musl": ["@napi-rs/canvas-linux-x64-musl@1.0.2", "", { "os": "linux", "cpu": "x64" }, "sha512-l7zZY5+jL5qnBZtDz7CoBtY6p7EkHu422g/0zWwrOrzIwWyWxZFRfZZORY1UG7YApymPLx+UbOkN206xXn/c1Q=="], - - "@napi-rs/canvas-win32-arm64-msvc": ["@napi-rs/canvas-win32-arm64-msvc@1.0.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-yE0koHCFF4PIbMc2o2SEALhnipz7WBISh5glLvQiomtIoCcW0np3H4Lw93ceJAfJttTTeIIWFbwH84F7EVzjMQ=="], - - "@napi-rs/canvas-win32-x64-msvc": ["@napi-rs/canvas-win32-x64-msvc@1.0.2", "", { "os": "win32", "cpu": "x64" }, "sha512-okU8/t2foV6C31n0GtvEMbfD5rOFc70+/6xUNME9Guld29sgSOIGUEDScAWFlcP3k5TYQRl9TNkwJEEjh15w8A=="], - "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.4", "", { "dependencies": { "@tybys/wasm-util": "^0.10.1" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" } }, "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow=="], "@noble/hashes": ["@noble/hashes@2.2.0", "", {}, "sha512-IYqDGiTXab6FniAgnSdZwgWbomxpy9FtYvLKs7wCUs2a8RkITG+DFGO1DM9cr+E3/RgADRpFjrKVaJ1z6sjtEg=="], @@ -2108,8 +1944,6 @@ "@one-ini/wasm": ["@one-ini/wasm@0.1.1", "", {}, "sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw=="], - "@openapi-contrib/openapi-schema-to-json-schema": ["@openapi-contrib/openapi-schema-to-json-schema@3.2.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3" } }, "sha512-Gj6C0JwCr8arj0sYuslWXUBSP/KnUlEGnPW4qxlXvAl543oaNQgMgIgkQUA6vs5BCCvwTEiL8m/wdWzfl4UvSw=="], - "@openauthjs/openauth": ["@openauthjs/openauth@0.0.0-20250322224806", "", { "dependencies": { "@standard-schema/spec": "1.0.0-beta.3", "aws4fetch": "1.0.20", "jose": "5.9.6" }, "peerDependencies": { "arctic": "^2.2.2", "hono": "^4.0.0" } }, "sha512-p5IWSRXvABcwocH2dNI0w8c1QJelIOFulwhKk+aLLFfUbs8u1pr7kQbYe8yCSM2+bcLHiwbogpUQc2ovrGwCuw=="], "@opencode-ai/app": ["@opencode-ai/app@workspace:packages/app"], @@ -2136,8 +1970,6 @@ "@opencode-ai/desktop": ["@opencode-ai/desktop@workspace:packages/desktop"], - "@opencode-ai/docs": ["@opencode-ai/docs@workspace:packages/docs"], - "@opencode-ai/effect-drizzle-sqlite": ["@opencode-ai/effect-drizzle-sqlite@workspace:packages/effect-drizzle-sqlite"], "@opencode-ai/effect-sqlite-node": ["@opencode-ai/effect-sqlite-node@workspace:packages/effect-sqlite-node"], @@ -2168,8 +2000,6 @@ "@opencode-ai/session-ui": ["@opencode-ai/session-ui@workspace:packages/session-ui"], - "@opencode-ai/simulation": ["@opencode-ai/simulation@workspace:packages/simulation"], - "@opencode-ai/slack": ["@opencode-ai/slack@workspace:packages/slack"], "@opencode-ai/stats-app": ["@opencode-ai/stats-app@workspace:packages/stats/app"], @@ -2214,27 +2044,27 @@ "@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.41.1", "", {}, "sha512-/UhIkaZgPutTFmQ7RnIJGgDXZmtEJ7Dvi86xNTFWcnRxVRNk/aotsqDJYeEvDP+FSMB2SdW+pQzNMcWP0rwuNA=="], - "@opentui/core": ["@opentui/core@0.4.3", "", { "dependencies": { "bun-ffi-structs": "0.2.4", "diff": "9.0.0", "marked": "17.0.1", "string-width": "7.2.0", "strip-ansi": "7.1.2" }, "optionalDependencies": { "@opentui/core-darwin-arm64": "0.4.3", "@opentui/core-darwin-x64": "0.4.3", "@opentui/core-linux-arm64": "0.4.3", "@opentui/core-linux-arm64-musl": "0.4.3", "@opentui/core-linux-x64": "0.4.3", "@opentui/core-linux-x64-musl": "0.4.3", "@opentui/core-win32-arm64": "0.4.3", "@opentui/core-win32-x64": "0.4.3" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-rrJfAk13tALDqldYjhc78eWQ+aKq1iknJgffIOg3OwyZoqQo+p6gtuqyhmWvXIfQzlNUbpgpCPcxbXlhMnlaHQ=="], + "@opentui/core": ["@opentui/core@0.4.5", "", { "dependencies": { "bun-ffi-structs": "0.2.4", "diff": "9.0.0", "marked": "17.0.1", "string-width": "7.2.0", "strip-ansi": "7.1.2" }, "optionalDependencies": { "@opentui/core-darwin-arm64": "0.4.5", "@opentui/core-darwin-x64": "0.4.5", "@opentui/core-linux-arm64": "0.4.5", "@opentui/core-linux-arm64-musl": "0.4.5", "@opentui/core-linux-x64": "0.4.5", "@opentui/core-linux-x64-musl": "0.4.5", "@opentui/core-win32-arm64": "0.4.5", "@opentui/core-win32-x64": "0.4.5" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-JsgRTPkA6e+Vxmumxai6SElOSlRQkbzNKHlCfemlArRiLhfC1IZ9RXJo2QH4xSu+uBOWAM90uss73/pPlkdEig=="], - "@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.4.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-p5+7AAxpxGuDGagyQfewKtmTFnN7THvTVY4FyKqUtJomNaHdQXPHztapNNzMx0DGWbwOUbVKzpL+yc3CZY3chQ=="], + "@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.4.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-8KUG0oRidnR+oW1RSZJ72/PhZLl+qRRMk5U/mieF4c0SJ5V3tYACpBZAKzQfHNd1f7QzD8FHZct1lPpQgtmkWg=="], - "@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.4.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-+fh0vEUE0lwVC7RW5ijYLRlTLp5NfvCRj8SzxDVd7IL2j2ssB6YXcfIbXq2EW7UGnrejwPRXf1tgUrIXW9KmOw=="], + "@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.4.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-R2bocsg55gwjOqCp/MWFgFYzRmsduKegB6nzgFAPCvAD/L5Jf30xpWJWFlSg3x8vxe1L9WJ84dfqa4M7mZZ3wA=="], - "@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.4.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-gl6qA5QJy6u8Cbt7gOtHbhhfMZ4qQDb0kEwFXHcMGmbnKzz4OHoq74D6tNjyvSQB9saoC7C6C0tvn2DcJOuNog=="], + "@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.4.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-R4MZ25a4CzOAGVjW9aj1hUfzQGVfCJwrwBDbNs2SXaIvzcZqkxCVtU4FoQ5LsaD0j/BdNQVg2CIfFkFsm1fDuQ=="], - "@opentui/core-linux-arm64-musl": ["@opentui/core-linux-arm64-musl@0.4.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-8p8g8/AEq/xFGpQ7XcIFKcAqjc0QwsZcv+Ll9RbCDpUA56FGH6jfLDir0KYTNTgYXJTIrBIENI9K46VuxMUMQA=="], + "@opentui/core-linux-arm64-musl": ["@opentui/core-linux-arm64-musl@0.4.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-ieqdyKI6EIYPalYAETB2wsdP83hr5Ifi+dFnBFUmdEEFHsoKwBmn2S7bsTOYlX7Bg03F4/YPIg+IvRpeC+cUJw=="], - "@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.4.3", "", { "os": "linux", "cpu": "x64" }, "sha512-dXpJitiZdYE3hq2Pvx6e9I0uPQSOcnaLLp1pDgWAHv+3kvKSHEX//9Yr/pV/Ua6qqT7p+2D/K4vXNap/NKVo2w=="], + "@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.4.5", "", { "os": "linux", "cpu": "x64" }, "sha512-SNyuQoxMKI1vuJhgxSSW96adWM6LqFl2SoS3GM4tGeneGOanVVG2Y06PvlytXvF4cKik97t0rqkVMRetmOs93w=="], - "@opentui/core-linux-x64-musl": ["@opentui/core-linux-x64-musl@0.4.3", "", { "os": "linux", "cpu": "x64" }, "sha512-/QiFpCrpU2O7vy8QYmLIQYbvAtKDgmqcVjR7dGtqSzkiQk3ktNJoo5RozG7ueXnjung1Wp0nKldKxo2Csg/OrA=="], + "@opentui/core-linux-x64-musl": ["@opentui/core-linux-x64-musl@0.4.5", "", { "os": "linux", "cpu": "x64" }, "sha512-mKVKcIcPiSVVZZsdPSBoWwoa2/TCeQAaMDeHF7PFw2kt5bTXZPP7xxWfRQLCNIcA1eaGl59UuwUWHDR2Ve548Q=="], - "@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.4.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-Mx2zuOjrhm/z2SDS6RExIyjP/SnN/8QhhagxURUw0jQi/NssGSeAllu1cBAFFnhobJL5QLTE4FU4CRhUK9svgg=="], + "@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.4.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-GHTTsqeR45q2Iek9Rb7ty+x/hAKn2jZ1ujlCgPR8LBKyF7h0E1dNFryoZ7ehMc3kJndP1sKn836IemKFqxuDdQ=="], - "@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.4.3", "", { "os": "win32", "cpu": "x64" }, "sha512-NuoqvWKGXaYnmlqvu7Gg2lLI6yVMnS9OfWBvxp+7Q+McSgHFSTQmYBXaPpvQ8HikpQXE1nCeMPtuSG4PdZHe2w=="], + "@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.4.5", "", { "os": "win32", "cpu": "x64" }, "sha512-Y8T/yXCDGagRGiQrtmuB6AhRcPucKFs/Dre3v8kJwNYqDccI4FzUPKclZ7djfmRZNjl7JUqPhZZP/PwDpQocMg=="], - "@opentui/keymap": ["@opentui/keymap@0.4.3", "", { "dependencies": { "@opentui/core": "0.4.3" }, "peerDependencies": { "@opentui/react": "0.4.3", "@opentui/solid": "0.4.3", "react": ">=19.2.0", "solid-js": "1.9.12" }, "optionalPeers": ["@opentui/react", "@opentui/solid", "react", "solid-js"] }, "sha512-sinX0pyQBRrEvo89PSSUbSUDIYpL3xWo81VEfec58VFoVRB5FG48/deAtvRTQfJ8w1kgbzN8hzdOXdSm61zBmw=="], + "@opentui/keymap": ["@opentui/keymap@0.4.5", "", { "dependencies": { "@opentui/core": "0.4.5" }, "peerDependencies": { "@opentui/react": "0.4.5", "@opentui/solid": "0.4.5", "react": ">=19.2.0", "solid-js": "1.9.12" }, "optionalPeers": ["@opentui/react", "@opentui/solid", "react", "solid-js"] }, "sha512-S1wzKHhF70zT6bH+VBFY+lSeTImLcIFW28JNQiME8MoPcy6KGPs7rKFSHrb/U7P8rsTJeRfW5A4d1Cy6PKodDg=="], - "@opentui/solid": ["@opentui/solid@0.4.3", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.4.3", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.12", "entities": "7.0.1", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.12" } }, "sha512-RcV0+S8HMdXOASyr7HmJUBuTUIaFPzAxMDa44VftS5C2JUgrmAuWo0Njv1q3TWRB1owjHnyKhEfWGKq7A82wxw=="], + "@opentui/solid": ["@opentui/solid@0.4.5", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.4.5", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.12", "entities": "7.0.1", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.12" } }, "sha512-B0RSkXnrtPVfEJOX+Hj+axjLJ3lzbG1BZw5I7Pvb9OPp48Vzg2cW2a3cSa86/q48ndLt647i/XwFPIw/jqnI5g=="], "@oslojs/asn1": ["@oslojs/asn1@1.0.0", "", { "dependencies": { "@oslojs/binary": "1.0.0" } }, "sha512-zw/wn0sj0j0QKbIXfIlnEcTviaCzYOY3V5rAyjR6YtOByFtJiT574+8p9Wlach0lZH9fddD4yb9laEAIl4vXQA=="], @@ -2316,7 +2146,7 @@ "@oxc-parser/binding-win32-x64-msvc": ["@oxc-parser/binding-win32-x64-msvc@0.127.0", "", { "os": "win32", "cpu": "x64" }, "sha512-rOrnSQSCbhI2kowr9XxE7m9a8oQXnBHjnS6j95LxxAnEZ0+Fz20WlRXG4ondQb+ejjt2KOsa65sE6++L6kUd+w=="], - "@oxc-project/types": ["@oxc-project/types@0.138.0", "", {}, "sha512-1a7ZKmrRTCoN1XMZ4L0PyyqrMnrNlLyPuOkdSX2MZg7IiIGRUyurNhAm73ptDOraoBcIordsIGKNPKUzy3ZmfA=="], + "@oxc-project/types": ["@oxc-project/types@0.127.0", "", {}, "sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ=="], "@oxc-resolver/binding-android-arm-eabi": ["@oxc-resolver/binding-android-arm-eabi@11.20.0", "", { "os": "android", "cpu": "arm" }, "sha512-IjfWOXRgJFNdORDl+Uf1aibNgZY2guOD3zmOhx1BGVb/MIiqlFTdmjpQNplSN58lhWehnX4UNqC3QwpUo8pjJg=="], @@ -2510,8 +2340,6 @@ "@poppinss/exception": ["@poppinss/exception@1.2.3", "", {}, "sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw=="], - "@posthog/core": ["@posthog/core@1.7.1", "", { "dependencies": { "cross-spawn": "^7.0.6" } }, "sha512-kjK0eFMIpKo9GXIbts8VtAknsoZ18oZorANdtuTj1CbgS28t4ZVq//HAWhnxEuXRTrtkd+SUJ6Ux3j2Af8NCuA=="], - "@preact/signals-core": ["@preact/signals-core@1.14.3", "", {}, "sha512-m0K3vnbSLC5rHs2ZVfeAMvBtT1zIyq4mxx5OlNncSgMj5Iz6W5Rn3kPrDxAC+iIKmiVe0lSl6U37t5ZkEWoVAw=="], "@protobuf-ts/plugin": ["@protobuf-ts/plugin@2.11.1", "", { "dependencies": { "@bufbuild/protobuf": "^2.4.0", "@bufbuild/protoplugin": "^2.4.0", "@protobuf-ts/protoc": "^2.11.1", "@protobuf-ts/runtime": "^2.11.1", "@protobuf-ts/runtime-rpc": "^2.11.1", "typescript": "^3.9" }, "bin": { "protoc-gen-ts": "bin/protoc-gen-ts", "protoc-gen-dump": "bin/protoc-gen-dump" } }, "sha512-HyuprDcw0bEEJqkOWe1rnXUP0gwYLij8YhPuZyZk6cJbIgc/Q0IFgoHQxOXNIXAcXM4Sbehh6kjVnCzasElw1A=="], @@ -2542,8 +2370,6 @@ "@protobufjs/utf8": ["@protobufjs/utf8@1.1.1", "", {}, "sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg=="], - "@puppeteer/browsers": ["@puppeteer/browsers@2.3.0", "", { "dependencies": { "debug": "^4.3.5", "extract-zip": "^2.0.1", "progress": "^2.0.3", "proxy-agent": "^6.4.0", "semver": "^7.6.3", "tar-fs": "^3.0.6", "unbzip2-stream": "^1.4.3", "yargs": "^17.7.2" }, "bin": { "browsers": "lib/cjs/main-cli.js" } }, "sha512-ioXoq9gPxkss4MYhD+SFaU9p1IHFUX0ILAWFPyjGaBdjLsYAlZw6j1iLA0N/m12uVHLFDfSYNF7EQccjinIMDA=="], - "@radix-ui/colors": ["@radix-ui/colors@1.0.1", "", {}, "sha512-xySw8f0ZVsAEP+e7iLl3EvcBXX7gsIlC1Zso/sPBW9gIWerBTgz6axrjU+MZ39wD+WFi5h5zdWpsg3+hwt2Qsg=="], "@radix-ui/primitive": ["@radix-ui/primitive@1.0.1", "", { "dependencies": { "@babel/runtime": "^7.13.10" } }, "sha512-yQ8oGX2GVsEYMWGxcovu1uGWPCxV5BFfeeYxqPmuAzUyLT9qmaMXSAhXpb0WrspIeqYzdJpkh2vHModJPgRIaw=="], @@ -2608,37 +2434,7 @@ "@remix-run/router": ["@remix-run/router@1.9.0", "", {}, "sha512-bV63itrKBC0zdT27qYm6SDZHlkXwFL1xMBuhkn+X7l0+IIhNaH5wuuvZKp6eKhCD4KFhujhfhCT1YxXW6esUIA=="], - "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.1.4", "", { "os": "android", "cpu": "arm64" }, "sha512-EZLpf/8y7GXkkra90ML47kzik/GMP3EMcE9bPyHmRfxLC6z9+aW5A8poCsoxjrT5GfEcNAAvWwUHjvP1pUQkfw=="], - - "@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.1.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-aUi+HBvmYb7j8krl1+qJgkG8C17fO79gk3c+jPw4S8glRFc1DTija9S3EyaTSQUm5GJXYKDAsugBEhFHH2vYiQ=="], - - "@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.1.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-F7hHC3gwY11+vByKPRWqwGbeXWVgKmL+pTGCinaEhdihzBV2aQ0fvZOch9cXYUOKuKKq429HeYXOqQLc7wFCEg=="], - - "@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.1.4", "", { "os": "freebsd", "cpu": "x64" }, "sha512-sI5yw+7s92SK6odiEhD5lKCBlWcpjHS5qyqpVQbZAJ0fIzEUXrmbl3DH2ybR3PZogulNJF+COLtmA8hUfvkCCQ=="], - - "@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.1.4", "", { "os": "linux", "cpu": "arm" }, "sha512-mCi0OKgEieFircrtVYmQAFGszRtMnZ6fpZAXrxanXAu7lqZcsK1E1RAaZNG0uKAnxox3B1f4EyQNnoyMfN1vAA=="], - - "@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.1.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-B9Ial3Kv5sh0SHnB1g/QWcUQCEvCF6QKGAl4zXypYj65mVI+B4AhFBwPtSN7pDrJeIx8Z7zdy4ntx+wQABom7w=="], - - "@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.1.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-lZVym0PuHE1KZ22gmFTC15lAkrg9iTszR617oYRB/iPY1A56ywoJzVKOJBKaot5RiikCObmur6pogpse3gRcng=="], - - "@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.1.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-t2DNiLJWNTbnEHyUzTumldML6ET4/g16467LZoDDJ3tSxGvguL5/NyC2lCsNKuyRycg9XeDQF5SSv+TNOhQEXg=="], - - "@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.1.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-0WIRnL1Uw4BvTZRLQt+PVgo6ZKTJadlC2btP+/EOXv2f/DWbY0rEgl+y834mIVwP1FkTlWVTrGGJXf12lru7EQ=="], - - "@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.1.4", "", { "os": "linux", "cpu": "x64" }, "sha512-JWtGshGfX+oENAKonoNkqEJX+7hC8yfhi9GUyPX1VX4mdh1y5r+ZiJLR5XzAB0aoP6s/PcILsGjKq8O0mm24bw=="], - - "@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.1.4", "", { "os": "linux", "cpu": "x64" }, "sha512-rT6yQcxUuXs4CnbofqwHRRV0iem349rLMYpTjkgQGLjrY4ado/eDzwPZPTCgTOlF6Nkp8NEv70yLMTn6qkWxsQ=="], - - "@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.1.4", "", { "os": "none", "cpu": "arm64" }, "sha512-KXMGoboq5cyaCQjDA4GLuRiOwBQ0EyFnJoVViLeZ45/3rFItRODEr+NdsBcVpll40hhNArlm/speWGRvj08LzA=="], - - "@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.1.4", "", { "dependencies": { "@emnapi/core": "1.11.1", "@emnapi/runtime": "1.11.1", "@napi-rs/wasm-runtime": "^1.1.6" }, "cpu": "none" }, "sha512-5K83rb36oJiY7BCyE9zLZtGcPV4g5wvq+xwdO0XPIwDVZI8cyB/AUjkNXGb92/rnmezEkjMOpgY61rtwjQtFwg=="], - - "@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.1.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-PnWBtw3TV5KOg69HQQDR0mnQuyCmSGR2pAB4DC1rPF808fgKeTUMj2EOEyKATpgiuxuR5APQmiDO7PDgEjTFSA=="], - - "@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.1.4", "", { "os": "win32", "cpu": "x64" }, "sha512-M1lpniBePobTfsa7Ks9a199e1akxsXn+GYBUKsEzv3YFzOm1HJAMNwKI3qr0Zq+mxwx9gOZoTdP1yXRYsZUocQ=="], - - "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.1", "", {}, "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw=="], + "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.27", "", {}, "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA=="], "@rollup/pluginutils": ["@rollup/pluginutils@5.4.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-MfPp06CjRLfXQ3wY0R8vJDYBy/MvVcc9OulEfR0B8Iv9ko+GCNaRZ+EpJYFl27LhKsZK0o420sYCRHCjfCgeUg=="], @@ -2692,8 +2488,6 @@ "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.60.4", "", { "os": "win32", "cpu": "x64" }, "sha512-QVTUovf40zgTqlFVrKA1uXMVvU2QWEFWfAH8Wdc48IxLvrJMQVMBRjuQyUpzZCDkakImib9eVazbWlC6ksWtJw=="], - "@scarf/scarf": ["@scarf/scarf@1.4.0", "", {}, "sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ=="], - "@selderee/plugin-htmlparser2": ["@selderee/plugin-htmlparser2@0.11.0", "", { "dependencies": { "domhandler": "^5.0.3", "selderee": "^0.11.0" } }, "sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ=="], "@sentry-internal/browser-utils": ["@sentry-internal/browser-utils@10.36.0", "", { "dependencies": { "@sentry/core": "10.36.0" } }, "sha512-WILVR8HQBWOxbqLRuTxjzRCMIACGsDTo6jXvzA8rz6ezElElLmIrn3CFAswrESLqEEUa4CQHl5bLgSVJCRNweA=="], @@ -2750,8 +2544,6 @@ "@shikijs/transformers": ["@shikijs/transformers@3.9.2", "", { "dependencies": { "@shikijs/core": "3.9.2", "@shikijs/types": "3.9.2" } }, "sha512-MW5hT4TyUp6bNAgTExRYLk1NNasVQMTCw1kgbxHcEC0O5cbepPWaB+1k+JzW9r3SP2/R8kiens8/3E6hGKfgsA=="], - "@shikijs/twoslash": ["@shikijs/twoslash@3.23.0", "", { "dependencies": { "@shikijs/core": "3.23.0", "@shikijs/types": "3.23.0", "twoslash": "^0.3.6" }, "peerDependencies": { "typescript": ">=5.5.0" } }, "sha512-pNaLJWMA3LU7PhT8tm9OQBZ1epy0jmdgeJzntBtr1EVXLbHxGzTj3mnf9vOdcl84l96qnlJXkJ/NGXZYBpXl5g=="], - "@shikijs/types": ["@shikijs/types@3.9.2", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-/M5L0Uc2ljyn2jKvj4Yiah7ow/W+DJSglVafvWAJ/b8AZDeeRAdMu3c2riDzB7N42VD+jSnWxeP9AKtd4TfYVw=="], "@shikijs/vscode-textmate": ["@shikijs/vscode-textmate@10.0.2", "", {}, "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg=="], @@ -2772,10 +2564,6 @@ "@sindresorhus/is": ["@sindresorhus/is@7.2.0", "", {}, "sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw=="], - "@sindresorhus/slugify": ["@sindresorhus/slugify@2.2.0", "", { "dependencies": { "@sindresorhus/transliterate": "^1.0.0", "escape-string-regexp": "^5.0.0" } }, "sha512-9Vybc/qX8Kj6pxJaapjkFbiUJPk7MAkCh/GFCxIBnnsuYCFPIXKvnLidG8xlepht3i24L5XemUmGtrJ3UWrl6w=="], - - "@sindresorhus/transliterate": ["@sindresorhus/transliterate@1.6.0", "", { "dependencies": { "escape-string-regexp": "^5.0.0" } }, "sha512-doH1gimEu3A46VX6aVxpHTeHrytJAG6HgdxntYnCFiIFHEM/ZGpG8KiZGBChchjQmG0XFIBL552kBTjVcMZXwQ=="], - "@slack/bolt": ["@slack/bolt@3.22.0", "", { "dependencies": { "@slack/logger": "^4.0.0", "@slack/oauth": "^2.6.3", "@slack/socket-mode": "^1.3.6", "@slack/types": "^2.13.0", "@slack/web-api": "^6.13.0", "@types/express": "^4.16.1", "@types/promise.allsettled": "^1.0.3", "@types/tsscmp": "^1.0.0", "axios": "^1.7.4", "express": "^4.21.0", "path-to-regexp": "^8.1.0", "promise.allsettled": "^1.0.2", "raw-body": "^2.3.3", "tsscmp": "^1.0.6" } }, "sha512-iKDqGPEJDnrVwxSVlFW6OKTkijd7s4qLBeSufoBsTM0reTyfdp/5izIQVkxNfzjHi3o6qjdYbRXkYad5HBsBog=="], "@slack/logger": ["@slack/logger@4.0.1", "", { "dependencies": { "@types/node": ">=18" } }, "sha512-6cmdPrV/RYfd2U0mDGiMK8S7OJqpCTm7enMLRR3edccsPX8j7zXTLnaEF4fhxxJJTAIOil6+qZrnUPTuaLvwrQ=="], @@ -2892,7 +2680,7 @@ "@solid-primitives/media": ["@solid-primitives/media@2.3.3", "", { "dependencies": { "@solid-primitives/event-listener": "^2.4.3", "@solid-primitives/rootless": "^1.5.2", "@solid-primitives/static-store": "^0.1.2", "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-hQ4hLOGvfbugQi5Eu1BFWAIJGIAzztq9x0h02xgBGl2l0Jaa3h7tg6bz5tV1NSuNYVGio4rPoa7zVQQLkkx9dA=="], - "@solid-primitives/memo": ["@solid-primitives/memo@1.5.1", "", { "dependencies": { "@solid-primitives/scheduled": "^1.5.3", "@solid-primitives/utils": "^6.4.1" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-VDPrkl9epp0tbby9MvsqphGFCYCtDRC5J8FKzTqHbQiG5hhR8n6xv4MfjhTW231IaBxxPHLxS43EE8c5Q23mSQ=="], + "@solid-primitives/memo": ["@solid-primitives/memo@1.5.0", "", { "dependencies": { "@solid-primitives/scheduled": "^1.5.3", "@solid-primitives/utils": "^6.4.0" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-nSMHpFdnOMP88t7lqtktUXJlhQdJk0BQs2v3jUqcn+OtFwUm27Oa/coKl1BHDxL/65jR5drjqxxvuhaIpiUi0w=="], "@solid-primitives/props": ["@solid-primitives/props@3.2.3", "", { "dependencies": { "@solid-primitives/utils": "^6.4.0" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-XzG6en9gSFwmvbKcATm2BxL63HegZ+BAG5fmHi8jyBppQHcaths7ffz+6vYvwYy3nlgLa20ufJLj7tst+PcHFA=="], @@ -2932,36 +2720,6 @@ "@standard-schema/spec": ["@standard-schema/spec@1.0.0", "", {}, "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA=="], - "@stoplight/better-ajv-errors": ["@stoplight/better-ajv-errors@1.0.3", "", { "dependencies": { "jsonpointer": "^5.0.0", "leven": "^3.1.0" }, "peerDependencies": { "ajv": ">=8" } }, "sha512-0p9uXkuB22qGdNfy3VeEhxkU5uwvp/KrBTAbrLBURv6ilxIVwanKwjMc41lQfIVgPGcOkmLbTolfFrSsueu7zA=="], - - "@stoplight/json": ["@stoplight/json@3.21.0", "", { "dependencies": { "@stoplight/ordered-object-literal": "^1.0.3", "@stoplight/path": "^1.3.2", "@stoplight/types": "^13.6.0", "jsonc-parser": "~2.2.1", "lodash": "^4.17.21", "safe-stable-stringify": "^1.1" } }, "sha512-5O0apqJ/t4sIevXCO3SBN9AHCEKKR/Zb4gaj7wYe5863jme9g02Q0n/GhM7ZCALkL+vGPTe4ZzTETP8TFtsw3g=="], - - "@stoplight/json-ref-readers": ["@stoplight/json-ref-readers@1.2.2", "", { "dependencies": { "node-fetch": "^2.6.0", "tslib": "^1.14.1" } }, "sha512-nty0tHUq2f1IKuFYsLM4CXLZGHdMn+X/IwEUIpeSOXt0QjMUbL0Em57iJUDzz+2MkWG83smIigNZ3fauGjqgdQ=="], - - "@stoplight/json-ref-resolver": ["@stoplight/json-ref-resolver@3.1.6", "", { "dependencies": { "@stoplight/json": "^3.21.0", "@stoplight/path": "^1.3.2", "@stoplight/types": "^12.3.0 || ^13.0.0", "@types/urijs": "^1.19.19", "dependency-graph": "~0.11.0", "fast-memoize": "^2.5.2", "immer": "^9.0.6", "lodash": "^4.17.21", "tslib": "^2.6.0", "urijs": "^1.19.11" } }, "sha512-YNcWv3R3n3U6iQYBsFOiWSuRGE5su1tJSiX6pAPRVk7dP0L7lqCteXGzuVRQ0gMZqUl8v1P0+fAKxF6PLo9B5A=="], - - "@stoplight/ordered-object-literal": ["@stoplight/ordered-object-literal@1.0.5", "", {}, "sha512-COTiuCU5bgMUtbIFBuyyh2/yVVzlr5Om0v5utQDgBCuQUOPgU1DwoffkTfg4UBQOvByi5foF4w4T+H9CoRe5wg=="], - - "@stoplight/path": ["@stoplight/path@1.3.2", "", {}, "sha512-lyIc6JUlUA8Ve5ELywPC8I2Sdnh1zc1zmbYgVarhXIp9YeAB0ReeqmGEOWNtlHkbP2DAA1AL65Wfn2ncjK/jtQ=="], - - "@stoplight/spectral-core": ["@stoplight/spectral-core@1.23.1", "", { "dependencies": { "@scarf/scarf": "^1.4.0", "@stoplight/better-ajv-errors": "1.0.3", "@stoplight/json": "~3.21.0", "@stoplight/path": "1.3.2", "@stoplight/spectral-parsers": "^1.0.0", "@stoplight/spectral-ref-resolver": "^1.0.4", "@stoplight/spectral-runtime": "^1.1.2", "@stoplight/types": "~13.6.0", "@types/es-aggregate-error": "^1.0.2", "@types/json-schema": "^7.0.11", "ajv": "^8.18.0", "ajv-errors": "~3.0.0", "ajv-formats": "~2.1.1", "es-aggregate-error": "^1.0.7", "expr-eval-fork": "^3.0.1", "jsonpath-plus": "^10.3.0", "lodash": "^4.18.1", "lodash.topath": "^4.5.2", "minimatch": "^3.1.4", "nimma": "0.2.3", "pony-cause": "^1.1.1", "tslib": "^2.8.1" } }, "sha512-VLC8OhpO/pMJKb6IHhurxJjXO1qB56Ng1unIb8b+hNxdw0+SEcASvmR+RpjfHYX/jv/DfSaA1x8QhFBJBmqBOQ=="], - - "@stoplight/spectral-formats": ["@stoplight/spectral-formats@1.8.5", "", { "dependencies": { "@scarf/scarf": "^1.4.0", "@stoplight/json": "^3.17.0", "@stoplight/spectral-core": "^1.23.0", "@types/json-schema": "^7.0.7", "tslib": "^2.8.1" } }, "sha512-xaC0rCH0p7/bzNJsz+JgLSj+Cp6uwYGWpePQxdLkF2G6a8Zyp3OyS7umkGYNiimEwKrOjvCNNTFJpeuiENZSBA=="], - - "@stoplight/spectral-functions": ["@stoplight/spectral-functions@1.10.5", "", { "dependencies": { "@scarf/scarf": "^1.4.0", "@stoplight/better-ajv-errors": "1.0.3", "@stoplight/json": "^3.17.1", "@stoplight/spectral-core": "^1.23.0", "@stoplight/spectral-formats": "^1.8.1", "@stoplight/spectral-runtime": "^1.1.2", "ajv": "^8.18.0", "ajv-draft-04": "~1.0.0", "ajv-errors": "~3.0.0", "ajv-formats": "~2.1.1", "lodash": "^4.18.1", "tslib": "^2.8.1" } }, "sha512-vDCd0NJ93715bcUpZZ5vNHiyxd4cgHF6tuXsDiXOXKAByg+I1fR5/dMijEo6Ce1Lz95a+RZ22JKYhF1YuzVvuA=="], - - "@stoplight/spectral-parsers": ["@stoplight/spectral-parsers@1.0.5", "", { "dependencies": { "@stoplight/json": "~3.21.0", "@stoplight/types": "^14.1.1", "@stoplight/yaml": "~4.3.0", "tslib": "^2.8.1" } }, "sha512-ANDTp2IHWGvsQDAY85/jQi9ZrF4mRrA5bciNHX+PUxPr4DwS6iv4h+FVWJMVwcEYdpyoIdyL+SRmHdJfQEPmwQ=="], - - "@stoplight/spectral-ref-resolver": ["@stoplight/spectral-ref-resolver@1.0.5", "", { "dependencies": { "@stoplight/json-ref-readers": "1.2.2", "@stoplight/json-ref-resolver": "~3.1.6", "@stoplight/spectral-runtime": "^1.1.2", "dependency-graph": "0.11.0", "tslib": "^2.8.1" } }, "sha512-gj3TieX5a9zMW29z3mBlAtDOCgN3GEc1VgZnCVlr5irmR4Qi5LuECuFItAq4pTn5Zu+sW5bqutsCH7D4PkpyAA=="], - - "@stoplight/spectral-runtime": ["@stoplight/spectral-runtime@1.1.6", "", { "dependencies": { "@stoplight/json": "^3.20.1", "@stoplight/path": "^1.3.2", "@stoplight/types": "^13.6.0", "lodash": "^4.18.1", "node-fetch": "^2.7.0", "tslib": "^2.8.1" } }, "sha512-Y8rEDyMN4bSMJCrDs2shdcVHYyCnH3FvXRP4dBhha4Z8iJv+JPp7KqOV/hwVB/hWFC209upiwj2oDmLfR0qCDg=="], - - "@stoplight/types": ["@stoplight/types@13.20.0", "", { "dependencies": { "@types/json-schema": "^7.0.4", "utility-types": "^3.10.0" } }, "sha512-2FNTv05If7ib79VPDA/r9eUet76jewXFH2y2K5vuge6SXbRHtWBhcaRmu+6QpF4/WRNoJj5XYRSwLGXDxysBGA=="], - - "@stoplight/yaml": ["@stoplight/yaml@4.3.0", "", { "dependencies": { "@stoplight/ordered-object-literal": "^1.0.5", "@stoplight/types": "^14.1.1", "@stoplight/yaml-ast-parser": "0.0.50", "tslib": "^2.2.0" } }, "sha512-JZlVFE6/dYpP9tQmV0/ADfn32L9uFarHWxfcRhReKUnljz1ZiUM5zpX+PH8h5CJs6lao3TuFqnPm9IJJCEkE2w=="], - - "@stoplight/yaml-ast-parser": ["@stoplight/yaml-ast-parser@0.0.50", "", {}, "sha512-Pb6M8TDO9DtSVla9yXSTAxmo9GVEouq5P40DWXdOie69bXogZTkgvopCq+yEvTMA0F6PEvdJmbtTV3ccIp11VQ=="], - "@storybook/addon-a11y": ["@storybook/addon-a11y@10.4.1", "", { "dependencies": { "@storybook/global": "^5.0.0", "axe-core": "^4.2.0" }, "peerDependencies": { "storybook": "^10.4.1" } }, "sha512-MGft/IXjJ20a9KbaSVG9bHTAAoanbucKrgEiJJRNqpim8DsXA01+XTdSk17LmiOCB203Rrq9mWgdQ6+79cc8iA=="], "@storybook/addon-docs": ["@storybook/addon-docs@10.4.1", "", { "dependencies": { "@mdx-js/react": "^3.0.0", "@storybook/csf-plugin": "10.4.1", "@storybook/icons": "^2.0.2", "@storybook/react-dom-shim": "10.4.1", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "ts-dedent": "^2.0.0" }, "peerDependencies": { "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "storybook": "^10.4.1" }, "optionalPeers": ["@types/react"] }, "sha512-IYqUdjoZe4VO2LFZlKL/gwy7DsQSWCq6hX+zc1MBmZo04yycDASk1tte57n9pdlW3ajw9yYMF/+lVBi+xQjyvw=="], @@ -3028,9 +2786,9 @@ "@tanstack/solid-query": ["@tanstack/solid-query@5.91.4", "", { "dependencies": { "@tanstack/query-core": "5.91.2" }, "peerDependencies": { "solid-js": "^1.6.0" } }, "sha512-oCEgn8iT7WnF/7ISd7usBpUK1C9EdvQfg8ZUpKNKZ4edVClICZrCX6f3/Bp8ZlwQnL21KLc2rp+CejEuehlRxg=="], - "@tanstack/solid-virtual": ["@tanstack/solid-virtual@3.13.28", "", { "dependencies": { "@tanstack/virtual-core": "3.17.0" }, "peerDependencies": { "solid-js": "^1.3.0" } }, "sha512-kRuOEL5orH/rzGgxNgfgOttsgV6cgrUeupVtrHMITb5p0rZ3hnxhbu/lhKcR9+7x+EJdfUtJIb2CVC85mlw15g=="], + "@tanstack/solid-virtual": ["@tanstack/solid-virtual@3.13.32", "", { "dependencies": { "@tanstack/virtual-core": "3.17.3" }, "peerDependencies": { "solid-js": "^1.3.0" } }, "sha512-yhX4A4Kgn+wyTg6Mmu8+zwoMTwjz4K1ucvLfRJ8f0rPGDDAIqSaf0v6oU0yT9+SvrjmUaZQ0VX7g4byexbhNng=="], - "@tanstack/virtual-core": ["@tanstack/virtual-core@3.17.0", "", {}, "sha512-gOxY/hFkPh/XQYhnThBHzkbkX3Ed+z/iushyz+R+JAr213aXxUDgQoTgTdrDpBSRsjFM73P/KfUyWmaF9WHMkQ=="], + "@tanstack/virtual-core": ["@tanstack/virtual-core@3.17.3", "", {}, "sha512-8Np/TFELpI0ySuJoVmjvOrQYXH/8sTX0Biv9szhFhY39xOdAAY+smrMxjxOum/ux3eM8MUJQsEJ0/R0UpvC8dw=="], "@testing-library/dom": ["@testing-library/dom@10.4.1", "", { "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", "@types/aria-query": "^5.0.1", "aria-query": "5.3.0", "dom-accessibility-api": "^0.5.9", "lz-string": "^1.5.0", "picocolors": "1.1.1", "pretty-format": "^27.0.2" } }, "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg=="], @@ -3040,8 +2798,6 @@ "@thisbeyond/solid-dnd": ["@thisbeyond/solid-dnd@0.7.5", "", { "peerDependencies": { "solid-js": "^1.5" } }, "sha512-DfI5ff+yYGpK9M21LhYwIPlbP2msKxN2ARwuu6GF8tT1GgNVDTI8VCQvH4TJFoVApP9d44izmAcTh/iTCH2UUw=="], - "@tootallnate/quickjs-emscripten": ["@tootallnate/quickjs-emscripten@0.23.0", "", {}, "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA=="], - "@tsconfig/bun": ["@tsconfig/bun@1.0.9", "", {}, "sha512-4M0/Ivfwcpz325z6CwSifOBZYji3DFOEpY6zEUt0+Xi2qRhzwvmqQN9XAHJh3OVvRJuAqVTLU2abdCplvp6mwQ=="], "@tsconfig/node22": ["@tsconfig/node22@22.0.2", "", {}, "sha512-Kmwj4u8sDRDrMYRoN9FDEcXD8UpBSaPQQ24Gz+Gamqfm7xxn+GBR7ge/Z7pK8OXNGyUzbSwJj+TH6B+DS/epyA=="], @@ -3064,8 +2820,6 @@ "@tybys/wasm-util": ["@tybys/wasm-util@0.10.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg=="], - "@types/acorn": ["@types/acorn@4.0.6", "", { "dependencies": { "@types/estree": "*" } }, "sha512-veQTnWP+1D/xbxVrPC3zHnCZRjSrKfhbMUlEA43iMZLu7EsnTtkJklIuwrCPbOi8YkvDQAiW05VQQFvvz9oieQ=="], - "@types/aria-query": ["@types/aria-query@5.0.4", "", {}, "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw=="], "@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="], @@ -3090,8 +2844,6 @@ "@types/connect": ["@types/connect@3.4.38", "", { "dependencies": { "@types/node": "*" } }, "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug=="], - "@types/cors": ["@types/cors@2.8.19", "", { "dependencies": { "@types/node": "*" } }, "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg=="], - "@types/cross-spawn": ["@types/cross-spawn@6.0.6", "", { "dependencies": { "@types/node": "*" } }, "sha512-fXRhhUkG4H3TQk5dBhQ7m/JDdSNHKwR2BBia62lhwEIq9xGiQKLxd6LymNhn47SjXhsUEPmxi+PKw2OkW4LLjA=="], "@types/d3-geo": ["@types/d3-geo@3.1.0", "", { "dependencies": { "@types/geojson": "*" } }, "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ=="], @@ -3104,8 +2856,6 @@ "@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="], - "@types/es-aggregate-error": ["@types/es-aggregate-error@1.0.6", "", { "dependencies": { "@types/node": "*" } }, "sha512-qJ7LIFp06h1QE1aVxbVd+zJP2wdaugYXYfd6JxsyRMrYHaxb6itXPogW2tz+ylUJ1n1b+JF1PHyYCfYHm0dvUg=="], - "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], "@types/estree-jsx": ["@types/estree-jsx@1.0.5", "", { "dependencies": { "@types/estree": "*" } }, "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg=="], @@ -3182,8 +2932,6 @@ "@types/react": ["@types/react@18.0.25", "", { "dependencies": { "@types/prop-types": "*", "@types/scheduler": "*", "csstype": "^3.0.2" } }, "sha512-xD6c0KDT4m7n9uD4ZHi02lzskaiqcBxf4zi+tXZY98a04wvc0hi/TcCPC2FOESZi51Nd7tlUeOJY8RofL799/g=="], - "@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="], - "@types/responselike": ["@types/responselike@1.0.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw=="], "@types/retry": ["@types/retry@0.12.0", "", {}, "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA=="], @@ -3214,8 +2962,6 @@ "@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], - "@types/urijs": ["@types/urijs@1.19.26", "", {}, "sha512-wkXrVzX5yoqLnndOwFsieJA7oKM8cNkOKJtf/3vVGSUFkWDKZvFHpIl9Pvqb/T9UsawBBFMTTD8xu7sK5MWuvg=="], - "@types/verror": ["@types/verror@1.10.11", "", {}, "sha512-RlDm9K7+o5stv0Co8i8ZRGxDbrTxhJtgjqjFyVh/tXQyl/rYtTKlnTvZ88oSTeYREWurwx20Js4kTuKCsFkUtg=="], "@types/whatwg-mimetype": ["@types/whatwg-mimetype@3.0.2", "", {}, "sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA=="], @@ -3256,12 +3002,6 @@ "@valibot/to-json-schema": ["@valibot/to-json-schema@1.6.0", "", { "peerDependencies": { "valibot": "^1.3.0" } }, "sha512-d6rYyK5KVa2XdqamWgZ4/Nr+cXhxjy7lmpe6Iajw15J/jmU+gyxl2IEd1Otg1d7Rl3gOQL5reulnSypzBtYy1A=="], - "@vercel/cli-config": ["@vercel/cli-config@0.2.0", "", { "dependencies": { "xdg-app-paths": "5", "zod": "4.1.11" } }, "sha512-fJRRRB7734BDuXZ89yBEaA2ncYhH7bWX30mk04W80J6VAfQc+4iB8lyzAdaGpFV3/vNlkt9VZt+/uoQoWX6UsQ=="], - - "@vercel/cli-exec": ["@vercel/cli-exec@1.0.0", "", { "dependencies": { "execa": "5.1.1" } }, "sha512-kQF8LGie/Hbdq9/psJxLE7owRTcqMQMhgybU04gCeR7cbQAr5t8OrjefDNColJv1QSSucFt4pLwRiARVmlOnug=="], - - "@vercel/functions": ["@vercel/functions@3.7.5", "", { "dependencies": { "@vercel/oidc": "3.8.0" }, "peerDependencies": { "@aws-sdk/credential-provider-web-identity": "*", "ws": ">=8" }, "optionalPeers": ["@aws-sdk/credential-provider-web-identity", "ws"] }, "sha512-ESf8BbeDebqRUyMi09JwRbQqpLn4g6fjcVVHPsHB56j2dSqRrSHO4h3X4aaxJf6iQQjzhAtDGI2xCWQ27JE8PA=="], - "@vercel/oidc": ["@vercel/oidc@3.2.0", "", {}, "sha512-UycprH3T6n3jH0k44NHMa7pnFHGu/N05MjojYr+Mc6I7obkoLIJujSWwin1pCvdy/eOxrI/l3uDLQsmcrOb4ug=="], "@vitejs/plugin-react": ["@vitejs/plugin-react@4.7.0", "", { "dependencies": { "@babel/core": "^7.28.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-beta.27", "@types/babel__core": "^7.20.5", "react-refresh": "^0.17.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA=="], @@ -3312,7 +3052,7 @@ "abstract-logging": ["abstract-logging@2.0.1", "", {}, "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA=="], - "accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="], + "accepts": ["accepts@1.3.8", "", { "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" } }, "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw=="], "acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="], @@ -3320,16 +3060,10 @@ "acorn-walk": ["acorn-walk@8.3.2", "", {}, "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A=="], - "address": ["address@1.2.2", "", {}, "sha512-4B/qKCfeE/ODUaAUpSwfzazo5x29WD4r3vXiWsB7I2mSDAihwEqKO+g8GELZUQSSAo5e1XTYh3ZVfLyxBc12nA=="], - - "adm-zip": ["adm-zip@0.5.16", "", {}, "sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ=="], - "agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], "agentkeepalive": ["agentkeepalive@4.6.0", "", { "dependencies": { "humanize-ms": "^1.2.1" } }, "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ=="], - "aggregate-error": ["aggregate-error@4.0.1", "", { "dependencies": { "clean-stack": "^4.0.0", "indent-string": "^5.0.0" } }, "sha512-0poP0T7el6Vq3rstR8Mn4V/IQrpBLO6POkUSrN7RhyY+GF/InCFShQzsQ39T25gkHhLgSLByyAz+Kjb+c2L98w=="], - "ai": ["ai@6.0.168", "", { "dependencies": { "@ai-sdk/gateway": "3.0.104", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-2HqCJuO+1V2aV7vfYs5LFEUfxbkGX+5oa54q/gCCTL7KLTdbxcCu5D7TdLA5kwsrs3Szgjah9q6D9tpjHM3hUQ=="], "ai-gateway-provider": ["ai-gateway-provider@3.1.2", "", { "optionalDependencies": { "@ai-sdk/amazon-bedrock": "^4.0.62", "@ai-sdk/anthropic": "^3.0.46", "@ai-sdk/azure": "^3.0.31", "@ai-sdk/cerebras": "^2.0.34", "@ai-sdk/cohere": "^3.0.21", "@ai-sdk/deepgram": "^2.0.20", "@ai-sdk/deepseek": "^2.0.20", "@ai-sdk/elevenlabs": "^2.0.20", "@ai-sdk/fireworks": "^2.0.34", "@ai-sdk/google": "^3.0.30", "@ai-sdk/google-vertex": "^4.0.61", "@ai-sdk/groq": "^3.0.24", "@ai-sdk/mistral": "^3.0.20", "@ai-sdk/openai": "^3.0.30", "@ai-sdk/perplexity": "^3.0.19", "@ai-sdk/xai": "^3.0.57", "@openrouter/ai-sdk-provider": "^2.2.3" }, "peerDependencies": { "@ai-sdk/openai-compatible": "^2.0.0", "@ai-sdk/provider": "^3.0.0", "@ai-sdk/provider-utils": "^4.0.0", "ai": "^6.0.0" } }, "sha512-krGNnJSoO/gJ7Hbe5nQDlsBpDUGIBGtMQTRUaW7s1MylsfvLduba0TLWzQaGtOmNRkP0pGhtGlwsnS6FNQMlyw=="], @@ -3338,8 +3072,6 @@ "ajv-draft-04": ["ajv-draft-04@1.0.0", "", { "peerDependencies": { "ajv": "^8.5.0" }, "optionalPeers": ["ajv"] }, "sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw=="], - "ajv-errors": ["ajv-errors@3.0.0", "", { "peerDependencies": { "ajv": "^8.0.1" } }, "sha512-V3wD15YHfHz6y0KdhYFjyy9vWtEVALT9UrxfN3zqlI6dMioHnJrqOYfyPKol3oqrnCM9uwkcdCwkJ0WUcbLMTQ=="], - "ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="], "ajv-keywords": ["ajv-keywords@3.5.2", "", { "peerDependencies": { "ajv": "^6.9.1" } }, "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ=="], @@ -3348,8 +3080,6 @@ "ansi-colors": ["ansi-colors@4.1.3", "", {}, "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw=="], - "ansi-escapes": ["ansi-escapes@7.3.0", "", { "dependencies": { "environment": "^1.0.0" } }, "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg=="], - "ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], @@ -3378,10 +3108,6 @@ "aria-query": ["aria-query@5.3.2", "", {}, "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw=="], - "arkregex": ["arkregex@0.0.3", "", { "dependencies": { "@ark/util": "0.55.0" } }, "sha512-bU21QJOJEFJK+BPNgv+5bVXkvRxyAvgnon75D92newgHxkBJTgiFwQxusyViYyJkETsddPlHyspshDQcCzmkNg=="], - - "arktype": ["arktype@2.1.27", "", { "dependencies": { "@ark/schema": "0.55.0", "@ark/util": "0.55.0", "arkregex": "0.0.3" } }, "sha512-enctOHxI4SULBv/TDtCVi5M8oLd4J5SVlPUblXDzSsOYQNMzmVbUosGBnJuZDKmFlN5Ie0/QVEuTE+Z5X1UhsQ=="], - "array-buffer-byte-length": ["array-buffer-byte-length@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "is-array-buffer": "^3.0.5" } }, "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw=="], "array-flatten": ["array-flatten@1.1.1", "", {}, "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="], @@ -3426,14 +3152,10 @@ "atomically": ["atomically@2.1.1", "", { "dependencies": { "stubborn-fs": "^2.0.0", "when-exit": "^2.1.4" } }, "sha512-P4w9o2dqARji6P7MHprklbfiArZAWvo07yW7qs3pdljb3BWr12FIB7W+p0zJiuiVsUpRO0iZn1kFFcpPegg0tQ=="], - "auto-bind": ["auto-bind@5.0.1", "", {}, "sha512-ooviqdwwgfIfNmDwo94wlshcdzfO64XV0Cg6oDsDYBJfITDz1EngD2z7DkbvCWn+XIMsIqW27sEVF6qcpJrRcg=="], - "autoprefixer": ["autoprefixer@10.5.0", "", { "dependencies": { "browserslist": "^4.28.2", "caniuse-lite": "^1.0.30001787", "fraction.js": "^5.3.4", "picocolors": "^1.1.1", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.1.0" }, "bin": { "autoprefixer": "bin/autoprefixer" } }, "sha512-FMhOoZV4+qR6aTUALKX2rEqGG+oyATvwBt9IIzVR5rMa2HRWPkxf+P+PAJLD1I/H5/II+HuZcBJYEFBpq39ong=="], "available-typed-arrays": ["available-typed-arrays@1.0.7", "", { "dependencies": { "possible-typed-array-names": "^1.0.0" } }, "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ=="], - "avsc": ["avsc@5.7.9", "", {}, "sha512-yOA4wFeI7ET3v32Di/sUybQ+ttP20JHSW3mxLuNGeO0uD6PPcvLrIQXSvy/rhJOWU5JrYh7U4OHplWMmtAtjMg=="], - "avvio": ["avvio@9.2.0", "", { "dependencies": { "@fastify/error": "^4.0.0", "fastq": "^1.17.1" } }, "sha512-2t/sy01ArdHHE0vRH5Hsay+RtCZt3dLPji7W7/MMOCEgze5b7SNDC4j5H6FnVgPkI1MTNFGzHdHrVXDDl7QSSQ=="], "aws-ssl-profiles": ["aws-ssl-profiles@1.1.2", "", {}, "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g=="], @@ -3478,20 +3200,14 @@ "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], - "base64id": ["base64id@2.0.0", "", {}, "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog=="], - "baseline-browser-mapping": ["baseline-browser-mapping@2.10.33", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-bA6+tcSLpz2tIEdDXZPpPTIuxBcC4+w6SieaYyfigIa4h8GlFxbA17v22Vx3JUtuZQj9SgOsnbK+aTBzyDyEuw=="], - "basic-ftp": ["basic-ftp@5.3.1", "", {}, "sha512-bopVNp6ugyA150DDuZfPFdt1KZ5a94ZDiwX4hMgZDzF+GttD80lEy8kj98kbyhLXnPvhtIo93mdnLIjpCAeeOw=="], - "bcp-47": ["bcp-47@2.1.0", "", { "dependencies": { "is-alphabetical": "^2.0.0", "is-alphanumerical": "^2.0.0", "is-decimal": "^2.0.0" } }, "sha512-9IIS3UPrvIa1Ej+lVDdDwO7zLehjqsaByECw0bu2RRGP73jALm6FYbzI5gWbgHLvNdkvfXB5YrSbocZdOS0c0w=="], "bcp-47-match": ["bcp-47-match@2.0.3", "", {}, "sha512-JtTezzbAibu8G0R9op9zb3vcWZd9JF6M0xOYGPn0fNCd7wOpRB1mU2mH9T8gaBGbAAyIIVgB2G7xG0GP98zMAQ=="], "before-after-hook": ["before-after-hook@2.2.3", "", {}, "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ=="], - "better-opn": ["better-opn@3.0.2", "", { "dependencies": { "open": "^8.0.4" } }, "sha512-aVNobHnJqLiUelTaHat9DZ1qM2w0C0Eym4LPI/3JxOnSokGVdsl1T1kN7TFvsEAD8G47A6VKQ0TVHqbBnYMJlQ=="], - "bignumber.js": ["bignumber.js@9.3.1", "", {}, "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ=="], "bin-links": ["bin-links@6.0.2", "", { "dependencies": { "cmd-shim": "^8.0.0", "npm-normalize-package-bin": "^5.0.0", "proc-log": "^6.0.0", "read-cmd-shim": "^6.0.0", "write-file-atomic": "^7.0.0" } }, "sha512-frE1t78WOwJ45PKV2cF2tNPjTcs9L1J9s6VkrV59wanRP4GlaomuxYPVma7BwthMg8WnfSory4w5PTE6FZZ81w=="], @@ -3500,15 +3216,13 @@ "binary-extensions": ["binary-extensions@2.3.0", "", {}, "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="], - "bl": ["bl@4.1.0", "", { "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", "readable-stream": "^3.4.0" } }, "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w=="], - "blake3-wasm": ["blake3-wasm@2.1.5", "", {}, "sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g=="], "blob-to-buffer": ["blob-to-buffer@1.2.9", "", {}, "sha512-BF033y5fN6OCofD3vgHmNtwZWRcq9NLyyxyILx9hfMy1sXYy4ojFl765hJ2lP0YaN2fuxPaLO2Vzzoxy0FLFFA=="], "bluebird": ["bluebird@3.7.2", "", {}, "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg=="], - "body-parser": ["body-parser@2.2.2", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.1", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA=="], + "body-parser": ["body-parser@1.20.5", "", { "dependencies": { "bytes": "~3.1.2", "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "~1.2.0", "http-errors": "~2.0.1", "iconv-lite": "~0.4.24", "on-finished": "~2.4.1", "qs": "~6.15.1", "raw-body": "~2.5.3", "type-is": "~1.6.18", "unpipe": "~1.0.0" } }, "sha512-3grm+/2tUOvu2cjJkvsIxrv/wVpfXQW4PsQHYm7yk4vfpu7Ekl6nEsYBoJUL6qDwZUx8wUhQ8tR2qz+ad9c9OA=="], "bonjour-service": ["bonjour-service@1.3.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "multicast-dns": "^7.2.5" } }, "sha512-3YuAUiSkWykd+2Azjgyxei8OWf8thdn8AITIog2M4UICzoqfjlqr64WIjEXZllf/W6vK1goqleSR6brGomxQqA=="], @@ -3572,8 +3286,6 @@ "call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="], - "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="], - "camel-case": ["camel-case@4.1.2", "", { "dependencies": { "pascal-case": "^3.1.2", "tslib": "^2.0.3" } }, "sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw=="], "camelcase": ["camelcase@8.0.0", "", {}, "sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA=="], @@ -3598,8 +3310,6 @@ "character-reference-invalid": ["character-reference-invalid@2.0.1", "", {}, "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw=="], - "chardet": ["chardet@2.2.0", "", {}, "sha512-rddelWYNPRrXq6PtNEN2S3f6t9ILzvqaN5pVgi4kqt9jHQaXIial9PznB5iSPVlQSLNaaH22ItWz3EJtQ10+OA=="], - "chart.js": ["chart.js@4.5.1", "", { "dependencies": { "@kurkle/color": "^0.3.0" } }, "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw=="], "check-error": ["check-error@2.1.3", "", {}, "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA=="], @@ -3612,8 +3322,6 @@ "chownr": ["chownr@3.0.0", "", {}, "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g=="], - "chromium-bidi": ["chromium-bidi@0.6.2", "", { "dependencies": { "mitt": "3.0.1", "urlpattern-polyfill": "10.0.0", "zod": "3.23.8" }, "peerDependencies": { "devtools-protocol": "*" } }, "sha512-4WVBa6ijmUTVr9cZD4eicQD8Mdy/HCX3bzEIYYpmk0glqYLoWH+LqQEvV9RpDRzoQSbY1KJHloYXbDMXMbDPhg=="], - "chromium-pickle-js": ["chromium-pickle-js@0.2.0", "", {}, "sha512-1R5Fho+jBq0DDydt+/vHWj5KJNJCKdARKOCwZUen84I5BreWoLqRLANH1U87eJy1tiASPtMnGqJJq0ZsLoRPOw=="], "ci-info": ["ci-info@4.4.0", "", {}, "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg=="], @@ -3624,18 +3332,12 @@ "clean-css": ["clean-css@5.3.3", "", { "dependencies": { "source-map": "~0.6.0" } }, "sha512-D5J+kHaVb/wKSFcyyV75uCn8fiY4sV38XJoe4CUyGQ+mOU/fMVYUdH1hJC+CJQ5uY3EnW27SbJYS4X8BiLrAFg=="], - "clean-stack": ["clean-stack@4.2.0", "", { "dependencies": { "escape-string-regexp": "5.0.0" } }, "sha512-LYv6XPxoyODi36Dp976riBtSY27VmFo+MKqEU9QCCWyTrdEPDog+RWA7xQWHi6Vbp61j5c4cdzzX1NidnwtUWg=="], - "cli-boxes": ["cli-boxes@3.0.0", "", {}, "sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g=="], - "cli-cursor": ["cli-cursor@4.0.0", "", { "dependencies": { "restore-cursor": "^4.0.0" } }, "sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg=="], - "cli-spinners": ["cli-spinners@3.4.0", "", {}, "sha512-bXfOC4QcT1tKXGorxL3wbJm6XJPDqEnij2gQ2m7ESQuE+/z9YFIWnl/5RpTiKWbMq3EVKR4fRLJGn6DVfu0mpw=="], "cli-truncate": ["cli-truncate@4.0.0", "", { "dependencies": { "slice-ansi": "^5.0.0", "string-width": "^7.0.0" } }, "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA=="], - "cli-width": ["cli-width@4.1.0", "", {}, "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ=="], - "clipboardy": ["clipboardy@4.0.0", "", { "dependencies": { "execa": "^8.0.1", "is-wsl": "^3.1.0", "is64bit": "^2.0.0" } }, "sha512-5mOlNS0mhX0707P2I0aZ2V/cmHUEO/fL7VFLqszkhUsxt7RwnmrInf/eEQKlf5GzvYeHIjT+Ov1HRfNmymlG0w=="], "cliui": ["cliui@9.0.1", "", { "dependencies": { "string-width": "^7.2.0", "strip-ansi": "^7.1.0", "wrap-ansi": "^9.0.0" } }, "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w=="], @@ -3652,14 +3354,10 @@ "cmd-shim": ["cmd-shim@8.0.0", "", {}, "sha512-Jk/BK6NCapZ58BKUxlSI+ouKRbjH1NLZCgJkYoab+vEHUY3f6OzpNBN9u7HFSv9J6TRDGs4PLOHezoKGaFRSCA=="], - "code-excerpt": ["code-excerpt@4.0.0", "", { "dependencies": { "convert-to-spaces": "^2.0.1" } }, "sha512-xxodCmBen3iy2i0WtAK8FlFNrRzjUqjRsMfho58xT/wvZU1YTM3fCnRjcy1gJPMepaRlgm/0e6w8SpWHpn3/cA=="], - "collapse-white-space": ["collapse-white-space@2.1.0", "", {}, "sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw=="], "color": ["color@4.2.3", "", { "dependencies": { "color-convert": "^2.0.1", "color-string": "^1.9.0" } }, "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A=="], - "color-blend": ["color-blend@4.0.0", "", {}, "sha512-fYODTHhI/NG+B5GnzvuL3kiFrK/UnkUezWFTgEPBTY5V+kpyfAn95Vn9sJeeCX6omrCOdxnqCL3CvH+6sXtIbw=="], - "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], @@ -3692,26 +3390,22 @@ "consola": ["consola@3.4.2", "", {}, "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA=="], - "content-disposition": ["content-disposition@1.1.0", "", {}, "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g=="], + "content-disposition": ["content-disposition@0.5.4", "", { "dependencies": { "safe-buffer": "5.2.1" } }, "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ=="], "content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="], "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], - "convert-to-spaces": ["convert-to-spaces@2.0.1", "", {}, "sha512-rcQ1bsQO9799wq24uE5AM2tAILy4gXGIK/njFWcVQkGNZ96edlpY+A7bjwvzjYvLDyzmG1MmMLZhpcsb+klNMQ=="], - "cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="], "cookie-es": ["cookie-es@2.0.1", "", {}, "sha512-aVf4A4hI2w70LnF7GG+7xDQUkliwiXWXFvTjkip4+b64ygDQ2sJPRSKFDHbxn8o0xu9QzPkMuuiWIXyFSE2slA=="], - "cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="], + "cookie-signature": ["cookie-signature@1.0.7", "", {}, "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA=="], "core-util-is": ["core-util-is@1.0.3", "", {}, "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="], "cors": ["cors@2.8.6", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw=="], - "cosmiconfig": ["cosmiconfig@9.0.2", "", { "dependencies": { "env-paths": "^2.2.1", "import-fresh": "^3.3.0", "js-yaml": "^4.1.0", "parse-json": "^5.2.0" }, "peerDependencies": { "typescript": ">=4.9.5" }, "optionalPeers": ["typescript"] }, "sha512-gtTZxTDau1wL7Y7zifc2dd8jHSK/k6BTx/2Xp/BpdlAdnlYWFVt7qhJqgwi7637yRwRQ3qL4ZidbB4I8tA5VOg=="], - "crc": ["crc@3.8.0", "", { "dependencies": { "buffer": "^5.1.0" } }, "sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ=="], "crc-32": ["crc-32@1.2.2", "", { "bin": { "crc32": "bin/crc32.njs" } }, "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ=="], @@ -3738,8 +3432,6 @@ "cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="], - "cssfilter": ["cssfilter@0.0.10", "", {}, "sha512-FAaLDaplstoRsDR8XGYH51znUN0UY7nMc6Z9/fvE8EXGwvJE9hu7W2vHwx1+bd6gCYnln9nLbzxFTrcO9YQDZw=="], - "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], "d3-array": ["d3-array@3.2.4", "", { "dependencies": { "internmap": "1 - 2" } }, "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg=="], @@ -3774,18 +3466,12 @@ "decimal.js": ["decimal.js@10.5.0", "", {}, "sha512-8vDa8Qxvr/+d94hSh5P3IJwI5t8/c0KsMp+g8bNw9cY2icONa5aPfvKeieW1WlG0WQYwwhJ7mjui2xtiePQSXw=="], - "decode-bmp": ["decode-bmp@0.2.1", "", { "dependencies": { "@canvas/image-data": "^1.0.0", "to-data-view": "^1.1.0" } }, "sha512-NiOaGe+GN0KJqi2STf24hfMkFitDUaIoUU3eKvP/wAbLe8o6FuW5n/x7MHPR0HKvBokp6MQY/j7w8lewEeVCIA=="], - - "decode-ico": ["decode-ico@0.4.1", "", { "dependencies": { "@canvas/image-data": "^1.0.0", "decode-bmp": "^0.2.0", "to-data-view": "^1.1.0" } }, "sha512-69NZfbKIzux1vBOd31al3XnMnH+2mqDhEgLdpygErm4d60N+UwA5Sq5WFjmEDQzumgB9fElojGwWG0vybVfFmA=="], - "decode-named-character-reference": ["decode-named-character-reference@1.3.0", "", { "dependencies": { "character-entities": "^2.0.0" } }, "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q=="], "decompress-response": ["decompress-response@6.0.0", "", { "dependencies": { "mimic-response": "^3.1.0" } }, "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ=="], "deep-eql": ["deep-eql@5.0.2", "", {}, "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q=="], - "deep-extend": ["deep-extend@0.6.0", "", {}, "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA=="], - "deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="], "default-browser": ["default-browser@5.5.0", "", { "dependencies": { "bundle-name": "^4.1.0", "default-browser-id": "^5.0.0" } }, "sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw=="], @@ -3802,16 +3488,12 @@ "defu": ["defu@6.1.7", "", {}, "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ=="], - "degenerator": ["degenerator@5.0.1", "", { "dependencies": { "ast-types": "^0.13.4", "escodegen": "^2.1.0", "esprima": "^4.0.1" } }, "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ=="], - "delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="], "denque": ["denque@2.1.0", "", {}, "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw=="], "depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="], - "dependency-graph": ["dependency-graph@0.11.0", "", {}, "sha512-JeMq7fEshyepOWDfcfHK06N3MhyPhz++vtqWhMT5O9A3K42rdsEDpfdVqjaqaAhsw6a+ZqeDvQVtD0hFHQWrzg=="], - "deprecation": ["deprecation@2.3.1", "", {}, "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ=="], "dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="], @@ -3820,22 +3502,18 @@ "destroy": ["destroy@1.2.0", "", {}, "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg=="], - "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], + "detect-libc": ["detect-libc@1.0.3", "", { "bin": { "detect-libc": "./bin/detect-libc.js" } }, "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg=="], "detect-node": ["detect-node@2.1.0", "", {}, "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g=="], "detect-node-es": ["detect-node-es@1.1.0", "", {}, "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="], - "detect-port": ["detect-port@1.5.1", "", { "dependencies": { "address": "^1.0.1", "debug": "4" }, "bin": { "detect": "bin/detect-port.js", "detect-port": "bin/detect-port.js" } }, "sha512-aBzdj76lueB6uUst5iAs7+0H/oOjqI5D16XUWxlWMIMROhcM0rfsNVk93zTngq1dDNpoXRr++Sus7ETAExppAQ=="], - "deterministic-object-hash": ["deterministic-object-hash@2.0.2", "", { "dependencies": { "base-64": "^1.0.0" } }, "sha512-KxektNH63SrbfUyDiwXqRb1rLwKt33AmMv+5Nhsw1kqZ13SJBRTgZHtGbE+hH3a1mVW1cz+4pqSWVPAtLVXTzQ=="], "devalue": ["devalue@5.8.1", "", {}, "sha512-4CXDYRBGqN+57wVJkuXBYmpAVUSg3L6JAQa/DFqm238G73E1wuyc/JhGQJzN7vUf/CMphYau2zXbfWzDR5aTEw=="], "devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="], - "devtools-protocol": ["devtools-protocol@0.0.1312386", "", {}, "sha512-DPnhUXvmvKT2dFA/j7B+riVLUt9Q6RKJlcppojL5CoRywJJKLDYnRlw0gTFKfgDPHP5E04UoB71SxoJlVZy8FA=="], - "dfa": ["dfa@1.2.0", "", {}, "sha512-ED3jP8saaweFTjeGX8HQPjeC1YYyZs98jGNZx6IiBvxW7JG5v492kamAQB3m2wop07CvU/RQmzcKr6bgcC5D/Q=="], "diacritics": ["diacritics@1.3.0", "", {}, "sha512-wlwEkqcsaxvPJML+rDh/2iS824jbREk6DUMUKkEaSlxdYHeS43cClJtsWglvw2RfeXGm6ohKDqsXteJ5sP5enA=="], @@ -3858,8 +3536,6 @@ "dns-packet": ["dns-packet@5.6.1", "", { "dependencies": { "@leichtgewicht/ip-codec": "^2.0.1" } }, "sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw=="], - "dns-socket": ["dns-socket@4.2.2", "", { "dependencies": { "dns-packet": "^5.2.4" } }, "sha512-BDeBd8najI4/lS00HSKpdFia+OvUMytaVjfzR9n5Lq8MlZRSvtbI+uLtx1+XmQFls5wFU9dssccTmQQ6nfpjdg=="], - "dom-accessibility-api": ["dom-accessibility-api@0.6.3", "", {}, "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w=="], "dom-serializer": ["dom-serializer@2.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", "entities": "^4.2.0" } }, "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg=="], @@ -3942,8 +3618,6 @@ "end-of-stream": ["end-of-stream@1.4.5", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg=="], - "engine.io": ["engine.io@6.6.9", "", { "dependencies": { "@types/cors": "^2.8.12", "@types/node": ">=10.0.0", "@types/ws": "^8.5.12", "accepts": "~1.3.4", "base64id": "2.0.0", "cookie": "~0.7.2", "cors": "~2.8.5", "debug": "~4.4.1", "engine.io-parser": "~5.2.1", "ws": "~8.21.0" } }, "sha512-clKkw4C7nJ22mGgoVcCg6V/W/TxdNyIOTr89k2ONZu81qqkddPFDF0LXcbAwhzPD8DjkiRCjzuiO6Y+fkpD4vg=="], - "engine.io-client": ["engine.io-client@6.6.5", "", { "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.4.1", "engine.io-parser": "~5.2.1", "ws": "~8.20.1", "xmlhttprequest-ssl": "~2.1.1" } }, "sha512-QCwxUDULPlXv8F6tqMMKx5dNkTe6OaBYRMPYeXKBlyOoKvAmE0ac6pW7fFhSscJ/5SI7666/U/B+MElbsrJlIg=="], "engine.io-parser": ["engine.io-parser@5.2.3", "", {}, "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q=="], @@ -3954,20 +3628,14 @@ "env-paths": ["env-paths@3.0.0", "", {}, "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A=="], - "environment": ["environment@1.1.0", "", {}, "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q=="], - "err-code": ["err-code@2.0.3", "", {}, "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA=="], - "error-ex": ["error-ex@1.3.4", "", { "dependencies": { "is-arrayish": "^0.2.1" } }, "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ=="], - "error-stack-parser": ["error-stack-parser@2.1.4", "", { "dependencies": { "stackframe": "^1.3.4" } }, "sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ=="], "error-stack-parser-es": ["error-stack-parser-es@1.0.5", "", {}, "sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA=="], "es-abstract": ["es-abstract@1.24.2", "", { "dependencies": { "array-buffer-byte-length": "^1.0.2", "arraybuffer.prototype.slice": "^1.0.4", "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "data-view-buffer": "^1.0.2", "data-view-byte-length": "^1.0.2", "data-view-byte-offset": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "es-set-tostringtag": "^2.1.0", "es-to-primitive": "^1.3.0", "function.prototype.name": "^1.1.8", "get-intrinsic": "^1.3.0", "get-proto": "^1.0.1", "get-symbol-description": "^1.1.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "internal-slot": "^1.1.0", "is-array-buffer": "^3.0.5", "is-callable": "^1.2.7", "is-data-view": "^1.0.2", "is-negative-zero": "^2.0.3", "is-regex": "^1.2.1", "is-set": "^2.0.3", "is-shared-array-buffer": "^1.0.4", "is-string": "^1.1.1", "is-typed-array": "^1.1.15", "is-weakref": "^1.1.1", "math-intrinsics": "^1.1.0", "object-inspect": "^1.13.4", "object-keys": "^1.1.1", "object.assign": "^4.1.7", "own-keys": "^1.0.1", "regexp.prototype.flags": "^1.5.4", "safe-array-concat": "^1.1.3", "safe-push-apply": "^1.0.0", "safe-regex-test": "^1.1.0", "set-proto": "^1.0.0", "stop-iteration-iterator": "^1.1.0", "string.prototype.trim": "^1.2.10", "string.prototype.trimend": "^1.0.9", "string.prototype.trimstart": "^1.0.8", "typed-array-buffer": "^1.0.3", "typed-array-byte-length": "^1.0.3", "typed-array-byte-offset": "^1.0.4", "typed-array-length": "^1.0.7", "unbox-primitive": "^1.1.0", "which-typed-array": "^1.1.19" } }, "sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg=="], - "es-aggregate-error": ["es-aggregate-error@1.0.14", "", { "dependencies": { "define-data-property": "^1.1.4", "define-properties": "^1.2.1", "es-abstract": "^1.24.0", "es-errors": "^1.3.0", "function-bind": "^1.1.2", "globalthis": "^1.0.4", "has-property-descriptors": "^1.0.2", "set-function-name": "^2.0.2" } }, "sha512-3YxX6rVb07B5TV11AV5wsL7nQCHXNwoHPsQC8S4AmBiqYhyNCJ5BRKXkXyDJvs8QzXN20NgRtxe3dEEQD9NLHA=="], - "es-array-method-boxes-properly": ["es-array-method-boxes-properly@1.0.0", "", {}, "sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA=="], "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], @@ -3984,8 +3652,6 @@ "es-to-primitive": ["es-to-primitive@1.3.0", "", { "dependencies": { "is-callable": "^1.2.7", "is-date-object": "^1.0.5", "is-symbol": "^1.0.4" } }, "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g=="], - "es-toolkit": ["es-toolkit@1.49.0", "", {}, "sha512-G5iZ6Pc/FNRY/soKZHC+TxGDD83rHUDXxzaWhGCX44vAv/tMs56WMusnm/KMNK+luUPsgA9U28cGr4RDlSzL2g=="], - "es6-error": ["es6-error@4.1.1", "", {}, "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg=="], "esast-util-from-estree": ["esast-util-from-estree@2.0.0", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "devlop": "^1.0.0", "estree-util-visit": "^2.0.0", "unist-util-position-from-estree": "^2.0.0" } }, "sha512-4CyanoAudUSBAn5K13H4JhsMH6L9ZP7XbLVe/dKybkxMO7eDyLsT8UHl9TRNrU2Gr9nz+FovfSIjuXWJ81uVwQ=="], @@ -4004,12 +3670,8 @@ "escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="], - "escodegen": ["escodegen@2.1.0", "", { "dependencies": { "esprima": "^4.0.1", "estraverse": "^5.2.0", "esutils": "^2.0.2" }, "optionalDependencies": { "source-map": "~0.6.1" }, "bin": { "esgenerate": "bin/esgenerate.js", "escodegen": "bin/escodegen.js" } }, "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w=="], - "esprima": ["esprima@4.0.1", "", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="], - "estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="], - "estree-util-attach-comments": ["estree-util-attach-comments@3.0.0", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-cKUwm/HUcTDsYh/9FgnuFqpfquUbwIqwKM26BVCGDPVgvaCl/nDCCjUfiLlx6lsEZ3Z4RFxNbOQ60pkaEwFxGw=="], "estree-util-build-jsx": ["estree-util-build-jsx@3.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "estree-walker": "^3.0.0" } }, "sha512-8U5eiL6BTrPxp/CHbs2yMgP8ftMhR5ww1eIKoWRMlqvltHF8fZn5LRDvTKuxD3DUn+shRbLGqXemcP51oFCsGQ=="], @@ -4024,8 +3686,6 @@ "estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], - "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="], - "etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="], "event-target-shim": ["event-target-shim@5.0.1", "", {}, "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ=="], @@ -4044,15 +3704,11 @@ "exit-hook": ["exit-hook@2.2.1", "", {}, "sha512-eNTPlAD67BmP31LDINZ3U7HSF8l57TxOY2PmBJ1shpCvpnxBF93mWCE8YHBnXs8qiUZJc9WDcWIeC3a2HIAMfw=="], - "expand-template": ["expand-template@2.0.3", "", {}, "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg=="], - "expect-type": ["expect-type@1.3.0", "", {}, "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA=="], "exponential-backoff": ["exponential-backoff@3.1.3", "", {}, "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA=="], - "expr-eval-fork": ["expr-eval-fork@3.0.3", "", {}, "sha512-BhC+hbc5lIVjygr840n5DEkW3MQq7H9o+mc1/N7Z5uIiCFVyESLL5DIE7LNq4CYUNxy+XjA+3jRrL/h0Kt2xcg=="], - - "express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="], + "express": ["express@4.22.2", "", { "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", "body-parser": "~1.20.5", "content-disposition": "~0.5.4", "content-type": "~1.0.4", "cookie": "~0.7.1", "cookie-signature": "~1.0.6", "debug": "2.6.9", "depd": "2.0.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "finalhandler": "~1.3.1", "fresh": "~0.5.2", "http-errors": "~2.0.0", "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "~2.4.1", "parseurl": "~1.3.3", "path-to-regexp": "~0.1.12", "proxy-addr": "~2.0.7", "qs": "~6.15.1", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", "send": "~0.19.0", "serve-static": "~1.16.2", "setprototypeof": "1.2.0", "statuses": "~2.0.1", "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" } }, "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q=="], "express-rate-limit": ["express-rate-limit@8.5.2", "", { "dependencies": { "ip-address": "^10.2.0" }, "peerDependencies": { "express": ">= 4.11" } }, "sha512-5Kb34ipNX694DH48vN9irak1Qx30nb0PLYHXfJgw4YEjiC3ZEmZJhwOp+VfiCYwFzvFTdB9QkArYS5kXa2cx2A=="], @@ -4086,8 +3742,6 @@ "fast-json-stringify": ["fast-json-stringify@6.4.0", "", { "dependencies": { "@fastify/merge-json-schemas": "^0.2.0", "ajv": "^8.12.0", "ajv-formats": "^3.0.1", "fast-uri": "^3.0.0", "json-schema-ref-resolver": "^3.0.0", "rfdc": "^1.2.0" } }, "sha512-ibRCQ0GZKJIQ+P3Et1h0LhPgp3PMTYk0MH8O+kW3lNYsvmaQww5Nn3f1jf73Q0jR1Yz3a1CDP4/NZD3vOajWJQ=="], - "fast-memoize": ["fast-memoize@2.5.2", "", {}, "sha512-Ue0LwpDYErFbmNnZSF0UH6eImUwDmogUO1jyE+JbN2gsQz/jICm1Ve7t9QT0rNSsfJt+Hs4/S3GnsDVjL4HVrw=="], - "fast-querystring": ["fast-querystring@1.1.2", "", { "dependencies": { "fast-decode-uri-component": "^1.0.1" } }, "sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg=="], "fast-uri": ["fast-uri@3.1.2", "", {}, "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ=="], @@ -4102,10 +3756,6 @@ "fastq": ["fastq@1.20.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw=="], - "fault": ["fault@2.0.1", "", { "dependencies": { "format": "^0.2.0" } }, "sha512-WtySTkS4OKev5JtpHXnib4Gxiurzh5NCGvWrFaZ34m6JehfTUhKZvn9njTfw48t6JumVQOmrKqpmGcdwxnhqBQ=="], - - "favicons": ["favicons@7.2.0", "", { "dependencies": { "escape-html": "^1.0.3", "sharp": "^0.33.1", "xml2js": "^0.6.1" } }, "sha512-k/2rVBRIRzOeom3wI9jBPaSEvoTSQEW4iM0EveBmBBKFxO8mSyyRWtDlfC3VnEfu0avmjrMzy8/ZFPSe6F71Hw=="], - "fd-slicer": ["fd-slicer@1.1.0", "", { "dependencies": { "pend": "~1.2.0" } }, "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g=="], "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], @@ -4116,7 +3766,7 @@ "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], - "finalhandler": ["finalhandler@2.1.1", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA=="], + "finalhandler": ["finalhandler@1.3.2", "", { "dependencies": { "debug": "2.6.9", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "on-finished": "~2.4.1", "parseurl": "~1.3.3", "statuses": "~2.0.2", "unpipe": "~1.0.0" } }, "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg=="], "find-babel-config": ["find-babel-config@2.1.2", "", { "dependencies": { "json5": "^2.2.3" } }, "sha512-ZfZp1rQyp4gyuxqt1ZqjFGVeVBvmpURMqdIWXbPRfB97Bf6BzdK/xSIbylEINzQ0kB5tlDQfn9HkNXXWsqTqLg=="], @@ -4144,8 +3794,6 @@ "form-data-encoder": ["form-data-encoder@1.7.2", "", {}, "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A=="], - "format": ["format@0.2.2", "", {}, "sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww=="], - "formdata-node": ["formdata-node@4.4.1", "", { "dependencies": { "node-domexception": "1.0.0", "web-streams-polyfill": "4.0.0-beta.3" } }, "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ=="], "formdata-polyfill": ["formdata-polyfill@4.0.10", "", { "dependencies": { "fetch-blob": "^3.1.2" } }, "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g=="], @@ -4154,15 +3802,9 @@ "fraction.js": ["fraction.js@5.3.4", "", {}, "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ=="], - "fractional-indexing": ["fractional-indexing@3.2.0", "", {}, "sha512-PcOxmqwYCW7O2ovKRU8OoQQj2yqTfEB/yeTYk4gPid6dN5ODRfU1hXd9tTVZzax/0NkO7AxpHykvZnT1aYp/BQ=="], - "framer-motion": ["framer-motion@8.5.5", "", { "dependencies": { "@motionone/dom": "^10.15.3", "hey-listen": "^1.0.8", "tslib": "^2.4.0" }, "optionalDependencies": { "@emotion/is-prop-valid": "^0.8.2" }, "peerDependencies": { "react": "^18.0.0", "react-dom": "^18.0.0" } }, "sha512-5IDx5bxkjWHWUF3CVJoSyUVOtrbAxtzYBBowRE2uYI/6VYhkEBD+rbTHEGuUmbGHRj6YqqSfoG7Aa1cLyWCrBA=="], - "fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="], - - "front-matter": ["front-matter@4.0.2", "", { "dependencies": { "js-yaml": "^3.13.1" } }, "sha512-I8ZuJ/qG92NWX8i5x1Y8qyj3vizhXS31OxjKDu3LKP+7/qBgfIKValiZIEwoVoJKUHlhWtYrktkxV1XsX+pPlg=="], - - "fs-constants": ["fs-constants@1.0.0", "", {}, "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="], + "fresh": ["fresh@0.5.2", "", {}, "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q=="], "fs-extra": ["fs-extra@10.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ=="], @@ -4182,8 +3824,6 @@ "gaxios": ["gaxios@7.1.4", "", { "dependencies": { "extend": "^3.0.2", "https-proxy-agent": "^7.0.1", "node-fetch": "^3.3.2" } }, "sha512-bTIgTsM2bWn3XklZISBTQX7ZSddGW+IO3bMdGaemHZ3tbqExMENHLx6kKZ/KlejgrMtj8q7wBItt51yegqalrA=="], - "gcd": ["gcd@0.0.1", "", {}, "sha512-VNx3UEGr+ILJTiMs1+xc5SX1cMgJCrXezKPa003APUWNqQqaF6n25W8VcR7nHN6yRWbvvUTwCpZCFJeWC2kXlw=="], - "gcp-metadata": ["gcp-metadata@8.1.2", "", { "dependencies": { "gaxios": "^7.0.0", "google-logging-utils": "^1.0.0", "json-bigint": "^1.0.0" } }, "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg=="], "generate-function": ["generate-function@2.3.1", "", { "dependencies": { "is-property": "^1.0.2" } }, "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ=="], @@ -4210,17 +3850,13 @@ "get-tsconfig": ["get-tsconfig@4.14.0", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA=="], - "get-uri": ["get-uri@6.0.5", "", { "dependencies": { "basic-ftp": "^5.0.2", "data-uri-to-buffer": "^6.0.2", "debug": "^4.3.4" } }, "sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg=="], - - "ghostty-web": ["ghostty-web@github:anomalyco/ghostty-web#513463a", {}, "anomalyco-ghostty-web-513463a", "sha512-GZR8LSmgGzViWnBJrqRI8MpAZRCJxhcr1Hi9Tyeh7YRooHZQjK9J97FQRD3tbBaM2wjq05gzGY2UEsG+JtZeBw=="], + "ghostty-web": ["ghostty-web@github:anomalyco/ghostty-web#83c0a07", {}, "anomalyco-ghostty-web-83c0a07", "sha512-Lf2v1agHkVUpMpHBWWuCZrhOEmcwwin5/Hboc9rZwQ7/CKkIh5rU1r1CvfLlhkMoFv+ed8z52RZ8hkzGZZj3MQ=="], "giget": ["giget@2.0.0", "", { "dependencies": { "citty": "^0.1.6", "consola": "^3.4.0", "defu": "^6.1.4", "node-fetch-native": "^1.6.6", "nypm": "^0.6.0", "pathe": "^2.0.3" }, "bin": { "giget": "dist/cli.mjs" } }, "sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA=="], - "github-from-package": ["github-from-package@0.0.0", "", {}, "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw=="], - "github-slugger": ["github-slugger@2.0.0", "", {}, "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw=="], - "gitlab-ai-provider": ["gitlab-ai-provider@6.10.0", "", { "dependencies": { "@anthropic-ai/sdk": "^0.71.0", "@anycable/core": "^0.9.2", "graphql-request": "^6.1.0", "isomorphic-ws": "^5.0.0", "openai": "^6.16.0", "socket.io-client": "^4.8.1", "vscode-jsonrpc": "^8.2.1", "zod": "^3.25.76" }, "peerDependencies": { "@ai-sdk/provider": ">=3.0.0", "@ai-sdk/provider-utils": ">=4.0.0" } }, "sha512-oWEZ06rDO6JjB7INHO882wyBAQqCZVHiDHwCs5M+VPmdDj8TzhGXcYesA2CcV5RoI5lfHLKwGp5uKFB62VWpqw=="], + "gitlab-ai-provider": ["gitlab-ai-provider@6.12.1", "", { "dependencies": { "@anthropic-ai/sdk": "^0.71.0", "@anycable/core": "^0.9.2", "graphql-request": "^6.1.0", "isomorphic-ws": "^5.0.0", "openai": "^6.16.0", "socket.io-client": "^4.8.1", "vscode-jsonrpc": "^8.2.1", "zod": "^3.25.76" }, "peerDependencies": { "@ai-sdk/provider": ">=3.0.0", "@ai-sdk/provider-utils": ">=4.0.0" } }, "sha512-Qn5iHqvjG8yktI5MWaUgdRR94l7O4WtYW0CAbhsCh1Tj0Fei/DeprOYPVyf4Nht1Ix6U2PXSYM32QOHI6Z2TDw=="], "glob": ["glob@13.0.5", "", { "dependencies": { "minimatch": "^10.2.1", "minipass": "^7.1.2", "path-scurry": "^2.0.0" } }, "sha512-BzXxZg24Ibra1pbQ/zE7Kys4Ua1ks7Bn6pKLkVPZ9FZe4JQS6/Q7ef3LG1H+k7lUf5l4T3PLSyYyYJVYUvfgTw=="], @@ -4274,12 +3910,8 @@ "hast-util-format": ["hast-util-format@1.1.0", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-embedded": "^3.0.0", "hast-util-minify-whitespace": "^1.0.0", "hast-util-phrasing": "^3.0.0", "hast-util-whitespace": "^3.0.0", "html-whitespace-sensitive-tag-names": "^3.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-yY1UDz6bC9rDvCWHpx12aIBGRG7krurX0p0Fm6pT547LwDIZZiNr8a+IHDogorAdreULSEzP82Nlv5SZkHZcjA=="], - "hast-util-from-dom": ["hast-util-from-dom@5.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "hastscript": "^9.0.0", "web-namespaces": "^2.0.0" } }, "sha512-N+LqofjR2zuzTjCPzyDUdSshy4Ma6li7p/c3pA78uTwzFgENbgbUrm2ugwsOdcjI1muO+o6Dgzp9p8WHtn/39Q=="], - "hast-util-from-html": ["hast-util-from-html@2.0.3", "", { "dependencies": { "@types/hast": "^3.0.0", "devlop": "^1.1.0", "hast-util-from-parse5": "^8.0.0", "parse5": "^7.0.0", "vfile": "^6.0.0", "vfile-message": "^4.0.0" } }, "sha512-CUSRHXyKjzHov8yKsQjGOElXy/3EKpyX56ELnkHH34vDVw1N1XSQ1ZcAvTyAPtGqLTuKP/uxM+aLkSPqF/EtMw=="], - "hast-util-from-html-isomorphic": ["hast-util-from-html-isomorphic@2.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-from-dom": "^5.0.0", "hast-util-from-html": "^2.0.0", "unist-util-remove-position": "^5.0.0" } }, "sha512-zJfpXq44yff2hmE0XmwEOzdWin5xwH+QIhMLOScpX91e/NSGPsAzNCvLQDIEPyO2TXi+lBmU6hjLIhV8MwP2kw=="], - "hast-util-from-parse5": ["hast-util-from-parse5@8.0.3", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "devlop": "^1.0.0", "hastscript": "^9.0.0", "property-information": "^7.0.0", "vfile": "^6.0.0", "vfile-location": "^5.0.0", "web-namespaces": "^2.0.0" } }, "sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg=="], "hast-util-has-property": ["hast-util-has-property@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-MNilsvEKLFpV604hwfhVStK0usFY/QmM5zX16bo7EjnAEGofr5YyI37kzopBlZJkHD4t887i+q/C8/tr5Q94cA=="], @@ -4306,8 +3938,6 @@ "hast-util-to-jsx-runtime": ["hast-util-to-jsx-runtime@2.3.6", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "hast-util-whitespace": "^3.0.0", "mdast-util-mdx-expression": "^2.0.0", "mdast-util-mdx-jsx": "^3.0.0", "mdast-util-mdxjs-esm": "^2.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "style-to-js": "^1.0.0", "unist-util-position": "^5.0.0", "vfile-message": "^4.0.0" } }, "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg=="], - "hast-util-to-mdast": ["hast-util-to-mdast@10.1.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@ungap/structured-clone": "^1.0.0", "hast-util-phrasing": "^3.0.0", "hast-util-to-html": "^9.0.0", "hast-util-to-text": "^4.0.0", "hast-util-whitespace": "^3.0.0", "mdast-util-phrasing": "^4.0.0", "mdast-util-to-hast": "^13.0.0", "mdast-util-to-string": "^4.0.0", "rehype-minify-whitespace": "^6.0.0", "trim-trailing-lines": "^2.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0" } }, "sha512-DsL/SvCK9V7+vfc6SLQ+vKIyBDXTk2KLSbfBYkH4zeF/uR1yBajHRhkzuaUSGOB1WJSTieJBdHwxlC+HLKvZZw=="], - "hast-util-to-parse5": ["hast-util-to-parse5@8.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "web-namespaces": "^2.0.0", "zwitch": "^2.0.0" } }, "sha512-MlWT6Pjt4CG9lFCjiz4BH7l9wmrMkfkJYCxFwKQic8+RTZgWPuWxwAfjJElsXkex7DJjfSJsQIt931ilUgmwdA=="], "hast-util-to-string": ["hast-util-to-string@3.0.1", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-XelQVTDWvqcl3axRfI0xSeoVKzyIFPwsAGSLIsKdJKQMXDYJS4WYrBNF/8J7RdhIcFI2BOHgAifggsvsxp/3+A=="], @@ -4322,8 +3952,6 @@ "heap-snapshot-toolkit": ["heap-snapshot-toolkit@1.1.3", "", {}, "sha512-joThu2rEsDu8/l4arupRDI1qP4CZXNG+J6Wr348vnbLGSiBkwRdqZ6aOHl5BzEiC+Dc8OTbMlmWjD0lbXD5K2Q=="], - "hex-rgb": ["hex-rgb@5.0.0", "", {}, "sha512-NQO+lgVUCtHxZ792FodgW0zflK+ozS9X9dwGp9XvvmPlH7pyxd588cn24TD3rmPm/N0AIRXF10Otah8yKqGw4w=="], - "hey-listen": ["hey-listen@1.0.8", "", {}, "sha512-COpmrF2NOg4TBWUJ5UVyaCU2A88wEMkUPK4hNqyCkqHbxT92BbvfjoSozkAIIm6XhicGlJHhFdullInrdhwU8Q=="], "hono": ["hono@4.10.7", "", {}, "sha512-icXIITfw/07Q88nLSkB9aiUrd8rYzSweK681Kjo/TSggaGbOX4RRyxxm71v+3PC8C/j+4rlxGeoTRxQDkaJkUw=="], @@ -4368,8 +3996,6 @@ "i18next": ["i18next@23.16.8", "", { "dependencies": { "@babel/runtime": "^7.23.2" } }, "sha512-06r/TitrM88Mg5FdUXAKL96dJMzgqLE5dv3ryBAra4KCwD9mJ4ndOTS95ZuymIGoE+2hzfdaMak2X11/es7ZWg=="], - "ico-endec": ["ico-endec@0.1.6", "", {}, "sha512-ZdLU38ZoED3g1j3iEyzcQj+wAkY2xfWNkymszfJPoxucIUhK7NayQ+/C4Kv0nDFMIsbtbEHldv3V8PU494/ueQ=="], - "iconv-corefoundation": ["iconv-corefoundation@1.1.7", "", { "dependencies": { "cli-truncate": "^2.1.0", "node-addon-api": "^1.6.3" }, "os": "darwin" }, "sha512-T10qvkw0zz4wnm560lOEg0PovVqUXuOFhhHAkixw8/sycy7TJt7v/RrkEKEQnAw2viPSJu6iAkErxnzR0g8PpQ=="], "iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="], @@ -4382,8 +4008,6 @@ "immer": ["immer@11.1.4", "", {}, "sha512-XREFCPo6ksxVzP4E0ekD5aMdf8WMwmdNaz6vuvxgI40UaEiu6q3p8X52aU6GdyvLY3XXX/8R7JOTXStz/nBbRw=="], - "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="], - "import-local": ["import-local@3.2.0", "", { "dependencies": { "pkg-dir": "^4.2.0", "resolve-cwd": "^3.0.0" }, "bin": { "import-local-fixture": "fixtures/cli.js" } }, "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA=="], "import-meta-resolve": ["import-meta-resolve@4.2.0", "", {}, "sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg=="], @@ -4396,14 +4020,8 @@ "ini": ["ini@7.0.0", "", {}, "sha512-ifK0CgjALofS5bkrcTy4RaQ9Vx2Knf/eLeIO+NaswQEpH1UblrtTSCIvN71qQDMq0PeQ/SSPojvEJp9vvvfr+w=="], - "ink": ["ink@6.3.0", "", { "dependencies": { "@alcalzone/ansi-tokenize": "^0.2.0", "ansi-escapes": "^7.0.0", "ansi-styles": "^6.2.1", "auto-bind": "^5.0.1", "chalk": "^5.6.0", "cli-boxes": "^3.0.0", "cli-cursor": "^4.0.0", "cli-truncate": "^4.0.0", "code-excerpt": "^4.0.0", "es-toolkit": "^1.39.10", "indent-string": "^5.0.0", "is-in-ci": "^2.0.0", "patch-console": "^2.0.0", "react-reconciler": "^0.32.0", "signal-exit": "^3.0.7", "slice-ansi": "^7.1.0", "stack-utils": "^2.0.6", "string-width": "^7.2.0", "type-fest": "^4.27.0", "widest-line": "^5.0.0", "wrap-ansi": "^9.0.0", "ws": "^8.18.0", "yoga-layout": "~3.2.1" }, "peerDependencies": { "@types/react": ">=19.0.0", "react": ">=19.0.0", "react-devtools-core": "^4.19.1" }, "optionalPeers": ["@types/react", "react-devtools-core"] }, "sha512-2CbJAa7XeziZYe6pDS5RVLirRY28iSGMQuEV8jRU5NQsONQNfcR/BZHHc9vkMg2lGYTHTM2pskxC1YmY28p6bQ=="], - - "ink-spinner": ["ink-spinner@5.0.0", "", { "dependencies": { "cli-spinners": "^2.7.0" }, "peerDependencies": { "ink": ">=4.0.0", "react": ">=18.0.0" } }, "sha512-EYEasbEjkqLGyPOUc8hBJZNuC5GvXGMLu0w5gdTNskPc7Izc5vO3tdQEYnzvshucyGCBXc86ig0ujXPMWaQCdA=="], - "inline-style-parser": ["inline-style-parser@0.2.7", "", {}, "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA=="], - "inquirer": ["inquirer@12.3.0", "", { "dependencies": { "@inquirer/core": "^10.1.2", "@inquirer/prompts": "^7.2.1", "@inquirer/type": "^3.0.2", "ansi-escapes": "^4.3.2", "mute-stream": "^2.0.0", "run-async": "^3.0.0", "rxjs": "^7.8.1" }, "peerDependencies": { "@types/node": ">=18" } }, "sha512-3NixUXq+hM8ezj2wc7wC37b32/rHq1MwNZDYdvx+d6jokOD+r+i8Q4Pkylh9tISYP114A128LCX8RKhopC5RfQ=="], - "internal-slot": ["internal-slot@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "hasown": "^2.0.2", "side-channel": "^1.1.0" } }, "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw=="], "internmap": ["internmap@2.0.3", "", {}, "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg=="], @@ -4412,8 +4030,6 @@ "ip-address": ["ip-address@10.2.0", "", {}, "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA=="], - "ip-regex": ["ip-regex@4.3.0", "", {}, "sha512-B9ZWJxHHOHUhUjCPrMpLD4xEq35bUTClHM1S6CBU5ixQnkZmwipwgc96vAd7AAGM9TGHvJR+Uss+/Ak6UphK+Q=="], - "ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="], "iron-webcrypto": ["iron-webcrypto@1.2.1", "", {}, "sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg=="], @@ -4466,14 +4082,10 @@ "is-hexadecimal": ["is-hexadecimal@2.0.1", "", {}, "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg=="], - "is-in-ci": ["is-in-ci@2.0.0", "", { "bin": { "is-in-ci": "cli.js" } }, "sha512-cFeerHriAnhrQSbpAxL37W1wcJKUUX07HyLWZCW1URJT/ra3GyUTzBgUnh24TMVfNTV2Hij2HLxkPHFZfOZy5w=="], - "is-in-ssh": ["is-in-ssh@1.0.0", "", {}, "sha512-jYa6Q9rH90kR1vKB6NM7qqd1mge3Fx4Dhw5TVlK1MUBqhEOuCagrEHMevNuCcbECmXZ0ThXkRm+Ymr51HwEPAw=="], "is-inside-container": ["is-inside-container@1.0.0", "", { "dependencies": { "is-docker": "^3.0.0" }, "bin": { "is-inside-container": "cli.js" } }, "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA=="], - "is-ip": ["is-ip@3.1.0", "", { "dependencies": { "ip-regex": "^4.0.0" } }, "sha512-35vd5necO7IitFPjd/YBeqwWnyDWbuLH9ZXQdMfDA8TEo7pv5X8yfrvVO3xbJbLUlERCMvf6X0hTUamQxCYJ9Q=="], - "is-map": ["is-map@2.0.3", "", {}, "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw=="], "is-negative-zero": ["is-negative-zero@2.0.3", "", {}, "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw=="], @@ -4482,8 +4094,6 @@ "is-number-object": ["is-number-object@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" } }, "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw=="], - "is-online": ["is-online@10.0.0", "", { "dependencies": { "got": "^12.1.0", "p-any": "^4.0.0", "p-timeout": "^5.1.0", "public-ip": "^5.0.0" } }, "sha512-WCPdKwNDjXJJmUubf2VHLMDBkUZEtuOvpXUfUnUFbEnM6In9ByiScL4f4jKACz/fsb2qDkesFerW3snf/AYz3A=="], - "is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="], "is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="], @@ -4556,8 +4166,6 @@ "jsbi": ["jsbi@4.3.2", "", {}, "sha512-9fqMSQbhJykSeii05nxKl4m6Eqn2P6rOlYiS+C5Dr/HPIU/7yZxu5qzbs40tgaFORiw2Amd0mirjxatXYMkIew=="], - "jsep": ["jsep@1.4.0", "", {}, "sha512-B7qPcEVE3NVkmSJbaYxvv4cHkVW7DQsZz13pUMrfS8z8Q/BuShN+gcTXrUlPiGqM2/t/EEaI030bpxMqY8gMlw=="], - "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], "json-bigint": ["json-bigint@1.0.0", "", { "dependencies": { "bignumber.js": "^9.0.0" } }, "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ=="], @@ -4590,10 +4198,6 @@ "jsonparse": ["jsonparse@1.3.1", "", {}, "sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg=="], - "jsonpath-plus": ["jsonpath-plus@10.4.0", "", { "dependencies": { "@jsep-plugin/assignment": "^1.3.0", "@jsep-plugin/regex": "^1.0.4", "jsep": "^1.4.0" }, "bin": { "jsonpath": "bin/jsonpath-cli.js", "jsonpath-plus": "bin/jsonpath-cli.js" } }, "sha512-T92WWatJXmhBbKsgH/0hl+jxjdXrifi5IKeMY02DWggRxX0UElcbVzPlmgLTbvsPeW1PasQ6xE2Q75stkhGbsA=="], - - "jsonpointer": ["jsonpointer@5.0.1", "", {}, "sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ=="], - "jsonwebtoken": ["jsonwebtoken@9.0.3", "", { "dependencies": { "jws": "^4.0.1", "lodash.includes": "^4.3.0", "lodash.isboolean": "^3.0.3", "lodash.isinteger": "^4.0.4", "lodash.isnumber": "^3.0.3", "lodash.isplainobject": "^4.0.6", "lodash.isstring": "^4.0.1", "lodash.once": "^4.0.0", "ms": "^2.1.1", "semver": "^7.5.4" } }, "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g=="], "just-diff": ["just-diff@6.0.2", "", {}, "sha512-S59eriX5u3/QhMNq3v/gm8Kd0w8OS6Tz2FS1NG4blv+z0MuQcBRJyFWjdovM0Rad4/P4aUPFtnkNjMjyMlMSYA=="], @@ -4608,8 +4212,6 @@ "katex": ["katex@0.16.27", "", { "dependencies": { "commander": "^8.3.0" }, "bin": { "katex": "cli.js" } }, "sha512-aeQoDkuRWSqQN6nSvVCEFvfXdqo1OQiCmmW1kc9xSdjutPv7BGO7pqY9sQRJpMOGrEdfDgF2TfRXe5eUAD2Waw=="], - "keytar": ["keytar@7.9.0", "", { "dependencies": { "node-addon-api": "^4.3.0", "prebuild-install": "^7.0.1" } }, "sha512-VPD8mtVtm5JNtA2AErl6Chp06JBfy7diFQ7TQQhdpWOl6MrCRB+eRbvAZUsbGQS9kiMq0coJsy0W0vHpDCkWsQ=="], - "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="], "kind-of": ["kind-of@6.0.3", "", {}, "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw=="], @@ -4628,12 +4230,8 @@ "lazystream": ["lazystream@1.0.1", "", { "dependencies": { "readable-stream": "^2.0.5" } }, "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw=="], - "lcm": ["lcm@0.0.3", "", { "dependencies": { "gcd": "^0.0.1" } }, "sha512-TB+ZjoillV6B26Vspf9l2L/vKaRY/4ep3hahcyVkCGFgsTNRUQdc24bQeNFiZeoxH0vr5+7SfNRMQuPHv/1IrQ=="], - "leac": ["leac@0.6.0", "", {}, "sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg=="], - "leven": ["leven@4.1.0", "", {}, "sha512-KZ9W9nWDT7rF7Dazg8xyLHGLrmpgq2nVNFUckhqdW3szVP6YhCpp/RAnpmVExA9JvrMynjwSLVrEj3AepHR6ew=="], - "light-my-request": ["light-my-request@6.6.0", "", { "dependencies": { "cookie": "^1.0.1", "process-warning": "^4.0.0", "set-cookie-parser": "^2.6.0" } }, "sha512-CHYbu8RtboSIoVsHZ6Ye4cj4Aw/yg2oAFimlF7mNvfDV192LR7nDiKtSIfCuLT7KokPSTn/9kfVLm5OGN0A28A=="], "lightningcss": ["lightningcss@1.30.1", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-darwin-arm64": "1.30.1", "lightningcss-darwin-x64": "1.30.1", "lightningcss-freebsd-x64": "1.30.1", "lightningcss-linux-arm-gnueabihf": "1.30.1", "lightningcss-linux-arm64-gnu": "1.30.1", "lightningcss-linux-arm64-musl": "1.30.1", "lightningcss-linux-x64-gnu": "1.30.1", "lightningcss-linux-x64-musl": "1.30.1", "lightningcss-win32-arm64-msvc": "1.30.1", "lightningcss-win32-x64-msvc": "1.30.1" } }, "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg=="], @@ -4684,8 +4282,6 @@ "lodash.once": ["lodash.once@4.1.1", "", {}, "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg=="], - "lodash.topath": ["lodash.topath@4.5.2", "", {}, "sha512-1/W4dM+35DwvE/iEd1M9ekewOSTlpFekhw9mhAtrwjVqUr83/ilQiyAvmg4tVX7Unkcfl1KC+i9WdaT4B6aQcg=="], - "loglevelnext": ["loglevelnext@6.0.0", "", {}, "sha512-FDl1AI2sJGjHHG3XKJd6sG3/6ncgiGCQ0YkW46nxe7SfqQq6hujd9CvFXIXtkGBUN83KPZ2KSOJK8q5P0bSSRQ=="], "long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="], @@ -4722,7 +4318,7 @@ "markdown-table": ["markdown-table@3.0.4", "", {}, "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw=="], - "marked": ["marked@17.0.1", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-boeBdiS0ghpWcSwoNm/jJBwdpFaMnZWRzjA6SkUMYb40SVaN1x7mmfGKp0jvexGcx+7y2La5zRZsYFZI6Qpypg=="], + "marked": ["marked@17.0.6", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-gB0gkNafnonOw0obSTEGZTT86IuhILt2Wfx0mWH/1Au83kybTayroZ/V6nS25mN7u8ASy+5fMhgB3XPNrOZdmA=="], "marked-katex-extension": ["marked-katex-extension@5.1.6", "", { "peerDependencies": { "katex": ">=0.16 <0.17", "marked": ">=4 <18" } }, "sha512-vYpLXwmlIDKILIhJtiRTgdyZRn5sEYdFBuTmbpjD7lbCIzg0/DWyK3HXIntN3Tp8zV6hvOUgpZNLWRCgWVc24A=="], @@ -4742,8 +4338,6 @@ "mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.3", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q=="], - "mdast-util-frontmatter": ["mdast-util-frontmatter@2.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "escape-string-regexp": "^5.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "micromark-extension-frontmatter": "^2.0.0" } }, "sha512-LRqI9+wdgC25P0URIJY9vwocIzCcksduHQ9OF2joxQoyTNVduwLAFUzjoopuRJbJAReaKrNQKAZKL3uCMugWJA=="], - "mdast-util-gfm": ["mdast-util-gfm@3.1.0", "", { "dependencies": { "mdast-util-from-markdown": "^2.0.0", "mdast-util-gfm-autolink-literal": "^2.0.0", "mdast-util-gfm-footnote": "^2.0.0", "mdast-util-gfm-strikethrough": "^2.0.0", "mdast-util-gfm-table": "^2.0.0", "mdast-util-gfm-task-list-item": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ=="], "mdast-util-gfm-autolink-literal": ["mdast-util-gfm-autolink-literal@2.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "ccount": "^2.0.0", "devlop": "^1.0.0", "mdast-util-find-and-replace": "^3.0.0", "micromark-util-character": "^2.0.0" } }, "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ=="], @@ -4756,8 +4350,6 @@ "mdast-util-gfm-task-list-item": ["mdast-util-gfm-task-list-item@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ=="], - "mdast-util-math": ["mdast-util-math@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "longest-streak": "^3.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.1.0", "unist-util-remove-position": "^5.0.0" } }, "sha512-Tl9GBNeG/AhJnQM221bJR2HPvLOSnLE/T9cJI9tlc6zwQk2nPk/4f0cHkOdEixQPC/j8UtKDdITswvLAy1OZ1w=="], - "mdast-util-mdx": ["mdast-util-mdx@3.0.0", "", { "dependencies": { "mdast-util-from-markdown": "^2.0.0", "mdast-util-mdx-expression": "^2.0.0", "mdast-util-mdx-jsx": "^3.0.0", "mdast-util-mdxjs-esm": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-JfbYLAW7XnYTTbUsmpu0kdBUVe+yKVJZBItEjwyYJiDJuZ9w4eeaqks4HQO+R7objWgS2ymV60GYpI14Ug554w=="], "mdast-util-mdx-expression": ["mdast-util-mdx-expression@2.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ=="], @@ -4776,11 +4368,11 @@ "mdn-data": ["mdn-data@2.27.1", "", {}, "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ=="], - "media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="], + "media-typer": ["media-typer@0.3.0", "", {}, "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ=="], "merge-anything": ["merge-anything@5.1.7", "", { "dependencies": { "is-what": "^4.1.8" } }, "sha512-eRtbOb1N5iyH0tkQDAoQ4Ipsp/5qSR79Dzrz8hEPxRX10RWWR/iQXdoKmBSRCThY1Fh5EhISDtpSc93fpxUniQ=="], - "merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="], + "merge-descriptors": ["merge-descriptors@1.0.3", "", {}, "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ=="], "merge-stream": ["merge-stream@2.0.0", "", {}, "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w=="], @@ -4794,8 +4386,6 @@ "micromark-extension-directive": ["micromark-extension-directive@3.0.2", "", { "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-factory-whitespace": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "parse-entities": "^4.0.0" } }, "sha512-wjcXHgk+PPdmvR58Le9d7zQYWy+vKEU9Se44p2CrCDPiLr2FMyiT4Fyb5UFKFC66wGB3kPlgD7q3TnoqPS7SZA=="], - "micromark-extension-frontmatter": ["micromark-extension-frontmatter@2.0.0", "", { "dependencies": { "fault": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-C4AkuM3dA58cgZha7zVnuVxBhDsbttIMiytjgsM2XbHAB2faRVaHRle40558FBN+DJcrLNCoqG5mlrpdU4cRtg=="], - "micromark-extension-gfm": ["micromark-extension-gfm@3.0.0", "", { "dependencies": { "micromark-extension-gfm-autolink-literal": "^2.0.0", "micromark-extension-gfm-footnote": "^2.0.0", "micromark-extension-gfm-strikethrough": "^2.0.0", "micromark-extension-gfm-table": "^2.0.0", "micromark-extension-gfm-tagfilter": "^2.0.0", "micromark-extension-gfm-task-list-item": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w=="], "micromark-extension-gfm-autolink-literal": ["micromark-extension-gfm-autolink-literal@2.1.0", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw=="], @@ -4810,11 +4400,9 @@ "micromark-extension-gfm-task-list-item": ["micromark-extension-gfm-task-list-item@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw=="], - "micromark-extension-math": ["micromark-extension-math@3.1.0", "", { "dependencies": { "@types/katex": "^0.16.0", "devlop": "^1.0.0", "katex": "^0.16.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-lvEqd+fHjATVs+2v/8kg9i5Q0AP2k85H0WUOwpIVvUML8BapsMvh1XAogmQjOCsLpoKRCVQqEkQBB3NhVBcsOg=="], - "micromark-extension-mdx-expression": ["micromark-extension-mdx-expression@3.0.1", "", { "dependencies": { "@types/estree": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-mdx-expression": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-events-to-acorn": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-dD/ADLJ1AeMvSAKBwO22zG22N4ybhe7kFIZ3LsDI0GlsNr2A3KYxb0LdC1u5rj4Nw+CHKY0RVdnHX8vj8ejm4Q=="], - "micromark-extension-mdx-jsx": ["micromark-extension-mdx-jsx@3.0.1", "", { "dependencies": { "@types/acorn": "^4.0.0", "@types/estree": "^1.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "micromark-factory-mdx-expression": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-events-to-acorn": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-vNuFb9czP8QCtAQcEJn0UJQJZA8Dk6DXKBqx+bg/w0WGuSxDxNr7hErW89tHUY31dUW4NqEOWwmEUNhjTFmHkg=="], + "micromark-extension-mdx-jsx": ["micromark-extension-mdx-jsx@3.0.2", "", { "dependencies": { "@types/estree": "^1.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "micromark-factory-mdx-expression": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-events-to-acorn": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-e5+q1DjMh62LZAJOnDraSSbDMvGJ8x3cbjygy2qFEi7HCeUT4BDKCvMozPozcD6WmOt6sVvYDNBKhFSz3kjOVQ=="], "micromark-extension-mdx-md": ["micromark-extension-mdx-md@2.0.0", "", { "dependencies": { "micromark-util-types": "^2.0.0" } }, "sha512-EpAiszsB3blw4Rpba7xTOUptcFeBFi+6PY8VnJ2hhimH+vCQDirWgsMpz7w1XcZE7LVrSAUGb9VJpG9ghlYvYQ=="], @@ -4900,14 +4488,8 @@ "minizlib": ["minizlib@3.1.0", "", { "dependencies": { "minipass": "^7.1.2" } }, "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw=="], - "mint": ["mint@4.2.666", "", { "dependencies": { "@mintlify/cli": "4.0.1269" }, "bin": { "mint": "index.js" } }, "sha512-FsdL35EH++MiVDoKxN8M6/obOsrgx0Ko7P/1Y2lBXh/jEPx1UD1Y8msmAOW6cuwaqGIzAxuC7ySmr7D3G2bmBg=="], - - "mitt": ["mitt@3.0.1", "", {}, "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw=="], - "mkdirp": ["mkdirp@0.5.6", "", { "dependencies": { "minimist": "^1.2.6" }, "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw=="], - "mkdirp-classic": ["mkdirp-classic@0.5.3", "", {}, "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A=="], - "morphdom": ["morphdom@2.7.8", "", {}, "sha512-D/fR4xgGUyVRbdMGU6Nejea1RFzYxYtyurG4Fbv2Fi/daKlWKuXGLOdXtl+3eIwL110cI2hz1ZojGICjjFLgTg=="], "motion": ["motion@12.34.5", "", { "dependencies": { "framer-motion": "^12.34.5", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-N06NLJ9IeBHeielRqIvYvjPfXuRdyTxa+9++BgpGa+hY2D7TcMkI6QzV3jaRuv0aZRXgMa7cPy9YcBUBisPzAQ=="], @@ -4932,8 +4514,6 @@ "mustache": ["mustache@4.2.0", "", { "bin": { "mustache": "bin/mustache" } }, "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ=="], - "mute-stream": ["mute-stream@2.0.0", "", {}, "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA=="], - "mysql2": ["mysql2@3.14.4", "", { "dependencies": { "aws-ssl-profiles": "^1.1.1", "denque": "^2.1.0", "generate-function": "^2.3.1", "iconv-lite": "^0.7.0", "long": "^5.2.1", "lru.min": "^1.0.0", "named-placeholders": "^1.1.3", "seq-queue": "^0.0.5", "sqlstring": "^2.3.2" } }, "sha512-Cs/jx3WZPNrYHVz+Iunp9ziahaG5uFMvD2R8Zlmc194AqXNxt9HBNu7ZsPYrUtmJsF0egETCWIdMIYAwOGjL1w=="], "mz": ["mz@2.7.0", "", { "dependencies": { "any-promise": "^1.0.0", "object-assign": "^4.0.1", "thenify-all": "^1.0.0" } }, "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q=="], @@ -4944,20 +4524,12 @@ "nanoid": ["nanoid@3.3.12", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ=="], - "napi-build-utils": ["napi-build-utils@2.0.0", "", {}, "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA=="], - "negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], "neotraverse": ["neotraverse@0.6.18", "", {}, "sha512-Z4SmBUweYa09+o6pG+eASabEpP6QkQ70yHj351pQoEXIs8uHbaU2DWVmzBANKgflPa47A50PtB2+NgRpQvr7vA=="], - "netmask": ["netmask@2.1.1", "", {}, "sha512-eonl3sLUha+S1GzTPxychyhnUzKyeQkZ7jLjKrBagJgPla13F+uQ71HgpFefyHgqrjEbCPkDArxYsjY8/+gLKA=="], - - "next-mdx-remote-client": ["next-mdx-remote-client@1.1.8", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@mdx-js/mdx": "^3.1.1", "@mdx-js/react": "^3.1.1", "@types/mdx": "^2.0.13", "remark-mdx-remove-esm": "^1.3.2", "serialize-error": "^13.0.1", "vfile": "^6.0.3", "vfile-matter": "^5.0.1" }, "peerDependencies": { "react": ">= 18.3.0 < 19.0.0", "react-dom": ">= 18.3.0 < 19.0.0" } }, "sha512-IElOrn02JjGQZxx+re7wMx/1AUG+Arte9aDImAtxjAfMw6xuSCaH5mTCunKelkWzFyFdRb565jO8jRICvvh96g=="], - "nf3": ["nf3@0.1.12", "", {}, "sha512-qbMXT7RTGh74MYWPeqTIED8nDW70NXOULVHpdWcdZ7IVHVnAsMV9fNugSNnvooipDc1FMOzpis7T9nXJEbJhvQ=="], - "nimma": ["nimma@0.2.3", "", { "dependencies": { "@jsep-plugin/regex": "^1.0.1", "@jsep-plugin/ternary": "^1.0.2", "astring": "^1.8.1", "jsep": "^1.2.0" }, "optionalDependencies": { "jsonpath-plus": "^6.0.1 || ^10.1.0", "lodash.topath": "^4.5.2" } }, "sha512-1ZOI8J+1PKKGceo/5CT5GfQOG6H8I2BencSK06YarZ2wXwH37BSSUWldqJmMJYA5JfqDqffxDXynt6f11AyKcA=="], - "nitro": ["nitro@3.0.1-alpha.1", "", { "dependencies": { "consola": "^3.4.2", "crossws": "^0.4.1", "db0": "^0.3.4", "h3": "2.0.1-rc.5", "jiti": "^2.6.1", "nf3": "^0.1.10", "ofetch": "^2.0.0-alpha.3", "ohash": "^2.0.11", "oxc-minify": "^0.96.0", "oxc-transform": "^0.96.0", "srvx": "^0.9.5", "undici": "^7.16.0", "unenv": "^2.0.0-rc.24", "unstorage": "^2.0.0-alpha.4" }, "peerDependencies": { "rolldown": "*", "rollup": "^4", "vite": "^7", "xml2js": "^0.6.2" }, "optionalPeers": ["rolldown", "rollup", "vite", "xml2js"], "bin": { "nitro": "dist/cli/index.mjs" } }, "sha512-U4AxIsXxdkxzkFrK0XAw0e5Qbojk8jQ50MjjRBtBakC4HurTtQoiZvF+lSe382jhuQZCfAyywGWOFa9QzXLFaw=="], "nlcst-to-string": ["nlcst-to-string@4.0.0", "", { "dependencies": { "@types/nlcst": "^2.0.0" } }, "sha512-YKLBCcUYKAg0FNlOBT6aI91qFmSiFKiluk655WzPF+DDMA02qIyy8uiRqI8QXtcFpEvll12LpL5MXqEmAZ+dcA=="], @@ -4990,8 +4562,6 @@ "node-releases": ["node-releases@2.0.46", "", {}, "sha512-GYVXHE2KnrzAfsAjl4uP++evGFCrAU1jta4ubEjIG7YWt/64Gqv66a30yKwWczVjA6j3bM4nBwH7Pk1JmDHaxQ=="], - "non-error": ["non-error@0.1.0", "", {}, "sha512-TMB1uHiGsHRGv1uYclfhivcnf0/PdFp2pNqRxXjncaAsjYMoisaQJI+SSZCqRq+VliwRTC8tsMQfmrWjDMhkPQ=="], - "nopt": ["nopt@9.0.0", "", { "dependencies": { "abbrev": "^4.0.0" }, "bin": { "nopt": "bin/nopt.js" } }, "sha512-Zhq3a+yFKrYwSBluL4H9XP3m3y5uvQkB/09CwDruCiRmR/UJYnn9W4R48ry0uGC70aeTPKLynBtscP9efFFcPw=="], "normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="], @@ -5018,8 +4588,6 @@ "nypm": ["nypm@0.6.6", "", { "dependencies": { "citty": "^0.2.2", "pathe": "^2.0.3", "tinyexec": "^1.1.1" }, "bin": { "nypm": "dist/cli.mjs" } }, "sha512-vRyr0r4cbBapw07Xw8xrj9Teq3o7MUD35rSaTcanDbW+aK2XHDgJFiU6ZTj2GBw7Q12ysdsyFss+Vdz4hQ0Y6Q=="], - "oauth4webapi": ["oauth4webapi@3.8.6", "", {}, "sha512-iwemM91xz8nryHti2yTmg5fhyEMVOkOXwHNqbvcATjyajb5oQxCQzrNOA6uElRHuMhQQTKUyFKV9y/CNyg25BQ=="], - "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], "object-hash": ["object-hash@2.2.0", "", {}, "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw=="], @@ -5066,8 +4634,6 @@ "opentui-spinner": ["opentui-spinner@0.0.7", "", { "dependencies": { "cli-spinners": "^3.3.0" }, "peerDependencies": { "@opentui/core": "^0.3.4", "@opentui/react": "^0.3.4", "@opentui/solid": "^0.3.4", "typescript": "^5" }, "optionalPeers": ["@opentui/react", "@opentui/solid"] }, "sha512-nPzwAvJG+y9rVEwwHLHqbsMzLnIk2zw+F9LqwA7aYJvpM5gsrKC2rrGi36A+tZpA+1RnWxXeWEgVZMchnaH18Q=="], - "os-paths": ["os-paths@4.4.0", "", {}, "sha512-wrAwOeXp1RRMFfQY8Sy7VaGVmPocaLwSFOYCGKSyo8qmJ+/yaafCl5BCA1IQZWqFSRBrKDYFeR9d/VyQzfH/jg=="], - "own-keys": ["own-keys@1.0.1", "", { "dependencies": { "get-intrinsic": "^1.2.6", "object-keys": "^1.1.1", "safe-push-apply": "^1.0.0" } }, "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg=="], "oxc-minify": ["oxc-minify@0.96.0", "", { "optionalDependencies": { "@oxc-minify/binding-android-arm64": "0.96.0", "@oxc-minify/binding-darwin-arm64": "0.96.0", "@oxc-minify/binding-darwin-x64": "0.96.0", "@oxc-minify/binding-freebsd-x64": "0.96.0", "@oxc-minify/binding-linux-arm-gnueabihf": "0.96.0", "@oxc-minify/binding-linux-arm-musleabihf": "0.96.0", "@oxc-minify/binding-linux-arm64-gnu": "0.96.0", "@oxc-minify/binding-linux-arm64-musl": "0.96.0", "@oxc-minify/binding-linux-riscv64-gnu": "0.96.0", "@oxc-minify/binding-linux-s390x-gnu": "0.96.0", "@oxc-minify/binding-linux-x64-gnu": "0.96.0", "@oxc-minify/binding-linux-x64-musl": "0.96.0", "@oxc-minify/binding-wasm32-wasi": "0.96.0", "@oxc-minify/binding-win32-arm64-msvc": "0.96.0", "@oxc-minify/binding-win32-x64-msvc": "0.96.0" } }, "sha512-dXeeGrfPJJ4rMdw+NrqiCRtbzVX2ogq//R0Xns08zql2HjV3Zi2SBJ65saqfDaJzd2bcHqvGWH+M44EQCHPAcA=="], @@ -5082,8 +4648,6 @@ "oxlint-tsgolint": ["oxlint-tsgolint@0.21.0", "", { "optionalDependencies": { "@oxlint-tsgolint/darwin-arm64": "0.21.0", "@oxlint-tsgolint/darwin-x64": "0.21.0", "@oxlint-tsgolint/linux-arm64": "0.21.0", "@oxlint-tsgolint/linux-x64": "0.21.0", "@oxlint-tsgolint/win32-arm64": "0.21.0", "@oxlint-tsgolint/win32-x64": "0.21.0" }, "bin": { "tsgolint": "bin/tsgolint.js" } }, "sha512-HiWPhANwRnN1pZJQ2SgNB3WRR+1etLJHmRzQ/MJhyINsEIaOUCjxhlXJKbEaVUwdnyXwRWqo/P9Fx21lz0/mSg=="], - "p-any": ["p-any@4.0.0", "", { "dependencies": { "p-cancelable": "^3.0.0", "p-some": "^6.0.0" } }, "sha512-S/B50s+pAVe0wmEZHmBs/9yJXeZ5KhHzOsgKzt0hRdgkoR3DxW9ts46fcsWi/r3VnzsnkKS7q4uimze+zjdryw=="], - "p-cancelable": ["p-cancelable@2.1.1", "", {}, "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg=="], "p-defer": ["p-defer@3.0.0", "", {}, "sha512-ugZxsxmtTln604yeYd29EGrNhazN2lywetzpKhfmQjW/VJmhpDmWbiX+h0zL8V91R0UXkhb3KtPmyq9PZw3aYw=="], @@ -5100,16 +4664,10 @@ "p-retry": ["p-retry@4.6.2", "", { "dependencies": { "@types/retry": "0.12.0", "retry": "^0.13.1" } }, "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ=="], - "p-some": ["p-some@6.0.0", "", { "dependencies": { "aggregate-error": "^4.0.0", "p-cancelable": "^3.0.0" } }, "sha512-CJbQCKdfSX3fIh8/QKgS+9rjm7OBNUTmwWswAFQAhc8j1NR1dsEDETUEuVUtQHZpV+J03LqWBEwvu0g1Yn+TYg=="], - "p-timeout": ["p-timeout@6.1.4", "", {}, "sha512-MyIV3ZA/PmyBN/ud8vV9XzwTrNtR4jFrObymZYnZqMmW0zA8Z17vnT0rBgFE/TlohB+YCHqXMgZzb3Csp49vqg=="], "p-try": ["p-try@2.2.0", "", {}, "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ=="], - "pac-proxy-agent": ["pac-proxy-agent@7.2.0", "", { "dependencies": { "@tootallnate/quickjs-emscripten": "^0.23.0", "agent-base": "^7.1.2", "debug": "^4.3.4", "get-uri": "^6.0.1", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.6", "pac-resolver": "^7.0.1", "socks-proxy-agent": "^8.0.5" } }, "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA=="], - - "pac-resolver": ["pac-resolver@7.0.1", "", { "dependencies": { "degenerator": "^5.0.0", "netmask": "^2.0.2" } }, "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg=="], - "package-json-from-dist": ["package-json-from-dist@1.0.1", "", {}, "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="], "package-manager-detector": ["package-manager-detector@1.6.0", "", {}, "sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA=="], @@ -5122,14 +4680,10 @@ "param-case": ["param-case@3.0.4", "", { "dependencies": { "dot-case": "^3.0.4", "tslib": "^2.0.3" } }, "sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A=="], - "parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="], - "parse-conflict-json": ["parse-conflict-json@5.0.1", "", { "dependencies": { "json-parse-even-better-errors": "^5.0.0", "just-diff": "^6.0.0", "just-diff-apply": "^5.2.0" } }, "sha512-ZHEmNKMq1wyJXNwLxyHnluPfRAFSIliBvbK/UiOceROt4Xh9Pz0fq49NytIaeaCUf5VR86hwQ/34FCcNU5/LKQ=="], "parse-entities": ["parse-entities@4.0.2", "", { "dependencies": { "@types/unist": "^2.0.0", "character-entities-legacy": "^3.0.0", "character-reference-invalid": "^2.0.0", "decode-named-character-reference": "^1.0.0", "is-alphanumerical": "^2.0.0", "is-decimal": "^2.0.0", "is-hexadecimal": "^2.0.0" } }, "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw=="], - "parse-json": ["parse-json@5.2.0", "", { "dependencies": { "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", "json-parse-even-better-errors": "^2.3.0", "lines-and-columns": "^1.1.6" } }, "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg=="], - "parse-latin": ["parse-latin@7.0.0", "", { "dependencies": { "@types/nlcst": "^2.0.0", "@types/unist": "^3.0.0", "nlcst-to-string": "^4.0.0", "unist-util-modify-children": "^4.0.0", "unist-util-visit-children": "^3.0.0", "vfile": "^6.0.0" } }, "sha512-mhHgobPPua5kZ98EF4HWiH167JWBfl4pvAIXXdbaVohtK7a6YBOy56kvhCqduqyo/f3yrHFWmqmiMg/BkBkYYQ=="], "parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="], @@ -5144,8 +4698,6 @@ "pascal-case": ["pascal-case@3.1.2", "", { "dependencies": { "no-case": "^3.0.4", "tslib": "^2.0.3" } }, "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g=="], - "patch-console": ["patch-console@2.0.0", "", {}, "sha512-0YNdUceMdaQwoKce1gatDScmMo5pu/tfABfnzEqeG0gtTmd7mh/WcwgUjtAeOU7N8nFFlbQBnFK2gXW5fGvmMA=="], - "path-browserify": ["path-browserify@1.0.1", "", {}, "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g=="], "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], @@ -5210,8 +4762,6 @@ "poe-oauth": ["poe-oauth@0.0.8", "", {}, "sha512-zlaRVLR6vuxBIYUkZoTIVo3f8h3qd27gv9Ms+kmGiYEiiV4TdccddTdNcGyI0DnuJ9tVi+5LP3Bvzez59IFbjw=="], - "pony-cause": ["pony-cause@1.1.1", "", {}, "sha512-PxkIc/2ZpLiEzQXu5YRDOUgBlfGYBY8156HY5ZcRAwwonMk5W/MrJP2LLkG/hF7GEQzaHo2aS7ho6ZLCOvf+6g=="], - "possible-typed-array-names": ["possible-typed-array-names@1.1.0", "", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="], "postcss": ["postcss@8.5.15", "", { "dependencies": { "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A=="], @@ -5232,8 +4782,6 @@ "postgres": ["postgres@3.4.7", "", {}, "sha512-Jtc2612XINuBjIl/QTWsV5UvE8UHuNblcO3vVADSrKsrc6RqGX6lOW1cEo3CM2v0XG4Nat8nI+YM7/f26VxXLw=="], - "posthog-node": ["posthog-node@5.17.2", "", { "dependencies": { "@posthog/core": "1.7.1" } }, "sha512-lz3YJOr0Nmiz0yHASaINEDHqoV+0bC3eD8aZAG+Ky292dAnVYul+ga/dMX8KCBXg8hHfKdxw0SztYD5j6dgUqQ=="], - "postject": ["postject@1.0.0-alpha.6", "", { "dependencies": { "commander": "^9.4.0" }, "bin": { "postject": "dist/cli.js" } }, "sha512-b9Eb8h2eVqNE8edvKdwqkrY6O7kAwmI8kcnBv1NScolYJbo59XUF0noFq+lxbC1yN20bmC0WBEbDC5H/7ASb0A=="], "powershell-utils": ["powershell-utils@0.1.0", "", {}, "sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A=="], @@ -5242,8 +4790,6 @@ "preact-render-to-string": ["preact-render-to-string@6.6.5", "", { "peerDependencies": { "preact": ">=10 || >= 11.0.0-0" } }, "sha512-O6MHzYNIKYaiSX3bOw0gGZfEbOmlIDtDfWwN1JJdc/T3ihzRT6tGGSEWE088dWrEDGa1u7101q+6fzQnO9XCPA=="], - "prebuild-install": ["prebuild-install@7.1.3", "", { "dependencies": { "detect-libc": "^2.0.0", "expand-template": "^2.0.3", "github-from-package": "0.0.0", "minimist": "^1.2.3", "mkdirp-classic": "^0.5.3", "napi-build-utils": "^2.0.0", "node-abi": "^3.3.0", "pump": "^3.0.0", "rc": "^1.2.7", "simple-get": "^4.0.0", "tar-fs": "^2.0.0", "tunnel-agent": "^0.6.0" }, "bin": { "prebuild-install": "bin.js" } }, "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug=="], - "prettier": ["prettier@3.6.2", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ=="], "pretty": ["pretty@2.0.0", "", { "dependencies": { "condense-newlines": "^0.2.1", "extend-shallow": "^2.0.1", "js-beautify": "^1.6.12" } }, "sha512-G9xUchgTEiNpormdYBl+Pha50gOUovT18IvAe7EYMZ1/f9W/WWMPRn+xI68yXNMUk3QXHDwo/1wV/4NejVNe1w=="], @@ -5284,22 +4830,14 @@ "proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="], - "proxy-agent": ["proxy-agent@6.5.0", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "^4.3.4", "http-proxy-agent": "^7.0.1", "https-proxy-agent": "^7.0.6", "lru-cache": "^7.14.1", "pac-proxy-agent": "^7.1.0", "proxy-from-env": "^1.1.0", "socks-proxy-agent": "^8.0.5" } }, "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A=="], - "proxy-from-env": ["proxy-from-env@2.1.0", "", {}, "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA=="], - "public-ip": ["public-ip@5.0.0", "", { "dependencies": { "dns-socket": "^4.2.2", "got": "^12.0.0", "is-ip": "^3.1.0" } }, "sha512-xaH3pZMni/R2BG7ZXXaWS9Wc9wFlhyDVJF47IJ+3ali0TGv+2PsckKxbmo+rnx3ZxiV2wblVhtdS3bohAP6GGw=="], - "pump": ["pump@3.0.4", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA=="], "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], "pupa": ["pupa@3.3.0", "", { "dependencies": { "escape-goat": "^4.0.0" } }, "sha512-LjgDO2zPtoXP2wJpDjZrGdojii1uqO0cnwKoIoUzkfS98HDmbeiGmYiXo3lXeFlq2xvne1QFQhwYXSUCLKtEuA=="], - "puppeteer": ["puppeteer@22.14.0", "", { "dependencies": { "@puppeteer/browsers": "2.3.0", "cosmiconfig": "^9.0.0", "devtools-protocol": "0.0.1312386", "puppeteer-core": "22.14.0" }, "bin": { "puppeteer": "lib/esm/puppeteer/node/cli.js" } }, "sha512-MGTR6/pM8zmWbTdazb6FKnwIihzsSEXBPH49mFFU96DNZpQOevCAZMnjBZGlZRGRzRK6aADCavR6SQtrbv5dQw=="], - - "puppeteer-core": ["puppeteer-core@22.14.0", "", { "dependencies": { "@puppeteer/browsers": "2.3.0", "chromium-bidi": "0.6.2", "debug": "^4.3.5", "devtools-protocol": "0.0.1312386", "ws": "^8.18.0" } }, "sha512-rl4tOY5LcA3e374GAlsGGHc05HL3eGNf5rZ+uxkl6id9zVZKcwcp1Z+Nd6byb6WPiPeecT/dwz8f/iUm+AZQSw=="], - "pure-rand": ["pure-rand@8.4.0", "", {}, "sha512-IoM8YF/jY0hiugFo/wOWqfmarlE6J0wc6fDK1PhftMk7MGhVZl88sZimmqBBFomLOCSmcCCpsfj7wXASCpvK9A=="], "pvtsutils": ["pvtsutils@1.3.6", "", { "dependencies": { "tslib": "^2.8.1" } }, "sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg=="], @@ -5320,9 +4858,7 @@ "range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="], - "raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="], - - "rc": ["rc@1.2.8", "", { "dependencies": { "deep-extend": "^0.6.0", "ini": "~1.3.0", "minimist": "^1.2.0", "strip-json-comments": "~2.0.1" }, "bin": { "rc": "./cli.js" } }, "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw=="], + "raw-body": ["raw-body@2.5.3", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.4.24", "unpipe": "~1.0.0" } }, "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA=="], "rc9": ["rc9@2.1.2", "", { "dependencies": { "defu": "^6.1.4", "destr": "^2.0.3" } }, "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg=="], @@ -5334,8 +4870,6 @@ "react-is": ["react-is@17.0.2", "", {}, "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="], - "react-reconciler": ["react-reconciler@0.32.0", "", { "dependencies": { "scheduler": "^0.26.0" }, "peerDependencies": { "react": "^19.1.0" } }, "sha512-2NPMOzgTlG0ZWdIf3qG+dcbLSoAc/uLfOwckc3ofy5sSK0pLJqnQLpUFxvGcN2rlXSjnVtGeeFLNimCQEj5gOQ=="], - "react-refresh": ["react-refresh@0.17.0", "", {}, "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ=="], "react-remove-scroll": ["react-remove-scroll@2.5.5", "", { "dependencies": { "react-remove-scroll-bar": "^2.3.3", "react-style-singleton": "^2.2.1", "tslib": "^2.1.0", "use-callback-ref": "^1.3.0", "use-sidecar": "^1.1.2" }, "peerDependencies": { "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", "react": "^16.8.0 || ^17.0.0 || ^18.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-ImKhrzJJsyXJfBZ4bzu8Bwpka14c/fQt0k+cyFp/PBhTfyDnU5hjOtM4AG/0AMyy8oKzOTR0lDgJIM7pYXI0kw=="], @@ -5396,10 +4930,6 @@ "rehype-format": ["rehype-format@5.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-format": "^1.0.0" } }, "sha512-zvmVru9uB0josBVpr946OR8ui7nJEdzZobwLOOqHb/OOD88W0Vk2SqLwoVOj0fM6IPCCO6TaV9CvQvJMWwukFQ=="], - "rehype-katex": ["rehype-katex@7.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/katex": "^0.16.0", "hast-util-from-html-isomorphic": "^2.0.0", "hast-util-to-text": "^4.0.0", "katex": "^0.16.0", "unist-util-visit-parents": "^6.0.0", "vfile": "^6.0.0" } }, "sha512-OiM2wrZ/wuhKkigASodFoo8wimG3H12LWQaH8qSPVJn9apWKFSH3YOCtbKpBorTVw/eI7cuT21XBbvwEswbIOA=="], - - "rehype-minify-whitespace": ["rehype-minify-whitespace@6.0.2", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-minify-whitespace": "^1.0.0" } }, "sha512-Zk0pyQ06A3Lyxhe9vGtOtzz3Z0+qZ5+7icZ/PL/2x1SHPbKao5oB/g/rlc6BCTajqBb33JcOe71Ye1oFsuYbnw=="], - "rehype-parse": ["rehype-parse@9.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-from-html": "^2.0.0", "unified": "^11.0.0" } }, "sha512-ksCzCD0Fgfh7trPDxr2rSylbwq9iYDkSn8TCDmEJ49ljEUBxDVCzCHv7QNzZOfODanX4+bWQ4WZqLCRWYLfhag=="], "rehype-raw": ["rehype-raw@7.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-raw": "^9.0.0", "vfile": "^6.0.0" } }, "sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww=="], @@ -5410,19 +4940,11 @@ "relateurl": ["relateurl@0.2.7", "", {}, "sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog=="], - "remark": ["remark@15.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "remark-parse": "^11.0.0", "remark-stringify": "^11.0.0", "unified": "^11.0.0" } }, "sha512-Eht5w30ruCXgFmxVUSlNWQ9iiimq07URKeFS3hNc8cUWy1llX4KDWfyEDZRycMc+znsN9Ux5/tJ/BFdgdOwA3A=="], - "remark-directive": ["remark-directive@3.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-directive": "^3.0.0", "micromark-extension-directive": "^3.0.0", "unified": "^11.0.0" } }, "sha512-gwglrEQEZcZYgVyG1tQuA+h58EZfq5CSULw7J90AFuCTyib1thgHPoqQ+h9iFvU6R+vnZ5oNFQR5QKgGpk741A=="], - "remark-frontmatter": ["remark-frontmatter@5.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-frontmatter": "^2.0.0", "micromark-extension-frontmatter": "^2.0.0", "unified": "^11.0.0" } }, "sha512-XTFYvNASMe5iPN0719nPrdItC9aU0ssC4v14mH1BCi1u0n1gAocqcujWUrByftZTbLhRtiKRyjYTSIOcr69UVQ=="], - "remark-gfm": ["remark-gfm@4.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-gfm": "^3.0.0", "micromark-extension-gfm": "^3.0.0", "remark-parse": "^11.0.0", "remark-stringify": "^11.0.0", "unified": "^11.0.0" } }, "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg=="], - "remark-math": ["remark-math@6.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-math": "^3.0.0", "micromark-extension-math": "^3.0.0", "unified": "^11.0.0" } }, "sha512-MMqgnP74Igy+S3WwnhQ7kqGlEerTETXMvJhrUzDikVZ2/uogJCb+WHUg97hK9/jcfc0dkD73s3LN8zU49cTEtA=="], - - "remark-mdx": ["remark-mdx@3.1.0", "", { "dependencies": { "mdast-util-mdx": "^3.0.0", "micromark-extension-mdxjs": "^3.0.0" } }, "sha512-Ngl/H3YXyBV9RcRNdlYsZujAmhsxwzxpDzpDEhFBVAGthS4GDgnctpDjgFl/ULx5UEDzqtW1cyBSNKqYYrqLBA=="], - - "remark-mdx-remove-esm": ["remark-mdx-remove-esm@1.3.2", "", { "dependencies": { "@types/mdast": "^4.0.4", "unist-util-remove": "^4.0.0" }, "peerDependencies": { "unified": "^11" } }, "sha512-BvL8VSdVXy9S7NlHP56nUJAHFc45h5E9HnHiLUGHe5tw3Yvm/3cVZvAzlkEEh2i+fkq2uKrf2xn5VmItBhMypA=="], + "remark-mdx": ["remark-mdx@3.1.1", "", { "dependencies": { "mdast-util-mdx": "^3.0.0", "micromark-extension-mdxjs": "^3.0.0" } }, "sha512-Pjj2IYlUY3+D8x00UJsIOg5BEvfMyeI+2uLPn9VO9Wg4MEtN/VTIq2NEJQfde9PnX15KgtHyl9S0BcTnWrIuWg=="], "remark-parse": ["remark-parse@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", "micromark-util-types": "^2.0.0", "unified": "^11.0.0" } }, "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA=="], @@ -5458,8 +4980,6 @@ "responselike": ["responselike@2.0.1", "", { "dependencies": { "lowercase-keys": "^2.0.0" } }, "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw=="], - "restore-cursor": ["restore-cursor@4.0.0", "", { "dependencies": { "onetime": "^5.1.0", "signal-exit": "^3.0.2" } }, "sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg=="], - "restructure": ["restructure@3.0.2", "", {}, "sha512-gSfoiOEA0VPE6Tukkrr7I0RBdE0s7H1eFCDBk05l1KIQT1UIKNc5JZy6jdyW6eYH3aR3g5b3PuL77rq0hvwtAw=="], "ret": ["ret@0.5.0", "", {}, "sha512-I1XxrZSQ+oErkRR4jYbAyEEu2I0avBvvMM5JN+6EBprOGRCs63ENqZ3vjavq8fBw2+62G5LF5XelKwuJpcvcxw=="], @@ -5482,8 +5002,6 @@ "roarr": ["roarr@2.15.4", "", { "dependencies": { "boolean": "^3.0.1", "detect-node": "^2.0.4", "globalthis": "^1.0.1", "json-stringify-safe": "^5.0.1", "semver-compare": "^1.0.0", "sprintf-js": "^1.1.2" } }, "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A=="], - "rolldown": ["rolldown@1.1.4", "", { "dependencies": { "@oxc-project/types": "=0.138.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.1.4", "@rolldown/binding-darwin-arm64": "1.1.4", "@rolldown/binding-darwin-x64": "1.1.4", "@rolldown/binding-freebsd-x64": "1.1.4", "@rolldown/binding-linux-arm-gnueabihf": "1.1.4", "@rolldown/binding-linux-arm64-gnu": "1.1.4", "@rolldown/binding-linux-arm64-musl": "1.1.4", "@rolldown/binding-linux-ppc64-gnu": "1.1.4", "@rolldown/binding-linux-s390x-gnu": "1.1.4", "@rolldown/binding-linux-x64-gnu": "1.1.4", "@rolldown/binding-linux-x64-musl": "1.1.4", "@rolldown/binding-openharmony-arm64": "1.1.4", "@rolldown/binding-wasm32-wasi": "1.1.4", "@rolldown/binding-win32-arm64-msvc": "1.1.4", "@rolldown/binding-win32-x64-msvc": "1.1.4" }, "bin": { "rolldown": "./bin/cli.mjs" } }, "sha512-IjZYiLxZwpnhwhdBH2ugdTGVSdhCQUmLxLoqyjiL0JxYjyRst+5a0P3xfrTxJ5F638j4Mvvw5FAX5XE6eHpXbA=="], - "rollup": ["rollup@4.60.4", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.60.4", "@rollup/rollup-android-arm64": "4.60.4", "@rollup/rollup-darwin-arm64": "4.60.4", "@rollup/rollup-darwin-x64": "4.60.4", "@rollup/rollup-freebsd-arm64": "4.60.4", "@rollup/rollup-freebsd-x64": "4.60.4", "@rollup/rollup-linux-arm-gnueabihf": "4.60.4", "@rollup/rollup-linux-arm-musleabihf": "4.60.4", "@rollup/rollup-linux-arm64-gnu": "4.60.4", "@rollup/rollup-linux-arm64-musl": "4.60.4", "@rollup/rollup-linux-loong64-gnu": "4.60.4", "@rollup/rollup-linux-loong64-musl": "4.60.4", "@rollup/rollup-linux-ppc64-gnu": "4.60.4", "@rollup/rollup-linux-ppc64-musl": "4.60.4", "@rollup/rollup-linux-riscv64-gnu": "4.60.4", "@rollup/rollup-linux-riscv64-musl": "4.60.4", "@rollup/rollup-linux-s390x-gnu": "4.60.4", "@rollup/rollup-linux-x64-gnu": "4.60.4", "@rollup/rollup-linux-x64-musl": "4.60.4", "@rollup/rollup-openbsd-x64": "4.60.4", "@rollup/rollup-openharmony-arm64": "4.60.4", "@rollup/rollup-win32-arm64-msvc": "4.60.4", "@rollup/rollup-win32-ia32-msvc": "4.60.4", "@rollup/rollup-win32-x64-gnu": "4.60.4", "@rollup/rollup-win32-x64-msvc": "4.60.4", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-WHeFSbZYsPu3+bLoNRUuAO+wavNlocOPf3wSHTP7hcFKVnJeWsYlCDbr3mTS14FCizf9ccIxXA8sGL8zKeQN3g=="], "rou3": ["rou3@0.7.12", "", {}, "sha512-iFE4hLDuloSWcD7mjdCDhx2bKcIsYbtOTpfH5MHHLSKMOUyjqQXTeZVa289uuwEGEKFoE/BAPbhaU4B774nceg=="], @@ -5492,12 +5010,8 @@ "run-applescript": ["run-applescript@7.1.0", "", {}, "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q=="], - "run-async": ["run-async@3.0.0", "", {}, "sha512-540WwVDOMxA6dN6We19EcT9sc3hkXPw5mzRNGM3FkdN/vtE9NFvj5lFAPNwUDmJjXidm3v7TC1cTE7t17Ulm1Q=="], - "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], - "rxjs": ["rxjs@7.8.2", "", { "dependencies": { "tslib": "^2.1.0" } }, "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA=="], - "s-js": ["s-js@0.4.9", "", {}, "sha512-RtpOm+cM6O0sHg6IA70wH+UC3FZcND+rccBZpBAHzlUgNO2Bm5BN+FnM8+OBxzXdwpKWFwX11JGF0MFRkhSoIQ=="], "safe-array-concat": ["safe-array-concat@1.1.4", "", { "dependencies": { "call-bind": "^1.0.9", "call-bound": "^1.0.4", "get-intrinsic": "^1.3.0", "has-symbols": "^1.1.0", "isarray": "^2.0.5" } }, "sha512-wtZlHyOje6OZTGqAoaDKxFkgRtkF9CnHAVnCHKfuj200wAgL+bSJhdsCD2l0Qx/2ekEXjPWcyKkfGb5CPboslg=="], @@ -5530,7 +5044,7 @@ "semver-compare": ["semver-compare@1.0.0", "", {}, "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow=="], - "send": ["send@1.2.1", "", { "dependencies": { "debug": "^4.4.3", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.1", "mime-types": "^3.0.2", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.2" } }, "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ=="], + "send": ["send@0.19.2", "", { "dependencies": { "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "fresh": "~0.5.2", "http-errors": "~2.0.1", "mime": "1.6.0", "ms": "2.1.3", "on-finished": "~2.4.1", "range-parser": "~1.2.1", "statuses": "~2.0.2" } }, "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg=="], "seq-queue": ["seq-queue@0.0.5", "", {}, "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q=="], @@ -5540,7 +5054,7 @@ "seroval-plugins": ["seroval-plugins@1.3.3", "", { "peerDependencies": { "seroval": "^1.0" } }, "sha512-16OL3NnUBw8JG1jBLUoZJsLnQq0n5Ua6aHalhJK4fMQkz1lqR7Osz1sA30trBtd9VUDc2NgkuRCn8+/pBwqZ+w=="], - "serve-static": ["serve-static@2.2.1", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw=="], + "serve-static": ["serve-static@1.16.3", "", { "dependencies": { "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", "send": "~0.19.1" } }, "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA=="], "set-cookie-parser": ["set-cookie-parser@2.7.2", "", {}, "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw=="], @@ -5554,8 +5068,6 @@ "sharp": ["sharp@0.33.5", "", { "dependencies": { "color": "^4.2.3", "detect-libc": "^2.0.3", "semver": "^7.6.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.33.5", "@img/sharp-darwin-x64": "0.33.5", "@img/sharp-libvips-darwin-arm64": "1.0.4", "@img/sharp-libvips-darwin-x64": "1.0.4", "@img/sharp-libvips-linux-arm": "1.0.5", "@img/sharp-libvips-linux-arm64": "1.0.4", "@img/sharp-libvips-linux-s390x": "1.0.4", "@img/sharp-libvips-linux-x64": "1.0.4", "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", "@img/sharp-libvips-linuxmusl-x64": "1.0.4", "@img/sharp-linux-arm": "0.33.5", "@img/sharp-linux-arm64": "0.33.5", "@img/sharp-linux-s390x": "0.33.5", "@img/sharp-linux-x64": "0.33.5", "@img/sharp-linuxmusl-arm64": "0.33.5", "@img/sharp-linuxmusl-x64": "0.33.5", "@img/sharp-wasm32": "0.33.5", "@img/sharp-win32-ia32": "0.33.5", "@img/sharp-win32-x64": "0.33.5" } }, "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw=="], - "sharp-ico": ["sharp-ico@0.1.5", "", { "dependencies": { "decode-ico": "*", "ico-endec": "*", "sharp": "*" } }, "sha512-a3jODQl82NPp1d5OYb0wY+oFaPk7AvyxipIowCHk7pBsZCWgbe0yAkU2OOXdoH0ENyANhyOQbs9xkAiRHcF02Q=="], - "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], @@ -5578,10 +5090,6 @@ "sigstore": ["sigstore@4.1.1", "", { "dependencies": { "@sigstore/bundle": "^4.0.0", "@sigstore/core": "^3.2.1", "@sigstore/protobuf-specs": "^0.5.0", "@sigstore/sign": "^4.1.1", "@sigstore/tuf": "^4.0.2", "@sigstore/verify": "^3.1.1" } }, "sha512-endqECJkfhozrXMK5ngu/UAA0xVcVEFdnHJCElGaExypjW+HK5i6zu3NteLoaX/iFbRUbC3+DjttQs0GARr+5w=="], - "simple-concat": ["simple-concat@1.0.1", "", {}, "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q=="], - - "simple-get": ["simple-get@4.0.1", "", { "dependencies": { "decompress-response": "^6.0.0", "once": "^1.3.1", "simple-concat": "^1.0.0" } }, "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA=="], - "simple-swizzle": ["simple-swizzle@0.2.4", "", { "dependencies": { "is-arrayish": "^0.3.1" } }, "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw=="], "simple-update-notifier": ["simple-update-notifier@2.0.0", "", { "dependencies": { "semver": "^7.5.3" } }, "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w=="], @@ -5598,10 +5106,6 @@ "smol-toml": ["smol-toml@1.6.1", "", {}, "sha512-dWUG8F5sIIARXih1DTaQAX4SsiTXhInKf1buxdY9DIg4ZYPZK5nGM1VRIYmEbDbsHt7USo99xSLFu5Q1IqTmsg=="], - "socket.io": ["socket.io@4.8.0", "", { "dependencies": { "accepts": "~1.3.4", "base64id": "~2.0.0", "cors": "~2.8.5", "debug": "~4.3.2", "engine.io": "~6.6.0", "socket.io-adapter": "~2.5.2", "socket.io-parser": "~4.2.4" } }, "sha512-8U6BEgGjQOfGz3HHTYaC/L1GaxDCJ/KM0XTkJly0EhZ5U/du9uNEZy4ZgYzEzIqlx2CMm25CrCqr1ck899eLNA=="], - - "socket.io-adapter": ["socket.io-adapter@2.5.8", "", { "dependencies": { "debug": "~4.4.1", "ws": "~8.21.0" } }, "sha512-6Oy52pbg+kvdCVvjcN+FnY7BvxZ7cIHNScbvztT/It5d0vbwoJoVZmF2gjJmnV0/4WlXRfG15zc45ySk9Ah8bw=="], - "socket.io-client": ["socket.io-client@4.8.3", "", { "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.4.1", "engine.io-client": "~6.6.1", "socket.io-parser": "~4.2.4" } }, "sha512-uP0bpjWrjQmUt5DTHq9RuoCBdFJF10cdX9X+a368j/Ft0wmaVgxlrjvK3kjvgCODOMMOz9lcaRzxmso0bTWZ/g=="], "socket.io-parser": ["socket.io-parser@4.2.6", "", { "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.4.1" } }, "sha512-asJqbVBDsBCJx0pTqw3WfesSY0iRX+2xzWEWzrpcH7L6fLzrhyF8WPI8UaeM4YCuDfpwA/cgsdugMsmtz8EJeg=="], @@ -5624,6 +5128,8 @@ "solid-refresh": ["solid-refresh@0.6.3", "", { "dependencies": { "@babel/generator": "^7.23.6", "@babel/helper-module-imports": "^7.22.15", "@babel/types": "^7.23.6" }, "peerDependencies": { "solid-js": "^1.3" } }, "sha512-F3aPsX6hVw9ttm5LYlth8Q15x6MlI/J3Dn+o3EQyRTtTxidepSTwAYdozt01/YA+7ObcciagGEyXIopGZzQtbA=="], + "solid-sonner": ["solid-sonner@0.3.1", "", { "peerDependencies": { "solid-js": "^1.6.0" } }, "sha512-F/+zi9yKJTHh5hX1UGJfkDvyC+F34Vi3jgy44NJwOKCgic1QtAon0b1iT9OsDO77RTgR+PCil+3Y5B8T2Owy1Q=="], + "solid-stripe": ["solid-stripe@0.8.1", "", { "peerDependencies": { "@stripe/stripe-js": ">=1.44.1 <8.0.0", "solid-js": "^1.6.0" } }, "sha512-l2SkWoe51rsvk9u1ILBRWyCHODZebChSGMR6zHYJTivTRC0XWrRnNNKs5x1PYXsaIU71KYI6ov5CZB5cOtGLWw=="], "solid-transition-size": ["solid-transition-size@0.1.4", "", { "dependencies": { "@corvu/utils": "~0.3.2" }, "peerDependencies": { "solid-js": "^1.8" } }, "sha512-ocHVnbfy23CgfaH4cEUR/AFg0Y3CEL8Oh3n9Qv8OHFJgPh+zkmERKZQfi/xH5XvxDCizg8VjPrVUhiHB1Gza8g=="], @@ -5678,8 +5184,6 @@ "sst-win32-x86": ["sst-win32-x86@4.13.1", "", { "os": "win32", "cpu": "none" }, "sha512-YPxBVdac/MsrzwlC6pF0NrrvMcmfdBLYjv7MbzHc5jNh1FQ1WPh6bdWQqgv0KD9EQTNLLEkej0beydgUvcCWJg=="], - "stack-utils": ["stack-utils@2.0.6", "", { "dependencies": { "escape-string-regexp": "^2.0.0" } }, "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ=="], - "stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="], "stackframe": ["stackframe@1.3.4", "", {}, "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw=="], @@ -5728,8 +5232,6 @@ "strip-indent": ["strip-indent@3.0.0", "", { "dependencies": { "min-indent": "^1.0.0" } }, "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ=="], - "strip-json-comments": ["strip-json-comments@2.0.1", "", {}, "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ=="], - "stripe": ["stripe@18.0.0", "", { "dependencies": { "@types/node": ">=8.1.0", "qs": "^6.11.0" } }, "sha512-3Fs33IzKUby//9kCkCa1uRpinAoTvj6rJgQ2jrBEysoxEvfsclvXdna1amyEYbA2EKkjynuB4+L/kleCCaWTpA=="], "strnum": ["strnum@1.1.2", "", {}, "sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA=="], @@ -5764,8 +5266,6 @@ "tar": ["tar@7.5.15", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.1.0", "yallist": "^5.0.0" } }, "sha512-dzGK0boVlC4W5QFuQN1EFSl3bIDYsk7Tj40U6eIBnK2k/8ml7TZ5agbI5j5+qnoVcAA+rNtBml8SEiLxZpNqRQ=="], - "tar-fs": ["tar-fs@2.1.5", "", { "dependencies": { "chownr": "^1.1.1", "mkdirp-classic": "^0.5.2", "pump": "^3.0.0", "tar-stream": "^2.1.4" } }, "sha512-OboTd8mmMhZDNPV+UjQcK9yKAatXu2aJ+r1w4im1Otd4M4fl2hwvdoXUxIYHFTHWK/3y3FarBP70v3vwmGlOxw=="], - "tar-stream": ["tar-stream@3.2.0", "", { "dependencies": { "b4a": "^1.6.4", "bare-fs": "^4.5.5", "fast-fifo": "^1.2.0", "streamx": "^2.15.0" } }, "sha512-ojzvCvVaNp6aOTFmG7jaRD0meowIAuPc3cMMhSgKiVWws1GyHbGd/xvnyuRKcKlMpt3qvxx6r0hreCNITP9hIg=="], "teex": ["teex@1.0.1", "", { "dependencies": { "streamx": "^2.12.5" } }, "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg=="], @@ -5786,8 +5286,6 @@ "thread-stream": ["thread-stream@4.2.0", "", { "dependencies": { "real-require": "^1.0.0" } }, "sha512-e2zZ96wSChazBsbENf/Pcm/4swHt2cEKQ92rhUjkL9GCKiTDJIaTBenjE/m9DXi0QBmTMDkFDdOomUy20A1tDQ=="], - "through": ["through@2.3.8", "", {}, "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg=="], - "thunky": ["thunky@1.1.0", "", {}, "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA=="], "tiny-async-pool": ["tiny-async-pool@1.3.0", "", { "dependencies": { "semver": "^5.5.0" } }, "sha512-01EAw5EDrcVrdgyCLgoSPvqznC0sVxDSVeiOz09FUpjh71G79VCqneOr+xvt7T1r76CF6ZZfPjHorN2+d+3mqA=="], @@ -5814,8 +5312,6 @@ "tmp-promise": ["tmp-promise@3.0.3", "", { "dependencies": { "tmp": "^0.2.0" } }, "sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ=="], - "to-data-view": ["to-data-view@1.1.0", "", {}, "sha512-1eAdufMg6mwgmlojAx3QeMnzB/BTVp7Tbndi3U7ftcT2zCZadjxkkmLmd97zmaxWi+sgGcgWrokmpEoy0Dn0vQ=="], - "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], "toad-cache": ["toad-cache@3.7.1", "", {}, "sha512-5DXWzE4Vz7xNHsv+xQ+MGfJYyC78Aok3tEr0MNwHoRf7vZnga1mQXZ4/Nsodld4VR6Wd+VhfmqnNrsRJyYPfrQ=="], @@ -5840,8 +5336,6 @@ "trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="], - "trim-trailing-lines": ["trim-trailing-lines@2.1.0", "", {}, "sha512-5UR5Biq4VlVOtzqkm2AZlgvSlDJtME46uV0br0gENbwN4l5+mMKT4b9gJKqWtuL2zAIqajGJGuvbCbcAJUZqBg=="], - "trough": ["trough@2.2.0", "", {}, "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw=="], "truncate-utf8-bytes": ["truncate-utf8-bytes@1.0.2", "", { "dependencies": { "utf8-byte-length": "^1.0.1" } }, "sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ=="], @@ -5862,8 +5356,6 @@ "tunnel": ["tunnel@0.0.6", "", {}, "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg=="], - "tunnel-agent": ["tunnel-agent@0.6.0", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w=="], - "turbo": ["turbo@2.10.2", "", { "optionalDependencies": { "@turbo/darwin-64": "2.10.2", "@turbo/darwin-arm64": "2.10.2", "@turbo/linux-64": "2.10.2", "@turbo/linux-arm64": "2.10.2", "@turbo/windows-64": "2.10.2", "@turbo/windows-arm64": "2.10.2" }, "bin": { "turbo": "bin/turbo" } }, "sha512-wTExrNrRjB8qzIcg+ZLm0A3GFNLDsWNwdS/RBXB0FPrBDyzk3i96Yx+TxWZC7a0k1SIreFB8ciUbxjmEqTH8IQ=="], "turndown": ["turndown@7.2.0", "", { "dependencies": { "@mixmark-io/domino": "^2.2.0" } }, "sha512-eCZGBN4nNNqM9Owkv9HAtWRYfLA4h909E/WGAWWBpmB275ehNhZyk87/Tpvjbp0jjNl9XwCsbe6bm6CqFsgD+A=="], @@ -5872,13 +5364,9 @@ "tw-to-css": ["tw-to-css@0.0.12", "", { "dependencies": { "postcss": "8.4.31", "postcss-css-variables": "0.18.0", "tailwindcss": "3.3.2" } }, "sha512-rQAsQvOtV1lBkyCw+iypMygNHrShYAItES5r8fMsrhhaj5qrV2LkZyXc8ccEH+u5bFjHjQ9iuxe90I7Kykf6pw=="], - "twoslash": ["twoslash@0.3.9", "", { "dependencies": { "@typescript/vfs": "^1.6.4", "twoslash-protocol": "0.3.9" }, "peerDependencies": { "typescript": "^5.5.0 || ^6.0.0" } }, "sha512-rDclk+OtzuTX+tnea7DYLCkqGQ3eP0IyfD+kzUJ7t46X/NzlaxwrhecmEBNuSCuEn3V+n1PhcjUUQQ7gUJzX5Q=="], - - "twoslash-protocol": ["twoslash-protocol@0.3.9", "", {}, "sha512-9/iwp+CXOnjFMPQuPL5PkuRbZnDoNpBvtJCLs9t8kDYkL3YHujbvnHfZA1i5fApDftVEdBw+T/4F+dH5kIzpYQ=="], - "type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="], - "type-is": ["type-is@2.1.0", "", { "dependencies": { "content-type": "^2.0.0", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA=="], + "type-is": ["type-is@1.6.18", "", { "dependencies": { "media-typer": "0.3.0", "mime-types": "~2.1.24" } }, "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g=="], "typed-array-buffer": ["typed-array-buffer@1.0.3", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-typed-array": "^1.1.14" } }, "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw=="], @@ -5904,8 +5392,6 @@ "unbox-primitive": ["unbox-primitive@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "has-bigints": "^1.0.2", "has-symbols": "^1.1.0", "which-boxed-primitive": "^1.1.1" } }, "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw=="], - "unbzip2-stream": ["unbzip2-stream@1.4.3", "", { "dependencies": { "buffer": "^5.2.1", "through": "^2.3.8" } }, "sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg=="], - "uncrypto": ["uncrypto@0.1.3", "", {}, "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q=="], "undici": ["undici@8.3.0", "", {}, "sha512-TkUDgb6tl7KOGZ+7e8E3d2FYgUQgF6z5YypqjWmixVQSQERFcVrVg0ySADm2LVLRh5ljAaHTCR5Fmz3Q34rB7Q=="], @@ -5922,22 +5408,16 @@ "unifont": ["unifont@0.5.2", "", { "dependencies": { "css-tree": "^3.0.0", "ofetch": "^1.4.1", "ohash": "^2.0.0" } }, "sha512-LzR4WUqzH9ILFvjLAUU7dK3Lnou/qd5kD+IakBtBK4S15/+x2y9VX+DcWQv6s551R6W+vzwgVS6tFg3XggGBgg=="], - "unist-builder": ["unist-builder@4.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-wmRFnH+BLpZnTKpc5L7O67Kac89s9HMrtELpnNaE6TAobq5DTZZs5YaTQfAZBA9bFPECx2uVAPO31c+GVug8mg=="], - "unist-util-find-after": ["unist-util-find-after@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ=="], "unist-util-is": ["unist-util-is@6.0.1", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g=="], - "unist-util-map": ["unist-util-map@4.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-HJs1tpkSmRJUzj6fskQrS5oYhBYlmtcvy4SepdDEEsL04FjBrgF0Mgggvxc1/qGBGgW7hRh9+UBK1aqTEnBpIA=="], - "unist-util-modify-children": ["unist-util-modify-children@4.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "array-iterate": "^2.0.0" } }, "sha512-+tdN5fGNddvsQdIzUF3Xx82CU9sMM+fA0dLgR9vOmT0oPT2jH+P1nd5lSqfCfXAw+93NhcXNY2qqvTUtE4cQkw=="], "unist-util-position": ["unist-util-position@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA=="], "unist-util-position-from-estree": ["unist-util-position-from-estree@2.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-KaFVRjoqLyF6YXCbVLNad/eS4+OfPQQn2yOd7zF/h5T/CSL2v8NpN6a5TPvtbXthAGw5nG+PuTtq+DdIZr+cRQ=="], - "unist-util-remove": ["unist-util-remove@4.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-b4gokeGId57UVRX/eVKej5gXqGlc9+trkORhFJpu9raqZkZhU0zm8Doi05+HaiBsMEIJowL+2WtQ5ItjsngPXg=="], - "unist-util-remove-position": ["unist-util-remove-position@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-visit": "^5.0.0" } }, "sha512-Hp5Kh3wLxv0PHj9m2yZhhLt58KzPtEYKQQ4yxfYFEO7EvHwzyDYnduhHnY1mDxoqr7VUwVuHXk9RXKIiYS1N8Q=="], "unist-util-stringify-position": ["unist-util-stringify-position@4.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ=="], @@ -5968,14 +5448,8 @@ "update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="], - "uqr": ["uqr@0.1.3", "", {}, "sha512-0rjE8iEJe4YmT9TOhwsZtqCMRLc5DXZUI2UEYUUg63ikBkqqE5EYWaI0etFe/5KUcmcYwLih2RND1kq+hrUJXA=="], - "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], - "urijs": ["urijs@1.19.11", "", {}, "sha512-HXgFDgDommxn5/bIv0cnQZsPhHDA90NPHD6+c/v21U5+Sx5hoP8+dP9IZXBU1gIfvdRfhG8cel9QNPeionfcCQ=="], - - "urlpattern-polyfill": ["urlpattern-polyfill@10.0.0", "", {}, "sha512-H/A06tKD7sS1O1X2SshBVeA5FLycRpjqiBeqGKmBwBDBy28EnRjORxTNe269KSSr5un5qyWi1iL61wLxpd+ZOg=="], - "use-callback-ref": ["use-callback-ref@1.3.3", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg=="], "use-sidecar": ["use-sidecar@1.1.3", "", { "dependencies": { "detect-node-es": "^1.1.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ=="], @@ -5986,8 +5460,6 @@ "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], - "utility-types": ["utility-types@3.11.0", "", {}, "sha512-6Z7Ma2aVEWisaL6TvBCy7P8rm2LQoPv6dJ7ecIaIixHcwfbJ0x7mWdbcwlIM5IGQxPZSFYeqRCqlOOeKoJYMkw=="], - "utils-merge": ["utils-merge@1.0.1", "", {}, "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA=="], "uuid": ["uuid@14.0.0", "", { "bin": { "uuid": "dist-node/bin/uuid" } }, "sha512-Qo+uWgilfSmAhXCMav1uYFynlQO7fMFiMVZsQqZRMIXp0O7rR7qjkj+cPvBHLgBqi960QCoo/PH2/6ZtVqKvrg=="], @@ -6006,8 +5478,6 @@ "vfile-location": ["vfile-location@5.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile": "^6.0.0" } }, "sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg=="], - "vfile-matter": ["vfile-matter@5.0.1", "", { "dependencies": { "vfile": "^6.0.0", "yaml": "^2.0.0" } }, "sha512-o6roP82AiX0XfkyTHyRCMXgHfltUNlXSEqCIS80f+mbAyiQBE2fxtDVMtseyytGx75sihiJFo/zR6r/4LTs2Cw=="], - "vfile-message": ["vfile-message@4.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw=="], "vite": ["vite@7.1.4", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.14" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-X5QFK4SGynAeeIt+A7ZWnApdUyHYm+pzv/8/A57LqSGcI88U6R6ipOs3uCesdc6yl7nl+zNO0t8LmqAdXcQihw=="], @@ -6112,12 +5582,8 @@ "wsl-utils": ["wsl-utils@0.3.1", "", { "dependencies": { "is-wsl": "^3.1.0", "powershell-utils": "^0.1.0" } }, "sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg=="], - "xdg-app-paths": ["xdg-app-paths@5.5.1", "", { "dependencies": { "os-paths": "^4.0.1", "xdg-portable": "^7.2.0" } }, "sha512-hI3flOB4PLZIy5prbtTpirobtPE2ZtZ52szO+2mM9Efp6ErM398La+C1lIpNWDfNoQk+6Lsi6nMcCwVB7pxeMQ=="], - "xdg-basedir": ["xdg-basedir@5.1.0", "", {}, "sha512-GCPAHLvrIH13+c0SuacwvRYj2SxJXQ4kaVTT5xgL3kPrz56XxkF21IGhjSE1+W0aw7gpBWRGXLCPnPby6lSpmQ=="], - "xdg-portable": ["xdg-portable@7.3.0", "", { "dependencies": { "os-paths": "^4.0.1" } }, "sha512-sqMMuL1rc0FmMBOzCpd0yuy9trqF2yTTVe+E9ogwCSWQCdDEtQUwrZPT6AxqtsFGRNxycgncbP/xmOOSPw5ZUw=="], - "xml-naming": ["xml-naming@0.1.0", "", {}, "sha512-k8KO9hrMyNk6tUWqUfkTEZbezRRpONVOzUTnc97VnCvyj6Tf9lyUR9EDAIeiVLv56jsMcoXEwjW8Kv5yPY52lw=="], "xml2js": ["xml2js@0.5.0", "", { "dependencies": { "sax": ">=0.6.0", "xmlbuilder": "~11.0.0" } }, "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA=="], @@ -6126,8 +5592,6 @@ "xmlhttprequest-ssl": ["xmlhttprequest-ssl@2.1.2", "", {}, "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ=="], - "xss": ["xss@1.0.15", "", { "dependencies": { "commander": "^2.20.3", "cssfilter": "0.0.10" }, "bin": { "xss": "bin/xss" } }, "sha512-FVdlVVC67WOIPvfOwhoMETV72f6GbW7aOabBC3WxN/oUdoEMDyLz4OgRv5/gck2ZeNqEQu+Tb0kloovXOfpYVg=="], - "xxhash-wasm": ["xxhash-wasm@1.1.0", "", {}, "sha512-147y/6YNh+tlp6nd/2pWq38i9h6mz/EuQ6njIrmW8D1BS5nCqs0P6DG+m6zTGnNz5I+uhZ0SHxBs9BsPrwcKDA=="], "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], @@ -6150,10 +5614,6 @@ "yoctocolors": ["yoctocolors@2.1.2", "", {}, "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug=="], - "yoctocolors-cjs": ["yoctocolors-cjs@2.1.3", "", {}, "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw=="], - - "yoga-layout": ["yoga-layout@3.2.1", "", {}, "sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ=="], - "youch": ["youch@4.1.0-beta.10", "", { "dependencies": { "@poppinss/colors": "^4.1.5", "@poppinss/dumper": "^0.6.4", "@speed-highlight/core": "^1.2.7", "cookie": "^1.0.2", "youch-core": "^0.3.3" } }, "sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ=="], "youch-core": ["youch-core@0.3.3", "", { "dependencies": { "@poppinss/exception": "^1.2.2", "error-stack-parser-es": "^1.0.5" } }, "sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA=="], @@ -6202,7 +5662,11 @@ "@ai-sdk/anthropic/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.27", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ubkAJ+xODouwtmN1tYlvTPphH1hPOBfZaEQe8U7skGvFAnIRs9PPpsq57bC2+Ky/MB4yzhd6YOsxTAx9sGpazw=="], - "@ai-sdk/azure/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="], + "@ai-sdk/azure/@ai-sdk/openai": ["@ai-sdk/openai@3.0.84", "", { "dependencies": { "@ai-sdk/provider": "3.0.14", "@ai-sdk/provider-utils": "4.0.38" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-cmgbeJL0bbY0yTJH4/AdmP5E7MjWRL9G8UdhIi0JlV/So03o82ORJofW8OzwCZPTORVQblFbpZXYGDcUd9NdUQ=="], + + "@ai-sdk/azure/@ai-sdk/provider": ["@ai-sdk/provider@3.0.14", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-5X1k57JBJ4H7H1QjX7CnJYAB1I19r/trVZTMcSms7/kLNZ8RaU4Nt2agcwZzv82Hfx6Q7/TOLU7agAKeFfc8cA=="], + + "@ai-sdk/azure/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.38", "", { "dependencies": { "@ai-sdk/provider": "3.0.14", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-/HHGmtKllqjg1OLc023v9w9kK3laW7Z6TzfZukYQWCsGBbzB9p60zTvvpXFVcs44NZBVXL3viOa1HRKUbeee8g=="], "@ai-sdk/cerebras/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="], @@ -6214,9 +5678,9 @@ "@ai-sdk/deepinfra/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="], - "@ai-sdk/deepseek/@ai-sdk/provider": ["@ai-sdk/provider@3.0.10", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-Q3BZ27qfpYqnCYGvE3vt+Qi6LGOF9R5Nmzn+9JoM1lCRsD9mYaIhfJLkSunN48nfGXJ6n+XNV0J/XVpqGQl7Dw=="], + "@ai-sdk/deepseek/@ai-sdk/provider": ["@ai-sdk/provider@3.0.14", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-5X1k57JBJ4H7H1QjX7CnJYAB1I19r/trVZTMcSms7/kLNZ8RaU4Nt2agcwZzv82Hfx6Q7/TOLU7agAKeFfc8cA=="], - "@ai-sdk/deepseek/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.27", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ubkAJ+xODouwtmN1tYlvTPphH1hPOBfZaEQe8U7skGvFAnIRs9PPpsq57bC2+Ky/MB4yzhd6YOsxTAx9sGpazw=="], + "@ai-sdk/deepseek/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.38", "", { "dependencies": { "@ai-sdk/provider": "3.0.14", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-/HHGmtKllqjg1OLc023v9w9kK3laW7Z6TzfZukYQWCsGBbzB9p60zTvvpXFVcs44NZBVXL3viOa1HRKUbeee8g=="], "@ai-sdk/elevenlabs/@ai-sdk/provider": ["@ai-sdk/provider@3.0.10", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-Q3BZ27qfpYqnCYGvE3vt+Qi6LGOF9R5Nmzn+9JoM1lCRsD9mYaIhfJLkSunN48nfGXJ6n+XNV0J/XVpqGQl7Dw=="], @@ -6242,7 +5706,9 @@ "@ai-sdk/groq/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="], - "@ai-sdk/mistral/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="], + "@ai-sdk/mistral/@ai-sdk/provider": ["@ai-sdk/provider@3.0.14", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-5X1k57JBJ4H7H1QjX7CnJYAB1I19r/trVZTMcSms7/kLNZ8RaU4Nt2agcwZzv82Hfx6Q7/TOLU7agAKeFfc8cA=="], + + "@ai-sdk/mistral/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.40", "", { "dependencies": { "@ai-sdk/provider": "3.0.14", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-OL5IrpUm9Y8Dwy+w/vvFwPotS6m52O9W0op2oXgXdCROMJIBalBI0oro6OIBYkPxvm5Xg02GSkoQN25RlR0bnw=="], "@ai-sdk/openai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="], @@ -6262,10 +5728,6 @@ "@ai-sdk/xai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.35", "", { "dependencies": { "@ai-sdk/provider": "3.0.13", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-bjYld/2KGPLt78kpqbya+fD4LYS7BqVQJyUjE3qAHrYB0FR2Q90BaWEVIBZaguTWXf/A8L6uG1zO1v9TxVlGWg=="], - "@alcalzone/ansi-tokenize/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], - - "@alcalzone/ansi-tokenize/is-fullwidth-code-point": ["is-fullwidth-code-point@5.1.0", "", { "dependencies": { "get-east-asian-width": "^1.3.1" } }, "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ=="], - "@astrojs/check/yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="], "@astrojs/cloudflare/vite": ["vite@6.4.2", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ=="], @@ -6288,12 +5750,6 @@ "@astrojs/starlight/js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], - "@asyncapi/parser/ajv-formats": ["ajv-formats@2.1.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA=="], - - "@asyncapi/parser/js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], - - "@asyncapi/parser/node-fetch": ["node-fetch@2.6.7", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ=="], - "@aws-crypto/crc32/@aws-sdk/types": ["@aws-sdk/types@3.973.9", "", { "dependencies": { "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-kuBfgQVdcz5Bmapc4A13YbpVw/pXkesfhetcFYwbntqas8sF41OHyd4o28+/TG2ZQdHBsv90Lsu5y6oitvYCdg=="], "@aws-crypto/crc32c/@aws-sdk/types": ["@aws-sdk/types@3.973.9", "", { "dependencies": { "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-kuBfgQVdcz5Bmapc4A13YbpVw/pXkesfhetcFYwbntqas8sF41OHyd4o28+/TG2ZQdHBsv90Lsu5y6oitvYCdg=="], @@ -6484,8 +5940,6 @@ "@img/sharp-wasm32/@emnapi/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="], - "@inquirer/core/wrap-ansi": ["wrap-ansi@6.2.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="], - "@jsx-email/cli/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], "@jsx-email/cli/esbuild": ["esbuild@0.19.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.19.12", "@esbuild/android-arm": "0.19.12", "@esbuild/android-arm64": "0.19.12", "@esbuild/android-x64": "0.19.12", "@esbuild/darwin-arm64": "0.19.12", "@esbuild/darwin-x64": "0.19.12", "@esbuild/freebsd-arm64": "0.19.12", "@esbuild/freebsd-x64": "0.19.12", "@esbuild/linux-arm": "0.19.12", "@esbuild/linux-arm64": "0.19.12", "@esbuild/linux-ia32": "0.19.12", "@esbuild/linux-loong64": "0.19.12", "@esbuild/linux-mips64el": "0.19.12", "@esbuild/linux-ppc64": "0.19.12", "@esbuild/linux-riscv64": "0.19.12", "@esbuild/linux-s390x": "0.19.12", "@esbuild/linux-x64": "0.19.12", "@esbuild/netbsd-x64": "0.19.12", "@esbuild/openbsd-x64": "0.19.12", "@esbuild/sunos-x64": "0.19.12", "@esbuild/win32-arm64": "0.19.12", "@esbuild/win32-ia32": "0.19.12", "@esbuild/win32-x64": "0.19.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg=="], @@ -6502,124 +5956,16 @@ "@mdx-js/mdx/acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="], - "@mdx-js/mdx/remark-mdx": ["remark-mdx@3.1.1", "", { "dependencies": { "mdast-util-mdx": "^3.0.0", "micromark-extension-mdxjs": "^3.0.0" } }, "sha512-Pjj2IYlUY3+D8x00UJsIOg5BEvfMyeI+2uLPn9VO9Wg4MEtN/VTIq2NEJQfde9PnX15KgtHyl9S0BcTnWrIuWg=="], - "@mdx-js/mdx/source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="], - "@mintlify/cli/chalk": ["chalk@5.2.0", "", {}, "sha512-ree3Gqw/nazQAPuJJEy+avdl7QfZMcUvmHIKgEZkGL+xOBzRvup5Hxo6LHuMceSxOabuJLJm5Yp/92R9eMmMvA=="], - - "@mintlify/cli/fs-extra": ["fs-extra@11.2.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw=="], - - "@mintlify/cli/js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], - - "@mintlify/cli/open": ["open@8.4.2", "", { "dependencies": { "define-lazy-prop": "^2.0.0", "is-docker": "^2.1.1", "is-wsl": "^2.2.0" } }, "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ=="], - - "@mintlify/cli/openid-client": ["openid-client@6.8.2", "", { "dependencies": { "jose": "^6.1.3", "oauth4webapi": "^3.8.4" } }, "sha512-uOvTCndr4udZsKihJ68H9bUICrriHdUVJ6Az+4Ns6cW55rwM5h0bjVIzDz2SxgOI84LKjFyjOFvERLzdTUROGA=="], - - "@mintlify/cli/react": ["react@19.2.3", "", {}, "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA=="], - - "@mintlify/cli/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], - - "@mintlify/cli/unist-util-visit": ["unist-util-visit@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg=="], - - "@mintlify/cli/yargs": ["yargs@17.7.1", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-cwiTb08Xuv5fqF4AovYacTFNxk62th7LKJ6BL9IGUpTJrWoU7/7WdQGTP2SjKf1dUNBGzDd28p/Yfs/GI6JrLw=="], - - "@mintlify/cli/zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], - - "@mintlify/common/acorn": ["acorn@8.11.2", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w=="], - - "@mintlify/common/hast-util-to-html": ["hast-util-to-html@9.0.4", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-whitespace": "^3.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "property-information": "^6.0.0", "space-separated-tokens": "^2.0.0", "stringify-entities": "^4.0.0", "zwitch": "^2.0.4" } }, "sha512-wxQzXtdbhiwGAUKrnQJXlOPmHnEehzphwkK7aluUPQ+lEc1xefC8pblMgpp2w5ldBTEfveRIrADcrhGIWrlTDA=="], - - "@mintlify/common/js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], - - "@mintlify/common/mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA=="], - - "@mintlify/common/mdast-util-gfm": ["mdast-util-gfm@3.0.0", "", { "dependencies": { "mdast-util-from-markdown": "^2.0.0", "mdast-util-gfm-autolink-literal": "^2.0.0", "mdast-util-gfm-footnote": "^2.0.0", "mdast-util-gfm-strikethrough": "^2.0.0", "mdast-util-gfm-table": "^2.0.0", "mdast-util-gfm-task-list-item": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-dgQEX5Amaq+DuUqf26jJqSK9qgixgd6rYDHAv4aTBuA92cTknZlKpPfa86Z/s8Dj8xsAQpFfBmPUHWJBWqS4Bw=="], - - "@mintlify/common/mdast-util-mdx-jsx": ["mdast-util-mdx-jsx@3.1.3", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "devlop": "^1.1.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "parse-entities": "^4.0.0", "stringify-entities": "^4.0.0", "unist-util-stringify-position": "^4.0.0", "vfile-message": "^4.0.0" } }, "sha512-bfOjvNt+1AcbPLTFMFWY149nJz0OjmewJs3LQQ5pIyVGxP4CdOqNVJL6kTaM5c68p8q82Xv3nCyFfUnuEcH3UQ=="], - - "@mintlify/common/postcss": ["postcss@8.5.14", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg=="], - - "@mintlify/common/remark-gfm": ["remark-gfm@4.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-gfm": "^3.0.0", "micromark-extension-gfm": "^3.0.0", "remark-parse": "^11.0.0", "remark-stringify": "^11.0.0", "unified": "^11.0.0" } }, "sha512-U92vJgBPkbw4Zfu/IiW2oTZLSL3Zpv+uI7My2eq8JxKgqraFdU8YUGicEJCEgSbeaG+QDFqIcwwfMTOEelPxuA=="], - - "@mintlify/common/remark-rehype": ["remark-rehype@11.1.1", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "mdast-util-to-hast": "^13.0.0", "unified": "^11.0.0", "vfile": "^6.0.0" } }, "sha512-g/osARvjkBXb6Wo0XvAeXQohVta8i84ACbenPpoSsxTOQH/Ae0/RGP4WZgnMH5pMLpsj4FG7OHmcIcXxpza8eQ=="], - - "@mintlify/common/sucrase": ["sucrase@3.34.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.2", "commander": "^4.0.0", "glob": "7.1.6", "lines-and-columns": "^1.1.6", "mz": "^2.7.0", "pirates": "^4.0.1", "ts-interface-checker": "^0.1.9" }, "bin": { "sucrase": "bin/sucrase", "sucrase-node": "bin/sucrase-node" } }, "sha512-70/LQEZ07TEcxiU2dz51FKaE6hCTWC6vr7FOk3Gr0U60C3shtAN+H+BFr9XlYe5xqf3RA8nrc+VIwzCfnxuXJw=="], - - "@mintlify/common/tailwindcss": ["tailwindcss@3.4.17", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", "chokidar": "^3.6.0", "didyoumean": "^1.2.2", "dlv": "^1.1.3", "fast-glob": "^3.3.2", "glob-parent": "^6.0.2", "is-glob": "^4.0.3", "jiti": "^1.21.6", "lilconfig": "^3.1.3", "micromatch": "^4.0.8", "normalize-path": "^3.0.0", "object-hash": "^3.0.0", "picocolors": "^1.1.1", "postcss": "^8.4.47", "postcss-import": "^15.1.0", "postcss-js": "^4.0.1", "postcss-load-config": "^4.0.2", "postcss-nested": "^6.2.0", "postcss-selector-parser": "^6.1.2", "resolve": "^1.22.8", "sucrase": "^3.35.0" }, "bin": { "tailwind": "lib/cli.js", "tailwindcss": "lib/cli.js" } }, "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og=="], - - "@mintlify/common/unist-util-visit": ["unist-util-visit@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg=="], - - "@mintlify/common/unist-util-visit-parents": ["unist-util-visit-parents@6.0.1", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw=="], - - "@mintlify/link-rot/fs-extra": ["fs-extra@11.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-0rcTq621PD5jM/e0a3EJoGC/1TC5ZBCERW82LQuwfGnCa1V8w7dpYH1yNu+SLb6E5dkeCBzKEyLGlFrnr+dUyw=="], - - "@mintlify/link-rot/unist-util-visit": ["unist-util-visit@4.1.2", "", { "dependencies": { "@types/unist": "^2.0.0", "unist-util-is": "^5.0.0", "unist-util-visit-parents": "^5.1.1" } }, "sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg=="], - - "@mintlify/mdx/@shikijs/transformers": ["@shikijs/transformers@3.20.0", "", { "dependencies": { "@shikijs/core": "3.20.0", "@shikijs/types": "3.20.0" } }, "sha512-PrHHMRr3Q5W1qB/42kJW6laqFyWdhrPF2hNR9qjOm1xcSiAO3hAHo7HaVyHE6pMyevmy3i51O8kuGGXC78uK3g=="], - - "@mintlify/mdx/react": ["react@19.2.3", "", {}, "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA=="], - - "@mintlify/mdx/shiki": ["shiki@3.23.0", "", { "dependencies": { "@shikijs/core": "3.23.0", "@shikijs/engine-javascript": "3.23.0", "@shikijs/engine-oniguruma": "3.23.0", "@shikijs/langs": "3.23.0", "@shikijs/themes": "3.23.0", "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-55Dj73uq9ZXL5zyeRPzHQsK7Nbyt6Y10k5s7OjuFZGMhpp4r/rsLBH0o/0fstIzX1Lep9VxefWljK/SKCzygIA=="], - - "@mintlify/prebuild/chalk": ["chalk@5.3.0", "", {}, "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w=="], - - "@mintlify/prebuild/fs-extra": ["fs-extra@11.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-0rcTq621PD5jM/e0a3EJoGC/1TC5ZBCERW82LQuwfGnCa1V8w7dpYH1yNu+SLb6E5dkeCBzKEyLGlFrnr+dUyw=="], - - "@mintlify/prebuild/js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], - - "@mintlify/prebuild/unist-util-visit": ["unist-util-visit@4.1.2", "", { "dependencies": { "@types/unist": "^2.0.0", "unist-util-is": "^5.0.0", "unist-util-visit-parents": "^5.1.1" } }, "sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg=="], - - "@mintlify/prebuild/uuid": ["uuid@11.1.1", "", { "bin": { "uuid": "dist/esm/bin/uuid" } }, "sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ=="], - - "@mintlify/previewing/chalk": ["chalk@5.2.0", "", {}, "sha512-ree3Gqw/nazQAPuJJEy+avdl7QfZMcUvmHIKgEZkGL+xOBzRvup5Hxo6LHuMceSxOabuJLJm5Yp/92R9eMmMvA=="], - - "@mintlify/previewing/chokidar": ["chokidar@3.5.3", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw=="], - - "@mintlify/previewing/express": ["express@4.22.0", "", { "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", "body-parser": "~1.20.3", "content-disposition": "~0.5.4", "content-type": "~1.0.4", "cookie": "~0.7.1", "cookie-signature": "~1.0.6", "debug": "2.6.9", "depd": "2.0.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "finalhandler": "~1.3.1", "fresh": "~0.5.2", "http-errors": "~2.0.0", "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "~2.4.1", "parseurl": "~1.3.3", "path-to-regexp": "~0.1.12", "proxy-addr": "~2.0.7", "qs": "~6.14.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", "send": "~0.19.0", "serve-static": "~1.16.2", "setprototypeof": "1.2.0", "statuses": "~2.0.1", "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" } }, "sha512-c2iPh3xp5vvCLgaHK03+mWLFPhox7j1LwyxcZwFVApEv5i0X+IjPpbT50SJJwwLpdBVfp45AkK/v+AFgv/XlfQ=="], - - "@mintlify/previewing/fs-extra": ["fs-extra@11.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-0rcTq621PD5jM/e0a3EJoGC/1TC5ZBCERW82LQuwfGnCa1V8w7dpYH1yNu+SLb6E5dkeCBzKEyLGlFrnr+dUyw=="], - - "@mintlify/previewing/got": ["got@13.0.0", "", { "dependencies": { "@sindresorhus/is": "^5.2.0", "@szmarczak/http-timer": "^5.0.1", "cacheable-lookup": "^7.0.0", "cacheable-request": "^10.2.8", "decompress-response": "^6.0.0", "form-data-encoder": "^2.1.2", "get-stream": "^6.0.1", "http2-wrapper": "^2.1.10", "lowercase-keys": "^3.0.0", "p-cancelable": "^3.0.0", "responselike": "^3.0.0" } }, "sha512-XfBk1CxOOScDcMr9O1yKkNaQyy865NbYs+F7dr4H0LZMVgCj2Le59k6PqbNHoL5ToeaEQUYh6c6yMfVcc6SJxA=="], - - "@mintlify/previewing/js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], - - "@mintlify/previewing/react": ["react@19.2.3", "", {}, "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA=="], - - "@mintlify/previewing/unist-util-visit": ["unist-util-visit@4.1.2", "", { "dependencies": { "@types/unist": "^2.0.0", "unist-util-is": "^5.0.0", "unist-util-visit-parents": "^5.1.1" } }, "sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg=="], - - "@mintlify/previewing/yargs": ["yargs@17.7.1", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-cwiTb08Xuv5fqF4AovYacTFNxk62th7LKJ6BL9IGUpTJrWoU7/7WdQGTP2SjKf1dUNBGzDd28p/Yfs/GI6JrLw=="], - - "@mintlify/scraping/fs-extra": ["fs-extra@11.1.1", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-MGIE4HOvQCeUCzmlHs0vXpih4ysz4wg9qiSAu6cd42lVwPbTM1TjV7RusoyQqMmk/95gdQZX72u+YW+c3eEpFQ=="], - - "@mintlify/scraping/js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], - - "@mintlify/scraping/mdast-util-mdx-jsx": ["mdast-util-mdx-jsx@3.1.3", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "devlop": "^1.1.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "parse-entities": "^4.0.0", "stringify-entities": "^4.0.0", "unist-util-stringify-position": "^4.0.0", "vfile-message": "^4.0.0" } }, "sha512-bfOjvNt+1AcbPLTFMFWY149nJz0OjmewJs3LQQ5pIyVGxP4CdOqNVJL6kTaM5c68p8q82Xv3nCyFfUnuEcH3UQ=="], - - "@mintlify/scraping/remark-gfm": ["remark-gfm@4.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-gfm": "^3.0.0", "micromark-extension-gfm": "^3.0.0", "remark-parse": "^11.0.0", "remark-stringify": "^11.0.0", "unified": "^11.0.0" } }, "sha512-U92vJgBPkbw4Zfu/IiW2oTZLSL3Zpv+uI7My2eq8JxKgqraFdU8YUGicEJCEgSbeaG+QDFqIcwwfMTOEelPxuA=="], - - "@mintlify/scraping/remark-mdx": ["remark-mdx@3.0.1", "", { "dependencies": { "mdast-util-mdx": "^3.0.0", "micromark-extension-mdxjs": "^3.0.0" } }, "sha512-3Pz3yPQ5Rht2pM5R+0J2MrGoBSrzf+tJG94N+t/ilfdh8YLyyKYtidAYwTveB20BoHAcwIopOUqhcmh2F7hGYA=="], - - "@mintlify/scraping/unist-util-visit": ["unist-util-visit@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg=="], - - "@mintlify/scraping/yargs": ["yargs@17.7.1", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-cwiTb08Xuv5fqF4AovYacTFNxk62th7LKJ6BL9IGUpTJrWoU7/7WdQGTP2SjKf1dUNBGzDd28p/Yfs/GI6JrLw=="], - - "@mintlify/scraping/zod": ["zod@3.24.0", "", {}, "sha512-Hz+wiY8yD0VLA2k/+nsg2Abez674dDGTai33SwNvMPuf9uIrBC9eFgIMQxBBbHFxVXi8W+5nX9DcAh9YNSQm/w=="], - - "@mintlify/validation/js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], - - "@mintlify/validation/object-hash": ["object-hash@3.0.0", "", {}, "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw=="], - - "@mintlify/validation/uuid": ["uuid@11.1.1", "", { "bin": { "uuid": "dist/esm/bin/uuid" } }, "sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ=="], - - "@mintlify/validation/zod": ["zod@3.24.0", "", {}, "sha512-Hz+wiY8yD0VLA2k/+nsg2Abez674dDGTai33SwNvMPuf9uIrBC9eFgIMQxBBbHFxVXi8W+5nX9DcAh9YNSQm/w=="], - - "@mintlify/validation/zod-to-json-schema": ["zod-to-json-schema@3.20.4", "", { "peerDependencies": { "zod": "^3.20.0" } }, "sha512-Un9+kInJ2Zt63n6Z7mLqBifzzPcOyX+b+Exuzf7L1+xqck9Q2EPByyTRduV3kmSPaXaRer1JCsucubpgL1fipg=="], + "@modelcontextprotocol/sdk/express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="], "@modelcontextprotocol/sdk/hono": ["hono@4.12.23", "", {}, "sha512-eIaZ9qDgu7XV0pxOCrg7/WhnQ6Ivm22UcxhXx/A3dcbqbbYgBEkc6e/J/s7j2tS96zoB0S9VBdLwQNCWwUo4LA=="], "@modelcontextprotocol/sdk/jose": ["jose@6.2.3", "", {}, "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw=="], + "@modelcontextprotocol/sdk/raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="], + "@modelcontextprotocol/sdk/zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="], "@npmcli/config/ini": ["ini@6.0.0", "", {}, "sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ=="], @@ -6688,9 +6034,9 @@ "@openauthjs/openauth/jose": ["jose@5.9.6", "", {}, "sha512-AMlnetc9+CV9asI19zHmrgS/WYsWUwCn2R7RzlbJWD7F9eWYUTGyBmU9o6PxngtLGOiDGPRu+Uc4fhKzbpteZQ=="], - "@opencode-ai/cli/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], + "@opencode-ai/app/@opencode-ai/client": ["@opencode-ai/client@vendor/opencode-ai-client-1.17.13-v2.tgz", {}, "sha512-332kgNifvpQOF9e3UA+pIa5xPrMhLaQkUiNiO+meS0Ba9HjSE6hfsWnEojMkD0DPSLqPP6rCF1dDoF7U0Y0OCQ=="], - "@opencode-ai/core/@ai-sdk/openai": ["@ai-sdk/openai@3.0.53", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-Wld+Rbc05KaUn08uBt06eEuwcgalcIFtIl32Yp+GxuZXUQwOb6YeAuq+C6da4ch6BurFoqEaLemJVwjBb7x+PQ=="], + "@opencode-ai/core/@ai-sdk/openai": ["@ai-sdk/openai@3.0.84", "", { "dependencies": { "@ai-sdk/provider": "3.0.14", "@ai-sdk/provider-utils": "4.0.38" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-cmgbeJL0bbY0yTJH4/AdmP5E7MjWRL9G8UdhIi0JlV/So03o82ORJofW8OzwCZPTORVQblFbpZXYGDcUd9NdUQ=="], "@opencode-ai/core/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.41", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-kNAGINk71AlOXx10Dq/PXw4t/9XjdK8uxfpVElRwtSFMdeSiLVt58p9TPx4/FJD+hxZuVhvxYj9r42osxWq79g=="], @@ -6698,8 +6044,6 @@ "@opencode-ai/desktop/@actions/artifact": ["@actions/artifact@4.0.0", "", { "dependencies": { "@actions/core": "^1.10.0", "@actions/github": "^6.0.1", "@actions/http-client": "^2.1.0", "@azure/core-http": "^3.0.5", "@azure/storage-blob": "^12.15.0", "@octokit/core": "^5.2.1", "@octokit/plugin-request-log": "^1.0.4", "@octokit/plugin-retry": "^3.0.9", "@octokit/request": "^8.4.1", "@octokit/request-error": "^5.1.1", "@protobuf-ts/plugin": "^2.2.3-alpha.1", "archiver": "^7.0.1", "jwt-decode": "^3.1.2", "unzip-stream": "^0.3.1" } }, "sha512-HCc2jMJRAfviGFAh0FsOR/jNfWhirxl7W6z8zDtttt0GltwxBLdEIjLiweOPFl9WbyJRW1VWnPUSAixJqcWUMQ=="], - "@opencode-ai/desktop/marked": ["marked@15.0.12", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA=="], - "@opencode-ai/desktop/typescript": ["typescript@5.6.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw=="], "@opencode-ai/llm/@smithy/eventstream-codec": ["@smithy/eventstream-codec@4.2.14", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.14.1", "@smithy/util-hex-encoding": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-erZq0nOIpzfeZdCyzZjdJb4nVSKLUmSkaQUVkRGQTXs30gyUGeKnrYEg+Xe1W5gE3aReS7IgsvANwVPxSzY6Pw=="], @@ -6708,6 +6052,8 @@ "@opencode-ai/script/semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="], + "@opencode-ai/session-ui/@opencode-ai/client": ["@opencode-ai/client@../app/vendor/opencode-ai-client-1.17.13-v2.tgz", {}, "sha512-332kgNifvpQOF9e3UA+pIa5xPrMhLaQkUiNiO+meS0Ba9HjSE6hfsWnEojMkD0DPSLqPP6rCF1dDoF7U0Y0OCQ=="], + "@opencode-ai/session-ui/@solid-primitives/resize-observer": ["@solid-primitives/resize-observer@2.1.3", "", { "dependencies": { "@solid-primitives/event-listener": "^2.4.3", "@solid-primitives/rootless": "^1.5.2", "@solid-primitives/static-store": "^0.1.2", "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-zBLje5E06TgOg93S7rGPldmhDnouNGhvfZVKOp+oG2XU8snA+GoCSSCz1M+jpNAg5Ek2EakU5UVQqL152WmdXQ=="], "@opencode-ai/ui/@solid-primitives/resize-observer": ["@solid-primitives/resize-observer@2.1.3", "", { "dependencies": { "@solid-primitives/event-listener": "^2.4.3", "@solid-primitives/rootless": "^1.5.2", "@solid-primitives/static-store": "^0.1.2", "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-zBLje5E06TgOg93S7rGPldmhDnouNGhvfZVKOp+oG2XU8snA+GoCSSCz1M+jpNAg5Ek2EakU5UVQqL152WmdXQ=="], @@ -6716,20 +6062,16 @@ "@opentui/core/diff": ["diff@9.0.0", "", {}, "sha512-svtcdpS8CgJyqAjEQIXdb3OjhFVVYjzGAPO8WGCmRbrml64SPw/jJD4GoE98aR7r25A0XcgrK3F02yw9R/vhQw=="], + "@opentui/core/marked": ["marked@17.0.1", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-boeBdiS0ghpWcSwoNm/jJBwdpFaMnZWRzjA6SkUMYb40SVaN1x7mmfGKp0jvexGcx+7y2La5zRZsYFZI6Qpypg=="], + "@opentui/solid/@babel/core": ["@babel/core@7.28.0", "", { "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.0", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.27.3", "@babel/helpers": "^7.27.6", "@babel/parser": "^7.28.0", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.0", "@babel/types": "^7.28.0", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ=="], "@oslojs/jwt/@oslojs/encoding": ["@oslojs/encoding@0.4.1", "", {}, "sha512-hkjo6MuIK/kQR5CrGNdAPZhS01ZCXuWDRJ187zh6qqF2+yMHZpD9fAYpX8q2bOO6Ryhl3XpCT6kUX76N8hhm4Q=="], - "@oxc-parser/binding-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.9.2", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" } }, "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA=="], - - "@oxc-parser/binding-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.9.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw=="], - "@oxc-resolver/binding-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.10.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" } }, "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw=="], "@oxc-resolver/binding-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="], - "@parcel/watcher/detect-libc": ["detect-libc@1.0.3", "", { "bin": { "detect-libc": "./bin/detect-libc.js" } }, "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg=="], - "@pierre/diffs/@shikijs/transformers": ["@shikijs/transformers@3.20.0", "", { "dependencies": { "@shikijs/core": "3.20.0", "@shikijs/types": "3.20.0" } }, "sha512-PrHHMRr3Q5W1qB/42kJW6laqFyWdhrPF2hNR9qjOm1xcSiAO3hAHo7HaVyHE6pMyevmy3i51O8kuGGXC78uK3g=="], "@pierre/diffs/diff": ["diff@8.0.3", "", {}, "sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ=="], @@ -6738,14 +6080,6 @@ "@protobuf-ts/plugin/typescript": ["typescript@3.9.10", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-w6fIxVE/H1PkLKcCPsFqKE7Kv7QUwhU8qQY2MueZXWx5cPZdwFupLgKK3vntcK98BtNHZtAF4LA/yl2a7k8R6Q=="], - "@puppeteer/browsers/semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="], - - "@puppeteer/browsers/tar-fs": ["tar-fs@3.1.3", "", { "dependencies": { "pump": "^3.0.0", "tar-stream": "^3.1.5" }, "optionalDependencies": { "bare-fs": "^4.0.1", "bare-path": "^3.0.0" } }, "sha512-/hU4AXnIdZu+Gvl1pk0oI5f5HxWsCJRtY2aFaJdk9VvyL48DWU6iU5WAIPG+wIi1YvWA6eTJvIviP/tMAZZNwQ=="], - - "@puppeteer/browsers/yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="], - - "@rolldown/binding-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.6", "", { "dependencies": { "@tybys/wasm-util": "^0.10.3" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" } }, "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg=="], - "@rollup/pluginutils/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], "@sentry/bundler-plugin-core/glob": ["glob@9.3.5", "", { "dependencies": { "fs.realpath": "^1.0.0", "minimatch": "^8.0.2", "minipass": "^4.2.4", "path-scurry": "^1.6.1" } }, "sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q=="], @@ -6770,16 +6104,8 @@ "@shikijs/themes/@shikijs/types": ["@shikijs/types@4.2.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-VT/MKtlpOhEPZloSH3Pb9WCZEBDoQVMa9jedp5UAwmJOar1DVc9DRODAxmYPW9M93IK4ryuqRejFfmlvlVDemw=="], - "@shikijs/twoslash/@shikijs/core": ["@shikijs/core@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-NSWQz0riNb67xthdm5br6lAkvpDJRTgB36fxlo37ZzM2yq0PQFFzbd8psqC2XMPgCzo1fW6cVi18+ArJ44wqgA=="], - - "@shikijs/twoslash/@shikijs/types": ["@shikijs/types@3.23.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-3JZ5HXOZfYjsYSk0yPwBrkupyYSLpAE26Qc0HLghhZNGTZg/SKxXIIgoxOpmmeQP0RRSDJTk1/vPfw9tbw+jSQ=="], - - "@slack/bolt/express": ["express@4.22.2", "", { "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", "body-parser": "~1.20.5", "content-disposition": "~0.5.4", "content-type": "~1.0.4", "cookie": "~0.7.1", "cookie-signature": "~1.0.6", "debug": "2.6.9", "depd": "2.0.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "finalhandler": "~1.3.1", "fresh": "~0.5.2", "http-errors": "~2.0.0", "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "~2.4.1", "parseurl": "~1.3.3", "path-to-regexp": "~0.1.12", "proxy-addr": "~2.0.7", "qs": "~6.15.1", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", "send": "~0.19.0", "serve-static": "~1.16.2", "setprototypeof": "1.2.0", "statuses": "~2.0.1", "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" } }, "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q=="], - "@slack/bolt/path-to-regexp": ["path-to-regexp@8.4.2", "", {}, "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA=="], - "@slack/bolt/raw-body": ["raw-body@2.5.3", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.4.24", "unpipe": "~1.0.0" } }, "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA=="], - "@slack/oauth/@slack/logger": ["@slack/logger@3.0.0", "", { "dependencies": { "@types/node": ">=12.0.0" } }, "sha512-DTuBFbqu4gGfajREEMrkq5jBhcnskinhr4+AnfJEk48zhVeEv3XnUKGIX98B74kxhYsIMfApGGySTn7V3b5yBA=="], "@slack/socket-mode/@slack/logger": ["@slack/logger@3.0.0", "", { "dependencies": { "@types/node": ">=12.0.0" } }, "sha512-DTuBFbqu4gGfajREEMrkq5jBhcnskinhr4+AnfJEk48zhVeEv3XnUKGIX98B74kxhYsIMfApGGySTn7V3b5yBA=="], @@ -6796,8 +6122,6 @@ "@slack/web-api/p-queue": ["p-queue@6.6.2", "", { "dependencies": { "eventemitter3": "^4.0.4", "p-timeout": "^3.2.0" } }, "sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ=="], - "@solid-primitives/memo/@solid-primitives/utils": ["@solid-primitives/utils@6.4.1", "", { "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-ISSB5QX1qP2ynrheIpYwc4oKR5Ny4siNuUyf1qZniy+Il+p/PtDB0QK1Dnle8noiHpwRD3gpPdubOC3qI/Zamg=="], - "@solidjs/start/path-to-regexp": ["path-to-regexp@8.4.2", "", {}, "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA=="], "@solidjs/start/shiki": ["shiki@1.29.2", "", { "dependencies": { "@shikijs/core": "1.29.2", "@shikijs/engine-javascript": "1.29.2", "@shikijs/engine-oniguruma": "1.29.2", "@shikijs/langs": "1.29.2", "@shikijs/themes": "1.29.2", "@shikijs/types": "1.29.2", "@shikijs/vscode-textmate": "^10.0.1", "@types/hast": "^3.0.4" } }, "sha512-njXuliz/cP+67jU2hukkxCNuH1yUi4QfdZZY+sMr5PPrIyXSu5iTb/qYC4BiWWB0vZ+7TbdvYUCeL23zpwCfbg=="], @@ -6808,30 +6132,10 @@ "@standard-community/standard-openapi/effect": ["effect@4.0.0-beta.74", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "fast-check": "^4.8.0", "find-my-way-ts": "^0.1.6", "ini": "^7.0.0", "kubernetes-types": "^1.30.0", "msgpackr": "^2.0.1", "multipasta": "^0.2.7", "toml": "^4.1.1", "uuid": "^14.0.0", "yaml": "^2.9.0" } }, "sha512-Yx+Kh12U+i2FmjwEfKs+ePFmpMd43RPD1oGqc/VraSS9bYzvF0Ff3PojwEFEVEewp8xc92Uxu28gTspU4qyvHA=="], - "@stoplight/better-ajv-errors/leven": ["leven@3.1.0", "", {}, "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A=="], - - "@stoplight/json/jsonc-parser": ["jsonc-parser@2.2.1", "", {}, "sha512-o6/yDBYccGvTz1+QFevz6l6OBZ2+fMVu2JZ9CIhzsYRX4mjaK5IyX9eldUdCmga16zlgQxyrj5pt9kzuj2C02w=="], - - "@stoplight/json/safe-stable-stringify": ["safe-stable-stringify@1.1.1", "", {}, "sha512-ERq4hUjKDbJfE4+XtZLFPCDi8Vb1JqaxAPTxWFLBx8XcAlf9Bda/ZJdVezs/NAfsMQScyIlUMx+Yeu7P7rx5jw=="], - - "@stoplight/json-ref-readers/tslib": ["tslib@1.14.1", "", {}, "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="], - - "@stoplight/json-ref-resolver/immer": ["immer@9.0.21", "", {}, "sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA=="], - - "@stoplight/spectral-core/@stoplight/types": ["@stoplight/types@13.6.0", "", { "dependencies": { "@types/json-schema": "^7.0.4", "utility-types": "^3.10.0" } }, "sha512-dzyuzvUjv3m1wmhPfq82lCVYGcXG0xUYgqnWfCq3PCVR4BKFhjdkHrnJ+jIDoMKvXb05AZP/ObQF6+NpDo29IQ=="], - - "@stoplight/spectral-core/ajv-formats": ["ajv-formats@2.1.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA=="], - - "@stoplight/spectral-core/minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="], - - "@stoplight/spectral-functions/ajv-formats": ["ajv-formats@2.1.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA=="], - - "@stoplight/spectral-parsers/@stoplight/types": ["@stoplight/types@14.1.1", "", { "dependencies": { "@types/json-schema": "^7.0.4", "utility-types": "^3.10.0" } }, "sha512-/kjtr+0t0tjKr+heVfviO9FrU/uGLc+QNX3fHJc19xsCNYqU7lVhaXxDmEID9BZTjG+/r9pK9xP/xU02XGg65g=="], - - "@stoplight/yaml/@stoplight/types": ["@stoplight/types@14.1.1", "", { "dependencies": { "@types/json-schema": "^7.0.4", "utility-types": "^3.10.0" } }, "sha512-/kjtr+0t0tjKr+heVfviO9FrU/uGLc+QNX3fHJc19xsCNYqU7lVhaXxDmEID9BZTjG+/r9pK9xP/xU02XGg65g=="], - "@storybook/csf-plugin/unplugin": ["unplugin@2.3.11", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "acorn": "^8.15.0", "picomatch": "^4.0.3", "webpack-virtual-modules": "^0.6.2" } }, "sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww=="], + "@tailwindcss/oxide/detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], + "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.10.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" }, "bundled": true }, "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw=="], "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="], @@ -6856,14 +6160,6 @@ "@types/plist/xmlbuilder": ["xmlbuilder@15.1.1", "", {}, "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg=="], - "@vercel/cli-config/zod": ["zod@4.1.11", "", {}, "sha512-WPsqwxITS2tzx1bzhIKsEs19ABD5vmCVa4xBo2tq/SrV4RNZtfws1EnCWQXM6yh8bD08a1idvkB5MZSBiZsjwg=="], - - "@vercel/cli-exec/execa": ["execa@5.1.1", "", { "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^6.0.0", "human-signals": "^2.1.0", "is-stream": "^2.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^4.0.1", "onetime": "^5.1.2", "signal-exit": "^3.0.3", "strip-final-newline": "^2.0.0" } }, "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg=="], - - "@vercel/functions/@vercel/oidc": ["@vercel/oidc@3.8.0", "", { "dependencies": { "@vercel/cli-config": "0.2.0", "@vercel/cli-exec": "1.0.0", "jose": "^5.9.6" } }, "sha512-r00laGW6Pv778RoR6M2NxX91ycSj+PBwVo+fOb9Bif+F0IyUKt25zrvBzfEzQpeAzbqOgPZyQibEWDdDFApd+A=="], - - "@vitejs/plugin-react/@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.27", "", {}, "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA=="], - "@vitest/coverage-v8/@vitest/utils": ["@vitest/utils@4.1.8", "", { "dependencies": { "@vitest/pretty-format": "4.1.8", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" } }, "sha512-uOJamYALNhfJ6iolExyQM40yIQwDqYnkKtQ5VCiSe17E33H0aQ/u+1GlRuz4LZBk6Mm3sg90G9hEbmEt37C1Zg=="], "@vitest/coverage-v8/magicast": ["magicast@0.5.3", "", { "dependencies": { "@babel/parser": "^7.29.3", "@babel/types": "^7.29.0", "source-map-js": "^1.2.1" } }, "sha512-pVKE4UdSQ7DvHzivsCIFx2BJn1mHG6KsyrFcaxFx6tONdneEuThrDx0Cj3AMg58KyN4pzYT+LHOotxDQDjNvkw=="], @@ -6876,12 +6172,20 @@ "@vscode/emmet-helper/jsonc-parser": ["jsonc-parser@2.3.1", "", {}, "sha512-H8jvkz1O50L3dMZCsLqiuB2tA7muqbSg1AtGEkN0leAqGjsUzDJir3Zwr02BhqdcITPg3ei3mZ+HjMocAknhhg=="], - "aggregate-error/indent-string": ["indent-string@5.0.0", "", {}, "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg=="], + "accepts/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], + + "accepts/negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="], "ai-gateway-provider/@ai-sdk/amazon-bedrock": ["@ai-sdk/amazon-bedrock@4.0.107", "", { "dependencies": { "@ai-sdk/anthropic": "3.0.78", "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27", "@smithy/eventstream-codec": "^4.0.1", "@smithy/util-utf8": "^4.0.0", "aws4fetch": "^1.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-8nT08pGPy25rleJNk56ep00UHK6kCtCmu+ZNqVVSSPDieADlIZqcaN1iRXAFBoCH0Fb9F6C2EjFDaySdsargfQ=="], "ai-gateway-provider/@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.78", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-0OY12G20cUt6iU6htpEA1491Oz++NVxZxlmWGX4B7rSbeZ5pnDmOu6YtW9BKzdZlNx5Gn23i6WMxyZFoMKNcgA=="], + "ai-gateway-provider/@ai-sdk/azure": ["@ai-sdk/azure@3.0.49", "", { "dependencies": { "@ai-sdk/openai": "3.0.48", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-wskgAL+OmrHG7by/iWIxEBQCEdc1mDudha/UZav46i0auzdFfsDB/k2rXZaC4/3nWSgMZkxr0W3ncyouEGX/eg=="], + + "ai-gateway-provider/@ai-sdk/deepseek": ["@ai-sdk/deepseek@2.0.35", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-9DhYurbAvcurOEGN6u2myYDybrrzGfcrkG8hwmFjwTrePW6KCMggm0YxP7e8RkLYcQKqCEMgFlyEB4BM6EmiKg=="], + + "ai-gateway-provider/@ai-sdk/mistral": ["@ai-sdk/mistral@3.0.27", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ZXe7nZQgliDdjz5ufH5RKpHWxbN72AzmzzKGbF/z+0K9GN5tUCnftrQRvTRFHA5jAzTapcm2BEevmGLVbMkW+A=="], + "ai-gateway-provider/@ai-sdk/openai": ["@ai-sdk/openai@3.0.53", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-Wld+Rbc05KaUn08uBt06eEuwcgalcIFtIl32Yp+GxuZXUQwOb6YeAuq+C6da4ch6BurFoqEaLemJVwjBb7x+PQ=="], "ai-gateway-provider/@ai-sdk/xai": ["@ai-sdk/xai@3.0.82", "", { "dependencies": { "@ai-sdk/openai-compatible": "2.0.41", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-A0VFMufnVf4wODcT3SPQUUzvYXiIO1VhFuXj9r6z/vP4rlo+QRDPw3WSTchcz93ROQWSfBE3I6Szqz342OHi5w=="], @@ -6936,11 +6240,9 @@ "babel-plugin-module-resolver/glob": ["glob@9.3.5", "", { "dependencies": { "fs.realpath": "^1.0.0", "minimatch": "^8.0.2", "minipass": "^4.2.4", "path-scurry": "^1.6.1" } }, "sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q=="], - "better-opn/open": ["open@8.4.2", "", { "dependencies": { "define-lazy-prop": "^2.0.0", "is-docker": "^2.1.1", "is-wsl": "^2.2.0" } }, "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ=="], + "body-parser/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], - "bl/buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="], - - "bl/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], + "body-parser/iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="], "builder-util/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], @@ -6950,8 +6252,6 @@ "c12/dotenv": ["dotenv@17.4.2", "", {}, "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw=="], - "chromium-bidi/zod": ["zod@3.23.8", "", {}, "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g=="], - "clone-response/mimic-response": ["mimic-response@1.0.1", "", {}, "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ=="], "compress-commons/is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="], @@ -6962,16 +6262,10 @@ "config-chain/ini": ["ini@1.3.8", "", {}, "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="], - "cosmiconfig/env-paths": ["env-paths@2.2.1", "", {}, "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A=="], - - "cosmiconfig/js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], - "crc/buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="], "cross-spawn/which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], - "degenerator/ast-types": ["ast-types@0.13.4", "", { "dependencies": { "tslib": "^2.0.1" } }, "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w=="], - "dir-compare/minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="], "dir-compare/p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="], @@ -7014,14 +6308,8 @@ "encoding/iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], - "engine.io/accepts": ["accepts@1.3.8", "", { "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" } }, "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw=="], - - "engine.io/cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="], - "engine.io-client/ws": ["ws@8.20.1", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w=="], - "error-ex/is-arrayish": ["is-arrayish@0.2.1", "", {}, "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg=="], - "esast-util-from-js/acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="], "esbuild-plugin-copy/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], @@ -7036,20 +6324,22 @@ "express/cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="], - "favicons/xml2js": ["xml2js@0.6.2", "", { "dependencies": { "sax": ">=0.6.0", "xmlbuilder": "~11.0.0" } }, "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA=="], + "express/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], + + "express/path-to-regexp": ["path-to-regexp@0.1.13", "", {}, "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA=="], "fetch-blob/web-streams-polyfill": ["web-streams-polyfill@3.3.3", "", {}, "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="], "filelist/minimatch": ["minimatch@5.1.9", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw=="], + "finalhandler/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], + "form-data/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], "fs-extra/jsonfile": ["jsonfile@6.2.1", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q=="], "gaxios/node-fetch": ["node-fetch@3.3.2", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="], - "get-uri/data-uri-to-buffer": ["data-uri-to-buffer@6.0.2", "", {}, "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw=="], - "gitlab-ai-provider/openai": ["openai@6.39.1", "", { "peerDependencies": { "ws": "^8.18.0", "zod": "^3.25 || ^4.0" }, "optionalPeers": ["ws", "zod"], "bin": { "openai": "bin/cli" } }, "sha512-z3dO9fEWOXBzlXynVb/xZ/tujzUjFWQWn3C0n0mw6Vo0zJTbEkaN4b2cLWjhJ6haJQx8LlREoafHRl+Gu/Hl+A=="], "gitlab-ai-provider/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], @@ -7068,26 +6358,6 @@ "iconv-corefoundation/node-addon-api": ["node-addon-api@1.7.2", "", {}, "sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg=="], - "import-fresh/resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], - - "ink/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], - - "ink/indent-string": ["indent-string@5.0.0", "", {}, "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg=="], - - "ink/react": ["react@19.2.3", "", {}, "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA=="], - - "ink/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], - - "ink/slice-ansi": ["slice-ansi@7.1.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "is-fullwidth-code-point": "^5.0.0" } }, "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w=="], - - "ink-spinner/cli-spinners": ["cli-spinners@2.9.2", "", {}, "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg=="], - - "inquirer/ansi-escapes": ["ansi-escapes@4.3.2", "", { "dependencies": { "type-fest": "^0.21.3" } }, "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ=="], - - "is-online/got": ["got@12.6.1", "", { "dependencies": { "@sindresorhus/is": "^5.2.0", "@szmarczak/http-timer": "^5.0.1", "cacheable-lookup": "^7.0.0", "cacheable-request": "^10.2.8", "decompress-response": "^6.0.0", "form-data-encoder": "^2.1.2", "get-stream": "^6.0.1", "http2-wrapper": "^2.1.10", "lowercase-keys": "^3.0.0", "p-cancelable": "^3.0.0", "responselike": "^3.0.0" } }, "sha512-mThBblvlAF1d4O5oqyvN+ZxLAYwIJK7bpMxgYqPD9okW0C3qm5FFn7k811QrcuEBwaogR3ngOFoCfs6mRv7teQ=="], - - "is-online/p-timeout": ["p-timeout@5.1.0", "", {}, "sha512-auFDyzzzGZZZdHz3BtET9VEz0SE/uMEAx7uWfGPucfzEwwe/xH0iVeZibQmANYE/hp9T2+UUZT5m+BKyrDp3Ew=="], - "istanbul-reports/html-escaper": ["html-escaper@2.0.2", "", {}, "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg=="], "js-beautify/glob": ["glob@10.5.0", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="], @@ -7096,20 +6366,18 @@ "katex/commander": ["commander@8.3.0", "", {}, "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww=="], - "keytar/node-addon-api": ["node-addon-api@4.3.0", "", {}, "sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ=="], - "lazystream/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="], "light-my-request/process-warning": ["process-warning@4.0.1", "", {}, "sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q=="], + "lightningcss/detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], + "matcher/escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], "md-to-react-email/marked": ["marked@7.0.4", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-t8eP0dXRJMtMvBojtkcsA7n48BkauktUKzfkPSCq85ZMTJ0v76Rke4DYz01omYpPTUh4p/f7HePgRo3ebG8+QQ=="], "micromark-extension-mdxjs/acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="], - "micromark-extension-mdxjs/micromark-extension-mdx-jsx": ["micromark-extension-mdx-jsx@3.0.2", "", { "dependencies": { "@types/estree": "^1.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "micromark-factory-mdx-expression": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-events-to-acorn": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-e5+q1DjMh62LZAJOnDraSSbDMvGJ8x3cbjygy2qFEi7HCeUT4BDKCvMozPozcD6WmOt6sVvYDNBKhFSz3kjOVQ=="], - "micromatch/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], "miniflare/acorn": ["acorn@8.14.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA=="], @@ -7126,10 +6394,6 @@ "motion/framer-motion": ["framer-motion@12.40.0", "", { "dependencies": { "motion-dom": "^12.40.0", "motion-utils": "^12.39.0", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-uaBd3qC1v3KQqBEjwTUd183K6PbS+j0yR9w9VmEOLWA/tnUcSn8Xa3uck7t4dgpDoUss8xQTcj8W2L07lrnLFg=="], - "next-mdx-remote-client/react": ["react@19.2.3", "", {}, "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA=="], - - "next-mdx-remote-client/serialize-error": ["serialize-error@13.0.1", "", { "dependencies": { "non-error": "^0.1.0", "type-fest": "^5.4.1" } }, "sha512-bBZaRwLH9PN5HbLCjPId4dP5bNGEtumcErgOX952IsvOhVPrm3/AeK1y0UHA/QaPG701eg0yEnOKsCOC6X/kaA=="], - "nitro/h3": ["h3@2.0.1-rc.5", "", { "dependencies": { "rou3": "^0.7.9", "srvx": "^0.9.1" }, "peerDependencies": { "crossws": "^0.4.1" }, "optionalPeers": ["crossws"] }, "sha512-qkohAzCab0nLzXNm78tBjZDvtKMTmtygS8BJLT3VPczAQofdqlFXDPkXdLMJN4r05+xqneG8snZJ0HgkERCZTg=="], "nitro/undici": ["undici@7.26.0", "", {}, "sha512-3O9Tf67pGhgOv9jM35AbhkXAKi13f3oy3aE4CSgr+TckGeY+/iu97ZXN+J7DpHPzLbVApFd1IFhcnBjREYXYcg=="], @@ -7138,6 +6402,8 @@ "node-gyp/undici": ["undici@6.26.0", "", {}, "sha512-4yqz8a3n5HmGTlsbADNtr/dJlhkh/55Rq798G6ibiULcXbDtaLpTl1pvdqcbFfeoj3iSi52lePFM7h9H21cw/A=="], + "node-gyp-build-optional-packages/detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], + "npm-run-path/path-key": ["path-key@4.0.0", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="], "nypm/citty": ["citty@0.2.2", "", {}, "sha512-+6vJA3L98yv+IdfKGZHBNiGW5KHn22e/JwID0Strsz8h4S/csAu/OuICwxrg44k5MRiZHWIo8XXuJgQTriRP4w=="], @@ -7146,7 +6412,7 @@ "opencode/@ai-sdk/cerebras": ["@ai-sdk/cerebras@2.0.60", "", { "dependencies": { "@ai-sdk/openai-compatible": "2.0.54", "@ai-sdk/provider": "3.0.12", "@ai-sdk/provider-utils": "4.0.33" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-Rnok3cThg6awBwaDSyiZpgRpbV7pqxGYrA89LODCo5cuEHeP2h0AM0lLHP7zIkclAdXfOm4wldKi/S2T/DGCOw=="], - "opencode/@ai-sdk/openai": ["@ai-sdk/openai@3.0.53", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-Wld+Rbc05KaUn08uBt06eEuwcgalcIFtIl32Yp+GxuZXUQwOb6YeAuq+C6da4ch6BurFoqEaLemJVwjBb7x+PQ=="], + "opencode/@ai-sdk/openai": ["@ai-sdk/openai@3.0.84", "", { "dependencies": { "@ai-sdk/provider": "3.0.14", "@ai-sdk/provider-utils": "4.0.38" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-cmgbeJL0bbY0yTJH4/AdmP5E7MjWRL9G8UdhIi0JlV/So03o82ORJofW8OzwCZPTORVQblFbpZXYGDcUd9NdUQ=="], "opencode/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.41", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-kNAGINk71AlOXx10Dq/PXw4t/9XjdK8uxfpVElRwtSFMdeSiLVt58p9TPx4/FJD+hxZuVhvxYj9r42osxWq79g=="], @@ -7162,20 +6428,12 @@ "openid-client/lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="], - "oxc-parser/@oxc-project/types": ["@oxc-project/types@0.127.0", "", {}, "sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ=="], - - "p-any/p-cancelable": ["p-cancelable@3.0.0", "", {}, "sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw=="], - "p-locate/p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="], "p-retry/retry": ["retry@0.13.1", "", {}, "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg=="], - "p-some/p-cancelable": ["p-cancelable@3.0.0", "", {}, "sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw=="], - "parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="], - "parse-json/json-parse-even-better-errors": ["json-parse-even-better-errors@2.3.1", "", {}, "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w=="], - "parse5/entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="], "pkg-dir/find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="], @@ -7196,8 +6454,6 @@ "postject/commander": ["commander@9.5.0", "", {}, "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ=="], - "prebuild-install/node-abi": ["node-abi@3.94.0", "", { "dependencies": { "semver": "^7.3.5" } }, "sha512-W5ZNO5KRPB5TkYmGVD9F6YqhsglXJzE6etpbmT+f6EQElhiX/UTG551cnsRGvLG3fyZEg9HwaDmNmj5nwJ4z9g=="], - "pretty-format/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], "pretty-format/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="], @@ -7206,32 +6462,24 @@ "proper-lockfile/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], - "proxy-agent/lru-cache": ["lru-cache@7.18.3", "", {}, "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA=="], - - "proxy-agent/proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="], - - "public-ip/got": ["got@12.6.1", "", { "dependencies": { "@sindresorhus/is": "^5.2.0", "@szmarczak/http-timer": "^5.0.1", "cacheable-lookup": "^7.0.0", "cacheable-request": "^10.2.8", "decompress-response": "^6.0.0", "form-data-encoder": "^2.1.2", "get-stream": "^6.0.1", "http2-wrapper": "^2.1.10", "lowercase-keys": "^3.0.0", "p-cancelable": "^3.0.0", "responselike": "^3.0.0" } }, "sha512-mThBblvlAF1d4O5oqyvN+ZxLAYwIJK7bpMxgYqPD9okW0C3qm5FFn7k811QrcuEBwaogR3ngOFoCfs6mRv7teQ=="], - - "rc/ini": ["ini@1.3.8", "", {}, "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="], - - "react-reconciler/react": ["react@19.2.3", "", {}, "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA=="], - - "react-reconciler/scheduler": ["scheduler@0.26.0", "", {}, "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA=="], + "raw-body/iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="], "readdir-glob/minimatch": ["minimatch@5.1.9", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw=="], - "restore-cursor/onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="], - - "restore-cursor/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], - "rimraf/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], "roarr/sprintf-js": ["sprintf-js@1.1.3", "", {}, "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA=="], "router/path-to-regexp": ["path-to-regexp@8.4.2", "", {}, "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA=="], + "send/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], + + "send/mime": ["mime@1.6.0", "", { "bin": { "mime": "cli.js" } }, "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="], + "serialize-error/type-fest": ["type-fest@0.13.1", "", {}, "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg=="], + "sharp/detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], + "shiki/@shikijs/core": ["@shikijs/core@4.2.0", "", { "dependencies": { "@shikijs/primitive": "4.2.0", "@shikijs/types": "4.2.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-Hc87Ab1Ld/vEbZRCbwx344I5v+4RU8CVToUTRkqXL1+TjbuOp9U5Xa0M23V4GEWHxVn+yO5otb+HkQVm3ptWQQ=="], "shiki/@shikijs/types": ["@shikijs/types@4.2.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-VT/MKtlpOhEPZloSH3Pb9WCZEBDoQVMa9jedp5UAwmJOar1DVc9DRODAxmYPW9M93IK4ryuqRejFfmlvlVDemw=="], @@ -7240,10 +6488,6 @@ "slice-ansi/is-fullwidth-code-point": ["is-fullwidth-code-point@4.0.0", "", {}, "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ=="], - "socket.io/accepts": ["accepts@1.3.8", "", { "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" } }, "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw=="], - - "socket.io/debug": ["debug@4.3.7", "", { "dependencies": { "ms": "^2.1.3" }, "peerDependencies": { "supports-color": "*" }, "optionalPeers": ["supports-color"] }, "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ=="], - "solid-transition-size/@corvu/utils": ["@corvu/utils@0.3.2", "", { "dependencies": { "@floating-ui/dom": "^1.6.7" }, "peerDependencies": { "solid-js": "^1.8" } }, "sha512-ZWlyWEE8qV9+CB9OAyo2bTrZGXQN9ZeM+JfYv89zoR+lRACKTDuoOZEdiyL8Uc7U5dUSH1uTqKhTTnaHWb+wZA=="], "sort-keys/is-plain-obj": ["is-plain-obj@1.1.0", "", {}, "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg=="], @@ -7252,8 +6496,6 @@ "sst/jose": ["jose@5.2.3", "", {}, "sha512-KUXdbctm1uHVL8BYhnyHkgp3zDX5KW8ZhAKVFEfUbU2P8Alpzjb+48hHvjOdQIyPshoblhzsuqOwEEAbtHVirA=="], - "stack-utils/escape-string-regexp": ["escape-string-regexp@2.0.0", "", {}, "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w=="], - "storybook/open": ["open@10.2.0", "", { "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", "is-inside-container": "^1.0.0", "wsl-utils": "^0.1.0" } }, "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA=="], "string-width-cjs/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], @@ -7266,10 +6508,6 @@ "tar/yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="], - "tar-fs/chownr": ["chownr@1.1.4", "", {}, "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="], - - "tar-fs/tar-stream": ["tar-stream@2.2.0", "", { "dependencies": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", "fs-constants": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^3.1.1" } }, "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ=="], - "terser/acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="], "terser/commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="], @@ -7286,9 +6524,7 @@ "tw-to-css/tailwindcss": ["tailwindcss@3.3.2", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", "chokidar": "^3.5.3", "didyoumean": "^1.2.2", "dlv": "^1.1.3", "fast-glob": "^3.2.12", "glob-parent": "^6.0.2", "is-glob": "^4.0.3", "jiti": "^1.18.2", "lilconfig": "^2.1.0", "micromatch": "^4.0.5", "normalize-path": "^3.0.0", "object-hash": "^3.0.0", "picocolors": "^1.0.0", "postcss": "^8.4.23", "postcss-import": "^15.1.0", "postcss-js": "^4.0.1", "postcss-load-config": "^4.0.1", "postcss-nested": "^6.0.1", "postcss-selector-parser": "^6.0.11", "postcss-value-parser": "^4.2.0", "resolve": "^1.22.2", "sucrase": "^3.32.0" }, "bin": { "tailwind": "lib/cli.js", "tailwindcss": "lib/cli.js" } }, "sha512-9jPkMiIBXvPc2KywkraqsUfbfj+dHDb+JPWtSJa9MLFdrPyazI7q6WX2sUrm7R9eVR7qqv3Pas7EvQFzxKnI6w=="], - "type-is/content-type": ["content-type@2.0.0", "", {}, "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ=="], - - "unbzip2-stream/buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="], + "type-is/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], "unifont/ofetch": ["ofetch@1.5.1", "", { "dependencies": { "destr": "^2.0.5", "node-fetch-native": "^1.6.7", "ufo": "^1.6.1" } }, "sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA=="], @@ -7334,8 +6570,6 @@ "wrap-ansi-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - "xss/commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="], - "yaml-language-server/request-light": ["request-light@0.5.8", "", {}, "sha512-3Zjgh+8b5fhRJBQZoy+zbVKpAQGLyka0MPgW3zruTF4dFFJ8Fqcfu9YsAvi/rvdcaTeWG3MkbZv4WKxAn/84Lg=="], "yaml-language-server/yaml": ["yaml@2.7.1", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-10ULxpnOCQXxJvBgxsn9ptjq6uviG/htZKk9veJGhlqn3w/DxQ631zFF+nlQXLwmImeS5amR2dl2U8sg6U9jsQ=="], @@ -7422,8 +6656,6 @@ "@astrojs/starlight/js-yaml/argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], - "@asyncapi/parser/js-yaml/argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], - "@aws-crypto/sha1-browser/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="], "@aws-crypto/sha256-browser/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="], @@ -7506,10 +6738,6 @@ "@hey-api/json-schema-ref-parser/js-yaml/argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], - "@inquirer/core/wrap-ansi/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], - - "@inquirer/core/wrap-ansi/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - "@jsx-email/cli/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.19.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA=="], "@jsx-email/cli/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.19.12", "", { "os": "android", "cpu": "arm" }, "sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w=="], @@ -7572,159 +6800,27 @@ "@malept/flatpak-bundler/fs-extra/jsonfile": ["jsonfile@6.2.1", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q=="], - "@mintlify/cli/fs-extra/jsonfile": ["jsonfile@6.2.1", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q=="], + "@modelcontextprotocol/sdk/express/accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="], - "@mintlify/cli/js-yaml/argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], + "@modelcontextprotocol/sdk/express/body-parser": ["body-parser@2.2.2", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.1", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA=="], - "@mintlify/cli/open/define-lazy-prop": ["define-lazy-prop@2.0.0", "", {}, "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og=="], + "@modelcontextprotocol/sdk/express/content-disposition": ["content-disposition@1.1.0", "", {}, "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g=="], - "@mintlify/cli/open/is-docker": ["is-docker@2.2.1", "", { "bin": { "is-docker": "cli.js" } }, "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ=="], + "@modelcontextprotocol/sdk/express/cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="], - "@mintlify/cli/open/is-wsl": ["is-wsl@2.2.0", "", { "dependencies": { "is-docker": "^2.0.0" } }, "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww=="], + "@modelcontextprotocol/sdk/express/cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="], - "@mintlify/cli/openid-client/jose": ["jose@6.2.3", "", {}, "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw=="], + "@modelcontextprotocol/sdk/express/finalhandler": ["finalhandler@2.1.1", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA=="], - "@mintlify/cli/yargs/cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="], + "@modelcontextprotocol/sdk/express/fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="], - "@mintlify/cli/yargs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + "@modelcontextprotocol/sdk/express/merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="], - "@mintlify/common/hast-util-to-html/property-information": ["property-information@6.5.0", "", {}, "sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig=="], + "@modelcontextprotocol/sdk/express/send": ["send@1.2.1", "", { "dependencies": { "debug": "^4.4.3", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.1", "mime-types": "^3.0.2", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.2" } }, "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ=="], - "@mintlify/common/js-yaml/argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], + "@modelcontextprotocol/sdk/express/serve-static": ["serve-static@2.2.1", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw=="], - "@mintlify/common/mdast-util-gfm/mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.3", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q=="], - - "@mintlify/common/mdast-util-mdx-jsx/mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.3", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q=="], - - "@mintlify/common/remark-gfm/mdast-util-gfm": ["mdast-util-gfm@3.1.0", "", { "dependencies": { "mdast-util-from-markdown": "^2.0.0", "mdast-util-gfm-autolink-literal": "^2.0.0", "mdast-util-gfm-footnote": "^2.0.0", "mdast-util-gfm-strikethrough": "^2.0.0", "mdast-util-gfm-table": "^2.0.0", "mdast-util-gfm-task-list-item": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ=="], - - "@mintlify/common/sucrase/commander": ["commander@4.1.1", "", {}, "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA=="], - - "@mintlify/common/sucrase/glob": ["glob@7.1.6", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.0.4", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA=="], - - "@mintlify/common/tailwindcss/chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="], - - "@mintlify/common/tailwindcss/glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], - - "@mintlify/common/tailwindcss/jiti": ["jiti@1.21.7", "", { "bin": { "jiti": "bin/jiti.js" } }, "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A=="], - - "@mintlify/common/tailwindcss/lilconfig": ["lilconfig@3.1.3", "", {}, "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw=="], - - "@mintlify/common/tailwindcss/object-hash": ["object-hash@3.0.0", "", {}, "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw=="], - - "@mintlify/common/tailwindcss/postcss": ["postcss@8.5.15", "", { "dependencies": { "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A=="], - - "@mintlify/common/tailwindcss/sucrase": ["sucrase@3.35.1", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.2", "commander": "^4.0.0", "lines-and-columns": "^1.1.6", "mz": "^2.7.0", "pirates": "^4.0.1", "tinyglobby": "^0.2.11", "ts-interface-checker": "^0.1.9" }, "bin": { "sucrase": "bin/sucrase", "sucrase-node": "bin/sucrase-node" } }, "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw=="], - - "@mintlify/common/unist-util-visit/unist-util-visit-parents": ["unist-util-visit-parents@6.0.2", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ=="], - - "@mintlify/link-rot/fs-extra/jsonfile": ["jsonfile@6.2.1", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q=="], - - "@mintlify/link-rot/unist-util-visit/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="], - - "@mintlify/link-rot/unist-util-visit/unist-util-is": ["unist-util-is@5.2.1", "", { "dependencies": { "@types/unist": "^2.0.0" } }, "sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw=="], - - "@mintlify/link-rot/unist-util-visit/unist-util-visit-parents": ["unist-util-visit-parents@5.1.3", "", { "dependencies": { "@types/unist": "^2.0.0", "unist-util-is": "^5.0.0" } }, "sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg=="], - - "@mintlify/mdx/@shikijs/transformers/@shikijs/core": ["@shikijs/core@3.20.0", "", { "dependencies": { "@shikijs/types": "3.20.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-f2ED7HYV4JEk827mtMDwe/yQ25pRiXZmtHjWF8uzZKuKiEsJR7Ce1nuQ+HhV9FzDcbIo4ObBCD9GPTzNuy9S1g=="], - - "@mintlify/mdx/@shikijs/transformers/@shikijs/types": ["@shikijs/types@3.20.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-lhYAATn10nkZcBQ0BlzSbJA3wcmL5MXUUF8d2Zzon6saZDlToKaiRX60n2+ZaHJCmXEcZRWNzn+k9vplr8Jhsw=="], - - "@mintlify/mdx/shiki/@shikijs/core": ["@shikijs/core@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-NSWQz0riNb67xthdm5br6lAkvpDJRTgB36fxlo37ZzM2yq0PQFFzbd8psqC2XMPgCzo1fW6cVi18+ArJ44wqgA=="], - - "@mintlify/mdx/shiki/@shikijs/engine-javascript": ["@shikijs/engine-javascript@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.4" } }, "sha512-aHt9eiGFobmWR5uqJUViySI1bHMqrAgamWE1TYSUoftkAeCCAiGawPMwM+VCadylQtF4V3VNOZ5LmfItH5f3yA=="], - - "@mintlify/mdx/shiki/@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-1nWINwKXxKKLqPibT5f4pAFLej9oZzQTsby8942OTlsJzOBZ0MWKiwzMsd+jhzu8YPCHAswGnnN1YtQfirL35g=="], - - "@mintlify/mdx/shiki/@shikijs/langs": ["@shikijs/langs@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0" } }, "sha512-2Ep4W3Re5aB1/62RSYQInK9mM3HsLeB91cHqznAJMuylqjzNVAVCMnNWRHFtcNHXsoNRayP9z1qj4Sq3nMqYXg=="], - - "@mintlify/mdx/shiki/@shikijs/themes": ["@shikijs/themes@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0" } }, "sha512-5qySYa1ZgAT18HR/ypENL9cUSGOeI2x+4IvYJu4JgVJdizn6kG4ia5Q1jDEOi7gTbN4RbuYtmHh0W3eccOrjMA=="], - - "@mintlify/mdx/shiki/@shikijs/types": ["@shikijs/types@3.23.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-3JZ5HXOZfYjsYSk0yPwBrkupyYSLpAE26Qc0HLghhZNGTZg/SKxXIIgoxOpmmeQP0RRSDJTk1/vPfw9tbw+jSQ=="], - - "@mintlify/prebuild/fs-extra/jsonfile": ["jsonfile@6.2.1", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q=="], - - "@mintlify/prebuild/js-yaml/argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], - - "@mintlify/prebuild/unist-util-visit/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="], - - "@mintlify/prebuild/unist-util-visit/unist-util-is": ["unist-util-is@5.2.1", "", { "dependencies": { "@types/unist": "^2.0.0" } }, "sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw=="], - - "@mintlify/prebuild/unist-util-visit/unist-util-visit-parents": ["unist-util-visit-parents@5.1.3", "", { "dependencies": { "@types/unist": "^2.0.0", "unist-util-is": "^5.0.0" } }, "sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg=="], - - "@mintlify/previewing/chokidar/readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="], - - "@mintlify/previewing/express/accepts": ["accepts@1.3.8", "", { "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" } }, "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw=="], - - "@mintlify/previewing/express/body-parser": ["body-parser@1.20.5", "", { "dependencies": { "bytes": "~3.1.2", "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "~1.2.0", "http-errors": "~2.0.1", "iconv-lite": "~0.4.24", "on-finished": "~2.4.1", "qs": "~6.15.1", "raw-body": "~2.5.3", "type-is": "~1.6.18", "unpipe": "~1.0.0" } }, "sha512-3grm+/2tUOvu2cjJkvsIxrv/wVpfXQW4PsQHYm7yk4vfpu7Ekl6nEsYBoJUL6qDwZUx8wUhQ8tR2qz+ad9c9OA=="], - - "@mintlify/previewing/express/content-disposition": ["content-disposition@0.5.4", "", { "dependencies": { "safe-buffer": "5.2.1" } }, "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ=="], - - "@mintlify/previewing/express/cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="], - - "@mintlify/previewing/express/cookie-signature": ["cookie-signature@1.0.7", "", {}, "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA=="], - - "@mintlify/previewing/express/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], - - "@mintlify/previewing/express/finalhandler": ["finalhandler@1.3.2", "", { "dependencies": { "debug": "2.6.9", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "on-finished": "~2.4.1", "parseurl": "~1.3.3", "statuses": "~2.0.2", "unpipe": "~1.0.0" } }, "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg=="], - - "@mintlify/previewing/express/fresh": ["fresh@0.5.2", "", {}, "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q=="], - - "@mintlify/previewing/express/merge-descriptors": ["merge-descriptors@1.0.3", "", {}, "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ=="], - - "@mintlify/previewing/express/path-to-regexp": ["path-to-regexp@0.1.13", "", {}, "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA=="], - - "@mintlify/previewing/express/qs": ["qs@6.14.2", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q=="], - - "@mintlify/previewing/express/send": ["send@0.19.2", "", { "dependencies": { "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "fresh": "~0.5.2", "http-errors": "~2.0.1", "mime": "1.6.0", "ms": "2.1.3", "on-finished": "~2.4.1", "range-parser": "~1.2.1", "statuses": "~2.0.2" } }, "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg=="], - - "@mintlify/previewing/express/serve-static": ["serve-static@1.16.3", "", { "dependencies": { "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", "send": "~0.19.1" } }, "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA=="], - - "@mintlify/previewing/express/type-is": ["type-is@1.6.18", "", { "dependencies": { "media-typer": "0.3.0", "mime-types": "~2.1.24" } }, "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g=="], - - "@mintlify/previewing/fs-extra/jsonfile": ["jsonfile@6.2.1", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q=="], - - "@mintlify/previewing/got/@sindresorhus/is": ["@sindresorhus/is@5.6.0", "", {}, "sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g=="], - - "@mintlify/previewing/got/@szmarczak/http-timer": ["@szmarczak/http-timer@5.0.1", "", { "dependencies": { "defer-to-connect": "^2.0.1" } }, "sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw=="], - - "@mintlify/previewing/got/cacheable-lookup": ["cacheable-lookup@7.0.0", "", {}, "sha512-+qJyx4xiKra8mZrcwhjMRMUhD5NR1R8esPkzIYxX96JiecFoxAXFuz/GpR3+ev4PE1WamHip78wV0vcmPQtp8w=="], - - "@mintlify/previewing/got/cacheable-request": ["cacheable-request@10.2.14", "", { "dependencies": { "@types/http-cache-semantics": "^4.0.2", "get-stream": "^6.0.1", "http-cache-semantics": "^4.1.1", "keyv": "^4.5.3", "mimic-response": "^4.0.0", "normalize-url": "^8.0.0", "responselike": "^3.0.0" } }, "sha512-zkDT5WAF4hSSoUgyfg5tFIxz8XQK+25W/TLVojJTMKBaxevLBBtLxgqguAuVQB8PVW79FVjHcU+GJ9tVbDZ9mQ=="], - - "@mintlify/previewing/got/form-data-encoder": ["form-data-encoder@2.1.4", "", {}, "sha512-yDYSgNMraqvnxiEXO4hi88+YZxaHC6QKzb5N84iRCTDeRO7ZALpir/lVmf/uXUhnwUr2O4HU8s/n6x+yNjQkHw=="], - - "@mintlify/previewing/got/get-stream": ["get-stream@6.0.1", "", {}, "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg=="], - - "@mintlify/previewing/got/http2-wrapper": ["http2-wrapper@2.2.1", "", { "dependencies": { "quick-lru": "^5.1.1", "resolve-alpn": "^1.2.0" } }, "sha512-V5nVw1PAOgfI3Lmeaj2Exmeg7fenjhRUgz1lPSezy1CuhPYbgQtbQj4jZfEAEMlaL+vupsvhjqCyjzob0yxsmQ=="], - - "@mintlify/previewing/got/lowercase-keys": ["lowercase-keys@3.0.0", "", {}, "sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ=="], - - "@mintlify/previewing/got/p-cancelable": ["p-cancelable@3.0.0", "", {}, "sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw=="], - - "@mintlify/previewing/got/responselike": ["responselike@3.0.0", "", { "dependencies": { "lowercase-keys": "^3.0.0" } }, "sha512-40yHxbNcl2+rzXvZuVkrYohathsSJlMTXKryG5y8uciHv1+xDLHQpgjG64JUO9nrEq2jGLH6IZ8BcZyw3wrweg=="], - - "@mintlify/previewing/js-yaml/argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], - - "@mintlify/previewing/unist-util-visit/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="], - - "@mintlify/previewing/unist-util-visit/unist-util-is": ["unist-util-is@5.2.1", "", { "dependencies": { "@types/unist": "^2.0.0" } }, "sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw=="], - - "@mintlify/previewing/unist-util-visit/unist-util-visit-parents": ["unist-util-visit-parents@5.1.3", "", { "dependencies": { "@types/unist": "^2.0.0", "unist-util-is": "^5.0.0" } }, "sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg=="], - - "@mintlify/previewing/yargs/cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="], - - "@mintlify/previewing/yargs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], - - "@mintlify/scraping/fs-extra/jsonfile": ["jsonfile@6.2.1", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q=="], - - "@mintlify/scraping/js-yaml/argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], - - "@mintlify/scraping/yargs/cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="], - - "@mintlify/scraping/yargs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], - - "@mintlify/validation/js-yaml/argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], + "@modelcontextprotocol/sdk/express/type-is": ["type-is@2.1.0", "", { "dependencies": { "content-type": "^2.0.0", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA=="], "@octokit/auth-app/@octokit/request/@octokit/endpoint": ["@octokit/endpoint@11.0.3", "", { "dependencies": { "@octokit/types": "^16.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-FWFlNxghg4HrXkD3ifYbS/IdL/mDHjh9QcsNyhQjN8dplUoZbejsdpmuqdA76nxj2xoWPs7p8uX2SNr9rYu0Ag=="], @@ -7824,6 +6920,10 @@ "@octokit/rest/@octokit/core/before-after-hook": ["before-after-hook@4.0.0", "", {}, "sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ=="], + "@opencode-ai/core/@ai-sdk/openai/@ai-sdk/provider": ["@ai-sdk/provider@3.0.14", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-5X1k57JBJ4H7H1QjX7CnJYAB1I19r/trVZTMcSms7/kLNZ8RaU4Nt2agcwZzv82Hfx6Q7/TOLU7agAKeFfc8cA=="], + + "@opencode-ai/core/@ai-sdk/openai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.38", "", { "dependencies": { "@ai-sdk/provider": "3.0.14", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-/HHGmtKllqjg1OLc023v9w9kK3laW7Z6TzfZukYQWCsGBbzB9p60zTvvpXFVcs44NZBVXL3viOa1HRKUbeee8g=="], + "@opencode-ai/desktop/@actions/artifact/@actions/http-client": ["@actions/http-client@2.2.3", "", { "dependencies": { "tunnel": "^0.0.6", "undici": "^5.25.4" } }, "sha512-mx8hyJi/hjFvbPokCg4uRd4ZX78t+YyRPtnKWwIl+RzNaVuFpQHfmlGVfsKEJN8LwTCvL+DfVgAM04XaHkm6bA=="], "@opencode-ai/web/@shikijs/transformers/@shikijs/core": ["@shikijs/core@3.20.0", "", { "dependencies": { "@shikijs/types": "3.20.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-f2ED7HYV4JEk827mtMDwe/yQ25pRiXZmtHjWF8uzZKuKiEsJR7Ce1nuQ+HhV9FzDcbIo4ObBCD9GPTzNuy9S1g=="], @@ -7832,20 +6932,10 @@ "@opentui/solid/@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], - "@oxc-parser/binding-wasm32-wasi/@emnapi/core/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w=="], - - "@oxc-resolver/binding-wasm32-wasi/@emnapi/core/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w=="], - "@pierre/diffs/@shikijs/transformers/@shikijs/core": ["@shikijs/core@3.20.0", "", { "dependencies": { "@shikijs/types": "3.20.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-f2ED7HYV4JEk827mtMDwe/yQ25pRiXZmtHjWF8uzZKuKiEsJR7Ce1nuQ+HhV9FzDcbIo4ObBCD9GPTzNuy9S1g=="], "@pierre/diffs/@shikijs/transformers/@shikijs/types": ["@shikijs/types@3.20.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-lhYAATn10nkZcBQ0BlzSbJA3wcmL5MXUUF8d2Zzon6saZDlToKaiRX60n2+ZaHJCmXEcZRWNzn+k9vplr8Jhsw=="], - "@puppeteer/browsers/yargs/cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="], - - "@puppeteer/browsers/yargs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], - - "@rolldown/binding-wasm32-wasi/@napi-rs/wasm-runtime/@tybys/wasm-util": ["@tybys/wasm-util@0.10.3", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg=="], - "@sentry/bundler-plugin-core/glob/minimatch": ["minimatch@8.0.7", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-V+1uQNdzybxa14e/p00HZnQNNcTjnRJjDxg2V8wtkjFctq4M7hXFws4oekyTP0Jebeq7QYtpFyOeBAjc88zvYg=="], "@sentry/bundler-plugin-core/glob/minipass": ["minipass@4.2.8", "", {}, "sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ=="], @@ -7858,34 +6948,6 @@ "@shikijs/stream/@shikijs/core/@shikijs/types": ["@shikijs/types@4.2.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-VT/MKtlpOhEPZloSH3Pb9WCZEBDoQVMa9jedp5UAwmJOar1DVc9DRODAxmYPW9M93IK4ryuqRejFfmlvlVDemw=="], - "@slack/bolt/express/accepts": ["accepts@1.3.8", "", { "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" } }, "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw=="], - - "@slack/bolt/express/body-parser": ["body-parser@1.20.5", "", { "dependencies": { "bytes": "~3.1.2", "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "~1.2.0", "http-errors": "~2.0.1", "iconv-lite": "~0.4.24", "on-finished": "~2.4.1", "qs": "~6.15.1", "raw-body": "~2.5.3", "type-is": "~1.6.18", "unpipe": "~1.0.0" } }, "sha512-3grm+/2tUOvu2cjJkvsIxrv/wVpfXQW4PsQHYm7yk4vfpu7Ekl6nEsYBoJUL6qDwZUx8wUhQ8tR2qz+ad9c9OA=="], - - "@slack/bolt/express/content-disposition": ["content-disposition@0.5.4", "", { "dependencies": { "safe-buffer": "5.2.1" } }, "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ=="], - - "@slack/bolt/express/cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="], - - "@slack/bolt/express/cookie-signature": ["cookie-signature@1.0.7", "", {}, "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA=="], - - "@slack/bolt/express/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], - - "@slack/bolt/express/finalhandler": ["finalhandler@1.3.2", "", { "dependencies": { "debug": "2.6.9", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "on-finished": "~2.4.1", "parseurl": "~1.3.3", "statuses": "~2.0.2", "unpipe": "~1.0.0" } }, "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg=="], - - "@slack/bolt/express/fresh": ["fresh@0.5.2", "", {}, "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q=="], - - "@slack/bolt/express/merge-descriptors": ["merge-descriptors@1.0.3", "", {}, "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ=="], - - "@slack/bolt/express/path-to-regexp": ["path-to-regexp@0.1.13", "", {}, "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA=="], - - "@slack/bolt/express/send": ["send@0.19.2", "", { "dependencies": { "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "fresh": "~0.5.2", "http-errors": "~2.0.1", "mime": "1.6.0", "ms": "2.1.3", "on-finished": "~2.4.1", "range-parser": "~1.2.1", "statuses": "~2.0.2" } }, "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg=="], - - "@slack/bolt/express/serve-static": ["serve-static@1.16.3", "", { "dependencies": { "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", "send": "~0.19.1" } }, "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA=="], - - "@slack/bolt/express/type-is": ["type-is@1.6.18", "", { "dependencies": { "media-typer": "0.3.0", "mime-types": "~2.1.24" } }, "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g=="], - - "@slack/bolt/raw-body/iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="], - "@slack/web-api/form-data/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], "@slack/web-api/p-queue/eventemitter3": ["eventemitter3@4.0.7", "", {}, "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw=="], @@ -7908,34 +6970,18 @@ "@standard-community/standard-openapi/effect/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - "@stoplight/spectral-core/minimatch/brace-expansion": ["brace-expansion@1.1.15", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg=="], - "@storybook/csf-plugin/unplugin/acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="], "@storybook/csf-plugin/unplugin/webpack-virtual-modules": ["webpack-virtual-modules@0.6.2", "", {}, "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ=="], "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime/@tybys/wasm-util": ["@tybys/wasm-util@0.10.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg=="], - "@vercel/cli-exec/execa/get-stream": ["get-stream@6.0.1", "", {}, "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg=="], - - "@vercel/cli-exec/execa/human-signals": ["human-signals@2.1.0", "", {}, "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw=="], - - "@vercel/cli-exec/execa/is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="], - - "@vercel/cli-exec/execa/npm-run-path": ["npm-run-path@4.0.1", "", { "dependencies": { "path-key": "^3.0.0" } }, "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw=="], - - "@vercel/cli-exec/execa/onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="], - - "@vercel/cli-exec/execa/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], - - "@vercel/cli-exec/execa/strip-final-newline": ["strip-final-newline@2.0.0", "", {}, "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA=="], - - "@vercel/functions/@vercel/oidc/jose": ["jose@5.9.6", "", {}, "sha512-AMlnetc9+CV9asI19zHmrgS/WYsWUwCn2R7RzlbJWD7F9eWYUTGyBmU9o6PxngtLGOiDGPRu+Uc4fhKzbpteZQ=="], - "@vitest/coverage-v8/@vitest/utils/@vitest/pretty-format": ["@vitest/pretty-format@4.1.8", "", { "dependencies": { "tinyrainbow": "^3.1.0" } }, "sha512-9GasEBxpZ1VYIpqHf/0+YGg121uSNwCKOJqIrTwWP/TB7DmFCiaBpNl3aPZzoLWfWkuqhbH8vJIVobZkvdo2cA=="], "@vitest/expect/@vitest/utils/@vitest/pretty-format": ["@vitest/pretty-format@3.2.4", "", { "dependencies": { "tinyrainbow": "^2.0.0" } }, "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA=="], + "accepts/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], + "ai-gateway-provider/@ai-sdk/amazon-bedrock/@ai-sdk/provider": ["@ai-sdk/provider@3.0.10", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-Q3BZ27qfpYqnCYGvE3vt+Qi6LGOF9R5Nmzn+9JoM1lCRsD9mYaIhfJLkSunN48nfGXJ6n+XNV0J/XVpqGQl7Dw=="], "ai-gateway-provider/@ai-sdk/amazon-bedrock/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.27", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ubkAJ+xODouwtmN1tYlvTPphH1hPOBfZaEQe8U7skGvFAnIRs9PPpsq57bC2+Ky/MB4yzhd6YOsxTAx9sGpazw=="], @@ -7948,6 +6994,16 @@ "ai-gateway-provider/@ai-sdk/anthropic/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.27", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ubkAJ+xODouwtmN1tYlvTPphH1hPOBfZaEQe8U7skGvFAnIRs9PPpsq57bC2+Ky/MB4yzhd6YOsxTAx9sGpazw=="], + "ai-gateway-provider/@ai-sdk/azure/@ai-sdk/openai": ["@ai-sdk/openai@3.0.48", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ALmj/53EXpcRqMbGpPJPP4UOSWw0q4VGpnDo7YctvsynjkrKDmoneDG/1a7VQnSPYHnJp6tTRMf5ZdxZ5whulg=="], + + "ai-gateway-provider/@ai-sdk/azure/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="], + + "ai-gateway-provider/@ai-sdk/deepseek/@ai-sdk/provider": ["@ai-sdk/provider@3.0.10", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-Q3BZ27qfpYqnCYGvE3vt+Qi6LGOF9R5Nmzn+9JoM1lCRsD9mYaIhfJLkSunN48nfGXJ6n+XNV0J/XVpqGQl7Dw=="], + + "ai-gateway-provider/@ai-sdk/deepseek/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.27", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ubkAJ+xODouwtmN1tYlvTPphH1hPOBfZaEQe8U7skGvFAnIRs9PPpsq57bC2+Ky/MB4yzhd6YOsxTAx9sGpazw=="], + + "ai-gateway-provider/@ai-sdk/mistral/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="], + "ai-gateway-provider/@ai-sdk/xai/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.41", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-kNAGINk71AlOXx10Dq/PXw4t/9XjdK8uxfpVElRwtSFMdeSiLVt58p9TPx4/FJD+hxZuVhvxYj9r42osxWq79g=="], "ajv-keywords/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], @@ -8002,11 +7058,7 @@ "babel-plugin-module-resolver/glob/path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="], - "better-opn/open/define-lazy-prop": ["define-lazy-prop@2.0.0", "", {}, "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og=="], - - "better-opn/open/is-docker": ["is-docker@2.2.1", "", { "bin": { "is-docker": "cli.js" } }, "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ=="], - - "better-opn/open/is-wsl": ["is-wsl@2.2.0", "", { "dependencies": { "is-docker": "^2.0.0" } }, "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww=="], + "body-parser/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], "builder-util/js-yaml/argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], @@ -8014,8 +7066,6 @@ "conf/dot-prop/type-fest": ["type-fest@5.7.0", "", { "dependencies": { "tagged-tag": "^1.0.0" } }, "sha512-1URUxUqfHFM1c+zfSPsa3gnkO7Aq21qyH75SIduNYz4SzY964rn1X2vCMQaHSHhktiw+0kPa2iyb6PUpXqB6Vg=="], - "cosmiconfig/js-yaml/argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], - "cross-spawn/which/isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], "dir-compare/minimatch/brace-expansion": ["brace-expansion@1.1.15", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg=="], @@ -8066,44 +7116,20 @@ "electron-winstaller/fs-extra/universalify": ["universalify@0.1.2", "", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="], - "engine.io/accepts/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], - - "engine.io/accepts/negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="], - "esbuild-plugin-copy/chokidar/readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="], + "express/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], + "filelist/minimatch/brace-expansion": ["brace-expansion@2.1.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA=="], + "finalhandler/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], + "form-data/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], "iconv-corefoundation/cli-truncate/slice-ansi": ["slice-ansi@3.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "astral-regex": "^2.0.0", "is-fullwidth-code-point": "^3.0.0" } }, "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ=="], "iconv-corefoundation/cli-truncate/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], - "ink/slice-ansi/is-fullwidth-code-point": ["is-fullwidth-code-point@5.1.0", "", { "dependencies": { "get-east-asian-width": "^1.3.1" } }, "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ=="], - - "inquirer/ansi-escapes/type-fest": ["type-fest@0.21.3", "", {}, "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w=="], - - "is-online/got/@sindresorhus/is": ["@sindresorhus/is@5.6.0", "", {}, "sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g=="], - - "is-online/got/@szmarczak/http-timer": ["@szmarczak/http-timer@5.0.1", "", { "dependencies": { "defer-to-connect": "^2.0.1" } }, "sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw=="], - - "is-online/got/cacheable-lookup": ["cacheable-lookup@7.0.0", "", {}, "sha512-+qJyx4xiKra8mZrcwhjMRMUhD5NR1R8esPkzIYxX96JiecFoxAXFuz/GpR3+ev4PE1WamHip78wV0vcmPQtp8w=="], - - "is-online/got/cacheable-request": ["cacheable-request@10.2.14", "", { "dependencies": { "@types/http-cache-semantics": "^4.0.2", "get-stream": "^6.0.1", "http-cache-semantics": "^4.1.1", "keyv": "^4.5.3", "mimic-response": "^4.0.0", "normalize-url": "^8.0.0", "responselike": "^3.0.0" } }, "sha512-zkDT5WAF4hSSoUgyfg5tFIxz8XQK+25W/TLVojJTMKBaxevLBBtLxgqguAuVQB8PVW79FVjHcU+GJ9tVbDZ9mQ=="], - - "is-online/got/form-data-encoder": ["form-data-encoder@2.1.4", "", {}, "sha512-yDYSgNMraqvnxiEXO4hi88+YZxaHC6QKzb5N84iRCTDeRO7ZALpir/lVmf/uXUhnwUr2O4HU8s/n6x+yNjQkHw=="], - - "is-online/got/get-stream": ["get-stream@6.0.1", "", {}, "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg=="], - - "is-online/got/http2-wrapper": ["http2-wrapper@2.2.1", "", { "dependencies": { "quick-lru": "^5.1.1", "resolve-alpn": "^1.2.0" } }, "sha512-V5nVw1PAOgfI3Lmeaj2Exmeg7fenjhRUgz1lPSezy1CuhPYbgQtbQj4jZfEAEMlaL+vupsvhjqCyjzob0yxsmQ=="], - - "is-online/got/lowercase-keys": ["lowercase-keys@3.0.0", "", {}, "sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ=="], - - "is-online/got/p-cancelable": ["p-cancelable@3.0.0", "", {}, "sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw=="], - - "is-online/got/responselike": ["responselike@3.0.0", "", { "dependencies": { "lowercase-keys": "^3.0.0" } }, "sha512-40yHxbNcl2+rzXvZuVkrYohathsSJlMTXKryG5y8uciHv1+xDLHQpgjG64JUO9nrEq2jGLH6IZ8BcZyw3wrweg=="], - "js-beautify/glob/jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="], "js-beautify/glob/minimatch": ["minimatch@9.0.9", "", { "dependencies": { "brace-expansion": "^2.0.2" } }, "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg=="], @@ -8122,8 +7148,6 @@ "motion/framer-motion/motion-utils": ["motion-utils@12.39.0", "", {}, "sha512-8nadJAJjTtqRkmRF36FoJTrywK9nnFmnPwnSMyxaOCU7GDjN9RTMJIxx9De8ErM+vpPhMccr/6fo5WciyQLnMQ=="], - "next-mdx-remote-client/serialize-error/type-fest": ["type-fest@5.7.0", "", { "dependencies": { "tagged-tag": "^1.0.0" } }, "sha512-1URUxUqfHFM1c+zfSPsa3gnkO7Aq21qyH75SIduNYz4SzY964rn1X2vCMQaHSHhktiw+0kPa2iyb6PUpXqB6Vg=="], - "opencode-gitlab-auth/open/wsl-utils": ["wsl-utils@0.1.0", "", { "dependencies": { "is-wsl": "^3.1.0" } }, "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw=="], "opencode/@ai-sdk/cerebras/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.54", "", { "dependencies": { "@ai-sdk/provider": "3.0.12", "@ai-sdk/provider-utils": "4.0.33" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-OyXt0zK8y2/ZIyWlbxTv2r1M7AK227S+Gl4BYOEF42q0wz1n5m4fwR8L4Fy/MQ4Ho6xje47MPsFcRdIqIyP6Rw=="], @@ -8132,50 +7156,26 @@ "opencode/@ai-sdk/cerebras/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.33", "", { "dependencies": { "@ai-sdk/provider": "3.0.12", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-nJ0bAfegMAIJtrzMJtbzer1cS3nb7c7DsyU1S4nrPm7ZU0Mn6SBBZv5IGZZGTbpWTJwqKTSPeZJTXalbAxt1BA=="], + "opencode/@ai-sdk/openai/@ai-sdk/provider": ["@ai-sdk/provider@3.0.14", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-5X1k57JBJ4H7H1QjX7CnJYAB1I19r/trVZTMcSms7/kLNZ8RaU4Nt2agcwZzv82Hfx6Q7/TOLU7agAKeFfc8cA=="], + + "opencode/@ai-sdk/openai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.38", "", { "dependencies": { "@ai-sdk/provider": "3.0.14", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-/HHGmtKllqjg1OLc023v9w9kK3laW7Z6TzfZukYQWCsGBbzB9p60zTvvpXFVcs44NZBVXL3viOa1HRKUbeee8g=="], + "p-locate/p-limit/yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], "pkg-dir/find-up/locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="], "pkg-up/find-up/locate-path": ["locate-path@3.0.0", "", { "dependencies": { "p-locate": "^3.0.0", "path-exists": "^3.0.0" } }, "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A=="], - "prebuild-install/node-abi/semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="], - - "public-ip/got/@sindresorhus/is": ["@sindresorhus/is@5.6.0", "", {}, "sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g=="], - - "public-ip/got/@szmarczak/http-timer": ["@szmarczak/http-timer@5.0.1", "", { "dependencies": { "defer-to-connect": "^2.0.1" } }, "sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw=="], - - "public-ip/got/cacheable-lookup": ["cacheable-lookup@7.0.0", "", {}, "sha512-+qJyx4xiKra8mZrcwhjMRMUhD5NR1R8esPkzIYxX96JiecFoxAXFuz/GpR3+ev4PE1WamHip78wV0vcmPQtp8w=="], - - "public-ip/got/cacheable-request": ["cacheable-request@10.2.14", "", { "dependencies": { "@types/http-cache-semantics": "^4.0.2", "get-stream": "^6.0.1", "http-cache-semantics": "^4.1.1", "keyv": "^4.5.3", "mimic-response": "^4.0.0", "normalize-url": "^8.0.0", "responselike": "^3.0.0" } }, "sha512-zkDT5WAF4hSSoUgyfg5tFIxz8XQK+25W/TLVojJTMKBaxevLBBtLxgqguAuVQB8PVW79FVjHcU+GJ9tVbDZ9mQ=="], - - "public-ip/got/form-data-encoder": ["form-data-encoder@2.1.4", "", {}, "sha512-yDYSgNMraqvnxiEXO4hi88+YZxaHC6QKzb5N84iRCTDeRO7ZALpir/lVmf/uXUhnwUr2O4HU8s/n6x+yNjQkHw=="], - - "public-ip/got/get-stream": ["get-stream@6.0.1", "", {}, "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg=="], - - "public-ip/got/http2-wrapper": ["http2-wrapper@2.2.1", "", { "dependencies": { "quick-lru": "^5.1.1", "resolve-alpn": "^1.2.0" } }, "sha512-V5nVw1PAOgfI3Lmeaj2Exmeg7fenjhRUgz1lPSezy1CuhPYbgQtbQj4jZfEAEMlaL+vupsvhjqCyjzob0yxsmQ=="], - - "public-ip/got/lowercase-keys": ["lowercase-keys@3.0.0", "", {}, "sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ=="], - - "public-ip/got/p-cancelable": ["p-cancelable@3.0.0", "", {}, "sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw=="], - - "public-ip/got/responselike": ["responselike@3.0.0", "", { "dependencies": { "lowercase-keys": "^3.0.0" } }, "sha512-40yHxbNcl2+rzXvZuVkrYohathsSJlMTXKryG5y8uciHv1+xDLHQpgjG64JUO9nrEq2jGLH6IZ8BcZyw3wrweg=="], - "readdir-glob/minimatch/brace-expansion": ["brace-expansion@2.1.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA=="], - "restore-cursor/onetime/mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="], - "rimraf/glob/minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="], - "socket.io/accepts/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], - - "socket.io/accepts/negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="], + "send/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], "storybook/open/wsl-utils": ["wsl-utils@0.1.0", "", { "dependencies": { "is-wsl": "^3.1.0" } }, "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw=="], "string-width-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - "tar-fs/tar-stream/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], - "tw-to-css/tailwindcss/chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="], "tw-to-css/tailwindcss/glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], @@ -8186,6 +7186,8 @@ "tw-to-css/tailwindcss/postcss": ["postcss@8.5.15", "", { "dependencies": { "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A=="], + "type-is/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], + "unplugin/chokidar/readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="], "unzipper/fs-extra/jsonfile": ["jsonfile@6.2.1", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q=="], @@ -8330,10 +7332,6 @@ "@electron/universal/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], - "@inquirer/core/wrap-ansi/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], - - "@inquirer/core/wrap-ansi/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - "@jsx-email/cli/tailwindcss/chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], "@jsx-email/cli/tailwindcss/chokidar/readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="], @@ -8382,63 +7380,9 @@ "@jsx-email/cli/vite/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.18.20", "", { "os": "win32", "cpu": "x64" }, "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ=="], - "@mintlify/cli/yargs/cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + "@modelcontextprotocol/sdk/express/type-is/content-type": ["content-type@2.0.0", "", {}, "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ=="], - "@mintlify/cli/yargs/cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], - - "@mintlify/cli/yargs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], - - "@mintlify/cli/yargs/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - - "@mintlify/common/remark-gfm/mdast-util-gfm/mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.3", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q=="], - - "@mintlify/common/sucrase/glob/minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="], - - "@mintlify/common/tailwindcss/chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], - - "@mintlify/common/tailwindcss/chokidar/readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="], - - "@mintlify/common/tailwindcss/sucrase/commander": ["commander@4.1.1", "", {}, "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA=="], - - "@mintlify/previewing/chokidar/readdirp/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], - - "@mintlify/previewing/express/accepts/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], - - "@mintlify/previewing/express/accepts/negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="], - - "@mintlify/previewing/express/body-parser/iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="], - - "@mintlify/previewing/express/body-parser/qs": ["qs@6.15.2", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw=="], - - "@mintlify/previewing/express/body-parser/raw-body": ["raw-body@2.5.3", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.4.24", "unpipe": "~1.0.0" } }, "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA=="], - - "@mintlify/previewing/express/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], - - "@mintlify/previewing/express/send/mime": ["mime@1.6.0", "", { "bin": { "mime": "cli.js" } }, "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="], - - "@mintlify/previewing/express/type-is/media-typer": ["media-typer@0.3.0", "", {}, "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ=="], - - "@mintlify/previewing/express/type-is/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], - - "@mintlify/previewing/got/cacheable-request/mimic-response": ["mimic-response@4.0.0", "", {}, "sha512-e5ISH9xMYU0DzrT+jl8q2ze9D6eWBto+I8CNpe+VI+K2J/F/k3PdkdTdz4wvGVH4NTpo+NRYTVIuMQEMMcsLqg=="], - - "@mintlify/previewing/got/cacheable-request/normalize-url": ["normalize-url@8.1.1", "", {}, "sha512-JYc0DPlpGWB40kH5g07gGTrYuMqV653k3uBKY6uITPWds3M0ov3GaWGp9lbE3Bzngx8+XkfzgvASb9vk9JDFXQ=="], - - "@mintlify/previewing/yargs/cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - - "@mintlify/previewing/yargs/cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], - - "@mintlify/previewing/yargs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], - - "@mintlify/previewing/yargs/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - - "@mintlify/scraping/yargs/cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - - "@mintlify/scraping/yargs/cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], - - "@mintlify/scraping/yargs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], - - "@mintlify/scraping/yargs/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + "@modelcontextprotocol/sdk/express/type-is/media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="], "@octokit/auth-app/@octokit/request-error/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@27.0.0", "", {}, "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA=="], @@ -8464,48 +7408,30 @@ "@octokit/rest/@octokit/core/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@27.0.0", "", {}, "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA=="], + "@opencode-ai/core/@ai-sdk/openai/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + "@opencode-ai/desktop/@actions/artifact/@actions/http-client/undici": ["undici@5.29.0", "", { "dependencies": { "@fastify/busboy": "^2.0.0" } }, "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg=="], - "@puppeteer/browsers/yargs/cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - - "@puppeteer/browsers/yargs/cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], - - "@puppeteer/browsers/yargs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], - - "@puppeteer/browsers/yargs/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - "@sentry/bundler-plugin-core/glob/minimatch/brace-expansion": ["brace-expansion@2.1.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA=="], "@sentry/bundler-plugin-core/glob/path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], "@sentry/bundler-plugin-core/glob/path-scurry/minipass": ["minipass@7.1.3", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="], - "@slack/bolt/express/accepts/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], - - "@slack/bolt/express/accepts/negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="], - - "@slack/bolt/express/body-parser/iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="], - - "@slack/bolt/express/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], - - "@slack/bolt/express/send/mime": ["mime@1.6.0", "", { "bin": { "mime": "cli.js" } }, "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="], - - "@slack/bolt/express/type-is/media-typer": ["media-typer@0.3.0", "", {}, "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ=="], - - "@slack/bolt/express/type-is/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], - "@slack/web-api/form-data/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], "@solidjs/start/shiki/@shikijs/engine-javascript/oniguruma-to-es": ["oniguruma-to-es@2.3.0", "", { "dependencies": { "emoji-regex-xs": "^1.0.0", "regex": "^5.1.1", "regex-recursion": "^5.1.1" } }, "sha512-bwALDxriqfKGfUufKGGepCzu9x7nJQuoRoAFp4AnwehhC2crqrDIAP/uN2qdlsAvSMpeRC3+Yzhqc7hLmle5+g=="], - "@stoplight/spectral-core/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], - - "@vercel/cli-exec/execa/onetime/mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="], - "ai-gateway-provider/@ai-sdk/amazon-bedrock/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], "ai-gateway-provider/@ai-sdk/anthropic/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + "ai-gateway-provider/@ai-sdk/azure/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + + "ai-gateway-provider/@ai-sdk/deepseek/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + + "ai-gateway-provider/@ai-sdk/mistral/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + "ansi-align/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], "app-builder-lib/@electron/get/fs-extra/universalify": ["universalify@0.1.2", "", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="], @@ -8560,8 +7486,6 @@ "electron-builder/yargs/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - "engine.io/accepts/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], - "esbuild-plugin-copy/chokidar/readdirp/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], "filelist/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], @@ -8570,10 +7494,6 @@ "iconv-corefoundation/cli-truncate/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - "is-online/got/cacheable-request/mimic-response": ["mimic-response@4.0.0", "", {}, "sha512-e5ISH9xMYU0DzrT+jl8q2ze9D6eWBto+I8CNpe+VI+K2J/F/k3PdkdTdz4wvGVH4NTpo+NRYTVIuMQEMMcsLqg=="], - - "is-online/got/cacheable-request/normalize-url": ["normalize-url@8.1.1", "", {}, "sha512-JYc0DPlpGWB40kH5g07gGTrYuMqV653k3uBKY6uITPWds3M0ov3GaWGp9lbE3Bzngx8+XkfzgvASb9vk9JDFXQ=="], - "js-beautify/glob/jackspeak/@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="], "js-beautify/glob/minimatch/brace-expansion": ["brace-expansion@2.1.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA=="], @@ -8582,22 +7502,18 @@ "opencode/@ai-sdk/cerebras/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + "opencode/@ai-sdk/openai/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + "pkg-dir/find-up/locate-path/p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="], "pkg-up/find-up/locate-path/p-locate": ["p-locate@3.0.0", "", { "dependencies": { "p-limit": "^2.0.0" } }, "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ=="], "pkg-up/find-up/locate-path/path-exists": ["path-exists@3.0.0", "", {}, "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ=="], - "public-ip/got/cacheable-request/mimic-response": ["mimic-response@4.0.0", "", {}, "sha512-e5ISH9xMYU0DzrT+jl8q2ze9D6eWBto+I8CNpe+VI+K2J/F/k3PdkdTdz4wvGVH4NTpo+NRYTVIuMQEMMcsLqg=="], - - "public-ip/got/cacheable-request/normalize-url": ["normalize-url@8.1.1", "", {}, "sha512-JYc0DPlpGWB40kH5g07gGTrYuMqV653k3uBKY6uITPWds3M0ov3GaWGp9lbE3Bzngx8+XkfzgvASb9vk9JDFXQ=="], - "readdir-glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], "rimraf/glob/minimatch/brace-expansion": ["brace-expansion@1.1.15", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg=="], - "socket.io/accepts/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], - "tw-to-css/tailwindcss/chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], "tw-to-css/tailwindcss/chokidar/readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="], @@ -8638,36 +7554,8 @@ "@jsx-email/cli/tailwindcss/chokidar/readdirp/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], - "@mintlify/cli/yargs/cliui/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - - "@mintlify/cli/yargs/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - - "@mintlify/common/sucrase/glob/minimatch/brace-expansion": ["brace-expansion@1.1.15", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg=="], - - "@mintlify/common/tailwindcss/chokidar/readdirp/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], - - "@mintlify/previewing/express/accepts/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], - - "@mintlify/previewing/express/type-is/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], - - "@mintlify/previewing/yargs/cliui/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - - "@mintlify/previewing/yargs/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - - "@mintlify/scraping/yargs/cliui/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - - "@mintlify/scraping/yargs/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - - "@puppeteer/browsers/yargs/cliui/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - - "@puppeteer/browsers/yargs/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - "@sentry/bundler-plugin-core/glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], - "@slack/bolt/express/accepts/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], - - "@slack/bolt/express/type-is/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], - "@solidjs/start/shiki/@shikijs/engine-javascript/oniguruma-to-es/regex": ["regex@5.1.1", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-dN5I359AVGPnwzJm2jN1k0W9LPZ+ePvoOeVMMfqIMFz53sSwXkxaJoxr50ptnsC771lK95BnTrVSZxq0b9yCGw=="], "@solidjs/start/shiki/@shikijs/engine-javascript/oniguruma-to-es/regex-recursion": ["regex-recursion@5.1.1", "", { "dependencies": { "regex": "^5.1.1", "regex-utilities": "^2.3.0" } }, "sha512-ae7SBCbzVNrIjgSbh7wMznPcQel1DNlDtzensnFxpiNpXt1U2ju/bHugH422r+4LAVS1FpW1YCwilmnNsjum9w=="], @@ -8702,8 +7590,6 @@ "tw-to-css/tailwindcss/chokidar/readdirp/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], - "@mintlify/common/sucrase/glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], - "archiver-utils/glob/jackspeak/@isaacs/cliui/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], "archiver-utils/glob/jackspeak/@isaacs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], diff --git a/github/index.ts b/github/index.ts index e8acd9a9ce..4e1af9cf55 100644 --- a/github/index.ts +++ b/github/index.ts @@ -495,6 +495,7 @@ async function subscribeSessionEvents() { console.log("Subscribing to session events...") const TOOL: Record = { + todowrite: ["Todo", "\x1b[33m\x1b[1m"], bash: ["Bash", "\x1b[31m\x1b[1m"], edit: ["Edit", "\x1b[32m\x1b[1m"], glob: ["Glob", "\x1b[34m\x1b[1m"], diff --git a/infra/console.ts b/infra/console.ts index 5fbf35de00..79556f5e0c 100644 --- a/infra/console.ts +++ b/infra/console.ts @@ -288,8 +288,12 @@ new sst.cloudflare.x.SolidStart("Console", { server: { placement: { region: "aws:us-east-2" }, transform: { - worker: { - tailConsumers: [{ service: logProcessor.nodes.worker.scriptName }], + worker: (args) => { + args.compatibilityFlags = $resolve(args.compatibilityFlags).apply((flags) => [ + ...(flags ?? []), + "global_fetch_strictly_public", + ]) + args.tailConsumers = [{ service: logProcessor.nodes.worker.scriptName }] }, }, }, diff --git a/infra/stage.ts b/infra/stage.ts index 8d80eefed8..f0db797448 100644 --- a/infra/stage.ts +++ b/infra/stage.ts @@ -8,6 +8,25 @@ export const zoneID = "430ba34c138cfb5360826c4909f99be8" export const awsStage = $app.stage === "production" ? "production" : "dev" export const deployAws = $app.stage === awsStage +if ($app.stage === "production") { + new cloudflare.DnsRecord("TrustCenter", { + zoneId: zoneID, + name: "trust.opencode.ai", + type: "CNAME", + content: "3a69a5bb27875189.vercel-dns-016.com", + proxied: false, + ttl: 60, + }) + + new cloudflare.DnsRecord("TrustCenterVerification", { + zoneId: zoneID, + name: "opencode.ai", + type: "TXT", + content: "compai-domain-verification=org_6993a99c6200a2d642bb115d", + ttl: 60, + }) +} + new cloudflare.RegionalHostname("RegionalHostname", { hostname: domain, regionKey: "us", diff --git a/nix/desktop.nix b/nix/desktop.nix index d0d7fa7eca..2df62f7a1c 100644 --- a/nix/desktop.nix +++ b/nix/desktop.nix @@ -8,6 +8,8 @@ makeWrapper, writableTmpDirAsHomeHook, autoPatchelfHook, + copyDesktopItems, + makeDesktopItem, opencode, }: let @@ -27,9 +29,12 @@ stdenv.mkDerivation (finalAttrs: { nodejs makeWrapper writableTmpDirAsHomeHook - ] ++ lib.optionals stdenv.hostPlatform.isLinux [ + ] + ++ lib.optionals stdenv.hostPlatform.isLinux [ autoPatchelfHook - ] ++ lib.optionals stdenv.hostPlatform.isDarwin [ + copyDesktopItems + ] + ++ lib.optionals stdenv.hostPlatform.isDarwin [ # Ad-hoc sign the .app: --config.mac.identity=null below skips signing. darwin.autoSignDarwinBinariesHook ]; @@ -38,20 +43,37 @@ stdenv.mkDerivation (finalAttrs: { (lib.getLib stdenv.cc.cc) ]; + desktopItems = lib.optional stdenv.hostPlatform.isLinux (makeDesktopItem { + name = "ai.opencode.desktop"; + desktopName = "OpenCode"; + exec = "opencode-desktop %U"; + icon = "ai.opencode.desktop"; + # Electron 41 derives X11 WM_CLASS from app.name. + startupWMClass = "OpenCode"; + categories = [ "Development" ]; + }); + env = opencode.env // { ELECTRON_SKIP_BINARY_DOWNLOAD = "1"; }; - # https://github.com/electron/electron/issues/31121 - # mac builds use a .app bundle which doesnt have this issue - postPatch = lib.optionalString stdenv.isLinux '' - BASE_PATH=packages/desktop - FILES=(src/main/windows.ts) - for file in "''${FILES[@]}"; do - substituteInPlace $BASE_PATH/$file \ - --replace-fail "process.resourcesPath" "'$out/opt/opencode-desktop/resources'" - done - ''; + postPatch = + # NOTE: Relax Bun version check to be a warning instead of an error + '' + substituteInPlace packages/script/src/index.ts \ + --replace-fail 'throw new Error(`This script requires bun@''${expectedBunVersionRange}' \ + 'console.warn(`Warning: This script requires bun@''${expectedBunVersionRange}' + '' + # https://github.com/electron/electron/issues/31121 + # mac builds use a .app bundle which doesnt have this issue + + lib.optionalString stdenv.isLinux '' + BASE_PATH=packages/desktop + FILES=(src/main/windows.ts) + for file in "''${FILES[@]}"; do + substituteInPlace $BASE_PATH/$file \ + --replace-fail "process.resourcesPath" "'$out/opt/opencode-desktop/resources'" + done + ''; preBuild = '' cp -r "${electron.dist}" $HOME/.electron-dist @@ -76,27 +98,38 @@ stdenv.mkDerivation (finalAttrs: { runHook postBuild ''; - installPhase = - '' - runHook preInstall - '' - + lib.optionalString stdenv.hostPlatform.isDarwin '' - mkdir -p $out/Applications - mv dist/mac*/*.app $out/Applications - makeWrapper "$out/Applications/OpenCode.app/Contents/MacOS/OpenCode" $out/bin/opencode-desktop - '' - + lib.optionalString stdenv.hostPlatform.isLinux '' - mkdir -p $out/opt/opencode-desktop - cp -r dist/linux*-unpacked/{resources,LICENSE*} $out/opt/opencode-desktop - makeWrapper ${lib.getExe electron} $out/bin/opencode-desktop \ - --inherit-argv0 \ - --set ELECTRON_FORCE_IS_PACKAGED 1 \ - --add-flags $out/opt/opencode-desktop/resources/app.asar \ - --add-flags "\''${NIXOS_OZONE_WL:+\''${WAYLAND_DISPLAY:+--ozone-platform-hint=auto --enable-features=WaylandWindowDecorations --enable-wayland-ime=true}}" - '' - + '' - runHook postInstall - ''; + installPhase = '' + runHook preInstall + '' + + lib.optionalString stdenv.hostPlatform.isDarwin '' + mkdir -p $out/Applications + mv dist/mac*/*.app $out/Applications + makeWrapper "$out/Applications/OpenCode.app/Contents/MacOS/OpenCode" $out/bin/opencode-desktop + '' + + lib.optionalString stdenv.hostPlatform.isLinux '' + mkdir -p $out/opt/opencode-desktop + cp -r dist/linux*-unpacked/{resources,LICENSE*} $out/opt/opencode-desktop + install -Dm644 resources/icons/32x32.png \ + "$out/share/icons/hicolor/32x32/apps/ai.opencode.desktop.png" + install -Dm644 resources/icons/64x64.png \ + "$out/share/icons/hicolor/64x64/apps/ai.opencode.desktop.png" + install -Dm644 resources/icons/128x128.png \ + "$out/share/icons/hicolor/128x128/apps/ai.opencode.desktop.png" + install -Dm644 resources/icons/128x128@2x.png \ + "$out/share/icons/hicolor/256x256/apps/ai.opencode.desktop.png" + install -Dm644 resources/icons/icon.png \ + "$out/share/icons/hicolor/512x512/apps/ai.opencode.desktop.png" + install -Dm644 resources/ai.opencode.desktop.metainfo.xml \ + "$out/share/metainfo/ai.opencode.desktop.metainfo.xml" + makeWrapper ${lib.getExe electron} $out/bin/opencode-desktop \ + --inherit-argv0 \ + --set ELECTRON_FORCE_IS_PACKAGED 1 \ + --add-flags $out/opt/opencode-desktop/resources/app.asar \ + --add-flags "\''${NIXOS_OZONE_WL:+\''${WAYLAND_DISPLAY:+--ozone-platform-hint=auto --enable-features=WaylandWindowDecorations --enable-wayland-ime=true}}" + '' + + '' + runHook postInstall + ''; autoPatchelfIgnoreMissingDeps = [ "libc.musl-x86_64.so.1" diff --git a/nix/hashes.json b/nix/hashes.json index 50208fdbbe..25f0e76812 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -1,8 +1,8 @@ { "nodeModules": { - "x86_64-linux": "sha256-JTtn+wXTXg+yklvIMDLcGFaYhTU6ZrCgKT9JTNEQ3gA=", - "aarch64-linux": "sha256-gXU6zyhvAZrZirkL/PlHdkHtEof/7PVSPCaE34Jnd4U=", - "aarch64-darwin": "sha256-Q0oTG3uzOlD/X2kJingLle529lKFoTpyCW2rHXOZ6iE=", - "x86_64-darwin": "sha256-LINvKHxPibTlJeNzfACQx0x+Yj5oROT6Du3I5AtqqXk=" + "x86_64-linux": "sha256-a7NyYa9vRUEqDfZNDPXXmFO58RDEgioyuGSl5CPBvxo=", + "aarch64-linux": "sha256-l4OJtSEllHvRhktjcaJYwkXBSaJvsIoyoLusbZfYMcM=", + "aarch64-darwin": "sha256-IIl0BQGs1/HLFh0auiQjiwfSQ2nfHcK2G2BAphYW59c=", + "x86_64-darwin": "sha256-vVeuPyd4ZIRYrHouphTuEb4rkRZLKKTAHe840jNh9rU=" } } diff --git a/package.json b/package.json index b6329f9c96..2a8eef6356 100644 --- a/package.json +++ b/package.json @@ -2,23 +2,18 @@ "$schema": "https://json.schemastore.org/package.json", "name": "opencode", "description": "AI-powered development tool", - "version": "0.0.0", "private": true, "type": "module", "packageManager": "bun@1.3.14", "scripts": { - "dev": "bun run --cwd packages/cli --conditions=browser src/index.ts", + "dev": "bun run --cwd packages/opencode --conditions=browser src/index.ts", "dev:desktop": "bun --cwd packages/desktop dev", "dev:web": "bun --cwd packages/app dev", "dev:console": "ulimit -n 10240 2>/dev/null; bun run --cwd packages/console/app dev", "dev:stats": "bun sst shell --stage=production -- bun run --cwd packages/stats/app dev", "dev:storybook": "bun --cwd packages/storybook storybook", "lint": "oxlint", - "lint:effect-patterns": "ast-grep scan -c script/ast-grep/sgconfig.yml packages/core/src packages/server/src packages/protocol/src packages/cli/src", - "test:lint-rules": "ast-grep test -c script/ast-grep/sgconfig.yml", - "typecheck": "bun turbo typecheck --concurrency=3", - "typecheck:profile": "bun script/profile-typecheck.ts", - "typecheck:profile:packages": "bun script/profile-typecheck-packages.ts", + "typecheck": "bun turbo typecheck", "upgrade-opentui": "bun run script/upgrade-opentui.ts", "postinstall": "bun run --cwd packages/core fix-node-pty", "prepare": "husky", @@ -45,10 +40,10 @@ "@octokit/rest": "22.0.0", "@hono/standard-validator": "0.2.0", "@hono/zod-validator": "0.4.2", - "@opentui/core": "0.4.3", - "@opentui/keymap": "0.4.3", - "@opentui/solid": "0.4.3", - "@tanstack/solid-virtual": "3.13.28", + "@opentui/core": "0.4.5", + "@opentui/keymap": "0.4.5", + "@opentui/solid": "0.4.5", + "@tanstack/solid-virtual": "3.13.32", "@shikijs/stream": "4.2.0", "ulid": "3.0.1", "@kobalte/core": "0.13.11", @@ -75,7 +70,7 @@ "hono-openapi": "1.1.2", "fuzzysort": "3.1.0", "luxon": "3.6.1", - "marked": "17.0.1", + "marked": "17.0.6", "marked-shiki": "1.2.1", "remend": "1.3.0", "@playwright/test": "1.59.1", @@ -95,13 +90,13 @@ "@sentry/solid": "10.36.0", "@sentry/vite-plugin": "4.6.0", "solid-js": "1.9.10", + "solid-sonner": "0.3.1", "vite-plugin-solid": "2.11.10", "@lydell/node-pty": "1.2.0-beta.12" } }, "devDependencies": { "@actions/artifact": "5.0.1", - "@ast-grep/cli": "0.44.0", "@tsconfig/bun": "catalog:", "@types/mime-types": "3.0.1", "@typescript/native-preview": "catalog:", @@ -149,19 +144,20 @@ "@types/node": "catalog:" }, "patchedDependencies": { + "@dnd-kit/dom@0.5.0": "patches/@dnd-kit%2Fdom@0.5.0.patch", "@ff-labs/fff-bun@0.9.3": "patches/@ff-labs%2Ffff-bun@0.9.3.patch", "@npmcli/agent@4.0.2": "patches/@npmcli%2Fagent@4.0.2.patch", "@silvia-odwyer/photon-node@0.3.4": "patches/@silvia-odwyer%2Fphoton-node@0.3.4.patch", "@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch", "solid-js@1.9.10": "patches/solid-js@1.9.10.patch", "@ai-sdk/xai@3.0.102": "patches/@ai-sdk%2Fxai@3.0.102.patch", + "@ai-sdk/mistral@3.0.51": "patches/@ai-sdk%2Fmistral@3.0.51.patch", "gcp-metadata@8.1.2": "patches/gcp-metadata@8.1.2.patch", "pacote@21.5.0": "patches/pacote@21.5.0.patch", "@ai-sdk/google@3.0.73": "patches/@ai-sdk%2Fgoogle@3.0.73.patch", - "@tanstack/solid-virtual@3.13.28": "patches/@tanstack%2Fsolid-virtual@3.13.28.patch", "@pierre/trees@1.0.0-beta.4": "patches/@pierre%2Ftrees@1.0.0-beta.4.patch", "@modelcontextprotocol/sdk@1.29.0": "patches/@modelcontextprotocol%2Fsdk@1.29.0.patch", - "@tanstack/virtual-core@3.17.0": "patches/@tanstack%2Fvirtual-core@3.17.0.patch", - "effect@4.0.0-beta.83": "patches/effect@4.0.0-beta.83.patch" + "effect@4.0.0-beta.83": "patches/effect@4.0.0-beta.83.patch", + "@tanstack/virtual-core@3.17.3": "patches/@tanstack%2Fvirtual-core@3.17.3.patch" } } diff --git a/packages/app/V1_API_MIGRATION.md b/packages/app/V1_API_MIGRATION.md new file mode 100644 index 0000000000..2850f10740 --- /dev/null +++ b/packages/app/V1_API_MIGRATION.md @@ -0,0 +1,220 @@ +# V1 API Migration Checklist + +The app is currently hybrid. In this document, V1 refers to the legacy unprefixed server APIs used by `@opencode-ai/sdk/v2`, despite the SDK package name. + +## Events + +- [x] Replace `GET /global/event` with `GET /api/event`. + - `src/context/server-sdk.tsx` +- [x] Reduce current granular session and message events into the existing app projections. + - `src/context/server-session-v2-reducer.ts` + - `src/context/server-session.ts` +- [ ] Remove transitional session event dependencies: `session.created`, `session.updated`, `session.diff`, `session.status`, `session.idle`, and `session.error`. + - `src/context/global-sync/event-reducer.ts` + - `src/context/server-session.ts` + - `src/context/notification.tsx` + - `src/pages/session/usage-exceeded-dialogs.tsx` +- [ ] Remove legacy message event compatibility: `message.updated`, `message.removed`, `message.part.updated`, `message.part.removed`, and `message.part.delta`. + - `src/context/global-sync/event-reducer.ts` + - `src/context/server-session.ts` +- [x] Adapt current permission and question events to the existing request model. + - `src/context/global-sync/event-reducer.ts` + - `src/context/permission.tsx` +- [x] Consume current file watcher events. + - `src/context/file.tsx` +- [x] Consume current VCS events. + - `src/context/global-sync/event-reducer.ts` + - `src/pages/session.tsx` +- [x] Consume current `pty.exited` events. + - `src/context/terminal.tsx` +- [ ] Migrate LSP and reference events. + - `src/context/global-sync/event-reducer.ts` + +## Sessions + +- [x] Replace `GET /session/status` with one server-scoped `GET /api/session/active` snapshot plus V2 execution events. + - `src/context/server-sync.tsx` +- [x] Migrate session listing from `GET /session`. + - `src/context/server-sync.tsx` + - `src/context/directory-sync.ts` + - `src/pages/layout.tsx` +- [x] Migrate the remaining direct session read from `GET /session/:sessionID`. + - `src/components/titlebar.tsx` +- [x] Migrate session updates from `PATCH /session/:sessionID`. + - `src/context/directory-sync.ts` + - `src/context/layout.tsx` + - `src/pages/home.tsx` + - `src/pages/layout.tsx` + - `src/pages/session/timeline/message-timeline.tsx` + - `src/components/titlebar-tab-nav.tsx` + - Renames use `POST /api/session/:sessionID/rename`; archival uses `POST /api/session/:sessionID/archive`. +- [x] Migrate session deletion from `DELETE /session/:sessionID`. + - `src/pages/session/timeline/message-timeline.tsx` +- [x] Remove session diff loading from `GET /session/:sessionID/diff`. + - Historical Session diffs remain unavailable until the current API defines their snapshot semantics. +- [x] Migrate abort from `POST /session/:sessionID/abort`. + - `src/components/prompt-input/submit.ts` + - `src/pages/session/use-session-commands.tsx` + - `src/pages/session.tsx` +- [x] Migrate revert and unrevert from `POST /session/:sessionID/revert` and `POST /session/:sessionID/unrevert`. + - `src/pages/session/use-session-commands.tsx` + - `src/pages/session.tsx` +- [x] Replace `POST /session/:sessionID/summarize` with the current compact API. + - `src/pages/session/use-session-commands.tsx` +- [x] Migrate slash commands from `POST /session/:sessionID/command`. + - `src/components/prompt-input/submit.ts` +- [x] Migrate shell execution from `POST /session/:sessionID/shell`. + - `src/components/prompt-input/submit.ts` +- [x] Migrate session fork from `POST /session/:sessionID/fork`. + - `src/components/dialog-fork.tsx` +- [ ] Migrate sharing from `POST /session/:sessionID/share` and `DELETE /session/:sessionID/share`. + - `src/pages/session/use-session-commands.tsx` + - `src/pages/session/timeline/message-timeline.tsx` + - Blocked: the current API has no sharing contract or implementation. + +## Session Compatibility Fallbacks + +These calls are retained as fallback adapters. The current production path supplies the current session and message APIs. + +- [ ] Remove fallback `GET /session/:sessionID` after compatibility support is unnecessary. + - `src/context/server-session.ts` +- [ ] Remove fallback `GET /session/:sessionID/message` after compatibility support is unnecessary. + - `src/context/server-session.ts` +- [ ] Remove fallback `GET /session/:sessionID/message/:messageID` after compatibility support is unnecessary. + - `src/context/server-session.ts` + +## Filesystem + +- [ ] Migrate file listing from `GET /file`. + - `src/context/file.tsx` +- [ ] Migrate file reads from `GET /file/content`. + - `src/context/file.tsx` + - `src/pages/session/review-tab.tsx` + - `src/pages/session/v2/review-panel-v2.tsx` +- [x] Migrate path discovery from `GET /path` to `GET /api/path`. + - `src/context/global-sync/bootstrap.ts` + - `src/components/dialog-select-directory.tsx` + - `src/components/dialog-select-directory-v2.tsx` + +## Projects And Worktrees + +- [x] Migrate project listing from `GET /project` to `GET /api/project`. + - `src/context/global-sync/bootstrap.ts` +- [x] Migrate the current project lookup from `GET /project/current` to `GET /api/project/current`. + - `src/context/global-sync/bootstrap.ts` +- [ ] Migrate Git initialization from `POST /project/git/init`. + - `src/pages/session.tsx` +- [x] Migrate project updates from `PATCH /project/:projectID` to `PATCH /api/project/:projectID`. + - `src/context/layout.tsx` + - `src/components/edit-project.ts` + - `src/pages/layout.tsx` +- [ ] Migrate experimental worktree listing, creation, removal, and reset from `/experimental/worktree`. + - `src/pages/layout.tsx` + - `src/components/prompt-input/submit.ts` + - Listing now uses `GET /api/project/:projectID/directories`; create, removal, and reset remain. +- [ ] Migrate instance disposal from `POST /instance/dispose`. + - `src/pages/layout.tsx` + +## VCS + +- [x] Migrate repository information from `GET /vcs` to `GET /api/vcs`. + - `src/context/global-sync/bootstrap.ts` +- [x] Migrate diffs from `GET /vcs/diff` to `GET /api/vcs/diff`. + - `src/pages/session.tsx` +- [x] Migrate status from `GET /vcs/status` to `GET /api/vcs/status`. + - `src/pages/layout.tsx` + +## Configuration And Authentication + +- [ ] Migrate global configuration reads from `GET /global/config`. + - `src/context/global-sync/bootstrap.ts` +- [ ] Migrate directory configuration reads from `GET /config`. + - `src/context/global-sync/bootstrap.ts` +- [ ] Migrate global configuration updates from `PATCH /global/config`. + - `src/context/server-sync.tsx` +- [x] Migrate provider authentication method discovery from `GET /provider/auth` to `GET /api/integration/:integrationID`. + - `src/components/dialog-connect-provider.tsx` +- [x] Migrate built-in provider OAuth authorization and callbacks to `/api/integration/:integrationID/connect/oauth/*`. + - `src/components/dialog-connect-provider.tsx` +- [ ] Migrate remaining credentials from `PUT /auth/:providerID` and `DELETE /auth/:providerID`. + - Built-in provider key connections now use `POST /api/integration/:integrationID/connect/key`. + - `src/components/dialog-connect-provider.tsx` + - `src/components/dialog-custom-provider.tsx` + - `src/components/settings-providers.tsx` + - `src/components/settings-v2/providers.tsx` +- [ ] Migrate global disposal from `POST /global/dispose`. + - `src/components/dialog-connect-provider.tsx` + - `src/components/settings-providers.tsx` + - `src/components/settings-v2/providers.tsx` + +## Permissions And Questions + +- [x] Migrate permission listing from `GET /permission` to `GET /api/permission/request`. + - `src/context/global-sync/bootstrap.ts` + - `src/context/permission.tsx` +- [x] Migrate permission responses from `/session/:sessionID/permissions/:permissionID`. + - `src/context/permission.tsx` + - `src/pages/session/composer/session-composer-state.ts` +- [x] Migrate question listing from `GET /question` to `GET /api/question/request`. + - `src/context/global-sync/bootstrap.ts` +- [x] Migrate question replies and rejections from `/question/:requestID/*` to `/api/session/:sessionID/question/:requestID/*`. + - `src/pages/session/composer/session-question-dock.tsx` + +## Commands, MCP, LSP, And References + +- [x] Migrate command listing from `GET /command` to `GET /api/command`. + - `src/context/global-sync/bootstrap.ts` + - `src/context/server-sync.tsx` +- [x] Migrate MCP listing, connection, and disconnection from `/mcp` to `/api/mcp`. + - `src/context/server-sync.tsx` +- [ ] Replace legacy MCP authentication with the Integration OAuth workflow. + - `src/context/server-sync.tsx` +- [x] Migrate experimental resource listing from `GET /experimental/resource` to `GET /api/mcp/resource`. + - `src/context/server-sync.tsx` +- [ ] Migrate LSP status from `GET /lsp`. + - `src/context/server-sync.tsx` +- [x] Move `GET /api/reference` off the legacy generated SDK transport. + - `src/context/global-sync/bootstrap.ts` + +## Search + +- [x] Migrate global session search from `GET /experimental/session` to `GET /api/session`. + - `src/components/command-palette.ts` + - `src/components/dialog-command-palette-v2.tsx` + +## PTY And Terminal + +- [x] Migrate PTY creation, reads, updates, and deletion from `/pty` to `/api/pty`. + - `src/context/terminal.tsx` + - `src/components/terminal.tsx` +- [x] Migrate shell listing from `GET /pty/shells` to `GET /api/pty/shells`. + - `src/components/settings-general.tsx` + - `src/components/settings-v2/general.tsx` +- [x] Migrate connection tokens from `POST /pty/:ptyID/connect-token` to `POST /api/pty/:ptyID/connect-token`. + - `src/components/terminal.tsx` +- [x] Migrate the direct WebSocket connection from `/pty/:ptyID/connect` to `/api/pty/:ptyID/connect`. + - `src/components/terminal.tsx` + +## Legacy Types And Adapters + +These are not V1 network requests, but they keep the UI coupled to V1 data contracts. + +- [ ] Replace the current-session-to-legacy-session adapter. + - `src/utils/session.ts` +- [ ] Replace the current-message-to-legacy-message-and-part adapter. + - `src/utils/session-message.ts` +- [ ] Replace current agent, provider, and model adapters to legacy SDK structures. + - `src/context/global-sync/utils.ts` +- [ ] Replace legacy `Session`, `Message`, `Part`, `PermissionRequest`, `QuestionRequest`, `Project`, `FileNode`, `FileDiffInfo`, and `Event` types throughout app state and rendering. +- [ ] Remove the `@opencode-ai/sdk` runtime dependency after all legacy calls and types are gone. + - `package.json` + +## Test Infrastructure + +- [ ] Replace V1 endpoint mocks with current API mocks. + - `e2e/utils/mock-server.ts` +- [x] Replace `/global/event` and `/event` interception with current event transport handling. + - `e2e/utils/sse-transport.ts` +- [ ] Replace `SessionV1` and legacy SDK fixtures in timeline performance tests. + - `e2e/performance/timeline-stability/fixture.ts` +- [ ] Remove remaining legacy SDK type fixtures from unit and browser tests. diff --git a/packages/app/e2e/performance/timeline-stability/file-matrix.spec.ts b/packages/app/e2e/performance/timeline-stability/file-matrix.spec.ts index 0690f02706..e0f0d72233 100644 --- a/packages/app/e2e/performance/timeline-stability/file-matrix.spec.ts +++ b/packages/app/e2e/performance/timeline-stability/file-matrix.spec.ts @@ -20,7 +20,7 @@ const profiles = [ { name: "edit", tool: "edit", input: { filePath: "src/edit.ts" } }, { name: "multi patch", - tool: "patch", + tool: "apply_patch", input: { files: ["src/a.ts", "src/b.ts", "src/old.ts", "src/moved.ts"] }, }, ] as const diff --git a/packages/app/e2e/performance/timeline-stability/file-mutation.spec.ts b/packages/app/e2e/performance/timeline-stability/file-mutation.spec.ts index f411339adb..798bf0df3b 100644 --- a/packages/app/e2e/performance/timeline-stability/file-mutation.spec.ts +++ b/packages/app/e2e/performance/timeline-stability/file-mutation.spec.ts @@ -25,7 +25,7 @@ test("adds patch files incrementally without resetting outer expansion", async ( userMessage(), assistantMessage( [ - toolPart(patchID, "patch", "running", { files: [first.filePath] }, { metadata: { files: [first] } }), + toolPart(patchID, "apply_patch", "running", { files: [first.filePath] }, { metadata: { files: [first] } }), textPart(followingID, "Following incremental patch"), ], { completed: false }, @@ -49,7 +49,7 @@ test("adds patch files incrementally without resetting outer expansion", async ( partUpdated( toolPart( patchID, - "patch", + "apply_patch", "running", { files: [first.filePath, second.filePath] }, { metadata: { files: [first, second] } }, @@ -61,7 +61,7 @@ test("adds patch files incrementally without resetting outer expansion", async ( partUpdated( toolPart( patchID, - "patch", + "apply_patch", "completed", { files: [first.filePath, second.filePath, third.filePath] }, { metadata: { files: [first, second, third] } }, diff --git a/packages/app/e2e/performance/timeline-stability/fixture.ts b/packages/app/e2e/performance/timeline-stability/fixture.ts index e63445e87d..df67da5a66 100644 --- a/packages/app/e2e/performance/timeline-stability/fixture.ts +++ b/packages/app/e2e/performance/timeline-stability/fixture.ts @@ -97,6 +97,7 @@ export async function setupTimeline( locale?: string deviceScaleFactor?: number seedHistory?: boolean + protocol?: "v1" | "v2" } = {}, ) { const sessions = input.sessions ?? [session()] @@ -114,6 +115,7 @@ export async function setupTimeline( retry: input.eventRetry ?? 20, }) await mockOpenCodeServer(page, { + protocol: input.protocol, directory, project: project(), provider: provider(), @@ -136,6 +138,9 @@ export async function setupTimeline( }, }), ) + if (settings.newLayoutDesigns === false) { + localStorage.setItem("app-version.v1", JSON.stringify({ version: "1.17.20" })) + } }, input.settings ?? {}) if (input.locale) { await page.addInitScript((locale) => { diff --git a/packages/app/e2e/performance/timeline-stability/tools.spec.ts b/packages/app/e2e/performance/timeline-stability/tools.spec.ts index d26968e1cb..d28fdaa65f 100644 --- a/packages/app/e2e/performance/timeline-stability/tools.spec.ts +++ b/packages/app/e2e/performance/timeline-stability/tools.spec.ts @@ -33,9 +33,11 @@ test.describe("timeline tool state stability", () => { } const names = { webfetch: "webfetch", websearch: "websearch", task: "task", skill: "skill", custom: "mcp_probe" } const questionID = "prt_state_question" + const todoID = "prt_state_todo" const initial = [ ...ids.map((id) => toolPart(`prt_state_${id}`, names[id], "pending", inputs[id])), toolPart(questionID, "question", "pending", questionInput()), + toolPart(todoID, "todowrite", "pending", { todos: [{ content: "Hidden", status: "pending" }] }), textPart("prt_state_following", "Following lightweight tools"), ] const childID = "ses_timeline_child" @@ -47,6 +49,7 @@ test.describe("timeline tool state stability", () => { await timeline.send(status("busy"), 120) for (const id of ids) await timeline.waitForPart(`prt_state_${id}`) await expect(page.locator(`[data-timeline-part-id="${questionID}"]`)).toHaveCount(0) + await expect(page.locator(`[data-timeline-part-id="${todoID}"]`)).toHaveCount(0) const regionIDs = [ "prt_state_webfetch", @@ -102,6 +105,7 @@ test.describe("timeline tool state stability", () => { ]), ) await expect(page.locator(`[data-timeline-part-id="${questionID}"]`)).toContainText("Keep it stable") + await expect(page.locator(`[data-timeline-part-id="${todoID}"]`)).toHaveCount(0) await expect( page.locator(`a[href$="/session/${childID}"]`, { has: page.locator('[data-component="task-tool-card"]') }), ).toBeVisible() diff --git a/packages/app/e2e/performance/timeline/session-parent-hydration-benchmark.spec.ts b/packages/app/e2e/performance/timeline/session-parent-hydration-benchmark.spec.ts index 2a214831da..838af17c93 100644 --- a/packages/app/e2e/performance/timeline/session-parent-hydration-benchmark.spec.ts +++ b/packages/app/e2e/performance/timeline/session-parent-hydration-benchmark.spec.ts @@ -41,7 +41,12 @@ const assistants = Array.from({ length: 14 }, (_, index) => { const messages = [user, ...assistants] const target = fixture.sessions.find((session) => session.id === fixture.targetID)! const lastID = userID -const lastPartID = assistants.at(-1)!.parts.at(-1)!.id +const lastAssistant = assistants.at(-1)! +const lastPart = lastAssistant.parts.at(-1)! +const lastPartID = + lastPart.type === "tool" + ? lastPart.id + : `${lastAssistant.info.id}:${lastPart.type}:${lastAssistant.parts.filter((part) => part.type === lastPart.type).length - 1}` benchmark("hydrates an orphaned latest turn after a cold session click", async ({ browser, report }, testInfo) => { benchmark.setTimeout(180_000) @@ -107,9 +112,25 @@ async function trial(page: Page, mode: ParentHydrationBenchmarkMode) { return { items: items.slice(start, end), cursor: start > 0 ? items[start]!.info.id : undefined } }, }) - await page.route(`**/session/${fixture.targetID}`, (route) => - route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify(target) }), - ) + await page.route(`**/session/${fixture.targetID}`, (route) => { + const current = new URL(route.request().url()).pathname.startsWith("/api/") + return route.fulfill({ + status: 200, + contentType: "application/json", + body: JSON.stringify( + current + ? { + data: { + ...target, + cost: 0, + tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } }, + location: { directory: target.directory }, + }, + } + : target, + ), + }) + }) await installStressSessionTabs(page, { sessionIDs: [fixture.sourceID] }) await page.goto(stressSessionHref(fixture.sourceID)) await expectSessionTitle(page, fixture.expected.sourceTitle) @@ -144,8 +165,8 @@ async function trial(page: Page, mode: ParentHydrationBenchmarkMode) { parent: requests.filter((request) => request.type === "parent").length, } if (mode === "candidate") { - expect(requestCounts.parent).toBe(1) - expect(historyGates).toBe(1) + expect(requestCounts.parent).toBe(0) + expect(historyGates).toBe(0) } return { metrics, requestCounts, historyGateCount: historyGates } } diff --git a/packages/app/e2e/performance/timeline/session-timeline-benchmark.fixture.ts b/packages/app/e2e/performance/timeline/session-timeline-benchmark.fixture.ts index a22d5cc331..a86a55cff2 100644 --- a/packages/app/e2e/performance/timeline/session-timeline-benchmark.fixture.ts +++ b/packages/app/e2e/performance/timeline/session-timeline-benchmark.fixture.ts @@ -295,7 +295,7 @@ function performanceTurn(index: number) { messageID: assistantID, type: "tool", callID: `call_0000_${suffix}_patch`, - tool: "patch", + tool: "apply_patch", state: { status: "completed", input: { patchText: realisticPatch(index) }, diff --git a/packages/app/e2e/performance/timeline/session-timeline-stress.fixture.ts b/packages/app/e2e/performance/timeline/session-timeline-stress.fixture.ts index 2e20d98415..529081a1d9 100644 --- a/packages/app/e2e/performance/timeline/session-timeline-stress.fixture.ts +++ b/packages/app/e2e/performance/timeline/session-timeline-stress.fixture.ts @@ -131,7 +131,7 @@ function toolPart( ): MessagePart { const metadata = metadataOverride ?? - (tool === "patch" + (tool === "apply_patch" ? { files: [patchFile(index, "update"), patchFile(index + 1, index % 2 === 0 ? "add" : "delete")] } : tool === "edit" || tool === "write" ? { @@ -219,7 +219,7 @@ function turn(index: number): Message[] { ? [toolPart(index, 7, "write", { filePath: `src/generated/write-${index}.ts`, content: code(index, 28) }, 560)] : []), ...(index % 8 === 0 - ? [toolPart(index, 8, "patch", { files: [`src/generated/patch-${index}.ts`] }, 620)] + ? [toolPart(index, 8, "apply_patch", { files: [`src/generated/patch-${index}.ts`] }, 620)] : []), ...(index % 7 === 0 ? [toolPart(index, 4, "bash", { command: "bun typecheck", description: "Verify generated output" }, 620)] @@ -269,6 +269,7 @@ const childMessages = Array.from({ length: 4 }, (_, index) => [ ]).flat() function renderable(part: MessagePart) { + if (part.type === "tool" && part.tool === "todowrite") return false if (part.type === "text") return !!part.text.trim() if (part.type === "reasoning") return !!part.text.trim() return part.type !== "step-start" && part.type !== "step-finish" && part.type !== "patch" diff --git a/packages/app/e2e/regression/cross-server-tab-close.spec.ts b/packages/app/e2e/regression/cross-server-tab-close.spec.ts index 9fd7991437..f09a2c7b63 100644 --- a/packages/app/e2e/regression/cross-server-tab-close.spec.ts +++ b/packages/app/e2e/regression/cross-server-tab-close.spec.ts @@ -1,5 +1,6 @@ import { expect, test, type Page, type Route } from "@playwright/test" import { base64Encode } from "@opencode-ai/core/util/encode" +import { currentSession } from "../utils/mock-server" const serverA = "http://127.0.0.1:4096" const serverB = "http://127.0.0.1:4097" @@ -33,7 +34,7 @@ test("closing the active server's last tab opens the remaining server tab", asyn await tabA.locator('[data-slot="tab-close"] button').click() await expect(page).toHaveURL(new RegExp(`${hrefB.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}$`)) - await expect.poll(() => requests.some((url) => url.startsWith(`${serverB}/session/${sessionB.id}`))).toBe(true) + await expect.poll(() => requests.some((url) => url.startsWith(`${serverB}/api/session/${sessionB.id}`))).toBe(true) await expect(page.getByText(sessionB.title).first()).toBeVisible() const sessionBRequests = requests.filter((url) => url.includes(`/session/${sessionB.id}`)) expect(sessionBRequests.every((url) => url.startsWith(serverB))).toBe(true) @@ -84,17 +85,21 @@ async function mockServers(page: Page, requests: string[]) { const current = url.origin === serverA ? sessionA : sessionB const directory = url.searchParams.get("directory") if (directory && directory !== current.directory) return json(route, { name: "InvalidDirectory" }, 500) - if (url.pathname === "/global/event" || url.pathname === "/event") return sse(route) - if (url.pathname === "/global/health") return json(route, { healthy: true }) - if (url.pathname === "/session") return json(route, [current]) + if (url.pathname === "/global/event" || url.pathname === "/event" || url.pathname === "/api/event") + return sse(route) + if (url.pathname === "/global/health") return json(route, {}, 404) + if (url.pathname === "/api/health") return json(route, { pid: 1 }) + if (url.pathname === "/api/session") return json(route, { data: [currentSession(current)], cursor: {} }) + if (url.pathname === "/api/session/active") return json(route, { data: {} }) + if (url.pathname === `/api/session/${current.id}`) return json(route, { data: currentSession(current) }) + if (url.pathname === `/api/session/${current.id}/message`) return json(route, { data: [], cursor: {} }) if (url.pathname === `/session/${current.id}`) return json(route, current) if (/^\/session\/[^/]+$/.test(url.pathname)) return json(route, { name: "NotFoundError" }, 404) if (url.pathname === `/session/${current.id}/message`) return json(route, []) - if (/^\/session\/[^/]+\/(children|diff)$/.test(url.pathname)) return json(route, []) + if (/^\/session\/[^/]+\/(children|todo|diff)$/.test(url.pathname)) return json(route, []) if (["/skill", "/command", "/lsp", "/formatter", "/permission", "/question", "/vcs/diff"].includes(url.pathname)) return json(route, []) - if (["/global/config", "/config", "/provider/auth", "/mcp", "/session/status"].includes(url.pathname)) - return json(route, {}) + if (["/global/config", "/config", "/provider/auth", "/mcp"].includes(url.pathname)) return json(route, {}) if (url.pathname === "/provider") return json(route, { all: [], connected: [], default: { providerID: "", modelID: "" } }) if (url.pathname === "/agent") return json(route, [{ name: "build", mode: "primary" }]) @@ -116,7 +121,20 @@ async function mockServers(page: Page, requests: string[]) { directory: current.directory, home: current.directory, }) + if (url.pathname === "/api/path") + return json(route, { + state: current.directory, + config: current.directory, + worktree: current.directory, + directory: current.directory, + home: current.directory, + }) if (url.pathname === "/vcs") return json(route, { branch: "main", default_branch: "main" }) + if (url.pathname === "/api/vcs") + return json(route, { + location: { directory: current.directory }, + data: { branch: "main", defaultBranch: "main" }, + }) return json(route, {}) }) } diff --git a/packages/app/e2e/regression/file-browser-sidebar-tab-switch.spec.ts b/packages/app/e2e/regression/file-browser-sidebar-tab-switch.spec.ts new file mode 100644 index 0000000000..136232c211 --- /dev/null +++ b/packages/app/e2e/regression/file-browser-sidebar-tab-switch.spec.ts @@ -0,0 +1,150 @@ +import { base64Encode } from "@opencode-ai/core/util/encode" +import { expect, test, type Page } from "@playwright/test" +import { mockOpenCodeServer } from "../utils/mock-server" +import { expectSessionTitle } from "../utils/waits" + +const directory = "C:/OpenCode/FileBrowserSidebar" +const projectID = "proj_file_browser_sidebar" +const sessionID = "ses_file_browser_sidebar" +const title = "File browser sidebar" +const server = `http://${process.env.PLAYWRIGHT_SERVER_HOST ?? "127.0.0.1"}:${process.env.PLAYWRIGHT_SERVER_PORT ?? "4096"}` +const files = Array.from({ length: 80 }, (_, index) => `file-${String(index).padStart(2, "0")}.ts`) +// Marks the file-browser sidebar DOM node so a remount (fresh node) is detectable. +const PROBE = "original" + +test.use({ viewport: { width: 1440, height: 900 } }) + +// The file-browser sidebar must stay mounted across preview/pinned file-tab +// switches. Remounting resets scroll and filter state. +test("keeps the file-browser sidebar mounted when switching file tabs", async ({ page }) => { + await setup(page) + + await page.goto(`/server/${base64Encode(server)}/session/${sessionID}`) + await expectSessionTitle(page, title) + + const panel = page.locator("#review-panel") + await panel.getByRole("button", { name: "Open file" }).click() + await expect(panel.getByRole("tab", { name: "Open file" })).toHaveAttribute("data-selected", "") + + const sidebar = panel.locator('[data-component="session-review-v2-sidebar-root"]') + await expect(sidebar).toBeVisible() + await expect(panel.getByRole("button", { name: "file-00.ts" })).toBeVisible() + + await panel.getByRole("button", { name: "file-00.ts" }).click() + await expect(panel.getByRole("tab", { name: "file-00.ts" })).toHaveAttribute("data-selected", "") + await expect(panel.getByText("contents:file-00.ts", { exact: true })).toBeVisible() + + const viewport = panel.locator('[data-slot="session-review-v2-sidebar-tree"] .scroll-view__viewport') + await viewport.hover() + await page.mouse.wheel(0, 100_000) + await expect + .poll(() => viewport.evaluate((element) => element.scrollHeight - element.clientHeight - element.scrollTop)) + .toBeLessThanOrEqual(1) + const scrolled = await viewport.evaluate((element) => element.scrollTop) + expect(scrolled).toBeGreaterThan(0) + await writeProbe(page) + + await panel.getByRole("button", { name: "file-79.ts" }).click() + await expect(panel.getByRole("tab", { name: "file-79.ts" })).toHaveAttribute("data-selected", "") + await expect(panel.getByText("contents:file-79.ts", { exact: true })).toBeVisible() + expect(await readProbe(page)).toBe(PROBE) + await expect.poll(() => viewport.evaluate((element) => element.scrollTop)).toBe(scrolled) + + await panel.getByRole("button", { name: "file-78.ts" }).dblclick() + await expect(panel.getByRole("tab", { name: "file-78.ts" })).toHaveAttribute("data-selected", "") + await panel.getByRole("button", { name: "file-79.ts" }).click() + await expect(panel.getByRole("tab", { name: "file-79.ts" })).toHaveAttribute("data-selected", "") + await panel.getByRole("tab", { name: "file-78.ts" }).click() + await expect(panel.getByRole("tab", { name: "file-78.ts" })).toHaveAttribute("data-selected", "") + expect(await readProbe(page)).toBe(PROBE) + await expect.poll(() => viewport.evaluate((element) => element.scrollTop)).toBe(scrolled) +}) + +type Probed = HTMLElement & { __e2eProbe?: string } + +async function writeProbe(page: Page) { + await page.locator('#review-panel [data-component="session-review-v2-sidebar-root"]').evaluate((el, probe) => { + ;(el as Probed).__e2eProbe = probe + }, PROBE) +} + +async function readProbe(page: Page) { + return page + .locator('#review-panel [data-component="session-review-v2-sidebar-root"]') + .evaluate((el) => (el as Probed).__e2eProbe) +} + +async function setup(page: Page) { + await mockOpenCodeServer(page, { + directory, + project: { + id: projectID, + worktree: directory, + vcs: "git", + name: "file-browser-sidebar", + time: { created: 1700000000000, updated: 1700000000000 }, + sandboxes: [], + }, + provider: { + all: [ + { + id: "opencode", + name: "OpenCode", + models: { test: { id: "test", name: "Test", limit: { context: 200_000 } } }, + }, + ], + connected: ["opencode"], + default: { providerID: "opencode", modelID: "test" }, + }, + sessions: [ + { + id: sessionID, + slug: sessionID, + projectID, + directory, + title, + version: "dev", + time: { created: 1700000000000, updated: 1700000000000 }, + }, + ], + vcsDiff: [], + fileList: (path) => { + if (path) return [] + return files.map((name) => ({ + name, + path: name, + absolute: `${directory}/${name}`, + type: "file" as const, + ignored: false, + })) + }, + fileContent: (path) => ({ type: "text", content: `contents:${path}` }), + pageMessages: () => ({ items: [] }), + }) + + await page.addInitScript( + ({ directory, server, sessionID }) => { + localStorage.setItem("settings.v3", JSON.stringify({ general: { newLayoutDesigns: true } })) + localStorage.setItem( + "opencode.global.dat:server", + JSON.stringify({ + projects: { local: [{ worktree: directory, expanded: true }] }, + lastProject: { local: directory }, + }), + ) + localStorage.setItem( + "opencode.global.dat:layout", + JSON.stringify({ review: { diffStyle: "split", panelOpened: true } }), + ) + localStorage.setItem( + "opencode.global.dat:review-panel-v2", + JSON.stringify({ sidebarOpened: true, sidebarWidth: 240, expandMode: "collapse" }), + ) + localStorage.setItem( + "opencode.window.browser.dat:tabs", + JSON.stringify([{ type: "session", server, sessionId: sessionID }]), + ) + }, + { directory, server, sessionID }, + ) +} diff --git a/packages/app/e2e/regression/legacy-new-session.spec.ts b/packages/app/e2e/regression/legacy-new-session.spec.ts index 30233a5aae..45cd64adcc 100644 --- a/packages/app/e2e/regression/legacy-new-session.spec.ts +++ b/packages/app/e2e/regression/legacy-new-session.spec.ts @@ -24,6 +24,7 @@ test("redirects a draft to the legacy new-session route", async ({ page }) => { await page.addInitScript( ({ directory, draftID, server }) => { localStorage.setItem("settings.v3", JSON.stringify({ general: { newLayoutDesigns: false } })) + localStorage.setItem("app-version.v1", JSON.stringify({ version: "1.17.20" })) localStorage.setItem( "opencode.window.browser.dat:tabs", JSON.stringify([{ type: "draft", draftID, server, directory }]), diff --git a/packages/app/e2e/regression/open-file-expand-folder.spec.ts b/packages/app/e2e/regression/open-file-expand-folder.spec.ts new file mode 100644 index 0000000000..37739d2fbb --- /dev/null +++ b/packages/app/e2e/regression/open-file-expand-folder.spec.ts @@ -0,0 +1,132 @@ +import { base64Encode } from "@opencode-ai/core/util/encode" +import { expect, test } from "@playwright/test" +import { mockOpenCodeServer } from "../utils/mock-server" +import { expectSessionTitle } from "../utils/waits" + +const directory = "C:/OpenCode/OpenFileExpand" +const projectID = "proj_open_file_expand" +const sessionID = "ses_open_file_expand" +const title = "Open file expand" +const server = `http://${process.env.PLAYWRIGHT_SERVER_HOST ?? "127.0.0.1"}:${process.env.PLAYWRIGHT_SERVER_PORT ?? "4096"}` + +test.use({ viewport: { width: 1440, height: 900 } }) + +test("expands a folder whose path has a trailing Windows separator", async ({ page }) => { + await mockOpenCodeServer(page, { + directory, + project: { + id: projectID, + worktree: directory, + vcs: "git", + name: "open-file-expand", + time: { created: 1700000000000, updated: 1700000000000 }, + sandboxes: [], + }, + provider: { + all: [ + { + id: "opencode", + name: "OpenCode", + models: { test: { id: "test", name: "Test", limit: { context: 200_000 } } }, + }, + ], + connected: ["opencode"], + default: { providerID: "opencode", modelID: "test" }, + }, + sessions: [ + { + id: sessionID, + slug: sessionID, + projectID, + directory, + title, + version: "dev", + time: { created: 1700000000000, updated: 1700000000000 }, + }, + ], + vcsDiff: [], + fileList: (path) => { + if (path === "frontend\\" || path === "frontend") { + return [ + { + name: "app.ts", + path: "frontend\\app.ts", + absolute: `${directory}/frontend/app.ts`, + type: "file" as const, + ignored: false, + }, + ] + } + if (path) return [] + return [ + { + name: "frontend", + path: "frontend\\", + absolute: `${directory}/frontend`, + type: "directory" as const, + ignored: false, + }, + { + name: "README.md", + path: "README.md", + absolute: `${directory}/README.md`, + type: "file" as const, + ignored: false, + }, + ] + }, + fileContent: (path) => ({ type: "text", content: `contents:${path}` }), + pageMessages: () => ({ items: [] }), + }) + + await page.addInitScript( + ({ directory, server, sessionID }) => { + localStorage.setItem( + "settings.v3", + JSON.stringify({ general: { newLayoutDesigns: true, shouldDisplayTabsToast: false } }), + ) + localStorage.setItem( + "opencode.global.dat:server", + JSON.stringify({ + projects: { local: [{ worktree: directory, expanded: true }] }, + lastProject: { local: directory }, + }), + ) + localStorage.setItem( + "opencode.global.dat:layout", + JSON.stringify({ review: { diffStyle: "split", panelOpened: true } }), + ) + localStorage.setItem( + "opencode.global.dat:review-panel-v2", + JSON.stringify({ sidebarOpened: true, sidebarWidth: 240, expandMode: "collapse" }), + ) + localStorage.setItem( + "opencode.window.browser.dat:tabs", + JSON.stringify([{ type: "session", server, sessionId: sessionID }]), + ) + }, + { directory, server, sessionID }, + ) + + await page.goto(`/server/${base64Encode(server)}/session/${sessionID}`) + await expectSessionTitle(page, title) + + const panel = page.locator("#review-panel") + await panel.getByRole("button", { name: "Open file" }).click() + await expect(panel.getByRole("tab", { name: "Open file" })).toHaveAttribute("data-selected", "") + + const sidebar = panel.locator('[data-component="session-review-v2-sidebar-root"]') + await expect(sidebar).toBeVisible() + + const frontendRow = panel.locator('[data-slot="file-tree-v2-row"][data-path="frontend"]') + await expect(frontendRow).toBeVisible() + await expect(frontendRow).toHaveAttribute("aria-expanded", "false") + await frontendRow.click() + await expect(frontendRow).toHaveAttribute("aria-expanded", "true") + + const appRow = panel.locator('[data-slot="file-tree-v2-row"][data-path="frontend/app.ts"]') + await expect(appRow).toBeVisible() + await appRow.click() + await expect(panel.getByRole("tab", { name: "app.ts" })).toHaveAttribute("data-selected", "") + await expect(panel.getByText("contents:frontend/app.ts", { exact: true })).toBeVisible() +}) diff --git a/packages/app/e2e/regression/project-picker-recent-search.spec.ts b/packages/app/e2e/regression/project-picker-recent-search.spec.ts new file mode 100644 index 0000000000..2cdb0b4a03 --- /dev/null +++ b/packages/app/e2e/regression/project-picker-recent-search.spec.ts @@ -0,0 +1,60 @@ +import { expect, test } from "@playwright/test" +import type { Page } from "@playwright/test" +import { fixture, pageMessages } from "../smoke/session-timeline.fixture" +import { mockOpenCodeServer } from "../utils/mock-server" +import { expectAppVisible } from "../utils/waits" + +const NAMES = ["alpha-service", "bravo-web", "charlie-api", "delta-tools", "echo-infra", "foxtrot-docs"] +const worktrees = NAMES.map((name) => `/opencode-demo/${name}`) + +// The sixth project sits outside the five-item recent cap, so it is only reachable if the +// dialog hands every recent project to the list filter instead of a pre-truncated slice. +const OUTSIDE_CAP = "foxtrot-docs" + +// Dialog rows carry data-directory-path; the sidebar project list does not, so this +// scopes assertions to the picker instead of matching the sidebar entry of the same name. +const rows = (page: Page) => page.locator("[data-directory-path]") +const row = (page: Page, name: string) => page.locator(`[data-directory-path*="${name}"]`) + +async function openProjectDialog(page: Page) { + await mockOpenCodeServer(page, { + sessions: fixture.sessions, + provider: fixture.provider, + directory: fixture.directory, + project: fixture.project, + pageMessages, + fileList: () => [], + findFiles: () => [], + }) + await page.addInitScript((dirs) => { + localStorage.setItem( + "opencode.global.dat:server", + JSON.stringify({ + projects: { local: dirs.map((worktree: string) => ({ worktree, expanded: false })) }, + lastProject: {}, + }), + ) + }, worktrees) + await page.goto("/") + const add = page.getByRole("button", { name: "Add project" }).first() + await expectAppVisible(add) + await add.click() + await expect(rows(page)).toHaveCount(5) + return page.getByRole("textbox").last() +} + +test("searches every recent project, not just the five most recent", async ({ page }) => { + const search = await openProjectDialog(page) + await expect(row(page, OUTSIDE_CAP)).toHaveCount(0) + + await search.fill("foxtrot") + + await expect(row(page, OUTSIDE_CAP)).toHaveCount(1) +}) + +test("still caps the idle recent list at five projects", async ({ page }) => { + await openProjectDialog(page) + + await expect(row(page, NAMES[4])).toHaveCount(1) + await expect(row(page, OUTSIDE_CAP)).toHaveCount(0) +}) diff --git a/packages/app/e2e/regression/prompt-input-v2-command-draft.spec.ts b/packages/app/e2e/regression/prompt-input-v2-command-draft.spec.ts new file mode 100644 index 0000000000..50dcc8820b --- /dev/null +++ b/packages/app/e2e/regression/prompt-input-v2-command-draft.spec.ts @@ -0,0 +1,50 @@ +import { expect, test } from "@playwright/test" +import { base64Encode } from "@opencode-ai/core/util/encode" +import { mockOpenCodeServer } from "../utils/mock-server" +import { expectAppVisible } from "../utils/waits" + +const directory = "C:/OpenCode/PromptInputV2Editing" +const projectID = "proj_prompt_input_v2_editing" +const sessionID = "ses_prompt_input_v2_editing" + +test("preserves the draft when a populated command menu triggers a built-in", async ({ page }) => { + await mockOpenCodeServer(page, { + directory, + project: { + id: projectID, + worktree: directory, + vcs: "git", + name: "prompt-input-v2-editing", + time: { created: 1700000000000, updated: 1700000000000 }, + sandboxes: [], + }, + provider: { all: [], connected: [], default: {} }, + sessions: [ + { + id: sessionID, + slug: "prompt-input-v2-editing", + projectID, + directory, + title: "Prompt input V2 editing", + version: "dev", + time: { created: 1700000000000, updated: 1700000000000 }, + }, + ], + pageMessages: () => ({ items: [] }), + }) + await page.addInitScript(() => { + localStorage.setItem("settings.v3", JSON.stringify({ general: { newLayoutDesigns: true } })) + }) + + await page.goto(`/${base64Encode(directory)}/session/${sessionID}`) + const composer = page.locator('[data-component="prompt-input-v2"]') + const input = composer.locator('[data-component="prompt-input"]') + await expectAppVisible(composer) + + await input.fill("keep me") + await composer.getByRole("button", { name: "Add images and files" }).click() + await page.getByRole("menuitem", { name: "Commands" }).click() + await page.locator('[data-suggestion-id="model.choose"]').click() + + await expect(input).toHaveText("keep me") +}) diff --git a/packages/app/e2e/regression/prompt-thinking-level.spec.ts b/packages/app/e2e/regression/prompt-thinking-level.spec.ts index 4219699f28..9315c347c0 100644 --- a/packages/app/e2e/regression/prompt-thinking-level.spec.ts +++ b/packages/app/e2e/regression/prompt-thinking-level.spec.ts @@ -54,18 +54,15 @@ test("shows the V2 thinking level control while relevant", async ({ page }) => { }) await page.goto(`/${base64Encode(directory)}/session/${sessionID}`) - const composer = page.locator('[data-component="session-composer"]') + const composer = page.locator('[data-component="prompt-input-v2"]') const input = composer.locator('[data-component="prompt-input"]') - const control = composer.locator('[data-component="prompt-variant-control"]') + const control = composer.getByRole("button", { name: "Choose model variant" }) await expectAppVisible(composer) await idleComposer(page) - await expect(control).toBeHidden() - - await composer.hover() await expect(control).toBeVisible() - await control.locator('[data-action="prompt-model-variant"]').click() + await control.click() const high = page.getByRole("menuitemradio", { name: "high" }) await expect(high).toBeVisible() await page.mouse.move(0, 0) diff --git a/packages/app/e2e/regression/remote-session-settings.spec.ts b/packages/app/e2e/regression/remote-session-settings.spec.ts new file mode 100644 index 0000000000..35a0aa44cd --- /dev/null +++ b/packages/app/e2e/regression/remote-session-settings.spec.ts @@ -0,0 +1,302 @@ +import { base64Encode } from "@opencode-ai/core/util/encode" +import { expect, test, type Page, type Route } from "@playwright/test" +import { installSseTransport } from "../utils/sse-transport" +import { currentSession } from "../utils/mock-server" + +const serverA = "http://127.0.0.1:4096" +const serverB = "http://127.0.0.1:4097" +const directoryA = "C:/server-a" +const directoryB = "/home/server-b" +const sessionA = session("ses_server_a", directoryA, "Server A session") +const childSessionA = { ...session("ses_server_a_child", directoryA, "Server A child session"), parentID: sessionA.id } +const sessionB = session("ses_server_b", directoryB, "Server B session") + +test("session settings use the remote server context", async ({ page }) => { + const permissionRequests: string[] = [] + await mockServers(page, permissionRequests) + await configureServers(page) + + await page.goto(`/server/${base64Encode(serverB)}/session/${sessionB.id}`) + await expect(page.getByText(sessionB.title).first()).toBeVisible() + await page.keyboard.press("Control+,") + + const dialog = page.locator(".settings-v2-dialog") + const autoAccept = dialog.locator('[data-action="settings-auto-accept-permissions"]') + const input = autoAccept.getByRole("switch") + await expect(autoAccept).toBeVisible() + await expect(input).toBeEnabled() + permissionRequests.length = 0 + await autoAccept.locator('[data-slot="switch-control"]').click() + await expect(input).toBeChecked() + await expect + .poll(() => + permissionRequests.some((request) => { + const url = new URL(request) + return url.origin === serverB && url.searchParams.get("directory") === directoryB + }), + ) + .toBe(true) + expect(permissionRequests.every((request) => new URL(request).origin === serverB)).toBe(true) + + await dialog.getByRole("tab", { name: "Models" }).click() + await expect(dialog.getByRole("switch", { name: "Server B Model" })).toBeEnabled() + await expect(dialog.getByRole("switch", { name: "Server A Model" })).toHaveCount(0) +}) + +test("auto-accept responds for an unfocused server session", async ({ page }) => { + const permissionRequests: string[] = [] + const permissionResponses: PermissionResponse[] = [] + const transport = await installSseTransport<{ directory: string; payload: Record }>(page, { + server: serverA, + retry: 20, + }) + await mockServers(page, permissionRequests, permissionResponses) + await configureServers(page, [ + { type: "session", server: serverA, sessionId: sessionA.id }, + { type: "session", server: serverB, sessionId: sessionB.id }, + ]) + + const hrefB = `/server/${base64Encode(serverB)}/session/${sessionB.id}` + await page.goto(`/server/${base64Encode(serverA)}/session/${sessionA.id}`) + await expect(page.getByText(sessionA.title).first()).toBeVisible() + await page.keyboard.press("Control+,") + const autoAccept = page.locator(".settings-v2-dialog").locator('[data-action="settings-auto-accept-permissions"]') + await autoAccept.locator('[data-slot="switch-control"]').click() + await expect(autoAccept.getByRole("switch")).toBeChecked() + await expect + .poll(() => + permissionRequests.some((request) => { + const url = new URL(request) + return url.origin === serverA && url.searchParams.get("directory") === directoryA + }), + ) + .toBe(true) + await page.keyboard.press("Escape") + + await page.locator(`[data-titlebar-tab-slot]:has(a[href="${hrefB}"])`).click() + await expect(page).toHaveURL(new RegExp(`${hrefB.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}$`)) + await expect(page.getByText(sessionB.title).first()).toBeVisible() + await transport.waitForConnection() + + await transport.send({ + directory: directoryA, + payload: { + id: "event-permission-background-a", + type: "permission.asked", + properties: { + id: "permission-background-a", + sessionID: sessionA.id, + permission: "bash", + patterns: ["git status"], + metadata: {}, + always: [], + }, + }, + }) + + await expect + .poll(() => permissionResponses) + .toEqual([ + { + origin: serverA, + directory: directoryA, + sessionID: sessionA.id, + permissionID: "permission-background-a", + body: { response: "once" }, + }, + ]) + + await transport.send({ + directory: directoryA, + payload: { + id: "event-permission-background-a-child", + type: "permission.asked", + properties: { + id: "permission-background-a-child", + sessionID: childSessionA.id, + permission: "bash", + patterns: ["git diff"], + metadata: {}, + always: [], + }, + }, + }) + + await expect + .poll(() => permissionResponses) + .toEqual([ + { + origin: serverA, + directory: directoryA, + sessionID: sessionA.id, + permissionID: "permission-background-a", + body: { response: "once" }, + }, + { + origin: serverA, + directory: directoryA, + sessionID: childSessionA.id, + permissionID: "permission-background-a-child", + body: { response: "once" }, + }, + ]) +}) + +type PermissionResponse = { + origin: string + directory?: string + sessionID: string + permissionID: string + body: unknown +} + +async function configureServers(page: Page, tabs: { type: "session"; server: string; sessionId: string }[] = []) { + await page.addInitScript( + ({ serverB, tabs }) => { + localStorage.setItem("settings.v3", JSON.stringify({ general: { newLayoutDesigns: true } })) + localStorage.setItem("opencode.global.dat:server", JSON.stringify({ list: [serverB] })) + localStorage.setItem("opencode.window.browser.dat:tabs", JSON.stringify(tabs)) + }, + { serverB, tabs }, + ) +} + +async function mockServers(page: Page, permissionRequests: string[], permissionResponses: PermissionResponse[] = []) { + await page.route("**/*", async (route) => { + const url = new URL(route.request().url()) + if (url.origin !== serverA && url.origin !== serverB) return route.fallback() + const remote = url.origin === serverB + const directory = remote ? directoryB : directoryA + const sessions = remote ? [sessionB] : [sessionA, childSessionA] + const requestDirectory = url.searchParams.get("directory") + const response = url.pathname.match(/^\/session\/([^/]+)\/permissions\/([^/]+)$/) + if (route.request().method() === "POST" && response) { + permissionResponses.push({ + origin: url.origin, + directory: requestDirectory ?? undefined, + sessionID: response[1]!, + permissionID: response[2]!, + body: route.request().postDataJSON(), + }) + return json(route, true) + } + if (requestDirectory && requestDirectory !== directory) return json(route, { name: "InvalidDirectory" }, 500) + if (url.pathname === "/global/event" || url.pathname === "/event" || url.pathname === "/api/event") + return sse(route) + if (url.pathname === "/global/health") return json(route, { healthy: true }) + if (url.pathname === "/api/provider" || url.pathname === "/api/model" || url.pathname === "/api/agent") + return json(route, { data: [] }) + if (url.pathname === "/api/model/default") return json(route, { data: null }) + if (["/api/command", "/api/reference", "/api/permission/request", "/api/question/request"].includes(url.pathname)) + return json(route, { location: { directory }, data: [] }) + if (url.pathname === "/api/mcp") return json(route, { location: { directory }, data: [] }) + if (url.pathname === "/api/mcp/resource") + return json(route, { location: { directory }, data: { resources: [], templates: [] } }) + if (url.pathname === "/api/project") { + return json(route, [ + { + id: remote ? sessionB.projectID : "project-server-a", + worktree: directory, + vcs: "git", + time: { created: 1, updated: 1 }, + sandboxes: [], + }, + ]) + } + if (url.pathname === "/api/project/current") + return json(route, { id: remote ? sessionB.projectID : "project-server-a", directory }) + if (url.pathname === "/api/session") return json(route, { data: sessions.map(currentSession), cursor: {} }) + if (url.pathname === "/api/session/active") return json(route, { data: {} }) + const currentSessionInfo = sessions.find((session) => url.pathname === `/api/session/${session.id}`) + if (currentSessionInfo) return json(route, { data: currentSession(currentSessionInfo) }) + if (sessions.some((session) => url.pathname === `/api/session/${session.id}/message`)) + return json(route, { data: [], cursor: {} }) + const current = sessions.find((session) => url.pathname === `/session/${session.id}`) + if (current) return json(route, current) + if (/^\/session\/[^/]+$/.test(url.pathname)) return json(route, { name: "NotFoundError" }, 404) + if (/^\/session\/[^/]+\/message$/.test(url.pathname)) return json(route, []) + if (/^\/session\/[^/]+\/(children|todo|diff)$/.test(url.pathname)) return json(route, []) + if (url.pathname === "/permission") { + permissionRequests.push(url.toString()) + return json(route, []) + } + if (["/skill", "/command", "/lsp", "/formatter", "/question", "/vcs/diff", "/pty/shells"].includes(url.pathname)) + return json(route, []) + if (["/global/config", "/config", "/provider/auth", "/mcp"].includes(url.pathname)) return json(route, {}) + if (url.pathname === "/provider") return json(route, provider(remote ? "server-b" : "server-a")) + if (url.pathname === "/agent") return json(route, [{ name: "build", mode: "primary" }]) + if (url.pathname === "/project" || url.pathname === "/project/current") { + const project = { + id: remote ? sessionB.projectID : "project-server-a", + worktree: directory, + vcs: "git", + time: { created: 1, updated: 1 }, + sandboxes: [], + } + return json(route, url.pathname === "/project" ? [project] : project) + } + if (url.pathname === "/path") + return json(route, { + state: directory, + config: directory, + worktree: directory, + directory, + home: directory, + }) + if (url.pathname === "/api/path") + return json(route, { state: directory, config: directory, worktree: directory, directory, home: directory }) + if (url.pathname === "/vcs") return json(route, { branch: "main", default_branch: "main" }) + if (url.pathname === "/api/vcs") + return json(route, { location: { directory }, data: { branch: "main", defaultBranch: "main" } }) + if (url.pathname === "/api/pty/shells") return json(route, { location: { directory }, data: [] }) + return json(route, {}) + }) +} + +function session(id: string, directory: string, title: string) { + return { + id, + slug: id, + projectID: `project-${id}`, + directory, + title, + version: "dev", + time: { created: 1, updated: 1 }, + } +} + +function provider(id: string) { + const name = id === "server-b" ? "Server B" : "Server A" + return { + all: [ + { + id, + name: `${name} Provider`, + models: { + [id]: { + id, + name: `${name} Model`, + family: id, + release_date: "2026-01-01", + limit: { context: 200_000 }, + }, + }, + }, + ], + connected: [id], + default: { providerID: id, modelID: id }, + } +} + +function json(route: Route, body: unknown, status = 200) { + return route.fulfill({ + status, + contentType: "application/json", + headers: { "access-control-allow-origin": "*" }, + body: JSON.stringify(body), + }) +} + +function sse(route: Route) { + return route.fulfill({ status: 200, contentType: "text/event-stream", body: ": ok\n\n" }) +} diff --git a/packages/app/e2e/regression/remote-tab-busy.spec.ts b/packages/app/e2e/regression/remote-tab-busy.spec.ts index ad7e2e1d99..2d9b1e2349 100644 --- a/packages/app/e2e/regression/remote-tab-busy.spec.ts +++ b/packages/app/e2e/regression/remote-tab-busy.spec.ts @@ -1,5 +1,6 @@ import { expect, test, type Page, type Route } from "@playwright/test" import { base64Encode } from "@opencode-ai/core/util/encode" +import { currentSession } from "../utils/mock-server" const serverA = "http://127.0.0.1:4096" const serverB = "http://127.0.0.1:4097" @@ -57,15 +58,19 @@ async function mockServers(page: Page) { const current = url.origin === serverA ? sessionA : sessionB const directory = url.searchParams.get("directory") if (directory && directory !== current.directory) return json(route, { name: "InvalidDirectory" }, 500) - if (url.pathname === "/global/event" || url.pathname === "/event") return sse(route) - if (url.pathname === "/global/health") return json(route, { healthy: true }) - if (url.pathname === "/session/status") - return json(route, url.origin === serverB ? { [sessionB.id]: { type: "busy" } } : {}) - if (url.pathname === "/session") return json(route, [current]) + if (url.pathname === "/global/event" || url.pathname === "/event" || url.pathname === "/api/event") + return sse(route, url.pathname === "/api/event") + if (url.pathname === "/global/health") return json(route, {}, 404) + if (url.pathname === "/api/health") return json(route, { pid: 1 }) + if (url.pathname === "/api/session/active") + return json(route, { data: url.origin === serverB ? { [sessionB.id]: { type: "running" } } : {} }) + if (url.pathname === "/api/session") return json(route, { data: [currentSession(current)], cursor: {} }) + if (url.pathname === `/api/session/${current.id}`) return json(route, { data: currentSession(current) }) + if (url.pathname === `/api/session/${current.id}/message`) return json(route, { data: [], cursor: {} }) if (url.pathname === `/session/${current.id}`) return json(route, current) if (/^\/session\/[^/]+$/.test(url.pathname)) return json(route, { name: "NotFoundError" }, 404) if (url.pathname === `/session/${current.id}/message`) return json(route, []) - if (/^\/session\/[^/]+\/(children|diff)$/.test(url.pathname)) return json(route, []) + if (/^\/session\/[^/]+\/(children|todo|diff)$/.test(url.pathname)) return json(route, []) if (["/skill", "/command", "/lsp", "/formatter", "/permission", "/question", "/vcs/diff"].includes(url.pathname)) return json(route, []) if (["/global/config", "/config", "/provider/auth", "/mcp"].includes(url.pathname)) return json(route, {}) @@ -90,7 +95,20 @@ async function mockServers(page: Page) { directory: current.directory, home: current.directory, }) + if (url.pathname === "/api/path") + return json(route, { + state: current.directory, + config: current.directory, + worktree: current.directory, + directory: current.directory, + home: current.directory, + }) if (url.pathname === "/vcs") return json(route, { branch: "main", default_branch: "main" }) + if (url.pathname === "/api/vcs") + return json(route, { + location: { directory: current.directory }, + data: { branch: "main", defaultBranch: "main" }, + }) return json(route, {}) }) } @@ -104,6 +122,10 @@ function json(route: Route, body: unknown, status = 200) { }) } -function sse(route: Route) { - return route.fulfill({ status: 200, contentType: "text/event-stream", body: ": ok\n\n" }) +function sse(route: Route, current: boolean) { + return route.fulfill({ + status: 200, + contentType: "text/event-stream", + body: current ? 'data: {"id":"evt_connected","type":"server.connected","data":{}}\n\n' : ": ok\n\n", + }) } diff --git a/packages/app/e2e/regression/review-line-comment.spec.ts b/packages/app/e2e/regression/review-line-comment.spec.ts index 042f926c53..7850f7820a 100644 --- a/packages/app/e2e/regression/review-line-comment.spec.ts +++ b/packages/app/e2e/regression/review-line-comment.spec.ts @@ -84,6 +84,7 @@ test("stages a submitted line comment in the prompt context", async ({ page }) = async function openReview(page: Page) { await page.setViewportSize({ width: 700, height: 900 }) await mockOpenCodeServer(page, { + protocol: "v2", directory, project: { id: "proj_review_line_comment_regression", @@ -143,9 +144,9 @@ async function openReview(page: Page) { await page.goto(`/${base64Encode(directory)}/session/${sessionID}`) await expectSessionTitle(page, title) - const diffResponse = page.waitForResponse((response) => new URL(response.url()).pathname === "/vcs/diff") + const diffResponse = page.waitForResponse((response) => new URL(response.url()).pathname === "/api/vcs/diff") await page.getByRole("tab", { name: "Changes" }).click() - expect(await (await diffResponse).json()).toHaveLength(1) + expect((await (await diffResponse).json()).data).toHaveLength(1) const review = page.locator('[data-component="session-review"]') await expectAppVisible(review) diff --git a/packages/app/e2e/regression/review-open-file.spec.ts b/packages/app/e2e/regression/review-open-file.spec.ts index 397c53f248..04e6d2cced 100644 --- a/packages/app/e2e/regression/review-open-file.spec.ts +++ b/packages/app/e2e/regression/review-open-file.spec.ts @@ -91,15 +91,17 @@ test("opens and searches project files inline", async ({ page }) => { const panel = page.locator("#review-panel") const sidebar = panel.locator('[data-slot="session-review-v2-sidebar"]') + const sidebarToggle = panel.getByRole("button", { name: "Toggle file tree" }) const contextButton = page.getByRole("button", { name: "View context usage" }) await contextButton.click() await expect(panel.getByRole("tab", { name: "Context" })).toHaveAttribute("data-selected", "") await panel.getByRole("button", { name: "Open file" }).click() await expect(panel.getByRole("tab", { name: "Open file" })).toHaveAttribute("data-selected", "") + await expect(sidebarToggle).toBeDisabled() await expect(sidebar).toBeVisible() await contextButton.click() await expect(panel.getByRole("tab", { name: "Context" })).toHaveAttribute("data-selected", "") - await expect(sidebar).toHaveCount(0) + await expect(sidebar).toBeHidden() await panel.getByRole("button", { name: "Open file" }).click() const filter = panel.getByRole("combobox", { name: "Filter files" }) await expect(filter).toBeFocused() @@ -108,6 +110,7 @@ test("opens and searches project files inline", async ({ page }) => { await panel.getByRole("button", { name: "README.md" }).click() await expect(panel.getByRole("tab", { name: "README.md" })).toHaveAttribute("data-selected", "") + await expect(sidebarToggle).toBeEnabled() await expect(panel.getByText("contents:README.md", { exact: true })).toBeVisible() await expect(sidebar).toHaveCount(0) @@ -122,12 +125,17 @@ test("opens and searches project files inline", async ({ page }) => { await expect(filter).toHaveAttribute("aria-activedescendant", resultID!) await filter.press("Enter") await expect(panel.getByRole("tab", { name: "nested.ts" })).toHaveAttribute("data-selected", "") + await expect(sidebarToggle).toBeEnabled() await expect(panel.getByText("contents:src/nested.ts", { exact: true })).toBeVisible() expect(searches).toContainEqual({ query: "nested", dirs: "false", limit: 200 }) await panel.getByRole("button", { name: "Open file" }).click() await expect(panel.getByRole("tab", { name: "nested.ts" })).toHaveCount(1) await expect(panel.getByRole("tab", { name: "Open file" })).toHaveAttribute("data-selected", "") + await expect(sidebarToggle).toBeDisabled() + await panel.locator("#session-side-panel-review-tab").click() + await expect(sidebarToggle).toBeEnabled() + await panel.getByRole("tab", { name: "Open file" }).click() await page.keyboard.press("Control+w") await expect(panel.getByRole("tab", { name: "Open file" })).toHaveCount(0) await expect(panel.getByRole("tab", { name: "nested.ts" })).toHaveAttribute("data-selected", "") diff --git a/packages/app/e2e/regression/review-state-persistence.spec.ts b/packages/app/e2e/regression/review-state-persistence.spec.ts index aa42f1bb51..0d6756201e 100644 --- a/packages/app/e2e/regression/review-state-persistence.spec.ts +++ b/packages/app/e2e/regression/review-state-persistence.spec.ts @@ -46,7 +46,7 @@ test("restores review mode and selected file per session", async ({ page }) => { async function selectMode(page: Page, current: string, next: string) { await page.getByRole("button", { name: current }).click() - await page.getByRole("option", { name: next }).click() + await page.getByRole("option", { name: next }).dispatchEvent("click") } async function selectFile(page: Page, file: string) { @@ -65,6 +65,7 @@ async function switchSession(page: Page, title: string) { async function setup(page: Page) { await mockOpenCodeServer(page, { + protocol: "v1", directory, project: { id: projectID, diff --git a/packages/app/e2e/regression/review-tab-switch.spec.ts b/packages/app/e2e/regression/review-tab-switch.spec.ts index c2ea406c5a..8634166e51 100644 --- a/packages/app/e2e/regression/review-tab-switch.spec.ts +++ b/packages/app/e2e/regression/review-tab-switch.spec.ts @@ -28,8 +28,8 @@ test("keeps the v2 review pane mounted when switching session tabs in a workspac await expectSessionTitle(page, titleA) await page.getByRole("button", { name: "Toggle review" }).click() - const reviewTab = page.getByRole("tab", { name: /Review/ }) - const reviewTabPanel = page.getByRole("tabpanel", { name: /Review/ }) + const reviewTab = page.locator("#session-side-panel-review-tab") + const reviewTabPanel = page.locator("#session-side-panel-review-tabpanel") await expect(reviewTab).toHaveAttribute("aria-controls", "session-side-panel-review-tabpanel") await expect(reviewTabPanel).toHaveAttribute("id", "session-side-panel-review-tabpanel") const review = page.locator('#review-panel [data-component="session-review-v2"]') diff --git a/packages/app/e2e/regression/review-terminal-stacked.spec.ts b/packages/app/e2e/regression/review-terminal-stacked.spec.ts index e3ba607b3d..79b564820e 100644 --- a/packages/app/e2e/regression/review-terminal-stacked.spec.ts +++ b/packages/app/e2e/regression/review-terminal-stacked.spec.ts @@ -20,10 +20,12 @@ const branchDiffs = [ test("keeps the review tree and terminal sized when both panels are open", async ({ page }) => { test.setTimeout(120_000) const events: Array<{ directory: string; payload: Record }> = [] + const sessionStatus = { [sessionID]: { type: "idle" as "busy" | "idle" } } let detailVersion = 1 let detailFailures = 1 await page.setViewportSize({ width: 1400, height: 900 }) await mockOpenCodeServer(page, { + protocol: "v1", directory, project: { id: projectID, @@ -55,7 +57,7 @@ test("keeps the review tree and terminal sized when both panels are open", async time: { created: 1700000000000, updated: 1700000000000 }, }, ], - sessionStatus: { [sessionID]: { type: "idle" } }, + sessionStatus: () => sessionStatus, pageMessages: () => ({ items: [] }), events: () => events.splice(0, 1), eventRetry: 16, @@ -64,7 +66,10 @@ test("keeps the review tree and terminal sized when both panels are open", async route.fulfill({ status: 200, contentType: "application/json", - body: JSON.stringify({ branch: "review-pane-performance", default_branch: "dev" }), + body: JSON.stringify({ + branch: "review-pane-performance", + default_branch: "dev", + }), }), ) await page.route("**/vcs/diff**", (route) => { @@ -86,15 +91,51 @@ test("keeps the review tree and terminal sized when both panels are open", async ), }) }) - await page.route("**/pty", (route) => + await page.route("**/pty*", (route) => route.fulfill({ status: 200, contentType: "application/json", - body: JSON.stringify({ id: "pty_review_terminal", title: "Terminal 1" }), + body: JSON.stringify({ + location: { directory, project: { id: projectID, directory } }, + data: { + id: "pty_review_terminal", + title: "Terminal 1", + command: "cmd.exe", + args: [], + cwd: directory, + status: "running", + pid: 1, + }, + }), }), ) - await page.route("**/pty/pty_review_terminal", (route) => - route.fulfill({ status: 200, contentType: "application/json", body: "{}" }), + await page.route("**/pty/pty_review_terminal*", (route) => + route.fulfill({ + status: 200, + contentType: "application/json", + body: JSON.stringify({ + location: { directory, project: { id: projectID, directory } }, + data: { + id: "pty_review_terminal", + title: "Terminal 1", + command: "cmd.exe", + args: [], + cwd: directory, + status: "running", + pid: 1, + }, + }), + }), + ) + await page.route("**/pty/pty_review_terminal/connect-token*", (route) => + route.fulfill({ + status: 200, + contentType: "application/json", + body: JSON.stringify({ + location: { directory, project: { id: projectID, directory } }, + data: { ticket: "e2e-ticket", expires_in: 60 }, + }), + }), ) await page.routeWebSocket("**/pty/pty_review_terminal/connect", () => undefined) await page.addInitScript(() => { @@ -111,7 +152,7 @@ test("keeps the review tree and terminal sized when both panels are open", async await expectTree(page, 8, "git-0.ts") await selectMode(page, "Git changes", "Branch changes") - await expect(page.getByRole("tab", { name: "Review 2740" })).toBeVisible() + await expect(page.locator("#session-side-panel-review-tab")).toHaveText("Files Changed 2740") await page.keyboard.press("Control+Backquote") await expect(page.locator("#terminal-panel")).toBeVisible() await expectTree(page, 2_773, "action.yml") @@ -143,6 +184,7 @@ test("keeps the review tree and terminal sized when both panels are open", async const preview = page.locator('[data-slot="session-review-v2-diff-scroll"]') await expect(preview).toContainText("after-1") detailVersion = 2 + sessionStatus[sessionID] = { type: "busy" } events.push(statusEvent("busy")) await expect(page.getByRole("button", { name: "Stop" })).toBeVisible() const refreshedDiff = page.waitForRequest((request) => { @@ -152,6 +194,7 @@ test("keeps the review tree and terminal sized when both panels are open", async url.searchParams.get("directory")?.replaceAll("\\", "/").endsWith("/src/branch/d00027") === true ) }) + sessionStatus[sessionID] = { type: "idle" } events.push(statusEvent("idle")) await refreshedDiff await expect(preview).toContainText("after-2") diff --git a/packages/app/e2e/regression/session-list-path-loading.spec.ts b/packages/app/e2e/regression/session-list-path-loading.spec.ts index 4a3855122a..3319514df6 100644 --- a/packages/app/e2e/regression/session-list-path-loading.spec.ts +++ b/packages/app/e2e/regression/session-list-path-loading.spec.ts @@ -16,8 +16,8 @@ test("shows loaded sessions before the directory path request resolves", async ( const pathBlocked = new Promise((resolve) => { releasePath = resolve }) - await page.route("**/path?*", async (route) => { - if (!new URL(route.request().url()).searchParams.has("directory")) return route.fallback() + await page.route("**/api/path?*", async (route) => { + if (!new URL(route.request().url()).searchParams.has("location[directory]")) return route.fallback() await pathBlocked return route.fallback() }) diff --git a/packages/app/e2e/regression/session-request-docks.spec.ts b/packages/app/e2e/regression/session-request-docks.spec.ts index 036eaaef42..5ea9d4f761 100644 --- a/packages/app/e2e/regression/session-request-docks.spec.ts +++ b/packages/app/e2e/regression/session-request-docks.spec.ts @@ -1,6 +1,7 @@ import { base64Encode } from "@opencode-ai/core/util/encode" import { expect, test, type Page } from "@playwright/test" import { mockOpenCodeServer } from "../utils/mock-server" +import { installSseTransport } from "../utils/sse-transport" import { expectSessionTitle } from "../utils/waits" const directory = "C:/OpenCode/RequestDocks" @@ -41,7 +42,8 @@ test("shows a pending question dock", async ({ page }) => { const rejectRequests: string[] = [] page.on("request", (request) => { if (request.method() !== "POST") return - if (new URL(request.url()).pathname === "/question/question-request/reject") rejectRequests.push(request.url()) + if (new URL(request.url()).pathname === `/api/session/${sessionID}/question/question-request/reject`) + rejectRequests.push(request.url()) }) await question.locator('[data-component="icon-button"][data-icon="chevron-down"]').click() @@ -63,7 +65,9 @@ test("shows a pending question dock", async ({ page }) => { await question.getByRole("radio", { name: /Minimal/ }).click() const reply = page.waitForRequest( - (request) => request.method() === "POST" && new URL(request.url()).pathname === "/question/question-request/reply", + (request) => + request.method() === "POST" && + new URL(request.url()).pathname === `/api/session/${sessionID}/question/question-request/reply`, ) await question.getByRole("button", { name: "Submit" }).click() expect((await reply).postDataJSON()).toEqual({ answers: [["Minimal"]] }) @@ -96,8 +100,69 @@ test("shows a pending permission dock", async ({ page }) => { const reply = page.waitForRequest((request) => request.method() === "POST") await permission.getByRole("button", { name: "Allow once" }).click() const request = await reply - expect(new URL(request.url()).pathname).toBe(`/session/${sessionID}/permissions/permission-request`) - expect(request.postDataJSON()).toEqual({ response: "once" }) + expect(new URL(request.url()).pathname).toBe(`/api/session/${sessionID}/permission/permission-request/reply`) + expect(request.postDataJSON()).toEqual({ reply: "once" }) +}) + +test("restores the draft caret before typing after a request dock closes", async ({ page }) => { + const transport = await installSseTransport(page, { + server: `http://${process.env.PLAYWRIGHT_SERVER_HOST ?? "127.0.0.1"}:${process.env.PLAYWRIGHT_SERVER_PORT ?? "4096"}`, + retry: 20, + }) + await mockServer(page, { questions: [] }) + await page.goto(`/${base64Encode(directory)}/session/${sessionID}`) + await transport.waitForConnection() + await expectSessionTitle(page, title) + + const editor = page.locator('[data-component="prompt-input"][contenteditable="true"]') + const draft = "keep the caret at the end" + await editor.fill(draft) + await page.evaluate(() => new Promise((resolve) => requestAnimationFrame(() => resolve()))) + for (let index = 0; index < 4; index++) await page.keyboard.press("ArrowLeft") + const cursor = draft.length - 4 + await expect + .poll(() => + editor.evaluate((element) => { + const selection = window.getSelection() + if (!selection?.rangeCount || !element.contains(selection.anchorNode)) return -1 + const range = selection.getRangeAt(0).cloneRange() + range.selectNodeContents(element) + range.setEnd(selection.anchorNode!, selection.anchorOffset) + return range.toString().length + }), + ) + .toBe(cursor) + await transport.send({ + directory, + payload: { + type: "question.asked", + properties: { + id: "question-caret", + sessionID, + questions: [ + { + header: "Continue", + question: "Continue?", + options: [{ label: "Yes", description: "Continue the session" }], + }, + ], + tool: { messageID: "message-caret", callID: "call-caret" }, + }, + }, + }) + const question = page.locator('[data-component="dock-prompt"][data-kind="question"]') + await expect(question).toBeVisible() + await expect(editor).toHaveCount(0) + + await transport.send({ + directory, + payload: { type: "question.rejected", properties: { sessionID, requestID: "question-caret" } }, + }) + await expect(question).toHaveCount(0) + await expect(editor).toBeVisible() + await page.keyboard.press("x") + + await expect(editor).toHaveText(`${draft.slice(0, cursor)}x${draft.slice(cursor)}`) }) async function mockServer( @@ -108,6 +173,7 @@ async function mockServer( }, ) { await mockOpenCodeServer(page, { + protocol: "v2", directory, project: { id: projectID, diff --git a/packages/app/e2e/regression/session-timeline-file-projection.spec.ts b/packages/app/e2e/regression/session-timeline-file-projection.spec.ts index a591ff9470..f07da121c6 100644 --- a/packages/app/e2e/regression/session-timeline-file-projection.spec.ts +++ b/packages/app/e2e/regression/session-timeline-file-projection.spec.ts @@ -24,7 +24,7 @@ test("renders a completed single-file patch", async ({ page }) => { assistantMessage([ toolPart( id, - "patch", + "apply_patch", "completed", { files: ["src/a.ts"] }, { diff --git a/packages/app/e2e/regression/session-timeline-file-state.spec.ts b/packages/app/e2e/regression/session-timeline-file-state.spec.ts index f0871a0da3..cb228c13c7 100644 --- a/packages/app/e2e/regression/session-timeline-file-state.spec.ts +++ b/packages/app/e2e/regression/session-timeline-file-state.spec.ts @@ -35,7 +35,7 @@ test("preserves nested patch file state through outer collapse and reopen", asyn assistantMessage([ toolPart( patchID, - "patch", + "apply_patch", "completed", { files: files.map((file) => file.filePath) }, { metadata: { files } }, diff --git a/packages/app/e2e/regression/session-timeline-history-root.spec.ts b/packages/app/e2e/regression/session-timeline-history-root.spec.ts index 15375cafed..e5ef7998ea 100644 --- a/packages/app/e2e/regression/session-timeline-history-root.spec.ts +++ b/packages/app/e2e/regression/session-timeline-history-root.spec.ts @@ -17,12 +17,14 @@ import { mockOpenCodeServer } from "../utils/mock-server" import { installSseTransport } from "../utils/sse-transport" import { expectSessionTitle } from "../utils/waits" -const assistants = Array.from({ length: 14 }, (_, index) => +const initialPageSize = 20 +const historyPageSize = 200 +const assistants = Array.from({ length: initialPageSize + 1 }, (_, index) => assistantMessage([textPart(`prt_history_root_${index}`, `Assistant response ${index}`)], { id: `msg_${String(index + 1001).padStart(4, "0")}_history_root_assistant`, parentID: userID, created: 1700000001000 + index * 1_000, - completed: index < 13, + completed: index < initialPageSize, }), ) const messages = [userMessage(), ...assistants] @@ -46,7 +48,7 @@ const scenarios = [ test.use({ viewport: { width: 646, height: 1385 } }) for (const scenario of scenarios) { - test(`keeps the latest user turn visible through ${scenario.name}`, async ({ page }) => { + test(`keeps visible timeline content visible through ${scenario.name}`, async ({ page }) => { const requests: { before?: string; phase: "start" | "end" }[] = [] const pages: { before?: string; limit: number }[] = [] const roots: { sessionID: string; messageID: string }[] = [] @@ -101,36 +103,51 @@ for (const scenario of scenarios) { } }, }) - await page.addInitScript( - ({ userPartID, lastPartID }) => { - const state = { armed: false, hidden: false, samples: 0, stop: false } - ;(window as Window & { __historyRootProbe?: typeof state }).__historyRootProbe = state - const sample = () => { - if (state.armed) { - const virtual = document.querySelector("[data-timeline-virtual-content]") - const viewport = virtual?.closest(".scroll-view__viewport") - const view = viewport?.getBoundingClientRect() - const visible = (partID: string) => { - const part = viewport?.querySelector(`[data-timeline-part-id="${partID}"]`) - const rect = part?.getBoundingClientRect() - return ( - !!rect && - !!view && - rect.width > 0 && - rect.height > 0 && - rect.bottom > view.top && - rect.top < view.bottom - ) - } - if (!virtual || !visible(userPartID) || !visible(lastPartID)) state.hidden = true - state.samples++ + await page.addInitScript(() => { + const visibleParts = () => { + const virtual = document.querySelector("[data-timeline-virtual-content]") + const viewport = virtual?.closest(".scroll-view__viewport") + const view = viewport?.getBoundingClientRect() + if (!viewport || !view) return [] + return [...viewport.querySelectorAll("[data-timeline-part-id]")] + .filter((part) => { + const rect = part.getBoundingClientRect() + return rect.width > 0 && rect.height > 0 && rect.bottom > view.top && rect.top < view.bottom + }) + .flatMap((part) => (part.dataset.timelinePartId ? [part.dataset.timelinePartId] : [])) + } + const state = { + armed: false, + hidden: false, + visibleParts: [] as string[], + samples: 0, + stop: false, + arm() { + state.visibleParts = visibleParts() + state.armed = true + }, + } + ;(window as Window & { __historyRootProbe?: typeof state }).__historyRootProbe = state + const sample = () => { + if (state.armed) { + const virtual = document.querySelector("[data-timeline-virtual-content]") + const viewport = virtual?.closest(".scroll-view__viewport") + const view = viewport?.getBoundingClientRect() + const visible = (partID: string) => { + const part = viewport?.querySelector(`[data-timeline-part-id="${CSS.escape(partID)}"]`) + const rect = part?.getBoundingClientRect() + return ( + !!rect && !!view && rect.width > 0 && rect.height > 0 && rect.bottom > view.top && rect.top < view.bottom + ) } - if (!state.stop) requestAnimationFrame(() => setTimeout(sample, 0)) + if (!virtual || state.visibleParts.length === 0 || state.visibleParts.some((partID) => !visible(partID))) + state.hidden = true + state.samples++ } - requestAnimationFrame(() => setTimeout(sample, 0)) - }, - { userPartID, lastPartID }, - ) + if (!state.stop) requestAnimationFrame(() => setTimeout(sample, 0)) + } + requestAnimationFrame(() => setTimeout(sample, 0)) + }) await page.goto(`/${base64Encode(directory)}/session/${sessionID}`) await transport.waitForConnection() @@ -143,23 +160,28 @@ for (const scenario of scenarios) { "messages:start:latest", "messages:end:latest", `message:${userID}`, - `messages:start:${messages.at(-2)!.info.id}`, + `messages:start:${messages.at(-initialPageSize)!.info.id}`, ]) + await expect(page.locator('[data-timeline-part-id^="prt_history_root_"]')).toHaveCount(initialPageSize) await page.evaluate(() => { ;( window as Window & { - __historyRootProbe?: { armed: boolean } + __historyRootProbe?: { arm(): void } } - ).__historyRootProbe!.armed = true + ).__historyRootProbe!.arm() }) await waitForProbeSamples(page, 0) - expect(await historyRootHidden(page)).toBe(false) + expect(await visibleContentHidden(page)).toBe(false) const beforeHistory = await probeSamples(page) history.resolve() - await expect(page.locator('[data-timeline-part-id^="prt_history_root_"]')).toHaveCount(14) + await expect(page.locator('[data-timeline-part-id^="prt_history_root_"]')).toHaveCount(assistants.length) + await expect.poll(() => requests.filter((request) => request.phase === "end").length).toBe(2) await expect(page.getByRole("button", { name: "Stop" })).toBeVisible() await waitForProbeSamples(page, beforeHistory) - expect(pages[0]).toEqual({ before: undefined, limit: 2 }) + expect(pages).toEqual([ + { before: undefined, limit: initialPageSize }, + { before: messages.at(-initialPageSize)!.info.id, limit: historyPageSize }, + ]) expect(roots).toEqual([{ sessionID, messageID: userID }]) const message = messageUpdated(scenario.info) @@ -213,7 +235,7 @@ async function waitForProbeSamples(page: Page, after: number) { ) } -function historyRootHidden(page: Page) { +function visibleContentHidden(page: Page) { return page.evaluate( () => (window as Window & { __historyRootProbe?: { hidden: boolean } }).__historyRootProbe!.hidden, ) diff --git a/packages/app/e2e/regression/session-timeline-lifecycle-state.spec.ts b/packages/app/e2e/regression/session-timeline-lifecycle-state.spec.ts index 3e2b171bca..b303071c87 100644 --- a/packages/app/e2e/regression/session-timeline-lifecycle-state.spec.ts +++ b/packages/app/e2e/regression/session-timeline-lifecycle-state.spec.ts @@ -32,6 +32,23 @@ for (const expanded of [false, true]) { }) } +test("shows and expands a running shell command without shimmering it", async ({ page }) => { + const id = "prt_shell_running_command" + const command = "sleep 10 && echo done" + await setupTimeline(page, { + messages: [userMessage(), assistantMessage([shell(id, "running", "still running", command)], { completed: false })], + settings: { shellToolPartsExpanded: false }, + }) + + const tool = page.locator(`[data-timeline-part-id="${id}"]`) + await expect(tool.locator('[data-component="text-shimmer"]')).toHaveAttribute("data-active", "true") + await expect(tool.locator('[data-component="shell-submessage"]')).toHaveText(command) + await expect(tool.locator('[data-component="shell-submessage"] [data-component="text-shimmer"]')).toHaveCount(0) + await tool.locator('[data-slot="collapsible-trigger"]').click() + await expect(tool.locator('[data-slot="collapsible-trigger"]')).toHaveAttribute("aria-expanded", "true") + await expect(tool.locator('[data-slot="bash-pre"]')).toContainText("still running") +}) + test("transitions thinking and hidden reasoning through busy to idle", async ({ page }) => { const reasoningID = "prt_reasoning_hidden" const assistant = assistantMessage([reasoningPart(reasoningID, "## Inspecting stability")], { completed: false }) diff --git a/packages/app/e2e/regression/session-timeline-projection.spec.ts b/packages/app/e2e/regression/session-timeline-projection.spec.ts index 45304df365..b1aabcc32c 100644 --- a/packages/app/e2e/regression/session-timeline-projection.spec.ts +++ b/packages/app/e2e/regression/session-timeline-projection.spec.ts @@ -35,6 +35,7 @@ test.describe("session timeline projection", () => { editPart("prt_edit"), toolPart("prt_write", "write", "completed", { filePath: "src/new.ts", content: "export const stable = true\n" }), patchPart("prt_patch"), + toolPart("prt_todo", "todowrite", "completed", { todos: [{ content: "Hidden", status: "pending" }] }), toolPart( "prt_question", "question", @@ -64,6 +65,7 @@ test.describe("session timeline projection", () => { ]) { await expect(page.locator(`[data-timeline-part-id="${id}"]`).first(), id).toBeVisible() } + await expect(page.locator('[data-timeline-part-id="prt_todo"]')).toHaveCount(0) }) test("projects gaps, dividers, assistant parts, and errors together", async ({ page }) => { @@ -150,7 +152,10 @@ test.describe("session timeline projection", () => { parentID: "msg_2000_diff_next_user", created: 1700000011000, }) - await setupTimeline(page, { messages: [user, assistantMessage(), nextUser, nextAssistant] }) + await setupTimeline(page, { + messages: [user, assistantMessage(), nextUser, nextAssistant], + settings: { newLayoutDesigns: false }, + }) const scroller = page.locator(".scroll-view__viewport", { has: page.locator("[data-timeline-row]") }) await scroller.evaluate((element) => (element.scrollTop = 0)) @@ -244,7 +249,7 @@ function editPart(id: string) { function patchPart(id: string) { return toolPart( id, - "patch", + "apply_patch", "completed", { files: ["src/a.ts", "src/b.ts"] }, { diff --git a/packages/app/e2e/regression/session-timeline-tool-projection.spec.ts b/packages/app/e2e/regression/session-timeline-tool-projection.spec.ts index 4c2c1c4ead..99f1acf270 100644 --- a/packages/app/e2e/regression/session-timeline-tool-projection.spec.ts +++ b/packages/app/e2e/regression/session-timeline-tool-projection.spec.ts @@ -8,7 +8,7 @@ import { } from "../performance/timeline-stability/fixture" test("renders every tool error outcome without leaking hidden tools", async ({ page }) => { - const ordinary = ["bash", "edit", "write", "patch", "webfetch", "websearch", "task", "skill", "mcp_probe"] + const ordinary = ["bash", "edit", "write", "apply_patch", "webfetch", "websearch", "task", "skill", "mcp_probe"] const parts = ordinary.map((tool, index) => toolPart(`prt_error_${index}`, tool, "error", errorInput(tool), { error: `${tool} failed visibly` }), ) @@ -17,11 +17,13 @@ test("renders every tool error outcome without leaking hidden tools", async ({ p error: "The user dismissed this question", }), toolPart("prt_question_error", "question", "error", questionInput(), { error: "Question transport failed" }), + toolPart("prt_todo_error", "todowrite", "error", { todos: [] }, { error: "Hidden todo failure" }), ) await setupTimeline(page, { messages: [userMessage(), assistantMessage(parts)] }) await expect(page.locator('[data-kind="tool-error-card"]')).toHaveCount(ordinary.length + 1) await expect(page.getByText(/dismissed/i)).toBeVisible() + await expect(page.locator('[data-timeline-part-id="prt_todo_error"]')).toHaveCount(0) for (let index = 0; index < ordinary.length; index++) { await expect(page.locator(`[data-timeline-part-id="prt_error_${index}"]`)).toBeVisible() } @@ -88,7 +90,7 @@ function questionInput() { function errorInput(tool: string) { if (tool === "bash") return { command: "exit 1" } if (["edit", "write"].includes(tool)) return { filePath: "src/error.ts", content: "" } - if (tool === "patch") return { files: ["src/error.ts"] } + if (tool === "apply_patch") return { files: ["src/error.ts"] } if (tool === "webfetch") return { url: "https://example.com" } if (tool === "websearch") return { query: "failure" } if (tool === "task") return { description: "Fail task", subagent_type: "explore" } diff --git a/packages/app/e2e/regression/session-timeline-transport.spec.ts b/packages/app/e2e/regression/session-timeline-transport.spec.ts index 850e966d0b..778ff3a3af 100644 --- a/packages/app/e2e/regression/session-timeline-transport.spec.ts +++ b/packages/app/e2e/regression/session-timeline-transport.spec.ts @@ -89,8 +89,8 @@ test("reconnects after a stream error", async ({ page }) => { expect((await timeline.transport.connections())[0]?.endedBy).toBe("error") }) -test("records event IDs and reconnect Last-Event-ID headers", async ({ page }) => { - const timeline = await setupTimeline(page, { eventRetry: 10 }) +test("does not request replay when reconnecting the volatile V2 event stream", async ({ page }) => { + const timeline = await setupTimeline(page, { eventRetry: 10, protocol: "v2" }) const first = await timeline.transport.send(partUpdated(textPart("prt_transport_id", "event with id")), { id: "timeline-event-7", }) @@ -100,7 +100,7 @@ test("records event IDs and reconnect Last-Event-ID headers", async ({ page }) = const connection = await timeline.transport.waitForConnection({ after: first.connectionID }) expect(first.eventID).toBe("timeline-event-7") - expect(connection.headers["last-event-id"]).toBe("timeline-event-7") + expect(connection.headers["last-event-id"]).toBeUndefined() }) test("passes through non-event fetches", async ({ page }) => { diff --git a/packages/app/e2e/regression/session-todo-dock-navigation.spec.ts b/packages/app/e2e/regression/session-todo-dock-navigation.spec.ts new file mode 100644 index 0000000000..55e7121275 --- /dev/null +++ b/packages/app/e2e/regression/session-todo-dock-navigation.spec.ts @@ -0,0 +1,190 @@ +import { base64Encode } from "@opencode-ai/core/util/encode" +import { expect, test, type Page } from "@playwright/test" +import { mockOpenCodeServer } from "../utils/mock-server" +import { expectSessionTitle } from "../utils/waits" + +const directory = "C:/OpenCode/TodoDockNavigation" +const projectID = "proj_todo_dock_navigation" +const sourceID = "ses_todo_dock_source" +const otherID = "ses_todo_dock_other" +const sourceTitle = "Todo dock animation" +const otherTitle = "Separate session" + +const activeTodos = [ + { id: "todo-1", content: "Receive todos in the active session", status: "completed", priority: "high" }, + { id: "todo-2", content: "Keep the dock visible across tabs", status: "completed", priority: "high" }, + { id: "todo-3", content: "Close after the final todo", status: "in_progress", priority: "high" }, +] + +type EventPayload = { + directory: string + payload: Record +} + +test.use({ viewport: { width: 1440, height: 900 }, reducedMotion: "no-preference" }) + +test("animates todo lifecycle without replaying it across session tabs", async ({ page }) => { + test.setTimeout(90_000) + const events: EventPayload[] = [] + const todos: Record = { [sourceID]: [], [otherID]: [] } + const sessionStatus: Record = {} + + await mockOpenCodeServer(page, { + directory, + project: { + id: projectID, + worktree: directory, + vcs: "git", + name: "todo-dock-navigation", + time: { created: 1700000000000, updated: 1700000000000 }, + sandboxes: [], + }, + provider: { + all: [ + { + id: "opencode", + name: "OpenCode", + models: { + "claude-opus-4-6": { + id: "claude-opus-4-6", + name: "Claude Opus 4.6", + limit: { context: 200_000 }, + }, + }, + }, + ], + connected: ["opencode"], + default: { providerID: "opencode", modelID: "claude-opus-4-6" }, + }, + sessions: [session(sourceID, sourceTitle, 1700000000000), session(otherID, otherTitle, 1700000001000)], + sessionStatus: { [sourceID]: { type: "busy" } }, + pageMessages: () => ({ items: [] }), + events: () => events.splice(0, 1), + eventRetry: 16, + sessionStatus: () => sessionStatus, + todos: (sessionID) => todos[sessionID] ?? [], + }) + await configurePage(page) + + await page.goto(sessionHref(sourceID)) + await expectSessionTitle(page, sourceTitle) + const dock = page.locator('[data-component="session-todo-dock"]') + await expect(dock).toHaveCount(0) + + sessionStatus[sourceID] = { type: "busy" } + events.push(statusEvent(sourceID, "busy")) + await expect(page.getByRole("button", { name: "Stop" })).toBeVisible() + + await page.waitForTimeout(700) + const opening = sampleDock(page, 1_000) + todos[sourceID] = activeTodos + events.push(todoEvent(sourceID, activeTodos)) + await expect(dock).toBeVisible() + await expect(dock.locator('[data-state="in_progress"]')).toHaveCount(1) + expect((await opening).some((sample) => sample.opacity > 0.05 && sample.opacity < 0.95)).toBe(true) + + await switchSession(page, otherID, otherTitle) + await expect(dock).toHaveCount(0) + + const returningOpen = sampleDock(page, 700) + await switchSession(page, sourceID, sourceTitle) + const openSamples = (await returningOpen).filter((sample) => sample.present) + expect(openSamples.length).toBeGreaterThan(0) + expect(openSamples[0]!.opacity).toBeGreaterThan(0.98) + expect(openSamples[0]!.height).toBeGreaterThan(70) + await expect(dock.locator('[data-state="in_progress"]')).toHaveCount(1) + + const completedTodos = activeTodos.map((todo) => ({ ...todo, status: "completed" })) + const closing = sampleDock(page, 1_000) + todos[sourceID] = completedTodos + events.push(todoEvent(sourceID, completedTodos)) + await expect(dock).toHaveCount(0) + expect((await closing).some((sample) => sample.opacity > 0.05 && sample.opacity < 0.95)).toBe(true) + todos[sourceID] = [] + events.push(todoEvent(sourceID, [])) + + await switchSession(page, otherID, otherTitle) + const returningEmpty = sampleDock(page, 700) + await switchSession(page, sourceID, sourceTitle) + await expect(dock).toHaveCount(0) + expect((await returningEmpty).every((sample) => !sample.present)).toBe(true) +}) + +function session(id: string, title: string, created: number) { + return { + id, + slug: id, + projectID, + directory, + title, + version: "dev", + time: { created, updated: created }, + } +} + +function statusEvent(sessionID: string, type: "busy" | "idle"): EventPayload { + return { + directory, + payload: { type: "session.status", properties: { sessionID, status: { type } } }, + } +} + +function todoEvent(sessionID: string, next: typeof activeTodos): EventPayload { + return { + directory, + payload: { type: "todo.updated", properties: { sessionID, todos: next } }, + } +} + +async function configurePage(page: Page) { + const server = `http://${process.env.PLAYWRIGHT_SERVER_HOST ?? "127.0.0.1"}:${process.env.PLAYWRIGHT_SERVER_PORT ?? "4096"}` + await page.addInitScript( + ({ directory, dirBase64, server, sessionIDs }) => { + localStorage.setItem("settings.v3", JSON.stringify({ general: { newLayoutDesigns: true } })) + localStorage.setItem( + "opencode.global.dat:server", + JSON.stringify({ + projects: { local: [{ worktree: directory, expanded: true }] }, + lastProject: { local: directory }, + }), + ) + localStorage.setItem( + "opencode.window.browser.dat:tabs", + JSON.stringify(sessionIDs.map((sessionId) => ({ type: "session", server, dirBase64, sessionId }))), + ) + }, + { directory, dirBase64: base64Encode(directory), server, sessionIDs: [sourceID, otherID] }, + ) +} + +function sessionHref(sessionID: string) { + const server = `http://${process.env.PLAYWRIGHT_SERVER_HOST ?? "127.0.0.1"}:${process.env.PLAYWRIGHT_SERVER_PORT ?? "4096"}` + return `/server/${base64Encode(server)}/session/${sessionID}` +} + +async function switchSession(page: Page, sessionID: string, title: string) { + const href = sessionHref(sessionID) + const tab = page.locator(`[data-slot="titlebar-tabs"] a[href="${href}"]`).first() + await expect(tab).toBeVisible() + await tab.click() + await expectSessionTitle(page, title) +} + +function sampleDock(page: Page, duration: number) { + return page.evaluate(async (duration) => { + const samples: { present: boolean; height: number; opacity: number }[] = [] + const start = performance.now() + while (performance.now() - start < duration) { + const dock = document.querySelector('[data-component="session-todo-dock"]') + const clip = dock?.parentElement?.parentElement + const label = dock?.querySelector('[data-action="session-todo-toggle"] span[aria-label]') + samples.push({ + present: !!dock, + height: clip?.getBoundingClientRect().height ?? 0, + opacity: label ? Number.parseFloat(getComputedStyle(label).opacity) : 0, + }) + await new Promise(requestAnimationFrame) + } + return samples + }, duration) +} diff --git a/packages/app/e2e/regression/subagent-child-navigation.spec.ts b/packages/app/e2e/regression/subagent-child-navigation.spec.ts index 19d2c29af0..019cc156ec 100644 --- a/packages/app/e2e/regression/subagent-child-navigation.spec.ts +++ b/packages/app/e2e/regression/subagent-child-navigation.spec.ts @@ -1,6 +1,6 @@ import { base64Encode } from "@opencode-ai/core/util/encode" import { expect, test, type Page } from "@playwright/test" -import { mockOpenCodeServer } from "../utils/mock-server" +import { currentSession, mockOpenCodeServer } from "../utils/mock-server" import { expectSessionTitle } from "../utils/waits" const directory = "C:/OpenCode/SubagentNavigation" @@ -72,16 +72,19 @@ async function setup(page: Page, events?: () => EventPayload[]) { events, eventRetry: events ? 16 : undefined, }) - // The child session resolves via /session/:id but is absent from the /session list, + // The child session resolves by ID but is absent from the session list, // matching a subagent session that has not been loaded into the list cache yet. await page.route( - (url) => url.pathname === "/session" && url.port === (process.env.PLAYWRIGHT_SERVER_PORT ?? "4096"), + (url) => url.pathname === "/api/session" && url.port === (process.env.PLAYWRIGHT_SERVER_PORT ?? "4096"), (route) => route.fulfill({ status: 200, contentType: "application/json", headers: { "access-control-allow-origin": "*" }, - body: JSON.stringify([session(parentID, parentTitle, 1700000000000)]), + body: JSON.stringify({ + data: [currentSession(session(parentID, parentTitle, 1700000000000))], + cursor: {}, + }), }), ) await configurePage(page) diff --git a/packages/app/e2e/regression/tab-navigate-mousedown.spec.ts b/packages/app/e2e/regression/tab-navigate-mousedown.spec.ts index 6bc417af80..b969b590d8 100644 --- a/packages/app/e2e/regression/tab-navigate-mousedown.spec.ts +++ b/packages/app/e2e/regression/tab-navigate-mousedown.spec.ts @@ -1,9 +1,12 @@ import { expect, test, type Page, type Route } from "@playwright/test" import { base64Encode } from "@opencode-ai/core/util/encode" +import { currentSession } from "../utils/mock-server" const server = "http://127.0.0.1:4096" const sessionA = session("ses_tab_a", "Tab A session") const sessionB = session("ses_tab_b", "Tab B session") +const sessionC = session("ses_tab_c", "Tab C session") +const unresolvedSessionID = "ses_tab_unresolved" test("pressing mouse down on a tab navigates before mouse up", async ({ page }) => { await mockServer(page) @@ -39,6 +42,34 @@ test("pressing mouse down on a tab navigates before mouse up", async ({ page }) await expect(page).toHaveURL(new RegExp(`${hrefB.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}$`)) }) +test("keyboard navigation follows the visible tab order", async ({ page }) => { + await mockServer(page) + await page.addInitScript( + ({ server, sessionA, unresolved, sessionC }) => { + localStorage.setItem("settings.v3", JSON.stringify({ general: { newLayoutDesigns: true } })) + localStorage.setItem( + "opencode.window.browser.dat:tabs", + JSON.stringify([ + { type: "session", server, sessionId: sessionA }, + { type: "session", server, sessionId: unresolved }, + { type: "session", server, sessionId: sessionC }, + ]), + ) + }, + { server, sessionA: sessionA.id, unresolved: unresolvedSessionID, sessionC: sessionC.id }, + ) + + const hrefA = `/server/${base64Encode(server)}/session/${sessionA.id}` + const hrefC = `/server/${base64Encode(server)}/session/${sessionC.id}` + await page.goto(hrefA) + await expect(page.locator("[data-titlebar-tab-slot]:visible")).toHaveCount(2) + await expect(page.locator(`[data-titlebar-tab-slot]:has(a[href="${hrefC}"])`)).toBeVisible() + + await page.keyboard.press("Control+Alt+ArrowRight") + + await expect(page).toHaveURL(new RegExp(`${hrefC.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}$`)) +}) + function session(id: string, title: string) { return { id, @@ -52,22 +83,29 @@ function session(id: string, title: string) { } async function mockServer(page: Page) { - const sessions = [sessionA, sessionB] + const sessions = [sessionA, sessionB, sessionC] await page.route("**/*", async (route) => { const url = new URL(route.request().url()) if (url.origin !== server) return route.fallback() - if (url.pathname === "/global/event" || url.pathname === "/event") return sse(route) + if ([`/api/session/${unresolvedSessionID}`, `/session/${unresolvedSessionID}`].includes(url.pathname)) + return new Promise(() => {}) + if (url.pathname === "/global/event" || url.pathname === "/event" || url.pathname === "/api/event") + return sse(route) if (url.pathname === "/global/health") return json(route, { healthy: true }) - if (url.pathname === "/session") return json(route, sessions) + if (url.pathname === "/api/session") return json(route, { data: sessions.map(currentSession), cursor: {} }) + if (url.pathname === "/api/session/active") return json(route, { data: {} }) + const currentSessionInfo = sessions.find((item) => url.pathname === `/api/session/${item.id}`) + if (currentSessionInfo) return json(route, { data: currentSession(currentSessionInfo) }) + if (sessions.some((item) => url.pathname === `/api/session/${item.id}/message`)) + return json(route, { data: [], cursor: {} }) const byId = sessions.find((item) => url.pathname === `/session/${item.id}`) if (byId) return json(route, byId) if (/^\/session\/[^/]+$/.test(url.pathname)) return json(route, { name: "NotFoundError" }, 404) if (/^\/session\/[^/]+\/message$/.test(url.pathname)) return json(route, []) - if (/^\/session\/[^/]+\/(children|diff)$/.test(url.pathname)) return json(route, []) + if (/^\/session\/[^/]+\/(children|todo|diff)$/.test(url.pathname)) return json(route, []) if (["/skill", "/command", "/lsp", "/formatter", "/permission", "/question", "/vcs/diff"].includes(url.pathname)) return json(route, []) - if (["/global/config", "/config", "/provider/auth", "/mcp", "/session/status"].includes(url.pathname)) - return json(route, {}) + if (["/global/config", "/config", "/provider/auth", "/mcp"].includes(url.pathname)) return json(route, {}) if (url.pathname === "/provider") return json(route, { all: [], connected: [], default: { providerID: "", modelID: "" } }) if (url.pathname === "/agent") return json(route, [{ name: "build", mode: "primary" }]) @@ -89,7 +127,20 @@ async function mockServer(page: Page) { directory: sessionA.directory, home: sessionA.directory, }) + if (url.pathname === "/api/path") + return json(route, { + state: sessionA.directory, + config: sessionA.directory, + worktree: sessionA.directory, + directory: sessionA.directory, + home: sessionA.directory, + }) if (url.pathname === "/vcs") return json(route, { branch: "main", default_branch: "main" }) + if (url.pathname === "/api/vcs") + return json(route, { + location: { directory: sessionA.directory }, + data: { branch: "main", defaultBranch: "main" }, + }) return json(route, {}) }) } diff --git a/packages/app/e2e/regression/terminal-composer-focus.spec.ts b/packages/app/e2e/regression/terminal-composer-focus.spec.ts index 2c2801d4b5..99bf689085 100644 --- a/packages/app/e2e/regression/terminal-composer-focus.spec.ts +++ b/packages/app/e2e/regression/terminal-composer-focus.spec.ts @@ -1,5 +1,5 @@ import { base64Encode } from "@opencode-ai/core/util/encode" -import { expect, test } from "@playwright/test" +import { expect, test, type Page } from "@playwright/test" import { mockOpenCodeServer } from "../utils/mock-server" import { expectSessionTitle } from "../utils/waits" @@ -7,11 +7,13 @@ const directory = "C:/OpenCode/TerminalComposerFocus" const projectID = "proj_terminal_composer_focus" const sessionID = "ses_terminal_composer_focus" const ptyID = "pty_terminal_composer_focus" +const newPtyID = "pty_terminal_composer_focus_new" test.use({ viewport: { width: 1440, height: 900 } }) -test("routes typing to the composer unless the open terminal is focused", async ({ page }) => { +test.beforeEach(async ({ page }) => { await mockOpenCodeServer(page, { + protocol: "v2", directory, project: { id: projectID, @@ -45,29 +47,36 @@ test("routes typing to the composer unless the open terminal is focused", async ], pageMessages: () => ({ items: [] }), }) - await page.route("**/pty", (route) => + await page.route("**/api/pty*", (route) => { + expect(new URL(route.request().url()).searchParams.get("location[directory]")).toBe(directory) + return route.fulfill({ + status: 200, + contentType: "application/json", + body: JSON.stringify({ location: ptyLocation(), data: ptyInfo(ptyID, "Terminal 1") }), + }) + }) + await page.route(`**/api/pty/${ptyID}*`, (route) => route.fulfill({ status: 200, contentType: "application/json", - body: JSON.stringify({ id: ptyID, title: "Terminal 1" }), + body: JSON.stringify({ location: ptyLocation(), data: ptyInfo(ptyID, "Terminal 1") }), }), ) - await page.route(`**/pty/${ptyID}`, (route) => - route.fulfill({ status: 200, contentType: "application/json", body: "{}" }), - ) - await page.route(`**/pty/${ptyID}/connect-token*`, (route) => + await page.route(`**/api/pty/${ptyID}/connect-token*`, (route) => route.fulfill({ status: 200, contentType: "application/json", headers: { "access-control-allow-origin": "*" }, - body: JSON.stringify({ ticket: "e2e-ticket" }), + body: JSON.stringify({ location: ptyLocation(), data: { ticket: "e2e-ticket", expires_in: 60 } }), }), ) - await page.routeWebSocket(new RegExp(`/pty/${ptyID}/connect`), () => undefined) + await page.routeWebSocket(new RegExp(`/api/pty/${ptyID}/connect`), () => undefined) await page.addInitScript(() => { localStorage.setItem("settings.v3", JSON.stringify({ general: { newLayoutDesigns: true } })) }) +}) +test("routes typing to the composer unless the open terminal is focused", async ({ page }) => { await page.goto(`/${base64Encode(directory)}/session/${sessionID}`) await expectSessionTitle(page, "Terminal composer focus") @@ -87,3 +96,132 @@ test("routes typing to the composer unless the open terminal is focused", async await expect(composer).toBeFocused() await expect(composer).toHaveText("a") }) + +test("keeps composer focus when a cached terminal finishes mounting", async ({ page }) => { + const ghostty = Promise.withResolvers() + const release = Promise.withResolvers() + const created = { count: 0 } + await page.route("**/api/pty*", (route) => { + created.count += 1 + return route.fulfill({ + status: 200, + contentType: "application/json", + body: JSON.stringify({ location: ptyLocation(), data: ptyInfo(ptyID, "Terminal 1") }), + }) + }) + await page.route(/ghostty-web/, async (route) => { + ghostty.resolve() + await release.promise + await route.continue() + }) + await seedCachedTerminal(page) + + await page.goto(`/${base64Encode(directory)}/session/${sessionID}`, { waitUntil: "commit" }) + await expectSessionTitle(page, "Terminal composer focus") + + const composer = page.locator('[data-component="prompt-input"]') + const terminal = page.locator('[data-component="terminal"]') + await expect(terminal).toBeVisible() + expect(created.count).toBe(0) + await ghostty.promise + await composer.click() + await expect(composer).toBeFocused() + + release.resolve() + await expect(terminal.locator("textarea")).toHaveCount(1) + await page.waitForTimeout(300) + await expect(composer).toBeFocused() +}) + +test("keeps newer composer focus while an explicit terminal open finishes", async ({ page }) => { + const ghostty = Promise.withResolvers() + const release = Promise.withResolvers() + await page.route(/ghostty-web/, async (route) => { + ghostty.resolve() + await release.promise + await route.continue() + }) + + await page.goto(`/${base64Encode(directory)}/session/${sessionID}`) + await expectSessionTitle(page, "Terminal composer focus") + + const composer = page.locator('[data-component="prompt-input"]') + const terminal = page.locator('[data-component="terminal"]') + await page.keyboard.press("Control+Backquote") + await expect(terminal).toBeVisible() + await ghostty.promise + await composer.click() + await expect(composer).toBeFocused() + + release.resolve() + await expect(terminal.locator("textarea")).toHaveCount(1) + await page.waitForTimeout(50) + await expect(composer).toBeFocused() +}) + +test("focuses a terminal created from the new-terminal button", async ({ page }) => { + const created = { count: 0 } + await page.route("**/api/pty*", (route) => { + created.count += 1 + const next = created.count === 1 ? ptyInfo(ptyID, "Terminal 1") : ptyInfo(newPtyID, "Terminal 2") + return route.fulfill({ + status: 200, + contentType: "application/json", + body: JSON.stringify({ location: ptyLocation(), data: next }), + }) + }) + await page.route(`**/api/pty/${newPtyID}*`, (route) => + route.fulfill({ + status: 200, + contentType: "application/json", + body: JSON.stringify({ location: ptyLocation(), data: ptyInfo(newPtyID, "Terminal 2") }), + }), + ) + await page.route(`**/api/pty/${newPtyID}/connect-token*`, (route) => + route.fulfill({ + status: 200, + contentType: "application/json", + headers: { "access-control-allow-origin": "*" }, + body: JSON.stringify({ location: ptyLocation(), data: { ticket: "e2e-ticket", expires_in: 60 } }), + }), + ) + await page.routeWebSocket(new RegExp(`/api/pty/${newPtyID}/connect`), () => undefined) + + await page.goto(`/${base64Encode(directory)}/session/${sessionID}`) + await expectSessionTitle(page, "Terminal composer focus") + + const composer = page.locator('[data-component="prompt-input"]') + const terminal = page.locator('[data-component="terminal"]') + await page.keyboard.press("Control+Backquote") + await expect(terminal.locator("textarea")).toHaveCount(1) + await composer.click() + await expect(composer).toBeFocused() + + await page.getByRole("button", { name: "New terminal" }).click() + await expect(page.getByRole("tab", { name: "Terminal 2" })).toHaveAttribute("aria-selected", "true") + await expect.poll(() => terminal.evaluate((element) => element.contains(document.activeElement))).toBe(true) +}) + +function seedCachedTerminal(page: Page) { + return page.addInitScript( + ({ terminalKey, ptyID }) => { + localStorage.setItem("opencode.global.dat:layout", JSON.stringify({ terminal: { height: 320, opened: true } })) + localStorage.setItem( + terminalKey, + JSON.stringify({ + active: ptyID, + all: [{ id: ptyID, title: "Terminal 1", titleNumber: 1 }], + }), + ) + }, + { terminalKey: `${base64Encode(directory)}/terminal.v1`, ptyID }, + ) +} + +function ptyLocation() { + return { directory, project: { id: projectID, directory } } +} + +function ptyInfo(id: string, title: string) { + return { id, title, command: "cmd.exe", args: [], cwd: directory, status: "running", pid: 1 } +} diff --git a/packages/app/e2e/regression/terminal-hidden.spec.ts b/packages/app/e2e/regression/terminal-hidden.spec.ts index 73821580af..8e08d60ff2 100644 --- a/packages/app/e2e/regression/terminal-hidden.spec.ts +++ b/packages/app/e2e/regression/terminal-hidden.spec.ts @@ -10,6 +10,7 @@ const title = "Hidden terminal regression" test("unmounts the terminal panel while it is hidden", async ({ page }) => { await page.setViewportSize({ width: 1400, height: 900 }) await mockOpenCodeServer(page, { + protocol: "v2", directory, project: { id: projectID, @@ -43,17 +44,53 @@ test("unmounts the terminal panel while it is hidden", async ({ page }) => { ], pageMessages: () => ({ items: [] }), }) - await page.route("**/pty", (route) => + await page.route("**/api/pty*", (route) => route.fulfill({ status: 200, contentType: "application/json", - body: JSON.stringify({ id: "pty_hidden_terminal", title: "Terminal 1" }), + body: JSON.stringify({ + location: { directory, project: { id: projectID, directory } }, + data: { + id: "pty_hidden_terminal", + title: "Terminal 1", + command: "cmd.exe", + args: [], + cwd: directory, + status: "running", + pid: 1, + }, + }), }), ) - await page.route("**/pty/pty_hidden_terminal", (route) => - route.fulfill({ status: 200, contentType: "application/json", body: "{}" }), + await page.route("**/api/pty/pty_hidden_terminal*", (route) => + route.fulfill({ + status: 200, + contentType: "application/json", + body: JSON.stringify({ + location: { directory, project: { id: projectID, directory } }, + data: { + id: "pty_hidden_terminal", + title: "Terminal 1", + command: "cmd.exe", + args: [], + cwd: directory, + status: "running", + pid: 1, + }, + }), + }), ) - await page.routeWebSocket("**/pty/pty_hidden_terminal/connect", () => undefined) + await page.route("**/api/pty/pty_hidden_terminal/connect-token*", (route) => + route.fulfill({ + status: 200, + contentType: "application/json", + body: JSON.stringify({ + location: { directory, project: { id: projectID, directory } }, + data: { ticket: "e2e-ticket", expires_in: 60 }, + }), + }), + ) + await page.routeWebSocket("**/api/pty/pty_hidden_terminal/connect", () => undefined) await page.goto(`/${base64Encode(directory)}/session/${sessionID}`) await expectSessionTitle(page, title) diff --git a/packages/app/e2e/regression/terminal-tab-switch.spec.ts b/packages/app/e2e/regression/terminal-tab-switch.spec.ts index cbb72958ad..165920753c 100644 --- a/packages/app/e2e/regression/terminal-tab-switch.spec.ts +++ b/packages/app/e2e/regression/terminal-tab-switch.spec.ts @@ -29,6 +29,10 @@ test("keeps the terminal session alive when switching session tabs in a workspac const terminal = page.locator('[data-component="terminal"]') await expect(terminal).toBeVisible() await expect.poll(() => connections.length).toBe(1) + const connection = new URL(connections[0]!) + expect(connection.pathname).toBe(`/api/pty/${ptyID}/connect`) + expect(connection.searchParams.get("location[directory]")).toBe(directory) + expect(connection.searchParams.get("ticket")).toBeNull() await writeProbe(page) await switchTab(page, titleB) @@ -62,6 +66,7 @@ async function readProbe(page: Page) { async function setup(page: Page) { await mockOpenCodeServer(page, { + protocol: "v2", directory, project: { id: projectID, @@ -85,26 +90,33 @@ async function setup(page: Page) { sessions: [session(sessionA, titleA, 1700000000000), session(sessionB, titleB, 1700000001000)], pageMessages: () => ({ items: [] }), }) - await page.route("**/pty", (route) => + await page.route("**/api/pty*", (route) => route.fulfill({ status: 200, contentType: "application/json", - body: JSON.stringify({ id: ptyID, title: "Terminal 1" }), + body: JSON.stringify({ location: ptyLocation(), data: ptyInfo() }), }), ) - await page.route(`**/pty/${ptyID}`, (route) => - route.fulfill({ status: 200, contentType: "application/json", body: "{}" }), - ) - await page.route(`**/pty/${ptyID}/connect-token*`, (route) => + await page.route(`**/api/pty/${ptyID}*`, (route) => route.fulfill({ + status: 200, + contentType: "application/json", + body: JSON.stringify({ location: ptyLocation(), data: ptyInfo() }), + }), + ) + await page.route(`**/api/pty/${ptyID}/connect-token*`, (route) => { + expect(route.request().headers()["x-opencode-ticket"]).toBe("1") + const url = new URL(route.request().url()) + expect(url.searchParams.get("location[directory]")).toBe(directory) + return route.fulfill({ status: 200, contentType: "application/json", headers: { "access-control-allow-origin": "*" }, - body: JSON.stringify({ ticket: "e2e-ticket" }), - }), - ) + body: JSON.stringify({ location: ptyLocation(), data: { ticket: "e2e-ticket", expires_in: 60 } }), + }) + }) const connections: string[] = [] - await page.routeWebSocket(new RegExp(`/pty/${ptyID}/connect`), (ws) => { + await page.routeWebSocket(new RegExp(`/api/pty/${ptyID}/connect`), (ws) => { connections.push(ws.url()) }) @@ -143,3 +155,11 @@ function session(id: string, title: string, created: number) { function sessionHref(sessionID: string) { return `/server/${base64Encode(server)}/session/${sessionID}` } + +function ptyLocation() { + return { directory, project: { id: projectID, directory } } +} + +function ptyInfo() { + return { id: ptyID, title: "Terminal 1", command: "cmd.exe", args: [], cwd: directory, status: "running", pid: 1 } +} diff --git a/packages/app/e2e/reproduction/timeline-suspense/index.html b/packages/app/e2e/reproduction/timeline-suspense/index.html new file mode 100644 index 0000000000..995008a9f7 --- /dev/null +++ b/packages/app/e2e/reproduction/timeline-suspense/index.html @@ -0,0 +1,34 @@ + + + + + + Timeline Suspense Reproduction + + + +
+ + + diff --git a/packages/app/e2e/reproduction/timeline-suspense/main.tsx b/packages/app/e2e/reproduction/timeline-suspense/main.tsx new file mode 100644 index 0000000000..cdcd809be3 --- /dev/null +++ b/packages/app/e2e/reproduction/timeline-suspense/main.tsx @@ -0,0 +1,315 @@ +import { createResource, createSignal, For, onMount, Suspense } from "solid-js" +import { render } from "solid-js/web" +import { createVirtualizer, observeElementOffset, observeElementRect } from "@tanstack/solid-virtual" +import { observeElementOffsetReconnectAware } from "../../../src/pages/session/timeline/observe-element-offset" + +const rowCount = 2_000 +const rowHeight = 40 +const parameters = new URLSearchParams(location.search) +const resourceMode = parameters.get("resource") === "guard" ? "guard" : "baseline" +const reconnectMode = parameters.get("reconnect") === "candidate" ? "candidate" : "baseline" + +type MutationEvent = { + kind: "removed" | "added" + callbackTime: number + callbackFrame: number + routeConnectedInCallback: boolean + nativeOffsetInCallback: number +} + +type Snapshot = { + mode: { + resource: "baseline" | "guard" + reconnect: "baseline" | "candidate" + } + operation: { + sequence: number + phase: string + time: number + frame: number + } + resourceState: string + routeConnected: boolean + viewportConnected: boolean + viewportOwnedByRoute: boolean + sameRoute: boolean + sameViewport: boolean + sameSurface: boolean + sameMountedRows: boolean + nativeOffset: number + coreOffset: number + rangeStart: number + rangeEnd: number + indexes: number[] + domIndexes: number[] + logicalSurfaceHeight: number + renderedSurfaceHeight: number + viewportClientHeight: number + viewportScrollHeight: number + visibleRows: number + minimumRowTop: number + domScrollEvents: number + lastScrollTrusted: boolean + coreOffsetCallbackCalls: number + offsetCallbackSources: "observer"[] + rectObserverCallbacks: number + ignoredDetachedZeroRects: number + syntheticScrollDispatches: number + mutationEvents: MutationEvent[] +} + +declare global { + interface Window { + timelineSuspense: { + prepare: () => Promise + trigger: () => void + resolve: () => void + frames: (count?: number) => Promise + snapshot: () => Snapshot + } + } +} + +function App() { + const [refresh, setRefresh] = createSignal(false) + let resolveResource: (() => void) | undefined + const [resource] = createResource( + refresh, + (version) => + new Promise((resolve) => { + resolveResource = () => resolve(`settled-${version}`) + }), + { initialValue: "settled" }, + ) + + function Route() { + let route: HTMLElement | undefined + let viewport: HTMLDivElement | undefined + let surface: HTMLDivElement | undefined + let initialRoute: HTMLElement | undefined + let initialViewport: HTMLDivElement | undefined + let initialSurface: HTMLDivElement | undefined + let initialRows: HTMLElement[] = [] + let phase = "mounting" + let browserFrame = 0 + let snapshotSequence = 0 + let domScrollEvents = 0 + let lastScrollTrusted = false + let coreOffsetCallbackCalls = 0 + let rectObserverCallbacks = 0 + let ignoredDetachedZeroRects = 0 + const offsetCallbackSources: "observer"[] = [] + const mutationEvents: MutationEvent[] = [] + const virtualizer = createVirtualizer({ + count: rowCount, + getScrollElement: () => viewport ?? null, + estimateSize: () => rowHeight, + initialRect: { width: 900, height: 600 }, + overscan: 2, + observeElementRect: (instance, callback) => + observeElementRect(instance, (rect) => { + rectObserverCallbacks++ + // A fixed 600px viewport has no usable geometry while detached. Keep the last connected rect. + if (!instance.scrollElement?.isConnected && rect.height === 0) { + ignoredDetachedZeroRects++ + return + } + callback(rect) + }), + observeElementOffset: (instance, callback) => { + const deliver = (offset: number, isScrolling: boolean) => { + coreOffsetCallbackCalls++ + offsetCallbackSources.push("observer") + callback(offset, isScrolling) + } + if (reconnectMode === "candidate") return observeElementOffsetReconnectAware(instance, deliver) + return observeElementOffset(instance, deliver) + }, + }) + + const frames = async (count = 2) => { + for (let index = 0; index < count; index++) { + await new Promise((resolve) => requestAnimationFrame(() => resolve())) + } + } + const mountedRows = () => [...(surface?.querySelectorAll("[data-row-index]") ?? [])] + const snapshot = (): Snapshot => { + const rows = mountedRows() + const view = viewport?.getBoundingClientRect() + const visibleRows = + viewport?.isConnected && view + ? rows.filter((row) => { + const rect = row.getBoundingClientRect() + return rect.bottom > view.top && rect.top < view.bottom + }).length + : 0 + return { + mode: { resource: resourceMode, reconnect: reconnectMode }, + operation: { + sequence: ++snapshotSequence, + phase, + time: performance.now(), + frame: browserFrame, + }, + resourceState: resource.state, + routeConnected: route?.isConnected ?? false, + viewportConnected: viewport?.isConnected ?? false, + viewportOwnedByRoute: !!route && !!viewport && route.contains(viewport), + sameRoute: route === initialRoute, + sameViewport: viewport === initialViewport, + sameSurface: surface === initialSurface, + sameMountedRows: + initialRows.length > 0 && + initialRows.length === rows.length && + initialRows.every((row, index) => row === rows[index]), + nativeOffset: viewport?.scrollTop ?? -1, + coreOffset: virtualizer.scrollOffset ?? -1, + rangeStart: virtualizer.range?.startIndex ?? -1, + rangeEnd: virtualizer.range?.endIndex ?? -1, + indexes: virtualizer.getVirtualItems().map((item) => item.index), + domIndexes: rows.map((row) => Number(row.dataset.rowIndex)), + logicalSurfaceHeight: Number.parseFloat(surface?.style.height ?? "-1"), + renderedSurfaceHeight: surface?.getBoundingClientRect().height ?? -1, + viewportClientHeight: viewport?.clientHeight ?? -1, + viewportScrollHeight: viewport?.scrollHeight ?? -1, + visibleRows, + minimumRowTop: + rows.length && view ? Math.min(...rows.map((row) => row.getBoundingClientRect().top - view.top)) : -1, + domScrollEvents, + lastScrollTrusted, + coreOffsetCallbackCalls, + offsetCallbackSources: [...offsetCallbackSources], + rectObserverCallbacks, + ignoredDetachedZeroRects, + syntheticScrollDispatches: 0, + mutationEvents: mutationEvents.map((event) => ({ ...event })), + } + } + + onMount(() => { + if (!route || !viewport || !surface) throw new Error("Timeline fixture did not mount") + const routeRoot = route.parentElement + if (!routeRoot) throw new Error("Timeline route root did not mount") + initialRoute = route + initialViewport = viewport + initialSurface = surface + viewport.addEventListener("scroll", (event) => { + domScrollEvents++ + lastScrollTrusted = event.isTrusted + }) + const countFrames = () => { + browserFrame++ + requestAnimationFrame(countFrames) + } + requestAnimationFrame(countFrames) + new MutationObserver((records) => { + const callbackTime = performance.now() + records.forEach((record) => { + ;([...(record.removedNodes ?? [])] as Node[]).forEach((node) => { + if (node !== route) return + phase = "detached" + mutationEvents.push({ + kind: "removed", + callbackTime, + callbackFrame: browserFrame, + routeConnectedInCallback: route.isConnected, + nativeOffsetInCallback: viewport.scrollTop, + }) + }) + ;([...(record.addedNodes ?? [])] as Node[]).forEach((node) => { + if (node !== route) return + phase = "reinserted" + mutationEvents.push({ + kind: "added", + callbackTime, + callbackFrame: browserFrame, + routeConnectedInCallback: route.isConnected, + nativeOffsetInCallback: viewport.scrollTop, + }) + }) + }) + }).observe(routeRoot, { childList: true }) + window.timelineSuspense = { + prepare: async () => { + phase = "preparing" + await frames(2) + viewport.scrollTop = viewport.scrollHeight + await frames(3) + await new Promise((resolve) => setTimeout(resolve, 200)) + await frames(2) + initialRows = mountedRows() + phase = "prepared" + return snapshot() + }, + trigger: () => { + phase = "triggering" + setRefresh(true) + }, + resolve: () => { + if (!resolveResource) throw new Error("Resource is not pending") + phase = "resolving" + resolveResource() + }, + frames, + snapshot, + } + }) + + return ( +
+ +
+
+ + {(item) => ( +
+ logical row {item.index} +
+ )} +
+
+
+
+ ) + } + + return ( +
+ + + +
+ ) +} + +render(() => , document.getElementById("root")!) diff --git a/packages/app/e2e/reproduction/timeline-suspense/playwright.config.ts b/packages/app/e2e/reproduction/timeline-suspense/playwright.config.ts new file mode 100644 index 0000000000..7fff1737ae --- /dev/null +++ b/packages/app/e2e/reproduction/timeline-suspense/playwright.config.ts @@ -0,0 +1,34 @@ +import { defineConfig, devices } from "@playwright/test" + +const port = Number(process.env.PLAYWRIGHT_TIMELINE_SUSPENSE_PORT ?? 4317) + +export default defineConfig({ + testDir: ".", + testMatch: "timeline-suspense.repro.ts", + outputDir: "../../test-results/timeline-suspense", + fullyParallel: false, + workers: 1, + retries: 0, + reporter: "line", + timeout: 30_000, + expect: { + timeout: 10_000, + }, + webServer: { + command: `bunx vite --config vite.config.ts --host 127.0.0.1 --port ${port} --strictPort`, + cwd: import.meta.dirname, + url: `http://127.0.0.1:${port}`, + reuseExistingServer: false, + }, + use: { + baseURL: `http://127.0.0.1:${port}`, + trace: "retain-on-failure", + screenshot: "only-on-failure", + }, + projects: [ + { + name: "chromium", + use: { ...devices["Desktop Chrome"] }, + }, + ], +}) diff --git a/packages/app/e2e/reproduction/timeline-suspense/timeline-suspense.repro.ts b/packages/app/e2e/reproduction/timeline-suspense/timeline-suspense.repro.ts new file mode 100644 index 0000000000..3edae33a9b --- /dev/null +++ b/packages/app/e2e/reproduction/timeline-suspense/timeline-suspense.repro.ts @@ -0,0 +1,179 @@ +import { expect, test, type Page } from "@playwright/test" + +test.beforeEach(async ({ page }) => { + page.on("pageerror", (error) => console.error(error)) + await page.goto("/") + await expect.poll(() => page.evaluate(() => !!window.timelineSuspense)).toBe(true) +}) + +test("desired: preserves visible timeline continuity across descendant resource suspension", async ({ page }) => { + await page.goto("/?reconnect=candidate") + await expect.poll(() => page.evaluate(() => window.timelineSuspense.snapshot().mode.reconnect)).toBe("candidate") + const before = await prepare(page) + await triggerBaselineSuspension(page) + const pending = await page.evaluate(() => window.timelineSuspense.snapshot()) + expect(pending.nativeOffset).toBe(0) + expect(pending.coreOffset).toBe(before.coreOffset) + expect(pending.indexes).toEqual(before.indexes) + expect(pending.sameRoute).toBe(true) + expect(pending.sameViewport).toBe(true) + expect(pending.sameSurface).toBe(true) + expect(pending.sameMountedRows).toBe(true) + + await resolveSuspension(page) + await expect.poll(() => page.evaluate(() => window.timelineSuspense.snapshot().coreOffset)).toBe(0) + await page.waitForTimeout(250) + await page.evaluate(() => window.timelineSuspense.frames(2)) + const after = await page.evaluate(() => window.timelineSuspense.snapshot()) + expect(after.sameRoute).toBe(true) + expect(after.sameViewport).toBe(true) + expect(after.sameSurface).toBe(true) + expect(after.nativeOffset).toBe(0) + expect(after.coreOffset).toBe(0) + expect(after.rangeStart).toBeLessThan(10) + expect(after.visibleRows, diagnostic({ before, pending, after })).toBeGreaterThan(0) + expect(after.domScrollEvents).toBe(before.domScrollEvents) + expect(after.coreOffsetCallbackCalls).toBe(before.coreOffsetCallbackCalls + 1) + expect(after.offsetCallbackSources.at(-1)).toBe("observer") + expect(after.syntheticScrollDispatches).toBe(0) +}) + +test("forensic: proves detached same-node viewport leaves TanStack's bottom range blank until a real scroll", async ({ + page, +}) => { + const before = await prepare(page) + const beforeRows = before.domIndexes + expect(before.mode).toEqual({ resource: "baseline", reconnect: "baseline" }) + expect(before.logicalSurfaceHeight).toBe(80_000) + expect(before.renderedSurfaceHeight).toBe(80_000) + expect(before.viewportClientHeight).toBe(600) + expect(before.viewportScrollHeight).toBe(80_000) + expect(before.rangeStart).toBeGreaterThan(1_900) + expect(before.nativeOffset).toBe(before.coreOffset) + expect(before.visibleRows).toBeGreaterThan(0) + + await triggerBaselineSuspension(page) + const pending = await page.evaluate(() => window.timelineSuspense.snapshot()) + expect(pending.resourceState).toBe("refreshing") + expect(pending.routeConnected).toBe(false) + expect(pending.viewportConnected).toBe(false) + expect(pending.viewportOwnedByRoute).toBe(true) + expect(pending.nativeOffset).toBe(0) + expect(pending.coreOffset).toBe(before.coreOffset) + expect(pending.rangeStart).toBe(before.rangeStart) + expect(pending.rangeEnd).toBe(before.rangeEnd) + expect(pending.indexes).toEqual(before.indexes) + expect(pending.domIndexes).toEqual(beforeRows) + expect(pending.sameMountedRows).toBe(true) + expect(pending.domScrollEvents).toBe(before.domScrollEvents) + expect(pending.coreOffsetCallbackCalls).toBe(before.coreOffsetCallbackCalls) + expect(pending.ignoredDetachedZeroRects).toBeGreaterThan(before.ignoredDetachedZeroRects) + expect(pending.mutationEvents).toHaveLength(1) + expect(pending.mutationEvents[0]).toMatchObject({ + kind: "removed", + routeConnectedInCallback: false, + nativeOffsetInCallback: 0, + }) + expect(pending.mutationEvents[0]!.callbackTime).toBeLessThanOrEqual(pending.operation.time) + expect(pending.mutationEvents[0]!.callbackFrame).toBeLessThanOrEqual(pending.operation.frame) + + const after = await resolveSuspension(page) + expect(after.resourceState).toBe("ready") + expect(after.routeConnected).toBe(true) + expect(after.viewportConnected).toBe(true) + expect(after.viewportOwnedByRoute).toBe(true) + expect(after.sameRoute).toBe(true) + expect(after.sameViewport).toBe(true) + expect(after.sameSurface).toBe(true) + expect(after.sameMountedRows).toBe(true) + expect(after.nativeOffset).toBe(0) + expect(after.coreOffset).toBe(before.coreOffset) + expect(after.rangeStart).toBe(before.rangeStart) + expect(after.rangeEnd).toBe(before.rangeEnd) + expect(after.indexes).toEqual(before.indexes) + expect(after.domIndexes).toEqual(beforeRows) + expect(after.domScrollEvents).toBe(before.domScrollEvents) + expect(after.coreOffsetCallbackCalls).toBe(before.coreOffsetCallbackCalls) + expect(after.mutationEvents).toHaveLength(2) + expect(after.mutationEvents[1]).toMatchObject({ + kind: "added", + routeConnectedInCallback: true, + nativeOffsetInCallback: 0, + }) + expect(after.mutationEvents[1]!.callbackTime).toBeLessThanOrEqual(after.operation.time) + expect(after.mutationEvents[1]!.callbackFrame).toBeLessThanOrEqual(after.operation.frame) + expect(after.visibleRows).toBe(0) + expect(after.minimumRowTop).toBeGreaterThan(50_000) + expect(after.syntheticScrollDispatches).toBe(0) + + await page.locator("[data-viewport]").hover() + await page.mouse.wheel(0, 80) + await expect + .poll(() => + page.evaluate(() => { + const value = window.timelineSuspense.snapshot() + return value.nativeOffset > 0 && value.coreOffset === value.nativeOffset + }), + ) + .toBe(true) + await page.evaluate(() => window.timelineSuspense.frames(2)) + const recovered = await page.evaluate(() => window.timelineSuspense.snapshot()) + expect(recovered.domScrollEvents).toBeGreaterThan(after.domScrollEvents) + expect(recovered.coreOffsetCallbackCalls).toBeGreaterThan(after.coreOffsetCallbackCalls) + expect(recovered.offsetCallbackSources.at(-1)).toBe("observer") + expect(recovered.lastScrollTrusted).toBe(true) + expect(recovered.rangeStart).toBeLessThan(10) + expect(recovered.visibleRows).toBeGreaterThan(0) +}) + +test("matrix: fixture-only settled-resource guard keeps the route connected", async ({ page }) => { + await page.goto("/?resource=guard") + await expect.poll(() => page.evaluate(() => window.timelineSuspense.snapshot().mode.resource)).toBe("guard") + const before = await prepare(page) + + await page.evaluate(() => window.timelineSuspense.trigger()) + await expect.poll(() => page.evaluate(() => window.timelineSuspense.snapshot().resourceState)).toBe("refreshing") + await page.evaluate(() => window.timelineSuspense.frames(3)) + const pending = await page.evaluate(() => window.timelineSuspense.snapshot()) + expect(pending.routeConnected).toBe(true) + expect(pending.mutationEvents).toEqual([]) + expect(pending.nativeOffset).toBe(before.nativeOffset) + expect(pending.coreOffset).toBe(before.coreOffset) + expect(pending.visibleRows).toBeGreaterThan(0) + + const after = await resolveSuspension(page) + expect(after.routeConnected).toBe(true) + expect(after.nativeOffset).toBe(before.nativeOffset) + expect(after.coreOffset).toBe(before.coreOffset) + expect(after.visibleRows).toBeGreaterThan(0) +}) + +async function prepare(page: Page) { + const before = await page.evaluate(() => window.timelineSuspense.prepare()) + expect(before.routeConnected).toBe(true) + expect(before.viewportConnected).toBe(true) + expect(before.viewportOwnedByRoute).toBe(true) + expect(before.sameMountedRows).toBe(true) + expect(before.rangeStart).toBeGreaterThan(1_900) + expect(before.nativeOffset).toBe(before.coreOffset) + return before +} + +async function triggerBaselineSuspension(page: Page) { + await page.evaluate(() => window.timelineSuspense.trigger()) + await expect.poll(() => page.evaluate(() => window.timelineSuspense.snapshot().resourceState)).toBe("refreshing") + await expect.poll(() => page.evaluate(() => window.timelineSuspense.snapshot().routeConnected)).toBe(false) + await page.evaluate(() => window.timelineSuspense.frames(3)) +} + +async function resolveSuspension(page: Page) { + await page.evaluate(() => window.timelineSuspense.resolve()) + await expect.poll(() => page.evaluate(() => window.timelineSuspense.snapshot().resourceState)).toBe("ready") + await expect.poll(() => page.evaluate(() => window.timelineSuspense.snapshot().routeConnected)).toBe(true) + await page.evaluate(() => window.timelineSuspense.frames(3)) + return page.evaluate(() => window.timelineSuspense.snapshot()) +} + +function diagnostic(value: unknown) { + return JSON.stringify(value, null, 2) +} diff --git a/packages/app/e2e/reproduction/timeline-suspense/vite.config.ts b/packages/app/e2e/reproduction/timeline-suspense/vite.config.ts new file mode 100644 index 0000000000..efb75ab731 --- /dev/null +++ b/packages/app/e2e/reproduction/timeline-suspense/vite.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from "vite" +import solid from "vite-plugin-solid" + +export default defineConfig({ + root: import.meta.dirname, + plugins: [solid()], +}) diff --git a/packages/app/e2e/smoke/session-timeline.fixture.ts b/packages/app/e2e/smoke/session-timeline.fixture.ts index beb2d7cf75..3dce37cafd 100644 --- a/packages/app/e2e/smoke/session-timeline.fixture.ts +++ b/packages/app/e2e/smoke/session-timeline.fixture.ts @@ -120,7 +120,7 @@ function toolPart( outputLength = 160, ): MessagePart { const metadata = - tool === "patch" + tool === "apply_patch" ? { files: [patchFile(index, "update"), patchFile(index + 1, index % 2 === 0 ? "add" : "delete")] } : tool === "edit" || tool === "write" ? { @@ -199,7 +199,7 @@ function turn(index: number): Message[] { ? [toolPart(index, 7, "write", { filePath: `src/generated/write-${index}.ts`, content: code(index, 28) }, 560)] : []), ...(index % 8 === 0 - ? [toolPart(index, 8, "patch", { files: [`src/generated/patch-${index}.ts`] }, 620)] + ? [toolPart(index, 8, "apply_patch", { files: [`src/generated/patch-${index}.ts`] }, 620)] : []), ...(index % 7 === 0 ? [toolPart(index, 4, "bash", { command: "bun typecheck" }, 620)] : []), ...(index % 10 === 0 ? [toolPart(index, 9, "webfetch", { url: "https://example.com/docs/sample" }, 120)] : []), @@ -229,6 +229,7 @@ const sourceMessages = Array.from({ length: 12 }, (_, index) => [ ]).flat() function renderable(part: MessagePart) { + if (part.type === "tool" && part.tool === "todowrite") return false if (part.type === "text") return !!part.text.trim() if (part.type === "reasoning") return !!part.text.trim() return part.type !== "step-start" && part.type !== "step-finish" && part.type !== "patch" diff --git a/packages/app/e2e/smoke/session-timeline.spec.ts b/packages/app/e2e/smoke/session-timeline.spec.ts index a73cc0ccdd..bdf3f55bdc 100644 --- a/packages/app/e2e/smoke/session-timeline.spec.ts +++ b/packages/app/e2e/smoke/session-timeline.spec.ts @@ -736,5 +736,5 @@ async function switchTitlebarSession(page: Page, sessionID: string, title: strin } async function expectSessionReady(page: Page) { - await expectAppVisible(page.getByRole("textbox", { name: /Ask anything/i })) + await expectAppVisible(page.getByRole("textbox", { name: "Prompt" })) } diff --git a/packages/app/e2e/tsconfig.json b/packages/app/e2e/tsconfig.json index 53aacbda02..4a6046e4fa 100644 --- a/packages/app/e2e/tsconfig.json +++ b/packages/app/e2e/tsconfig.json @@ -10,6 +10,9 @@ "./performance/timeline-stability/fixture.test.ts", "./performance/timeline-stability/fixture.ts", "./performance/unit/visual-stability.test.ts", + "./reproduction/timeline-suspense/**/*.ts", + "./reproduction/timeline-suspense/**/*.tsx", + "../src/pages/session/timeline/observe-element-offset.ts", "./regression/new-session-panel-corner.spec.ts", "./regression/session-timeline-context-resize.spec.ts", "./utils/**/*.ts" diff --git a/packages/app/e2e/user-story/model-selection-flow.spec.ts b/packages/app/e2e/user-story/model-selection-flow.spec.ts new file mode 100644 index 0000000000..22b8bb41fe --- /dev/null +++ b/packages/app/e2e/user-story/model-selection-flow.spec.ts @@ -0,0 +1,97 @@ +import { expect, test } from "@playwright/test" +import { mockOpenCodeServer } from "../utils/mock-server" +import { expectAppVisible } from "../utils/waits" + +const directory = "C:/OpenCode/NewProject" + +test("creates a session in a new project, connects OpenCode Go, and selects its model", async ({ page }) => { + let connectedGo = false + let pendingGo = false + const connections: Array<{ integrationID: string; body: unknown }> = [] + + await mockOpenCodeServer(page, { + directory, + project: { + id: "proj_model_selection_flow", + worktree: directory, + vcs: "git", + name: "NewProject", + time: { created: 1_700_000_000_000, updated: 1_700_000_000_000 }, + sandboxes: [], + }, + provider: () => ({ + all: [ + { + id: "opencode", + name: "OpenCode", + models: { + "free-model": { + id: "free-model", + name: "Free Model", + cost: { input: 0, output: 0 }, + limit: { context: 200_000 }, + }, + }, + }, + { + id: "opencode-go", + name: "OpenCode Go", + models: { + "go-model-1": { + id: "go-model-1", + name: "Go Model 1", + cost: { input: 1, output: 1 }, + limit: { context: 200_000 }, + }, + }, + }, + ], + connected: connectedGo ? ["opencode", "opencode-go"] : ["opencode"], + default: { providerID: "opencode", modelID: "free-model" }, + }), + integrationMethods: { "opencode-go": [{ type: "api", label: "API key" }] }, + onConnectKey: (input) => { + connections.push(input) + if (input.integrationID === "opencode-go") pendingGo = true + }, + onInstanceDispose: () => { + if (pendingGo) connectedGo = true + }, + sessions: [], + pageMessages: () => ({ items: [] }), + fileList: (path) => + path ? [] : [{ name: "NewProject", path: "NewProject", absolute: directory, type: "directory", ignored: false }], + findFiles: () => ["NewProject"], + }) + await page.addInitScript(() => { + localStorage.setItem("settings.v3", JSON.stringify({ general: { newLayoutDesigns: true } })) + localStorage.setItem("opencode.global.dat:server", JSON.stringify({ projects: { local: [] } })) + }) + + await page.goto("/") + const addProject = page.locator('[data-action="home-add-project-row"]') + await expectAppVisible(addProject) + await addProject.click() + await page.locator("[data-directory-path]").click() + + await page.locator('[data-action="home-new-session"]').click() + await expectAppVisible(page.locator('[data-component="prompt-input-v2"]')) + + const modelControl = page.locator('[data-action="prompt-model"]') + await modelControl.click() + await expect(page.locator('[data-section="free-models"]')).toContainText("Free models provided by OpenCode") + + await page.locator('[data-provider-id="opencode-go"]').click() + await page.locator('[data-input="provider-api-key"]').fill("mock-go-api-key") + await page.locator('[data-action="provider-connect-submit"]').click() + await expect(page.locator('[data-component="dialog-v2"]')).toHaveCount(0) + expect(connections).toEqual([{ integrationID: "opencode-go", body: { type: "api", key: "mock-go-api-key" } }]) + + await expect(modelControl).toHaveAttribute("data-control-type", "popover") + await modelControl.click() + const goModel = page.locator('[data-option-key="opencode-go:go-model-1"]') + await expect(goModel).toBeVisible() + await goModel.click() + + await expect(modelControl).toContainText("Go Model 1") +}) diff --git a/packages/app/e2e/utils/mock-server.ts b/packages/app/e2e/utils/mock-server.ts index 34c60ba7f4..76987421b6 100644 --- a/packages/app/e2e/utils/mock-server.ts +++ b/packages/app/e2e/utils/mock-server.ts @@ -4,7 +4,11 @@ const emptyList = new Set(["/skill", "/command", "/lsp", "/formatter", "/vcs/sta const emptyObject = new Set(["/global/config", "/config", "/provider/auth", "/mcp", "/experimental/resource"]) export interface MockServerConfig { - provider: unknown + protocol?: "v1" | "v2" + provider: unknown | (() => unknown) + integrationMethods?: Record + onConnectKey?: (input: { integrationID: string; body: unknown }) => void + onInstanceDispose?: () => void directory: string project: unknown sessions: ({ id: string } & Record)[] @@ -17,19 +21,19 @@ export interface MockServerConfig { onMessage?: (input: { sessionID: string; messageID: string }) => void events?: () => unknown[] eventRetry?: number + todos?: (sessionID: string) => unknown[] permissions?: unknown[] | (() => unknown[]) questions?: unknown[] | (() => unknown[]) fileList?: (path: string) => unknown | Promise fileContent?: (path: string) => unknown | Promise findFiles?: (input: { query: string; dirs?: string; limit?: number }) => unknown - sessionStatus?: unknown + sessionStatus?: Record | (() => Record) } export async function mockOpenCodeServer(page: Page, config: MockServerConfig) { const cursors = new Map() let nextCursor = 0 const staticRoutes: Record = { - "/provider": config.provider, "/path": { state: config.directory, config: config.directory, @@ -53,14 +57,47 @@ export async function mockOpenCodeServer(page: Page, config: MockServerConfig) { if (url.port !== targetPort && url.port !== appPort) return route.fallback() const path = url.pathname - if (path === "/global/event" || path === "/event") return sse(route, config.events?.(), config.eventRetry) - if (path === "/global/health") return json(route, { healthy: true }) + if (path === "/global/event" || path === "/event" || path === "/api/event") { + const events = config.events?.() + return sse( + route, + path === "/api/event" + ? [{ id: "evt_mock_connected", type: "server.connected", data: {} }, ...(events?.map(currentEvent) ?? [])] + : [ + ...(path === "/global/event" + ? [{ payload: { id: "evt_mock_connected", type: "server.connected", properties: {} } }] + : []), + ...(events ?? []), + ], + config.eventRetry, + ) + } + if (path === "/global/health") + return config.protocol === "v2" ? json(route, {}, undefined, 404) : json(route, { healthy: true }) + if (path === "/api/health" && config.protocol === "v2") + return json(route, { healthy: true, version: "2.0.0", pid: 1 }) if (path === "/experimental/capabilities") return json(route, { backgroundSubagents: true }) + if (path === "/provider") + return json(route, typeof config.provider === "function" ? config.provider() : config.provider) + if (path === "/provider/auth") return json(route, config.integrationMethods ?? {}) + const legacyAuth = path.match(/^\/auth\/([^/]+)$/)?.[1] + if (legacyAuth && route.request().method() === "PUT") { + config.onConnectKey?.({ integrationID: legacyAuth, body: route.request().postDataJSON() }) + return json(route, true) + } + if (path === "/instance/dispose" && route.request().method() === "POST") { + config.onInstanceDispose?.() + return json(route, true) + } if (path === "/permission") return json(route, typeof config.permissions === "function" ? config.permissions() : (config.permissions ?? [])) if (path === "/question") return json(route, typeof config.questions === "function" ? config.questions() : (config.questions ?? [])) - if (path === "/session/status") return json(route, config.sessionStatus ?? {}) + if (path === "/session/status") + return json( + route, + typeof config.sessionStatus === "function" ? config.sessionStatus() : (config.sessionStatus ?? {}), + ) if (path === "/vcs/diff" && config.vcsDiff) return json(route, config.vcsDiff) if (path === "/file" && config.fileList) return json(route, await config.fileList(url.searchParams.get("path") ?? "")) @@ -83,10 +120,138 @@ export async function mockOpenCodeServer(page: Page, config: MockServerConfig) { }, data: [], }) + if (path === "/api/agent") + return json(route, { + location: location(config), + data: [ + { + id: "build", + name: "Build", + mode: "primary", + hidden: false, + request: { settings: {}, headers: {}, body: {} }, + permissions: [], + }, + ], + }) + if (path === "/api/command") return json(route, { location: location(config), data: [] }) + if (path === "/api/mcp") return json(route, { location: location(config), data: [] }) + if (path === "/api/mcp/resource") + return json(route, { location: location(config), data: { resources: [], templates: [] } }) + const integration = path.match(/^\/api\/integration\/([^/]+)$/)?.[1] + if (integration && route.request().method() === "GET") + return json(route, { + location: location(config), + data: { id: integration, name: integration, methods: [{ type: "key", label: "API key" }], connections: [] }, + }) + const integrationConnect = path.match(/^\/api\/integration\/([^/]+)\/connect\/key$/)?.[1] + if (integrationConnect && route.request().method() === "POST") { + config.onConnectKey?.({ integrationID: integrationConnect, body: route.request().postDataJSON() }) + return route.fulfill({ status: 204, headers: { "access-control-allow-origin": "*" } }) + } + if (path === "/api/project") return json(route, [config.project]) + if (path === "/api/project/current") + return json(route, { id: (config.project as { id?: string }).id, directory: config.directory }) + if (path.startsWith("/api/project/") && route.request().method() === "PATCH") return json(route, config.project) + if (path === "/api/path") + return json(route, { + state: config.directory, + config: config.directory, + worktree: config.directory, + directory: config.directory, + home: "C:/OpenCode", + }) + if (path === "/api/permission/request") + return json(route, { + location: location(config), + data: (typeof config.permissions === "function" ? config.permissions() : (config.permissions ?? [])).map( + currentPermission, + ), + }) + if (path === "/api/question/request") + return json(route, { + location: location(config), + data: typeof config.questions === "function" ? config.questions() : (config.questions ?? []), + }) + if (path === "/api/vcs") + return json(route, { location: location(config), data: { branch: "main", defaultBranch: "main" } }) + if (path === "/api/vcs/status") return json(route, { location: location(config), data: [] }) + if (path === "/api/vcs/diff") return json(route, { location: location(config), data: config.vcsDiff ?? [] }) + if (path === "/api/pty/shells") return json(route, { location: location(config), data: [] }) + if (/^\/api\/pty\/[^/]+\/connect-token$/.test(path)) + return json(route, { location: location(config), data: { ticket: "e2e-ticket", expires_in: 60 } }) if (emptyObject.has(path)) return json(route, {}) if (emptyList.has(path)) return json(route, []) + if (path === "/api/session") { + const directory = url.searchParams.get("directory") + const parentID = url.searchParams.get("parentID") + const limit = Number(url.searchParams.get("limit") ?? 50) + const offset = Number(url.searchParams.get("cursor") ?? 0) + const sessions = config.sessions + .filter((session) => !directory || session.directory === directory) + .filter((session) => parentID !== "null" || session.parentID === undefined) + .filter((session) => { + const search = url.searchParams.get("search")?.toLowerCase() + return ( + !search || + String(session.title ?? "") + .toLowerCase() + .includes(search) + ) + }) + const ordered = url.searchParams.get("order") === "asc" ? sessions.toReversed() : sessions + const data = ordered.slice(offset, offset + limit) + const next = offset + limit < ordered.length ? String(offset + limit) : undefined + return json(route, { + data: data.map((session) => currentSession(session, config.directory)), + cursor: { next }, + }) + } + if (path === "/api/session/active") { + const statuses = (config.sessionStatus ?? {}) as Record + return json(route, { + data: Object.fromEntries( + Object.entries(statuses).flatMap(([id, status]) => + status.type === "idle" ? [] : [[id, { type: "running" }]], + ), + ), + }) + } + if (/^\/api\/session\/[^/]+\/shell$/.test(path) && route.request().method() === "POST") { + return route.fulfill({ status: 204, headers: { "access-control-allow-origin": "*" } }) + } + if (/^\/api\/session\/[^/]+\/question\/[^/]+\/(reply|reject)$/.test(path) && route.request().method() === "POST") { + return route.fulfill({ status: 204, headers: { "access-control-allow-origin": "*" } }) + } + if (/^\/api\/session\/[^/]+\/permission\/[^/]+\/reply$/.test(path) && route.request().method() === "POST") { + return route.fulfill({ status: 204, headers: { "access-control-allow-origin": "*" } }) + } + if (/^\/question\/[^/]+\/(reply|reject)$/.test(path) && route.request().method() === "POST") { + return json(route, true) + } + if (/^\/session\/[^/]+\/permissions\/[^/]+$/.test(path) && route.request().method() === "POST") { + return json(route, true) + } + if ( + /^\/api\/session\/[^/]+\/(archive|rename|interrupt|revert\/clear|revert\/commit)$/.test(path) && + route.request().method() === "POST" + ) { + return route.fulfill({ status: 204, headers: { "access-control-allow-origin": "*" } }) + } + if (/^\/api\/session\/[^/]+$/.test(path) && route.request().method() === "DELETE") { + return route.fulfill({ status: 204, headers: { "access-control-allow-origin": "*" } }) + } if (path in staticRoutes) return json(route, staticRoutes[path]) + const currentSessionMatch = path.match(/^\/api\/session\/([^/]+)$/) + if (currentSessionMatch) { + const session = config.sessions.find((item) => item.id === currentSessionMatch[1]) + if (!session) return json(route, { error: "Session not found" }, undefined, 404) + return json(route, { + data: currentSession(session, config.directory), + }) + } + const sessionMatch = path.match(/^\/session\/([^/]+)$/) if (sessionMatch) { const session = config.sessions.find((s) => s.id === sessionMatch[1]) @@ -105,8 +270,28 @@ export async function mockOpenCodeServer(page: Page, config: MockServerConfig) { return json(route, message) } + const todoMatch = path.match(/^\/session\/([^/]+)\/todo$/) + if (todoMatch) return json(route, config.todos?.(todoMatch[1]!) ?? []) if (/^\/session\/[^/]+\/(children|diff)$/.test(path)) return json(route, []) + const currentMessagesMatch = path.match(/^\/api\/session\/([^/]+)\/message$/) + if (currentMessagesMatch) { + const token = url.searchParams.get("cursor") ?? undefined + const before = token ? cursors.get(token) : undefined + if (token && !before) return json(route, { error: "Invalid cursor" }, undefined, 400) + config.onMessages?.({ sessionID: currentMessagesMatch[1], before, phase: "start" }) + await config.beforeMessagesResponse?.({ sessionID: currentMessagesMatch[1]!, before }) + if (config.messageDelay !== undefined) await new Promise((resolve) => setTimeout(resolve, config.messageDelay)) + const pageData = config.pageMessages(currentMessagesMatch[1], Number(url.searchParams.get("limit") ?? 50), before) + config.onMessages?.({ sessionID: currentMessagesMatch[1], before, phase: "end" }) + const cursor = pageData.cursor ? `cursor_${++nextCursor}` : undefined + if (cursor) cursors.set(cursor, pageData.cursor!) + return json(route, { + data: pageData.items.map(currentMessage).reverse(), + cursor: { next: cursor }, + }) + } + const messagesMatch = path.match(/^\/session\/([^/]+)\/message$/) if (messagesMatch) { const token = url.searchParams.get("before") ?? undefined @@ -129,6 +314,115 @@ export async function mockOpenCodeServer(page: Page, config: MockServerConfig) { }) } +function location(config: MockServerConfig) { + return { + directory: config.directory, + project: { id: (config.project as { id?: string }).id, directory: config.directory }, + } +} + +function currentPermission(value: unknown) { + const permission = value as Record + if (permission.action) return permission + const tool = permission.tool as { messageID?: string; callID?: string } | undefined + return { + id: permission.id, + sessionID: permission.sessionID, + action: permission.permission, + resources: permission.patterns ?? [], + save: permission.always, + metadata: permission.metadata, + source: + tool?.messageID && tool.callID ? { type: "tool", messageID: tool.messageID, callID: tool.callID } : undefined, + } +} + +export function currentSession(session: { id: string } & Record, fallbackDirectory?: string) { + const time = session.time && typeof session.time === "object" ? session.time : {} + return { + id: session.id, + parentID: session.parentID, + projectID: session.projectID ?? "project", + agent: session.agent ?? "build", + model: session.model ?? { id: "mock-model", providerID: "mock-provider" }, + cost: session.cost ?? 0, + tokens: session.tokens ?? { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } }, + time: { + created: "created" in time && typeof time.created === "number" ? time.created : 0, + updated: "updated" in time && typeof time.updated === "number" ? time.updated : 0, + ...(session.time && typeof session.time === "object" && "archived" in session.time + ? { archived: session.time.archived } + : {}), + }, + title: session.title ?? session.id, + location: { + directory: typeof session.directory === "string" ? session.directory : fallbackDirectory, + ...(typeof session.workspaceID === "string" ? { workspaceID: session.workspaceID } : {}), + }, + subpath: session.path, + revert: session.revert, + } +} + +function currentMessage(value: unknown) { + const item = value as { + info: Record & { id: string; role: "user" | "assistant"; time: { created: number } } + parts: Array & { type: string }> + } + if (item.info.role === "user") { + return { + id: item.info.id, + type: "user", + time: item.info.time, + text: item.parts + .flatMap((part) => (part.type === "text" && typeof part.text === "string" ? [part.text] : [])) + .join("\n"), + } + } + return { + id: item.info.id, + type: "assistant", + time: item.info.time, + agent: item.info.agent ?? "build", + model: { id: item.info.modelID ?? "model", providerID: item.info.providerID ?? "provider" }, + cost: item.info.cost, + tokens: item.info.tokens, + error: item.info.error, + content: item.parts.flatMap((part) => { + if (part.type === "text" || part.type === "reasoning") return [{ type: part.type, text: part.text ?? "" }] + if (part.type !== "tool") return [] + const state = part.state as Record + return [ + { + type: "tool", + id: part.id, + name: part.tool, + time: state.time ?? { created: item.info.time.created }, + state: + state.status === "pending" + ? { status: "streaming", input: state.raw ?? JSON.stringify(state.input ?? {}) } + : state.status === "completed" + ? { + status: "completed", + input: state.input ?? {}, + structured: state.metadata ?? {}, + content: [{ type: "text", text: state.output ?? "" }], + } + : state.status === "error" + ? { + status: "error", + input: state.input ?? {}, + structured: state.metadata ?? {}, + content: [], + error: { type: "ToolError", message: state.error ?? "Tool failed" }, + } + : { status: "running", input: state.input ?? {}, structured: state.metadata ?? {}, content: [] }, + }, + ] + }), + } +} + function json(route: Route, body: unknown, headers?: Record, status = 200) { return route.fulfill({ status, @@ -149,3 +443,18 @@ function sse(route: Route, events?: unknown[], retry?: number) { body: `${retry === undefined ? "" : `retry: ${retry}\n\n`}${events?.map((event) => `data: ${JSON.stringify(event)}\n\n`).join("") || ": ok\n\n"}`, }) } + +function currentEvent(input: unknown) { + if (!input || typeof input !== "object" || !("payload" in input)) return input + const envelope = input as { directory?: string; payload?: unknown } + if (!envelope.payload || typeof envelope.payload !== "object") return input + const payload = envelope.payload as { id?: string; type?: string; properties?: unknown } + if (!payload.type) return input + return { + id: payload.id ?? `evt_mock_${Date.now()}`, + created: Date.now(), + type: payload.type, + data: payload.properties ?? {}, + location: envelope.directory && envelope.directory !== "global" ? { directory: envelope.directory } : undefined, + } +} diff --git a/packages/app/e2e/utils/sse-transport.ts b/packages/app/e2e/utils/sse-transport.ts index 186962998d..b0e3b74c6d 100644 --- a/packages/app/e2e/utils/sse-transport.ts +++ b/packages/app/e2e/utils/sse-transport.ts @@ -3,7 +3,7 @@ import type { Page } from "@playwright/test" export type SseConnectionRecord = { id: number url: string - path: "/global/event" | "/event" + path: "/global/event" | "/event" | "/api/event" headers: Record openedAt: number endedAt?: number @@ -93,6 +93,21 @@ export async function installSseTransport( eventOptions.retry === undefined ? "" : `retry: ${eventOptions.retry}\n`, `data: ${JSON.stringify(payload)}\n\n`, ].join("") + const currentEvent = (input: unknown) => { + if (!input || typeof input !== "object" || !("payload" in input)) return input + const envelope = input as { directory?: string; payload?: unknown } + if (!envelope.payload || typeof envelope.payload !== "object") return input + const payload = envelope.payload as { id?: string; type?: string; properties?: unknown } + if (!payload.type) return input + return { + id: payload.id ?? `evt_mock_${Date.now()}`, + created: Date.now(), + type: payload.type, + data: payload.properties ?? {}, + location: + envelope.directory && envelope.directory !== "global" ? { directory: envelope.directory } : undefined, + } + } const acknowledge = ( connection: Connection, bytes: number, @@ -140,15 +155,13 @@ export async function installSseTransport( output.forEach((chunk) => connection.controller.enqueue(chunk)) return acknowledge(connection, input.bytes.length, output.length) } - const encoded = input.deliveries.map((delivery) => ({ - delivery, - bytes: encoder.encode(frame(delivery.payload, delivery.options)), - })) + const encoded = input.deliveries.map((delivery) => { + const payload = connection.path === "/api/event" ? currentEvent(delivery.payload) : delivery.payload + return { delivery, payload, bytes: encoder.encode(frame(payload, delivery.options)) } + }) encoded.forEach((item) => marker(item.delivery.options?.marker)) if (input.burst) { - const bytes = encoder.encode( - encoded.map((item) => frame(item.delivery.payload, item.delivery.options)).join(""), - ) + const bytes = encoder.encode(encoded.map((item) => frame(item.payload, item.delivery.options)).join("")) connection.controller.enqueue(bytes) return encoded.map((item) => acknowledge(connection, item.bytes.byteLength, 1, item.delivery.options?.id)) } @@ -161,8 +174,11 @@ export async function installSseTransport( const fetch = (input: RequestInfo | URL, init?: RequestInit) => { const request = new Request(input, init) const url = new URL(request.url) - if (url.origin !== server || (url.pathname !== "/global/event" && url.pathname !== "/event")) - return originalFetch(input, init) + if ( + url.origin !== server || + (url.pathname !== "/global/event" && url.pathname !== "/event" && url.pathname !== "/api/event") + ) + return originalFetch(request) const id = ++nextConnectionID const record = { @@ -177,6 +193,18 @@ export async function installSseTransport( record.controller = controller connections.push(record) if (retry !== undefined) controller.enqueue(encoder.encode(`retry: ${retry}\n\n`)) + if (url.pathname === "/api/event") + controller.enqueue( + encoder.encode(frame({ id: `evt_mock_connected_${id}`, type: "server.connected", data: {} })), + ) + if (url.pathname === "/global/event") + controller.enqueue( + encoder.encode( + frame({ + payload: { id: `evt_mock_connected_${id}`, type: "server.connected", properties: {} }, + }), + ), + ) request.signal.addEventListener( "abort", () => { diff --git a/packages/app/package.json b/packages/app/package.json index 510e2015db..2dd4a05d86 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/app", - "version": "1.17.18", + "version": "1.18.11", "description": "", "type": "module", "exports": { @@ -19,9 +19,9 @@ "build": "vite build", "serve": "vite preview", "test": "bun run test:unit && bun run test:browser", - "test:unit": "bun test --only-failures --preload ./happydom.ts ./src", + "test:unit": "bun test --conditions=solid --only-failures --preload ./happydom.ts ./src", "test:browser": "bun test --conditions=browser --preload ./happydom.ts ./test-browser", - "test:unit:watch": "bun test --watch --preload ./happydom.ts ./src", + "test:unit:watch": "bun test --conditions=solid --watch --preload ./happydom.ts ./src", "test:e2e": "playwright test", "test:e2e:local": "playwright test", "test:e2e:ui": "playwright test --ui", @@ -53,6 +53,7 @@ "@dnd-kit/helpers": "0.5.0", "@dnd-kit/solid": "0.5.0", "@kobalte/core": "catalog:", + "@opencode-ai/client": "file:vendor/opencode-ai-client-1.17.13-v2.tgz", "@opencode-ai/core": "workspace:*", "@opencode-ai/schema": "workspace:*", "@opencode-ai/sdk": "workspace:*", @@ -81,7 +82,7 @@ "diff": "catalog:", "effect": "catalog:", "fuzzysort": "catalog:", - "ghostty-web": "github:anomalyco/ghostty-web#513463a6f1190253057e8a3f0dac8f6ee8393553", + "ghostty-web": "github:anomalyco/ghostty-web#83c0a07b8628b748aed073b232cb4b52a6ca11c1", "luxon": "catalog:", "marked": "catalog:", "marked-shiki": "catalog:", diff --git a/packages/app/src/addons/serialize.test.ts b/packages/app/src/addons/serialize.test.ts index 6828e60f84..d45af34c43 100644 --- a/packages/app/src/addons/serialize.test.ts +++ b/packages/app/src/addons/serialize.test.ts @@ -37,6 +37,14 @@ function writeAndWait(term: Terminal, data: string): Promise { } describe("SerializeAddon", () => { + test("preserves color scheme reporting mode", async () => { + const { term, addon } = createTerminal() + await writeAndWait(term, "\x1b[?2031h") + + expect(addon.serialize().startsWith("\x1b[?2031h")).toBe(true) + expect(addon.serialize({ excludeModes: true }).startsWith("\x1b[?2031h")).toBe(false) + }) + describe("ANSI color preservation", () => { test("should preserve text attributes (bold, italic, underline)", async () => { const { term, addon } = createTerminal() diff --git a/packages/app/src/addons/serialize.ts b/packages/app/src/addons/serialize.ts index 3823fb443a..515153488c 100644 --- a/packages/app/src/addons/serialize.ts +++ b/packages/app/src/addons/serialize.ts @@ -89,6 +89,13 @@ const getTerminalBuffers = (value: ITerminalCore): TerminalBuffers | undefined = return { active, normal, alternate } } +const getTerminalMode = (value: ITerminalCore, mode: number) => { + if (!isRecord(value)) return false + const terminal = value.wasmTerm + if (!isRecord(terminal) || typeof terminal.getMode !== "function") return false + return terminal.getMode(mode) === true +} + // ============================================================================ // Types // ============================================================================ @@ -544,7 +551,8 @@ export class SerializeAddon implements ITerminalAddon { return "" } - let content = options?.range + let content = !options?.excludeModes && getTerminalMode(this._terminal, 2031) ? "\u001b[?2031h" : "" + content += options?.range ? this._serializeBufferByRange(normalBuffer, options.range, true) : this._serializeBufferByScrollback(normalBuffer, options?.scrollback) diff --git a/packages/app/src/app.tsx b/packages/app/src/app.tsx index 86263d172d..f47c432e42 100644 --- a/packages/app/src/app.tsx +++ b/packages/app/src/app.tsx @@ -9,7 +9,16 @@ import { Font } from "@opencode-ai/ui/font" import { Splash } from "@opencode-ai/ui/logo" import { ThemeProvider } from "@opencode-ai/ui/theme/context" import { MetaProvider } from "@solidjs/meta" -import { type BaseRouterProps, Navigate, Route, Router, useNavigate, useParams, useSearchParams } from "@solidjs/router" +import { + type BaseRouterProps, + Navigate, + Route, + Router, + useLocation, + useNavigate, + useParams, + useSearchParams, +} from "@solidjs/router" import { QueryClient, QueryClientProvider } from "@tanstack/solid-query" import { Effect } from "effect" import { base64Encode } from "@opencode-ai/core/util/encode" @@ -29,6 +38,7 @@ import { Show, } from "solid-js" import { Dynamic } from "solid-js/web" +import { makeEventListener } from "@solid-primitives/event-listener" import { CommandProvider, useCommand, type CommandOption } from "@/context/command" import { CommentsProvider } from "@/context/comments" import { FileProvider } from "@/context/file" @@ -57,7 +67,8 @@ import { legacySessionHref, legacySessionServer, requireServerKey, sessionHref } import { createSessionLineage } from "@/pages/session/session-lineage" import { SessionPage, SessionRouteErrorBoundary, TargetSessionRouteContent } from "@/pages/session" -import { NewHome, LegacyHome } from "@/pages/home" +import { NewHome } from "@/pages/home" +import { LegacyHome } from "@/pages/home/legacy-home" const NewSession = lazy(() => import("@/pages/new-session")) @@ -153,8 +164,7 @@ function LegacyTargetSessionRedirect() { } // Wraps the non-draft routes. They are gated on (and keyed to) the globally selected -// server via ServerKey, then provide the server-scoped shell (Permission/Layout/ -// Notification/Models + the visual Layout) for that server. +// server via ServerKey, then provide the server-scoped shell for that server. function SelectedServerProviders(props: ParentProps) { return ( @@ -207,7 +217,7 @@ function ResolvedDraftRoute(props: { draft: DraftTab }) { - + @@ -215,7 +225,7 @@ function ResolvedDraftRoute(props: { draft: DraftTab }) { - + @@ -227,6 +237,30 @@ function UiI18nBridge(props: ParentProps) { return {props.children} } +function LayoutCompatibility(props: ParentProps) { + const global = useGlobal() + const navigate = useNavigate() + const server = useServer() + const settings = useSettings() + + createEffect(() => { + if (settings.general.newLayoutDesigns()) return + const current = server.current + if (!current) return + const protocol = global.ensureServerCtx(current).sdk.protocolKind() + if (protocol !== "v2") return + const next = global.servers.list().find((s) => { + if (ServerConnection.key(s) === ServerConnection.key(current)) return false + return global.ensureServerCtx(s).sdk.protocolKind() !== "v2" + }) + if (!next) return + navigate("/") + queueMicrotask(() => server.setActive(ServerConnection.key(next))) + }) + + return <>{props.children} +} + declare global { interface Window { __OPENCODE__?: { @@ -309,24 +343,21 @@ function DesktopCommands() { // Server-scoped providers shared by the legacy shell and the top-level new shell. type ServerScopedShellProps = ParentProps<{ directory?: () => string | undefined - sessionID?: () => string | undefined serverScoped?: JSX.Element }> function ServerScopedProviders(props: ServerScopedShellProps) { return ( - - - {props.serverScoped} - {props.children} - - + + {props.serverScoped} + {props.children} + ) } function LegacyServerScopedShell(props: ServerScopedShellProps) { return ( - + {props.children} ) @@ -342,14 +373,6 @@ function NewAppLayout(props: ParentProps<{ serverScoped?: JSX.Element }>) { ) } -function DraftServerScopedProviders(props: ParentProps<{ directory?: () => string | undefined }>) { - return ( - - {props.children} - - ) -} - // The draft page only renders the prompt composer, so it drops TerminalProvider. // FileProvider and CommentsProvider stay because PromptInput uses file search and comment context. function DraftProviders(props: ParentProps) { @@ -559,13 +582,15 @@ export function AppInterface(props: { component={props.router ?? Router} root={(routerProps) => ( - - - - {routerProps.children} - - - + + + + + {routerProps.children} + + + + )} > diff --git a/packages/app/src/assets/help/home.png b/packages/app/src/assets/help/home.png new file mode 100644 index 0000000000..aeca6d977a Binary files /dev/null and b/packages/app/src/assets/help/home.png differ diff --git a/packages/app/src/assets/help/introducing-tabs.mp4 b/packages/app/src/assets/help/introducing-tabs.mp4 index 2bcc8aeef6..fd46be1f46 100644 Binary files a/packages/app/src/assets/help/introducing-tabs.mp4 and b/packages/app/src/assets/help/introducing-tabs.mp4 differ diff --git a/packages/app/src/assets/help/tabs.png b/packages/app/src/assets/help/tabs.png new file mode 100644 index 0000000000..d2c6e68a3d Binary files /dev/null and b/packages/app/src/assets/help/tabs.png differ diff --git a/packages/app/src/components/command-palette.ts b/packages/app/src/components/command-palette.ts index b61a61977e..8014ea1c43 100644 --- a/packages/app/src/components/command-palette.ts +++ b/packages/app/src/components/command-palette.ts @@ -1,17 +1,20 @@ -import { base64Encode } from "@opencode-ai/core/util/encode" import { getFilename } from "@opencode-ai/core/util/path" +import type { Project } from "@opencode-ai/sdk/v2/client" +import type { SessionInfo } from "@opencode-ai/client/promise" import { useDialog } from "@opencode-ai/ui/context/dialog" -import { useNavigate } from "@solidjs/router" import { createMemo, onCleanup } from "solid-js" -import { useCommand, type CommandOption } from "@/context/command" +import { commandPaletteOptions, useCommand, type CommandOption } from "@/context/command" import { useFile } from "@/context/file" +import { useGlobal } from "@/context/global" import { useLanguage } from "@/context/language" -import { useLayout } from "@/context/layout" -import { useServerSDK, type ServerSDK } from "@/context/server-sdk" -import { useServerSync } from "@/context/server-sync" +import { useLayout, type LocalProject } from "@/context/layout" +import { ServerConnection } from "@/context/server" +import { useServerSDK } from "@/context/server-sdk" +import { useTabs } from "@/context/tabs" +import { displayName, projectForSession } from "@/pages/layout/helpers" import { createSessionTabs } from "@/pages/session/helpers" import { useSessionLayout } from "@/pages/session/session-layout" -import { decode64 } from "@/utils/base64" +import { normalizeSessionInfo } from "@/utils/session" export type CommandPaletteEntry = { id: string @@ -24,6 +27,8 @@ export type CommandPaletteEntry = { path?: string directory?: string sessionID?: string + server?: ServerConnection.Key + project?: LocalProject archived?: number updated?: number } @@ -75,28 +80,25 @@ export function createCommandPaletteFileOpener(onOpenFile?: (path: string) => vo export function createCommandPaletteModel(props: { filesOnly?: () => boolean; onOpenFile?: (path: string) => void }) { const command = useCommand() + const global = useGlobal() const language = useLanguage() - const layout = useLayout() const file = useFile() const dialog = useDialog() - const navigate = useNavigate() const serverSDK = useServerSDK()() - const serverSync = useServerSync() - const { params, tabs } = useSessionLayout() + const serverCtx = global.ensureServerCtx(serverSDK.server) + const appTabs = useTabs() + const { tabs: sessionTabs } = useSessionLayout() const openFile = createCommandPaletteFileOpener(props.onOpenFile) const state = { cleanup: undefined as (() => void) | void, committed: false } const filesOnly = () => props.filesOnly?.() ?? false const allowedCommands = createMemo(() => { if (filesOnly()) return [] - return command.options.filter( - (option) => - !option.disabled && !option.hidden && !option.id.startsWith("suggested.") && option.id !== "file.open", - ) + return commandPaletteOptions(command.options) }) const commandEntries = createMemo(() => { const category = language.t("palette.group.commands") - return allowedCommands().map((option) => createCommandEntry(option, category)) + return allowedCommands().map((option) => createCommandPaletteCommandEntry(option, category)) }) const preferredCommandEntries = createMemo(() => { const all = allowedCommands() @@ -105,11 +107,11 @@ export function createCommandPaletteModel(props: { filesOnly?: () => boolean; on const base = picked.length ? picked : all.slice(0, ENTRY_LIMIT) const sorted = picked.length ? [...base].sort((a, b) => (order.get(a.id) ?? 0) - (order.get(b.id) ?? 0)) : base const category = language.t("palette.group.commands") - return sorted.map((option) => createCommandEntry(option, category)) + return sorted.map((option) => createCommandPaletteCommandEntry(option, category)) }) const tabState = createSessionTabs({ - tabs, + tabs: sessionTabs, pathFromTab: file.pathFromTab, normalizeTab: (tab) => (tab.startsWith("file://") ? file.tab(tab) : tab), }) @@ -140,36 +142,11 @@ export function createCommandPaletteModel(props: { filesOnly?: () => boolean; on .map((path) => createCommandPaletteFileEntry(path, category)) }) - const projectDirectory = createMemo(() => decode64(params.dir) ?? "") - const project = createMemo(() => { - const directory = projectDirectory() - if (!directory) return undefined - return layout.projects.list().find((item) => item.worktree === directory || item.sandboxes?.includes(directory)) - }) - const workspaces = createMemo(() => { - const directory = projectDirectory() - const current = project() - if (!current) return directory ? [directory] : [] - const dirs = [current.worktree, ...(current.sandboxes ?? [])] - if (directory && !dirs.includes(directory)) return [...dirs, directory] - return dirs - }) - const homedir = createMemo(() => serverSync().data.path.home) - const sessions = createSessionEntries({ - workspaces, - label: (directory) => { - const current = project() - const kind = - current && directory === current.worktree - ? language.t("workspace.type.local") - : language.t("workspace.type.sandbox") - const [store] = serverSync().child(directory, { bootstrap: false }) - const home = homedir() - const path = home ? directory.replace(home, "~") : directory - const name = store.vcs?.branch ?? getFilename(directory) - return `${kind} : ${name || path}` - }, - load: (directory) => serverSDK.client.session.list({ directory, roots: true }), + const sessions = createServerSessionEntries({ + server: ServerConnection.key(serverSDK.server), + opened: serverCtx.projects.list, + stored: () => serverCtx.sync.data.project, + load: (search, signal) => serverSDK.api.session.list({ parentID: null, search, limit: 50 }, { signal }), untitled: () => language.t("command.session.new"), category: () => language.t("command.category.session"), }) @@ -191,8 +168,17 @@ export function createCommandPaletteModel(props: { filesOnly?: () => boolean; on return } if (item.type === "session") { - if (!item.directory || !item.sessionID) return - navigate(`/${base64Encode(item.directory)}/session/${item.sessionID}`) + if (!item.sessionID || !item.server) return + const directory = item.project?.worktree ?? item.directory + if (directory) { + serverCtx.projects.open(directory) + serverCtx.projects.touch(directory) + } + const tab = appTabs.addSessionTab({ + server: item.server, + sessionId: item.sessionID, + }) + appTabs.select(tab) return } if (!item.path) return @@ -218,7 +204,7 @@ export function createCommandPaletteModel(props: { filesOnly?: () => boolean; on } } -function createCommandEntry(option: CommandOption, category: string): CommandPaletteEntry { +export function createCommandPaletteCommandEntry(option: CommandOption, category: string): CommandPaletteEntry { return { id: "command:" + option.id, type: "command", @@ -230,96 +216,66 @@ function createCommandEntry(option: CommandOption, category: string): CommandPal } } -function createSessionEntries(props: { - workspaces: () => string[] - label: (directory: string) => string - load: (directory: string) => ReturnType +export function createServerSessionEntries(props: { + server: ServerConnection.Key + opened: () => LocalProject[] + stored: () => Project[] + load: (search: string, signal: AbortSignal) => Promise<{ data: SessionInfo[] }> untitled: () => string category: () => string }) { - const state: { - token: number - inflight: Promise | undefined - cached: CommandPaletteEntry[] | undefined - } = { token: 0, inflight: undefined, cached: undefined } + let abort: AbortController | undefined - return (text: string) => { - if (!text.trim()) { - state.token += 1 - state.inflight = undefined - state.cached = undefined - return [] as CommandPaletteEntry[] + onCleanup(() => abort?.abort()) + + return async (text: string): Promise => { + const search = text.trim() + if (!search) { + abort?.abort() + return [] } - if (state.cached) return state.cached - if (state.inflight) return state.inflight - - const current = state.token - const dirs = props.workspaces() - if (dirs.length === 0) return [] as CommandPaletteEntry[] - - state.inflight = Promise.all( - dirs.map((directory) => { - const description = props.label(directory) - return props - .load(directory) - .then((result) => - (result.data ?? []) - .filter((session) => !!session?.id) - .map((session) => ({ - id: session.id, - title: session.title ?? props.untitled(), - description, - directory, - archived: session.time?.archived, - updated: session.time?.updated, - })), - ) - .catch(() => [] as SessionEntryInput[]) - }), - ) - .then((results) => { - if (state.token !== current) return [] as CommandPaletteEntry[] - const seen = new Set() - const next = results - .flat() - .filter((item) => { - const key = `${item.directory}:${item.id}` - if (seen.has(key)) return false - seen.add(key) - return true - }) - .map((item) => createSessionEntry(item, props.category())) - state.cached = next - return next - }) + abort?.abort() + const current = new AbortController() + abort = current + await new Promise((resolve) => { + const timer = setTimeout(resolve, 100) + current.signal.addEventListener( + "abort", + () => { + clearTimeout(timer) + resolve() + }, + { once: true }, + ) + }) + if (current.signal.aborted) return [] + const opened = props.opened() + const openedByID = new Map(opened.flatMap((project) => (project.id ? [[project.id, project] as const] : []))) + const stored = props.stored().map((project) => ({ ...project, expanded: false })) + const storedByID = new Map(stored.map((project) => [project.id, project] as const)) + return props + .load(search, current.signal) + .then((result) => + result.data + .map(normalizeSessionInfo) + .filter((session) => !session.time.archived) + .map((session) => { + const project = + projectForSession(session, opened, openedByID) ?? projectForSession(session, stored, storedByID) + return { + id: `session:${props.server}:${session.id}`, + type: "session" as const, + title: session.title || props.untitled(), + description: project ? displayName(project) : getFilename(session.directory), + category: props.category(), + directory: session.directory, + sessionID: session.id, + server: props.server, + project, + updated: session.time.updated, + } + }), + ) .catch(() => [] as CommandPaletteEntry[]) - .finally(() => { - state.inflight = undefined - }) - - return state.inflight - } -} - -type SessionEntryInput = { - directory: string - id: string - title: string - description: string - archived?: number - updated?: number -} - -function createSessionEntry(input: SessionEntryInput, category: string): CommandPaletteEntry { - return { - id: `session:${input.directory}:${input.id}`, - type: "session", - title: input.title, - description: input.description, - category, - directory: input.directory, - sessionID: input.id, - archived: input.archived, - updated: input.updated, } } diff --git a/packages/app/src/components/debug-bar.tsx b/packages/app/src/components/debug-bar.tsx index e55e128b82..adadeda303 100644 --- a/packages/app/src/components/debug-bar.tsx +++ b/packages/app/src/components/debug-bar.tsx @@ -5,6 +5,7 @@ import { makeEventListener } from "@solid-primitives/event-listener" import { Tooltip } from "@opencode-ai/ui/tooltip" import { TooltipV2 } from "@opencode-ai/ui/v2/tooltip-v2" import { useLanguage } from "@/context/language" +import { usePlatform } from "@/context/platform" type Mem = Performance & { memory?: { @@ -65,7 +66,7 @@ function Cell(props: {
- {props.label} -
-
- {props.value} +
+ {props.label} +
+
+ {props.value} +
) @@ -107,8 +116,55 @@ function Cell(props: { ) } +function FocusCell(props: { active: boolean; inline?: boolean; onClick: () => void }) { + const content = () => ( + + ) + + if (props.inline) { + return ( + + {content()} + + ) + } + + return ( + + {content()} + + ) +} + export function DebugBar(props: { inline?: boolean } = {}) { const language = useLanguage() + const platform = usePlatform() const location = useLocation() const routing = useIsRouting() const [state, setState] = createStore({ @@ -116,6 +172,7 @@ export function DebugBar(props: { inline?: boolean } = {}) { delay: undefined as number | undefined, fps: undefined as number | undefined, gap: undefined as number | undefined, + focus: false, heap: { limit: undefined as number | undefined, used: undefined as number | undefined, @@ -142,6 +199,16 @@ export function DebugBar(props: { inline?: boolean } = {}) { } const longv = () => (state.long.count === undefined ? na() : `${time(state.long.block) ?? na()}/${state.long.count}`) const navv = () => (state.nav.pending ? "..." : (time(state.nav.dur) ?? na())) + const toggleFocus = async () => { + if (!platform.setForceFocus) return + const enabled = !state.focus + await platform.setForceFocus(enabled) + setState("focus", enabled) + } + + onCleanup(() => { + if (state.focus) void platform.setForceFocus?.(false).catch(() => undefined) + }) let prev = "" let start = 0 @@ -490,8 +557,11 @@ export function DebugBar(props: { inline?: boolean } = {}) { bad={bad(heap(), 0.8)} dim={state.heap.used === undefined} inline={props.inline} - wide + wide={!platform.setForceFocus} /> + {platform.setForceFocus && ( + void toggleFocus()} /> + )} ) diff --git a/packages/app/src/components/dialog-command-palette-v2.tsx b/packages/app/src/components/dialog-command-palette-v2.tsx index b1cafe1e7a..e996fd0be7 100644 --- a/packages/app/src/components/dialog-command-palette-v2.tsx +++ b/packages/app/src/components/dialog-command-palette-v2.tsx @@ -5,13 +5,20 @@ import { Dialog, DialogBody } from "@opencode-ai/ui/v2/dialog-v2" import { Icon } from "@opencode-ai/ui/v2/icon" import { KeybindV2 } from "@opencode-ai/ui/v2/keybind-v2" import { TextInputV2 } from "@opencode-ai/ui/v2/text-input-v2" -import { createEffect, createMemo, createResource, createSignal, For, Match, Show, Switch } from "solid-js" -import { formatKeybindParts } from "@/context/command" +import { useDialog } from "@opencode-ai/ui/context/dialog" +import { createEffect, createMemo, createResource, createSignal, For, Match, onCleanup, Show, Switch } from "solid-js" +import { commandPaletteOptions, formatKeybindParts, useCommand } from "@/context/command" +import { useGlobal } from "@/context/global" import { useLanguage } from "@/context/language" +import { ServerConnection } from "@/context/server" +import { useTabs } from "@/context/tabs" +import { SessionTabAvatar } from "@/pages/layout/session-tab-avatar" import { getRelativeTime } from "@/utils/time" import { + createCommandPaletteCommandEntry, createCommandPaletteFileEntry, createCommandPaletteModel, + createServerSessionEntries, uniqueCommandPaletteEntries, type CommandPaletteEntry, } from "./command-palette" @@ -30,9 +37,6 @@ function matchesEntry(entry: CommandPaletteEntry, query: string) { export function DialogCommandPaletteV2(props: { onOpenFile?: (path: string) => void }) { const palette = createCommandPaletteModel(props) - const [query, setQuery] = createSignal("") - const [active, setActive] = createSignal(0) - const loadItems = async (text: string) => { const q = text.trim() if (!q) return [...palette.preferredCommandEntries(), ...palette.recentFileEntries()] @@ -41,16 +45,104 @@ export function DialogCommandPaletteV2(props: { onOpenFile?: (path: string) => v const category = palette.language.t("palette.group.files") return [ ...palette.commandEntries().filter((entry) => matchesEntry(entry, q)), - ...nextSessions.filter((entry) => matchesEntry(entry, q)), + ...nextSessions, ...files.map((path) => createCommandPaletteFileEntry(path, category)), ] } - const [entries] = createResource(query, loadItems, { initialValue: [] as CommandPaletteEntry[] }) + return ( + + ) +} + +export function DialogHomeCommandPaletteV2(props: { + server: ServerConnection.Any + onSelectSession: (entry: CommandPaletteEntry) => void +}) { + const command = useCommand() + const dialog = useDialog() + const global = useGlobal() + const language = useLanguage() + const serverCtx = global.ensureServerCtx(props.server) + const state = { cleanup: undefined as (() => void) | void, committed: false } + const commandEntries = createMemo(() => { + const category = language.t("palette.group.commands") + return commandPaletteOptions(command.options).map((option) => createCommandPaletteCommandEntry(option, category)) + }) + const sessions = createServerSessionEntries({ + server: ServerConnection.key(props.server), + opened: serverCtx.projects.list, + stored: () => serverCtx.sync.data.project, + load: (search, signal) => serverCtx.sdk.api.session.list({ parentID: null, search, limit: 50 }, { signal }), + untitled: () => language.t("command.session.new"), + category: () => language.t("command.category.session"), + }) + + const highlight = (item: CommandPaletteEntry | undefined) => { + state.cleanup?.() + state.cleanup = undefined + if (item?.type !== "command") return + state.cleanup = item.option?.onHighlight?.() + } + const select = (item: CommandPaletteEntry | undefined) => { + if (!item) return + state.committed = true + state.cleanup = undefined + dialog.close() + if (item.type === "command") { + item.option?.onSelect?.("palette") + return + } + if (item.type === "session") props.onSelectSession(item) + } + const loadItems = async (text: string) => { + const query = text.trim() + if (!query) return commandEntries().slice(0, 5) + return [...commandEntries().filter((entry) => matchesEntry(entry, query)), ...(await sessions(query))] + } + + onCleanup(() => { + if (state.committed) return + state.cleanup?.() + }) + + return ( + dialog.close()} + /> + ) +} + +function CommandPaletteView(props: { + placeholder: string + loadItems: (text: string) => CommandPaletteEntry[] | Promise + highlight: (item: CommandPaletteEntry | undefined) => void + select: (item: CommandPaletteEntry | undefined) => void + close: () => void +}) { + const language = useLanguage() + const tabs = useTabs() + const [query, setQuery] = createSignal("") + const [active, setActive] = createSignal(0) + + const [entries] = createResource(query, props.loadItems, { initialValue: [] as CommandPaletteEntry[] }) // Render stale results while a new query loads to avoid flashing "Loading" per keystroke. const visibleEntries = createMemo(() => uniqueCommandPaletteEntries(entries.latest ?? [])) const groupedEntries = createMemo(() => groups(visibleEntries())) const activeEntry = createMemo(() => visibleEntries()[active()]) + const openSessions = createMemo( + () => new Set(tabs.store.flatMap((tab) => (tab.type === "session" ? [`${tab.server}\0${tab.sessionId}`] : []))), + ) createEffect(() => { query() @@ -59,7 +151,7 @@ export function DialogCommandPaletteV2(props: { onOpenFile?: (path: string) => v }) createEffect(() => { - palette.highlight(activeEntry()) + props.highlight(activeEntry()) }) let resultsRef: HTMLDivElement | undefined @@ -86,12 +178,12 @@ export function DialogCommandPaletteV2(props: { onOpenFile?: (path: string) => v } if (event.key === "Enter") { event.preventDefault() - palette.select(activeEntry()) + props.select(activeEntry()) return } if (event.key === "Escape") { event.preventDefault() - palette.close() + props.close() } } @@ -105,7 +197,7 @@ export function DialogCommandPaletteV2(props: { onOpenFile?: (path: string) => v autocomplete="off" spellcheck={false} appearance="large" - placeholder={palette.language.t("palette.search.placeholder")} + placeholder={props.placeholder} leadingIcon={} onInput={(event) => setQuery(event.currentTarget.value)} onKeyDown={handleKeyDown} @@ -117,7 +209,7 @@ export function DialogCommandPaletteV2(props: { onOpenFile?: (path: string) => v when={visibleEntries().length > 0} fallback={
- {entries.loading ? palette.language.t("common.loading") : palette.language.t("palette.empty")} + {entries.loading ? language.t("common.loading") : language.t("palette.empty")}
} > @@ -132,9 +224,14 @@ export function DialogCommandPaletteV2(props: { onOpenFile?: (path: string) => v setActive(visibleEntries().findIndex((entry) => entry.id === item.id))} - onSelect={() => palette.select(item)} + onSelect={() => props.select(item)} /> )} @@ -153,13 +250,19 @@ function PaletteRow(props: { item: CommandPaletteEntry active: boolean language: ReturnType + sessionOpen: boolean onActive: () => void onSelect: () => void }) { + const session = () => + props.item.server && props.item.directory && props.item.sessionID + ? { server: props.item.server, directory: props.item.directory, sessionID: props.item.sessionID } + : undefined + return ( + ) +} + +function ProviderConnectionDialogStory(props) { + onCleanup(mockProviderAuth(props.provider, props.methods)) + const dialog = useDialog() + const controller = useProviderConnectController() + controller.select(props.provider) + const open = () => dialog.show(() => ) + + onMount(open) + + return ( + + ) +} + +function renderConnection(provider, methods) { + return () => ( + + + + ) +} + +export default { + title: "App/Dialogs/Connect Provider", + id: "app-dialog-connect-provider", +} + +export const V2 = { + render: () => ( + + + + ), +} + +export const ApiKey = { + render: renderConnection("openrouter", [{ type: "api", label: "API key" }]), +} + +export const OpenCodeZen = { + render: renderConnection("opencode", [{ type: "api", label: "API key" }]), +} + +export const LoginMethods = { + render: renderConnection("openai", [ + { type: "oauth", label: "ChatGPT Pro/Plus (browser)" }, + { type: "oauth", label: "ChatGPT Pro/Plus (headless)" }, + { type: "api", label: "API key" }, + ]), +} diff --git a/packages/app/src/components/dialog-connect-provider.tsx b/packages/app/src/components/dialog-connect-provider.tsx index 6499642b38..0267b4b4ef 100644 --- a/packages/app/src/components/dialog-connect-provider.tsx +++ b/packages/app/src/components/dialog-connect-provider.tsx @@ -1,4 +1,4 @@ -import type { ProviderAuthAuthorization, ProviderAuthMethod } from "@opencode-ai/sdk/v2/client" +import type { IntegrationMethod, IntegrationOauthConnectOutput } from "@opencode-ai/client/promise" import { Button } from "@opencode-ai/ui/button" import { useDialog } from "@opencode-ai/ui/context/dialog" import { Dialog } from "@opencode-ai/ui/dialog" @@ -9,6 +9,9 @@ import { ProviderIcon } from "@opencode-ai/ui/provider-icon" import { Spinner } from "@opencode-ai/ui/spinner" import { Tag } from "@opencode-ai/ui/tag" import { TextField } from "@opencode-ai/ui/text-field" +import { ButtonV2 } from "@opencode-ai/ui/v2/button-v2" +import { DialogBody, DialogHeader, DialogTitle, DialogV2 } from "@opencode-ai/ui/v2/dialog-v2" +import { TextInputV2 } from "@opencode-ai/ui/v2/text-input-v2" import { showToast } from "@/utils/toast" import { type Accessor, @@ -16,6 +19,8 @@ import { createEffect, createMemo, createResource, + createUniqueId, + For, Match, onCleanup, onMount, @@ -23,14 +28,18 @@ import { Switch, } from "solid-js" import { createStore, produce } from "solid-js/store" -import { Link } from "@/components/link" +import { useParams } from "@solidjs/router" +import { ExternalLink } from "@/components/external-link" import { useServerSDK } from "@/context/server-sdk" import { useServerSync } from "@/context/server-sync" import { useLanguage } from "@/context/language" +import { useSettings } from "@/context/settings" import { popularProviders, useProviders } from "@/hooks/use-providers" import { CustomProviderForm } from "./dialog-custom-provider" +import { decode64 } from "@/utils/base64" const CUSTOM_ID = "_custom" +type ConnectMethod = Extract export function useProviderConnectController(options: { onBack?: () => void } = {}) { const [store, setStore] = createStore({ selected: undefined as string | undefined }) @@ -50,32 +59,22 @@ export const DialogConnectProvider: Component<{ const fallback = useProviderConnectController() const controller = props.controller ?? fallback const language = useLanguage() + const settings = useSettings() + const newLayout = settings.general.newLayoutDesigns const reset = controller.back const back = { current: reset } + let focusHost: HTMLDivElement | undefined + const holdFocus = () => focusHost?.focus({ preventScroll: true }) const select = (provider?: string) => { back.current = reset controller.select(provider) } - return ( - - back.current()} - aria-label={language.t("common.goBack")} - /> - - } - > + function Content() { + return ( - + {(provider) => ( @@ -88,15 +87,77 @@ export const DialogConnectProvider: Component<{ )} - + - + ) + } + + return ( + + back.current()} + aria-label={language.t("common.goBack")} + /> + + } + > + + + } + > + + + {language.t("command.provider.connect")}} + > + + + + +
+ +
+
+
+ ) } -function ProviderPicker(props: { directory?: Accessor; onSelect: (provider: string) => void }) { - const providers = useProviders(props.directory) +function ProviderPicker(props: { + directory?: Accessor + onSelect: (provider: string) => void + onPrepare?: () => void +}) { + const settings = useSettings() + if (settings.general.newLayoutDesigns()) + return + const providers = useProviders(() => props.directory?.()) const language = useLanguage() const popularGroup = () => language.t("dialog.provider.group.popular") const otherGroup = () => language.t("dialog.provider.group.other") @@ -163,6 +224,157 @@ function ProviderPicker(props: { directory?: Accessor; onSel ) } +function ProviderPickerV2(props: { + directory?: Accessor + onSelect: (provider: string) => void + onPrepare?: () => void +}) { + const providers = useProviders(() => props.directory?.()) + const language = useLanguage() + const [store, setStore] = createStore({ + filter: "", + active: undefined as string | undefined, + connecting: undefined as string | undefined, + }) + const featured = ["opencode", "opencode-go", "anthropic", "openai", "google", "openrouter", "vercel"] + const custom = () => ({ id: CUSTOM_ID, name: language.t("dialog.provider.custom.label") }) + const all = createMemo(() => { + language.locale() + const query = store.filter.trim().toLowerCase() + const values = [custom(), ...providers.all().values()] + if (!query) return values + return values.filter((provider) => `${provider.id} ${provider.name}`.toLowerCase().includes(query)) + }) + const popular = createMemo(() => + all() + .filter((provider) => featured.includes(provider.id)) + .sort((a, b) => featured.indexOf(a.id) - featured.indexOf(b.id)), + ) + const other = createMemo(() => + all() + .filter((provider) => !featured.includes(provider.id)) + .sort((a, b) => { + if (a.id === CUSTOM_ID) return -1 + if (b.id === CUSTOM_ID) return 1 + return a.name.localeCompare(b.name) + }), + ) + const rows = createMemo(() => [...popular(), ...other()]) + let picker: HTMLDivElement | undefined + let search: HTMLInputElement | undefined + + onMount(() => search?.focus({ preventScroll: true })) + + const connect = (provider: string) => { + props.onPrepare?.() + props.onSelect(provider) + } + + const move = (event: KeyboardEvent, direction: number) => { + const items = rows() + if (items.length === 0) return + const index = items.findIndex((provider) => provider.id === store.active) + const next = index < 0 ? (direction > 0 ? 0 : items.length - 1) : (index + direction + items.length) % items.length + setStore("active", items[next].id) + picker + ?.querySelector(`[data-provider-id="${CSS.escape(items[next].id)}"]`) + ?.focus({ preventScroll: true }) + event.preventDefault() + } + + const handleKeyDown = (event: KeyboardEvent) => { + if (event.key === "ArrowDown") return move(event, 1) + if (event.key === "ArrowUp") return move(event, -1) + if (event.key !== "Enter" || !store.active) return + connect(store.active) + event.preventDefault() + } + + return ( +
+
+ } + placeholder={language.t("dialog.provider.search.placeholder")} + value={store.filter} + onInput={(event) => { + setStore({ filter: event.currentTarget.value, active: undefined }) + }} + /> +
+
+
+ + {(group) => ( + 0}> +
+
+ {group.title} +
+ + {(provider) => ( + + )} + +
+
+ )} +
+ +
+ {language.t("dialog.provider.empty")} +
+
+
+
+
+
+ ) +} + function ProviderConnection(props: { provider: string directory?: Accessor @@ -172,8 +384,16 @@ function ProviderConnection(props: { const dialog = useDialog() const serverSync = useServerSync() const serverSDK = useServerSDK() + const params = useParams() const language = useLanguage() - const providers = useProviders(props.directory) + const settings = useSettings() + const newLayout = settings.general.newLayoutDesigns + const providers = useProviders(() => props.directory?.()) + const directory = () => props.directory?.() ?? decode64(params.dir) + const location = () => { + const value = directory() + return value ? { directory: value } : undefined + } const alive = { value: true } const timer = { current: undefined as ReturnType | undefined } @@ -188,28 +408,32 @@ function ProviderConnection(props: { const provider = createMemo( () => providers.all().get(props.provider) ?? serverSync().data.provider.all.get(props.provider)!, ) - const fallback = createMemo(() => [ + const fallback = createMemo(() => [ { - type: "api" as const, + type: "key" as const, label: language.t("provider.connect.method.apiKey"), }, ]) - const [auth] = createResource( - () => props.provider, - async () => { - const cached = serverSync().data.provider_auth[props.provider] - if (cached) return cached - const res = await serverSDK().client.provider.auth() - if (!alive.value) return fallback() - serverSync().set("provider_auth", res.data ?? {}) - return res.data?.[props.provider] ?? fallback() - }, + const [integration] = createResource( + () => ({ provider: props.provider, directory: directory() }), + (input) => + serverSDK() + .api.integration.get({ + integrationID: input.provider, + location: input.directory ? { directory: input.directory } : undefined, + }) + .then((result) => result.data), ) - const loading = createMemo(() => auth.loading && !serverSync().data.provider_auth[props.provider]) - const methods = createMemo(() => auth.latest ?? serverSync().data.provider_auth[props.provider] ?? fallback()) + const loading = createMemo(() => integration.loading) + const methods = createMemo(() => { + const values = integration.latest?.methods.filter( + (method): method is ConnectMethod => method.type === "key" || method.type === "oauth", + ) + return values?.length ? values : fallback() + }) const [store, setStore] = createStore({ methodIndex: undefined as undefined | number, - authorization: undefined as undefined | ProviderAuthAuthorization, + authorization: undefined as undefined | IntegrationOauthConnectOutput["data"], promptInputs: undefined as undefined | Record, state: "pending" as undefined | "pending" | "complete" | "error" | "prompt", error: undefined as string | undefined, @@ -221,7 +445,7 @@ function ProviderConnection(props: { | { type: "auth.prompt" } | { type: "auth.inputs"; inputs: Record } | { type: "auth.pending" } - | { type: "auth.complete"; authorization: ProviderAuthAuthorization } + | { type: "auth.complete"; authorization: IntegrationOauthConnectOutput["data"] } | { type: "auth.error"; error: string } function dispatch(action: Action) { @@ -275,10 +499,20 @@ function ProviderConnection(props: { const methodLabel = (value?: { type?: string; label?: string }) => { if (!value) return "" - if (value.type === "api") return language.t("provider.connect.method.apiKey") + if (value.type === "key") return language.t("provider.connect.method.apiKey") return value.label ?? "" } + const methodDetails = (value?: { type?: string; label?: string }) => { + const label = methodLabel(value) + const suffix = value?.label?.match(/\s+\((browser|headless)\)$/i) + const hint = suffix?.[1] + return { + label: suffix ? label.slice(0, -suffix[0].length) : label, + hint: hint ? hint[0].toUpperCase() + hint.slice(1) : value?.type === "key" ? "Browser" : undefined, + } + } + function formatError(value: unknown, fallback: string): string { if (value && typeof value === "object" && "data" in value) { const data = (value as { data?: { message?: unknown } }).data @@ -306,46 +540,22 @@ function ProviderConnection(props: { const method = methods()[index] dispatch({ type: "method.select", index }) - if (method.type === "api" && method.prompts?.length) { - if (!inputs) { - dispatch({ type: "auth.prompt" }) - return - } - dispatch({ type: "auth.inputs", inputs }) - return - } - if (method.type === "oauth") { if (method.prompts?.length && !inputs) { dispatch({ type: "auth.prompt" }) return } dispatch({ type: "auth.pending" }) - const start = Date.now() await serverSDK() - .client.provider.oauth.authorize( - { - providerID: props.provider, - method: index, - inputs, - }, - { throwOnError: true }, - ) + .api.integration.oauth.connect({ + integrationID: props.provider, + methodID: method.id, + inputs: inputs ?? {}, + location: location(), + }) .then((x) => { if (!alive.value) return - const elapsed = Date.now() - start - const delay = 1000 - elapsed - - if (delay > 0) { - if (timer.current !== undefined) clearTimeout(timer.current) - timer.current = setTimeout(() => { - timer.current = undefined - if (!alive.value) return - dispatch({ type: "auth.complete", authorization: x.data! }) - }, delay) - return - } - dispatch({ type: "auth.complete", authorization: x.data! }) + dispatch({ type: "auth.complete", authorization: x.data }) }) .catch((e) => { if (!alive.value) return @@ -360,9 +570,9 @@ function ProviderConnection(props: { index: 0, }) - const prompts = createMemo>(() => { + const prompts = createMemo(() => { const value = method() - return value?.prompts ?? [] + return value?.type === "oauth" ? (value.prompts ?? []) : [] }) const matches = (prompt: NonNullable[number]>, value: Record) => { if (!prompt.when) return true @@ -393,10 +603,6 @@ function ProviderConnection(props: { setFormStore("index", next) return } - if (method()?.type === "api") { - dispatch({ type: "auth.inputs", inputs: value }) - return - } await selectMethod(store.methodIndex, value) } @@ -498,7 +704,9 @@ function ProviderConnection(props: { }) async function complete() { - await serverSDK().client.global.dispose() + await serverSync() + .refreshProviders() + .catch(() => undefined) dialog.close() showToast({ variant: "success", @@ -519,6 +727,37 @@ function ProviderConnection(props: { props.setBack(goBack) function MethodSelection() { + if (newLayout()) + return ( +
+
+ {language.t("provider.connect.selectMethod", { provider: provider().name })} +
+
+ + {(item, index) => { + const details = () => methodDetails(item) + return ( + + ) + }} + +
+
+ ) + return ( <>
@@ -531,7 +770,7 @@ function ProviderConnection(props: { listRef = ref }} items={methods} - key={(m) => m?.label} + key={(m) => m?.label ?? m?.type} onSelect={async (selected, index) => { if (!selected) return void selectMethod(index) @@ -552,11 +791,18 @@ function ProviderConnection(props: { } function ApiAuthView() { + let apiKey: HTMLInputElement | undefined + const errorID = createUniqueId() const [formStore, setFormStore] = createStore({ value: "", error: undefined as string | undefined, }) + onMount(() => { + if (!newLayout()) return + apiKey?.focus({ preventScroll: true }) + }) + async function handleSubmit(e: SubmitEvent) { e.preventDefault() @@ -570,17 +816,67 @@ function ProviderConnection(props: { } setFormStore("error", undefined) - await serverSDK().client.auth.set({ - providerID: props.provider, - auth: { - type: "api", - key: apiKey, - ...(store.promptInputs ? { metadata: store.promptInputs } : {}), - }, + await serverSDK().api.integration.connect.key({ + integrationID: props.provider, + location: location(), + key: apiKey, }) await complete() } + if (newLayout()) + return ( +
+ +
+
{language.t("provider.connect.opencodeZen.line1")}
+
{language.t("provider.connect.opencodeZen.line2")}
+
+ {language.t("provider.connect.opencodeZen.visit.prefix")} + + {language.t("provider.connect.opencodeZen.visit.link")} + + {language.t("provider.connect.opencodeZen.visit.suffix")} +
+
+
+
+ + + {(error) => ( + + )} + + + {language.t("common.continue")} + +
+
+ ) + return (
@@ -590,9 +886,9 @@ function ProviderConnection(props: {
{language.t("provider.connect.opencodeZen.line2")}
{language.t("provider.connect.opencodeZen.visit.prefix")} - + {language.t("provider.connect.opencodeZen.visit.link")} - + {language.t("provider.connect.opencodeZen.visit.suffix")}
@@ -605,7 +901,8 @@ function ProviderConnection(props: {
{ + if (!newLayout()) return + codeInput?.focus({ preventScroll: true }) + }) + async function handleSubmit(e: SubmitEvent) { e.preventDefault() @@ -643,12 +947,13 @@ function ProviderConnection(props: { setFormStore("error", undefined) const result = await serverSDK() - .client.provider.oauth.callback({ - providerID: props.provider, - method: store.methodIndex, + .api.integration.oauth.complete({ + integrationID: props.provider, + attemptID: store.authorization!.attemptID, + location: location(), code, }) - .then((value) => (value.error ? { ok: false as const, error: value.error } : { ok: true as const })) + .then(() => ({ ok: true as const })) .catch((error) => ({ ok: false as const, error })) if (result.ok) { await complete() @@ -657,16 +962,59 @@ function ProviderConnection(props: { setFormStore("error", formatError(result.error, language.t("provider.connect.oauth.code.invalid"))) } + if (newLayout()) + return ( +
+
+ {language.t("provider.connect.oauth.code.visit.prefix")} + + {language.t("provider.connect.oauth.code.visit.link")} + + {language.t("provider.connect.oauth.code.visit.suffix", { provider: provider().name })} +
+ + + + {(error) => ( + + )} + + + {language.t("common.continue")} + + +
+ ) + return (
{language.t("provider.connect.oauth.code.visit.prefix")} - {language.t("provider.connect.oauth.code.visit.link")} + + {language.t("provider.connect.oauth.code.visit.link")} + {language.t("provider.connect.oauth.code.visit.suffix", { provider: provider().name })}
{ - void (async () => { + const poll = async () => { + const authorization = store.authorization + if (!authorization || !alive.value) return const result = await serverSDK() - .client.provider.oauth.callback({ - providerID: props.provider, - method: store.methodIndex, + .api.integration.oauth.status({ + integrationID: props.provider, + attemptID: authorization.attemptID, + location: location(), }) - .then((value) => (value.error ? { ok: false as const, error: value.error } : { ok: true as const })) + .then((value) => ({ ok: true as const, status: value.data })) .catch((error) => ({ ok: false as const, error })) - if (!alive.value) return - if (!result.ok) { - const message = formatError(result.error, language.t("common.requestFailed")) - dispatch({ type: "auth.error", error: message }) + dispatch({ type: "auth.error", error: formatError(result.error, language.t("common.requestFailed")) }) return } - - await complete() - })() + if (result.status.status === "complete") { + await complete() + return + } + if (result.status.status === "failed") { + dispatch({ type: "auth.error", error: result.status.message }) + return + } + if (result.status.status === "expired") { + dispatch({ type: "auth.error", error: language.t("common.requestFailed") }) + return + } + timer.current = setTimeout(poll, 1_000) + } + void poll() }) return (
{language.t("provider.connect.oauth.auto.visit.prefix")} - {language.t("provider.connect.oauth.auto.visit.link")} + + {language.t("provider.connect.oauth.auto.visit.link")} + {language.t("provider.connect.oauth.auto.visit.suffix", { provider: provider().name })}
-
- -
+
+
+ +
{language.t("provider.connect.title.anthropicProMax")} @@ -750,8 +1121,12 @@ function ProviderConnection(props: {
-
-
+
+
@@ -783,15 +1158,15 @@ function ProviderConnection(props: {
- + - + - + diff --git a/packages/app/src/components/dialog-custom-provider.tsx b/packages/app/src/components/dialog-custom-provider.tsx index fb5aa04260..e34e4c39b8 100644 --- a/packages/app/src/components/dialog-custom-provider.tsx +++ b/packages/app/src/components/dialog-custom-provider.tsx @@ -8,7 +8,7 @@ import { TextField } from "@opencode-ai/ui/text-field" import { showToast } from "@/utils/toast" import { batch, For } from "solid-js" import { createStore, produce } from "solid-js/store" -import { Link } from "@/components/link" +import { ExternalLink } from "@/components/external-link" import { useServerSDK } from "@/context/server-sdk" import { useServerSync } from "@/context/server-sync" import { useLanguage } from "@/context/language" @@ -40,7 +40,7 @@ export function DialogCustomProvider(props: Props) { ) } -export function CustomProviderForm() { +export function CustomProviderForm(props: { autofocus?: boolean } = {}) { const dialog = useDialog() const serverSync = useServerSync() const serverSDK = useServerSDK() @@ -131,6 +131,7 @@ export function CustomProviderForm() { const saveMutation = useMutation(() => ({ mutationFn: async (result: NonNullable>) => { + if ((await serverSDK().protocol) !== "v1") throw new Error("Custom providers are unavailable on this server") const disabledProviders = serverSync().data.config.disabled_providers ?? [] const nextDisabled = disabledProviders.filter((id) => id !== result.providerID) @@ -177,22 +178,22 @@ export function CustomProviderForm() { return (
- +
{language.t("provider.custom.title")}

{language.t("provider.custom.description.prefix")} - + {language.t("provider.custom.description.link")} - + {language.t("provider.custom.description.suffix")}

+ + + {language.t("dialog.project.edit.title")} + + + + + {language.t("dialog.project.edit.name")} + model.setStore("name", event.currentTarget.value)} + /> + + +
+
+ {language.t("dialog.project.edit.icon")} +
+
+ + { + model.setIconInput(element) + }} + type="file" + accept="image/*" + class="hidden" + onChange={model.inputChange} + /> +
+ {language.t("dialog.project.edit.icon.hint")} + {language.t("dialog.project.edit.icon.recommended")} +
+
+
+ + +
+
+ {language.t("dialog.project.edit.color")} +
+
+ + {(color) => ( + + )} + +
+
+
+ + + {language.t("dialog.project.edit.worktree.startup")} + {language.t("dialog.project.edit.worktree.startup.description")} + model.setStore("startup", event.currentTarget.value)} + /> + +
+ + + {language.t("common.cancel")} + + + {model.save.isPending ? language.t("common.saving") : language.t("common.save")} + + + + + ) +} diff --git a/packages/app/src/components/dialog-edit-project.tsx b/packages/app/src/components/dialog-edit-project.tsx index b861492af5..86a9630359 100644 --- a/packages/app/src/components/dialog-edit-project.tsx +++ b/packages/app/src/components/dialog-edit-project.tsx @@ -1,123 +1,32 @@ import { Button } from "@opencode-ai/ui/button" -import { useDialog } from "@opencode-ai/ui/context/dialog" import { Dialog } from "@opencode-ai/ui/dialog" import { TextField } from "@opencode-ai/ui/text-field" -import { useMutation } from "@tanstack/solid-query" import { Icon } from "@opencode-ai/ui/icon" -import { createMemo, For, Show } from "solid-js" -import { createStore } from "solid-js/store" +import { For, Show } from "solid-js" import { type LocalProject, getAvatarColors } from "@/context/layout" -import { getFilename } from "@opencode-ai/core/util/path" import { Avatar } from "@opencode-ai/ui/avatar" import { useLanguage } from "@/context/language" import { getProjectAvatarSource } from "@/pages/layout/helpers" import { ServerConnection } from "@/context/server" -import { useGlobal } from "@/context/global" +import { createEditProjectModel } from "./edit-project" const AVATAR_COLOR_KEYS = ["pink", "mint", "orange", "purple", "cyan", "lime"] as const export function DialogEditProject(props: { project: LocalProject; server: ServerConnection.Any }) { - const dialog = useDialog() - const global = useGlobal() const language = useLanguage() - const serverCtx = createMemo(() => global.ensureServerCtx(props.server)) - const serverSDK = () => serverCtx().sdk - const serverSync = () => serverCtx().sync - - const folderName = createMemo(() => getFilename(props.project.worktree)) - const defaultName = createMemo(() => props.project.name || folderName()) - - const [store, setStore] = createStore({ - name: defaultName(), - color: props.project.icon?.color, - iconOverride: props.project.icon?.override, - startup: props.project.commands?.start ?? "", - dragOver: false, - iconHover: false, - }) - - let iconInput: HTMLInputElement | undefined - - function handleFileSelect(file: File) { - if (!file.type.startsWith("image/")) return - const reader = new FileReader() - reader.onload = (e) => { - setStore("iconOverride", e.target?.result as string) - setStore("iconHover", false) - } - reader.readAsDataURL(file) - } - - function handleDrop(e: DragEvent) { - e.preventDefault() - setStore("dragOver", false) - const file = e.dataTransfer?.files[0] - if (file) handleFileSelect(file) - } - - function handleDragOver(e: DragEvent) { - e.preventDefault() - setStore("dragOver", true) - } - - function handleDragLeave() { - setStore("dragOver", false) - } - - function handleInputChange(e: Event) { - const input = e.target as HTMLInputElement - const file = input.files?.[0] - if (file) handleFileSelect(file) - } - - function clearIcon() { - setStore("iconOverride", "") - } - - const saveMutation = useMutation(() => ({ - mutationFn: async () => { - const name = store.name.trim() === folderName() ? "" : store.name.trim() - const start = store.startup.trim() - - if (props.project.id && props.project.id !== "global") { - await serverSDK().client.project.update({ - projectID: props.project.id, - directory: props.project.worktree, - name, - icon: { color: store.color || "", override: store.iconOverride || "" }, - commands: { start }, - }) - serverSync().project.icon(props.project.worktree, store.iconOverride || undefined) - dialog.close() - return - } - - serverSync().project.meta(props.project.worktree, { - name, - icon: { color: store.color || undefined, override: store.iconOverride || undefined }, - commands: { start: start || undefined }, - }) - dialog.close() - }, - })) - - function handleSubmit(e: SubmitEvent) { - e.preventDefault() - if (saveMutation.isPending) return - saveMutation.mutate() - } + const model = createEditProjectModel(props) return ( -
+
setStore("name", v)} + placeholder={model.folderName()} + value={model.store.name} + onChange={(v) => model.setStore("name", v)} />
@@ -125,38 +34,32 @@ export function DialogEditProject(props: { project: LocalProject; server: Server
setStore("iconHover", true)} - onMouseLeave={() => setStore("iconHover", false)} + onMouseEnter={() => model.setStore("iconHover", true)} + onMouseLeave={() => model.setStore("iconHover", false)} >
{ - if (store.iconOverride && store.iconHover) { - clearIcon() - } else { - iconInput?.click() - } + "border-text-interactive-base bg-surface-info-base/20": model.store.dragOver, + "border-border-base hover:border-border-strong": !model.store.dragOver, + "overflow-hidden": !!model.store.iconOverride, }} + onDrop={model.drop} + onDragOver={model.dragOver} + onDragLeave={model.dragLeave} + onClick={model.iconClick} >
@@ -174,8 +77,8 @@ export function DialogEditProject(props: { project: LocalProject; server: Server
@@ -183,8 +86,8 @@ export function DialogEditProject(props: { project: LocalProject; server: Server
@@ -193,12 +96,12 @@ export function DialogEditProject(props: { project: LocalProject; server: Server { - iconInput = el + model.setIconInput(el) }} type="file" accept="image/*" class="hidden" - onChange={handleInputChange} + onChange={model.inputChange} />
{language.t("dialog.project.edit.icon.hint")} @@ -207,7 +110,7 @@ export function DialogEditProject(props: { project: LocalProject; server: Server
- +
@@ -216,21 +119,21 @@ export function DialogEditProject(props: { project: LocalProject; server: Server
- -
diff --git a/packages/app/src/components/dialog-fork.tsx b/packages/app/src/components/dialog-fork.tsx index 601f03084c..5187d980ea 100644 --- a/packages/app/src/components/dialog-fork.tsx +++ b/packages/app/src/components/dialog-fork.tsx @@ -69,15 +69,11 @@ export const DialogFork: Component = () => { const dir = base64Encode(sdk().directory) sdk() - .client.session.fork({ sessionID, messageID: item.id }) + .api.session.fork({ sessionID, messageID: item.id }) .then((forked) => { - if (!forked.data) { - showToast({ title: language.t("common.requestFailed") }) - return - } dialog.close() - prompt.set(restored, undefined, { dir, id: forked.data.id }) - navigate(`/${dir}/session/${forked.data.id}`) + prompt.set(restored, undefined, { dir, id: forked.id }) + navigate(`/${dir}/session/${forked.id}`) }) .catch((err: unknown) => { const message = err instanceof Error ? err.message : String(err) diff --git a/packages/app/src/components/dialog-select-directory-v2.tsx b/packages/app/src/components/dialog-select-directory-v2.tsx index 69d46ddcb6..f3376ad35f 100644 --- a/packages/app/src/components/dialog-select-directory-v2.tsx +++ b/packages/app/src/components/dialog-select-directory-v2.tsx @@ -8,6 +8,7 @@ import { createEffect, createMemo, createResource, createSignal, For, onCleanup, import { useGlobal } from "@/context/global" import { useLanguage } from "@/context/language" import { ServerConnection } from "@/context/server" +import type { Path } from "@opencode-ai/sdk/v2/client" import { absoluteTreePath, activeTreeNavigation, @@ -28,6 +29,7 @@ import { } from "./directory-picker-domain" import "./dialog-select-directory-v2.css" import { DividerV2 } from "@opencode-ai/ui/v2/divider-v2" +import { getFilename } from "@opencode-ai/core/util/path" interface DialogSelectDirectoryV2Props { title?: string @@ -67,11 +69,13 @@ export function DialogSelectDirectoryV2(props: DialogSelectDirectoryV2Props) { const missingBase = createMemo(() => !(sync.data.path.home || sync.data.path.directory)) const [fallbackPath] = createResource( () => (missingBase() ? true : undefined), - () => - sdk.client.path + async (): Promise => { + if ((await sdk.protocol) !== "v1") return + return sdk.client.path .get() .then((result) => result.data) - .catch(() => undefined), + .catch(() => undefined) + }, { initialValue: undefined }, ) const home = createMemo(() => sync.data.path.home || fallbackPath()?.home || "") @@ -85,18 +89,26 @@ export function DialogSelectDirectoryV2(props: DialogSelectDirectoryV2Props) { ) const search = createDirectorySearch({ sdk, home, base: () => root() || start() }) const [suggestions] = createResource(input, async (value) => { - const typed = cleanPickerInput(value).replace(/\/+$/, "") + const cleaned = cleanPickerInput(value) + const typed = cleaned.replace(/\/+$/, "") const current = displayPickerPath(root(), value, home()).replace(/\/+$/, "") - if (!typed || typed === current) return { query: value, items: [] } + if (!cleaned || (root() && typed === current)) return { query: value, items: [] } const directories = (await search(value)).map((absolute) => ({ absolute, type: "directory" as const })) if (!policy.includeFiles) return { query: value, items: directories.slice(0, 5) } - const files = await sdk.client.find - .files({ directory: root(), query: pickerFileSearchQuery(root(), value, home()), type: "file", limit: 20 }) - .then((result) => result.data ?? []) + const base = pickerRoot(cleaned) || root() || start() + if (!base) return { query: value, items: directories.slice(0, 5) } + const files = await sdk.api.file + .find({ + location: { directory: base }, + query: pickerFileSearchQuery(base, value, home()), + type: "file", + limit: 20, + }) + .then((result) => result.data) .catch(() => []) const results = [ ...directories, - ...files.map((path) => ({ absolute: absoluteTreePath(root(), path), type: "file" as const })), + ...files.map((entry) => ({ absolute: absoluteTreePath(base, entry.path), type: "file" as const })), ] return { query: value, @@ -115,9 +127,14 @@ export function DialogSelectDirectoryV2(props: DialogSelectDirectoryV2Props) { existing ?? loads.schedule(`${generation}:${key}`, eager ? "background" : "user", () => { if (!activeTreeNavigation(generation, navigation)) return Promise.resolve(undefined) - return sdk.client.file - .list({ directory: absolute, path: "" }) - .then((result) => result.data ?? []) + return sdk.api.file + .list({ location: { directory: absolute } }) + .then((result) => + result.data.map((entry) => ({ + name: getFilename(entry.path.replace(/[\\/]+$/, "")), + type: entry.type, + })), + ) .catch(() => undefined) }) listings.set(key, request) @@ -312,6 +329,7 @@ export function DialogSelectDirectoryV2(props: DialogSelectDirectoryV2Props) { {(suggestion, index) => ( + ) +} + +export default { + title: "App/Dialogs/Select Model", + id: "app-dialog-select-model", +} + +export const WithoutProviders = { + render: () => , +} diff --git a/packages/app/src/components/dialog-select-model-unpaid-v2.tsx b/packages/app/src/components/dialog-select-model-unpaid-v2.tsx index 0ce9f18c16..2d0c3a7305 100644 --- a/packages/app/src/components/dialog-select-model-unpaid-v2.tsx +++ b/packages/app/src/components/dialog-select-model-unpaid-v2.tsx @@ -1,23 +1,26 @@ import { DialogBody, DialogHeader, DialogTitle, DialogV2 } from "@opencode-ai/ui/v2/dialog-v2" import { Icon } from "@opencode-ai/ui/v2/icon" import { ProviderIcon } from "@opencode-ai/ui/provider-icon" -import { ScrollView } from "@opencode-ai/ui/scroll-view" import { Tag } from "@opencode-ai/ui/v2/badge-v2" import { TooltipV2 } from "@opencode-ai/ui/v2/tooltip-v2" import { useDialog } from "@opencode-ai/ui/context/dialog" +import { useTheme } from "@opencode-ai/ui/theme" import { createMemo, onCleanup, onMount, type Component, For, Show } from "solid-js" import { useLocal } from "@/context/local" -import { popularProviders, useProviders } from "@/hooks/use-providers" +import { useProviders } from "@/hooks/use-providers" import { decode64 } from "@/utils/base64" import { useLanguage } from "@/context/language" import { ModelTooltip } from "./model-tooltip" type ModelState = ReturnType["model"] +const featuredProviders = ["opencode", "opencode-go", "openai", "anthropic", "google", "github-copilot"] +const displayModelName = (name: string) => name.replace(/\s+(?:\(free\)|free)$/i, "") export const DialogSelectModelUnpaidV2: Component<{ model?: ModelState }> = (props) => { const local = useLocal() const model = props.model ?? local.model const dialog = useDialog() + const theme = useTheme() const directory = () => decode64(local.slug()) const providers = useProviders(directory) const language = useLanguage() @@ -28,6 +31,7 @@ export const DialogSelectModelUnpaidV2: Component<{ model?: ModelState }> = (pro }) const isFree = (item: ReturnType[number]) => item.provider.id === "opencode" && (!item.cost || item.cost.input === 0) + const freeModels = createMemo(() => model.list().filter(isFree)) const openProviders = (provider?: string) => { void import("./dialog-connect-provider").then((x) => { @@ -62,111 +66,110 @@ export const DialogSelectModelUnpaidV2: Component<{ model?: ModelState }> = (pro }) return ( - + {language.t("dialog.model.select.title")} -
- - -
-
-
-
- {language.t("dialog.model.unpaid.freeModels.title")} -
+ +
+
+
+
+ {language.t("dialog.model.unpaid.freeModels.title")}
- - {(item) => ( - } - > - - - )} -
- -
-
-
-
- {language.t("dialog.model.unpaid.addMore.title")} -
-
-
- { - if (popularProviders.includes(a.id) && popularProviders.includes(b.id)) { - return popularProviders.indexOf(a.id) - popularProviders.indexOf(b.id) - } - return a.name.localeCompare(b.name) - })} - > - {(provider) => ( - - )} - + + {(item) => ( + + } + > + + )} + +
+ +
+
+
+
+ {language.t("dialog.model.unpaid.addMore.title")}
+
+ featuredProviders.includes(provider.id)) + .sort((a, b) => featuredProviders.indexOf(a.id) - featuredProviders.indexOf(b.id))} + > + {(provider) => ( + + )} + + +
- +
) diff --git a/packages/app/src/components/dialog-select-model.tsx b/packages/app/src/components/dialog-select-model.tsx index 4fb7891ec9..9066f72434 100644 --- a/packages/app/src/components/dialog-select-model.tsx +++ b/packages/app/src/components/dialog-select-model.tsx @@ -1,15 +1,5 @@ import { Popover as Kobalte } from "@kobalte/core/popover" -import { - Component, - ComponentProps, - createEffect, - createMemo, - For, - JSX, - onCleanup, - Show, - ValidComponent, -} from "solid-js" +import { Component, ComponentProps, createEffect, createMemo, For, JSX, Show } from "solid-js" import { createStore } from "solid-js/store" import { useLocal } from "@/context/local" import { useDialog } from "@opencode-ai/ui/context/dialog" @@ -29,6 +19,7 @@ import { ModelTooltip } from "./model-tooltip" import { useLanguage } from "@/context/language" import { decode64 } from "@/utils/base64" import { handleDocumentSearchKeydown } from "@/utils/search-keydown" +import { createMenuDismissController } from "@/utils/menu-dismiss-controller" import { createEventListener } from "@solid-primitives/event-listener" import { matchesModelSearch } from "./dialog-select-model-search" @@ -122,14 +113,13 @@ const ModelList: Component<{ } type ModelSelectorTriggerProps = Omit, "as" | "ref"> +type ModelSelectorTrigger = (props: ModelSelectorTriggerProps) => JSX.Element type Dismiss = "escape" | "outside" | "select" | "manage" | "provider" export function ModelSelectorPopover(props: { provider?: string model?: ModelState - children?: JSX.Element - triggerAs?: ValidComponent - triggerProps?: ModelSelectorTriggerProps + trigger: ModelSelectorTrigger onClose?: (cause: "escape" | "select") => void }) { const [store, setStore] = createStore<{ @@ -174,9 +164,7 @@ export function ModelSelectorPopover(props: { placement="top-start" gutter={4} > - - {props.children} - + void }) { - const model = props.model ?? useLocal().model - const language = useLanguage() const dialog = useDialog() - const [store, setStore] = createStore({ open: false, search: "", active: "" }) - let searchRef: HTMLInputElement | undefined - let contentRef: HTMLDivElement | undefined - let restoreTrigger = true + const controller = createModelSelectorController({ + model: props.model, + provider: () => props.provider, + onSelect: () => props.onClose?.(), + }) + return ( + { + void import("./dialog-manage-models").then((module) => { + void dialog.show(() => ) + }) + }} + onClose={() => props.onClose?.()} + /> + ) +} + +function createModelSelectorController(input: { + provider: () => string | undefined + model?: ModelState + onSelect: () => void +}) { + const model = input.model ?? useLocal().model const allModels = createMemo(() => model .list() .filter((item) => model.visible({ modelID: item.id, providerID: item.provider.id })) - .filter((item) => (props.provider ? item.provider.id === props.provider : true)), + .filter((item) => (input.provider() ? item.provider.id === input.provider() : true)), ) - const models = createMemo(() => { - const search = store.search.trim() - const filtered = search - ? allModels().filter((item) => matchesModelSearch(search, [item.name, item.id, item.provider.name])) - : allModels() - return [...filtered].sort((a, b) => a.name.localeCompare(b.name)) - }) - const groups = createMemo(() => { - const byProvider = new Map() - for (const item of models()) { - byProvider.set(item.provider.id, [...(byProvider.get(item.provider.id) ?? []), item]) - } - return Array.from(byProvider, ([category, items]) => ({ category, items })).sort(sortModelGroups) - }) - const keys = () => [...models().map(modelKey), manageKey] - const current = () => { - const value = model.current() - return value ? `${value.provider.id}:${value.id}` : undefined + return { + models: (search: string) => { + const query = search.trim() + const filtered = query + ? allModels().filter((item) => matchesModelSearch(query, [item.name, item.id, item.provider.name])) + : allModels() + return [...filtered].sort((a, b) => a.name.localeCompare(b.name)) + }, + groups: (models: ModelItem[]) => { + const byProvider = new Map() + for (const item of models) { + byProvider.set(item.provider.id, [...(byProvider.get(item.provider.id) ?? []), item]) + } + return Array.from(byProvider, ([category, items]) => ({ category, items })).sort(sortModelGroups) + }, + current: () => { + const value = model.current() + return value ? modelKey(value) : undefined + }, + select: (item: ModelItem) => { + model.set({ modelID: item.id, providerID: item.provider.id }, { recent: true }) + input.onSelect() + }, } +} + +function ModelSelectorPopoverV2View(props: { + trigger: ModelSelectorTrigger + models: (search: string) => ModelItem[] + groups: (models: ModelItem[]) => { category: string; items: ModelItem[] }[] + current: () => string | undefined + select: (item: ModelItem) => void + onManage: () => void + onClose: () => void +}) { + const language = useLanguage() + const [store, setStore] = createStore({ open: false, search: "", active: "" }) + let searchRef: HTMLInputElement | undefined + let contentRef: HTMLDivElement | undefined + const dismiss = createMenuDismissController(() => contentRef) + + const models = createMemo(() => props.models(store.search)) + const groups = createMemo(() => props.groups(models())) + const keys = () => [...models().map(modelKey), manageKey] const initialActive = () => { - const selected = current() + const selected = props.current() const options = keys() if (selected && options.includes(selected)) return selected return options[0] ?? "" } const activeItem = () => store.active ? contentRef?.querySelector(`[data-option-key="${CSS.escape(store.active)}"]`) : undefined - const afterClose = (callback: () => void) => { - const complete = () => { - if (contentRef?.isConnected) { - requestAnimationFrame(complete) - return - } - requestAnimationFrame(() => requestAnimationFrame(callback)) - } - requestAnimationFrame(complete) - } const setOpen = (open: boolean) => { if (open) { - restoreTrigger = true + dismiss.allowTriggerRestore() setStore({ open: true, active: initialActive() }) setTimeout(() => requestAnimationFrame(() => { @@ -308,23 +331,15 @@ export function ModelSelectorPopoverV2(props: { } setStore({ open: false, search: "", active: "" }) } - const select = (item: ModelItem) => { - model.set({ modelID: item.id, providerID: item.provider.id }, { recent: true }) - props.onClose?.() - } const selectModel = (item: ModelItem) => { - restoreTrigger = false + dismiss.preventTriggerRestore() setOpen(false) - afterClose(() => select(item)) + dismiss.afterClose(() => props.select(item)) } const manage = () => { - restoreTrigger = false + dismiss.preventTriggerRestore() setOpen(false) - afterClose(() => { - void import("./dialog-manage-models").then((x) => { - dialog.show(() => ) - }) - }) + dismiss.afterClose(props.onManage) } const selectActive = () => { const item = models().find((item) => modelKey(item) === store.active) @@ -343,10 +358,7 @@ export function ModelSelectorPopoverV2(props: { queueMicrotask(() => activeItem()?.scrollIntoView({ block: "nearest" })) } const setSearch = (value: string) => { - const search = value.trim() - const first = [...allModels()] - .sort((a, b) => a.name.localeCompare(b.name)) - .find((item) => matchesModelSearch(search, [item.name, item.id, item.provider.name])) + const first = props.models(value)[0] setStore({ search: value, active: first ? modelKey(first) : manageKey }) } @@ -362,18 +374,14 @@ export function ModelSelectorPopoverV2(props: { return ( - - {props.children} - + (contentRef = el)} + ref={(element: HTMLDivElement) => (contentRef = element)} class="w-[284px] overflow-hidden rounded-md border-0 bg-v2-background-bg-layer-01 !p-0 shadow-[var(--v2-elevation-floating)] focus:outline-none" - onPointerDownOutside={() => (restoreTrigger = false)} - onFocusOutside={() => (restoreTrigger = false)} - onCloseAutoFocus={(event) => { - if (!restoreTrigger) event.preventDefault() - }} + onPointerDownOutside={dismiss.preventTriggerRestore} + onFocusOutside={dismiss.preventTriggerRestore} + onCloseAutoFocus={dismiss.onCloseAutoFocus} >
@@ -393,9 +401,9 @@ export function ModelSelectorPopoverV2(props: { event.stopPropagation() if (event.key === "Escape") { event.preventDefault() - restoreTrigger = false + dismiss.preventTriggerRestore() setOpen(false) - afterClose(() => props.onClose?.()) + dismiss.afterClose(props.onClose) return } if (event.altKey || event.metaKey) return @@ -445,7 +453,7 @@ export function ModelSelectorPopoverV2(props: { {group.items[0].provider.name} - + {(item) => ( { diff --git a/packages/app/src/components/dialog-select-server.tsx b/packages/app/src/components/dialog-select-server.tsx index 0876906890..aa16976228 100644 --- a/packages/app/src/components/dialog-select-server.tsx +++ b/packages/app/src/components/dialog-select-server.tsx @@ -16,6 +16,7 @@ import { useGlobal } from "@/context/global" import { useLanguage } from "@/context/language" import { usePlatform } from "@/context/platform" import { normalizeServerUrl, ServerConnection, useServer } from "@/context/server" +import { detectServerProtocol } from "@/utils/server-protocol" import { type ServerHealth, useCheckServerHealth } from "@/utils/server-health" import { useSettings } from "@/context/settings" import { useTabs } from "@/context/tabs" @@ -263,6 +264,13 @@ export function useServerManagementController(options: { onSelect?: () => void; setStore("addServer", { error: language.t("dialog.server.add.error") }) return } + if ( + !settings.general.newLayoutDesigns() && + (await detectServerProtocol(conn.http, platform.fetch ?? globalThis.fetch)) === "v2" + ) { + setStore("addServer", { error: language.t("dialog.server.add.error") }) + return + } resetAdd() if (options.navigateOnAdd === false) { @@ -307,6 +315,13 @@ export function useServerManagementController(options: { onSelect?: () => void; setStore("editServer", { error: language.t("dialog.server.add.error") }) return } + if ( + !settings.general.newLayoutDesigns() && + (await detectServerProtocol(conn.http, platform.fetch ?? globalThis.fetch)) === "v2" + ) { + setStore("editServer", { error: language.t("dialog.server.add.error") }) + return + } if (normalized === input.original.http.url) { server.add(conn) } else { @@ -344,7 +359,10 @@ export function useServerManagementController(options: { onSelect?: () => void; ) const sortedItems = createMemo(() => { - const list = items() + const raw = items() + const list = settings.general.newLayoutDesigns() + ? raw + : raw.filter((x) => global.ensureServerCtx(x).sdk.protocolKind() !== "v2") if (!list.length) return list const active = current() const order = new Map(list.map((url, index) => [url, index] as const)) diff --git a/packages/app/src/components/dialog-usage-exceeded.tsx b/packages/app/src/components/dialog-usage-exceeded.tsx index e428d4c2bb..bf5da751e2 100644 --- a/packages/app/src/components/dialog-usage-exceeded.tsx +++ b/packages/app/src/components/dialog-usage-exceeded.tsx @@ -17,7 +17,7 @@ export function DialogUsageExceeded(props: DialogGoUpsellProps) { const platform = usePlatform() const runAction = () => { - if (props.link) platform.openLink(props.link) + if (props.link) platform.openExternal(props.link) props.onClose?.() dialog.close() } diff --git a/packages/app/src/components/directory-picker-domain.test.ts b/packages/app/src/components/directory-picker-domain.test.ts index 5746410610..1bc9af0833 100644 --- a/packages/app/src/components/directory-picker-domain.test.ts +++ b/packages/app/src/components/directory-picker-domain.test.ts @@ -133,10 +133,10 @@ test("scopes file autocomplete to the current browser root", () => { test("resolves directory autocomplete from the current browser root", async () => { const directories: string[] = [] const sdk = { - client: { - find: { - files: (input: { directory: string }) => { - directories.push(input.directory) + api: { + file: { + find: (input: { location?: { directory?: string } }) => { + directories.push(input.location?.directory ?? "") return Promise.resolve({ data: [] }) }, }, @@ -152,6 +152,29 @@ test("resolves directory autocomplete from the current browser root", async () = expect(directories).toEqual(["/repo", "/repo/src"]) }) +test("searches from an absolute root without a default base", async () => { + const directories: string[] = [] + const sdk = { + api: { + file: { + list: (input: { location?: { directory?: string } }) => { + directories.push(input.location?.directory ?? "") + return Promise.resolve({ + data: [ + { path: "Users/", type: "directory" }, + { path: "tmp/", type: "directory" }, + ], + }) + }, + }, + }, + } as unknown as Parameters[0]["sdk"] + const search = createDirectorySearch({ sdk, home: () => "", base: () => undefined }) + + expect(await search("/")).toEqual(["/Users", "/tmp"]) + expect(directories).toEqual(["/"]) +}) + test("identifies the next directory level to preload", () => { expect( preloadTreeDirectories("src/", [ diff --git a/packages/app/src/components/directory-picker-domain.ts b/packages/app/src/components/directory-picker-domain.ts index 9900265962..9539ae1d01 100644 --- a/packages/app/src/components/directory-picker-domain.ts +++ b/packages/app/src/components/directory-picker-domain.ts @@ -326,15 +326,15 @@ export function createDirectorySearch(args: { sdk: ServerSDK; base: () => string let current = 0 const scoped = (value: string) => { + const raw = normalizePickerDrive(value) + const root = pickerRoot(raw) + if (root) return { directory: trimPickerPath(root), path: raw.slice(root.length) } const base = args.base() if (!base) return - const raw = normalizePickerDrive(value) if (!raw) return { directory: trimPickerPath(base), path: "" } const home = args.home() if (raw === "~") return { directory: trimPickerPath(home || base), path: "" } if (raw.startsWith("~/")) return { directory: trimPickerPath(home || base), path: raw.slice(2) } - const root = pickerRoot(raw) - if (root) return { directory: trimPickerPath(root), path: raw.slice(root.length) } return { directory: trimPickerPath(base), path: raw } } @@ -342,14 +342,17 @@ export function createDirectorySearch(args: { sdk: ServerSDK; base: () => string const key = trimPickerPath(directory) const existing = cache.get(key) if (existing) return existing - const request = args.sdk.client.file - .list({ directory: key, path: "" }) - .then((result) => result.data ?? []) + const request = args.sdk.api.file + .list({ location: { directory: key } }) + .then((result) => result.data) .catch(() => []) .then((nodes) => nodes .filter((node) => node.type === "directory") - .map((node) => ({ name: node.name, absolute: trimPickerPath(normalizePickerDrive(node.absolute)) })), + .map((node) => { + const relative = trimPickerPath(normalizePickerDrive(node.path)) + return { name: getFilename(relative), absolute: joinPickerPath(key, relative) } + }), ) cache.set(key, request) return request @@ -371,9 +374,9 @@ export function createDirectorySearch(args: { sdk: ServerSDK; base: () => string const pathInput = raw.startsWith("~") || !!pickerRoot(raw) || raw.includes("/") const query = normalizePickerDrive(input.path) if (!pathInput) { - const results = await args.sdk.client.find - .files({ directory: input.directory, query, type: "directory", limit: 50 }) - .then((result) => result.data ?? []) + const results = await args.sdk.api.file + .find({ location: { directory: input.directory }, query, type: "directory", limit: 50 }) + .then((result) => result.data.map((entry) => entry.path)) .catch(() => []) if (!active()) return [] return results.map((path) => joinPickerPath(input.directory, path)).slice(0, 50) diff --git a/packages/app/src/components/edit-project.ts b/packages/app/src/components/edit-project.ts new file mode 100644 index 0000000000..0c5d576e93 --- /dev/null +++ b/packages/app/src/components/edit-project.ts @@ -0,0 +1,133 @@ +import { getFilename } from "@opencode-ai/core/util/path" +import { useDialog } from "@opencode-ai/ui/context/dialog" +import { useMutation } from "@tanstack/solid-query" +import { normalizeProjectInfo } from "@/context/global-sync/utils" +import { createMemo } from "solid-js" +import { createStore } from "solid-js/store" +import { useGlobal } from "@/context/global" +import { type LocalProject } from "@/context/layout" +import { ServerConnection } from "@/context/server" + +export function createEditProjectModel(props: { project: LocalProject; server: ServerConnection.Any }) { + const dialog = useDialog() + const global = useGlobal() + const serverCtx = createMemo(() => global.ensureServerCtx(props.server)) + const folderName = createMemo(() => getFilename(props.project.worktree)) + const defaultName = createMemo(() => props.project.name || folderName()) + const [store, setStore] = createStore({ + name: defaultName(), + color: props.project.icon?.color, + iconOverride: props.project.icon?.override, + startup: props.project.commands?.start ?? "", + dragOver: false, + iconHover: false, + }) + let iconInput: HTMLInputElement | undefined + + function selectFile(file: File) { + if (!file.type.startsWith("image/")) return + const reader = new FileReader() + reader.onload = (event) => { + const result = event.target?.result + if (typeof result !== "string") return + setStore("iconOverride", result) + setStore("iconHover", false) + } + reader.readAsDataURL(file) + } + + function drop(event: DragEvent) { + event.preventDefault() + setStore("dragOver", false) + const file = event.dataTransfer?.files[0] + if (file) selectFile(file) + } + + function dragOver(event: DragEvent) { + event.preventDefault() + setStore("dragOver", true) + } + + function dragLeave() { + setStore("dragOver", false) + } + + function inputChange(event: Event) { + const file = (event.currentTarget as HTMLInputElement).files?.[0] + if (file) selectFile(file) + } + + function iconClick() { + if (store.iconOverride && store.iconHover) { + setStore("iconOverride", "") + return + } + iconInput?.click() + } + + const save = useMutation(() => ({ + mutationFn: async () => { + const name = store.name.trim() === folderName() ? "" : store.name.trim() + const start = store.startup.trim() + + if (props.project.id && props.project.id !== "global") { + if ((await serverCtx().sdk.protocol) !== "v1") return + const project = await serverCtx() + .sdk.client.project.update({ + projectID: props.project.id, + directory: props.project.worktree, + name, + icon: { color: store.color || "", override: store.iconOverride || "" }, + commands: { start }, + }) + .then((result) => result.data) + if (!project) return + // const project = await serverCtx().sdk.api.project.update({ + // projectID: props.project.id, + // name, + // icon: { color: store.color || "", override: store.iconOverride || "" }, + // commands: { start }, + // }) + serverCtx().sync.set("project", (items) => + items.map((item) => (item.id === project.id ? normalizeProjectInfo(project) : item)), + ) + serverCtx().sync.project.icon(props.project.worktree, store.iconOverride || undefined) + dialog.close() + return + } + + serverCtx().sync.project.meta(props.project.worktree, { + name, + icon: { color: store.color || undefined, override: store.iconOverride || undefined }, + commands: { start: start || undefined }, + }) + dialog.close() + }, + })) + + function submit(event: SubmitEvent) { + event.preventDefault() + if (save.isPending) return + save.mutate() + } + + return { + store, + setStore, + folderName, + defaultName, + save, + submit, + drop, + dragOver, + dragLeave, + inputChange, + iconClick, + close() { + dialog.close() + }, + setIconInput(input: HTMLInputElement) { + iconInput = input + }, + } +} diff --git a/packages/app/src/components/external-link.tsx b/packages/app/src/components/external-link.tsx new file mode 100644 index 0000000000..133e752eab --- /dev/null +++ b/packages/app/src/components/external-link.tsx @@ -0,0 +1,21 @@ +import { ComponentProps, splitProps } from "solid-js" + +export interface ExternalLinkProps extends Omit, "href"> { + href: string +} + +export function ExternalLink(props: ExternalLinkProps) { + const [local, rest] = splitProps(props, ["href", "children", "class", "target", "rel"]) + + return ( + + {local.children} + + ) +} diff --git a/packages/app/src/components/file-tree-v2-model.test.ts b/packages/app/src/components/file-tree-v2-model.test.ts index 288f7112e9..f2a63fe86d 100644 --- a/packages/app/src/components/file-tree-v2-model.test.ts +++ b/packages/app/src/components/file-tree-v2-model.test.ts @@ -1,8 +1,9 @@ import { describe, expect, test } from "bun:test" -import { buildFileTreeV2Model, flattenFileTreeV2 } from "./file-tree-v2-model" +import { buildFileTreeV2Model, flattenFileTreeV2, flattenLiveFileTreeV2 } from "./file-tree-v2-model" +import type { FileNode } from "@opencode-ai/sdk/v2" -describe("file tree v2 model", () => { - test("builds sorted depth-first rows", () => { +describe("buildFileTreeV2Model", () => { + test("builds a sorted tree and flattens expanded directories", () => { const model = buildFileTreeV2Model(["src/z.ts", "src/lib/b.ts", "src/lib/a.ts", "README.md", "docs/guide.md"]) expect(model.total).toBe(8) @@ -18,7 +19,7 @@ describe("file tree v2 model", () => { ]) }) - test("omits descendants of collapsed directories", () => { + test("skips children of collapsed directories", () => { const model = buildFileTreeV2Model(["src/lib/a.ts", "src/z.ts"]) expect(flattenFileTreeV2(model, (path) => path !== "src/lib").map((row) => row.node.path)).toEqual([ @@ -28,19 +29,46 @@ describe("file tree v2 model", () => { ]) }) - test("normalizes separators and duplicate paths", () => { + test("normalizes duplicate and messy paths", () => { const model = buildFileTreeV2Model(["src\\lib\\a.ts", "src/lib/a.ts", "/src//lib/b.ts/"]) const rows = flattenFileTreeV2(model, () => true) - expect(model.total).toBe(4) expect(rows.map((row) => row.node.path)).toEqual(["src", "src/lib", "src/lib/a.ts", "src/lib/b.ts"]) expect(rows.find((row) => row.node.path === "src/lib/a.ts")?.node.originalPath).toBe("src\\lib\\a.ts") }) - test("supports paths deeper than the legacy recursion limit", () => { - const file = `${Array.from({ length: 130 }, (_, index) => `dir-${index}`).join("/")}/file.ts` + test("handles deeply nested paths", () => { + const file = Array.from({ length: 130 }, (_, index) => `d${index}`).join("/") + "/leaf.ts" const model = buildFileTreeV2Model([file]) expect(flattenFileTreeV2(model, () => true)).toHaveLength(131) }) }) + +describe("flattenLiveFileTreeV2", () => { + test("flattens live children using original paths for nested lookups", () => { + const nodes: Record = { + "": [ + { name: "src", path: "src", absolute: "/repo/src", type: "directory", ignored: false }, + { name: "README.md", path: "README.md", absolute: "/repo/README.md", type: "file", ignored: false }, + ], + src: [ + { name: "a.ts", path: "src/a.ts", absolute: "/repo/src/a.ts", type: "file", ignored: false }, + { name: "lib", path: "src/lib", absolute: "/repo/src/lib", type: "directory", ignored: false }, + ], + "src/lib": [{ name: "b.ts", path: "src/lib/b.ts", absolute: "/repo/src/lib/b.ts", type: "file", ignored: false }], + } + + expect( + flattenLiveFileTreeV2( + (path) => nodes[path] ?? [], + (path) => path === "src", + ).map((row) => [row.node.path, row.node.originalPath, row.level]), + ).toEqual([ + ["src", "src", 0], + ["src/a.ts", "src/a.ts", 1], + ["src/lib", "src/lib", 1], + ["README.md", "README.md", 0], + ]) + }) +}) diff --git a/packages/app/src/components/file-tree-v2-model.ts b/packages/app/src/components/file-tree-v2-model.ts index 2127b6800f..27783a9bb1 100644 --- a/packages/app/src/components/file-tree-v2-model.ts +++ b/packages/app/src/components/file-tree-v2-model.ts @@ -75,3 +75,33 @@ export function flattenFileTreeV2(model: FileTreeV2Model, expanded: (path: strin return rows } + +export function flattenLiveFileTreeV2( + children: (path: string) => readonly FileNode[], + expanded: (path: string) => boolean, +) { + const rows: FileTreeV2Row[] = [] + const stack = children("") + .toReversed() + .map((node) => ({ node: toLiveNode(node), level: 0 })) + + while (stack.length > 0) { + const row = stack.pop()! + rows.push(row) + if (row.node.type !== "directory" || !expanded(row.node.path)) continue + const nested = children(row.node.originalPath) + for (let index = nested.length - 1; index >= 0; index--) { + stack.push({ node: toLiveNode(nested[index]!), level: row.level + 1 }) + } + } + + return rows +} + +function toLiveNode(node: FileNode): FileTreeV2Node { + return { + ...node, + path: normalizeFileTreeV2Path(node.path), + originalPath: node.path, + } +} diff --git a/packages/app/src/components/file-tree-v2.tsx b/packages/app/src/components/file-tree-v2.tsx index c712a80443..26cf49d411 100644 --- a/packages/app/src/components/file-tree-v2.tsx +++ b/packages/app/src/components/file-tree-v2.tsx @@ -16,7 +16,13 @@ import type { FileNode } from "@opencode-ai/sdk/v2" import { Icon } from "@opencode-ai/ui/v2/icon" import { pathToFileUrl, withFileDragImage, type Kind } from "@/components/file-tree" import { createVirtualizer, defaultRangeExtractor } from "@tanstack/solid-virtual" -import { buildFileTreeV2Model, flattenFileTreeV2, normalizeFileTreeV2Path } from "@/components/file-tree-v2-model" +import { + buildFileTreeV2Model, + flattenFileTreeV2, + flattenLiveFileTreeV2, + normalizeFileTreeV2Path, + type FileTreeV2Node, +} from "@/components/file-tree-v2-model" import { virtualScrollElement } from "@/components/virtual-scroll-element" export type { Kind } from "@/components/file-tree" @@ -36,7 +42,7 @@ function guideLineLeft(level: number) { export const kindLabel = (kind: Kind) => { if (kind === "add") return "A" if (kind === "del") return "D" - return "" + return "M" } export const kindChange = (kind: Kind) => { @@ -68,7 +74,7 @@ const FileTreeNodeV2 = ( "class", "classList", ]) - const kind = () => local.kinds?.get(local.node.path) + const kind = () => local.kinds?.get(normalizeFileTreeV2Path(local.node.path)) return ( - {(_, index) => ( -
- )} + {(_, index) =>
} ) } @@ -126,12 +127,18 @@ export default function FileTreeV2(props: { kinds?: ReadonlyMap draggable?: boolean onFileClick?: (file: FileNode) => void + onFileDoubleClick?: (file: FileNode) => void }) { const file = useFile() + const live = () => props.allowed === undefined const draggable = () => props.draggable ?? true const active = () => normalizeFileTreeV2Path(props.active ?? "") - const model = createMemo(() => buildFileTreeV2Model(props.allowed ?? [])) - const rows = createMemo(() => flattenFileTreeV2(model(), (path) => file.tree.state(path)?.expanded ?? true)) + const model = createMemo(() => (live() ? undefined : buildFileTreeV2Model(props.allowed ?? []))) + const expanded = (path: string) => file.tree.state(path)?.expanded ?? !live() + const rows = createMemo(() => { + if (live()) return flattenLiveFileTreeV2((path) => file.tree.children(path), expanded) + return flattenFileTreeV2(model()!, expanded) + }) const [root, setRoot] = createSignal() const [focused, setFocused] = createSignal() const virtualizer = createVirtualizer({ @@ -155,16 +162,49 @@ export default function FileTreeV2(props: { return [...indexes, index].sort((a, b) => a - b) }, }) + + createEffect(() => { + if (!live()) return + void file.tree.list("") + }) + + // Only scroll when the active path changes (or first appears in the tree). + // Do not re-scroll when expand/collapse reshuffles `rows()`. + let scrolledActive: string | undefined createEffect(() => { const path = active() - if (!path) return + if (!path) { + scrolledActive = undefined + return + } const index = rows().findIndex((row) => row.node.path === path) if (index < 0) return + if (scrolledActive === path) return + scrolledActive = path queueMicrotask(() => { - if (virtualizer.range && index >= virtualizer.range.startIndex && index <= virtualizer.range.endIndex) return - virtualizer.scrollToIndex(index, { align: "auto" }) + const next = rows().findIndex((row) => row.node.path === path) + if (next < 0) return + if (virtualizer.range && next >= virtualizer.range.startIndex && next <= virtualizer.range.endIndex) return + virtualizer.scrollToIndex(next, { align: "auto" }) }) }) + + const selectFile = (node: FileTreeV2Node, action?: (file: FileNode) => void) => { + action?.({ + ...node, + path: node.originalPath, + absolute: node.originalPath, + }) + } + + const toggleDirectory = (path: string, originalPath: string) => { + if (expanded(path)) { + file.tree.collapse(originalPath) + return + } + file.tree.expand(originalPath, live() ? undefined : { list: false }) + } + const rowByKey = createMemo(() => new Map(rows().map((row) => [row.node.path, row] as const))) const virtualItemByKey = createMemo( () => new Map(virtualizer.getVirtualItems().map((item) => [item.key, item] as const)), @@ -175,7 +215,7 @@ export default function FileTreeV2(props: {
@@ -209,13 +249,8 @@ export default function FileTreeV2(props: { class="relative" onFocus={() => setFocused(row().node.path)} onBlur={() => setFocused(undefined)} - onClick={() => - props.onFileClick?.({ - ...row().node, - path: row().node.originalPath, - absolute: row().node.originalPath, - }) - } + onClick={() => selectFile(row().node, props.onFileClick)} + onDblClick={() => selectFile(row().node, props.onFileDoubleClick)} > 0}> @@ -239,17 +274,13 @@ export default function FileTreeV2(props: { class="relative" onFocus={() => setFocused(row().node.path)} onBlur={() => setFocused(undefined)} - aria-expanded={file.tree.state(row().node.path)?.expanded ?? true} - onClick={() => - file.tree.state(row().node.path)?.expanded === false - ? file.tree.expand(row().node.path, { list: false }) - : file.tree.collapse(row().node.path) - } + aria-expanded={expanded(row().node.path)} + onClick={() => toggleDirectory(row().node.path, row().node.originalPath)} >
diff --git a/packages/app/src/components/help-button.tsx b/packages/app/src/components/help-button.tsx index de156e1b33..18dd727084 100644 --- a/packages/app/src/components/help-button.tsx +++ b/packages/app/src/components/help-button.tsx @@ -1,77 +1,35 @@ import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon" import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2" import { createSignal, Show } from "solid-js" -import { createStore } from "solid-js/store" import { Drawer, DrawerClose, DrawerContent } from "@/components/ui/drawer" import { usePlatform } from "@/context/platform" +import { useSettings } from "@/context/settings" import introducingTabsVideo from "@/assets/help/introducing-tabs.mp4" -import { Persist, persisted } from "@/utils/persist" - -const helpIcon = ( - -) - -const triggerClass = - "size-7 !rounded-full shrink-0 bg-v2-background-bg-base shadow-[var(--v2-elevation-button-neutral)]" +import homeImage from "@/assets/help/home.png" +import tabsImage from "@/assets/help/tabs.png" // TODO: wire to changelog / seen-state when available const showPopover = () => true -export function HelpButton() { - if (import.meta.env.VITE_OPENCODE_CHANNEL !== "dev") return null - - const platform = usePlatform() - - return ( - { - event.preventDefault() - platform.openLink(event.currentTarget.href) - }} - > - {helpIcon} - - ) -} - // can remove this after the tabs rollout has been out for a while export function TabsInfoPopup() { - if (import.meta.env.VITE_OPENCODE_CHANNEL !== "dev") return null - - const [state, setState] = persisted(Persist.global("tabsInfoPopup"), createStore({ dismissed: false })) - // setState({ dismissed: false }) // for testing + const settings = useSettings() + const platform = usePlatform() const [drawerOpen, setDrawerOpen] = createSignal(false) + const windows = () => platform.platform === "desktop" && platform.os === "windows" return ( - +
- -
-

- June 16 -

+ + } + class="absolute top-[10px] left-[-36px]" /> + +
+

+ July 14 +

+ + } + /> +
-
+

- Introducing Tabs Navigation. -

-

- We've introduced tabs as the primary navigation in OpenCode. Your most important session are now pinned at - the top of your screen at all times. No more hunting through menus or losing your place mid-session. Switch - contexts instantly, pick up exactly where you left off, and keep your focus where it belongs: on the - sessions. + Introducing Tabs

+
+

OpenCode Desktop is now built around tabs.

+ +

+ Start a new session in a tab, or open an existing session from any of your projects. Open a new tab when + you're starting something new, and close it when you're done. +

+

+ Keeping a few tabs open makes it easier to organize your active sessions. Rename tabs to something + memorable if you plan to keep them around. +

+

+ You'll find all your sessions and projects on the new Home screen. Selecting a session opens it in a tab. +

+ +

When you reopen the app, your tabs are still open.

+

+ The new design does not support Git Worktrees yet, it's coming soon. So if you'd prefer to continue using + the previous layout, you can switch between layouts in Settings. Just keep in mind that the new layout + will become permanent in a few weeks. +

+
diff --git a/packages/app/src/components/link.tsx b/packages/app/src/components/link.tsx deleted file mode 100644 index 85f7efc539..0000000000 --- a/packages/app/src/components/link.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { ComponentProps, splitProps } from "solid-js" -import { usePlatform } from "@/context/platform" - -export interface LinkProps extends Omit, "href"> { - href: string -} - -export function Link(props: LinkProps) { - const platform = usePlatform() - const [local, rest] = splitProps(props, ["href", "children", "class"]) - - return ( - { - if (!local.href) return - event.preventDefault() - platform.openLink(local.href) - }} - {...rest} - > - {local.children} - - ) -} diff --git a/packages/app/src/components/prompt-input-v2.tsx b/packages/app/src/components/prompt-input-v2.tsx new file mode 100644 index 0000000000..96b99aaf4a --- /dev/null +++ b/packages/app/src/components/prompt-input-v2.tsx @@ -0,0 +1,586 @@ +import { ImagePreview } from "@opencode-ai/ui/image-preview" +import { useDialog } from "@opencode-ai/ui/context/dialog" +import { ProviderIcon } from "@opencode-ai/ui/provider-icon" +import { ButtonV2 } from "@opencode-ai/ui/v2/button-v2" +import { Icon } from "@opencode-ai/ui/v2/icon" +import { KeybindV2 } from "@opencode-ai/ui/v2/keybind-v2" +import { TooltipV2 } from "@opencode-ai/ui/v2/tooltip-v2" +import type { ReferenceInfo } from "@opencode-ai/sdk/v2/client" +import { createEffect, createMemo, on, Show } from "solid-js" +import { ModelSelectorPopoverV2 } from "@/components/dialog-select-model" +import { DialogSelectModelUnpaidV2 } from "@/components/dialog-select-model-unpaid-v2" +import type { PromptInputProps } from "@/components/prompt-input/contracts" +import { normalizePromptHistoryEntry, promptLength, type PromptHistoryComment } from "@/components/prompt-input/history" +import { createPersistedPromptInputHistory } from "@/components/prompt-input/history-store" +import { promptDesignPlaceholder, promptPlaceholder } from "@/components/prompt-input/placeholder" +import { createPromptSubmit } from "@/components/prompt-input/submit" +import { selectionFromLines, type SelectedLineRange, useFile } from "@/context/file" +import { useComments } from "@/context/comments" +import { useCommand } from "@/context/command" +import { useLanguage } from "@/context/language" +import { useLayout } from "@/context/layout" +import { usePermission } from "@/context/permission" +import { type ImageAttachmentPart, usePrompt } from "@/context/prompt" +import { usePlatform } from "@/context/platform" +import { useSDK } from "@/context/sdk" +import { useSync } from "@/context/sync" +import { createSessionTabs } from "@/pages/session/helpers" +import { showToast } from "@/utils/toast" +import { PromptInputV2, type PromptInputV2Suggestion } from "@opencode-ai/session-ui/v2/prompt-input" +import { + createPromptInputV2Controller, + createPromptInputV2State, + type PromptInputV2Interaction, +} from "@opencode-ai/session-ui/v2/prompt-input/interaction" + +export type PromptInputV2ComposerProps = { + class?: string + controller: PromptInputV2ComposerController + borderUnderlay?: boolean +} + +export type PromptInputV2ControllerProps = Omit +export type PromptInputV2ComposerController = PromptInputV2Interaction & { + readonly model: PromptInputProps["controls"]["model"] +} + +export function PromptInputV2Composer(props: PromptInputV2ComposerProps) { + const dialog = useDialog() + const command = useCommand() + const language = useLanguage() + + return ( +
+ + dialog.show(() => ) + } + /> + } + /> +
+ ) +} + +export function usePromptInputV2Controller(props: PromptInputV2ControllerProps): PromptInputV2ComposerController { + const sdk = useSDK() + const sync = useSync() + const files = useFile() + const layout = useLayout() + const comments = useComments() + const dialog = useDialog() + const command = useCommand() + const permission = usePermission() + const language = useLanguage() + const platform = usePlatform() + const prompt = props.state ?? usePrompt() + let editor: HTMLDivElement | undefined + + const interaction = createPromptInputV2State() + const mode = () => interaction[0].mode + const history = props.history ?? createPersistedPromptInputHistory() + const tabs = () => props.controls.session.tabs + const activeFileTab = createSessionTabs({ + tabs, + pathFromTab: files.pathFromTab, + normalizeTab: (tab) => (tab.startsWith("file://") ? files.tab(tab) : tab), + }).activeFileTab + const recent = createMemo(() => { + const all = tabs().all() + const active = activeFileTab() + const order = active ? [active, ...all.filter((tab) => tab !== active)] : all + return order.reduce((result, tab) => { + const path = files.pathFromTab(tab) + if (!path || result.includes(path)) return result + return [...result, path] + }, []) + }) + const info = createMemo(() => (props.controls.session.id ? sync().session.get(props.controls.session.id) : undefined)) + const working = createMemo(() => sync().data.session_working(props.controls.session.id ?? "")) + const attachments = createMemo(() => + prompt.current().filter((part): part is ImageAttachmentPart => part.type === "image"), + ) + const commentCount = createMemo(() => { + if (mode() === "shell") return 0 + return prompt.context.items().filter((item) => !!item.comment?.trim()).length + }) + const blank = createMemo(() => { + const text = prompt + .current() + .map((part) => ("content" in part ? part.content : "")) + .join("") + return text.trim().length === 0 && attachments().length === 0 && commentCount() === 0 + }) + const stopping = createMemo(() => working() && blank()) + const placeholder = createMemo(() => + promptPlaceholder({ + mode: mode(), + commentCount: commentCount(), + example: mode() === "shell" ? "git status" : "", + suggest: false, + t: (key, params) => language.t(key as Parameters[0], params as never), + }), + ) + const designPlaceholder = () => promptDesignPlaceholder(mode(), placeholder()) + + const historyComments = () => { + const byID = new Map(comments.all().map((item) => [`${item.file}\n${item.id}`, item] as const)) + return prompt.context.items().flatMap((item) => { + const comment = item.comment?.trim() + if (!comment) return [] + const selection = item.commentID ? byID.get(`${item.path}\n${item.commentID}`)?.selection : undefined + const nextSelection = + selection ?? + (item.selection + ? ({ start: item.selection.startLine, end: item.selection.endLine } satisfies SelectedLineRange) + : undefined) + if (!nextSelection) return [] + return [ + { + id: item.commentID ?? item.key, + path: item.path, + selection: { ...nextSelection }, + comment, + time: item.commentID ? (byID.get(`${item.path}\n${item.commentID}`)?.time ?? Date.now()) : Date.now(), + origin: item.commentOrigin, + preview: item.preview, + } satisfies PromptHistoryComment, + ] + }) + } + const restoreHistoryComments = (items: PromptHistoryComment[]) => { + comments.replace( + items.map((item) => ({ + id: item.id, + file: item.path, + selection: { ...item.selection }, + comment: item.comment, + time: item.time, + })), + ) + prompt.context.replaceComments( + items.map((item) => ({ + type: "file", + path: item.path, + selection: selectionFromLines(item.selection), + comment: item.comment, + commentID: item.id, + commentOrigin: item.origin, + preview: item.preview, + })), + ) + } + + const accepting = createMemo(() => { + const id = props.controls.session.id + if (!id) return permission.isAutoAcceptingDirectory(sdk().directory) + return permission.isAutoAccepting(id, sdk().directory) + }) + const submission = createPromptSubmit({ + prompt, + info, + imageAttachments: attachments, + commentCount, + autoAccept: accepting, + mode, + working, + editor: () => editor, + queueScroll: () => requestAnimationFrame(() => editor?.scrollIntoView({ block: "nearest" })), + promptLength, + addToHistory: (value, mode) => controller.addHistory(value, mode), + resetHistoryNavigation: () => controller.resetHistory(), + setMode: (next) => controller.dispatch({ type: next === "shell" ? "mode.shell" : "mode.normal" }), + setPopover: (popover) => { + if (!popover) controller.dispatch({ type: "popover.close" }) + }, + newSessionWorktree: () => props.newSessionWorktree, + onNewSessionWorktreeReset: props.onNewSessionWorktreeReset, + shouldQueue: props.shouldQueue, + onQueue: props.onQueue, + onAbort: props.onAbort, + onSubmit: props.onSubmit, + model: props.controls.model.selection, + }) + + const referenceDescription = (reference: ReferenceInfo) => + reference.source.type === "git" ? reference.source.repository : reference.source.path + const references = createMemo(() => + sync() + .data.reference.filter((reference) => !reference.hidden) + .map((reference) => ({ + id: `reference:${reference.name}`, + kind: "reference" as const, + label: `@${reference.name}`, + path: reference.path, + description: reference.description ?? referenceDescription(reference), + mention: { + type: "file" as const, + path: reference.path, + content: `@${reference.name}`, + start: 0, + end: 0, + mime: "application/x-directory", + filename: reference.name, + }, + })), + ) + const resources = createMemo(() => + Object.values(sync().data.mcp_resource).map((resource) => ({ + id: `resource:${resource.server}:${resource.uri}`, + kind: "resource" as const, + label: `@${resource.name}`, + path: resource.uri, + description: resource.description, + mention: { + type: "file" as const, + path: resource.uri, + content: `@${resource.name}`, + start: 0, + end: 0, + mime: resource.mimeType ?? "text/plain", + filename: resource.name, + url: resource.uri, + source: { + type: "resource" as const, + text: { value: `@${resource.name}`, start: 0, end: resource.name.length + 1 }, + clientName: resource.server, + uri: resource.uri, + }, + }, + resource, + })), + ) + const context = createMemo(() => [ + ...references(), + ...props.controls.agents.available + .filter((agent) => !agent.hidden && agent.mode !== "primary") + .map((agent) => ({ + id: `agent:${agent.name}`, + kind: "agent" as const, + label: `@${agent.name}`, + mention: { type: "agent" as const, name: agent.name, content: `@${agent.name}`, start: 0, end: 0 }, + })), + ...resources(), + ...recent().map((path) => ({ + id: `file:${path}`, + kind: "file" as const, + label: path, + path, + recent: true, + mention: { type: "file" as const, path, content: `@${path}`, start: 0, end: 0 }, + })), + ]) + const slashCommands = createMemo(() => [ + ...sync().data.command.map((item) => ({ + id: `custom.${item.name}`, + trigger: item.name, + title: item.name, + description: item.description, + type: "custom" as const, + })), + ...command.options + .filter((item) => !item.disabled && !item.id.startsWith("suggested.") && item.slash) + .map((item) => ({ + id: item.id, + trigger: item.slash!, + title: item.title, + description: item.description, + type: "builtin" as const, + })), + ]) + const commands = createMemo(() => + slashCommands().map((item) => ({ + id: item.id, + kind: "command", + label: `/${item.trigger}`, + trigger: item.trigger, + title: item.title, + description: item.description, + keybind: command.keybindParts(item.id), + })), + ) + const variants = createMemo(() => ["default", ...props.controls.model.selection.variant.list()]) + const controller = createPromptInputV2Controller({ + store: () => prompt.capture().store, + state: interaction, + identity: () => prompt.capture(), + history: { + entries: (mode) => + history.entries(mode).map((value) => { + const entry = normalizePromptHistoryEntry(value) + return { prompt: entry.prompt, metadata: entry.comments } + }), + add: (value, mode) => history.add(value, mode, mode === "shell" ? [] : historyComments()), + capture: historyComments, + restore: (metadata) => restoreHistoryComments(metadata as PromptHistoryComment[]), + }, + commands, + context, + searchContextFiles: async (query) => + (await files.searchFilesAndDirectories(query)).map((path) => ({ + id: `file:${path}`, + kind: "file", + label: path, + path, + mention: { type: "file", path, content: `@${path}`, start: 0, end: 0 }, + })), + onContextRemove(item) { + if (item?.commentID) comments.remove(item.path, item.commentID) + }, + openAttachment: (attachment) => + dialog.show(() => ), + openContext(key) { + const item = controller.contextItem(key) + if (item) openComment(item, props, sync, layout, files, comments) + }, + onEditor(element) { + editor = element as HTMLDivElement + props.ref?.(editor) + }, + onSuggestionSelect(item) { + if (item.kind !== "command") return + const selected = slashCommands().find((entry) => entry.id === item.id) + if (!selected || selected.type === "custom") return + return () => command.trigger(selected.id, "slash") + }, + attachments: { + picker: platform.openAttachmentPickerDialog, + directory: () => sdk().directory, + isDialogActive: () => !!dialog.active, + warn: () => + showToast({ + title: language.t("prompt.toast.pasteUnsupported.title"), + description: language.t("prompt.toast.pasteUnsupported.description"), + }), + duplicate: () => showToast({ title: language.t("prompt.toast.attachmentDuplicate.title") }), + onError: (error) => + showToast({ + variant: "error", + title: language.t("common.requestFailed"), + description: error instanceof Error ? error.message : String(error), + }), + readClipboardImage: platform.readClipboardImage, + getPathForFile: platform.getPathForFile, + store: platform.draftStore?.putBlob, + }, + view: { + placeholder: designPlaceholder, + get agent() { + return props.controls.agents.visible && props.controls.agents.options.length > 0 + ? { + options: () => props.controls.agents.options.map((name) => ({ id: name, label: name })), + current: () => props.controls.agents.current, + onSelect: (value: string) => props.controls.agents.select(value), + keybind: () => command.keybindParts("agent.cycle"), + } + : undefined + }, + variant: { + options: () => variants().map((value) => ({ id: value, label: value })), + current: () => props.controls.model.selection.variant.current() ?? "default", + onSelect: (value) => props.controls.model.selection.variant.set(value === "default" ? undefined : value), + keybind: () => command.keybindParts("model.variant.cycle"), + }, + submit: { + stopping, + working, + onSubmit: () => void submission.handleSubmit(new Event("submit")), + onStop: () => void submission.abort(), + }, + }, + }) + Object.defineProperty(controller, "model", { get: () => props.controls.model }) + + command.register("prompt-input", () => [ + { + id: "file.attach", + title: language.t("prompt.action.attachFile"), + category: language.t("command.category.file"), + keybind: "mod+u", + disabled: controller.state.mode !== "normal", + onSelect: () => controller.attach(), + }, + { + id: "prompt.mode.shell", + title: language.t("command.prompt.mode.shell"), + category: language.t("command.category.session"), + keybind: "mod+shift+x", + disabled: controller.state.mode === "shell", + onSelect: () => controller.dispatch({ type: "mode.shell" }), + }, + { + id: "prompt.mode.normal", + title: language.t("command.prompt.mode.normal"), + category: language.t("command.category.session"), + keybind: "mod+shift+e", + disabled: controller.state.mode === "normal", + onSelect: () => controller.dispatch({ type: "mode.normal" }), + }, + ]) + + createEffect( + on( + () => props.edit?.id, + (id) => { + const edit = props.edit + if (!id || !edit) return + prompt.context.items().forEach((item) => prompt.context.remove(item.key)) + edit.context.forEach((item) => + prompt.context.add({ + type: item.type, + path: item.path, + selection: item.selection, + comment: item.comment, + commentID: item.commentID, + commentOrigin: item.commentOrigin, + preview: item.preview, + }), + ) + controller.dispatch({ type: "mode.normal" }) + controller.resetHistory() + prompt.set(edit.prompt, promptLength(edit.prompt)) + controller.restoreFocus() + props.onEditLoaded?.() + }, + { defer: true }, + ), + ) + + return controller as PromptInputV2ComposerController +} + +function PromptInputV2ModelControl(props: { + loading: boolean + paid: boolean + title: string + keybind: string[] + model: PromptInputV2ComposerController["model"]["selection"] + providerID?: string + modelName: string + onClose: () => void + onUnpaidClick: () => void +}) { + const shouldAnimate = createMemo((previous) => previous ?? props.loading) + const content = () => ( + <> + + {(providerID) => ( + + )} + + {props.modelName} + + + + + ) + return ( + + + {props.title} + + + } + > + + {content()} + + } + > + ( + + {content()} + + )} + onClose={props.onClose} + /> + + + + ) +} + +function openComment( + item: { path: string; commentID?: string; commentOrigin?: "review" | "file" }, + props: PromptInputV2ControllerProps, + sync: ReturnType, + layout: ReturnType, + files: ReturnType, + comments: ReturnType, +) { + if (!item.commentID) return + const focus = { file: item.path, id: item.commentID } + comments.setActive(focus) + const queueFocus = (attempts = 6) => { + requestAnimationFrame(() => { + comments.setFocus({ ...focus }) + if (attempts <= 0) return + requestAnimationFrame(() => { + const current = comments.focus() + if (current?.file === focus.file && current.id === focus.id) queueFocus(attempts - 1) + }) + }) + } + const diffs = props.controls.session.id ? sync().data.session_diff[props.controls.session.id] : undefined + const review = + item.commentOrigin === "review" || (item.commentOrigin !== "file" && diffs?.some((diff) => diff.file === item.path)) + if (!props.controls.session.reviewPanel.opened()) props.controls.session.reviewPanel.open() + if (review) { + layout.fileTree.setTab("changes") + props.controls.session.tabs.setActive("review") + queueFocus() + return + } + layout.fileTree.setTab("all") + const tab = files.tab(item.path) + void props.controls.session.tabs.open(tab) + props.controls.session.tabs.setActive(tab) + void Promise.resolve(files.load(item.path)).finally(() => queueFocus()) +} diff --git a/packages/app/src/components/prompt-input.stories.tsx b/packages/app/src/components/prompt-input.stories.tsx index 272079e106..0b9c26ce41 100644 --- a/packages/app/src/components/prompt-input.stories.tsx +++ b/packages/app/src/components/prompt-input.stories.tsx @@ -1,6 +1,8 @@ // @ts-nocheck import { createStore } from "solid-js/store" +import type { Todo } from "@opencode-ai/sdk/v2" import { createPromptState } from "@/context/prompt" +import { SessionComposerRegion, createSessionComposerRegionController } from "@/pages/session/composer" import { createPromptInputHistory, PromptInput } from "./prompt-input" function createPromptInputStoryRuntime() { @@ -28,8 +30,16 @@ function PromptInputExample() { activeTab: undefined as string | undefined, reviewOpen: false, }) + const storyModel = { + id: "claude-3-7-sonnet", + name: "Claude 3.7 Sonnet", + provider: { id: "anthropic", name: "Anthropic" }, + } const model = { - current: () => ({ id: "claude-3-7-sonnet", name: "Claude 3.7 Sonnet", provider: { id: "anthropic" } }), + current: () => storyModel, + list: () => [storyModel], + visible: () => true, + set: () => {}, variant: { list: () => ["fast", "thinking"], current: () => controls.variant, @@ -65,7 +75,6 @@ function PromptInputExample() { open: () => setControls("reviewOpen", true), }, }, - newLayoutDesigns: true, } const addReviewComment = () => { const comment = controls.comments + 1 @@ -102,6 +111,93 @@ function PromptInputExample() { ) } +const todos: Todo[] = [ + { id: "todo-1", content: "Inspect the session composer animation", status: "completed" }, + { id: "todo-2", content: "Keep the dock settled on initial render", status: "in_progress" }, + { id: "todo-3", content: "Verify session navigation behavior", status: "pending" }, +] + +function PromptInputWithOpenDock() { + const input = createPromptInputStoryRuntime() + const [controls, setControls] = createStore({ + agent: "build", + activeTab: undefined as string | undefined, + todoCollapsed: false, + }) + const inputControls = { + agents: { + available: [], + options: ["build"], + get current() { + return controls.agent + }, + loading: false, + visible: true, + select: (agent?: string) => setControls("agent", agent ?? "build"), + }, + model: { + selection: { + current: () => ({ id: "claude-3-7-sonnet", name: "Claude 3.7 Sonnet", provider: { id: "anthropic" } }), + variant: { list: () => [], current: () => undefined, set: () => {} }, + }, + paid: true, + loading: false, + }, + session: { + id: "story-session", + tabs: { + active: () => controls.activeTab, + all: () => [], + open: () => {}, + setActive: (tab: string) => setControls("activeTab", tab), + }, + reviewPanel: { opened: () => false, open: () => {} }, + }, + } + const state = { + blocked: () => false, + questionRequest: () => undefined, + permissionRequest: () => undefined, + permissionResponding: () => false, + decide: () => {}, + todos: () => todos, + dock: () => true, + closing: () => false, + opening: () => false, + } + return ( + "story-session", + sessionID: () => "story-session", + prompt: input.state, + ready: () => true, + centered: () => false, + todo: { + collapsed: () => controls.todoCollapsed, + onToggle: () => setControls("todoCollapsed", (collapsed) => !collapsed), + }, + followup: () => undefined, + revert: () => undefined, + onResponseSubmit: () => {}, + openParent: () => {}, + setPromptRef: () => {}, + setDockRef: () => {}, + })} + promptInput={ + {}} + newSessionWorktree="" + onNewSessionWorktreeReset={() => {}} + /> + } + /> + ) +} + export default { title: "App/PromptInput", id: "app-prompt-input", @@ -116,3 +212,12 @@ export const Basic = {
), } + +export const DockAlreadyOpen = { + render: () => ( +
+

Prompt Input with open Todo dock

+ +
+ ), +} diff --git a/packages/app/src/components/prompt-input.tsx b/packages/app/src/components/prompt-input.tsx index bbb71e8722..08c1ee3489 100644 --- a/packages/app/src/components/prompt-input.tsx +++ b/packages/app/src/components/prompt-input.tsx @@ -13,12 +13,11 @@ import { Match, type JSX, } from "solid-js" -import { createStore, type SetStoreFunction, type Store } from "solid-js/store" -import type { useLocal } from "@/context/local" import { selectionFromLines, type SelectedLineRange, useFile } from "@/context/file" import { ContentPart, DEFAULT_PROMPT, + isCommentItem, isPromptEqual, Prompt, usePrompt, @@ -48,7 +47,6 @@ import { ModelSelectorPopover, ModelSelectorPopoverV2 } from "@/components/dialo import { DialogSelectModelUnpaid } from "@/components/dialog-select-model-unpaid" import { DialogSelectModelUnpaidV2 } from "@/components/dialog-select-model-unpaid-v2" import { useCommand } from "@/context/command" -import { Persist, persisted } from "@/utils/persist" import { usePermission } from "@/context/permission" import { useLanguage } from "@/context/language" import { usePlatform } from "@/context/platform" @@ -59,121 +57,34 @@ import { ACCEPTED_FILE_TYPES, pickAttachmentFiles } from "./prompt-input/files" import { canNavigateHistoryAtCursor, navigatePromptHistory, - prependHistoryEntry, type PromptHistoryComment, type PromptHistoryEntry, - type PromptHistoryStoredEntry, promptLength, } from "./prompt-input/history" -import { createPromptSubmit, type FollowupDraft } from "./prompt-input/submit" +import { + createPersistedPromptInputHistory, + createPromptInputHistory, + type PromptInputHistory, +} from "./prompt-input/history-store" +import { + type PromptInputControls, + type PromptInputProps, + type PromptInputState, + type PromptInputSubmission, +} from "./prompt-input/contracts" +import { createPromptSubmit } from "./prompt-input/submit" import { PromptPopover, type AtOption, type SlashCommand } from "./prompt-input/slash-popover" import { PromptContextItems } from "./prompt-input/context-items" import { PromptImageAttachments } from "./prompt-input/image-attachments" import { PromptDragOverlay } from "./prompt-input/drag-overlay" -import { promptPlaceholder } from "./prompt-input/placeholder" +import { promptDesignPlaceholder, promptPlaceholder } from "./prompt-input/placeholder" import { createPromptInputTransientState } from "./prompt-input/transient-state" import { showToast } from "@/utils/toast" import { ImagePreview } from "@opencode-ai/ui/image-preview" import type { ReferenceInfo } from "@opencode-ai/sdk/v2/client" -export type PromptInputState = ReturnType - -export type PromptInputHistory = { - entries: (mode: "normal" | "shell") => PromptHistoryStoredEntry[] - add: (prompt: Prompt, mode: "normal" | "shell", comments: PromptHistoryComment[]) => void -} - -export type PromptInputSubmission = { - abort: () => Promise | void - handleSubmit: (event: Event) => Promise | void -} - -export type PromptInputControls = { - agents: { - available: { name: string; hidden?: boolean; mode: string }[] - options: string[] - current: string - loading: boolean - visible: boolean - select: (name: string | undefined) => void - } - model: { - selection: ReturnType["model"] - paid: boolean - loading: boolean - } - session: { - id?: string - tabs: { - active: () => string | undefined - all: () => string[] - open: (tab: string) => void | Promise - setActive: (tab: string) => void - } - reviewPanel: { - opened: () => boolean - open: () => void - } - } - newLayoutDesigns: boolean -} - -export function createPromptInputHistory(): PromptInputHistory { - const [normal, setNormal] = createStore({ entries: [] }) - const [shell, setShell] = createStore({ entries: [] }) - return createPromptInputHistoryStore(normal, setNormal, shell, setShell) -} - -type PromptHistoryState = { entries: PromptHistoryStoredEntry[] } - -function createPromptInputHistoryStore( - normal: Store, - setNormal: SetStoreFunction, - shell: Store, - setShell: SetStoreFunction, -): PromptInputHistory { - return { - entries: (mode) => (mode === "shell" ? shell.entries : normal.entries), - add(prompt, mode, comments) { - const current = mode === "shell" ? shell : normal - const setCurrent = mode === "shell" ? setShell : setNormal - const next = prependHistoryEntry(current.entries, prompt, comments) - if (next === current.entries) return - setCurrent("entries", next) - }, - } -} - -function createPersistedPromptInputHistory() { - const [normal, setNormal] = persisted( - Persist.global("prompt-history", ["prompt-history.v1"]), - createStore({ entries: [] }), - ) - const [shell, setShell] = persisted( - Persist.global("prompt-history-shell", ["prompt-history-shell.v1"]), - createStore({ entries: [] }), - ) - return createPromptInputHistoryStore(normal, setNormal, shell, setShell) -} - -export interface PromptInputProps { - class?: string - variant?: "dock" | "new-session" - state?: PromptInputState - history?: PromptInputHistory - submission?: PromptInputSubmission - controls: PromptInputControls - ref?: (el: HTMLDivElement) => void - newSessionWorktree?: string - onNewSessionWorktreeReset?: () => void - edit?: { id: string; prompt: Prompt; context: FollowupDraft["context"] } - onEditLoaded?: () => void - shouldQueue?: () => boolean - onQueue?: (draft: FollowupDraft) => void - onAbort?: () => void - onSubmit?: () => void - toolbar?: JSX.Element -} +export { createPromptInputHistory } +export type { PromptInputControls, PromptInputHistory, PromptInputProps, PromptInputState, PromptInputSubmission } const EXAMPLES = [ "prompt.example.1", @@ -632,7 +543,9 @@ export const PromptInput: Component = (props) => { const isImeComposing = (event: KeyboardEvent) => event.isComposing || composing() || event.keyCode === 229 const handleBlur = () => { - savedCursor = currentCursor() + const cursor = currentCursor() + savedCursor = cursor + if (cursor !== null && cursor !== prompt.cursor()) prompt.set(prompt.current(), cursor) closePopover() setComposing(false) } @@ -678,7 +591,7 @@ export const PromptInput: Component = (props) => { type: "resource", name: resource.name, uri: resource.uri, - client: resource.client, + client: resource.server, display: resource.name, description: resource.description, mime: resource.mimeType, @@ -796,7 +709,7 @@ export const PromptInput: Component = (props) => { title: cmd.name, description: cmd.description, type: "custom" as const, - source: cmd.source, + // source: cmd.source, })) return [...custom, ...builtin] @@ -1188,28 +1101,6 @@ export const PromptInput: Component = (props) => { return true } - const openCommands = () => { - const populated = prompt.dirty() || commentCount() > 0 - requestAnimationFrame(() => { - if (!populated) { - if (!addPart({ type: "text", content: "/", start: 0, end: 0 })) return - slashOnInput("") - setStore({ popover: "slash", slashMenu: false, slashMenuQuery: "" }) - return - } - slashOnInput("") - setStore({ popover: "slash", slashMenu: true, slashMenuQuery: "" }) - }) - } - - const openContext = () => { - requestAnimationFrame(() => { - if (!addPart({ type: "text", content: "@", start: 0, end: 0 })) return - atOnInput("") - setStore({ popover: "at", slashMenu: false, slashMenuQuery: "" }) - }) - } - const addToHistory = (prompt: Prompt, mode: "normal" | "shell") => { history.add(prompt, mode, mode === "shell" ? [] : historyComments()) } @@ -1536,50 +1427,11 @@ export const PromptInput: Component = (props) => { (p) => p, ) - const designPlaceholder = () => { - if (store.mode === "shell") return placeholder() - return "Ask anything, / for commands, @ for context..." - } - - const modelControlState = createMemo(() => ({ - loading: providersLoading(), - shouldAnimate: providersShouldFadeIn(), - paid: props.controls.model.paid, - title: language.t("command.model.choose"), - keybind: command.keybindParts("model.choose"), - model: props.controls.model.selection, - providerID: props.controls.model.selection.current()?.provider?.id, - modelName: props.controls.model.selection.current()?.name ?? language.t("dialog.model.select.title"), - newLayoutDesigns: props.controls.newLayoutDesigns, - style: control(), - onClose: restoreFocus, - onUnpaidClick: () => { - if (props.controls.newLayoutDesigns) { - dialog.show(() => ) - return - } - dialog.show(() => ) - }, - })) - - const newSession = () => props.variant === "new-session" const bindEditorRef = (el: HTMLDivElement) => { editorRef = el restoreEndOnFocus = true props.ref?.(el) } - const showAgentControl = createMemo(() => props.controls.agents.visible && props.controls.agents.options.length > 0) - const agentControlState = createMemo(() => ({ - title: language.t("command.agent.cycle"), - keybind: command.keybindParts("agent.cycle"), - options: props.controls.agents.options, - current: props.controls.agents.current, - style: control(), - onSelect: (value) => { - props.controls.agents.select(value) - restoreFocus() - }, - })) return (
{(promptReady(), null)} @@ -1604,503 +1456,281 @@ export const PromptInput: Component = (props) => { onSlashMenuKeyDown={handleSlashMenuKeyDown} commandKeybind={command.keybind} commandKeybindParts={command.keybindParts} - newLayoutDesigns={props.controls.newLayoutDesigns} + newLayoutDesigns={false} t={(key) => language.t(key as Parameters[0])} /> - - -
- - - { - const active = comments.active() - return !!item.commentID && item.commentID === active?.id && item.path === active?.file - }} - openComment={openComment} - remove={(item) => { - if (item.commentID) comments.remove(item.path, item.commentID) - prompt.context.remove(item.key) - }} - t={(key) => language.t(key as Parameters[0])} - /> - - dialog.show(() => ) - } - onRemove={removeAttachment} - removeLabel={language.t("prompt.attachment.remove")} - /> -
{ - const target = e.target - if (!(target instanceof HTMLElement)) return - if (target.closest('[data-action^="prompt-"]')) return - editorRef?.focus() - }} - > -
(scrollRef = el)}> -
-
- {designPlaceholder()} -
-
-
-
-
- {fileAttachmentInput()} - - {language.t("prompt.menu.addImagesAndFiles")} - - - } - > - - } - variant="ghost-muted" - size="large" - style={buttons()} - disabled={store.mode !== "normal"} - tabIndex={store.mode === "normal" ? undefined : -1} - aria-label={language.t("prompt.menu.addImagesAndFiles")} - /> - - - - {language.t("prompt.menu.imagesAndFiles")} - - - - {language.t("prompt.menu.commands")} - - - {language.t("prompt.menu.context")} - - setMode("shell")} shortcut="!"> - {language.t("prompt.menu.shellCommand")} - - - - - - - - - {props.toolbar} - - -
- - {language.t("command.model.variant.cycle")} - - - } - > - setStore("variantOpen", open)} - > - - - {props.controls.model.selection.variant.current() ?? language.t("common.default")} - - - - - - - - { - props.controls.model.selection.variant.set(value === "default" ? undefined : value) - restoreFocus() - }} - > - {variants().map((value) => ( - - {value === "default" ? language.t("common.default") : value} - - ))} - - - - - -
-
-
- - - -
- -
- - - + + { + const active = comments.active() + return !!item.commentID && item.commentID === active?.id && item.path === active?.file + }} + openComment={openComment} + remove={(item) => { + if (item.commentID) comments.remove(item.path, item.commentID) + prompt.context.remove(item.key) + }} + newLayoutDesigns={false} + t={(key) => language.t(key as Parameters[0])} + /> + + dialog.show(() => ) + } + onRemove={removeAttachment} + removeLabel={language.t("prompt.attachment.remove")} + newLayoutDesigns={false} + /> +
{ + const target = e.target + if (!(target instanceof HTMLElement)) return + if (target.closest('[data-action="prompt-attach"], [data-action="prompt-submit"]')) { + return + } + editorRef?.focus() + }} + > +
(scrollRef = el)} + style={{ "scroll-padding-bottom": space }} > - - { - const active = comments.active() - return !!item.commentID && item.commentID === active?.id && item.path === active?.file +
{ - if (item.commentID) comments.remove(item.path, item.commentID) - prompt.context.remove(item.key) - }} - t={(key) => language.t(key as Parameters[0])} - /> - - dialog.show(() => ) - } - onRemove={removeAttachment} - removeLabel={language.t("prompt.attachment.remove")} + style={{ "padding-bottom": space }} />
{ - const target = e.target - if (!(target instanceof HTMLElement)) return - if (target.closest('[data-action="prompt-attach"], [data-action="prompt-submit"]')) { - return - } - editorRef?.focus() + class="absolute top-0 inset-x-0 pl-3 pr-2 pt-2 text-14-regular text-text-weak pointer-events-none whitespace-nowrap truncate" + classList={{ "font-mono!": store.mode === "shell" }} + style={{ "padding-bottom": space, display: prompt.dirty() ? "none" : undefined }} + > + {placeholder()} +
+
+ + -
) @@ -2430,7 +2447,7 @@ function UpdateAvailableToast(props: { onCleanup(() => { if (toastId === undefined) return - toaster.dismiss(toastId) + dismissToast(toastId) }) return null diff --git a/packages/app/src/pages/layout/project-avatar-state.ts b/packages/app/src/pages/layout/project-avatar-state.ts index ca105af37c..236f6bd405 100644 --- a/packages/app/src/pages/layout/project-avatar-state.ts +++ b/packages/app/src/pages/layout/project-avatar-state.ts @@ -21,9 +21,10 @@ export function useSessionTabAvatarState( const hasPermissions = createMemo(() => { const serverSync = sync() if (!serverSync) return false + const permissionState = permission.ensureServerState(server()) const [store] = serverSync.child(directory(), { bootstrap: false }) return !!sessionPermissionRequest(store.session, serverSync.session.data.permission, sessionId(), (item) => { - return !permission.autoResponds(item, directory()) + return !permissionState.autoResponds(item, directory()) }) }) const hasQuestions = createMemo(() => { @@ -33,9 +34,11 @@ export function useSessionTabAvatarState( return !!sessionQuestionRequest(store.session, serverSync.session.data.question, sessionId()) }) const needsAttention = createMemo(() => hasPermissions() || hasQuestions()) - const unread = createMemo( - () => needsAttention() || notification.ensureServerState(server()).session.unseenCount(sessionId()) > 0, - ) + const notificationState = createMemo(() => { + if (!connection()) return + return notification.ensureServerState(server()) + }) + const unread = createMemo(() => needsAttention() || (notificationState()?.session.unseenCount(sessionId()) ?? 0) > 0) const loading = createMemo(() => { const serverSync = sync() if (!serverSync) return false diff --git a/packages/app/src/pages/layout/session-tab-avatar.tsx b/packages/app/src/pages/layout/session-tab-avatar.tsx index 3c776c8671..0902173643 100644 --- a/packages/app/src/pages/layout/session-tab-avatar.tsx +++ b/packages/app/src/pages/layout/session-tab-avatar.tsx @@ -19,16 +19,34 @@ export function SessionTabAvatar(props: { () => props.directory, () => props.sessionId, ) + return ( + + ) +} + +export function SessionTabAvatarView(props: { + project?: LocalProject + directory: string + revealProjectOnHover?: boolean + unread: boolean + loading: boolean +}) { const projectAvatar = () => ( ) return ( - + sdk().directory) - const openProviderSettings = useSettingsDialog("providers") - const route = useSessionKey() - const [searchParams, setSearchParams] = useSearchParams<{ draftId?: string; prompt?: string }>() - const local = useLocal() - const model = createPromptModelSelection({ agent: local.agent.current }) - - useComposerCommands({ model }) - - let inputRef: HTMLDivElement | undefined - - const inputController = createPromptInputController({ - sessionKey: route.sessionKey, - sessionID: () => route.params.id, - queryOptions: serverSync().queryOptions, - model, - }) - const projectControls = createPromptProjectControls() - const projectController = createPromptProjectController({ - controls: projectControls, - onDone: () => inputRef?.focus(), - }) - - command.register("new-session", () => [ - { - id: "input.focus", - title: language.t("command.input.focus"), - category: language.t("command.category.view"), - keybind: "ctrl+l", - onSelect: () => inputRef?.focus(), - }, - ]) - - const [store, setStore] = createStore<{ worktree?: string }>({}) const rightMount = useTitlebarRightMount() - - const showWorkspaceBar = createMemo(() => workspaceBarEnabled && sync().project?.vcs === "git") - const newSessionWorktree = createMemo(() => { - if (!showWorkspaceBar()) return "main" - if (store.worktree) return store.worktree - const project = sync().project - if (project && sdk().directory !== project.worktree) return sdk().directory - return "main" + const workspace = createNewSessionWorkspaceController() + const draft = createNewSessionDraftController({ + worktree: workspace.selection.value, + resetWorktree: workspace.selection.reset, }) - const projectRoot = createMemo(() => sync().project?.worktree ?? sdk().directory) - const localBranch = createMemo(() => serverSync().child(projectRoot())[0].vcs?.branch) - const selectedBranch = createMemo(() => { - const worktree = newSessionWorktree() - if (worktree === "main" || worktree === "create") return localBranch() - return serverSync().child(worktree)[0].vcs?.branch ?? localBranch() + const project = createPromptProjectController({ + controls: draft.project.controls, + onDone: draft.input.restoreFocus, + }) + useNewSessionCommands({ + restoreFocus: draft.input.restoreFocus, + project: { + empty: project.empty, + open: () => project.setOpen(true), + }, }) - createEffect(() => { - if (!prompt.ready()) return - untrack(() => { - const text = searchParams.prompt - if (!text) return - prompt.set([{ type: "text", content: text, start: 0, end: text.length }], text.length) - setSearchParams({ ...searchParams, prompt: undefined }) - }) - }) - - createEffect(() => { - if (!prompt.ready()) return - requestAnimationFrame(() => inputRef?.focus()) + if (!draft.prompt.ready()) return + draft.input.restoreFocus() }) const ready = Promise.resolve() - const [promptReady] = createResource( - () => prompt.ready.promise ?? ready, + const [suspendUntilPromptReady] = createResource( + () => draft.prompt.readyPromise() ?? ready, (promise) => promise.then(() => true), ) return (
- - {(mount) => ( - - - - - - - - )} - + {suspendUntilPromptReady()} +
-
-
- -
- - {language.t("prompt.loading")} -
- } - > -
- { - inputRef = el - }} - newSessionWorktree={newSessionWorktree()} - onNewSessionWorktreeReset={() => setStore("worktree", undefined)} - onSubmit={() => comments.clear()} - toolbar={ - - - - } - /> - -
- - - - setStore( - "worktree", - value === "main" && sync().project?.worktree !== sdk().directory - ? sync().project?.worktree - : value, - ) - } - onDone={() => inputRef?.focus()} - /> - -
-
-
- -
- - serverSync().child(sdk().directory)[0].provider_ready} - connected={() => providers.paid().length > 0} - openProviders={openProviderSettings} - /> -
-
+
) } - -function ProviderTip(props: { ready: () => boolean; connected: () => boolean; openProviders: () => void }) { - const language = useLanguage() - const [persistedState, setPersistedState, , persistedReady] = persisted( - Persist.global("new-session.provider-tip"), - createStore({ dismissedAt: 0 }), - ) - const visible = createMemo( - () => - props.ready() && - persistedReady() && - !props.connected() && - Date.now() - persistedState.dismissedAt >= providerTipDismissalDuration, - ) - - function dismiss() { - setPersistedState("dismissedAt", Date.now()) - } - - const [ref, setRef] = createSignal() - const presence = createPresence({ - show: () => visible(), - element: () => ref() ?? null, - }) - - return ( - -
-
- - - - -
-
-
- ) -} diff --git a/packages/app/src/pages/new-session/new-session-draft-controller.ts b/packages/app/src/pages/new-session/new-session-draft-controller.ts new file mode 100644 index 0000000000..bf22834e48 --- /dev/null +++ b/packages/app/src/pages/new-session/new-session-draft-controller.ts @@ -0,0 +1,64 @@ +import { useSearchParams } from "@solidjs/router" +import { createEffect, untrack } from "solid-js" +import { usePromptInputV2Controller } from "@/components/prompt-input-v2" +import { useComments } from "@/context/comments" +import { useLocal } from "@/context/local" +import { usePrompt } from "@/context/prompt" +import { useServerSync } from "@/context/server-sync" +import { createPromptInputController, createPromptProjectControls } from "@/pages/session/composer" +import { createPromptModelSelection } from "@/pages/session/composer/prompt-model-selection" +import { useSessionKey } from "@/pages/session/session-layout" +import { useComposerCommands } from "@/pages/session/use-composer-commands" + +export function createNewSessionDraftController(workspace: { worktree: () => string; resetWorktree: () => void }) { + const prompt = usePrompt() + const serverSync = useServerSync() + const comments = useComments() + const local = useLocal() + const route = useSessionKey() + const [searchParams, setSearchParams] = useSearchParams<{ draftId?: string; prompt?: string }>() + const model = createPromptModelSelection({ agent: () => local.agent.current() }) + + useComposerCommands({ model }) + + const controls = createPromptInputController({ + sessionKey: route.sessionKey, + sessionID: () => route.params.id, + queryOptions: serverSync().queryOptions, + model, + }) + const projectControls = createPromptProjectControls() + const input = usePromptInputV2Controller({ + get controls() { + return controls() + }, + get newSessionWorktree() { + return workspace.worktree() + }, + onNewSessionWorktreeReset: workspace.resetWorktree, + onSubmit: comments.clear, + }) + + createEffect(() => { + if (!prompt.ready()) return + untrack(() => { + const text = searchParams.prompt + if (!text) return + prompt.set([{ type: "text", content: text, start: 0, end: text.length }], text.length) + setSearchParams({ ...searchParams, prompt: undefined }) + }) + }) + + return { + input, + prompt: { + ready: prompt.ready, + readyPromise: () => prompt.ready.promise, + }, + project: { + controls: projectControls, + }, + } +} + +export type NewSessionDraftController = ReturnType diff --git a/packages/app/src/pages/new-session/new-session-view.tsx b/packages/app/src/pages/new-session/new-session-view.tsx new file mode 100644 index 0000000000..5960e64335 --- /dev/null +++ b/packages/app/src/pages/new-session/new-session-view.tsx @@ -0,0 +1,162 @@ +import { useDialog } from "@opencode-ai/ui/context/dialog" +import { Tooltip } from "@opencode-ai/ui/tooltip" +import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon" +import { TooltipV2 } from "@opencode-ai/ui/v2/tooltip-v2" +import { WordmarkV2 } from "@opencode-ai/ui/v2/wordmark-v2" +import { Show, createMemo, createSignal, type Accessor } from "solid-js" +import { createStore } from "solid-js/store" +import { Portal } from "solid-js/web" +import createPresence from "solid-presence" +import { PromptInputV2Composer } from "@/components/prompt-input-v2" +import { PromptGitStatus, PromptWorkspaceSelector } from "@/components/prompt-workspace-selector" +import { + PromptProjectAddButton, + PromptProjectSelector, + type PromptProjectController, +} from "@/components/prompt-project-selector" +import { StatusPopoverV2 } from "@/components/status-popover" +import { useLanguage } from "@/context/language" +import { useSDK } from "@/context/sdk" +import { useServerSync } from "@/context/server-sync" +import { useProviders } from "@/hooks/use-providers" +import { NEW_SESSION_CONTENT_WIDTH } from "@/pages/session/new-session-layout" +import { Persist, persisted } from "@/utils/persist" +import type { NewSessionDraftController } from "./new-session-draft-controller" +import type { NewSessionWorkspaceController } from "./new-session-workspace-controller" + +const providerTipDismissalDuration = 30 * 24 * 60 * 60 * 1000 + +export function NewSessionView(props: { + input: NewSessionDraftController["input"] + project: PromptProjectController + workspace: NewSessionWorkspaceController +}) { + return ( +
+
+
+
+ +
+ + + + + +
+ + + } + > + + +
+
+
+
+
+ +
+
+ ) +} + +export function NewSessionStatus(props: { mount: Accessor; visible: Accessor }) { + const language = useLanguage() + + return ( + + {(mount) => ( + + + + + + + + )} + + ) +} + +function ProviderTip() { + const language = useLanguage() + const dialog = useDialog() + const sdk = useSDK() + const serverSync = useServerSync() + const providers = useProviders(() => sdk().directory) + const [persistedState, setPersistedState, , persistedReady] = persisted( + Persist.global("new-session.provider-tip"), + createStore({ dismissedAt: 0 }), + ) + const visible = createMemo( + () => + serverSync().child(sdk().directory)[0].provider_ready && + persistedReady() && + providers.paid().length === 0 && + Date.now() - persistedState.dismissedAt >= providerTipDismissalDuration, + ) + const [ref, setRef] = createSignal() + const presence = createPresence({ + show: visible, + element: () => ref() ?? null, + }) + const openProviders = () => { + void import("@/components/dialog-connect-provider").then(({ DialogConnectProvider }) => { + void dialog.show(() => sdk().directory} />) + }) + } + + return ( + +
+
+ + + + +
+
+
+ ) +} diff --git a/packages/app/src/pages/new-session/new-session-workspace-controller.test.ts b/packages/app/src/pages/new-session/new-session-workspace-controller.test.ts new file mode 100644 index 0000000000..2a79ae77fa --- /dev/null +++ b/packages/app/src/pages/new-session/new-session-workspace-controller.test.ts @@ -0,0 +1,43 @@ +import { describe, expect, test } from "bun:test" +import { + normalizeNewSessionWorktree, + resolveNewSessionBranch, + resolveNewSessionWorktree, +} from "./new-session-workspace-controller" + +describe("new session workspace selection", () => { + test("uses main when the workspace bar is unavailable", () => { + expect( + resolveNewSessionWorktree({ + enabled: false, + selected: "/project/feature", + directory: "/project/feature", + projectWorktree: "/project", + }), + ).toBe("main") + }) + + test("derives an existing worktree from the current directory", () => { + expect( + resolveNewSessionWorktree({ enabled: true, directory: "/project/feature", projectWorktree: "/project" }), + ).toBe("/project/feature") + expect(resolveNewSessionWorktree({ enabled: true, directory: "/project", projectWorktree: "/project" })).toBe( + "main", + ) + }) + + test("normalizes main to the project root outside the main worktree", () => { + expect(normalizeNewSessionWorktree("main", "/project/feature", "/project")).toBe("/project") + expect(normalizeNewSessionWorktree("main", "/project", "/project")).toBe("main") + }) + + test("falls back to the local branch for main, create, and unknown worktrees", () => { + const branch = (worktree: string) => (worktree === "/project/feature" ? "feature" : undefined) + expect(resolveNewSessionBranch({ worktree: "main", local: "dev", worktreeBranch: branch })).toBe("dev") + expect(resolveNewSessionBranch({ worktree: "create", local: "dev", worktreeBranch: branch })).toBe("dev") + expect(resolveNewSessionBranch({ worktree: "/project/feature", local: "dev", worktreeBranch: branch })).toBe( + "feature", + ) + expect(resolveNewSessionBranch({ worktree: "/missing", local: "dev", worktreeBranch: branch })).toBe("dev") + }) +}) diff --git a/packages/app/src/pages/new-session/new-session-workspace-controller.ts b/packages/app/src/pages/new-session/new-session-workspace-controller.ts new file mode 100644 index 0000000000..f3fc9b2708 --- /dev/null +++ b/packages/app/src/pages/new-session/new-session-workspace-controller.ts @@ -0,0 +1,77 @@ +import { createMemo, createSignal } from "solid-js" +import { useSDK } from "@/context/sdk" +import { useServerSync } from "@/context/server-sync" +import { useSync } from "@/context/sync" + +const workspaceBarEnabled = import.meta.env.VITE_OPENCODE_CHANNEL !== "prod" + +export function resolveNewSessionWorktree(input: { + enabled: boolean + selected?: string + directory: string + projectWorktree?: string +}) { + if (!input.enabled) return "main" + if (input.selected) return input.selected + if (input.projectWorktree && input.directory !== input.projectWorktree) return input.directory + return "main" +} + +export function normalizeNewSessionWorktree(value: string, directory: string, projectWorktree?: string) { + if (value === "main" && projectWorktree !== directory) return projectWorktree + return value +} + +export function resolveNewSessionBranch(input: { + worktree: string + local?: string + worktreeBranch: (worktree: string) => string | undefined +}) { + if (input.worktree === "main" || input.worktree === "create") return input.local + return input.worktreeBranch(input.worktree) ?? input.local +} + +export function createNewSessionWorkspaceController() { + const sdk = useSDK() + const sync = useSync() + const serverSync = useServerSync() + const [worktree, setWorktree] = createSignal() + const visible = createMemo(() => workspaceBarEnabled && sync().project?.vcs === "git") + const value = createMemo(() => + resolveNewSessionWorktree({ + enabled: visible(), + selected: worktree(), + directory: sdk().directory, + projectWorktree: sync().project?.worktree, + }), + ) + const projectRoot = createMemo(() => sync().project?.worktree ?? sdk().directory) + const localBranch = createMemo(() => serverSync().child(projectRoot())[0].vcs?.branch) + const branch = createMemo(() => + resolveNewSessionBranch({ + worktree: value(), + local: localBranch(), + worktreeBranch: (worktree) => serverSync().child(worktree)[0].vcs?.branch, + }), + ) + + return { + selection: { + value, + reset: () => setWorktree(), + set: (worktree: string) => + setWorktree(normalizeNewSessionWorktree(worktree, sdk().directory, sync().project?.worktree)), + }, + project: { + root: projectRoot, + workspaces: () => sync().project?.sandboxes ?? [], + git: () => sync().project?.vcs === "git", + }, + bar: { + visible, + branch, + }, + } +} + +export type NewSessionWorkspaceController = ReturnType diff --git a/packages/app/src/pages/new-session/use-new-session-commands.tsx b/packages/app/src/pages/new-session/use-new-session-commands.tsx new file mode 100644 index 0000000000..a0d835f97f --- /dev/null +++ b/packages/app/src/pages/new-session/use-new-session-commands.tsx @@ -0,0 +1,44 @@ +import { useDialog } from "@opencode-ai/ui/context/dialog" +import { useSettingsCommand } from "@/components/settings-dialog" +import { useCommand } from "@/context/command" +import { useLanguage } from "@/context/language" + +export function useNewSessionCommands(input: { + restoreFocus: () => void + project: { + empty: () => boolean + open: () => void + } +}) { + const command = useCommand() + const dialog = useDialog() + const language = useLanguage() + + useSettingsCommand() + command.register("new-session", () => [ + { + id: "command.palette", + title: language.t("command.palette"), + hidden: true, + onSelect: async () => { + const { DialogSelectFile } = await import("@/components/dialog-select-file") + void dialog.show(() => ) + }, + }, + { + id: "input.focus", + title: language.t("command.input.focus"), + category: language.t("command.category.view"), + keybind: "ctrl+l", + onSelect: input.restoreFocus, + }, + { + id: "project.select", + title: language.t("session.new.project.search"), + category: language.t("command.category.project"), + keybind: "mod+shift+o", + disabled: input.project.empty(), + onSelect: input.project.open, + }, + ]) +} diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx index 9a0518fb91..c6e3a5fcbb 100644 --- a/packages/app/src/pages/session.tsx +++ b/packages/app/src/pages/session.tsx @@ -1,10 +1,12 @@ -import type { Project, UserMessage, VcsFileDiff } from "@opencode-ai/sdk/v2" +import type { FilePart, Project, UserMessage, VcsFileDiff } from "@opencode-ai/sdk/v2" +import { getFilename } from "@opencode-ai/core/util/path" import { useDialog } from "@opencode-ai/ui/context/dialog" import { createQuery, skipToken, useMutation, useQueryClient } from "@tanstack/solid-query" import { batch, ErrorBoundary, onCleanup, + Suspense, Show, Match, Switch, @@ -40,13 +42,13 @@ import { useLocation, useNavigate, useParams, useSearchParams } from "@solidjs/r import { NewSessionView, SessionHeader } from "@/components/session" import { ErrorPage } from "@/pages/error" import { CommentsProvider, useComments } from "@/context/comments" +import { useCommand } from "@/context/command" import { DirectoryDataProvider } from "@/pages/directory-layout" import { useServerSync } from "@/context/server-sync" import { useLanguage } from "@/context/language" import { useLayout } from "@/context/layout" import { ModelsProvider } from "@/context/models" import { useNotification } from "@/context/notification" -import { PermissionProvider } from "@/context/permission" import { PromptProvider, usePrompt } from "@/context/prompt" import { usePlatform } from "@/context/platform" import { SDKProvider, useSDK } from "@/context/sdk" @@ -57,6 +59,10 @@ import { useSync } from "@/context/sync" import { useTabs } from "@/context/tabs" import { TerminalProvider, useTerminal } from "@/context/terminal" import { PromptInput } from "@/components/prompt-input" +import { PromptInputV2Composer, usePromptInputV2Controller } from "@/components/prompt-input-v2" +import { useSettingsCommand } from "@/components/settings-dialog" +import { setCursorPosition } from "@/components/prompt-input/editor-dom" +import { promptLength } from "@/components/prompt-input/history" import { type FollowupDraft, sendFollowupDraft } from "@/components/prompt-input/submit" import { createPromptInputController, @@ -79,6 +85,7 @@ import { SessionSidePanel } from "@/pages/session/session-side-panel" import { sessionPanelLayout } from "@/pages/session/session-panel-layout" import { SessionReviewEmptyChangesV2 } from "@opencode-ai/session-ui/v2/session-review-empty-changes-v2" import { SessionReviewEmptyNoGitV2 } from "@opencode-ai/session-ui/v2/session-review-empty-no-git-v2" +import { SessionReviewV2SidebarToggle } from "@opencode-ai/session-ui/v2/session-review-v2" import { ReviewPanelV2 } from "@/pages/session/v2/review-panel-v2" import { createReviewPanelV2State } from "@/pages/session/v2/review-panel-v2-state" import { reviewDiffDirectory, reviewDiffNeedsLoad, reviewRootDirectory } from "@/pages/session/v2/review-diff-kinds" @@ -151,13 +158,25 @@ export function SessionPage() { // workspace-scoped state (terminal, directory providers) lives below. export function TargetSessionRouteContent() { const params = useParams<{ serverKey: string; id: string }>() + const serverSync = useServerSync() + const directory = createMemo(() => serverSync().session.lineage.peek(params.id)?.session.directory) return ( - - - + // Settings must keep the target-server SDK, sync, and models context and remain registered + // when session content falls back to the route error boundary. + params.id}> + + + + + ) } +function TargetSessionSettingsCommand() { + useSettingsCommand() + return null +} + export function SessionRouteErrorBoundary( props: ParentProps<{ sessionID?: string; serverKey?: ServerConnection.Key; padded?: boolean }>, ) { @@ -246,19 +265,17 @@ function ResolvedTargetSessionRoute() { }) return ( - params.id}> - {/* Non-keyed: closes only while the target's directory is unknown (uncached - lineage mid-resolution), which tears down the workspace subtree including - the terminal. Same-workspace tab switches keep it open because warm - targets resolve synchronously from the sync cache. */} - - - - - - - - + // Non-keyed: closes only while the target's directory is unknown (uncached + // lineage mid-resolution), which tears down the workspace subtree including + // the terminal. Same-workspace tab switches keep it open because warm + // targets resolve synchronously from the sync cache. + + + + + + + ) } @@ -279,10 +296,10 @@ function TargetServerScopedProviders( props: ParentProps<{ directory?: () => string | undefined; sessionID?: () => string | undefined }>, ) { return ( - + <> {props.children} - + ) } @@ -348,6 +365,7 @@ export default function Page() { const platform = usePlatform() const prompt = usePrompt() const comments = useComments() + const command = useCommand() const terminal = useTerminal() const [searchParams, setSearchParams] = useSearchParams<{ prompt?: string }>() const location = useLocation() @@ -515,7 +533,6 @@ export default function Page() { const info = createMemo(() => (params.id ? sync().session.get(params.id) : undefined)) const isChildSession = createMemo(() => !!info()?.parentID) - const diffs = createMemo(() => (params.id ? list(sync().data.session_diff[params.id]) : [])) const canReview = createMemo(() => !!sync().project) const reviewTab = createMemo(() => isDesktop()) const tabState = createSessionTabs({ @@ -614,6 +631,8 @@ export default function Page() { }) let reviewFrame: number | undefined + let todoFrame: number | undefined + let todoTimer: number | undefined let diffFrame: number | undefined let diffTimer: number | undefined @@ -649,7 +668,8 @@ export default function Page() { const mobileChanges = createMemo(() => !isDesktop() && store.mobileTab === "changes") const wantsReview = createMemo(() => isDesktop() - ? desktopFileTreeOpen() || (desktopReviewOpen() && activeTab() === "review") + ? desktopFileTreeOpen() || + (desktopReviewOpen() && (activeTab() === "review" || (newSessionDesign() && !!activeFileTab()))) : store.mobileTab === "changes", ) const vcsMode = createMemo(() => { @@ -670,8 +690,8 @@ export default function Page() { queryFn: mode ? () => sdk() - .client.vcs.diff({ mode }) - .then((result) => list(result.data)) + .api.vcs.diff({ location: { directory: sdk().directory }, mode: mode === "git" ? "working" : mode }) + .then((result) => result.data) .catch((error) => { console.debug("[session-review] failed to load vcs diff", { mode, error }) return [] @@ -718,8 +738,12 @@ export default function Page() { retry: 2, queryFn: () => sdk() - .client.vcs.diff({ mode, directory: scope, context }) - .then((result) => result.data ?? []), + .api.vcs.diff({ + location: { directory: scope }, + mode: mode === "git" ? "working" : mode, + context, + }) + .then((result) => result.data), }) .then((diffs) => diffs.find((diff) => diff.file === file)) @@ -867,6 +891,41 @@ export default function Page() { const hasScrollGesture = () => Date.now() - ui.scrollGesture < scrollGestureWindowMs + createEffect( + on( + () => { + const id = params.id + return [ + sdk().directory, + id, + id ? (sync().data.session_status[id]?.type ?? "idle") : "idle", + id ? composer.blocked() : false, + ] as const + }, + ([dir, id, status, blocked]) => { + if (todoFrame !== undefined) cancelAnimationFrame(todoFrame) + if (todoTimer !== undefined) window.clearTimeout(todoTimer) + todoFrame = undefined + todoTimer = undefined + if (!id) return + if (status === "idle" && !blocked) return + const cached = untrack(() => sync().data.todo[id] !== undefined) + + todoFrame = requestAnimationFrame(() => { + todoFrame = undefined + todoTimer = window.setTimeout(() => { + todoTimer = undefined + if (sdk().directory !== dir || params.id !== id) return + untrack(() => { + void sync().session.todo(id, cached ? { force: true } : undefined) + }) + }, 0) + }) + }, + { defer: true }, + ), + ) + createEffect( on( () => visibleUserMessages().at(-1)?.id, @@ -891,10 +950,11 @@ export default function Page() { ) const stopVcs = sdk().event.listen((evt) => { - if (evt.details.type !== "filesystem.changed") return + const details = evt.details as { type: string; properties?: unknown } + if (details.type !== "file.watcher.updated" && details.type !== "filesystem.changed") return const props = - typeof evt.details.properties === "object" && evt.details.properties - ? (evt.details.properties as Record) + typeof details.properties === "object" && details.properties + ? (details.properties as Record) : undefined const file = typeof props?.file === "string" ? props.file : undefined if (!file || file.startsWith(".git/")) return @@ -1015,7 +1075,10 @@ export default function Page() { if (event.key.length === 1 && event.key !== "Unidentified" && !(event.ctrlKey || event.metaKey)) { if (composer.blocked() || isChildSession()) return - inputRef?.focus() + const input = inputRef + if (!input) return + input.focus() + setCursorPosition(input, prompt.cursor() ?? promptLength(prompt.current())) } } @@ -1081,6 +1144,14 @@ export default function Page() { review: reviewTab, fileBrowser: () => newSessionDesign() && isDesktop() && !!params.id, }) + command.register("session-palette", () => [ + { + id: "command.palette", + title: language.t("command.palette"), + hidden: true, + onSelect: () => command.trigger("file.open", "palette"), + }, + ]) const openReviewFile = createOpenReviewFile({ showAllFiles, @@ -1398,44 +1469,6 @@ export default function Page() { requestAnimationFrame(() => attempt(0)) }) - createEffect(() => { - const id = params.id - if (!id) return - - if (!wantsReview()) return - if (sync().data.session_diff[id] !== undefined) return - if (sync().status === "loading") return - - void sync().session.diff(id) - }) - - createEffect( - on( - () => [sessionKey(), wantsReview()] as const, - ([key, wants]) => { - if (diffFrame !== undefined) cancelAnimationFrame(diffFrame) - if (diffTimer !== undefined) window.clearTimeout(diffTimer) - diffFrame = undefined - diffTimer = undefined - if (!wants) return - - const id = params.id - if (!id) return - if (!untrack(() => sync().data.session_diff[id] !== undefined)) return - - diffFrame = requestAnimationFrame(() => { - diffFrame = undefined - diffTimer = window.setTimeout(() => { - diffTimer = undefined - if (sessionKey() !== key) return - void sync().session.diff(id, { force: true }) - }, 0) - }) - }, - { defer: true }, - ), - ) - let treeDir: string | undefined createEffect(() => { const dir = sdk().directory @@ -1691,7 +1724,7 @@ export default function Page() { setFollowup("failed", input.sessionID, undefined) const ok = await sendFollowupDraft({ - client: sdk().client, + api: sdk().api.session, sync: sync(), serverSync: serverSync(), draft: item, @@ -1787,13 +1820,13 @@ export default function Page() { const halt = (sessionID: string) => busy(sessionID) ? sdk() - .client.session.abort({ sessionID }) + .api.session.interrupt({ sessionID }) .catch(() => {}) : Promise.resolve() const revertMutation = useMutation(() => ({ mutationFn: async (input: { sessionID: string; messageID: string }) => { - const client = sdk().client + const session = sdk().api.session const target = sync() const last = target.session.get(input.sessionID)?.revert const value = draft(input.messageID) @@ -1803,10 +1836,8 @@ export default function Page() { roll(input.sessionID, { messageID: input.messageID }, target) prompt.set(value) }, - request: () => halt(input.sessionID).then(() => client.session.revert(input)), - complete: (result) => { - if (result.data) merge(result.data, target) - }, + request: () => halt(input.sessionID).then(() => session.revert.stage(input)), + complete: () => undefined, rollback: () => roll(input.sessionID, last, target), fail, }) @@ -1818,7 +1849,7 @@ export default function Page() { const sessionID = params.id if (!sessionID) return - const client = sdk().client + const session = sdk().api.session const target = sync() const next = userMessages().find((item) => item.id > id) const last = target.session.get(sessionID)?.revert @@ -1835,11 +1866,9 @@ export default function Page() { }, request: () => !next - ? halt(sessionID).then(() => client.session.unrevert({ sessionID })) - : halt(sessionID).then(() => client.session.revert({ sessionID, messageID: next.id })), - complete: (result) => { - if (result.data) merge(result.data, target) - }, + ? halt(sessionID).then(() => session.revert.clear({ sessionID })) + : halt(sessionID).then(() => session.revert.stage({ sessionID, messageID: next.id }).then(() => undefined)), + complete: () => undefined, rollback: () => roll(sessionID, last, target), fail, }) @@ -1867,7 +1896,30 @@ export default function Page() { .map((item) => ({ id: item.id, text: line(item.id) })) }) - const actions = { revert } + // attachment bytes are embedded as a data URL, so downloading always works; + // revealing requires the on-disk path captured by the client that attached the file + const openAttachment = (file: FilePart) => { + const download = () => { + const anchor = document.createElement("a") + anchor.href = file.url + anchor.download = getFilename(file.filename) || "attachment" + anchor.click() + } + const path = file.filename ?? "" + const absolute = path.startsWith("/") || path.startsWith("\\\\") || /^[a-zA-Z]:[\\/]/.test(path) + if (platform.revealPath && absolute) { + void platform.revealPath(path).then( + (revealed) => { + if (!revealed) download() + }, + () => download(), + ) + return + } + download() + } + + const actions = { revert, openAttachment } createEffect(() => { const sessionID = params.id @@ -1948,6 +2000,8 @@ export default function Page() { onCleanup(() => { if (reviewFrame !== undefined) cancelAnimationFrame(reviewFrame) + if (todoFrame !== undefined) cancelAnimationFrame(todoFrame) + if (todoTimer !== undefined) window.clearTimeout(todoTimer) if (diffFrame !== undefined) cancelAnimationFrame(diffFrame) if (diffTimer !== undefined) window.clearTimeout(diffTimer) if (scrollStateFrame !== undefined) cancelAnimationFrame(scrollStateFrame) @@ -1956,78 +2010,6 @@ export default function Page() { useUsageExceededDialogs() - const composerRegion = () => { - const controller = createSessionComposerRegionController({ - state: composer, - sessionKey, - sessionID: () => params.id, - prompt, - centered, - followup: () => - params.id && !isChildSession() - ? { - items: followupDock(), - sending: sendingFollowup(), - onSend: (id) => void sendFollowup(params.id!, id, { manual: true }), - onEdit: editFollowup, - } - : undefined, - revert: () => - rolled().length > 0 - ? { - items: rolled(), - restoring: restoring(), - disabled: reverting(), - onRestore: restore, - } - : undefined, - onResponseSubmit: resumeScroll, - openParent: () => { - const id = info()?.parentID - if (!id) return - navigate( - params.serverKey - ? sessionHref(requireServerKey(params.serverKey), id) - : legacySessionHref(sdk().directory, id), - ) - }, - setPromptRef: (el) => { - inputRef = el - }, - setDockRef: (el) => { - promptDock = el - }, - }) - return ( - { - inputRef = el - }} - newSessionWorktree={newSessionWorktree()} - onNewSessionWorktreeReset={() => setStore("newSessionWorktree", "main")} - onSubmit={() => { - comments.clear() - resumeScroll() - }} - edit={editingFollowup()} - onEditLoaded={clearFollowupEdit} - shouldQueue={queueEnabled} - onQueue={queueFollowup} - onAbort={() => { - const id = params.id - if (!id) return - setFollowup("paused", id, true) - }} - /> - } - /> - ) - } - const mobileTabs = (compact = false, bottom = false) => (
- {(_) => composerRegion()} + + {(_) => { + const controller = createSessionComposerRegionController({ + state: composer, + sessionKey, + sessionID: () => params.id, + prompt, + ready: () => !store.deferRender && messagesReady(), + centered, + todo: { + collapsed: () => view().todoCollapsed.get(), + onToggle: () => view().todoCollapsed.set(!view().todoCollapsed.get()), + }, + followup: () => + params.id && !isChildSession() + ? { + items: followupDock(), + sending: sendingFollowup(), + onSend: (id) => void sendFollowup(params.id!, id, { manual: true }), + onEdit: editFollowup, + } + : undefined, + revert: () => + rolled().length > 0 + ? { + items: rolled(), + restoring: restoring(), + disabled: reverting(), + onRestore: restore, + } + : undefined, + onResponseSubmit: resumeScroll, + openParent: () => { + const id = info()?.parentID + if (!id) return + navigate( + params.serverKey + ? sessionHref(requireServerKey(params.serverKey), id) + : legacySessionHref(sdk().directory, id), + ) + }, + setPromptRef: (el) => { + inputRef = el + }, + setDockRef: (el) => { + promptDock = el + }, + }) + return ( + { + inputRef = el + }} + newSessionWorktree={newSessionWorktree()} + onNewSessionWorktreeReset={() => setStore("newSessionWorktree", "main")} + onSubmit={() => { + comments.clear() + resumeScroll() + }} + edit={editingFollowup()} + onEditLoaded={clearFollowupEdit} + shouldQueue={queueEnabled} + onQueue={queueFollowup} + onAbort={() => { + const id = params.id + if (!id) return + setFollowup("paused", id, true) + }} + /> + } + > + {(_) => { + const controller = usePromptInputV2Controller({ + get controls() { + return inputController() + }, + ref: (el) => { + inputRef = el + }, + get newSessionWorktree() { + return newSessionWorktree() + }, + onNewSessionWorktreeReset: () => setStore("newSessionWorktree", "main"), + onSubmit: () => { + comments.clear() + resumeScroll() + }, + get edit() { + return editingFollowup() + }, + onEditLoaded: clearFollowupEdit, + shouldQueue: queueEnabled, + onQueue: queueFollowup, + onAbort: () => { + const id = params.id + if (!id) return + setFollowup("paused", id, true) + }, + }) + return + }} + + } + /> + ) + }} + {mobileTabs(true, true)} ) @@ -2203,42 +2298,53 @@ export default function Page() {
- + + +
- hasReview() || reviewV2State.sidebarOpened()} - reviewCount={reviewCount} - reviewPanel={reviewPanelV2} - fileBrowserState={reviewV2State} - activeDiff={activeReviewFile()} - focusReviewDiff={focusReviewDiff} - reviewSnap={ui.reviewSnap} - size={size} - stacked={desktopV2PanelLayout().stacked} - /> + + hasReview() || reviewV2State.sidebarOpened()} + reviewCount={reviewCount} + reviewPanel={reviewPanelV2} + reviewSidebarToggle={(disabled) => ( + + )} + fileBrowserState={reviewV2State} + activeDiff={activeReviewFile()} + focusReviewDiff={focusReviewDiff} + reviewSnap={ui.reviewSnap} + size={size} + stacked={desktopV2PanelLayout().stacked} + /> +
diff --git a/packages/app/src/pages/session/composer/session-composer-controls.ts b/packages/app/src/pages/session/composer/session-composer-controls.ts index 8d5d96719c..a9b0070bc0 100644 --- a/packages/app/src/pages/session/composer/session-composer-controls.ts +++ b/packages/app/src/pages/session/composer/session-composer-controls.ts @@ -2,7 +2,7 @@ import { base64Encode } from "@opencode-ai/core/util/encode" import { createQuery } from "@tanstack/solid-query" import { useNavigate, useSearchParams } from "@solidjs/router" import { type Accessor, createMemo } from "solid-js" -import type { PromptInputControls } from "@/components/prompt-input" +import type { PromptInputControls } from "@/components/prompt-input/contracts" import type { PromptProjectControls } from "@/components/prompt-project-selector" import { useDirectoryPicker } from "@/components/directory-picker" import { useGlobal } from "@/context/global" @@ -12,7 +12,6 @@ import type { QueryOptionsApi } from "@/context/server-sync" import { useServerSDK } from "@/context/server-sdk" import { serverName, ServerConnection, useServer } from "@/context/server" import { useSDK } from "@/context/sdk" -import { useSettings } from "@/context/settings" import { useSync } from "@/context/sync" import { useTabs } from "@/context/tabs" import { useProviders } from "@/hooks/use-providers" @@ -26,36 +25,39 @@ export function createPromptInputController(input: { }) { const layout = useLayout() const local = useLocal() - const providers = useProviders() - const settings = useSettings() - const sync = useSync() const sdk = useSDK() + const sync = useSync() + const providers = useProviders(() => sdk().directory) const view = layout.view(input.sessionKey) const agentsQuery = createQuery(() => input.queryOptions.agents(pathKey(sdk().directory))) const globalProvidersQuery = createQuery(() => input.queryOptions.providers(null)) const providersQuery = createQuery(() => input.queryOptions.providers(pathKey(sdk().directory))) - return createMemo(() => ({ - agents: { - available: sync().data.agent, - options: local.agent.list().map((agent) => agent.name), - current: local.agent.current()?.name ?? "", - loading: agentsQuery.isLoading, - visible: settings.visibility.customAgents(), - select: local.agent.set, - }, - model: { - selection: input.model ?? local.model, - paid: providers.paid().length > 0, - loading: agentsQuery.isLoading || providersQuery.isLoading || globalProvidersQuery.isLoading, - }, - session: { - id: input.sessionID(), - tabs: layout.tabs(input.sessionKey), - reviewPanel: view.reviewPanel, - }, - newLayoutDesigns: settings.general.newLayoutDesigns(), - })) + return createMemo(() => { + return { + agents: { + available: sync().data.agent, + options: local.agent.list().map((agent) => agent.name), + current: local.agent.current()?.name ?? "", + loading: agentsQuery.isLoading, + visible: local.agent.visible(), + select: local.agent.set, + }, + model: { + selection: input.model ?? local.model, + paid: providers.paid().length > 0, + loading: + (local.agent.visible() && agentsQuery.isLoading) || + providersQuery.isLoading || + globalProvidersQuery.isLoading, + }, + session: { + id: input.sessionID(), + tabs: layout.tabs(input.sessionKey), + reviewPanel: view.reviewPanel, + }, + } + }) } export function createPromptProjectControls() { diff --git a/packages/app/src/pages/session/composer/session-composer-region-controller.ts b/packages/app/src/pages/session/composer/session-composer-region-controller.ts index ff425b4c20..4073f1c191 100644 --- a/packages/app/src/pages/session/composer/session-composer-region-controller.ts +++ b/packages/app/src/pages/session/composer/session-composer-region-controller.ts @@ -1,4 +1,7 @@ -import { type Accessor, createEffect, createMemo, createResource } from "solid-js" +import { createResizeObserver } from "@solid-primitives/resize-observer" +import { useSpring } from "@opencode-ai/ui/motion-spring" +import { type Accessor, createEffect, createMemo, createResource, onCleanup } from "solid-js" +import { createStore } from "solid-js/store" import type { PromptInputState } from "@/components/prompt-input" import { useSync } from "@/context/sync" import { getSessionHandoff, setSessionHandoff } from "@/pages/session/handoff" @@ -23,7 +26,12 @@ export function createSessionComposerRegionController(input: { sessionKey: Accessor sessionID: Accessor prompt: PromptInputState + ready: Accessor centered: Accessor + todo: { + collapsed: Accessor + onToggle: () => void + } followup: Accessor revert: Accessor onResponseSubmit: () => void @@ -32,6 +40,41 @@ export function createSessionComposerRegionController(input: { setDockRef: (el: HTMLDivElement) => void }) { const sync = useSync() + const [store, setStore] = createStore({ + ready: input.ready() || input.state.dock(), + height: 320, + body: undefined as HTMLDivElement | undefined, + }) + let timer: number | undefined + let frame: number | undefined + + const clear = () => { + if (timer !== undefined) window.clearTimeout(timer) + if (frame !== undefined) cancelAnimationFrame(frame) + timer = undefined + frame = undefined + } + + createEffect(() => { + input.sessionKey() + const ready = input.ready() + const dock = input.state.dock() + + clear() + if (store.ready || (!ready && !dock)) return + if (dock) { + setStore("ready", true) + return + } + + frame = requestAnimationFrame(() => { + frame = undefined + timer = window.setTimeout(() => { + setStore("ready", true) + timer = undefined + }, 140) + }) + }) createEffect(() => { if (!input.prompt.ready()) return @@ -49,10 +92,27 @@ export function createSessionComposerRegionController(input: { }) }) + createEffect(() => { + const el = store.body + if (!el) return + const update = () => setStore("height", el.getBoundingClientRect().height) + createResizeObserver(el, update) + update() + }) + + onCleanup(clear) + const parentID = createMemo(() => { const id = input.sessionID() return id ? sync().session.get(id)?.parentID : undefined }) + const open = createMemo(() => store.ready && input.state.dock() && !input.state.closing()) + const progress = useSpring( + () => (open() ? 1 : 0), + { visualDuration: 0.3, bounce: 0 }, + () => `${input.sessionKey()}\0${store.ready}`, + ) + const value = createMemo(() => Math.max(0, Math.min(1, progress()))) const ready = Promise.resolve() const [promptReady] = createResource( () => input.prompt.ready.promise ?? ready, @@ -62,6 +122,7 @@ export function createSessionComposerRegionController(input: { return { state: input.state, centered: input.centered, + todo: input.todo, followup: input.followup, revert: input.revert, onResponseSubmit: input.onResponseSubmit, @@ -73,7 +134,11 @@ export function createSessionComposerRegionController(input: { showComposer: () => !input.state.blocked() || !!parentID(), handoffPrompt: () => getSessionHandoff(input.sessionKey())?.prompt, promptReady: () => input.prompt.ready() || promptReady(), - lift: () => (input.revert()?.items.length ? 18 : 0), + dock: () => (store.ready && input.state.dock()) || value() > 0.001, + dockProgress: value, + dockHeight: () => Math.max(78, store.height), + lift: () => (input.revert()?.items.length ? 18 : 36 * value()), + setDockBodyRef: (el: HTMLDivElement) => setStore("body", el), } } diff --git a/packages/app/src/pages/session/composer/session-composer-region.tsx b/packages/app/src/pages/session/composer/session-composer-region.tsx index feb2b0aee4..600ff41e3d 100644 --- a/packages/app/src/pages/session/composer/session-composer-region.tsx +++ b/packages/app/src/pages/session/composer/session-composer-region.tsx @@ -5,6 +5,7 @@ import { SessionPermissionDock } from "@/pages/session/composer/session-permissi import { SessionQuestionDock } from "@/pages/session/composer/session-question-dock" import { SessionFollowupDock } from "@/pages/session/composer/session-followup-dock" import { SessionRevertDock } from "@/pages/session/composer/session-revert-dock" +import { SessionTodoDock } from "@/pages/session/composer/session-todo-dock" import type { SessionComposerRegionController } from "./session-composer-region-controller" export function SessionComposerRegion(props: { @@ -59,6 +60,28 @@ export function SessionComposerRegion(props: { + +
+
+ +
+
+
)} -
+
{controller.handoffPrompt() || language.t("prompt.loading")}
@@ -83,7 +109,11 @@ export function SessionComposerRegion(props: { > {(revert) => ( -
+
@@ -103,3 +104,35 @@ describe("sessionQuestionRequest", () => { expect(sessionQuestionRequest(sessions, questions, "root")?.id).toBe("q-grand") }) }) + +describe("todoState", () => { + test("hides when there are no todos", () => { + expect(todoState({ count: 0, done: false, live: true })).toBe("hide") + }) + + test("opens while the session is still working", () => { + expect(todoState({ count: 2, done: false, live: true })).toBe("open") + }) + + test("closes completed todos after a running turn", () => { + expect(todoState({ count: 2, done: true, live: true })).toBe("close") + }) + + test("clears stale todos when the turn ends", () => { + expect(todoState({ count: 2, done: false, live: false })).toBe("clear") + }) + + test("clears completed todos when the session is no longer live", () => { + expect(todoState({ count: 2, done: true, live: false })).toBe("clear") + }) +}) + +describe("todoDockAtBoundary", () => { + test("shows active todos when entering a session", () => { + expect(todoDockAtBoundary("open")).toBe(true) + }) + + test("hides completed todos when entering a session", () => { + expect(todoDockAtBoundary("close")).toBe(false) + }) +}) diff --git a/packages/app/src/pages/session/composer/session-composer-state.ts b/packages/app/src/pages/session/composer/session-composer-state.ts index eb282c15fc..f54e0c9e4f 100644 --- a/packages/app/src/pages/session/composer/session-composer-state.ts +++ b/packages/app/src/pages/session/composer/session-composer-state.ts @@ -1,20 +1,35 @@ -import { createMemo } from "solid-js" +import { createEffect, createMemo, on, onCleanup } from "solid-js" import { createStore } from "solid-js/store" -import type { PermissionRequest, QuestionRequest } from "@opencode-ai/sdk/v2" +import type { PermissionRequest, QuestionRequest, Todo } from "@opencode-ai/sdk/v2" import { useParams } from "@solidjs/router" import { showToast } from "@/utils/toast" +import { useServerSync } from "@/context/server-sync" import { useLanguage } from "@/context/language" import { usePermission } from "@/context/permission" import { useSDK } from "@/context/sdk" import { useSync } from "@/context/sync" import { sessionPermissionRequest, sessionQuestionRequest } from "./session-request-tree" +export const todoState = (input: { + count: number + done: boolean + live: boolean +}): "hide" | "clear" | "open" | "close" => { + if (input.count === 0) return "hide" + if (!input.live) return "clear" + if (!input.done) return "open" + return "close" +} + +export const todoDockAtBoundary = (state: ReturnType) => state === "open" + const idle = { type: "idle" as const } -export function createSessionComposerController() { +export function createSessionComposerController(options?: { closeMs?: number | (() => number) }) { const params = useParams() const sdk = useSDK() const sync = useSync() + const serverSync = useServerSync() const language = useLanguage() const permission = usePermission() @@ -34,8 +49,24 @@ export function createSessionComposerController() { return !!permissionRequest() || !!questionRequest() }) + const todos = createMemo((): Todo[] => { + const id = params.id + if (!id) return [] + return serverSync().session.data.todo[id] ?? [] + }) + + const done = createMemo( + () => todos().length > 0 && todos().every((todo) => todo.status === "completed" || todo.status === "cancelled"), + ) + + const live = createMemo(() => sync().data.session_working(params.id ?? "") || blocked()) + const [store, setStore] = createStore({ + sessionID: params.id, responding: undefined as string | undefined, + dock: todos().length > 0 && !done() && live(), + closing: false, + opening: false, }) const permissionResponding = createMemo(() => { @@ -51,7 +82,7 @@ export function createSessionComposerController() { setStore("responding", perm.id) sdk() - .client.permission.respond({ sessionID: perm.sessionID, permissionID: perm.id, response }) + .api.permission.reply({ sessionID: perm.sessionID, requestID: perm.id, reply: response }) .catch((err: unknown) => { const description = err instanceof Error ? err.message : String(err) showToast({ title: language.t("common.requestFailed"), description }) @@ -61,12 +92,112 @@ export function createSessionComposerController() { }) } + let timer: number | undefined + let raf: number | undefined + + const closeMs = () => { + const value = options?.closeMs + if (typeof value === "function") return Math.max(0, value()) + if (typeof value === "number") return Math.max(0, value) + return 400 + } + + const scheduleClose = () => { + if (timer) window.clearTimeout(timer) + timer = window.setTimeout(() => { + setStore({ dock: false, closing: false }) + timer = undefined + }, closeMs()) + } + + // Keep stale turn todos from reopening if the model never clears them. + const clear = () => { + const id = params.id + if (!id) return + sync().set("todo", id, []) + } + + createEffect( + on( + () => [params.id, todos().length, done(), live()] as const, + ([id, count, complete, active], previous) => { + if (raf) cancelAnimationFrame(raf) + raf = undefined + + const next = todoState({ + count, + done: complete, + live: active, + }) + + if (!previous || previous[0] !== id) { + if (timer) window.clearTimeout(timer) + timer = undefined + setStore({ sessionID: id, dock: todoDockAtBoundary(next), closing: false, opening: false }) + if (next === "clear") clear() + return + } + + if (next === "hide") { + if (timer) window.clearTimeout(timer) + timer = undefined + setStore({ dock: false, closing: false, opening: false }) + return + } + + if (next === "clear") { + if (timer) window.clearTimeout(timer) + timer = undefined + clear() + return + } + + if (next === "open") { + if (timer) window.clearTimeout(timer) + timer = undefined + const hidden = !store.dock || store.closing + setStore({ dock: true, closing: false }) + if (hidden) { + setStore("opening", true) + raf = requestAnimationFrame(() => { + setStore("opening", false) + raf = undefined + }) + return + } + setStore("opening", false) + return + } + + setStore({ dock: true, opening: false, closing: true }) + if (!timer) scheduleClose() + }, + ), + ) + + onCleanup(() => { + if (!timer) return + window.clearTimeout(timer) + }) + + onCleanup(() => { + if (!raf) return + cancelAnimationFrame(raf) + }) + return { blocked, questionRequest, permissionRequest, permissionResponding, decide, + todos, + dock: () => + store.sessionID === params.id + ? store.dock + : todoDockAtBoundary(todoState({ count: todos().length, done: done(), live: live() })), + closing: () => store.sessionID === params.id && store.closing, + opening: () => store.sessionID === params.id && store.opening, } } diff --git a/packages/app/src/pages/session/composer/session-question-dock.tsx b/packages/app/src/pages/session/composer/session-question-dock.tsx index 445a9f47a0..941424e247 100644 --- a/packages/app/src/pages/session/composer/session-question-dock.tsx +++ b/packages/app/src/pages/session/composer/session-question-dock.tsx @@ -223,7 +223,8 @@ export const SessionQuestionDock: Component<{ request: QuestionRequest; onSubmit } const replyMutation = useMutation(() => ({ - mutationFn: (answers: QuestionAnswer[]) => sdk().client.question.reply({ requestID: props.request.id, answers }), + mutationFn: (answers: QuestionAnswer[]) => + sdk().api.question.reply({ sessionID: props.request.sessionID, requestID: props.request.id, answers }), onMutate: () => { props.onSubmit() }, @@ -235,7 +236,7 @@ export const SessionQuestionDock: Component<{ request: QuestionRequest; onSubmit })) const rejectMutation = useMutation(() => ({ - mutationFn: () => sdk().client.question.reject({ requestID: props.request.id }), + mutationFn: () => sdk().api.question.reject({ sessionID: props.request.sessionID, requestID: props.request.id }), onMutate: () => { props.onSubmit() }, diff --git a/packages/app/src/pages/session/composer/session-revert-dock.stories.tsx b/packages/app/src/pages/session/composer/session-revert-dock.stories.tsx index dd030870a1..ce3fd26b02 100644 --- a/packages/app/src/pages/session/composer/session-revert-dock.stories.tsx +++ b/packages/app/src/pages/session/composer/session-revert-dock.stories.tsx @@ -1,5 +1,6 @@ import { For } from "solid-js" import { createStore } from "solid-js/store" +import { DockShell } from "@opencode-ai/ui/dock-surface" import { SessionRevertDock } from "@/pages/session/composer/session-revert-dock" import { SettingsProvider, useSettings } from "@/context/settings" @@ -78,12 +79,17 @@ function Stage(props: { count: number }) { {/* Reproduce the real composer stack: dock + card overlapping the dock's bottom by lift() = 18px */}
-
Ask anything... -
+
diff --git a/packages/app/src/pages/session/composer/session-todo-dock.tsx b/packages/app/src/pages/session/composer/session-todo-dock.tsx new file mode 100644 index 0000000000..d44b490262 --- /dev/null +++ b/packages/app/src/pages/session/composer/session-todo-dock.tsx @@ -0,0 +1,277 @@ +import type { Todo } from "@opencode-ai/sdk/v2" +import { AnimatedNumber } from "@opencode-ai/ui/animated-number" +import { Checkbox } from "@opencode-ai/ui/checkbox" +import { DockTray } from "@opencode-ai/ui/dock-surface" +import { IconButton } from "@opencode-ai/ui/icon-button" +import { useSpring } from "@opencode-ai/ui/motion-spring" +import { TextReveal } from "@opencode-ai/ui/text-reveal" +import { TextStrikethrough } from "@opencode-ai/ui/text-strikethrough" +import { createResizeObserver } from "@solid-primitives/resize-observer" +import { Index, createEffect, createMemo } from "solid-js" +import { Dynamic } from "solid-js/web" +import { createStore } from "solid-js/store" +import { useLanguage } from "@/context/language" +import { useSettings } from "@/context/settings" + +const doneToken = "\u0000done\u0000" +const totalToken = "\u0000total\u0000" + +function dot(status: Todo["status"]) { + if (status !== "in_progress") return undefined + return ( + + + + ) +} + +export function SessionTodoDock(props: { + todos: Todo[] + collapsed: boolean + onToggle: () => void + collapseLabel: string + expandLabel: string + dockProgress: number +}) { + const language = useLanguage() + const settings = useSettings() + const [store, setStore] = createStore({ + height: 78, + }) + + const total = createMemo(() => props.todos.length) + const done = createMemo(() => props.todos.filter((todo) => todo.status === "completed").length) + const label = createMemo(() => language.t("session.todo.progress", { done: done(), total: total() })) + const progress = createMemo(() => + language + .t("session.todo.progress", { done: doneToken, total: totalToken }) + .split(/(\u0000done\u0000|\u0000total\u0000)/), + ) + + const active = createMemo( + () => + props.todos.find((todo) => todo.status === "in_progress") ?? + props.todos.find((todo) => todo.status === "pending") ?? + props.todos.filter((todo) => todo.status === "completed").at(-1) ?? + props.todos[0], + ) + + const preview = createMemo(() => active()?.content ?? "") + const collapse = useSpring(() => (props.collapsed ? 1 : 0), { visualDuration: 0.3, bounce: 0 }) + const dock = createMemo(() => Math.max(0, Math.min(1, props.dockProgress))) + const shut = createMemo(() => 1 - dock()) + const value = createMemo(() => Math.max(0, Math.min(1, collapse()))) + const hide = createMemo(() => Math.max(value(), shut())) + const off = createMemo(() => hide() > 0.98) + const turn = createMemo(() => Math.max(0, Math.min(1, value()))) + const full = createMemo(() => Math.max(78, store.height)) + let contentRef: HTMLDivElement | undefined + + createEffect(() => { + const el = contentRef + if (!el) return + const update = () => { + setStore("height", (height) => Math.max(height, el.scrollHeight)) + } + update() + createResizeObserver(el, update) + }) + + return ( + +
+
{ + if (event.key !== "Enter" && event.key !== " ") return + event.preventDefault() + props.onToggle() + }} + > + + + {(item) => + item() === doneToken ? ( + + ) : item() === totalToken ? ( + + ) : ( + {item()} + ) + } + + +
+ +
+
+ { + event.preventDefault() + event.stopPropagation() + }} + onClick={(event) => { + event.stopPropagation() + props.onToggle() + }} + aria-label={props.collapsed ? props.expandLabel : props.collapseLabel} + /> +
+
+ +
0.1, + }} + style={{ + visibility: off() ? "hidden" : "visible", + opacity: `${Math.max(0, Math.min(1, 1 - hide()))}`, + }} + > + +
+
+
+ ) +} + +function TodoList(props: { todos: Todo[] }) { + const [store, setStore] = createStore({ + stuck: false, + }) + + return ( +
+
{ + setStore("stuck", e.currentTarget.scrollTop > 0) + }} + > + + {(todo) => ( + + + + )} + +
+
+
+ ) +} diff --git a/packages/app/src/pages/session/composer/todo-panel-motion.stories.tsx b/packages/app/src/pages/session/composer/todo-panel-motion.stories.tsx new file mode 100644 index 0000000000..41d1f984bd --- /dev/null +++ b/packages/app/src/pages/session/composer/todo-panel-motion.stories.tsx @@ -0,0 +1,622 @@ +// @ts-nocheck +import { createEffect, createMemo, onCleanup } from "solid-js" +import { createStore } from "solid-js/store" +import type { Todo } from "@opencode-ai/sdk/v2" +import { useServerSync } from "@/context/global-sync" +import { PromptInput } from "@/components/prompt-input" +import { usePrompt } from "@/context/prompt" +import { + SessionComposerRegion, + createSessionComposerController, + createSessionComposerRegionController, +} from "@/pages/session/composer" + +export default { + title: "UI/Todo Panel Motion", + id: "components-todo-panel-motion", + tags: ["autodocs"], + parameters: { + docs: { + description: { + component: `### Overview +This playground renders the real session composer region from app code. + +### Source path +- \`packages/app/src/pages/session/composer/session-composer-region.tsx\` + +### Includes +- \`SessionTodoDock\` (real) +- \`PromptInput\` (real) + +No visual reimplementation layer is used for the dock/input stack.`, + }, + }, + }, +} + +const pool = [ + "Refactor ToolStatusTitle DOM measurement to offscreen global measurer (unconstrained by timeline layout)", + "Remove inline measure nodes/CSS hooks and keep width morph behavior intact", + "Run typechecks/tests and report what changed", + "Verify reduced-motion behavior in timeline", + "Review diff for animation edge cases", + "Document rollout notes in PR description", + "Check keyboard and screen reader semantics", + "Add storybook controls for iteration speed", +] + +const btn = (accent?: boolean) => + ({ + padding: "6px 14px", + "border-radius": "6px", + border: "1px solid var(--color-divider, #333)", + background: accent ? "var(--color-accent, #58f)" : "var(--color-fill-element, #222)", + color: "var(--color-text, #eee)", + cursor: "pointer", + "font-size": "13px", + }) as const + +const controls = { + agents: { available: [], options: ["build"], current: "build", loading: false, visible: true, select: () => {} }, + model: { + selection: { + current: () => ({ id: "claude-3-7-sonnet", name: "Claude 3.7 Sonnet", provider: { id: "anthropic" } }), + variant: { list: () => [], current: () => undefined, set: () => {} }, + }, + paid: true, + loading: false, + }, + session: { + id: "story-session", + tabs: { active: () => undefined, all: () => [], open: () => {}, setActive: () => {} }, + reviewPanel: { opened: () => false, open: () => {} }, + }, +} + +const css = ` +[data-component="todo-stage"] { + display: grid; + gap: 20px; + padding: 20px; +} + +[data-component="todo-preview"] { + height: 560px; + min-height: 0; +} + +[data-component="todo-session-root"] { + position: relative; + width: 100%; + height: 100%; + overflow: hidden; + display: flex; + flex-direction: column; + background: var(--background-base); + border: 1px solid var(--border-weak-base); + border-radius: 12px; +} + +[data-component="todo-session-frame"] { + flex: 1 1 auto; + min-height: 0; + display: flex; + flex-direction: column; +} + +[data-component="todo-session-panel"] { + position: relative; + flex: 1 1 auto; + min-height: 0; + height: 100%; + display: flex; + flex-direction: column; + background: var(--background-stronger); +} + +[data-slot="todo-preview-content"] { + flex: 1 1 auto; + min-height: 0; + overflow: hidden; +} + +[data-slot="todo-preview-scroll"] { + height: 100%; + overflow: auto; + min-height: 0; + padding: 14px 16px; + display: flex; + flex-direction: column; + gap: 10px; +} + +[data-slot="todo-preview-spacer"] { + flex: 1 1 auto; + min-height: 0; +} + +[data-slot="todo-preview-msg"] { + border-radius: 8px; + border: 1px solid var(--border-weak-base); + background: var(--surface-base); + color: var(--text-weak); + padding: 8px 10px; + font-size: 13px; + line-height: 1.35; +} + +[data-slot="todo-preview-msg"][data-strong="true"] { + color: var(--text-strong); +} +` + +export const Playground = { + render: () => { + const global = useServerSync() + const prompt = usePrompt() + const [cfg, setCfg] = createStore({ + open: true, + collapsed: false, + step: 1, + dockOpenDuration: 0.3, + dockOpenBounce: 0, + dockCloseDuration: 0.3, + dockCloseBounce: 0, + drawerExpandDuration: 0.3, + drawerExpandBounce: 0, + drawerCollapseDuration: 0.3, + drawerCollapseBounce: 0, + subtitleDuration: 600, + subtitleAuto: true, + subtitleTravel: 25, + subtitleEdge: 17, + countDuration: 600, + countMask: 18, + countMaskHeight: 0, + countWidthDuration: 560, + }) + const open = () => cfg.open + const step = () => cfg.step + const dockOpenDuration = () => cfg.dockOpenDuration + const dockOpenBounce = () => cfg.dockOpenBounce + const dockCloseDuration = () => cfg.dockCloseDuration + const dockCloseBounce = () => cfg.dockCloseBounce + const drawerExpandDuration = () => cfg.drawerExpandDuration + const drawerExpandBounce = () => cfg.drawerExpandBounce + const drawerCollapseDuration = () => cfg.drawerCollapseDuration + const drawerCollapseBounce = () => cfg.drawerCollapseBounce + const subtitleDuration = () => cfg.subtitleDuration + const subtitleAuto = () => cfg.subtitleAuto + const subtitleTravel = () => cfg.subtitleTravel + const subtitleEdge = () => cfg.subtitleEdge + const countDuration = () => cfg.countDuration + const countMask = () => cfg.countMask + const countMaskHeight = () => cfg.countMaskHeight + const countWidthDuration = () => cfg.countWidthDuration + const state = createSessionComposerController({ closeMs: () => Math.round(dockCloseDuration() * 1000) }) + let frame + let scrollRef + + const todos = createMemo(() => { + const done = Math.max(0, Math.min(3, step())) + return pool.slice(0, 3).map((content, i) => ({ + id: `todo-${i + 1}`, + content, + status: i < done ? "completed" : i === done && done < 3 ? "in_progress" : "pending", + })) + }) + + createEffect(() => { + global.todo.set("story-session", todos()) + }) + + const clear = () => { + if (frame) cancelAnimationFrame(frame) + frame = undefined + } + + const pin = () => { + if (!scrollRef) return + scrollRef.scrollTop = scrollRef.scrollHeight + } + + const collapsed = () => cfg.collapsed + const setCollapsed = (value: boolean) => setCfg("collapsed", value) + const openDock = () => { + clear() + setCfg("open", true) + frame = requestAnimationFrame(() => { + pin() + frame = undefined + }) + } + + const closeDock = () => { + clear() + setCfg("open", false) + } + + const dockOpen = () => open() + + const toggleDock = () => { + if (dockOpen()) { + closeDock() + return + } + openDock() + } + + const toggleDrawer = () => { + if (!dockOpen()) { + openDock() + frame = requestAnimationFrame(() => { + pin() + setCollapsed(true) + frame = undefined + }) + return + } + setCollapsed(!collapsed()) + } + + const cycle = () => { + setCfg("step", (value) => (value + 1) % 4) + } + + onCleanup(clear) + + return ( +
+ + +
+
+
+
+
+
+
+
+ Thinking Checking type safety +
+
Shell Prints five topic blocks between timed commands
+
+
+ +
+ "story-session", + sessionID: () => "story-session", + prompt, + ready: () => true, + centered: () => false, + todo: { collapsed, onToggle: () => setCollapsed(!collapsed()) }, + followup: () => undefined, + revert: () => undefined, + onResponseSubmit: pin, + openParent: () => {}, + setPromptRef: () => {}, + setDockRef: () => {}, + })} + promptInput={ + {}, handleSubmit: (event) => event.preventDefault() }} + ref={() => {}} + newSessionWorktree="" + onNewSessionWorktreeReset={() => {}} + /> + } + /> +
+
+
+
+
+ +
+ + + + {[0, 1, 2, 3].map((value) => ( + + ))} +
+ +
+
Dock open
+ + + +
+ Dock close +
+ + + +
+ Drawer expand +
+ + + +
+ Drawer collapse +
+ + + +
+ Subtitle odometer +
+ + + + + +
+ Count odometer +
+ + + + +
+
+ ) + }, +} diff --git a/packages/app/src/pages/session/file-tabs.tsx b/packages/app/src/pages/session/file-tabs.tsx index dd51dd0585..b67b810a4b 100644 --- a/packages/app/src/pages/session/file-tabs.tsx +++ b/packages/app/src/pages/session/file-tabs.tsx @@ -1,4 +1,4 @@ -import { createEffect, createMemo, createSignal, Match, on, onCleanup, Switch } from "solid-js" +import { createEffect, createMemo, createSignal, Match, on, onCleanup, Show, Switch } from "solid-js" import { createStore } from "solid-js/store" import { Dynamic } from "solid-js/web" import { makeEventListener } from "@solid-primitives/event-listener" @@ -6,9 +6,12 @@ import type { FileSearchHandle } from "@opencode-ai/session-ui/file" import { useFileComponent } from "@opencode-ai/ui/context/file" import { cloneSelectedLineRange, previewSelectedLines } from "@opencode-ai/session-ui/pierre/selection-bridge" import { createLineCommentController } from "@opencode-ai/session-ui/line-comment-annotations" +import { createLineCommentControllerV2 } from "@opencode-ai/session-ui/v2/line-comment-annotations-v2" import { sampledChecksum } from "@opencode-ai/core/util/encode" import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu" import { IconButton } from "@opencode-ai/ui/icon-button" +import { LineCommentV2OverflowIcon } from "@opencode-ai/ui/v2/line-comment-v2" +import { MenuV2 } from "@opencode-ai/ui/v2/menu-v2" import { Tabs } from "@opencode-ai/ui/tabs" import { ScrollView } from "@opencode-ai/ui/scroll-view" import { showToast } from "@/utils/toast" @@ -16,10 +19,17 @@ import { selectionFromLines, useFile, type FileSelection, type SelectedLineRange import { useComments } from "@/context/comments" import { useLanguage } from "@/context/language" import { usePrompt } from "@/context/prompt" +import { useSettings } from "@/context/settings" import { getSessionHandoff } from "@/pages/session/handoff" import { useSessionLayout } from "@/pages/session/session-layout" import { createSessionTabs } from "@/pages/session/helpers" +type SessionFileViewProps = { + tab: string +} + +const selectionSide = (range: SelectedLineRange) => range.endSide ?? range.side ?? "additions" + function FileCommentMenu(props: { moreLabel: string editLabel: string @@ -53,6 +63,30 @@ function FileCommentMenu(props: { ) } +function FileCommentMenuV2(props: { + moreLabel: string + editLabel: string + deleteLabel: string + onEdit: VoidFunction + onDelete: VoidFunction +}) { + return ( +
event.stopPropagation()} onClick={(event) => event.stopPropagation()}> + + + + + + + {props.editLabel} + {props.deleteLabel} + + + +
+ ) +} + type ScrollPos = { x: number; y: number } function createScrollSync(input: { tab: () => string; view: ReturnType["view"] }) { @@ -179,7 +213,17 @@ export function FileTabContent(props: { tab: string }) { ) } -export function SessionFileView(props: { tab: string }) { +export function SessionFileView(props: SessionFileViewProps) { + const settings = useSettings() + + return ( + }> + + + ) +} + +function SessionFileViewV1(props: { tab: string }) { const file = useFile() const comments = useComments() const language = useLanguage() @@ -463,3 +507,294 @@ export function SessionFileView(props: { tab: string }) { return content() } + +function SessionFileViewV2(props: { tab: string }) { + const file = useFile() + const comments = useComments() + const language = useLanguage() + const prompt = usePrompt() + const fileComponent = useFileComponent() + const { sessionKey, tabs, view } = useSessionLayout() + const activeFileTab = createSessionTabs({ + tabs, + pathFromTab: file.pathFromTab, + normalizeTab: (tab) => (tab.startsWith("file://") ? file.tab(tab) : tab), + }).activeFileTab + + let find: FileSearchHandle | null = null + + const search = { + register: (handle: FileSearchHandle | null) => { + find = handle + }, + } + + const path = createMemo(() => file.pathFromTab(props.tab)) + const state = createMemo(() => { + const p = path() + if (!p) return + return file.get(p) + }) + const contents = createMemo(() => state()?.content?.content ?? "") + const cacheKey = createMemo(() => sampledChecksum(contents())) + const selectedLines = createMemo(() => { + const p = path() + if (!p) return null + if (file.ready()) return (file.selectedLines(p) as SelectedLineRange | undefined) ?? null + return (getSessionHandoff(sessionKey())?.files[p] as SelectedLineRange | undefined) ?? null + }) + const scrollSync = createScrollSync({ + tab: () => props.tab, + view, + }) + + const selectionPreview = (source: string, selection: FileSelection) => { + return previewSelectedLines(source, { + start: selection.startLine, + end: selection.endLine, + }) + } + + const buildPreview = (filePath: string, lines: SelectedLineRange) => { + const source = filePath === path() ? contents() : file.get(filePath)?.content?.content + if (!source) return undefined + return selectionPreview(source, selectionFromLines(lines)) + } + + const addCommentToContext = (input: { + file: string + selection: SelectedLineRange + comment: string + preview?: string + origin?: "review" | "file" + }) => { + const selection = selectionFromLines(input.selection) + const preview = input.preview ?? buildPreview(input.file, input.selection) + + const saved = comments.add({ + file: input.file, + selection: input.selection, + comment: input.comment, + }) + prompt.context.add({ + type: "file", + path: input.file, + selection, + comment: input.comment, + commentID: saved.id, + commentOrigin: input.origin, + preview, + }) + } + + const updateCommentInContext = (input: { + id: string + file: string + selection: SelectedLineRange + comment: string + }) => { + comments.update(input.file, input.id, input.comment) + const preview = input.file === path() ? buildPreview(input.file, input.selection) : undefined + prompt.context.updateComment(input.file, input.id, { + comment: input.comment, + ...(preview ? { preview } : {}), + }) + } + + const removeCommentFromContext = (input: { id: string; file: string }) => { + comments.remove(input.file, input.id) + prompt.context.removeComment(input.file, input.id) + } + + const fileComments = createMemo(() => { + const p = path() + if (!p) return [] + return comments.list(p) + }) + + const commentedLines = createMemo(() => fileComments().map((comment) => comment.selection)) + + const [note, setNote] = createStore({ + openedComment: null as string | null, + commenting: null as SelectedLineRange | null, + selected: null as SelectedLineRange | null, + }) + + const syncSelected = (range: SelectedLineRange | null) => { + const p = path() + if (!p) return + file.setSelectedLines(p, range ? cloneSelectedLineRange(range) : null) + } + + const activeSelection = () => note.selected ?? selectedLines() + + const commentsUi = createLineCommentControllerV2({ + comments: fileComments, + label: language.t("ui.lineComment.submit"), + draftKey: () => path() ?? props.tab, + mention: { + items: file.searchFilesAndDirectories, + }, + getSide: selectionSide, + state: { + opened: () => note.openedComment, + setOpened: (id) => setNote("openedComment", id), + selected: () => note.selected, + setSelected: (range) => setNote("selected", range), + commenting: () => note.commenting, + setCommenting: (range) => setNote("commenting", range), + syncSelected, + hoverSelected: syncSelected, + }, + onSubmit: ({ comment, selection }) => { + const p = path() + if (!p) return + addCommentToContext({ file: p, selection, comment, origin: "file" }) + }, + onUpdate: ({ id, comment, selection }) => { + const p = path() + if (!p) return + updateCommentInContext({ id, file: p, selection, comment }) + }, + onDelete: (comment) => { + const p = path() + if (!p) return + removeCommentFromContext({ id: comment.id, file: p }) + }, + editSubmitLabel: language.t("common.save"), + renderCommentActions: (_, controls) => ( + + ), + }) + + createEffect(() => { + if (typeof window === "undefined") return + + const onKeyDown = (event: KeyboardEvent) => { + if (activeFileTab() !== props.tab) return + if (!(event.metaKey || event.ctrlKey) || event.altKey || event.shiftKey) return + if (event.key.toLowerCase() !== "f") return + + event.preventDefault() + event.stopPropagation() + find?.focus() + } + + makeEventListener(window, "keydown", onKeyDown, { capture: true }) + }) + + createEffect( + on( + path, + () => { + commentsUi.note.reset() + }, + { defer: true }, + ), + ) + + createEffect(() => { + const focus = comments.focus() + const p = path() + if (!focus || !p) return + if (focus.file !== p) return + if (activeFileTab() !== props.tab) return + + const target = fileComments().find((comment) => comment.id === focus.id) + if (!target) return + + commentsUi.note.openComment(target.id, target.selection, { cancelDraft: true }) + requestAnimationFrame(() => comments.clearFocus()) + }) + + let prev = { + loaded: false, + ready: false, + active: false, + } + + createEffect(() => { + const loaded = !!state()?.loaded + const ready = file.ready() + const active = activeFileTab() === props.tab + const restore = (loaded && !prev.loaded) || (ready && !prev.ready) || (active && loaded && !prev.active) + prev = { loaded, ready, active } + if (!restore) return + scrollSync.queueRestore() + }) + + const renderFile = (source: string) => ( +
+ { + scrollSync.queueRestore() + }} + annotations={commentsUi.annotations()} + renderAnnotation={commentsUi.renderAnnotation} + renderGutterUtility={commentsUi.renderGutterUtility} + onLineSelected={(range: SelectedLineRange | null) => { + commentsUi.onLineSelected(range) + }} + onLineSelectionEnd={(range: SelectedLineRange | null) => { + if (!range) { + commentsUi.note.select(null) + commentsUi.note.cancelDraft() + return + } + commentsUi.onLineSelectionEnd(range) + }} + onLineNumberSelectionEnd={(range: SelectedLineRange | null) => { + commentsUi.onLineNumberSelectionEnd(range) + }} + search={search} + class="select-text" + media={{ + mode: "auto", + path: path(), + current: state()?.content, + onLoad: scrollSync.queueRestore, + onError: (args: { kind: "image" | "audio" | "svg" }) => { + if (args.kind !== "svg") return + showToast({ + variant: "error", + title: language.t("toast.file.loadFailed.title"), + }) + }, + }} + /> +
+ ) + + const content = () => ( +
+ + + {renderFile(contents())} + +
{language.t("common.loading")}...
+
+ {(err) =>
{err()}
}
+
+
+
+ ) + + return content() +} diff --git a/packages/app/src/pages/session/review-tab.tsx b/packages/app/src/pages/session/review-tab.tsx index 586942399d..1b65af7121 100644 --- a/packages/app/src/pages/session/review-tab.tsx +++ b/packages/app/src/pages/session/review-tab.tsx @@ -1,6 +1,7 @@ import { createEffect, onCleanup, type JSX } from "solid-js" import { makeEventListener } from "@solid-primitives/event-listener" -import type { FileDiffInfo, VcsFileDiff } from "@opencode-ai/sdk/v2" +import type { SnapshotFileDiff, VcsFileDiff } from "@opencode-ai/sdk/v2" +import type { FileDiffInfo } from "@opencode-ai/client/promise" import { SessionReview } from "@opencode-ai/session-ui/session-review" import type { SessionReviewCommentActions, @@ -14,7 +15,7 @@ import type { LineComment } from "@/context/comments" export type DiffStyle = "unified" | "split" -type ReviewDiff = FileDiffInfo | VcsFileDiff +type ReviewDiff = FileDiffInfo | SnapshotFileDiff | VcsFileDiff export interface SessionReviewTabProps { title?: JSX.Element diff --git a/packages/app/src/pages/session/session-side-panel.tsx b/packages/app/src/pages/session/session-side-panel.tsx index b85e7ab13d..6f074fa203 100644 --- a/packages/app/src/pages/session/session-side-panel.tsx +++ b/packages/app/src/pages/session/session-side-panel.tsx @@ -1,28 +1,47 @@ import { For, Match, Show, Switch, createEffect, createMemo, onCleanup, type JSX } from "solid-js" import { createStore } from "solid-js/store" import { createMediaQuery } from "@solid-primitives/media" +import { DragDropProvider as DndKitProvider, PointerSensor } from "@dnd-kit/solid" +import { isSortable } from "@dnd-kit/solid/sortable" +import { Accessibility, AutoScroller, Feedback, PointerActivationConstraints } from "@dnd-kit/dom" +import { RestrictToHorizontalAxis } from "@dnd-kit/abstract/modifiers" +import { RestrictToElement } from "@dnd-kit/dom/modifiers" +import { + DragDropProvider, + DragDropSensors, + DragOverlay, + SortableProvider, + closestCenter, + type DragEvent, +} from "@thisbeyond/solid-dnd" import { Tabs } from "@opencode-ai/ui/tabs" import { IconButton } from "@opencode-ai/ui/icon-button" import { Icon } from "@opencode-ai/ui/icon" import { TooltipKeybind } from "@opencode-ai/ui/tooltip" import { ResizeHandle } from "@opencode-ai/ui/resize-handle" import { Mark } from "@opencode-ai/ui/logo" -import { DragDropProvider, DragDropSensors, DragOverlay, SortableProvider, closestCenter } from "@thisbeyond/solid-dnd" -import type { DragEvent } from "@thisbeyond/solid-dnd" -import type { FileDiffInfo, VcsFileDiff } from "@opencode-ai/sdk/v2" +import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2" +import { KeybindV2 } from "@opencode-ai/ui/v2/keybind-v2" +import { TooltipV2 } from "@opencode-ai/ui/v2/tooltip-v2" +import type { SnapshotFileDiff, VcsFileDiff } from "@opencode-ai/sdk/v2" +import type { FileDiffInfo } from "@opencode-ai/client/promise" import { ConstrainDragYAxis, getDraggableId } from "@/utils/solid-dnd" import { useDialog } from "@opencode-ai/ui/context/dialog" import FileTree from "@/components/file-tree" +import { normalizeFileTreeV2Path } from "@/components/file-tree-v2-model" import { SessionContextUsage } from "@/components/session-context-usage" const reviewTabID = "session-side-panel-review-tab" const reviewTabPanelID = "session-side-panel-review-tabpanel" -import { SessionContextTab, SortableTab, FileVisual } from "@/components/session" +const fileBrowserTabPanelID = "session-side-panel-file-browser-tabpanel" +import { SessionContextTab, SortableTab, SortableTabV2, FileVisual } from "@/components/session" +import { OpenInAppV2 } from "@/components/session/open-in-app-v2" import { useCommand } from "@/context/command" import { useFile, type SelectedLineRange } from "@/context/file" import { useLanguage } from "@/context/language" import { useLayout } from "@/context/layout" +import { useSDK } from "@/context/sdk" import { useSettings } from "@/context/settings" import { createFileTabListSync } from "@/pages/session/file-tab-scroll" import { FileTabContent } from "@/pages/session/file-tabs" @@ -38,15 +57,24 @@ import { setSessionHandoff } from "@/pages/session/handoff" import { useSessionLayout } from "@/pages/session/session-layout" import { SessionFileBrowserTab, type SessionFileBrowserState } from "@/pages/session/v2/session-file-browser-tab" +type ReviewDiff = FileDiffInfo | SnapshotFileDiff | VcsFileDiff +type RenderDiff = FileDiffInfo | (SnapshotFileDiff & { file: string }) | VcsFileDiff +const FILE_TREE_WIDTH_MIN = 240 + +function renderDiff(value: ReviewDiff): value is RenderDiff { + return typeof value.file === "string" +} + export function SessionSidePanel(props: { canReview: () => boolean - diffs: () => (FileDiffInfo | VcsFileDiff)[] + diffs: () => ReviewDiff[] diffsReady: () => boolean empty: () => string hasReview: () => boolean reviewHasFocusableContent: () => boolean reviewCount: () => number reviewPanel: () => JSX.Element + reviewSidebarToggle?: (disabled: boolean) => JSX.Element fileBrowserState?: SessionFileBrowserState activeDiff?: string focusReviewDiff: (path: string) => void @@ -60,7 +88,9 @@ export function SessionSidePanel(props: { const language = useLanguage() const command = useCommand() const dialog = useDialog() + const sdk = useSDK() const { sessionKey, tabs, view, params } = useSessionLayout() + const projectDirectory = createMemo(() => sdk().directory) const isDesktop = createMediaQuery("(min-width: 768px)") const shown = settings.visibility.fileTree @@ -75,15 +105,16 @@ export function SessionSidePanel(props: { }), ) const open = createMemo(() => reviewOpen() || fileOpen()) + const fileTreeWidth = createMemo(() => Math.max(FILE_TREE_WIDTH_MIN, layout.fileTree.width())) const reviewTab = createMemo(() => isDesktop()) const panelWidth = createMemo(() => { if (!open()) return "0px" if (reviewOpen()) return "auto" - return `${layout.fileTree.width()}px` + return `${fileTreeWidth()}px` }) - const treeWidth = createMemo(() => (fileOpen() ? `${layout.fileTree.width()}px` : "0px")) + const treeWidth = createMemo(() => (fileOpen() ? `${fileTreeWidth()}px` : "0px")) - const diffs = createMemo(() => props.diffs()) + const diffs = createMemo(() => props.diffs().filter(renderDiff)) const diffFiles = createMemo(() => diffs().map((d) => d.file)) const kinds = createMemo(() => { const merge = (a: "add" | "del" | "mix" | undefined, b: "add" | "del" | "mix") => { @@ -92,11 +123,9 @@ export function SessionSidePanel(props: { return "mix" as const } - const normalize = (p: string) => p.replaceAll("\\\\", "/").replace(/\/+$/, "") - const out = new Map() for (const diff of diffs()) { - const file = normalize(diff.file) + const file = normalizeFileTreeV2Path(diff.file) const kind = diff.status === "added" ? "add" : diff.status === "deleted" ? "del" : "mix" out.set(file, kind) @@ -153,6 +182,7 @@ export function SessionSidePanel(props: { fileBrowser: () => !!props.fileBrowserState, }) const contextOpen = tabState.contextOpen + const openFileOpen = tabState.openFileOpen const panelTabs = tabState.panelTabs const openedTabs = tabState.openedTabs const activeTab = tabState.activeTab @@ -170,10 +200,8 @@ export function SessionSidePanel(props: { layout.fileTree.setTab("all") } - const [store, setStore] = createStore({ - activeDraggable: undefined as string | undefined, - }) let fileFilter: HTMLInputElement | undefined + let tabList: HTMLDivElement | undefined const temporaryTab = tabs().preview const previewTab = (value: string) => { const next = normalizeTab(value) @@ -196,10 +224,27 @@ export function SessionSidePanel(props: { } const browserTab = createMemo(() => { if (!props.fileBrowserState) return undefined - if (activeTab() === SESSION_OPEN_FILE_TAB) return SESSION_OPEN_FILE_TAB + const active = activeTab() + if (active === SESSION_OPEN_FILE_TAB) return SESSION_OPEN_FILE_TAB + if (active && file.pathFromTab(active)) return active return activeFileTab() }) - const browserKinds = createMemo(() => new Map([...kinds()].filter(([, kind]) => kind !== "mix"))) + // Keep the file-browser shell mounted while any file tab exists. Kobalte briefly + // selects Review while the tab For replaces a preview trigger, which would + // otherwise dispose the sidebar and reset scroll. + const fileBrowserMounted = createMemo(() => { + if (!props.fileBrowserState) return false + return openedTabs().length > 0 || openFileOpen() || !!browserTab() + }) + const fileBrowserVisible = createMemo(() => { + const active = activeTab() + return active !== "review" && active !== "context" && active !== "empty" + }) + const openFileKeybind = createMemo(() => command.keybindParts("file.open")) + const closeTabKeybind = createMemo(() => command.keybindParts("tab.close")) + const [store, setStore] = createStore({ + activeDraggable: undefined as string | undefined, + }) const handleDragStart = (event: unknown) => { const id = getDraggableId(event) @@ -285,72 +330,289 @@ export function SessionSidePanel(props: { "bg-background-base": !settings.general.newLayoutDesigns(), }} > - - - - -
- { - const stop = createFileTabListSync({ el, contextOpen }) - onCleanup(stop) - }} - > - - + + + +
+ { + const stop = createFileTabListSync({ el, contextOpen }) + onCleanup(stop) + }} > -
-
{language.t("session.tab.review")}
- -
{props.reviewCount()}
-
-
- - - - + +
+
{language.t("session.tab.review")}
+ +
{props.reviewCount()}
+
+
+
+
+ + + tabs().close("context")} + aria-label={language.t("common.closeTab")} + /> + + } + hideCloseButton + onMiddleClick={() => tabs().close("context")} + > +
+ +
{language.t("session.tab.context")}
+
+
+
+ + + {(tab) => ( + + } + > + + tabs().close(SESSION_OPEN_FILE_TAB)} + aria-label={language.t("common.closeTab")} + /> + + } + hideCloseButton + onMiddleClick={() => tabs().close(SESSION_OPEN_FILE_TAB)} + > +
+ + {language.t("command.file.open")} +
+
+
+ )} +
+
+
tabs().close("context")} - aria-label={language.t("common.closeTab")} + iconSize="large" + class="!rounded-md" + onClick={() => { + void import("@/components/dialog-select-file").then((x) => { + dialog.show(() => ) + }) + }} + aria-label={language.t("command.file.open")} /> - } - hideCloseButton - onMiddleClick={() => tabs().close("context")} - > -
- -
{language.t("session.tab.context")}
- + +
+ + +
+ {props.reviewPanel()} +
- + + + +
+
+ +
+ {language.t("session.files.selectToOpen")} +
+
+
+
+
+ + + +
+ +
+
+
+ + + {(tab) => } + + + + + {(tab) => { + const path = file.pathFromTab(tab) + return ( +
+ + {(p) => } + +
+ ) + }} +
+
+ + } + > + + event.target instanceof Element && + (!!event.target.closest('[data-slot="tabs-trigger-close-button"]') || + !!event.target.closest(".session-review-v2-open-in-app-slot")), + }), + ]} + modifiers={[ + RestrictToHorizontalAxis, + RestrictToElement.configure({ element: () => tabList ?? null }), + ]} + plugins={(defaults) => [ + ...defaults.filter((plugin) => plugin !== Accessibility), + AutoScroller.configure({ acceleration: 8, threshold: { x: 0.05, y: 0 } }), + Feedback.configure({ dropAnimation: null }), + ]} + onDragEnd={(event) => { + const source = event.operation.source + if (event.canceled || !isSortable(source) || source.initialIndex === source.index) return + tabs().move(source.id.toString(), source.index) + }} + > + +
+ { + tabList = el + const stop = createFileTabListSync({ el, contextOpen }) + onCleanup(stop) + }} + > + + {(toggle) => ( +
+ {toggle()(activeTab() === SESSION_OPEN_FILE_TAB)} +
+ )} +
+ + + {props.hasReview() + ? language.t("session.review.filesChanged", { count: props.reviewCount() }) + : language.t("session.tab.review")} + + + + + {language.t("common.closeTab")} + 0}> + + + + } + placement="bottom" + gutter={10} + > + tabs().close("context")} + aria-label={language.t("common.closeTab")} + /> + + } + hideCloseButton + onMiddleClick={() => tabs().close("context")} + > +
+ +
{language.t("session.tab.context")}
+
+
+
{(tab) => ( tabs().all().indexOf(tab)} temporary={temporaryTab() === tab} onTabClose={tabs().close} onTabDoubleClick={temporaryTab() === tab ? openTab : undefined} @@ -360,9 +622,15 @@ export function SessionSidePanel(props: { + {language.t("common.closeTab")} + 0}> + + + + } placement="bottom" gutter={10} > @@ -373,7 +641,7 @@ export function SessionSidePanel(props: { onClick={() => tabs().close(SESSION_OPEN_FILE_TAB)} aria-label={language.t("common.closeTab")} /> - + } hideCloseButton onMiddleClick={() => tabs().close(SESSION_OPEN_FILE_TAB)} @@ -386,106 +654,104 @@ export function SessionSidePanel(props: { )} - -
- - { - if (props.fileBrowserState) { - openFileBrowser() - return - } - void import("@/components/dialog-select-file").then((x) => { - dialog.show(() => ) - }) - }} - aria-label={language.t("command.file.open")} - /> - + + {language.t("command.file.open")} + 0}> + + + + } + placement="bottom" + class="flex items-center" + > + } + variant="ghost-muted" + size="large" + onClick={() => openFileBrowser()} + aria-label={language.t("command.file.open")} + /> + +
+
+
event.stopPropagation()} + onClick={(event) => event.stopPropagation()} + > +
- -
- - -
- {props.reviewPanel()}
-
- - -
-
- -
- {language.t("session.files.selectToOpen")} + +
+ {props.reviewPanel()} +
+
+ + + +
+
+ +
+ {language.t("session.files.selectToOpen")} +
-
- - + + - - -
- -
-
-
- - - previewTab(file.tab(path))} - onSelectPermanent={(path) => openTab(file.tab(path))} - filterRef={(element) => (fileFilter = element)} - /> - - - - {(tab) => } - - - - - {(tab) => { - const path = file.pathFromTab(tab) - return ( -
- - {(p) => } - + + +
+
- ) - }} -
- - + + + + +
+ previewTab(file.tab(path))} + onSelectPermanent={(path) => openTab(file.tab(path))} + filterRef={(element) => (fileFilter = element)} + /> +
+
+ + +
@@ -513,10 +779,19 @@ export function SessionSidePanel(props: { > - {props.reviewCount()}{" "} - {language.t( - props.reviewCount() === 1 ? "session.review.change.one" : "session.review.change.other", - )} + + {props.reviewCount()}{" "} + {language.t( + props.reviewCount() === 1 ? "session.review.change.one" : "session.review.change.other", + )} + + } + > + {language.t("session.review.filesChanged", { count: props.reviewCount() })} + {language.t("session.files.all")} @@ -572,8 +847,8 @@ export function SessionSidePanel(props: { { props.size.touch() diff --git a/packages/app/src/pages/session/terminal-panel-v2.tsx b/packages/app/src/pages/session/terminal-panel-v2.tsx index b74d0fe227..6a92e24ea6 100644 --- a/packages/app/src/pages/session/terminal-panel-v2.tsx +++ b/packages/app/src/pages/session/terminal-panel-v2.tsx @@ -28,7 +28,6 @@ import { getTerminalHandoff, setTerminalHandoff } from "@/pages/session/handoff" import { useSessionLayout } from "@/pages/session/session-layout" export function TerminalPanelV2(props: { stacked?: boolean } = {}) { - const delays = [120, 240] const layout = useLayout() const terminal = useTerminal() const sdk = useSDK() @@ -46,6 +45,8 @@ export function TerminalPanelV2(props: { stacked?: boolean } = {}) { let root: HTMLDivElement | undefined let tabList: HTMLDivElement | undefined + onCleanup(() => terminal.cancelFocus()) + const [store, setStore] = createStore({ autoCreated: false, recovered: {} as Record, @@ -94,36 +95,12 @@ export function TerminalPanelV2(props: { stacked?: boolean } = {}) { ), ) - const focus = (id: string) => { - focusTerminalById(id) - - const frame = requestAnimationFrame(() => { - if (!opened()) return - if (terminal.active() !== id) return - focusTerminalById(id) - }) - - const timers = delays.map((ms) => - window.setTimeout(() => { - if (!opened()) return - if (terminal.active() !== id) return - focusTerminalById(id) - }, ms), - ) - - return () => { - cancelAnimationFrame(frame) - for (const timer of timers) clearTimeout(timer) - } - } - createEffect( on( - () => [opened(), terminal.active()] as const, - ([next, id]) => { - if (!next || !id) return - const stop = focus(id) - onCleanup(stop) + () => [opened(), terminal.active(), terminal.focusRequested(terminal.active())] as const, + ([next, id, requested]) => { + if (!next || !id || !requested) return + focusTerminalById(id) }, ), ) @@ -285,7 +262,15 @@ export function TerminalPanelV2(props: { stacked?: boolean } = {}) { onChange={(id) => terminal.open(id)} class={newLayout() ? "!h-[52px] !flex-none" : "!h-auto !flex-none"} > - + { + const active = document.activeElement + if (event.target === active) return + if (active instanceof HTMLInputElement && event.currentTarget.contains(active)) active.blur() + }} + > {(pty, index) => ( @@ -304,7 +289,7 @@ export function TerminalPanelV2(props: { stacked?: boolean } = {}) { icon="plus-small" variant="ghost" iconSize="large" - onClick={terminal.new} + onClick={() => terminal.new({ focus: true })} aria-label={language.t("command.terminal.new")} /> @@ -326,7 +311,7 @@ export function TerminalPanelV2(props: { stacked?: boolean } = {}) { icon="plus-small" variant="ghost" iconSize="large" - onClick={terminal.new} + onClick={() => terminal.new({ focus: true })} aria-label={language.t("command.terminal.new")} /> @@ -344,7 +329,8 @@ export function TerminalPanelV2(props: { stacked?: boolean } = {}) {
terminal.consumeFocus(id)} class="!px-[14px]" onConnect={() => markTerminalConnected(terminalRecoveryKey(pty()), id, ops.trim)} onCleanup={ops.update} diff --git a/packages/app/src/pages/session/terminal-panel.tsx b/packages/app/src/pages/session/terminal-panel.tsx index 54195e2ebc..2596eaee83 100644 --- a/packages/app/src/pages/session/terminal-panel.tsx +++ b/packages/app/src/pages/session/terminal-panel.tsx @@ -38,6 +38,8 @@ export function TerminalPanel() { const close = () => view().terminal.close() let root: HTMLDivElement | undefined + onCleanup(() => terminal.cancelFocus()) + const [store, setStore] = createStore({ autoCreated: false, activeDraggable: undefined as string | undefined, @@ -285,7 +287,7 @@ export function TerminalPanel() { icon="plus-small" variant="ghost" iconSize="large" - onClick={terminal.new} + onClick={() => terminal.new()} aria-label={language.t("command.terminal.new")} /> @@ -303,6 +305,7 @@ export function TerminalPanel() { terminal.consumeFocus(id)} onConnect={() => markTerminalConnected(terminalRecoveryKey(pty()), id, ops.trim)} onCleanup={ops.update} onConnectError={() => recoverTerminal(terminalRecoveryKey(pty()), id, ops.clone)} diff --git a/packages/app/src/pages/session/timeline/message-timeline.tsx b/packages/app/src/pages/session/timeline/message-timeline.tsx index 168bdf2f69..eca1541cea 100644 --- a/packages/app/src/pages/session/timeline/message-timeline.tsx +++ b/packages/app/src/pages/session/timeline/message-timeline.tsx @@ -72,6 +72,7 @@ import { useSync } from "@/context/sync" import { notifySessionTabsRemoved } from "@/components/titlebar-session-events" import { sessionTitle } from "@/utils/session-title" import { scheduleConnectedMeasure } from "./measure" +import { observeElementOffsetReconnectAware } from "./observe-element-offset" import { createTimelineProjection } from "./projection" import { MessageComment, SummaryDiff, TimelineRow, TimelineRowMap } from "./rows" import { filterVirtualIndexes } from "./virtual-items" @@ -282,6 +283,14 @@ export function MessageTimeline(props: { return sync().data.session_status[id] ?? idle }) const sessionMessages = createMemo(() => (sessionID() ? (sync().data.message[sessionID()!] ?? []) : [])) + const projectedMessages = createMemo(() => { + const id = sessionID() + if (!id) return [] + const visible = new Set(props.userMessages.map((message) => message.id)) + const boundary = sessionMessages().find((message) => message.role === "user" && !visible.has(message.id))?.id + const messages = sync().data.session_message[id] ?? [] + return boundary ? messages.filter((message) => message.id < boundary) : messages + }) const info = createMemo(() => { const id = sessionID() if (!id) return @@ -324,9 +333,11 @@ export function MessageTimeline(props: { const projection = createTimelineProjection({ messages: sessionMessages, userMessages: () => props.userMessages, + sessionMessages: projectedMessages, parts: getMsgParts, status: sessionStatus, showReasoningSummaries: settings.general.showReasoningSummaries, + inlineComments: settings.general.newLayoutDesigns, }) const activeMessageID = projection.activeMessageID const assistantMessagesByParent = projection.assistantMessagesByParent @@ -407,6 +418,7 @@ export function MessageTimeline(props: { return timelineRows().length }, getScrollElement: () => listRoot() ?? null, + observeElementOffset: observeElementOffsetReconnectAware, initialOffset: () => (props.shouldAnchorBottom() ? Number.MAX_SAFE_INTEGER : 0), initialMeasurementsCache: initialMeasurements, estimateSize: () => timelineFallbackItemSize, @@ -634,7 +646,7 @@ export function MessageTimeline(props: { const viewShare = () => { const url = shareUrl() if (!url) return - platform.openLink(url) + platform.openExternal(url) } const errorMessage = (err: unknown) => { @@ -662,7 +674,7 @@ export function MessageTimeline(props: { const titleMutation = useMutation(() => ({ mutationFn: (input: { id: string; title: string }) => - sdk().client.session.update({ sessionID: input.id, title: input.title }), + sdk().api.session.rename({ sessionID: input.id, title: input.title }), onSuccess: (_, input) => { sync().set( produce((draft) => { @@ -800,13 +812,14 @@ export function MessageTimeline(props: { const archiveSession = async (sessionID: string) => { const session = sync().session.get(sessionID) if (!session) return + if ((await sdk().protocol) !== "v1") return const sessions = sync().data.session ?? [] const index = sessions.findIndex((s) => s.id === sessionID) const nextSession = index === -1 ? undefined : (sessions[index + 1] ?? sessions[index - 1]) await sdk() - .client.session.update({ sessionID, time: { archived: Date.now() } }) + .client.session.update({ sessionID, directory: sdk().directory, time: { archived: Date.now() } }) .then(() => { sync().set( produce((draft) => { @@ -835,8 +848,8 @@ export function MessageTimeline(props: { const nextSession = index === -1 ? undefined : (sessions[index + 1] ?? sessions[index - 1]) const result = await sdk() - .client.session.delete({ sessionID }) - .then((x) => x.data) + .api.session.remove({ sessionID }) + .then(() => true) .catch((err) => { showToast({ title: language.t("session.delete.failed.title"), @@ -1135,6 +1148,10 @@ export function MessageTimeline(props: { const m = messageByID().get(userMessageRow().userMessageID) if (m?.role === "user") return m }) + const messageComments = createMemo(() => { + if (!settings.general.newLayoutDesigns()) return [] + return getMsgParts(userMessageRow().userMessageID).flatMap((part) => MessageComment.fromPart(part) ?? []) + }) return ( @@ -1146,6 +1163,7 @@ export function MessageTimeline(props: { parts={getMsgParts(userMessageRow().userMessageID)} actions={props.actions} useV2Actions={settings.general.newLayoutDesigns()} + comments={messageComments()} />
@@ -1249,7 +1267,7 @@ export function MessageTimeline(props: { const part = getMsgPart(value.group.ref.messageID, value.group.ref.partID) if (part?.type === "tool") return part } - const asyncFile = () => ["edit", "write", "patch", "apply_patch"].includes(tool()?.tool ?? "") + const asyncFile = () => ["edit", "write", "apply_patch"].includes(tool()?.tool ?? "") const [ready, setReady] = createSignal(initialItem.size <= timelineFallbackItemSize || !asyncFile()) let contentMeasureFrame: number | undefined diff --git a/packages/app/src/pages/session/timeline/observe-element-offset.test.ts b/packages/app/src/pages/session/timeline/observe-element-offset.test.ts new file mode 100644 index 0000000000..d656a31415 --- /dev/null +++ b/packages/app/src/pages/session/timeline/observe-element-offset.test.ts @@ -0,0 +1,199 @@ +import { expect, test } from "bun:test" +import { type Virtualizer } from "@tanstack/solid-virtual" +import { mutationNodesContainElement, observeElementOffsetReconnectAware } from "./observe-element-offset" + +test("matches only the scroll element or an ancestor containing it", () => { + const route = document.createElement("section") + const viewport = document.createElement("div") + const child = document.createElement("div") + const sibling = document.createElement("div") + route.append(viewport) + viewport.append(child) + + expect(mutationNodesContainElement([viewport], viewport)).toBe(true) + expect(mutationNodesContainElement([route], viewport)).toBe(true) + expect(mutationNodesContainElement([child, sibling], viewport)).toBe(false) +}) + +test("reports a divergent native offset once and ignores equal offsets and unrelated mutations", async () => { + const route = document.createElement("section") + const viewport = document.createElement("div") + const unrelated = document.createElement("div") + route.append(viewport) + document.body.append(route) + const instance = { + scrollElement: viewport, + targetWindow: window, + scrollOffset: 79_400, + options: { + horizontal: false, + isRtl: false, + isScrollingResetDelay: 0, + useScrollendEvent: false, + }, + } as unknown as Virtualizer + const calls: [number, boolean][] = [] + const cleanup = observeElementOffsetReconnectAware(instance, (offset, isScrolling) => { + calls.push([offset, isScrolling]) + instance.scrollOffset = offset + }) + + document.body.append(unrelated) + unrelated.remove() + await frames(2) + expect(calls).toEqual([]) + + route.remove() + document.body.append(route) + await new Promise((resolve) => setTimeout(resolve, 0)) + await frames(3) + expect(calls).toEqual([[0, false]]) + + route.remove() + document.body.append(route) + await new Promise((resolve) => setTimeout(resolve, 0)) + await frames(3) + expect(calls).toEqual([[0, false]]) + + cleanup?.() + route.remove() +}) + +test("keeps checking until stale reset-delay callbacks can no longer win", async () => { + const route = document.createElement("section") + const viewport = document.createElement("div") + route.append(viewport) + document.body.append(route) + const instance = { + scrollElement: viewport, + targetWindow: window, + scrollOffset: 79_400, + options: { + horizontal: false, + isRtl: false, + isScrollingResetDelay: 20, + useScrollendEvent: false, + }, + } as unknown as Virtualizer + const calls: number[] = [] + const cleanup = observeElementOffsetReconnectAware(instance, (offset) => { + calls.push(offset) + instance.scrollOffset = offset + }) + + route.remove() + document.body.append(route) + await new Promise((resolve) => setTimeout(resolve, 0)) + await frames(1) + expect(instance.scrollOffset).toBe(0) + + instance.scrollOffset = 79_400 + await new Promise((resolve) => setTimeout(resolve, 25)) + await frames(3) + + expect(instance.scrollOffset).toBe(0) + expect(calls).toEqual([0, 0]) + cleanup?.() + route.remove() +}) + +test.each([ + { name: "LTR", isRtl: false, expected: 240 }, + { name: "RTL", isRtl: true, expected: -240 }, +])("reports the TanStack horizontal $name offset after reconnect", async ({ isRtl, expected }) => { + const route = document.createElement("section") + const viewport = document.createElement("div") + route.append(viewport) + document.body.append(route) + viewport.scrollLeft = 240 + const instance = { + scrollElement: viewport, + targetWindow: window, + scrollOffset: 0, + options: { + horizontal: true, + isRtl, + isScrollingResetDelay: 0, + useScrollendEvent: false, + }, + } as unknown as Virtualizer + const calls: [number, boolean][] = [] + const cleanup = observeElementOffsetReconnectAware(instance, (offset, isScrolling) => { + calls.push([offset, isScrolling]) + instance.scrollOffset = offset + }) + + route.remove() + document.body.append(route) + await new Promise((resolve) => setTimeout(resolve, 0)) + await frames(3) + + expect(calls).toEqual([[expected, false]]) + cleanup?.() + route.remove() +}) + +test("cleanup suppresses an already queued delegated offset callback", async () => { + const viewport = document.createElement("div") + document.body.append(viewport) + viewport.scrollTop = 100 + const instance = { + scrollElement: viewport, + targetWindow: window, + scrollOffset: 0, + options: { + horizontal: false, + isRtl: false, + isScrollingResetDelay: 10, + useScrollendEvent: false, + }, + } as unknown as Virtualizer + const calls: [number, boolean][] = [] + const cleanup = observeElementOffsetReconnectAware(instance, (offset, isScrolling) => + calls.push([offset, isScrolling]), + ) + + viewport.dispatchEvent(new Event("scroll")) + cleanup?.() + await new Promise((resolve) => setTimeout(resolve, 25)) + + expect(calls).toEqual([[100, true]]) + viewport.remove() +}) + +test("cleanup cancels reconnect checks and delegated offset observation", async () => { + const route = document.createElement("section") + const viewport = document.createElement("div") + route.append(viewport) + document.body.append(route) + const instance = { + scrollElement: viewport, + targetWindow: window, + scrollOffset: 0, + options: { + horizontal: false, + isRtl: false, + isScrollingResetDelay: 50, + useScrollendEvent: false, + }, + } as unknown as Virtualizer + const calls: number[] = [] + const cleanup = observeElementOffsetReconnectAware(instance, (offset) => calls.push(offset)) + + route.remove() + document.body.append(route) + await new Promise((resolve) => setTimeout(resolve, 0)) + cleanup?.() + instance.scrollOffset = 100 + viewport.dispatchEvent(new Event("scroll")) + await frames(4) + + expect(calls).toEqual([]) + route.remove() +}) + +async function frames(count: number) { + for (let index = 0; index < count; index++) { + await new Promise((resolve) => requestAnimationFrame(() => resolve())) + } +} diff --git a/packages/app/src/pages/session/timeline/observe-element-offset.ts b/packages/app/src/pages/session/timeline/observe-element-offset.ts new file mode 100644 index 0000000000..e815423a66 --- /dev/null +++ b/packages/app/src/pages/session/timeline/observe-element-offset.ts @@ -0,0 +1,73 @@ +import { observeElementOffset, type Virtualizer } from "@tanstack/solid-virtual" + +export function observeElementOffsetReconnectAware( + instance: Virtualizer, + callback: (offset: number, isScrolling: boolean) => void, +) { + let active = true + const deliver = (offset: number, isScrolling: boolean) => { + if (!active) return + callback(offset, isScrolling) + } + const cleanupOffset = observeElementOffset(instance, deliver) + const element = instance.scrollElement + const targetWindow = instance.targetWindow + const root = element?.closest("main") ?? element?.ownerDocument.body + if (!element || !targetWindow || !root) + return () => { + active = false + cleanupOffset?.() + } + + let removed = false + let frame: number | undefined + const clearCheck = () => { + if (frame === undefined) return + targetWindow.cancelAnimationFrame(frame) + frame = undefined + } + const startCheck = () => { + clearCheck() + const deadline = targetWindow.performance.now() + instance.options.isScrollingResetDelay + let framesAfterDeadline = 0 + const check = (time: number) => { + frame = undefined + if (element.isConnected) { + const offset = instance.options.horizontal + ? element.scrollLeft * (instance.options.isRtl ? -1 : 1) + : element.scrollTop + if (instance.scrollOffset === null || Math.abs(offset - instance.scrollOffset) > 1) deliver(offset, false) + } + if (time >= deadline) framesAfterDeadline += 1 + if (framesAfterDeadline >= 2) return + frame = targetWindow.requestAnimationFrame(check) + } + frame = targetWindow.requestAnimationFrame(check) + } + const observer = new targetWindow.MutationObserver((records) => { + if (!active) return + records.forEach((record) => { + if (record.target === element || element.contains(record.target)) return + if (mutationNodesContainElement(record.removedNodes, element)) { + removed = true + clearCheck() + } + if (!removed || !element.isConnected || !mutationNodesContainElement(record.addedNodes, element)) return + removed = false + startCheck() + }) + }) + // Session routes are replaced below persistent main; body is the fallback for isolated hosts. + observer.observe(root, { childList: true, subtree: true }) + + return () => { + active = false + observer.disconnect() + clearCheck() + cleanupOffset?.() + } +} + +export function mutationNodesContainElement(nodes: Iterable, element: Element) { + return [...nodes].some((node) => node === element || node.contains(element)) +} diff --git a/packages/app/src/pages/session/timeline/projection.ts b/packages/app/src/pages/session/timeline/projection.ts index ec2a3190ba..e30c936d73 100644 --- a/packages/app/src/pages/session/timeline/projection.ts +++ b/packages/app/src/pages/session/timeline/projection.ts @@ -1,19 +1,19 @@ -import { Binary } from "@opencode-ai/core/util/binary" +import type { SessionMessageInfo } from "@opencode-ai/client/promise" import type { AssistantMessage, Message, Part, SessionStatus, UserMessage } from "@opencode-ai/sdk/v2" -import { createMemo, mapArray, type Accessor } from "solid-js" +import { createMemo, type Accessor } from "solid-js" import { reuseTimelineRows } from "./row-reconciliation" import { Timeline, TimelineRow } from "./rows" export { reuseTimelineRows } from "./row-reconciliation" -const emptyAssistantMessages: AssistantMessage[] = [] - export function createTimelineProjection(input: { messages: Accessor userMessages: Accessor + sessionMessages: Accessor parts: (messageID: string) => Part[] status: Accessor showReasoningSummaries: Accessor + inlineComments: Accessor }) { const messageByID = createMemo(() => new Map(input.messages().map((message) => [message.id, message] as const))) const assistantMessagesByParent = createMemo(() => { @@ -29,46 +29,20 @@ export function createTimelineProjection(input: { }) return result }) - const activeMessageID = createMemo(() => { - const parentID = input - .messages() - .findLast( - (message): message is AssistantMessage => - message.role === "assistant" && typeof message.time.completed !== "number", - )?.parentID - if (parentID) { - const messages = input.messages() - const result = Binary.search(messages, parentID, (message) => message.id) - const message = result.found ? messages[result.index] : messages.find((item) => item.id === parentID) - if (message?.role === "user") return message.id - } - - if (input.status().type === "idle") return - return input.messages().findLast((message) => message.role === "user")?.id - }) - const messageRowMemos = createMemo( - mapArray(input.userMessages, (userMessage, indexAccessor) => - createMemo((previous: TimelineRow.TimelineRow[] | undefined) => - reuseTimelineRows( - previous, - Timeline.constructMessageRows( - userMessage, - input.parts, - assistantMessagesByParent().get(userMessage.id) ?? emptyAssistantMessages, - indexAccessor(), - input.showReasoningSummaries(), - input.status().type, - activeMessageID() === userMessage.id, - ), - ), - ), + const projection = createMemo(() => + Timeline.constructSessionMessageRows( + input.sessionMessages(), + (messageID) => messageByID().get(messageID) as UserMessage | AssistantMessage | undefined, + input.parts, + input.showReasoningSummaries(), + input.status().type, + input.inlineComments(), + input.userMessages(), ), ) + const activeMessageID = createMemo(() => projection().activeMessageID) const rows = createMemo((previous: TimelineRow.TimelineRow[] | undefined) => - reuseTimelineRows( - previous, - messageRowMemos().flatMap((memo) => memo()), - ), + reuseTimelineRows(previous, projection().rows), ) const rowByKey = createMemo(() => new Map(rows().map((row) => [TimelineRow.key(row), row] as const))) const messageRowIndex = createMemo(() => { diff --git a/packages/app/src/pages/session/timeline/rows-current.test.ts b/packages/app/src/pages/session/timeline/rows-current.test.ts new file mode 100644 index 0000000000..a5952a5e66 --- /dev/null +++ b/packages/app/src/pages/session/timeline/rows-current.test.ts @@ -0,0 +1,207 @@ +import { describe, expect, mock, test } from "bun:test" +import type { SessionMessageInfo } from "@opencode-ai/client/promise" +import { normalizeSessionMessages } from "@/utils/session-message" + +mock.module("@opencode-ai/session-ui/message-part", () => ({ + renderable: () => true, + groupParts: (refs: Array<{ messageID: string; part: { id: string } }>) => + refs.map((ref) => ({ + type: "part" as const, + key: ref.part.id, + ref: { messageID: ref.messageID, partID: ref.part.id }, + })), +})) + +const { Timeline, TimelineRow } = await import("./rows") + +describe("current session timeline rows", () => { + test("derives turns and tagged rows from chronological current messages", () => { + const source = [ + { id: "msg_1", type: "user", text: "first", time: { created: 1 } }, + { + id: "msg_2", + type: "assistant", + agent: "build", + model: { id: "model", providerID: "provider" }, + content: [{ type: "text", text: "answer" }], + time: { created: 2, completed: 3 }, + }, + { id: "msg_3", type: "user", text: "second", time: { created: 4 } }, + { + id: "msg_4", + type: "assistant", + agent: "build", + model: { id: "model", providerID: "provider" }, + content: [{ type: "reasoning", text: "working" }], + time: { created: 5 }, + }, + ] satisfies SessionMessageInfo[] + const normalized = normalizeSessionMessages("ses_1", source) + const messages = new Map(normalized.messages.map((message) => [message.id, message])) + + const result = Timeline.constructSessionMessageRows( + source, + (messageID) => messages.get(messageID), + (messageID) => normalized.parts.get(messageID) ?? [], + true, + "busy", + true, + normalized.messages.filter((message) => message.role === "user"), + ) + + expect(result.activeMessageID).toBe("msg_3") + expect(result.rows.map(TimelineRow.key)).toEqual([ + "user-message:msg_1", + "assistant-part:msg_1:msg_2:text:0", + "turn-gap:msg_3", + "user-message:msg_3", + "assistant-part:msg_3:msg_4:reasoning:0", + ]) + }) + + test("renders a current shell message as a standalone turn", () => { + const source = [ + { + id: "msg_shell", + type: "shell", + shellID: "shell_1", + command: "pwd", + status: "exited", + exit: 0, + output: { output: "/repo", cursor: 5, size: 5, truncated: false }, + time: { created: 1, completed: 2 }, + }, + ] satisfies SessionMessageInfo[] + const normalized = normalizeSessionMessages("ses_1", source) + const messages = new Map(normalized.messages.map((message) => [message.id, message])) + + const result = Timeline.constructSessionMessageRows( + source, + (messageID) => messages.get(messageID), + (messageID) => normalized.parts.get(messageID) ?? [], + true, + "idle", + true, + normalized.messages.filter((message) => message.role === "user"), + ) + + expect(result.activeMessageID).toBe("msg_shell") + expect(result.rows.map(TimelineRow.key)).toEqual([ + "user-message:msg_shell", + "assistant-part:msg_shell:msg_shell:tool", + ]) + }) + + test("keeps a projected parent missing from the source page before newer turns", () => { + const source = [ + { id: "msg_user_1", type: "user", text: "first question", time: { created: 1 } }, + { + id: "msg_assistant_1", + type: "assistant", + agent: "build", + model: { id: "model", providerID: "provider" }, + content: [{ type: "text", text: "first answer" }], + time: { created: 2, completed: 3 }, + }, + { id: "msg_user_2", type: "user", text: "second question", time: { created: 4 } }, + { + id: "msg_assistant_2", + type: "assistant", + agent: "build", + model: { id: "model", providerID: "provider" }, + content: [{ type: "text", text: "second answer" }], + time: { created: 5, completed: 6 }, + }, + ] satisfies SessionMessageInfo[] + const normalized = normalizeSessionMessages("ses_1", source) + const messages = new Map(normalized.messages.map((message) => [message.id, message])) + + const result = Timeline.constructSessionMessageRows( + source.slice(1), + (messageID) => messages.get(messageID), + (messageID) => normalized.parts.get(messageID) ?? [], + true, + "idle", + true, + normalized.messages.filter((message) => message.role === "user"), + ) + + expect(result.rows.map(TimelineRow.key)).toEqual([ + "user-message:msg_user_1", + "assistant-part:msg_user_1:msg_assistant_1:text:0", + "turn-gap:msg_user_2", + "user-message:msg_user_2", + "assistant-part:msg_user_2:msg_assistant_2:text:0", + ]) + }) + + test("renders an optimistic user turn and thinking before the protocol message arrives", () => { + const source = [ + { id: "msg_1", type: "user", text: "existing", time: { created: 1 } }, + ] satisfies SessionMessageInfo[] + const normalized = normalizeSessionMessages("ses_1", source) + const optimistic = { + id: "msg_2", + sessionID: "ses_1", + role: "user" as const, + time: { created: 2 }, + agent: "build", + model: { modelID: "model", providerID: "provider" }, + } + const result = Timeline.constructSessionMessageRows( + source, + (messageID) => + messageID === optimistic.id ? optimistic : normalized.messages.find((message) => message.id === messageID), + () => [], + true, + "busy", + true, + [...normalized.messages.filter((message) => message.role === "user"), optimistic], + ) + + expect(result.activeMessageID).toBe(optimistic.id) + expect(result.rows.map(TimelineRow.key)).toEqual([ + "user-message:msg_1", + "turn-gap:msg_2", + "user-message:msg_2", + "thinking:msg_2", + ]) + }) + + test("removes a failed assistant error when the turn continues streaming", () => { + const source = [ + { id: "msg_user", type: "user", text: "recover", time: { created: 1 } }, + { + id: "msg_failed", + type: "assistant", + agent: "build", + model: { id: "model", providerID: "provider" }, + content: [], + error: { type: "ProviderError", message: "temporary failure" }, + time: { created: 2, completed: 3 }, + }, + { + id: "msg_recovery", + type: "assistant", + agent: "build", + model: { id: "model", providerID: "provider" }, + content: [{ type: "text", text: "streaming again" }], + time: { created: 4 }, + }, + ] satisfies SessionMessageInfo[] + const normalized = normalizeSessionMessages("ses_1", source) + const messages = new Map(normalized.messages.map((message) => [message.id, message])) + + const result = Timeline.constructSessionMessageRows( + source, + (messageID) => messages.get(messageID), + (messageID) => normalized.parts.get(messageID) ?? [], + true, + "busy", + true, + normalized.messages.filter((message) => message.role === "user"), + ) + + expect(result.rows.map((row) => row._tag)).toEqual(["UserMessage", "AssistantPart"]) + }) +}) diff --git a/packages/app/src/pages/session/timeline/rows.ts b/packages/app/src/pages/session/timeline/rows.ts index 6a9cb669d0..879646e86a 100644 --- a/packages/app/src/pages/session/timeline/rows.ts +++ b/packages/app/src/pages/session/timeline/rows.ts @@ -1,7 +1,9 @@ import { parseCommentNote, readCommentMetadata } from "@/utils/comment-note" -import { AssistantMessage, Part, SessionStatus, SnapshotFileDiff, UserMessage } from "@opencode-ai/sdk/v2" +import type { SessionMessageInfo } from "@opencode-ai/client/promise" +import { AssistantMessage, Part, SessionStatus, UserMessage } from "@opencode-ai/sdk/v2" import { groupParts, renderable, type PartGroup } from "@opencode-ai/session-ui/message-part" import { TimelineRow, type SummaryDiff } from "./timeline-row" +import { uniqueSummaryDiffs } from "./summary-diffs" export { TimelineRow, type SummaryDiff } from "./timeline-row" @@ -30,6 +32,71 @@ export type TimelineRowMap = { } export namespace Timeline { + export function constructSessionMessageRows( + messages: SessionMessageInfo[], + getMessage: (messageID: string) => UserMessage | AssistantMessage | undefined, + getMessageParts: (messageID: string) => Part[], + showReasoning: boolean, + status: SessionStatus["type"], + inlineComments: boolean, + projectedUserMessages: UserMessage[], + ) { + const turns: { user: UserMessage; assistants: AssistantMessage[] }[] = [] + const turnByUserID = new Map() + messages.forEach((message) => { + const projected = getMessage(message.id) + if (message.type === "shell" && projected?.role === "user") { + const assistant = getMessage(`${message.id}:assistant`) + const turn = { user: projected, assistants: assistant?.role === "assistant" ? [assistant] : [] } + turns.push(turn) + turnByUserID.set(projected.id, turn) + return + } + if (projected?.role === "user") { + if (turnByUserID.has(projected.id)) return + const turn = { user: projected, assistants: [] } + turns.push(turn) + turnByUserID.set(projected.id, turn) + return + } + if (projected?.role !== "assistant") return + const existing = turnByUserID.get(projected.parentID) + if (existing) { + existing.assistants.push(projected) + return + } + const user = getMessage(projected.parentID) + if (user?.role !== "user") return + const turn = { user, assistants: [projected] } + turns.push(turn) + turnByUserID.set(user.id, turn) + }) + const latestUserMessageID = turns.at(-1)?.user.id + projectedUserMessages.forEach((user) => { + if (turnByUserID.has(user.id)) return + if (latestUserMessageID && user.id < latestUserMessageID) return + const turn = { user, assistants: [] } + turns.push(turn) + turnByUserID.set(user.id, turn) + }) + const activeMessageID = turns.at(-1)?.user.id + return { + activeMessageID, + rows: turns.flatMap((turn, index) => + constructMessageRows( + turn.user, + getMessageParts, + turn.assistants, + index, + showReasoning, + status, + turn.user.id === activeMessageID, + inlineComments, + ), + ), + } + } + export function constructMessageRows( userMessage: UserMessage, getMessageParts: (messageID: string) => Part[], @@ -38,6 +105,8 @@ export namespace Timeline { showReasoning: boolean, status: SessionStatus["type"], isActive: boolean, + // v2 renders comments inside the user message attachments row instead of a strip row + inlineComments: boolean, ) { const rows: TimelineRow.TimelineRow[] = [] @@ -47,7 +116,8 @@ export namespace Timeline { const compaction = userParts.some((p) => p.type === "compaction") const interruptedMessageIndex = assistantMessages.findIndex((m) => m.error?.name === "MessageAbortedError") const interrupted = interruptedMessageIndex !== -1 - const error = assistantMessages.find((m) => m.error && m.error.name !== "MessageAbortedError")?.error + const latestError = assistantMessages.at(-1)?.error + const error = latestError?.name === "MessageAbortedError" ? undefined : latestError const assistantPartRefs = assistantMessages.flatMap((message, messageIndex) => getMessageParts(message.id) @@ -74,7 +144,7 @@ export namespace Timeline { : groupParts(assistantPartRefs).map((group) => ({ type: "part" as const, group })) if (previousUserMessage) rows.push(new TimelineRow.TurnGap({ userMessageID: userMessage.id })) - if (comments.length > 0) + if (comments.length > 0 && !inlineComments) rows.push( new TimelineRow.CommentStrip({ userMessageID: userMessage.id, @@ -84,7 +154,7 @@ export namespace Timeline { rows.push( new TimelineRow.UserMessage({ userMessageID: userMessage.id, - anchor: comments.length === 0, + anchor: inlineComments || comments.length === 0, }), ) @@ -135,14 +205,7 @@ export namespace Timeline { if (isActive && status === "retry") rows.push(new TimelineRow.Retry({ userMessageID: userMessage.id })) - const diffs = (userMessage.summary?.diffs ?? []) - .reduceRight((result, diff) => { - if (!isSummaryDiff(diff)) return result - if (result.some((item) => item.file === diff.file)) return result - result.push(diff) - return result - }, []) - .reverse() + const diffs = uniqueSummaryDiffs(userMessage.summary?.diffs) if (diffs.length > 0 && (status === "idle" || !isActive)) { rows.push( new TimelineRow.DiffSummary({ @@ -167,10 +230,6 @@ export namespace Timeline { return rows } - function isSummaryDiff(value: SnapshotFileDiff): value is SummaryDiff { - return typeof value.file === "string" - } - function reasoningHeading(text: string) { const markdown = text.replace(/\r\n?/g, "\n") const html = markdown.match(/]*>([\s\S]*?)<\/h[1-6]>/i) diff --git a/packages/app/src/pages/session/timeline/summary-diffs.test.ts b/packages/app/src/pages/session/timeline/summary-diffs.test.ts new file mode 100644 index 0000000000..9b66bf6771 --- /dev/null +++ b/packages/app/src/pages/session/timeline/summary-diffs.test.ts @@ -0,0 +1,42 @@ +import { describe, expect, test } from "bun:test" +import type { SnapshotFileDiff } from "@opencode-ai/sdk/v2" +import { uniqueSummaryDiffs } from "./summary-diffs" + +const diff = (file: string, additions: number) => + ({ + file, + additions, + deletions: 0, + }) satisfies SnapshotFileDiff + +describe("uniqueSummaryDiffs", () => { + test("drops entries without files and preserves unique input", () => { + const alpha = diff("alpha.ts", 1) + const beta = diff("beta.ts", 1) + const invalid = { additions: 1, deletions: 0 } satisfies SnapshotFileDiff + + expect(uniqueSummaryDiffs(undefined)).toEqual([]) + expect(uniqueSummaryDiffs([])).toEqual([]) + expect(uniqueSummaryDiffs([invalid])).toEqual([]) + + const result = uniqueSummaryDiffs([alpha, invalid, beta]) + expect(result).toEqual([alpha, beta]) + expect(result[0]).toBe(alpha) + expect(result[1]).toBe(beta) + }) + + test("keeps the last diff per file in the legacy display order", () => { + const oldAlpha = diff("alpha.ts", 1) + const oldBeta = diff("beta.ts", 1) + const newAlpha = diff("alpha.ts", 2) + const charlie = diff("charlie.ts", 1) + const newBeta = diff("beta.ts", 2) + + const result = uniqueSummaryDiffs([oldAlpha, oldBeta, newAlpha, charlie, newBeta]) + + expect(result).toEqual([newAlpha, charlie, newBeta]) + expect(result[0]).toBe(newAlpha) + expect(result[1]).toBe(charlie) + expect(result[2]).toBe(newBeta) + }) +}) diff --git a/packages/app/src/pages/session/timeline/summary-diffs.ts b/packages/app/src/pages/session/timeline/summary-diffs.ts new file mode 100644 index 0000000000..2df37af5a6 --- /dev/null +++ b/packages/app/src/pages/session/timeline/summary-diffs.ts @@ -0,0 +1,20 @@ +import type { SnapshotFileDiff } from "@opencode-ai/sdk/v2" +import type { SummaryDiff } from "./timeline-row" + +export function uniqueSummaryDiffs(diffs: SnapshotFileDiff[] | undefined) { + const files = new Set() + return (diffs ?? []) + .reduceRight((result, diff) => { + if (!isSummaryDiff(diff)) return result + const file = diff.file + if (files.has(file)) return result + files.add(file) + result.push(diff) + return result + }, []) + .reverse() +} + +function isSummaryDiff(diff: SnapshotFileDiff): diff is SummaryDiff { + return typeof diff.file === "string" +} diff --git a/packages/app/src/pages/session/use-composer-commands.tsx b/packages/app/src/pages/session/use-composer-commands.tsx index 9b75c7bf4c..e7e51489ee 100644 --- a/packages/app/src/pages/session/use-composer-commands.tsx +++ b/packages/app/src/pages/session/use-composer-commands.tsx @@ -1,7 +1,6 @@ import { useCommand, type CommandOption } from "@/context/command" import { useLanguage } from "@/context/language" import { useLocal, type ModelSelection } from "@/context/local" -import { useSettings } from "@/context/settings" import { useDialog } from "@opencode-ai/ui/context/dialog" import { getCursorPosition, setCursorPosition } from "@/components/prompt-input/editor-dom" import { useSessionLayout } from "./session-layout" @@ -19,7 +18,6 @@ export const useComposerCommands = (input: { model?: ModelSelection } = {}) => { const dialog = useDialog() const language = useLanguage() const local = useLocal() - const settings = useSettings() const { sessionKey } = useSessionLayout() const sessionOwnership = createSessionOwnership(sessionKey) const model = input.model ?? local.model @@ -70,7 +68,7 @@ export const useComposerCommands = (input: { model?: ModelSelection } = {}) => { description: language.t("command.agent.cycle.description"), keybind: "mod+.", slash: "agent", - disabled: !settings.visibility.customAgents(), + disabled: !local.agent.visible(), onSelect: () => local.agent.move(1), }), agentCommand({ @@ -78,7 +76,7 @@ export const useComposerCommands = (input: { model?: ModelSelection } = {}) => { title: language.t("command.agent.cycle.reverse"), description: language.t("command.agent.cycle.reverse.description"), keybind: "shift+mod+.", - disabled: !settings.visibility.customAgents(), + disabled: !local.agent.visible(), onSelect: () => local.agent.move(-1), }), ]) diff --git a/packages/app/src/pages/session/use-session-commands.tsx b/packages/app/src/pages/session/use-session-commands.tsx index e23268c64b..12dd96a5e6 100644 --- a/packages/app/src/pages/session/use-session-commands.tsx +++ b/packages/app/src/pages/session/use-session-commands.tsx @@ -5,7 +5,6 @@ import { previewSelectedLines } from "@opencode-ai/session-ui/pierre/selection-b import { useFile, selectionFromLines, type FileSelection, type SelectedLineRange } from "@/context/file" import { useLanguage } from "@/context/language" import { useLayout } from "@/context/layout" -import { useLocal } from "@/context/local" import { usePermission } from "@/context/permission" import { usePrompt } from "@/context/prompt" import { useSDK } from "@/context/sdk" @@ -19,6 +18,7 @@ import { extractPromptFromParts } from "@/utils/prompt" import { UserMessage } from "@opencode-ai/sdk/v2" import { useSessionLayout } from "@/pages/session/session-layout" import { createSessionOwnership } from "./session-ownership" +import { useLocal } from "@/context/local" export type SessionCommandContext = { navigateMessageByOffset: (offset: number) => void @@ -40,7 +40,6 @@ export const useSessionCommands = (actions: SessionCommandContext) => { const dialog = useDialog() const file = useFile() const language = useLanguage() - const local = useLocal() const permission = usePermission() const prompt = usePrompt() const sdk = useSDK() @@ -48,6 +47,7 @@ export const useSessionCommands = (actions: SessionCommandContext) => { const sync = useSync() const terminal = useTerminal() const layout = useLayout() + const local = useLocal() const navigate = useNavigate() const { params, sessionKey, tabs, view } = useSessionLayout() const sessionOwnership = createSessionOwnership(sessionKey) @@ -264,7 +264,8 @@ export const useSessionCommands = (actions: SessionCommandContext) => { } const openTerminal = () => { - if (terminal.all().length > 0) terminal.new() + if (terminal.all().length > 0) terminal.new({ focus: true }) + if (terminal.all().length === 0) terminal.requestFocus() view().terminal.open() } @@ -305,7 +306,7 @@ export const useSessionCommands = (actions: SessionCommandContext) => { const sessionID = params.id if (!sessionID) return const owner = sessionOwnership.capture() - const client = sdk().client + const session = sdk().api.session const directory = sdk().directory const promptSession = prompt.capture() const revert = info()?.revert?.messageID @@ -315,13 +316,13 @@ export const useSessionCommands = (actions: SessionCommandContext) => { const parts = sync().data.part[message.id] if (sync().data.session_working(sessionID)) { - await client.session.abort({ sessionID }).catch(() => {}) + await session.interrupt({ sessionID }).catch(() => {}) } await runCommand({ owner, prompt: promptSession, - request: () => client.session.revert({ sessionID, messageID: message.id }), + request: () => session.revert.stage({ sessionID, messageID: message.id }), updatePrompt: (promptSession) => { if (parts) promptSession.set(extractPromptFromParts(parts, { directory })) }, @@ -333,7 +334,7 @@ export const useSessionCommands = (actions: SessionCommandContext) => { const sessionID = params.id if (!sessionID) return const owner = sessionOwnership.capture() - const client = sdk().client + const session = sdk().api.session const messages = userMessages() const promptSession = prompt.capture() @@ -345,7 +346,7 @@ export const useSessionCommands = (actions: SessionCommandContext) => { await runCommand({ owner, prompt: promptSession, - request: () => client.session.unrevert({ sessionID }), + request: () => session.revert.clear({ sessionID }), updatePrompt: (promptSession) => promptSession.reset(), updateViewport: () => setActiveMessage(findLast(messages, (x) => x.id >= revertMessageID)), }) @@ -355,7 +356,7 @@ export const useSessionCommands = (actions: SessionCommandContext) => { await runCommand({ owner, prompt: promptSession, - request: () => client.session.revert({ sessionID, messageID: next.id }), + request: () => session.revert.stage({ sessionID, messageID: next.id }), updatePrompt: () => undefined, updateViewport: () => setActiveMessage(findLast(messages, (x) => x.id < next.id)), }) @@ -374,10 +375,9 @@ export const useSessionCommands = (actions: SessionCommandContext) => { return } - await sdk().client.session.summarize({ + await sdk().api.session.compact({ sessionID, - modelID: model.id, - providerID: model.provider.id, + model: { providerID: model.provider.id, modelID: model.id }, }) } @@ -467,7 +467,7 @@ export const useSessionCommands = (actions: SessionCommandContext) => { id: "file.open", title: language.t("command.file.open"), description: language.t("palette.search.placeholder"), - keybind: "mod+k,mod+p", + keybind: "mod+p", slash: "open", onSelect: openFile, }), @@ -498,7 +498,15 @@ export const useSessionCommands = (actions: SessionCommandContext) => { title: language.t("command.terminal.toggle"), keybind: "ctrl+`", slash: "terminal", - onSelect: () => view().terminal.toggle(), + onSelect: () => { + if (view().terminal.opened()) { + terminal.cancelFocus() + view().terminal.close() + return + } + terminal.requestFocus(terminal.active()) + view().terminal.open() + }, }), viewCommand({ id: "review.toggle", diff --git a/packages/app/src/pages/session/v2/review-diff-kinds.ts b/packages/app/src/pages/session/v2/review-diff-kinds.ts index 8b252b0258..d3adb1f2ff 100644 --- a/packages/app/src/pages/session/v2/review-diff-kinds.ts +++ b/packages/app/src/pages/session/v2/review-diff-kinds.ts @@ -1,13 +1,18 @@ -import type { FileDiffInfo, VcsFileDiff } from "@opencode-ai/sdk/v2" +import type { SnapshotFileDiff, VcsFileDiff } from "@opencode-ai/sdk/v2" +import type { FileDiffInfo } from "@opencode-ai/client/promise" import type { Kind } from "@/components/file-tree-v2" import { normalizeFileTreeV2Path } from "@/components/file-tree-v2-model" -export type RenderDiff = FileDiffInfo | VcsFileDiff +export type RenderDiff = FileDiffInfo | (SnapshotFileDiff & { file: string }) | VcsFileDiff export function normalizePath(p: string) { return normalizeFileTreeV2Path(p) } +export function filterRenderableDiff(value: FileDiffInfo | SnapshotFileDiff | VcsFileDiff): value is RenderDiff { + return typeof value.file === "string" +} + export function reviewDiffNeedsLoad(diff: RenderDiff) { if (diff.additions === 0 && diff.deletions === 0) return false return !diff.patch || !/^@@ /m.test(diff.patch) diff --git a/packages/app/src/pages/session/v2/review-panel-v2-state.ts b/packages/app/src/pages/session/v2/review-panel-v2-state.ts index d5894bfb69..645055e107 100644 --- a/packages/app/src/pages/session/v2/review-panel-v2-state.ts +++ b/packages/app/src/pages/session/v2/review-panel-v2-state.ts @@ -9,7 +9,7 @@ import { createStore } from "solid-js/store" import { Persist, persisted } from "@/utils/persist" export function createReviewPanelV2State() { - const [store, setStore] = persisted( + const [store, setStore, , ready] = persisted( Persist.global("review-panel-v2"), createStore({ sidebarOpened: true, @@ -24,6 +24,7 @@ export function createReviewPanelV2State() { return { sidebarOpened: () => store.sidebarOpened, sidebarWidth: () => store.sidebarWidth, + sidebarTransition: ready, filter, setFilter, expandMode: () => store.expandMode, diff --git a/packages/app/src/pages/session/v2/review-panel-v2.tsx b/packages/app/src/pages/session/v2/review-panel-v2.tsx index cf9367d063..fcd6bbb79f 100644 --- a/packages/app/src/pages/session/v2/review-panel-v2.tsx +++ b/packages/app/src/pages/session/v2/review-panel-v2.tsx @@ -1,11 +1,11 @@ import { createMemo, createResource, createSignal, Show, type JSX } from "solid-js" -import type { FileDiffInfo, VcsFileDiff } from "@opencode-ai/sdk/v2" +import type { SnapshotFileDiff, VcsFileDiff } from "@opencode-ai/sdk/v2" +import type { FileDiffInfo } from "@opencode-ai/client/promise" import { SESSION_REVIEW_V2_SIDEBAR_WIDTH_MAX, SESSION_REVIEW_V2_SIDEBAR_WIDTH_MIN, SessionReviewV2, SessionReviewV2Sidebar, - SessionReviewV2SidebarToggle, } from "@opencode-ai/session-ui/v2/session-review-v2" import { SessionReviewFilePreviewV2 } from "@opencode-ai/session-ui/v2/session-review-file-preview-v2" import { DiffChanges } from "@opencode-ai/ui/v2/diff-changes-v2" @@ -22,6 +22,7 @@ import FileTreeV2 from "@/components/file-tree-v2" import { useLanguage } from "@/context/language" import { useSDK } from "@/context/sdk" import { + filterRenderableDiff, filterReviewFiles, reviewDiffKinds, reviewDiffNeedsLoad, @@ -30,7 +31,7 @@ import { import type { ReviewPanelV2State } from "@/pages/session/v2/review-panel-v2-state" import { applyFileListKeyDown, SessionFileListV2 } from "@/pages/session/v2/session-file-list-v2" -type ReviewDiff = FileDiffInfo | VcsFileDiff +type ReviewDiff = FileDiffInfo | SnapshotFileDiff | VcsFileDiff export type ReviewPanelV2Props = { title?: JSX.Element @@ -56,7 +57,7 @@ export type ReviewPanelV2Props = { export function ReviewPanelV2(props: ReviewPanelV2Props) { const sdk = useSDK() - const diffs = createMemo(() => props.diffs()) + const diffs = createMemo(() => props.diffs().filter(filterRenderableDiff)) const filteredFiles = createMemo(() => filterReviewFiles( diffs().map((diff) => diff.file), @@ -65,6 +66,8 @@ export function ReviewPanelV2(props: ReviewPanelV2Props) { ) const searching = createMemo(() => props.state.filter().trim().length > 0) const kinds = createMemo(() => reviewDiffKinds(diffs())) + // Changes-only trees omit "M" — every row is already a change; A/D stay visible. + const treeKinds = createMemo(() => new Map([...kinds()].filter(([, kind]) => kind !== "mix"))) const activeDiff = createMemo(() => { // A focused comment takes over the preview until the preview applies it and // clears the focus; the owner then persists the file as the active selection. @@ -112,9 +115,6 @@ export function ReviewPanelV2(props: ReviewPanelV2Props) { stats={} empty={props.empty} sidebarOpen={props.state.sidebarOpened()} - sidebarToggle={ - - } sidebar={ // Always mounted: the sidebar header hosts the changes-mode dropdown, // which must stay reachable when the current mode has zero diffs. @@ -126,7 +126,7 @@ export function ReviewPanelV2(props: ReviewPanelV2Props) { diffs={diffs} filteredFiles={filteredFiles} searching={searching} - kinds={kinds} + kinds={treeKinds} activeDiff={activeDiff} /> } @@ -201,6 +201,7 @@ function ReviewPanelV2Sidebar(props: { return ( } filter={props.state.filter()} diff --git a/packages/app/src/pages/session/v2/session-file-browser-tab.tsx b/packages/app/src/pages/session/v2/session-file-browser-tab.tsx index 9cbdd40df1..639429e80b 100644 --- a/packages/app/src/pages/session/v2/session-file-browser-tab.tsx +++ b/packages/app/src/pages/session/v2/session-file-browser-tab.tsx @@ -1,14 +1,9 @@ import { createMemo, createSignal, createUniqueId, Show } from "solid-js" import { createQuery } from "@tanstack/solid-query" -import { Tabs } from "@opencode-ai/ui/tabs" import { Icon } from "@opencode-ai/ui/icon" -import { - SessionFilePanelV2, - SessionFilePanelV2Empty, - SessionFilePanelV2Title, -} from "@opencode-ai/session-ui/v2/session-file-panel-v2" -import { SessionReviewV2Sidebar, SessionReviewV2SidebarToggle } from "@opencode-ai/session-ui/v2/session-review-v2" -import FileTree, { type Kind } from "@/components/file-tree" +import { SessionFilePanelV2, SessionFilePanelV2Empty } from "@opencode-ai/session-ui/v2/session-file-panel-v2" +import { SessionReviewV2Sidebar } from "@opencode-ai/session-ui/v2/session-review-v2" +import FileTreeV2, { type Kind } from "@/components/file-tree-v2" import { useFile } from "@/context/file" import { useLanguage } from "@/context/language" import { useLayout } from "@/context/layout" @@ -24,6 +19,7 @@ const emptyFiles: string[] = [] export type SessionFileBrowserState = { sidebarOpened: () => boolean sidebarWidth: () => number + sidebarTransition: () => boolean resizeSidebar: (width: number) => void toggleSidebar: () => void } @@ -93,102 +89,93 @@ export function SessionFileBrowserTab(props: { }) } + // Keep the sidebar outside Kobalte Tabs.Content: a morphing content value + // unmounts the whole panel on every file-tab switch and resets sidebar scroll. return ( - - - - - {title()} - - - } - sidebar={ - {title()}} - filter={filter()} - onFilterChange={setFilter} - onFilterKeyDown={onFilterKeyDown} - filterAutofocus={props.placeholder} - filterRef={props.filterRef} - filterControls={resultsID} - filterActiveDescendant={highlighted() ? optionID(highlighted()!) : undefined} - filterExpanded={query().length > 0 && files().length > 0} - width={props.state.sidebarWidth()} - onWidthChange={props.state.resizeSidebar} + {title()}} + filter={filter()} + onFilterChange={setFilter} + onFilterKeyDown={onFilterKeyDown} + filterAutofocus={props.placeholder} + filterRef={props.filterRef} + filterControls={resultsID} + filterActiveDescendant={highlighted() ? optionID(highlighted()!) : undefined} + filterExpanded={query().length > 0 && files().length > 0} + width={props.state.sidebarWidth()} + onWidthChange={props.state.resizeSidebar} + > + props.onSelect(node.path)} + onFileDoubleClick={(node) => props.onSelectPermanent(node.path)} + /> + } > props.onSelect(node.path)} - onFileDoubleClick={(node) => props.onSelectPermanent(node.path)} - /> +
+ {language.t("common.loading")} + {language.t("common.loading.ellipsis")} +
} > 0} fallback={
- {language.t("common.loading")} - {language.t("common.loading.ellipsis")} + {language.t("palette.empty")}
} > - 0} - fallback={ -
- {language.t("palette.empty")} -
- } - > - { - setExplicitHighlight(path) - props.onSelect(path) - }} - onFileDoubleClick={props.onSelectPermanent} - /> -
+ { + setExplicitHighlight(path) + props.onSelect(path) + }} + onFileDoubleClick={props.onSelectPermanent} + />
-
+
+ + } + > + +
+ +
{language.t("command.file.open")}
+
{language.t("session.files.selectToOpen")}
+
+ } > - -
- -
{language.t("command.file.open")}
-
{language.t("session.files.selectToOpen")}
-
- - } - > -
- - {(tab) => } - -
-
- - +
+ + {(tab) => } + +
+
+ ) } diff --git a/packages/app/src/utils/diffs.test.ts b/packages/app/src/utils/diffs.test.ts index f6d768e1de..a3d25f4279 100644 --- a/packages/app/src/utils/diffs.test.ts +++ b/packages/app/src/utils/diffs.test.ts @@ -1,5 +1,6 @@ import { describe, expect, test } from "bun:test" -import type { FileDiffInfo } from "@opencode-ai/sdk/v2" +import type { SnapshotFileDiff } from "@opencode-ai/sdk/v2" +import type { FileDiffInfo } from "@opencode-ai/client/promise" import type { Message } from "@opencode-ai/sdk/v2/client" import { diffs, message } from "./diffs" @@ -9,7 +10,7 @@ const item = { additions: 1, deletions: 1, status: "modified", -} satisfies FileDiffInfo +} satisfies FileDiffInfo & SnapshotFileDiff describe("diffs", () => { test("keeps valid arrays", () => { diff --git a/packages/app/src/utils/diffs.ts b/packages/app/src/utils/diffs.ts index 60df039410..a8eec75a9a 100644 --- a/packages/app/src/utils/diffs.ts +++ b/packages/app/src/utils/diffs.ts @@ -1,7 +1,8 @@ -import type { FileDiffInfo } from "@opencode-ai/sdk/v2" +import type { SnapshotFileDiff, VcsFileDiff } from "@opencode-ai/sdk/v2" +import type { FileDiffInfo } from "@opencode-ai/client/promise" import type { Message } from "@opencode-ai/sdk/v2/client" -type Diff = FileDiffInfo +type Diff = FileDiffInfo | SnapshotFileDiff | VcsFileDiff function diff(value: unknown): value is Diff { if (!value || typeof value !== "object" || Array.isArray(value)) return false diff --git a/packages/app/src/utils/draft-store.ts b/packages/app/src/utils/draft-store.ts new file mode 100644 index 0000000000..cd0895f52c --- /dev/null +++ b/packages/app/src/utils/draft-store.ts @@ -0,0 +1,171 @@ +import type { AsyncStorage } from "@solid-primitives/storage" + +export type BlobReference = { id: string; url: string } + +type Driver = { + get(key: string): Promise + set(key: string, value: string): Promise + remove(key: string): Promise + putBlob(blob: Blob): Promise + getBlob(id: string): Promise +} + +export type DraftStore = AsyncStorage & { putBlob(blob: Blob): Promise } +const urls = new Map() + +function blobUrl(id: string, blob: Blob) { + const existing = urls.get(id) + if (existing) return existing + const url = URL.createObjectURL(blob) + urls.set(id, url) + return url +} + +async function blobID(blob: Blob) { + const id = Array.from(new Uint8Array(await crypto.subtle.digest("SHA-256", await blob.arrayBuffer()))) + .map((byte) => byte.toString(16).padStart(2, "0")) + .join("") + return id +} + +export async function createBlobReference(blob: Blob): Promise { + const id = await blobID(blob) + return { id, url: blobUrl(id, blob) } +} + +export function createDraftStore(driver: Driver): DraftStore { + const versions = new Map() + const putBlob = async (blob: Blob) => { + const id = await driver.putBlob(blob) + return { id, url: blobUrl(id, blob) } + } + const encode = async (value: unknown): Promise => { + if (Array.isArray(value)) return Promise.all(value.map(encode)) + if (!value || typeof value !== "object") return value + const item = value as Record + if (item.type === "image" && typeof item.dataUrl === "string") { + const blob = await fetch(item.dataUrl).then((response) => response.blob()) + const { dataUrl: _, ...rest } = item + return { ...rest, blob: { id: await driver.putBlob(blob) } } + } + if ("blob" in item && item.blob && typeof item.blob === "object") { + const blob = item.blob as Record + if (typeof blob.id === "string" && blob.id.startsWith("data:")) { + const data = await fetch(blob.id).then((response) => response.blob()) + return { ...item, blob: { id: await driver.putBlob(data) } } + } + return { ...item, blob: { id: blob.id } } + } + return Object.fromEntries( + await Promise.all(Object.entries(item).map(async ([key, entry]) => [key, await encode(entry)])), + ) + } + const decode = async (value: unknown): Promise => { + if (Array.isArray(value)) return Promise.all(value.map(decode)) + if (!value || typeof value !== "object") return value + const item = value as Record + if (item.blob && typeof item.blob === "object") { + const ref = item.blob as Record + if (typeof ref.id === "string") { + const blob = await driver.getBlob(ref.id) + if (blob) return { ...item, blob: { id: ref.id, url: blobUrl(ref.id, blob) } } + } + } + return Object.fromEntries( + await Promise.all(Object.entries(item).map(async ([key, entry]) => [key, await decode(entry)])), + ) + } + return { + getItem: async (key) => { + const value = await driver.get(key) + return value === null ? null : JSON.stringify(await decode(JSON.parse(value))) + }, + setItem: async (key, value) => { + const version = (versions.get(key) ?? 0) + 1 + versions.set(key, version) + const encoded = JSON.stringify(await encode(JSON.parse(value))) + if (versions.get(key) === version) await driver.set(key, encoded) + }, + removeItem: async (key) => { + versions.set(key, (versions.get(key) ?? 0) + 1) + await driver.remove(key) + }, + putBlob, + } +} + +export function createBrowserDraftStore(): DraftStore { + const request = indexedDB.open("opencode-drafts", 1) + request.addEventListener("upgradeneeded", () => { + request.result.createObjectStore("documents") + request.result.createObjectStore("blobs") + }) + const db = new Promise((resolve, reject) => { + request.addEventListener("success", () => { + const database = request.result + const transaction = database.transaction(["documents", "blobs"], "readwrite") + const documents = transaction.objectStore("documents").getAll() + documents.addEventListener("success", () => { + const used = new Set() + JSON.parse(`[${documents.result.join(",")}]`, (_key, item) => { + if (item?.blob && typeof item.blob.id === "string") used.add(item.blob.id) + return item + }) + const blobs = transaction.objectStore("blobs").openKeyCursor() + blobs.addEventListener("success", () => { + const cursor = blobs.result + if (!cursor) return + if (!used.has(String(cursor.key))) cursor.delete() + cursor.continue() + }) + }) + transaction.addEventListener("complete", () => resolve(database)) + transaction.addEventListener("abort", () => resolve(database)) + }) + request.addEventListener("error", () => reject(request.error)) + }) + const get = async (store: string, key: string) => { + const result = (await db).transaction(store).objectStore(store).get(key) + return new Promise((resolve, reject) => { + result.addEventListener("success", () => resolve(result.result)) + result.addEventListener("error", () => reject(result.error)) + }) + } + const write = async (store: string, key: string, value?: unknown) => { + const transaction = (await db).transaction(store, "readwrite") + if (value === undefined) transaction.objectStore(store).delete(key) + else transaction.objectStore(store).put(value, key) + return new Promise((resolve, reject) => { + transaction.addEventListener("complete", () => resolve()) + transaction.addEventListener("error", () => reject(transaction.error)) + }) + } + return createDraftStore({ + get: async (key) => ((await get("documents", key)) as string | undefined) ?? null, + set: (key, value) => write("documents", key, value), + remove: (key) => write("documents", key), + putBlob: async (blob) => { + const id = await blobID(blob) + await write("blobs", id, blob) + return id + }, + getBlob: async (id) => ((await get("blobs", id)) as Blob | undefined) ?? null, + }) +} + +export async function blobDataUrl(blob: BlobReference, mime: string) { + const data = await fetch(blob.url).then((response) => response.blob()) + return new Promise((resolve, reject) => { + const reader = new FileReader() + reader.addEventListener("error", () => reject(reader.error)) + reader.addEventListener("load", () => { + const value = typeof reader.result === "string" ? reader.result : "" + resolve(`data:${mime};base64,${value.slice(value.indexOf(",") + 1)}`) + }) + reader.readAsDataURL(data) + }) +} + +export function createLegacyBlobReference(dataUrl: string): BlobReference { + return { id: dataUrl, url: dataUrl } +} diff --git a/packages/app/src/utils/menu-dismiss-controller.ts b/packages/app/src/utils/menu-dismiss-controller.ts new file mode 100644 index 0000000000..0a3009eb71 --- /dev/null +++ b/packages/app/src/utils/menu-dismiss-controller.ts @@ -0,0 +1,30 @@ +/** Coordinates focus restoration and actions that must run after menu content unmounts. */ +export function createMenuDismissController(content: () => HTMLElement | undefined) { + let restoreTrigger = true + + return { + /** Allows the menu primitive to restore focus to its trigger when closing. */ + allowTriggerRestore() { + restoreTrigger = true + }, + /** Keeps focus at its current or next destination instead of returning it to the trigger. */ + preventTriggerRestore() { + restoreTrigger = false + }, + /** Applies the current restoration policy during the menu primitive's close-focus event. */ + onCloseAutoFocus(event: Event) { + if (!restoreTrigger) event.preventDefault() + }, + /** Runs an action after the menu unmounts and its focus-close work has settled. */ + afterClose(callback: () => void) { + const complete = () => { + if (content()?.isConnected) { + requestAnimationFrame(complete) + return + } + requestAnimationFrame(() => requestAnimationFrame(callback)) + } + requestAnimationFrame(complete) + }, + } +} diff --git a/packages/app/src/utils/notification-click.test.ts b/packages/app/src/utils/notification-click.test.ts deleted file mode 100644 index fa81b0e025..0000000000 --- a/packages/app/src/utils/notification-click.test.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { afterEach, describe, expect, test } from "bun:test" -import { handleNotificationClick, setNavigate } from "./notification-click" - -describe("notification click", () => { - afterEach(() => { - setNavigate(undefined as any) - }) - - test("navigates via registered navigate function", () => { - const calls: string[] = [] - setNavigate((href) => calls.push(href)) - handleNotificationClick("/abc/session/123") - expect(calls).toEqual(["/abc/session/123"]) - }) - - test("does not navigate when href is missing", () => { - const calls: string[] = [] - setNavigate((href) => calls.push(href)) - handleNotificationClick(undefined) - expect(calls).toEqual([]) - }) - - test("falls back to location.assign without registered navigate", () => { - handleNotificationClick("/abc/session/123") - // falls back to window.location.assign — no error thrown - }) -}) diff --git a/packages/app/src/utils/notification-click.ts b/packages/app/src/utils/notification-click.ts deleted file mode 100644 index 316b278206..0000000000 --- a/packages/app/src/utils/notification-click.ts +++ /dev/null @@ -1,13 +0,0 @@ -let nav: ((href: string) => void) | undefined - -export const setNavigate = (fn: (href: string) => void) => { - nav = fn -} - -export const handleNotificationClick = (href?: string) => { - window.focus() - if (!href) return - if (nav) return nav(href) - console.warn("notification-click: navigate function not set, falling back to window.location.assign") - window.location.assign(href) -} diff --git a/packages/app/src/utils/persist.ts b/packages/app/src/utils/persist.ts index a2daae4866..a7742c399e 100644 --- a/packages/app/src/utils/persist.ts +++ b/packages/app/src/utils/persist.ts @@ -15,6 +15,7 @@ type PersistedWithReady = [ ] type PersistTarget = { + draft?: boolean storage?: string scope?: "window" legacyStorageNames?: string[] @@ -295,6 +296,14 @@ async function removeAsync(storage: AsyncStorage, key: string) { } catch {} } +function toAsyncStorage(storage: SyncStorage | AsyncStorage): AsyncStorage { + return { + getItem: async (key) => storage.getItem(key), + setItem: async (key, value) => storage.setItem(key, value), + removeItem: async (key) => storage.removeItem(key), + } +} + async function migrateLegacyAsync(input: { current: AsyncStorage legacyStore?: AsyncStorage @@ -513,6 +522,9 @@ export const Persist = { if (session) return Persist.serverSession(scope, dir, session, key, legacy) return Persist.serverWorkspace(scope, dir, key, legacy) }, + prompt(target: PersistTarget): PersistTarget { + return { ...target, draft: true } + }, } function resolveTarget(target: PersistTarget, platform: Platform): PersistTarget { @@ -526,9 +538,12 @@ function resolveTarget(target: PersistTarget, platform: Platform): PersistTarget } export function removePersisted( - target: { storage?: string; legacyStorageNames?: string[]; key: string }, + target: { draft?: boolean; storage?: string; legacyStorageNames?: string[]; key: string }, platform?: Platform, ) { + if (target.draft && platform?.draftStore) { + void platform.draftStore.removeItem(`${target.storage ?? "default"}:${target.key}`) + } const isDesktop = platform?.platform === "desktop" && !!platform.storage if (isDesktop) { @@ -561,8 +576,17 @@ export function persisted( const legacy = config.legacy ?? [] const isDesktop = platform.platform === "desktop" && !!platform.storage + const draft = config.draft ? platform.draftStore : undefined const currentStorage = (() => { + if (draft) { + const prefix = `${config.storage ?? "default"}:` + return { + getItem: (key: string) => draft.getItem(prefix + key), + setItem: (key: string, value: string) => draft.setItem(prefix + key, value), + removeItem: (key: string) => draft.removeItem(prefix + key), + } satisfies AsyncStorage + } if (isDesktop) return platform.storage?.(config.storage) if (!config.storage) return localStorageDirect() return localStorageWithPrefix(config.storage) @@ -577,7 +601,7 @@ export function persisted( const legacyStorageNames = config.legacyStorageNames ?? [] const storage = (() => { - if (!isDesktop) { + if (!isDesktop && !draft) { const current = currentStorage as SyncStorage const legacyStore = legacyStorage as SyncStorage const legacyStores = legacyStorageNames.map(localStorageWithPrefix) @@ -609,15 +633,26 @@ export function persisted( const current = currentStorage as AsyncStorage const legacyStore = legacyStorage as AsyncStorage | undefined - const legacyStores = legacyStorageNames - .map((name) => platform.storage?.(name) as AsyncStorage | undefined) + const oldCurrent = draft + ? isDesktop + ? platform.storage?.(config.storage) + : config.storage + ? localStorageWithPrefix(config.storage) + : localStorageDirect() + : undefined + const legacyStores = [ + oldCurrent, + ...legacyStorageNames.map((name) => (isDesktop ? platform.storage?.(name) : localStorageWithPrefix(name))), + ] .filter((x) => !!x) + .map(toAsyncStorage) + let draftLatest: string | undefined const api: AsyncStorage = { getItem: async (key) => { const value = await readCurrentAsync({ storage: current, key, defaults, migrate: config.migrate }) if (value !== undefined) return value - return migrateLegacyAsync({ + const migrated = await migrateLegacyAsync({ current, legacyStore, stores: legacyStores, @@ -626,8 +661,15 @@ export function persisted( defaults, migrate: config.migrate, }) + if (draftLatest === undefined) { + if (draft && migrated !== null) return (await current.getItem(key)) ?? migrated + return migrated + } + await current.setItem(key, draftLatest) + return draftLatest }, setItem: async (key, value) => { + if (draft) draftLatest = value await current.setItem(key, value) }, removeItem: async (key) => { diff --git a/packages/app/src/utils/prompt.test.ts b/packages/app/src/utils/prompt.test.ts index 1ecaf02c97..8b86d43b90 100644 --- a/packages/app/src/utils/prompt.test.ts +++ b/packages/app/src/utils/prompt.test.ts @@ -37,8 +37,18 @@ describe("extractPromptFromParts", () => { expect(result).toHaveLength(3) expect(result[0]).toMatchObject({ type: "text", content: "check these" }) expect(result.slice(1)).toMatchObject([ - { type: "image", filename: "a.png", mime: "image/png", dataUrl: "data:image/png;base64,AAA" }, - { type: "image", filename: "b.pdf", mime: "application/pdf", dataUrl: "data:application/pdf;base64,BBB" }, + { + type: "image", + filename: "a.png", + mime: "image/png", + blob: expect.objectContaining({ id: expect.any(String) }), + }, + { + type: "image", + filename: "b.pdf", + mime: "application/pdf", + blob: expect.objectContaining({ id: expect.any(String) }), + }, ]) }) }) diff --git a/packages/app/src/utils/prompt.ts b/packages/app/src/utils/prompt.ts index 35aec0071a..67d32086bb 100644 --- a/packages/app/src/utils/prompt.ts +++ b/packages/app/src/utils/prompt.ts @@ -1,5 +1,6 @@ import type { AgentPart as MessageAgentPart, FilePart, Part, TextPart } from "@opencode-ai/sdk/v2" import type { AgentPart, FileAttachmentPart, ImageAttachmentPart, Prompt } from "@/context/prompt" +import { createLegacyBlobReference } from "@/utils/draft-store" type Inline = | { @@ -107,7 +108,7 @@ export function extractPromptFromParts(parts: Part[], opts?: { directory?: strin id: filePart.id, filename: filePart.filename ?? attachmentName, mime: filePart.mime, - dataUrl: filePart.url, + blob: createLegacyBlobReference(filePart.url), }) } } diff --git a/packages/app/src/utils/server-compat.test.ts b/packages/app/src/utils/server-compat.test.ts new file mode 100644 index 0000000000..52e5ec6e3b --- /dev/null +++ b/packages/app/src/utils/server-compat.test.ts @@ -0,0 +1,236 @@ +import { describe, expect, test } from "bun:test" +import { createApiForServer, createSdkForServer } from "./server" +import { createCompatibleApi } from "./server-compat" + +function setup( + protocol: "v1" | "v2" | Promise<"v1" | "v2">, + responses?: { vcs?: { branch: string; default_branch: string } }, +) { + const requests: Request[] = [] + const fetcher = Object.assign( + async (input: string | URL | Request, init?: RequestInit) => { + const request = new Request(input, init) + requests.push(request) + if (request.method === "PATCH") { + return Response.json({ + id: "ses_1", + slug: "ses_1", + projectID: "project", + directory: "/repo", + title: "Session", + version: "1", + time: { created: 1, updated: 1 }, + }) + } + if (request.method === "POST" && request.url.endsWith("/prompt_async")) + return new Response(undefined, { status: 204 }) + if (request.method === "POST" && request.url.endsWith("/prompt")) { + return Response.json({ + admittedSeq: 1, + id: "msg_1", + sessionID: "ses_1", + timeCreated: 1, + type: "user", + data: { text: "hello" }, + delivery: "steer", + }) + } + if (request.method === "GET" && new URL(request.url).pathname === "/vcs") + return Response.json(responses?.vcs ?? {}) + if (request.method === "GET") return Response.json([]) + return new Response(undefined, { status: 204 }) + }, + { preconnect: globalThis.fetch.preconnect }, + ) + const server = { url: "http://localhost:4096" } + const api = createCompatibleApi({ + protocol: typeof protocol === "string" ? Promise.resolve(protocol) : protocol, + current: createApiForServer({ server, fetch: fetcher }), + legacy: (directory) => createSdkForServer({ server, fetch: fetcher, directory, throwOnError: true }), + directory: "/repo", + }) + return { api, requests } +} + +describe("createCompatibleApi", () => { + /* + test("routes V1 archive through the legacy session update", async () => { + const { api, requests } = setup("v1") + await api.session.archive({ sessionID: "ses_1", directory: "/repo" }) + + const url = new URL(requests[0]!.url) + expect(url.pathname).toBe("/session/ses_1") + expect(requests[0]!.headers.get("x-opencode-directory")).toBe("%2Frepo") + expect(requests[0]!.method).toBe("PATCH") + expect(await requests[0]!.json()).toMatchObject({ time: { archived: expect.any(Number) } }) + }) + */ + + test("converts current prompts to the V1 prompt contract", async () => { + const { api, requests } = setup("v1") + await api.session.prompt({ + sessionID: "ses_1", + id: "msg_1", + text: "hello @src/index.ts", + agent: "build", + model: { providerID: "provider", modelID: "model" }, + files: [ + { uri: "file:///repo/src/index.ts", name: "index.ts", mention: { text: "@src/index.ts", start: 6, end: 19 } }, + { uri: "data:text/plain;base64,aGVsbG8=", name: "notes.txt" }, + ], + }) + + expect(new URL(requests[0]!.url).pathname).toBe("/session/ses_1/prompt_async") + const body = await requests[0]!.json() + expect(body).toMatchObject({ + messageID: "msg_1", + agent: "build", + model: { providerID: "provider", modelID: "model" }, + parts: [ + { type: "text", text: "hello @src/index.ts" }, + { + type: "file", + mime: "text/plain", + url: "file:///repo/src/index.ts", + filename: "index.ts", + source: { + type: "file", + text: { value: "@src/index.ts", start: 6, end: 19 }, + path: "file:///repo/src/index.ts", + }, + }, + { + type: "file", + mime: "text/plain", + url: "data:text/plain;base64,aGVsbG8=", + filename: "notes.txt", + }, + ], + }) + expect(body.parts[2]).not.toHaveProperty("source") + }) + + test("preserves original parts for V1 optimistic reconciliation", async () => { + const { api, requests } = setup("v1") + await api.session.prompt({ + sessionID: "ses_1", + id: "msg_1", + text: "look", + files: [{ uri: "data:image/png;base64,AAAA", name: "image.png" }], + legacyParts: [ + { id: "prt_text", type: "text", text: "look" }, + { id: "prt_image", type: "file", mime: "image/png", url: "data:image/png;base64,AAAA", filename: "image.png" }, + ], + }) + + expect((await requests[0]!.json()).parts).toEqual([ + { id: "prt_text", type: "text", text: "look" }, + { id: "prt_image", type: "file", mime: "image/png", url: "data:image/png;base64,AAAA", filename: "image.png" }, + ]) + }) + + test("resolves protocol detection once across implementation methods", async () => { + let detections = 0 + const resolved = Promise.resolve<"v1" | "v2">("v2") + const protocol = new Proxy(resolved, { + get(target, property) { + if (property !== "then") return Reflect.get(target, property, target) + detections++ + return target.then.bind(target) + }, + }) + const { api } = setup(protocol) + + await api.session.list() + await api.session.list() + + expect(detections).toBe(1) + }) + + /* + test("keeps V2 session actions on the current API", async () => { + const { api, requests } = setup("v2") + await api.session.archive({ sessionID: "ses_1" }) + + expect(new URL(requests[0]!.url).pathname).toBe("/api/session/ses_1/archive") + expect(requests[0]!.method).toBe("POST") + }) + */ + + test("uses the global V1 session search endpoint", async () => { + const { api, requests } = setup("v1") + await api.session.list({ parentID: null, search: "session", limit: 50 }) + + expect(new URL(requests[0]!.url).pathname).toBe("/experimental/session") + }) + + /* + test("projects the V1 default branch", async () => { + const { api } = setup("v1", { vcs: { branch: "feature", default_branch: "dev" } }) + + expect(await api.vcs.get({ location: { directory: "/repo" } })).toMatchObject({ + data: { branch: "feature", defaultBranch: "dev" }, + }) + }) + */ + + test("translates current file searches to the V1 dirs parameter", async () => { + const { api, requests } = setup("v1") + await api.file.find({ location: { directory: "/repo" }, query: "src", type: "file", limit: 20 }) + + const url = new URL(requests[0]!.url) + expect(url.pathname).toBe("/find/file") + expect(url.searchParams.get("dirs")).toBe("false") + expect(url.searchParams.get("limit")).toBe("20") + }) + + test("routes V1 permission replies through the requested directory", async () => { + const { api, requests } = setup("v1") + await api.permission.reply({ + sessionID: "ses_1", + requestID: "permission_1", + reply: "once", + location: { directory: "/other" }, + }) + + expect(new URL(requests[0]!.url).pathname).toBe("/session/ses_1/permissions/permission_1") + expect(new URL(requests[0]!.url).searchParams.get("directory")).toBe("/other") + }) + + test("disposes the V1 instance after connecting a provider", async () => { + const { api, requests } = setup("v1") + + await api.integration.connect.key({ + integrationID: "openrouter", + key: "secret", + location: { directory: "/repo" }, + }) + + expect(requests.map((request) => new URL(request.url).pathname)).toEqual([ + "/auth/openrouter", + "/instance/dispose", + "/instance/dispose", + ]) + expect(requests[1]!.headers.get("x-opencode-directory")).toBe("%2Frepo") + expect(requests[2]!.headers.get("x-opencode-directory")).toBeNull() + }) + + test("disposes the V1 instance after completing provider OAuth", async () => { + const { api, requests } = setup("v1") + + await api.integration.oauth.complete({ + integrationID: "openrouter", + attemptID: "openrouter:0", + code: "code", + location: { directory: "/repo" }, + }) + + expect(requests.map((request) => new URL(request.url).pathname)).toEqual([ + "/provider/openrouter/oauth/callback", + "/instance/dispose", + "/instance/dispose", + ]) + expect(requests[1]!.headers.get("x-opencode-directory")).toBe("%2Frepo") + expect(requests[2]!.headers.get("x-opencode-directory")).toBeNull() + }) +}) diff --git a/packages/app/src/utils/server-compat.ts b/packages/app/src/utils/server-compat.ts new file mode 100644 index 0000000000..1df1338b71 --- /dev/null +++ b/packages/app/src/utils/server-compat.ts @@ -0,0 +1,518 @@ +import type { ServerApi } from "./server" +import type { ServerProtocol } from "./server-protocol" +import type { AgentPartInput, FilePartInput, OpencodeClient, Session, TextPartInput } from "@opencode-ai/sdk/v2/client" +import type { + Project, + ProjectCurrent, + SessionApi, + SessionCommandInput, + SessionCommandOutput, + SessionCompactInput, + SessionCompactOutput, + SessionInfo, + SessionPromptInput, + SessionPromptOutput, + SessionShellInput, + SessionShellOutput, +} from "@opencode-ai/client/promise" + +type LegacyClient = OpencodeClient +type LegacyFor = (directory?: string) => LegacyClient +type CompatibleSessionApi = Omit< + SessionApi, + "prompt" | "command" | "shell" | "compact" | "rename" | "archive" | "remove" +> & { + prompt: (input: SessionPromptInput & LegacyPrompt) => Promise + command: (input: SessionCommandInput) => Promise + shell: (input: SessionShellInput & LegacyPrompt) => Promise + compact: (input: SessionCompactInput & { model?: LegacyPrompt["model"] }) => Promise + rename: (input: Parameters[0] & LegacyLocation) => ReturnType + // archive: (input: Parameters[0] & LegacyLocation) => ReturnType + remove: (input: Parameters[0] & LegacyLocation) => ReturnType +} +type CompatiblePermissionApi = Omit & { + reply: ( + input: Parameters[0] & { location?: { directory?: string } }, + ) => ReturnType +} +export type CompatibleApi = Omit & { + readonly session: CompatibleSessionApi + readonly permission: CompatiblePermissionApi +} +type LegacyPrompt = { + agent?: string + model?: { providerID: string; modelID: string } + variant?: string + legacyParts?: (TextPartInput | FilePartInput | AgentPartInput)[] +} +type LegacyLocation = { directory?: string } +type CompatibleInput = { + protocol: Promise + current: ServerApi + legacy: LegacyFor + directory?: string +} + +function mime(uri: string) { + const match = /^data:([^;,]+)/.exec(uri) + return match?.[1] ?? "application/octet-stream" +} + +function sessionInfo(session: Session): SessionInfo { + return { + id: session.id, + parentID: session.parentID, + projectID: session.projectID, + agent: session.agent, + model: session.model && { + id: session.model.id, + providerID: session.model.providerID, + variant: session.model.variant, + }, + cost: session.cost ?? 0, + tokens: session.tokens ?? { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } }, + time: session.time, + title: session.title, + location: { directory: session.directory, workspaceID: session.workspaceID }, + subpath: session.path, + revert: session.revert && { + messageID: session.revert.messageID, + partID: session.revert.partID, + snapshot: session.revert.snapshot, + }, + } +} + +export function createCompatibleApi(input: CompatibleInput): CompatibleApi { + const v1 = createV1Api(input) + return lazyApi( + input.protocol.then((protocol) => (protocol === "v1" ? v1 : input.current)), + input.current, + ) +} + +function lazyApi(implementation: Promise, shape: T): T { + const cache = new Map() + return new Proxy(shape, { + get(target, property, receiver) { + const sample = Reflect.get(target, property, receiver) + if (typeof sample === "function") { + return (...args: unknown[]) => + implementation.then((value) => { + const method = Reflect.get(value, property) + if (typeof method !== "function") throw new Error(`API method unavailable: ${String(property)}`) + return Reflect.apply(method, value, args) + }) + } + if (sample === null || typeof sample !== "object") return sample + if (cache.has(property)) return cache.get(property) + const nested = lazyApi( + implementation.then((value) => { + const result = Reflect.get(value, property) + if (result === null || typeof result !== "object") { + throw new Error(`API namespace unavailable: ${String(property)}`) + } + return result + }), + sample, + ) + cache.set(property, nested) + return nested + }, + }) +} + +function createV1Api(input: CompatibleInput): CompatibleApi { + const directory = (location?: { directory?: string }) => location?.directory ?? input.directory + const legacy = (location?: { directory?: string }) => input.legacy(directory(location)) + const located = (data: T, value?: { directory?: string }) => ({ + location: { + directory: directory(value) ?? "", + project: { id: "", directory: directory(value) ?? "" }, + }, + data, + }) + + return { + ...input.current, + session: { + ...input.current.session, + async list( + value?: Parameters[0], + options?: Parameters[1], + ) { + if (!value?.directory && value?.search !== undefined) { + const result = await legacy().experimental.session.list( + { + roots: value.parentID === null ? true : undefined, + search: value.search, + limit: value.limit, + }, + options, + ) + return { data: (result.data ?? []).map(sessionInfo), cursor: {} } + } + const result = await legacy({ directory: value?.directory }).session.list({ + directory: value?.directory, + roots: value?.parentID === null ? true : undefined, + search: value?.search, + limit: value?.limit, + }) + return { data: (result.data ?? []).map(sessionInfo), cursor: {} } + }, + async create(value?: Parameters[0]) { + const result = await legacy(value?.location ?? undefined).session.create({ + directory: directory(value?.location ?? undefined), + }) + if (!result.data) throw new Error("Failed to create session") + return sessionInfo(result.data) + }, + async get(value: Parameters[0]) { + const result = await legacy().session.get(value) + if (!result.data) throw new Error(`Session not found: ${value.sessionID}`) + return sessionInfo(result.data) + }, + async active() { + const result = await legacy().session.status() + return Object.fromEntries( + Object.entries(result.data ?? {}).flatMap(([sessionID, status]) => + status.type === "idle" ? [] : [[sessionID, { type: "running" as const }]], + ), + ) + }, + async rename(value: Parameters[0] & LegacyLocation) { + await legacy(value).session.update({ sessionID: value.sessionID, title: value.title }) + }, + // async archive(value: Parameters[0] & LegacyLocation) { + // await legacy(value).session.update({ sessionID: value.sessionID, time: { archived: Date.now() } }) + // }, + async remove(value: Parameters[0] & LegacyLocation) { + await legacy(value).session.delete(value) + }, + async fork(value: Parameters[0]) { + const result = await legacy().session.fork(value) + if (!result.data) throw new Error("Failed to fork session") + return sessionInfo(result.data) + }, + async interrupt(value: Parameters[0]) { + await legacy().session.abort(value) + }, + async prompt(value: SessionPromptInput & LegacyPrompt) { + await legacy().session.promptAsync({ + sessionID: value.sessionID, + messageID: value.id ?? undefined, + agent: value.agent, + model: value.model, + variant: value.variant, + parts: value.legacyParts ?? [ + { type: "text", text: value.text }, + ...(value.files ?? []).map((file) => ({ + type: "file" as const, + mime: file.mention ? "text/plain" : mime(file.uri), + url: file.uri, + filename: file.name, + source: file.mention + ? { + type: "file" as const, + text: { value: file.mention.text, start: file.mention.start, end: file.mention.end }, + path: file.uri, + } + : undefined, + })), + ...(value.agents ?? []).map((agent) => ({ + type: "agent" as const, + name: agent.name, + source: agent.mention + ? { value: agent.mention.text, start: agent.mention.start, end: agent.mention.end } + : undefined, + })), + ], + }) + return { + admittedSeq: 0, + id: value.id ?? "", + sessionID: value.sessionID, + timeCreated: Date.now(), + type: "user", + data: { text: value.text }, + delivery: value.delivery ?? "steer", + } + }, + async command(value: SessionCommandInput) { + await legacy().session.command({ + sessionID: value.sessionID, + messageID: value.id ?? undefined, + command: value.command, + arguments: value.arguments ?? "", + agent: value.agent ?? undefined, + model: value.model ? `${value.model.providerID}/${value.model.id}` : undefined, + variant: value.model?.variant, + parts: value.files?.map((file) => ({ + type: "file" as const, + mime: mime(file.uri), + url: file.uri, + filename: file.name, + })), + }) + return { + admittedSeq: 0, + id: value.id ?? "", + sessionID: value.sessionID, + timeCreated: Date.now(), + type: "user", + data: { text: `/${value.command} ${value.arguments ?? ""}`.trim() }, + delivery: value.delivery ?? "steer", + } + }, + async shell(value: SessionShellInput & LegacyPrompt) { + await legacy().session.shell({ + sessionID: value.sessionID, + command: value.command, + agent: value.agent, + model: value.model, + }) + }, + compact: async (value: SessionCompactInput & { model?: LegacyPrompt["model"] }) => { + if (!value.model) throw new Error("A model is required to compact a V1 session") + await legacy().session.summarize({ + sessionID: value.sessionID, + providerID: value.model.providerID, + modelID: value.model.modelID, + }) + return { + admittedSeq: 0, + id: value.id ?? "", + sessionID: value.sessionID, + timeCreated: Date.now(), + type: "compaction", + } + }, + revert: { + stage: async (value: Parameters[0]) => { + await legacy().session.revert(value) + return { messageID: value.messageID } + }, + clear: async (value: Parameters[0]) => { + await legacy().session.unrevert(value) + }, + commit: input.current.session.revert.commit, + }, + }, + project: { + ...input.current.project, + async list() { + return ((await legacy().project.list()).data ?? []) as Project[] + }, + async current(value?: Parameters[0]) { + const result = await legacy(value?.location).project.current() + if (!result.data) throw new Error("Project not found") + return { id: result.data.id, directory: result.data.worktree } satisfies ProjectCurrent + }, + // async update(value: Parameters[0]) { + // const project = (await legacy().project.list()).data?.find((item) => item.id === value.projectID) + // const result = await legacy({ directory: project?.worktree }).project.update({ + // ...value, + // directory: project?.worktree, + // }) + // if (!result.data) throw new Error(`Project not found: ${value.projectID}`) + // return result.data as Project + // }, + async directories(value: Parameters[0]) { + const result = await legacy(value.location).worktree.list() + return (result.data ?? []).map((item) => ({ directory: item })) + }, + }, + // path: { + // ...input.current.path, + // async get(value?: Parameters[0]) { + // const result = await legacy(value?.location).path.get() + // if (!result.data) throw new Error("Path unavailable") + // return result.data + // }, + // }, + vcs: { + ...input.current.vcs, + // async get(value?: Parameters[0]) { + // const result = await legacy(value?.location).vcs.get() + // return located({ branch: result.data?.branch, defaultBranch: result.data?.default_branch }, value?.location) + // }, + async status(value?: Parameters[0]) { + const result = await legacy(value?.location).vcs.status() + return located(result.data ?? [], value?.location) + }, + async diff(value: Parameters[0]) { + const result = await legacy(value.location).vcs.diff({ + mode: value.mode === "working" ? "git" : value.mode, + context: value.context, + }) + return located( + (result.data ?? []).map((file) => ({ + file: file.file, + patch: file.patch ?? "", + additions: file.additions, + deletions: file.deletions, + status: file.status ?? "modified", + })), + value.location, + ) + }, + }, + file: { + ...input.current.file, + async list(value?: Parameters[0]) { + const result = await legacy(value?.location).file.list({ path: value?.path ?? "" }) + return located(result.data ?? [], value?.location) + }, + async find(value: Parameters[0]) { + const result = await legacy(value.location).find.files({ + query: value.query, + dirs: value.type === undefined ? undefined : value.type === "directory" ? "true" : "false", + limit: value.limit, + }) + return located( + (result.data ?? []).map((path) => ({ path, type: value.type ?? "file" })), + value.location, + ) + }, + }, + integration: { + ...input.current.integration, + async get(value: Parameters[0]) { + const methods = ((await legacy(value.location).provider.auth()).data?.[value.integrationID] ?? []).map( + (method, index) => + method.type === "api" + ? { type: "key" as const, label: method.label } + : { type: "oauth" as const, id: String(index), label: method.label, prompts: method.prompts }, + ) + return located( + { + id: value.integrationID, + name: value.integrationID, + methods, + connections: [], + }, + value.location, + ) + }, + connect: { + ...input.current.integration.connect, + key: async (value: Parameters[0]) => { + await legacy(value.location).auth.set({ + providerID: value.integrationID, + auth: { type: "api", key: value.key }, + }) + await legacy(value.location).instance.dispose() + await input.legacy().instance.dispose() + }, + }, + oauth: { + ...input.current.integration.oauth, + connect: async (value: Parameters[0]) => { + const method = Number(value.methodID) + const result = await legacy(value.location).provider.oauth.authorize( + { providerID: value.integrationID, method, inputs: value.inputs }, + { throwOnError: true }, + ) + if (!result.data) throw new Error("Failed to start OAuth authorization") + return located( + { + attemptID: `${value.integrationID}:${method}`, + url: result.data.url, + instructions: result.data.instructions, + mode: result.data.method, + time: { created: Date.now(), expires: Date.now() + 10 * 60 * 1000 }, + }, + value.location, + ) + }, + complete: async (value: Parameters[0]) => { + const method = Number(value.attemptID.split(":").at(-1)) + await legacy(value.location).provider.oauth.callback( + { providerID: value.integrationID, method, code: value.code }, + { throwOnError: true }, + ) + await legacy(value.location).instance.dispose() + await input.legacy().instance.dispose() + }, + status: async (value: Parameters[0]) => { + const method = Number(value.attemptID.split(":").at(-1)) + await legacy(value.location).provider.oauth.callback( + { providerID: value.integrationID, method }, + { throwOnError: true }, + ) + await legacy(value.location).instance.dispose() + await input.legacy().instance.dispose() + return located( + { status: "complete" as const, time: { created: Date.now(), expires: Date.now() } }, + value.location, + ) + }, + }, + }, + pty: { + ...input.current.pty, + // async shells(value?: Parameters[0]) { + // return located((await legacy(value?.location).pty.shells()).data ?? [], value?.location) + // }, + async list(value?: Parameters[0]) { + return located((await legacy(value?.location).pty.list()).data ?? [], value?.location) + }, + async create(value?: Parameters[0]) { + const result = await legacy(value?.location).pty.create({ + command: value?.command, + args: value?.args ? [...value.args] : undefined, + cwd: value?.cwd, + title: value?.title, + env: value?.env, + }) + if (!result.data) throw new Error("Failed to create terminal") + return located(result.data, value?.location) + }, + async get(value: Parameters[0]) { + const result = await legacy(value.location).pty.get({ ptyID: value.ptyID }) + if (!result.data) throw new Error(`Terminal not found: ${value.ptyID}`) + return located(result.data, value.location) + }, + async update(value: Parameters[0]) { + const result = await legacy(value.location).pty.update({ + ptyID: value.ptyID, + title: value.title, + size: value.size, + }) + if (!result.data) throw new Error(`Terminal not found: ${value.ptyID}`) + return located(result.data, value.location) + }, + async remove(value: Parameters[0]) { + await legacy(value.location).pty.remove({ ptyID: value.ptyID }) + }, + // async connectToken(value: Parameters[0]) { + // const result = await legacy(value.location).pty.connectToken({ ptyID: value.ptyID }) + // if (!result.data) throw new Error(`Failed to connect terminal: ${value.ptyID}`) + // return located(result.data, value.location) + // }, + }, + permission: { + ...input.current.permission, + async reply(value: Parameters[0] & { location?: { directory?: string } }) { + await legacy(value.location).permission.respond({ + sessionID: value.sessionID, + permissionID: value.requestID, + response: value.reply, + directory: directory(value.location), + }) + }, + }, + question: { + ...input.current.question, + async reply(value: Parameters[0]) { + await legacy().question.reply({ + requestID: value.requestID, + answers: value.answers.map((answer) => [...answer]), + }) + }, + async reject(value: Parameters[0]) { + await legacy().question.reject({ requestID: value.requestID }) + }, + }, + } +} diff --git a/packages/app/src/utils/server-health.test.ts b/packages/app/src/utils/server-health.test.ts index b1c8f2c7e2..69a8c7b3be 100644 --- a/packages/app/src/utils/server-health.test.ts +++ b/packages/app/src/utils/server-health.test.ts @@ -14,15 +14,45 @@ function abortFromInput(input: RequestInfo | URL, init?: RequestInit) { describe("checkServerHealth", () => { test("returns healthy response with version", async () => { - const fetch = (async () => - new Response(JSON.stringify({ healthy: true, version: "1.2.3" }), { + let request: URL | undefined + const fetch = (async (input: RequestInfo | URL) => { + request = input instanceof URL ? input : new URL(input instanceof Request ? input.url : input) + return new Response(JSON.stringify({ healthy: true, version: "1.2.3" }), { status: 200, headers: { "content-type": "application/json" }, - })) as unknown as typeof globalThis.fetch + }) + }) as unknown as typeof globalThis.fetch const result = await checkServerHealth(server, fetch) expect(result).toEqual({ healthy: true, version: "1.2.3" }) + expect(request?.pathname).toBe("/api/health") + }) + + test("falls back to the V1 health endpoint", async () => { + const paths: string[] = [] + const fetch = (async (input: RequestInfo | URL) => { + const url = input instanceof URL ? input : new URL(input instanceof Request ? input.url : input) + paths.push(url.pathname) + if (url.pathname === "/api/health") return new Response(undefined, { status: 404 }) + return Response.json({ healthy: true, version: "1.18.4" }) + }) as unknown as typeof globalThis.fetch + + expect(await checkServerHealth(server, fetch)).toEqual({ healthy: true, version: "1.18.4" }) + expect(paths).toEqual(["/api/health", "/global/health"]) + }) + + test("falls back when the current health response is malformed", async () => { + const paths: string[] = [] + const fetch = (async (input: RequestInfo | URL) => { + const url = input instanceof URL ? input : new URL(input instanceof Request ? input.url : input) + paths.push(url.pathname) + if (url.pathname === "/api/health") return Response.json({}) + return Response.json({ healthy: true, version: "1.18.4" }) + }) as unknown as typeof globalThis.fetch + + expect(await checkServerHealth(server, fetch)).toEqual({ healthy: true, version: "1.18.4" }) + expect(paths).toEqual(["/api/health", "/global/health"]) }) test("allows slow servers thirty seconds by default", async () => { @@ -142,7 +172,7 @@ describe("checkServerHealth", () => { retryDelayMs: 1, }) - expect(count).toBe(3) + expect(count).toBe(6) expect(result).toEqual({ healthy: false }) }) }) diff --git a/packages/app/src/utils/server-health.ts b/packages/app/src/utils/server-health.ts index 1b684d9af7..1d7d9e4b2e 100644 --- a/packages/app/src/utils/server-health.ts +++ b/packages/app/src/utils/server-health.ts @@ -1,6 +1,7 @@ import { usePlatform } from "@/context/platform" import { ServerConnection } from "@/context/server" -import { createSdkForServer } from "./server" +import { authTokenFromCredentials, createSdkForServer } from "./server" +import { ClientError, OpenCode } from "@opencode-ai/client" import { Accessor, createEffect, onCleanup } from "solid-js" import { createStore, reconcile } from "solid-js/store" @@ -61,6 +62,7 @@ function wait(ms: number, signal?: AbortSignal) { function retryable(error: unknown, signal?: AbortSignal) { if (signal?.aborted) return false + if (error instanceof ClientError) return error.reason === "Transport" if (!(error instanceof Error)) return false if (error.name === "AbortError" || error.name === "TimeoutError") return false if (error instanceof TypeError) return true @@ -82,15 +84,31 @@ export async function checkServerHealth( .then(() => attempt(count + 1)) .catch(() => ({ healthy: false })) } - const attempt = (count: number): Promise => - createSdkForServer({ - server, + const attempt = async (count: number): Promise => { + const current = await OpenCode.make({ + baseUrl: server.url, fetch, - signal, + headers: server.password + ? { + Authorization: `Basic ${authTokenFromCredentials({ username: server.username, password: server.password })}`, + } + : undefined, }) + .health.get({ signal }) + .then((x) => + typeof x.healthy === "boolean" + ? { data: { healthy: x.healthy, version: x.version } } + : { error: new Error("Invalid health response") }, + ) + .catch((error) => ({ error })) + if ("data" in current && current.data) return current.data + if (signal?.aborted) return { healthy: false } + + return createSdkForServer({ server, fetch, signal }) .global.health() .then((x) => (x.error ? next(count, x.error) : { healthy: x.data?.healthy === true, version: x.data?.version })) .catch((error) => next(count, error)) + } return attempt(0).finally(() => timeout?.clear?.()) } diff --git a/packages/app/src/utils/server-protocol.test.ts b/packages/app/src/utils/server-protocol.test.ts new file mode 100644 index 0000000000..2130a968c4 --- /dev/null +++ b/packages/app/src/utils/server-protocol.test.ts @@ -0,0 +1,40 @@ +import { describe, expect, test } from "bun:test" +import { detectServerProtocol } from "./server-protocol" + +const server = { url: "http://localhost:4096" } +const json = (value: unknown, status = 200) => + new Response(JSON.stringify(value), { status, headers: { "content-type": "application/json" } }) +const mockFetch = (run: (input: string | URL | Request) => Promise) => + Object.assign(run, { preconnect: globalThis.fetch.preconnect }) + +describe("detectServerProtocol", () => { + test("prefers the legacy health endpoint when both API generations exist", async () => { + const fetcher = mockFetch((input) => { + const path = new URL(input instanceof Request ? input.url : input).pathname + if (path === "/global/health") return Promise.resolve(json({ healthy: true, version: "1.18.4" })) + return Promise.resolve(json({ healthy: true, version: "2.0.0", pid: 123 })) + }) + + expect(await detectServerProtocol(server, fetcher)).toBe("v1") + }) + + test("recognizes V2 health by its process identifier", async () => { + const fetcher = mockFetch((input) => { + const path = new URL(input instanceof Request ? input.url : input).pathname + if (path === "/global/health") return Promise.resolve(json({}, 404)) + return Promise.resolve(json({ healthy: true, version: "2.0.0", pid: 123 })) + }) + + expect(await detectServerProtocol(server, fetcher)).toBe("v2") + }) + + test("recognizes the transitional V1 API health response", async () => { + const fetcher = mockFetch((input) => { + const path = new URL(input instanceof Request ? input.url : input).pathname + if (path === "/global/health") return Promise.resolve(json({}, 404)) + return Promise.resolve(json({ healthy: true })) + }) + + expect(await detectServerProtocol(server, fetcher)).toBe("v1") + }) +}) diff --git a/packages/app/src/utils/server-protocol.ts b/packages/app/src/utils/server-protocol.ts new file mode 100644 index 0000000000..27b8dc208e --- /dev/null +++ b/packages/app/src/utils/server-protocol.ts @@ -0,0 +1,35 @@ +import type { ServerConnection } from "@/context/server" +import { authTokenFromCredentials } from "./server" + +export type ServerProtocol = "v1" | "v2" + +function headers(server: ServerConnection.HttpBase) { + if (!server.password) return + return { + Authorization: `Basic ${authTokenFromCredentials({ username: server.username, password: server.password })}`, + } +} + +async function probe(server: ServerConnection.HttpBase, fetch: typeof globalThis.fetch, path: string) { + const response = await fetch(new URL(path, server.url), { + headers: headers(server), + signal: AbortSignal.timeout(5_000), + }) + if (!response.ok || !response.headers.get("content-type")?.includes("application/json")) return + const value: unknown = await response.json() + if (!value || typeof value !== "object") return + return value +} + +export async function detectServerProtocol( + server: ServerConnection.HttpBase, + fetch: typeof globalThis.fetch, +): Promise { + const legacy = await probe(server, fetch, "/global/health").catch(() => undefined) + if (legacy && "healthy" in legacy && legacy.healthy === true) return "v1" + + const current = await probe(server, fetch, "/api/health").catch(() => undefined) + if (current && "pid" in current && typeof current.pid === "number") return "v2" + if (current && "healthy" in current && current.healthy === true) return "v1" + return "v2" +} diff --git a/packages/app/src/utils/server.ts b/packages/app/src/utils/server.ts index 603784e4d4..1c8292ca9d 100644 --- a/packages/app/src/utils/server.ts +++ b/packages/app/src/utils/server.ts @@ -1,4 +1,5 @@ import { createOpencodeClient } from "@opencode-ai/sdk/v2/client" +import { OpenCode, type OpenCodeClient } from "@opencode-ai/client/promise" import type { ServerConnection } from "@/context/server" import { decode64 } from "@/utils/base64" @@ -39,3 +40,23 @@ export function createSdkForServer({ baseUrl: server.url, }) } + +export function createApiForServer(input: { + server: ServerConnection.HttpBase + fetch?: typeof globalThis.fetch +}): OpenCodeClient { + return OpenCode.make({ + baseUrl: input.server.url, + fetch: input.fetch, + headers: input.server.password + ? { + Authorization: `Basic ${authTokenFromCredentials({ + username: input.server.username, + password: input.server.password, + })}`, + } + : undefined, + }) +} + +export type ServerApi = OpenCodeClient diff --git a/packages/app/src/utils/session-message.test.ts b/packages/app/src/utils/session-message.test.ts new file mode 100644 index 0000000000..a69c414e16 --- /dev/null +++ b/packages/app/src/utils/session-message.test.ts @@ -0,0 +1,214 @@ +import { describe, expect, test } from "bun:test" +import type { SessionMessageInfo } from "@opencode-ai/client/promise" +import { normalizeSessionMessages } from "./session-message" + +describe("normalizeSessionMessages", () => { + test("projects current turns into stable legacy rendering records", () => { + const source = [ + { id: "msg_1", type: "agent-switched", agent: "build", time: { created: 1 } }, + { + id: "msg_2", + type: "model-switched", + model: { id: "claude", providerID: "anthropic", variant: "high" }, + time: { created: 2 }, + }, + { + id: "msg_3", + type: "user", + text: "inspect @src/client.ts", + files: [ + { + data: "aGVsbG8=", + mime: "text/plain", + name: "note.txt", + source: { type: "inline" }, + }, + { + data: "ZXhwb3J0IHt9", + mime: "text/plain", + name: "client.ts", + source: { type: "inline" }, + mention: { text: "@src/client.ts", start: 8, end: 22 }, + }, + ], + agents: [{ name: "review", mention: { text: "@review", start: 0, end: 7 } }], + time: { created: 3 }, + }, + { + id: "msg_4", + type: "assistant", + agent: "build", + model: { id: "claude", providerID: "anthropic", variant: "high" }, + content: [ + { type: "reasoning", text: "Thinking", time: { created: 4, completed: 5 } }, + { type: "text", text: "Result" }, + { + type: "tool", + id: "call_1", + name: "read", + state: { + status: "completed", + input: { filePath: "note.txt" }, + metadata: { title: "note.txt" }, + content: [{ type: "text", text: "hello" }], + }, + time: { created: 5, ran: 6, completed: 7 }, + }, + ], + cost: 0.1, + tokens: { input: 10, output: 5, reasoning: 2, cache: { read: 1, write: 0 } }, + time: { created: 4, completed: 7 }, + }, + { + id: "msg_5", + type: "compaction", + status: "completed", + reason: "auto", + summary: "summary", + recent: "recent", + time: { created: 8 }, + }, + ] satisfies SessionMessageInfo[] + + const result = normalizeSessionMessages("ses_1", source) + + expect(result.messages).toHaveLength(2) + expect(result.messages[0]).toMatchObject({ + id: "msg_3", + role: "user", + agent: "build", + model: { providerID: "anthropic", modelID: "claude", variant: "high" }, + }) + expect(result.messages[1]).toMatchObject({ id: "msg_4", role: "assistant", parentID: "msg_3", cost: 0.1 }) + expect(result.parts.get("msg_3")?.map((part) => part.id)).toEqual([ + "msg_3:text:0", + "msg_3:file:0", + "msg_3:file:1", + "msg_3:agent:0", + "msg_5:compaction", + ]) + expect(result.parts.get("msg_3")?.[2]).toMatchObject({ + type: "file", + source: { + type: "file", + path: "src/client.ts", + text: { value: "@src/client.ts", start: 8, end: 22 }, + }, + }) + expect(result.parts.get("msg_4")?.map((part) => part.id)).toEqual(["msg_4:reasoning:0", "msg_4:text:0", "call_1"]) + expect(result.parts.get("msg_4")?.[2]).toMatchObject({ + type: "tool", + tool: "read", + state: { status: "completed", output: "hello" }, + }) + }) + + test("does not invent a parent for an assistant-only page", () => { + const source = [ + { + id: "msg_2", + type: "assistant", + agent: "build", + model: { id: "model", providerID: "provider" }, + content: [{ type: "text", text: "orphan" }], + time: { created: 2 }, + }, + ] satisfies SessionMessageInfo[] + + expect(normalizeSessionMessages("ses_1", source).messages).toEqual([]) + }) + + test("projects a current shell message into a renderable standalone turn", () => { + const source = [ + { + id: "msg_shell", + type: "shell", + shellID: "shell_1", + command: "printf hello", + status: "exited", + exit: 0, + output: { output: "hello", cursor: 5, size: 5, truncated: false }, + time: { created: 1, completed: 2 }, + }, + ] satisfies SessionMessageInfo[] + + const result = normalizeSessionMessages("ses_1", source) + + expect(result.messages).toEqual([ + expect.objectContaining({ id: "msg_shell", role: "user" }), + expect.objectContaining({ id: "msg_shell:assistant", role: "assistant", parentID: "msg_shell" }), + ]) + expect(result.parts.get("msg_shell")).toEqual([expect.objectContaining({ type: "text", text: "printf hello" })]) + expect(result.parts.get("msg_shell:assistant")).toEqual([ + expect.objectContaining({ + type: "tool", + tool: "bash", + state: expect.objectContaining({ + status: "completed", + input: { command: "printf hello" }, + output: "hello", + title: "Shell", + }), + }), + ]) + }) + + test("adapts current edit fields for the legacy edit renderer", () => { + const source = [ + { id: "msg_user", type: "user", text: "edit it", time: { created: 1 } }, + { + id: "msg_assistant", + type: "assistant", + agent: "build", + model: { id: "model", providerID: "provider" }, + content: [ + { + type: "tool", + id: "call_edit", + name: "edit", + state: { + status: "completed", + input: { path: "/repo/README.md", oldString: "old", newString: "new" }, + content: [{ type: "text", text: "Edited file successfully" }], + metadata: { + files: [ + { + file: "README.md", + patch: "@@ -1 +1 @@\n-old\n+new", + additions: 1, + deletions: 1, + status: "modified", + }, + ], + replacements: 1, + }, + }, + time: { created: 2, ran: 3, completed: 4 }, + }, + ], + time: { created: 2, completed: 4 }, + }, + ] satisfies SessionMessageInfo[] + + const result = normalizeSessionMessages("ses_1", source) + + expect(result.parts.get("msg_assistant")).toEqual([ + expect.objectContaining({ + type: "tool", + tool: "edit", + state: expect.objectContaining({ + status: "completed", + input: expect.objectContaining({ path: "/repo/README.md", filePath: "/repo/README.md" }), + metadata: expect.objectContaining({ + filediff: { + file: "README.md", + patch: "@@ -1 +1 @@\n-old\n+new", + additions: 1, + deletions: 1, + }, + }), + }), + }), + ]) + }) +}) diff --git a/packages/app/src/utils/session-message.ts b/packages/app/src/utils/session-message.ts new file mode 100644 index 0000000000..93d86a66bb --- /dev/null +++ b/packages/app/src/utils/session-message.ts @@ -0,0 +1,358 @@ +import type { + SessionMessageAssistant, + SessionMessageAssistantTool, + SessionMessageInfo, + SessionMessageShell, + SessionMessageUser, +} from "@opencode-ai/client/promise" +import type { AssistantMessage, FilePart, Message, Part, ToolPart, UserMessage } from "@opencode-ai/sdk/v2" +import { Option, Schema } from "effect" + +const emptyTokens = { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } } +const emptyModel: { id: string; providerID: string; variant?: string } = { id: "", providerID: "" } +const decodeToolInput = Schema.decodeUnknownOption(Schema.UnknownFromJsonString) + +function record(value: unknown): value is Record { + return !!value && typeof value === "object" && !Array.isArray(value) +} + +function normalizeToolInput(name: string, input: Record) { + if (!["edit", "write"].includes(name) || typeof input.path !== "string" || typeof input.filePath === "string") + return input + return { ...input, filePath: input.path } +} + +function normalizeToolMetadata(name: string, metadata: Record) { + if (name !== "edit" || !Array.isArray(metadata.files)) return metadata + const file = metadata.files.find(record) + if (!file || typeof file.file !== "string") return metadata + return { + ...metadata, + filediff: { + file: file.file, + patch: typeof file.patch === "string" ? file.patch : undefined, + additions: typeof file.additions === "number" ? file.additions : 0, + deletions: typeof file.deletions === "number" ? file.deletions : 0, + }, + } +} + +export function normalizeSessionMessages(sessionID: string, source: readonly SessionMessageInfo[]) { + const messages: Message[] = [] + const parts = new Map() + let agent = "" + let model = emptyModel + let parentID: string | undefined + + source.forEach((message) => { + if (message.type === "agent-switched") { + agent = message.agent + return + } + if (message.type === "model-switched") { + model = message.model + return + } + if (message.type === "user") { + parentID = message.id + messages.push(userMessage(sessionID, message, agent, model)) + parts.set(message.id, userParts(sessionID, message)) + return + } + if (message.type === "synthetic" && message.description?.trim()) { + parentID = message.id + messages.push({ + id: message.id, + sessionID, + role: "user", + time: message.time, + agent, + model: { providerID: model.providerID, modelID: model.id, variant: model.variant }, + }) + parts.set(message.id, [textPart(sessionID, message.id, 0, message.description, true)]) + return + } + if (message.type === "shell") { + messages.push(...shellMessages(sessionID, message, agent, model)) + parts.set(message.id, [textPart(sessionID, message.id, 0, message.command)]) + parts.set(`${message.id}:assistant`, [shellPart(sessionID, message)]) + parentID = undefined + return + } + if (message.type === "assistant") { + agent = message.agent + model = message.model + if (!parentID) return + const parent = messages.findLast((item) => item.id === parentID) + if (parent?.role === "user") { + parent.agent = message.agent + parent.model = { + providerID: message.model.providerID, + modelID: message.model.id, + variant: message.model.variant, + } + } + messages.push(assistantMessage(sessionID, parentID, message)) + parts.set(message.id, assistantParts(sessionID, message)) + return + } + if (message.type !== "compaction" || !parentID) return + parts.set(parentID, [ + ...(parts.get(parentID) ?? []), + { + id: `${message.id}:compaction`, + sessionID, + messageID: parentID, + type: "compaction", + auto: message.reason === "auto", + }, + ]) + }) + + return { messages, parts } +} + +function shellMessages( + sessionID: string, + message: SessionMessageShell, + agent: string, + model: { id: string; providerID: string; variant?: string }, +): [UserMessage, AssistantMessage] { + return [ + { + id: message.id, + sessionID, + role: "user", + time: { created: message.time.created }, + agent, + model: { providerID: model.providerID, modelID: model.id, variant: model.variant }, + }, + { + id: `${message.id}:assistant`, + sessionID, + role: "assistant", + time: message.time, + parentID: message.id, + modelID: model.id, + providerID: model.providerID, + variant: model.variant, + mode: agent, + agent, + path: { cwd: "", root: "" }, + cost: 0, + tokens: emptyTokens, + }, + ] +} + +function shellPart(sessionID: string, message: SessionMessageShell): ToolPart { + const input = { command: message.command } + const start = message.time.created + const state: ToolPart["state"] = + message.status === "running" + ? { status: "running", input, time: { start } } + : { + status: "completed", + input, + output: message.output?.output ?? "", + title: "Shell", + metadata: { + status: message.status, + exit: message.exit, + truncated: message.output?.truncated, + }, + time: { start, end: message.time.completed ?? start }, + } + return { + id: `${message.id}:tool`, + sessionID, + messageID: `${message.id}:assistant`, + type: "tool", + callID: message.shellID, + tool: "bash", + state, + } +} + +export function sessionMessagePartID(messageID: string, type: "text" | "reasoning", ordinal: number) { + return `${messageID}:${type}:${ordinal}` +} + +function userMessage( + sessionID: string, + message: SessionMessageUser, + agent: string, + model: { id: string; providerID: string; variant?: string }, +): UserMessage { + return { + id: message.id, + sessionID, + role: "user", + time: message.time, + agent, + model: { providerID: model.providerID, modelID: model.id, variant: model.variant }, + } +} + +function userParts(sessionID: string, message: SessionMessageUser): Part[] { + return [ + textPart(sessionID, message.id, 0, message.text), + ...(message.files ?? []).map( + (file, index): FilePart => ({ + id: `${message.id}:file:${index}`, + sessionID, + messageID: message.id, + type: "file", + mime: file.mime, + filename: file.name, + url: file.source.type === "uri" ? file.source.uri : `data:${file.mime};base64,${file.data}`, + source: file.mention + ? { + type: "file", + text: { value: file.mention.text, start: file.mention.start, end: file.mention.end }, + path: file.mention.text.startsWith("@") ? file.mention.text.slice(1) : (file.name ?? file.mention.text), + } + : undefined, + }), + ), + ...(message.agents ?? []).map( + (item, index): Part => ({ + id: `${message.id}:agent:${index}`, + sessionID, + messageID: message.id, + type: "agent", + name: item.name, + source: item.mention + ? { value: item.mention.text, start: item.mention.start, end: item.mention.end } + : undefined, + }), + ), + ] +} + +function assistantMessage(sessionID: string, parentID: string, message: SessionMessageAssistant): AssistantMessage { + const error = message.error + ? message.error.type.toLowerCase().includes("abort") || message.error.type.toLowerCase().includes("interrupt") + ? { name: "MessageAbortedError" as const, data: { message: message.error.message } } + : { name: "UnknownError" as const, data: { message: message.error.message } } + : undefined + return { + id: message.id, + sessionID, + role: "assistant", + time: message.time, + error, + parentID, + modelID: message.model.id, + providerID: message.model.providerID, + variant: message.model.variant, + mode: message.agent, + agent: message.agent, + path: { cwd: "", root: "" }, + cost: message.cost ?? 0, + tokens: message.tokens ?? emptyTokens, + finish: message.finish, + } +} + +function assistantParts(sessionID: string, message: SessionMessageAssistant): Part[] { + const ordinals = { text: 0, reasoning: 0 } + return message.content.flatMap((content): Part[] => { + if (content.type === "text") { + const part = textPart(sessionID, message.id, ordinals.text++, content.text) + return content.text.trim() ? [part] : [] + } + if (content.type === "reasoning") { + const part: Part = { + id: sessionMessagePartID(message.id, "reasoning", ordinals.reasoning++), + sessionID, + messageID: message.id, + type: "reasoning", + text: content.text, + metadata: content.state, + time: { + start: content.time?.created ?? message.time.created, + end: content.time?.completed, + }, + } + return content.text.trim() ? [part] : [] + } + return [toolPart(sessionID, message.id, content)] + }) +} + +function textPart(sessionID: string, messageID: string, ordinal: number, text: string, synthetic?: boolean): Part { + return { + id: sessionMessagePartID(messageID, "text", ordinal), + sessionID, + messageID, + type: "text", + text, + synthetic, + } +} + +function toolPart(sessionID: string, messageID: string, tool: SessionMessageAssistantTool): ToolPart { + const start = tool.time.ran ?? tool.time.created + const state = (() => { + if (tool.state.status === "streaming") { + const value = Option.getOrUndefined(decodeToolInput(tool.state.input)) + const input = normalizeToolInput(tool.name, record(value) ? value : {}) + return { status: "pending" as const, input, raw: tool.state.input } + } + if (tool.state.status === "running") { + return { + status: "running" as const, + input: normalizeToolInput(tool.name, tool.state.input), + // metadata: normalizeToolMetadata(tool.name, tool.state.structured), + metadata: normalizeToolMetadata(tool.name, tool.state.metadata ?? {}), + time: { start }, + } + } + if (tool.state.status === "error") { + return { + status: "error" as const, + input: normalizeToolInput(tool.name, tool.state.input), + error: tool.state.error.message, + // metadata: normalizeToolMetadata(tool.name, tool.state.structured), + metadata: normalizeToolMetadata(tool.name, tool.state.metadata ?? {}), + time: { start, end: tool.time.completed ?? start }, + } + } + const attachments = tool.state.content.flatMap((item, index): FilePart[] => + item.type === "file" + ? [ + { + id: `${tool.id}:file:${index}`, + sessionID, + messageID, + type: "file", + mime: item.mime, + filename: item.name, + url: item.uri, + }, + ] + : [], + ) + return { + status: "completed" as const, + input: normalizeToolInput(tool.name, tool.state.input), + output: tool.state.content.flatMap((item) => (item.type === "text" ? [item.text] : [])).join("\n"), + title: tool.name, + // metadata: normalizeToolMetadata(tool.name, tool.state.structured), + metadata: normalizeToolMetadata(tool.name, tool.state.metadata ?? {}), + time: { start, end: tool.time.completed ?? start }, + attachments: attachments.length ? attachments : undefined, + } + })() + return { + id: tool.id, + sessionID, + messageID, + type: "tool", + callID: tool.id, + tool: tool.name, + state, + metadata: { providerState: tool.providerState, providerResultState: tool.providerResultState }, + } +} diff --git a/packages/app/src/utils/session.test.ts b/packages/app/src/utils/session.test.ts new file mode 100644 index 0000000000..b15c23b660 --- /dev/null +++ b/packages/app/src/utils/session.test.ts @@ -0,0 +1,94 @@ +import { describe, expect, test } from "bun:test" +import type { SessionApi, SessionInfo, SessionListInput } from "@opencode-ai/client/promise" +import { listAllSessions, normalizeSessionInfo } from "./session" + +describe("normalizeSessionInfo", () => { + test("adapts a current session to the app session shape", () => { + const result = normalizeSessionInfo({ + id: "session-1", + projectID: "project-1", + agent: "build", + model: { id: "gpt-5", providerID: "openai", variant: "high" }, + cost: 0, + tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } }, + time: { created: 1, updated: 1 }, + title: "New session", + location: { directory: "/repo/worktree", workspaceID: "workspace-1" }, + subpath: "worktree", + revert: { messageID: "message-1", partID: "part-1", snapshot: "snapshot", files: [] }, + } as SessionInfo) + + expect(result).toEqual({ + id: "session-1", + slug: "session-1", + projectID: "project-1", + workspaceID: "workspace-1", + directory: "/repo/worktree", + path: "worktree", + parentID: undefined, + cost: 0, + tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } }, + title: "New session", + agent: "build", + model: { id: "gpt-5", providerID: "openai", variant: "high" }, + version: "", + time: { created: 1, updated: 1 }, + revert: { messageID: "message-1", partID: "part-1", snapshot: "snapshot" }, + }) + }) +}) + +describe("listAllSessions", () => { + test("loads every page in server order and retains the query", async () => { + const calls: SessionListInput[] = [] + const pages = new Map([ + [undefined, { data: [sessionInfo("session-3"), sessionInfo("session-2")], cursor: { next: "next" } }], + ["next", { data: [sessionInfo("session-1", true)], cursor: {} }], + ]) + const api = { + list: async (query = {}) => { + calls.push(query) + return pages.get(query.cursor) ?? { data: [], cursor: {} } + }, + } satisfies Pick + + const result = await listAllSessions(api, { directory: "/repo", order: "desc" }) + + expect(result.map((session) => session.id)).toEqual(["session-3", "session-2", "session-1"]) + expect(result[2]?.time.archived).toBe(2) + expect(calls).toEqual([ + { directory: "/repo", order: "desc", limit: 100, cursor: undefined }, + { directory: "/repo", order: "desc", limit: 100, cursor: "next" }, + ]) + }) + + test("requests the terminal empty page when the server returns a next cursor", async () => { + const cursors: Array = [] + const api = { + list: async (query = {}) => { + cursors.push(query.cursor) + if (query.cursor) return { data: [], cursor: { next: "unused" } } + return { data: [sessionInfo("session-1")], cursor: { next: "terminal" } } + }, + } satisfies Pick + + const result = await listAllSessions(api, { directory: "/repo", limit: 25 }) + + expect(result.map((session) => session.id)).toEqual(["session-1"]) + expect(cursors).toEqual([undefined, "terminal"]) + }) +}) + +function sessionInfo(id: string, archived = false) { + return { + id, + projectID: "project-1", + agent: "build", + model: { id: "model-1", providerID: "provider-1" }, + cost: 0, + tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } }, + time: { created: 1, updated: 1, archived: archived ? 2 : undefined }, + title: id, + location: { directory: "/repo" }, + } as SessionInfo +} diff --git a/packages/app/src/utils/session.ts b/packages/app/src/utils/session.ts new file mode 100644 index 0000000000..faf847967b --- /dev/null +++ b/packages/app/src/utils/session.ts @@ -0,0 +1,37 @@ +import type { SessionApi, SessionInfo, SessionListInput } from "@opencode-ai/client/promise" +import type { Session } from "@opencode-ai/sdk/v2/client" + +export function normalizeSessionInfo(input: SessionInfo | Session): Session { + if (!("location" in input)) return input + return { + id: input.id, + slug: input.id, + projectID: input.projectID, + workspaceID: input.location.workspaceID, + directory: input.location.directory, + path: input.subpath, + parentID: input.parentID, + cost: input.cost, + tokens: input.tokens, + title: input.title, + agent: input.agent, + model: input.model, + version: "", + time: input.time, + revert: input.revert && { + messageID: input.revert.messageID, + partID: input.revert.partID, + snapshot: input.revert.snapshot, + }, + } +} + +export async function listAllSessions(api: Pick, input: Omit) { + const load = async (cursor?: string): Promise => { + const result = await api.list({ ...input, limit: input.limit ?? 100, cursor }) + const sessions = result.data.map(normalizeSessionInfo) + if (result.data.length === 0 || !result.cursor.next) return sessions + return [...sessions, ...(await load(result.cursor.next))] + } + return load() +} diff --git a/packages/app/src/utils/terminal-websocket-url.test.ts b/packages/app/src/utils/terminal-websocket-url.test.ts index 5fa1506b1e..aac854ca82 100644 --- a/packages/app/src/utils/terminal-websocket-url.test.ts +++ b/packages/app/src/utils/terminal-websocket-url.test.ts @@ -2,8 +2,28 @@ import { describe, expect, test } from "bun:test" import { terminalWebSocketURL } from "./terminal-websocket-url" describe("terminalWebSocketURL", () => { - test("uses query auth without embedding credentials in websocket URL", () => { + test("uses the current ticketed PTY route", () => { const url = terminalWebSocketURL({ + url: "http://127.0.0.1:49365", + id: "pty_test", + directory: "/tmp/project", + cursor: 0, + ticket: "connect-ticket", + }) + + expect(url.protocol).toBe("ws:") + expect(url.username).toBe("") + expect(url.password).toBe("") + expect(url.pathname).toBe("/api/pty/pty_test/connect") + expect(url.searchParams.get("location[directory]")).toBe("/tmp/project") + expect(url.searchParams.get("cursor")).toBe("0") + expect(url.searchParams.get("ticket")).toBe("connect-ticket") + expect(url.searchParams.has("auth_token")).toBe(false) + }) + + test("uses query auth without embedding credentials in websocket URL for v1", () => { + const url = terminalWebSocketURL({ + protocol: "v1", url: "http://127.0.0.1:49365", id: "pty_test", directory: "/tmp/project", @@ -16,11 +36,14 @@ describe("terminalWebSocketURL", () => { expect(url.protocol).toBe("ws:") expect(url.username).toBe("") expect(url.password).toBe("") + expect(url.pathname).toBe("/pty/pty_test/connect") + expect(url.searchParams.get("directory")).toBe("/tmp/project") expect(url.searchParams.get("auth_token")).toBe(btoa("opencode:secret")) }) - test("omits query auth for same-origin saved credentials", () => { + test("omits query auth for same-origin saved credentials for v1", () => { const url = terminalWebSocketURL({ + protocol: "v1", url: "https://app.example.test", id: "pty_test", directory: "/tmp/project", @@ -31,11 +54,14 @@ describe("terminalWebSocketURL", () => { }) expect(url.protocol).toBe("wss:") + expect(url.pathname).toBe("/pty/pty_test/connect") + expect(url.searchParams.get("directory")).toBe("/tmp/project") expect(url.searchParams.has("auth_token")).toBe(false) }) - test("uses query auth for same-origin credentials from auth_token", () => { + test("uses query auth for same-origin credentials from auth_token for v1", () => { const url = terminalWebSocketURL({ + protocol: "v1", url: "https://app.example.test", id: "pty_test", directory: "/tmp/project", @@ -47,6 +73,8 @@ describe("terminalWebSocketURL", () => { }) expect(url.protocol).toBe("wss:") + expect(url.pathname).toBe("/pty/pty_test/connect") + expect(url.searchParams.get("directory")).toBe("/tmp/project") expect(url.searchParams.get("auth_token")).toBe(btoa("opencode:secret")) }) }) diff --git a/packages/app/src/utils/terminal-websocket-url.ts b/packages/app/src/utils/terminal-websocket-url.ts index 06facdc7d2..a32b239cc9 100644 --- a/packages/app/src/utils/terminal-websocket-url.ts +++ b/packages/app/src/utils/terminal-websocket-url.ts @@ -1,6 +1,7 @@ import { authTokenFromCredentials } from "@/utils/server" export function terminalWebSocketURL(input: { + protocol?: "v1" | "v2" url: string id: string directory: string @@ -11,18 +12,24 @@ export function terminalWebSocketURL(input: { password?: string authToken?: boolean }) { - const next = new URL(`${input.url}/pty/${input.id}/connect`) - next.searchParams.set("directory", input.directory) + const isV1 = input.protocol === "v1" + const next = new URL(`${input.url}${isV1 ? `/pty/${input.id}/connect` : `/api/pty/${input.id}/connect`}`) + if (isV1) { + next.searchParams.set("directory", input.directory) + } else { + next.searchParams.set("location[directory]", input.directory) + } next.searchParams.set("cursor", String(input.cursor)) next.protocol = next.protocol === "https:" ? "wss:" : "ws:" if (input.ticket) { next.searchParams.set("ticket", input.ticket) return next } - if (input.password && (!input.sameOrigin || input.authToken)) + if (isV1 && input.password && (!input.sameOrigin || input.authToken)) { next.searchParams.set( "auth_token", authTokenFromCredentials({ username: input.username, password: input.password }), ) + } return next } diff --git a/packages/app/src/utils/toast.tsx b/packages/app/src/utils/toast.tsx index e444548508..6f23b63d1e 100644 --- a/packages/app/src/utils/toast.tsx +++ b/packages/app/src/utils/toast.tsx @@ -1,6 +1,12 @@ import { Icon, type IconProps } from "@opencode-ai/ui/icon" -import { Toast, showToast as showLegacyToast, type ToastOptions, type ToastVariant } from "@opencode-ai/ui/toast" -import { ToastV2, showToastV2 } from "@opencode-ai/ui/v2/toast-v2" +import { + Toast, + showToast as showLegacyToast, + toaster as legacyToaster, + type ToastOptions, + type ToastVariant, +} from "@opencode-ai/ui/toast" +import { ToastV2, showToastV2, toasterV2 } from "@opencode-ai/ui/v2/toast-v2" let v2 = false @@ -27,6 +33,13 @@ export function showToast(options: ToastOptions | string) { }) } +// v1 and v2 ids come from separate registries, so dismissal has to use the same +// implementation that issued the id. +export function dismissToast(toastId: number) { + if (!v2) return legacyToaster.dismiss(toastId) + return toasterV2.dismiss(toastId) +} + function resolveIcon(icon: IconProps["name"] | undefined, variant: ToastVariant | undefined) { const name = icon ?? (variant === "success" ? "check" : undefined) if (!name) return diff --git a/packages/app/test-browser/command-palette.test.ts b/packages/app/test-browser/command-palette.test.ts new file mode 100644 index 0000000000..6a74834fd0 --- /dev/null +++ b/packages/app/test-browser/command-palette.test.ts @@ -0,0 +1,78 @@ +import { describe, expect, test } from "bun:test" +import type { Project } from "@opencode-ai/sdk/v2/client" +import type { SessionInfo } from "@opencode-ai/client/promise" +import { createRoot } from "solid-js" +import { createServerSessionEntries } from "@/components/command-palette" +import type { LocalProject } from "@/context/layout" +import { ServerConnection } from "@/context/server" +import { getProjectAvatarSource } from "@/pages/layout/helpers" + +const stored: Project = { + id: "project-1", + name: "Palette project", + worktree: "/workspace/project", + sandboxes: [], + time: { created: 1, updated: 1 }, +} + +const session: SessionInfo = { + id: "session-1", + projectID: stored.id, + agent: "build", + model: { id: "model-1", providerID: "provider-1" }, + cost: 0, + tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } }, + location: { directory: stored.worktree }, + title: "Palette session", + time: { created: 1, updated: 2 }, +} + +describe("command palette sessions", () => { + test("uses the home project avatar and cancels superseded searches", async () => { + const server = ServerConnection.Key.make("selected-server") + const opened: LocalProject = { + ...stored, + icon: { override: "home-project-avatar" }, + expanded: true, + } + const searches: string[] = [] + const result = await new Promise>>>( + (resolve, reject) => { + createRoot((dispose) => { + const search = createServerSessionEntries({ + server, + opened: () => [opened], + stored: () => [{ ...stored, icon: { url: "stored-project-avatar" } }], + load: async (text) => { + searches.push(text) + return { + data: [session, { ...session, id: "archived-session", time: { ...session.time, archived: 3 } }], + } + }, + untitled: () => "Untitled", + category: () => "Sessions", + }) + const first = search("palette") + const second = search("palette session") + Promise.all([first, second]) + .then(([cancelled, entries]) => { + expect(cancelled).toEqual([]) + resolve(entries) + }) + .catch(reject) + .finally(dispose) + }) + }, + ) + + expect(searches).toEqual(["palette session"]) + expect(result).toHaveLength(1) + expect(getProjectAvatarSource(result[0]?.project?.id, result[0]?.project?.icon)).toBe("home-project-avatar") + expect(result[0]).toMatchObject({ + server, + sessionID: session.id, + description: stored.name, + project: { id: stored.id, icon: opened.icon }, + }) + }) +}) diff --git a/packages/app/test-browser/prompt-attachments.test.ts b/packages/app/test-browser/prompt-attachments.test.ts index ca7686598e..49dad279cd 100644 --- a/packages/app/test-browser/prompt-attachments.test.ts +++ b/packages/app/test-browser/prompt-attachments.test.ts @@ -1,7 +1,10 @@ import { describe, expect, test } from "bun:test" import { createRoot } from "solid-js" +import { createStore } from "solid-js/store" import { createPromptAttachmentsCore } from "@/components/prompt-input/attachments" import { createPromptState } from "@/context/prompt" +import { createPromptInputV2Attachments } from "../../session-ui/src/v2/components/prompt-input/attachments" +import type { PromptInputV2Prompt } from "../../session-ui/src/v2/components/prompt-input/types" describe("prompt attachment session ownership", () => { test("adds an asynchronously read image to the session where the read started", async () => { @@ -85,6 +88,88 @@ describe("prompt attachment session ownership", () => { }) }) +test("rejects a duplicate native clipboard attachment in the V2 prompt store", async () => { + await createRoot(async (dispose) => { + const [state, setState] = createStore({ prompt: [] as PromptInputV2Prompt }) + const duplicate = Promise.withResolvers() + const files = [ + new File(["hello"], "clipboard-1.txt", { type: "text/plain" }), + new File(["hello"], "clipboard-2.txt", { type: "text/plain" }), + ] + const attachments = createPromptInputV2Attachments({ + capture: () => ({ + current: () => state.prompt, + cursor: () => 0, + set: (prompt) => setState("prompt", prompt), + }), + editor: () => document.createElement("div"), + focusEditor: () => undefined, + addPart: () => false, + setDraggingType: () => undefined, + directory: () => "/", + isDialogActive: () => false, + warn: () => undefined, + duplicate: duplicate.resolve, + onError: () => undefined, + readClipboardImage: async () => files.shift() ?? null, + }) + const event = { + clipboardData: { items: [], getData: () => "" }, + preventDefault: () => undefined, + stopPropagation: () => undefined, + } as unknown as ClipboardEvent + + await attachments.handlePaste(event) + await attachments.handlePaste(event) + await duplicate.promise + + expect(state.prompt).toHaveLength(1) + dispose() + }) +}) + +test("rejects desktop duplicates and keeps changed files in the V2 prompt store", async () => { + await createRoot(async (dispose) => { + const [state, setState] = createStore({ prompt: [] as PromptInputV2Prompt }) + const duplicates: string[] = [] + const attachments = createPromptInputV2Attachments({ + capture: () => ({ + current: () => state.prompt, + cursor: () => 0, + set: (prompt) => setState("prompt", prompt), + }), + editor: () => document.createElement("div"), + focusEditor: () => undefined, + addPart: () => false, + setDraggingType: () => undefined, + directory: () => "/", + isDialogActive: () => false, + warn: () => undefined, + duplicate: () => duplicates.push("duplicate"), + onError: () => undefined, + getPathForFile: (file) => (file.name === "browser.txt" ? "" : `/tmp/${file.name}`), + }) + const first = new File(["first"], "a.txt", { type: "text/plain" }) + const second = new File(["second"], "b.txt", { type: "text/plain" }) + + await attachments.addAttachments([first, second]) + await attachments.addAttachments([first, second]) + expect(state.prompt).toHaveLength(2) + expect(duplicates).toEqual(["duplicate", "duplicate"]) + + await attachments.addAttachments([new File(["edited"], "a.txt", { type: "text/plain" })]) + expect(state.prompt).toHaveLength(3) + + await attachments.addAttachments([ + new File(["same"], "browser.txt", { type: "text/plain" }), + new File(["same"], "browser.txt", { type: "text/plain" }), + ]) + expect(state.prompt).toHaveLength(4) + expect(duplicates).toHaveLength(3) + dispose() + }) +}) + function images(prompt: ReturnType) { return prompt.current().filter((part) => part.type === "image") } diff --git a/packages/app/test-browser/prompt-persistence.test.ts b/packages/app/test-browser/prompt-persistence.test.ts index a7b08078d7..f3d8f8ae4f 100644 --- a/packages/app/test-browser/prompt-persistence.test.ts +++ b/packages/app/test-browser/prompt-persistence.test.ts @@ -2,6 +2,7 @@ import { beforeAll, describe, expect, mock, test } from "bun:test" import type { AsyncStorage } from "@solid-primitives/storage" import { createEffect, createRoot } from "solid-js" import { ServerScope } from "@/utils/server-scope" +import { createDraftStore } from "@/utils/draft-store" let Prompt: typeof import("@/context/prompt") let read: ((value: string | null) => void) | undefined @@ -30,7 +31,7 @@ beforeAll(async () => { }), })) mock.module("@/context/platform", () => ({ - usePlatform: () => ({ platform: "desktop", storage: () => storage }), + usePlatform: () => ({ platform: "desktop", storage: () => storage, draftStore: storage }), })) Prompt = await import("@/context/prompt") @@ -69,3 +70,50 @@ describe("prompt persistence", () => { }) }) }) + +test("moves legacy image data URLs into blobs and hydrates object URLs", async () => { + const documents = new Map() + const blobs = new Map() + const store = createDraftStore({ + get: async (key) => documents.get(key) ?? null, + set: async (key, value) => void documents.set(key, value), + remove: async (key) => void documents.delete(key), + putBlob: async (blob) => { + const id = String(blob.size) + blobs.set(id, blob) + return id + }, + getBlob: async (id) => blobs.get(id) ?? null, + }) + + await store.setItem("prompt", JSON.stringify({ prompt: [{ type: "image", dataUrl: "data:image/png;base64,YQ==" }] })) + expect(documents.get("prompt")).not.toContain("dataUrl") + const value = JSON.parse((await store.getItem("prompt"))!) + expect(value.prompt[0].blob.id).toBe("1") + expect(value.prompt[0].blob.url).toStartWith("blob:") +}) + +test("does not let delayed blob migration overwrite a newer draft", async () => { + const documents = new Map() + const migration = Promise.withResolvers() + const store = createDraftStore({ + get: async () => null, + set: async (key, value) => void documents.set(key, value), + remove: async () => undefined, + putBlob: async () => { + await migration.promise + return "blob" + }, + getBlob: async () => null, + }) + const older = store.setItem( + "prompt", + JSON.stringify({ prompt: [{ type: "image", dataUrl: "data:image/png;base64,YQ==" }] }), + ) + await Bun.sleep(0) + await store.setItem("prompt", JSON.stringify({ prompt: [{ type: "text", content: "latest" }] })) + migration.resolve() + await older + + expect(documents.get("prompt")).toContain("latest") +}) diff --git a/packages/app/test-browser/prompt-transient-state.test.ts b/packages/app/test-browser/prompt-transient-state.test.ts index 61b5c6a14f..5777c9c392 100644 --- a/packages/app/test-browser/prompt-transient-state.test.ts +++ b/packages/app/test-browser/prompt-transient-state.test.ts @@ -18,7 +18,6 @@ test("resets transient prompt input state when the prompt session changes", () = draggingType: "image", mode: "shell", applyingHistory: true, - variantOpen: true, }) setIdentity("B") @@ -33,7 +32,6 @@ test("resets transient prompt input state when the prompt session changes", () = draggingType: null, mode: "normal", applyingHistory: false, - variantOpen: false, }) dispose() }) diff --git a/packages/app/test-browser/review-panel-v2-state.test.ts b/packages/app/test-browser/review-panel-v2-state.test.ts new file mode 100644 index 0000000000..2a7862ce17 --- /dev/null +++ b/packages/app/test-browser/review-panel-v2-state.test.ts @@ -0,0 +1,65 @@ +import { beforeAll, expect, mock, test } from "bun:test" +import type { AsyncStorage } from "@solid-primitives/storage" +import { createEffect, createRoot } from "solid-js" + +let createReviewPanelV2State: typeof import("@/pages/session/v2/review-panel-v2-state").createReviewPanelV2State +let read: ((value: string | null) => void) | undefined + +const storage: AsyncStorage = { + getItem: () => new Promise((resolve) => (read = resolve)), + setItem: async () => undefined, + removeItem: async () => undefined, + clear: async () => undefined, + key: async () => null, + getLength: async () => 0, + length: Promise.resolve(0), +} + +beforeAll(async () => { + mock.module("@opencode-ai/session-ui/v2/session-review-v2", () => ({ + SESSION_REVIEW_V2_SIDEBAR_WIDTH_DEFAULT: 240, + SESSION_REVIEW_V2_SIDEBAR_WIDTH_MIN: 200, + SESSION_REVIEW_V2_SIDEBAR_WIDTH_MAX: 480, + })) + mock.module("@/context/platform", () => ({ + usePlatform: () => ({ platform: "desktop", storage: () => storage }), + })) + + createReviewPanelV2State = (await import("@/pages/session/v2/review-panel-v2-state")).createReviewPanelV2State +}) + +test("enables sidebar motion only after custom width hydration", async () => { + await new Promise((resolve, reject) => { + createRoot((dispose) => { + const state = createReviewPanelV2State() + const transition = + "sidebarTransition" in state && typeof state.sidebarTransition === "function" + ? (state.sidebarTransition as () => boolean) + : undefined + + try { + expect(transition).toBeFunction() + expect(transition?.()).toBeFalse() + expect(state.sidebarWidth()).toBe(240) + } catch (error) { + dispose() + reject(error) + return + } + + createEffect(() => { + if (!transition?.()) return + try { + expect(state.sidebarWidth()).toBe(360) + dispose() + resolve() + } catch (error) { + dispose() + reject(error) + } + }) + + read?.(JSON.stringify({ sidebarOpened: true, sidebarWidth: 360, expandMode: "collapse" })) + }) + }) +}) diff --git a/packages/app/test-browser/settings-keybinds.test.ts b/packages/app/test-browser/settings-keybinds.test.ts new file mode 100644 index 0000000000..75f0b75543 --- /dev/null +++ b/packages/app/test-browser/settings-keybinds.test.ts @@ -0,0 +1,107 @@ +import { describe, expect, test } from "bun:test" +import { createRoot } from "solid-js" +import { createKeybindSettingsController } from "../src/components/settings-keybinds" + +function setup(overrides: Record = {}) { + const changes: [string, string][] = [] + const suppression: boolean[] = [] + const notifications: { title: string; description?: string }[] = [] + let resets = 0 + let controller: ReturnType + + const dispose = createRoot((dispose) => { + controller = createKeybindSettingsController( + { + command: { + catalog: [ + { id: "session.alpha", title: "Alpha", keybind: "mod+a" }, + { id: "session.beta", title: "Beta", keybind: "mod+b" }, + ], + options: [], + keybinds: (enabled) => suppression.push(enabled), + }, + settings: { + current: { keybinds: overrides }, + keybinds: { + get: (id) => overrides[id], + set: (id, value) => { + overrides[id] = value + changes.push([id, value]) + }, + resetAll: () => { + resets++ + }, + }, + }, + notify: (toast) => notifications.push(toast), + }, + { + locale: () => "en", + t: (key, params) => { + if (params) return `${key}:${Object.values(params).join("|")}` + if (key === "common.key.alt") return "Alt" + return String(key) + }, + }, + ) + return dispose + }) + + return { + controller: controller!, + changes, + suppression, + notifications, + resets: () => resets, + dispose, + } +} + +function modKey(key: string) { + const mac = /(Mac|iPod|iPhone|iPad)/.test(navigator.platform) + return new KeyboardEvent("keydown", { key, ctrlKey: !mac, metaKey: mac, bubbles: true, cancelable: true }) +} + +describe("keybind settings controller", () => { + test("derives the catalog, effective bindings, and filtered groups", () => { + const state = setup({ "session.beta": "alt+k" }) + + expect(state.controller.catalog.title("session.alpha")).toBe("Alpha") + expect(state.controller.catalog.keybind("session.beta")).toBe("Alt+K") + expect(state.controller.catalog.filtered("alt k").get("Session")).toEqual(["session.beta"]) + expect(state.controller.settings.hasOverrides()).toBe(true) + + state.dispose() + }) + + test("captures bindings, rejects conflicts, and restores command handling", () => { + const state = setup() + + state.controller.capture.toggle("session.beta") + document.dispatchEvent(modKey("a")) + expect(state.changes).toEqual([]) + expect(state.notifications).toHaveLength(1) + expect(state.controller.capture.active()).toBe("session.beta") + + document.dispatchEvent(modKey("x")) + expect(state.changes).toEqual([["session.beta", "mod+x"]]) + expect(state.suppression).toEqual([false, true]) + expect(state.controller.capture.active()).toBeNull() + + state.controller.capture.toggle("session.alpha") + state.dispose() + expect(state.suppression).toEqual([false, true, false, true]) + document.dispatchEvent(modKey("z")) + expect(state.changes).toEqual([["session.beta", "mod+x"]]) + }) + + test("resets persisted overrides and reports success", () => { + const state = setup({ "session.alpha": "none" }) + + state.controller.settings.reset() + expect(state.resets()).toBe(1) + expect(state.notifications[0]?.title).toBe("settings.shortcuts.reset.toast.title") + + state.dispose() + }) +}) diff --git a/packages/app/test-browser/solid-router-cleanup.test.ts b/packages/app/test-browser/solid-router-cleanup.test.ts new file mode 100644 index 0000000000..5795fa5648 --- /dev/null +++ b/packages/app/test-browser/solid-router-cleanup.test.ts @@ -0,0 +1,64 @@ +import { expect, test } from "bun:test" +import { MetaProvider, Title } from "@solidjs/meta" +import { MemoryRouter, Route, createMemoryHistory, useParams } from "@solidjs/router" +import { createMemo } from "solid-js" +import { createComponent, render } from "solid-js/web" + +test("route cleanup cannot invalidate an owner list being disposed", async () => { + const host = document.createElement("div") + document.body.append(host) + const history = createMemoryHistory() + + const RepoPage = () => { + const params = useParams<{ id?: string }>() + const title = createMemo(() => params.id ?? "") + const button = document.createElement("button") + button.textContent = "Back" + button.addEventListener("click", () => history.set({ value: "/", scroll: false, replace: false })) + return [ + createComponent(Title, { + get children() { + return title() + }, + }), + button, + ] + } + + const HomePage = () => { + const button = document.createElement("button") + button.textContent = "Go" + button.addEventListener("click", () => history.set({ value: "/project", scroll: false, replace: false })) + return button + } + + const App = () => + createComponent(MetaProvider, { + get children() { + return createComponent(MemoryRouter, { + history, + get children() { + return [ + createComponent(Route, { path: "/", component: HomePage }), + createComponent(Route, { path: "/:id", component: RepoPage }), + ] + }, + }) + }, + }) + + const dispose = render(() => createComponent(App, {}), host) + const go = host.querySelector("button") + expect(go?.textContent).toBe("Go") + go?.click() + await new Promise((resolve) => setTimeout(resolve, 0)) + + const back = host.querySelector("button") + expect(back?.textContent).toBe("Back") + back?.click() + await new Promise((resolve) => setTimeout(resolve, 0)) + + expect(host.querySelector("button")?.textContent).toBe("Go") + dispose() + host.remove() +}) diff --git a/packages/app/test-browser/solid-virtual.test.ts b/packages/app/test-browser/solid-virtual.test.ts index 716fa7fa9e..9327eacf42 100644 --- a/packages/app/test-browser/solid-virtual.test.ts +++ b/packages/app/test-browser/solid-virtual.test.ts @@ -1,8 +1,32 @@ import { expect, test } from "bun:test" -import { createVirtualizer, defaultRangeExtractor } from "@tanstack/solid-virtual" +import { createVirtualizer, defaultRangeExtractor, Virtualizer } from "@tanstack/solid-virtual" import { createRoot, createSignal } from "solid-js" import { filterVirtualIndexes } from "@/pages/session/timeline/virtual-items" +test("end anchoring survives consecutive resizes when the first scroll write is clamped", () => { + const writes: { offset: number; adjustments?: number }[] = [] + const virtualizer = new Virtualizer({ + count: 5, + estimateSize: () => 50, + initialOffset: 50, + initialRect: { width: 400, height: 200 }, + anchorTo: "end", + scrollEndThreshold: 1, + getScrollElement: () => null, + scrollToFn: (offset, options) => writes.push({ offset, adjustments: options.adjustments }), + observeElementRect: () => {}, + observeElementOffset: () => {}, + }) + + virtualizer.getTotalSize() + virtualizer.resizeItem(4, 120) + expect(writes).toEqual([{ offset: 50, adjustments: 70 }]) + writes.length = 0 + + virtualizer.resizeItem(4, 200) + expect(writes).toEqual([{ offset: 120, adjustments: 80 }]) +}) + test("reactive count updates preserve measured row sizes", () => { createRoot((dispose) => { const [count, setCount] = createSignal(2) @@ -42,23 +66,26 @@ test("initial rect projects rows before a scroll element connects", () => { }) }) -test("logical scroll offset includes pending measurement adjustments", () => { - createRoot((dispose) => { - const virtualizer = createVirtualizer({ - count: 2, - getScrollElement: () => null, - estimateSize: () => 60, - initialOffset: 100, - initialRect: { width: 800, height: 60 }, - }) - - virtualizer.getTotalSize() - virtualizer.resizeItem(0, 100) - - expect(virtualizer.scrollOffset).toBe(100) - expect(virtualizer.getLogicalScrollOffset()).toBe(140) - dispose() +test("clamps oversized offsets with scroll margin and padding changes", () => { + const options = (paddingEnd: number) => ({ + count: 20, + estimateSize: () => 60, + initialOffset: Number.MAX_SAFE_INTEGER, + initialRect: { width: 800, height: 600 }, + scrollMargin: 64, + paddingEnd, + overscan: 1, + getScrollElement: () => null, + scrollToFn: () => {}, + observeElementRect: () => {}, + observeElementOffset: () => {}, }) + const virtualizer = new Virtualizer(options(64)) + + expect(virtualizer.getVirtualItems().map((item) => item.index)).toEqual([10, 11, 12, 13, 14, 15, 16, 17, 18, 19]) + + virtualizer.setOptions(options(600)) + expect(virtualizer.getVirtualItems().map((item) => item.index)).toEqual([18, 19]) }) test("stale pinned indexes do not produce missing virtual items after count shrinks", () => { diff --git a/packages/app/test-browser/toast-owner.test.ts b/packages/app/test-browser/toast-owner.test.ts index 104a505851..25ba5c000e 100644 --- a/packages/app/test-browser/toast-owner.test.ts +++ b/packages/app/test-browser/toast-owner.test.ts @@ -1,8 +1,46 @@ -import { describe, expect, test } from "bun:test" +import { beforeEach, describe, expect, test } from "bun:test" import { createSignal, type JSX } from "solid-js" import { showToastV2, toasterV2 } from "@opencode-ai/ui/v2/toast-v2" describe("showToastV2", () => { + // The toast registry is module state, so each test starts from an empty stack. + beforeEach(() => { + toasterV2.dismiss() + }) + + test("coalesces exact active content", () => { + const first = showToastV2({ title: "Repeated error", description: "Try again" }) + const second = showToastV2({ title: "Repeated error", description: "Try again" }) + const different = showToastV2({ title: "Repeated error", description: "A different error" }) + + expect(second).toBe(first) + expect(different).not.toBe(first) + + toasterV2.dismiss(first) + toasterV2.dismiss(different) + }) + + test("allows dismissed content to appear again", () => { + const first = showToastV2("Dismiss and retry") + toasterV2.dismiss(first) + + const second = showToastV2("Dismiss and retry") + expect(second).not.toBe(first) + + toasterV2.dismiss(second) + }) + + test("recreates matching content when it is not the topmost toast", () => { + const first = showToastV2("First toast") + const topmost = showToastV2("Topmost toast") + const repeated = showToastV2("First toast") + + expect(repeated).not.toBe(first) + + toasterV2.dismiss(topmost) + toasterV2.dismiss(repeated) + }) + test("creates no reactive computations at call time", () => { const [tick, setTick] = createSignal(0) let reads = 0 diff --git a/packages/app/vendor/opencode-ai-client-1.17.13-v2.tgz b/packages/app/vendor/opencode-ai-client-1.17.13-v2.tgz new file mode 100644 index 0000000000..bc1b664f82 Binary files /dev/null and b/packages/app/vendor/opencode-ai-client-1.17.13-v2.tgz differ diff --git a/packages/cli/AGENTS.md b/packages/cli/AGENTS.md deleted file mode 100644 index 73bebeb94f..0000000000 --- a/packages/cli/AGENTS.md +++ /dev/null @@ -1,7 +0,0 @@ -# V2 CLI and TUI development guide - -## Migration context - -- The TUI is being ported from legacy APIs to the new V2 APIs. New and migrated TUI behavior should use `sdk.client.v2` and the location-scoped data in `packages/tui/src/context/data.tsx` instead of adding dependencies on legacy sync state. -- Preserve established TUI behavior unless the task intentionally changes it. -- Load the `opencode-dev` skill before interactively running, debugging, or verifying opencode's V2 CLI, TUI, or server. diff --git a/packages/cli/bin/opencode2.cjs b/packages/cli/bin/lildax.cjs old mode 100755 new mode 100644 similarity index 96% rename from packages/cli/bin/opencode2.cjs rename to packages/cli/bin/lildax.cjs index 8795c71cd2..ab99b84b0f --- a/packages/cli/bin/opencode2.cjs +++ b/packages/cli/bin/lildax.cjs @@ -31,11 +31,11 @@ function run(target) { const envPath = process.env.OPENCODE_BIN_PATH const scriptDir = path.dirname(fs.realpathSync(__filename)) -const cached = path.join(scriptDir, ".opencode2") +const cached = path.join(scriptDir, ".lildax") const platform = { darwin: "darwin", linux: "linux", win32: "windows" }[os.platform()] || os.platform() const arch = { x64: "x64", arm64: "arm64", arm: "arm" }[os.arch()] || os.arch() const base = "@opencode-ai/cli-" + platform + "-" + arch -const binary = platform === "windows" ? "opencode2.exe" : "opencode2" +const binary = platform === "windows" ? "lildax.exe" : "lildax" function supportsAvx2() { if (arch !== "x64") return false @@ -121,7 +121,7 @@ function findBinary(startDir) { const resolved = envPath || (fs.existsSync(cached) ? cached : findBinary(scriptDir)) if (!resolved) { console.error( - "It seems that your package manager failed to install the right opencode2 CLI package. Try manually installing " + + "It seems that your package manager failed to install the right lildax CLI package. Try manually installing " + names.map((name) => `"${name}"`).join(" or ") + " package", ) diff --git a/packages/cli/bunfig.toml b/packages/cli/bunfig.toml index b16283cb5b..7693482f3b 100644 --- a/packages/cli/bunfig.toml +++ b/packages/cli/bunfig.toml @@ -1,4 +1 @@ preload = ["@opentui/solid/preload"] - -[test] -preload = ["@opentui/solid/preload"] diff --git a/packages/cli/package.json b/packages/cli/package.json index e5172df3c4..be5d853de0 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,64 +1,36 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/cli", - "version": "1.17.18", + "version": "1.18.11", "type": "module", "license": "MIT", "bin": { - "opencode2": "./bin/opencode2.cjs" + "lildax": "./bin/lildax.cjs" }, "files": [ "bin" ], - "exports": { - "./daemon": "./src/daemon.ts", - "./mini": "./src/mini/index.ts", - "./mini/footer.command": "./src/mini/footer.command.tsx", - "./mini/footer.menu": "./src/mini/footer.menu.tsx", - "./mini/footer.permission": "./src/mini/footer.permission.tsx", - "./mini/footer.prompt": "./src/mini/footer.prompt.tsx", - "./mini/footer.question": "./src/mini/footer.question.tsx", - "./mini/footer.subagent": "./src/mini/footer.subagent.tsx", - "./mini/footer.view": "./src/mini/footer.view.tsx", - "./mini/scrollback.writer": "./src/mini/scrollback.writer.tsx", - "./mini/*": "./src/mini/*.ts", - "./server-process": "./src/server-process.ts" - }, "scripts": { "build": "bun run script/build.ts", "dev": "bun run src/index.ts", - "test": "bun test --timeout 30000 --only-failures", "typecheck": "tsgo --noEmit" }, "dependencies": { "@effect/platform-node": "catalog:", - "@opencode-ai/client": "workspace:*", "@opencode-ai/core": "workspace:*", - "@opencode-ai/plugin": "workspace:*", - "@opencode-ai/schema": "workspace:*", "@opencode-ai/sdk": "workspace:*", "@opencode-ai/server": "workspace:*", "@opencode-ai/tui": "workspace:*", "@opentui/core": "catalog:", - "@opentui/keymap": "catalog:", "@opentui/solid": "catalog:", "@parcel/watcher": "2.5.1", "effect": "catalog:", - "fuzzysort": "catalog:", - "immer": "11.1.4", - "jsonc-parser": "3.3.1", - "open": "10.1.2", - "opentui-spinner": "catalog:", - "semver": "catalog:", - "solid-js": "catalog:", - "strip-ansi": "7.1.2", - "uqr": "0.1.3" + "solid-js": "catalog:" }, "devDependencies": { "@opencode-ai/script": "workspace:*", "@tsconfig/bun": "catalog:", "@types/bun": "catalog:", - "@types/semver": "catalog:", "@typescript/native-preview": "catalog:" } } diff --git a/packages/cli/script/build.ts b/packages/cli/script/build.ts index aa498c8141..f42d8b07b0 100755 --- a/packages/cli/script/build.ts +++ b/packages/cli/script/build.ts @@ -1,7 +1,6 @@ #!/usr/bin/env bun import { $ } from "bun" -import fs from "fs" import { rm } from "fs/promises" import path from "path" import { Script } from "@opencode-ai/script" @@ -10,7 +9,7 @@ import pkg from "../package.json" import { modelsData } from "./generate" const dir = path.resolve(import.meta.dirname, "..") -const binary = "opencode2" +const binary = "lildax" process.chdir(dir) await rm("dist", { recursive: true, force: true }) @@ -18,6 +17,7 @@ await rm("dist", { recursive: true, force: true }) const singleFlag = process.argv.includes("--single") const baselineFlag = process.argv.includes("--baseline") const skipInstall = process.argv.includes("--skip-install") +const sourcemapsFlag = process.argv.includes("--sourcemaps") const plugin = createSolidTransformPlugin() const allTargets: { @@ -50,10 +50,6 @@ const targets = singleFlag if (!skipInstall) await $`bun install --os="*" --cpu="*" @opentui/core@${pkg.dependencies["@opentui/core"]}` -const localParserWorker = path.resolve(dir, "node_modules/@opentui/core/parser.worker.js") -const rootParserWorker = path.resolve(dir, "../../node_modules/@opentui/core/parser.worker.js") -const parserWorker = fs.realpathSync(fs.existsSync(localParserWorker) ? localParserWorker : rootParserWorker) - for (const item of targets) { const target = [ binary, @@ -67,13 +63,13 @@ for (const item of targets) { const name = target.replace(binary, "cli") console.log(`building ${name}`) const result = await Bun.build({ - entrypoints: ["./src/index.ts", parserWorker], + entrypoints: ["./src/index.ts"], tsconfig: "./tsconfig.json", plugins: [plugin], external: ["node-gyp"], format: "esm", minify: true, - sourcemap: "inline", + sourcemap: sourcemapsFlag ? "linked" : "none", splitting: true, compile: { autoloadBunfig: false, @@ -93,10 +89,6 @@ for (const item of targets) { OPENCODE_LIBC: item.os === "linux" ? `'${item.abi ?? "glibc"}'` : "undefined", // FFF_LIBC selects the fff native lib variant: "musl" or "gnu". FFF_LIBC: item.os === "linux" ? `'${item.abi ?? "gnu"}'` : "undefined", - OTUI_TREE_SITTER_WORKER_PATH: - (item.os === "win32" ? '"B:/~BUN/root/' : '"/$bunfs/root/') + - path.relative(dir, parserWorker).replaceAll("\\", "/") + - '"', ...(item.os === "linux" ? { "process.env.OPENTUI_LIBC": JSON.stringify(item.abi ?? "glibc") } : {}), }, }) diff --git a/packages/cli/script/generate.ts b/packages/cli/script/generate.ts index d98565e298..e162f2ea7e 100755 --- a/packages/cli/script/generate.ts +++ b/packages/cli/script/generate.ts @@ -1,4 +1,4 @@ -const modelsUrl = process.env.OPENCODE_MODELS_URL || "https://models.dev" +const modelsUrl = process.env.OPENCODE_MODELS_URL || "https://models.opencode.ai" export const modelsData = process.env.MODELS_DEV_API_JSON ? await Bun.file(process.env.MODELS_DEV_API_JSON).text() diff --git a/packages/cli/script/publish.ts b/packages/cli/script/publish.ts index 116e5efa32..d2855413ca 100755 --- a/packages/cli/script/publish.ts +++ b/packages/cli/script/publish.ts @@ -25,15 +25,14 @@ for (const filepath of new Bun.Glob("*/package.json").scanSync({ cwd: "./dist" } } console.log("binaries", binaries) const version = Object.values(binaries)[0] -const name = pkg.name -await $`mkdir -p ./dist/${name}/bin` -await $`cp ./bin/opencode2.cjs ./dist/${name}/bin/opencode2` -await Bun.file(`./dist/${name}/package.json`).write( +await $`mkdir -p ./dist/${pkg.name}/bin` +await $`cp ./bin/lildax.cjs ./dist/${pkg.name}/bin/lildax` +await Bun.file(`./dist/${pkg.name}/package.json`).write( JSON.stringify( { - name, - bin: { opencode2: "./bin/opencode2" }, + name: pkg.name, + bin: { lildax: "./bin/lildax" }, version, license: pkg.license, repository: { type: "git", url: "git+https://github.com/anomalyco/opencode.git" }, @@ -51,4 +50,4 @@ await Promise.all( publish(`./dist/${name.replace("@opencode-ai/", "")}`, name, version), ), ) -await publish(`./dist/${name}`, name, version) +await publish(`./dist/${pkg.name}`, pkg.name, version) diff --git a/packages/cli/src/commands/commands.ts b/packages/cli/src/commands/commands.ts index a80c5ba108..19d1f5e68b 100644 --- a/packages/cli/src/commands/commands.ts +++ b/packages/cli/src/commands/commands.ts @@ -3,41 +3,12 @@ import { Spec } from "../framework/spec" declare const OPENCODE_CLI_NAME: string | undefined -const ServerParams = { - standalone: Flag.boolean("standalone").pipe( - Flag.withDescription("Run with a private server instead of the background service"), - Flag.withDefault(false), - ), - server: Flag.string("server").pipe( - Flag.withDescription("Connect to a server URL instead of the background service"), - Flag.optional, - ), -} - export const Commands = Spec.make(typeof OPENCODE_CLI_NAME === "string" ? OPENCODE_CLI_NAME : "opencode", { description: "OpenCode 2.0 preview command line interface", - params: { - ...ServerParams, - directory: Argument.string("directory").pipe( - Argument.withDescription("Directory to start OpenCode in"), - Argument.optional, - ), - continue: Flag.boolean("continue").pipe( - Flag.withAlias("c"), - Flag.withDescription("Continue the last session"), - Flag.withDefault(false), - ), - session: Flag.string("session").pipe( - Flag.withAlias("s"), - Flag.withDescription("Session ID to continue"), - Flag.optional, - ), - }, commands: [ Spec.make("api", { description: "Make a request to the running server", params: { - ...ServerParams, request: Argument.string("operation | method path").pipe( Argument.withDescription("OpenAPI operation ID, or an HTTP method followed by a path"), Argument.variadic({ min: 1, max: 2 }), @@ -55,140 +26,7 @@ export const Commands = Spec.make(typeof OPENCODE_CLI_NAME === "string" ? OPENCO description: "Debugging and troubleshooting tools", commands: [Spec.make("agents", { description: "List all agents" })], }), - Spec.make("console", { - description: "Manage OpenCode Console access", - commands: [ - Spec.make("login", { - description: "Log in to OpenCode Console", - params: { - url: Argument.string("url").pipe(Argument.withDescription("Console server URL"), Argument.optional), - }, - }), - ], - }), - Spec.make("mcp", { - description: "Manage MCP (Model Context Protocol) servers", - commands: [ - Spec.make("list", { description: "List configured MCP servers and their status" }), - Spec.make("add", { - description: "Add an MCP server to your configuration", - params: { - name: Argument.string("name").pipe(Argument.withDescription("Name of the MCP server")), - command: Argument.string("command").pipe( - Argument.withDescription("Command and arguments for a local server, passed after --"), - Argument.variadic({ min: 0 }), - ), - url: Flag.string("url").pipe(Flag.withDescription("URL for a remote MCP server"), Flag.optional), - header: Flag.keyValuePair("header").pipe( - Flag.withDescription("HTTP header for a remote server, as name=value"), - Flag.optional, - ), - env: Flag.keyValuePair("env").pipe( - Flag.withDescription("Environment variable for a local server, as name=value"), - Flag.optional, - ), - global: Flag.boolean("global").pipe( - Flag.withDescription("Write to the global config instead of the project config"), - Flag.withDefault(false), - ), - }, - }), - Spec.make("auth", { - description: "Authenticate with an OAuth-capable remote MCP server", - params: { name: Argument.string("name").pipe(Argument.withDescription("Name of the MCP server")) }, - }), - Spec.make("logout", { - description: "Remove stored OAuth credentials for an MCP server", - params: { name: Argument.string("name").pipe(Argument.withDescription("Name of the MCP server")) }, - }), - ], - }), Spec.make("migrate", { description: "Migrate v1 data to v2" }), - Spec.make("mini", { - description: "Start the minimal interactive interface", - params: { - ...ServerParams, - continue: Flag.boolean("continue").pipe( - Flag.withAlias("c"), - Flag.withDescription("Continue the last session"), - Flag.withDefault(false), - ), - session: Flag.string("session").pipe( - Flag.withAlias("s"), - Flag.withDescription("Session ID to continue"), - Flag.optional, - ), - fork: Flag.boolean("fork").pipe( - Flag.withDescription("Fork the session when continuing"), - Flag.withDefault(false), - ), - replay: Flag.boolean("replay").pipe( - Flag.withDescription("Replay session history on resume and after resize"), - Flag.withDefault(true), - ), - replayLimit: Flag.integer("replay-limit").pipe( - Flag.withDescription("Cap visible replay to the newest N messages"), - Flag.optional, - ), - model: Flag.string("model").pipe( - Flag.withAlias("m"), - Flag.withDescription("Model to use in the format provider/model"), - Flag.optional, - ), - agent: Flag.string("agent").pipe(Flag.withDescription("Agent to use"), Flag.optional), - prompt: Flag.string("prompt").pipe(Flag.withDescription("Prompt to use"), Flag.optional), - demo: Flag.boolean("demo").pipe(Flag.withDefault(false), Flag.withHidden), - }, - }), - Spec.make("run", { - description: "Run OpenCode with a message", - params: { - ...ServerParams, - message: Argument.string("message").pipe( - Argument.withDescription("Message to send"), - Argument.variadic({ min: 0 }), - ), - continue: Flag.boolean("continue").pipe( - Flag.withAlias("c"), - Flag.withDescription("Continue the last session"), - Flag.withDefault(false), - ), - session: Flag.string("session").pipe( - Flag.withAlias("s"), - Flag.withDescription("Session ID to continue"), - Flag.optional, - ), - fork: Flag.boolean("fork").pipe( - Flag.withDescription("Fork the session before continuing"), - Flag.withDefault(false), - ), - model: Flag.string("model").pipe( - Flag.withAlias("m"), - Flag.withDescription("Model to use in the format provider/model#variant"), - Flag.optional, - ), - agent: Flag.string("agent").pipe(Flag.withDescription("Agent to use"), Flag.optional), - format: Flag.choice("format", ["default", "json"]).pipe( - Flag.withDescription("Output format"), - Flag.withDefault("default"), - ), - file: Flag.string("file").pipe( - Flag.withAlias("f"), - Flag.withDescription("File to attach to the message"), - Flag.atMost(100), - ), - title: Flag.string("title").pipe(Flag.withDescription("Session title"), Flag.optional), - thinking: Flag.boolean("thinking").pipe( - Flag.withDescription("Show thinking blocks"), - Flag.withDefault(false), - ), - auto: Flag.boolean("auto").pipe( - Flag.withDescription("Auto-approve permissions that are not explicitly denied"), - Flag.withDefault(false), - ), - yolo: Flag.boolean("yolo").pipe(Flag.withDefault(false), Flag.withHidden), - }, - }), Spec.make("service", { description: "Manage the background server", commands: [ @@ -196,28 +34,18 @@ export const Commands = Spec.make(typeof OPENCODE_CLI_NAME === "string" ? OPENCO Spec.make("restart", { description: "Restart the background server" }), Spec.make("status", { description: "Show background server status" }), Spec.make("stop", { description: "Stop the background server" }), - Spec.make("get", { - description: "Get service configuration", - params: { key: Argument.string("key").pipe(Argument.optional) }, - }), - Spec.make("set", { - description: "Set service configuration", - params: { key: Argument.string("key"), value: Argument.string("value") }, - }), - Spec.make("unset", { - description: "Unset service configuration", - params: { key: Argument.string("key") }, + Spec.make("password", { + description: "Get or set the server password", + params: { value: Argument.string("value").pipe(Argument.optional) }, }), ], }), - Spec.make("pair", { description: "Show server pairing information" }), Spec.make("serve", { description: "Start the v2 API server", params: { - hostname: Flag.string("hostname").pipe(Flag.optional), + hostname: Flag.string("hostname").pipe(Flag.withDefault("127.0.0.1")), port: Flag.integer("port").pipe(Flag.optional), - service: Flag.boolean("service").pipe(Flag.withDefault(false)), - stdio: Flag.boolean("stdio").pipe(Flag.withDefault(false)), + register: Flag.boolean("register").pipe(Flag.withDefault(false)), }, }), ], diff --git a/packages/cli/src/commands/handlers/api.ts b/packages/cli/src/commands/handlers/api.ts index 49e367d48e..cf00394cb9 100644 --- a/packages/cli/src/commands/handlers/api.ts +++ b/packages/cli/src/commands/handlers/api.ts @@ -2,8 +2,7 @@ import { EOL } from "node:os" import { Effect, Option } from "effect" import { Commands } from "../commands" import { Runtime } from "../../framework/runtime" -import { Service } from "@opencode-ai/client/effect" -import { Server } from "../../services/server" +import { Daemon } from "../../services/daemon" const methods = new Set(["delete", "get", "head", "options", "patch", "post", "put"]) @@ -18,15 +17,11 @@ type OpenApi = { export default Runtime.handler( Commands.commands.api, Effect.fn("cli.api")(function* (input) { - const server = yield* Server.resolve({ - server: Option.getOrUndefined(input.server), - standalone: input.standalone, - mismatch: "ignore", - }) - const endpoint = server.endpoint + const daemon = yield* Daemon.Service + const transport = yield* daemon.transport() const params = Option.getOrElse(input.param, () => ({})) - const request = yield* resolveRequest(endpoint, input.request, params) - const headers = new Headers(Service.headers(endpoint)) + const request = yield* resolveRequest(transport, input.request, params) + const headers = new Headers(transport.headers) for (const header of input.header) { const index = header.indexOf(":") if (index < 1) return yield* Effect.fail(new Error(`Invalid header, expected name:value: ${header}`)) @@ -36,7 +31,7 @@ export default Runtime.handler( if (body !== undefined && !headers.has("content-type")) headers.set("content-type", "application/json") const response = yield* Effect.tryPromise(() => - fetch(new URL(request.path, endpoint.url), { + fetch(new URL(request.path, transport.url), { method: request.method, headers, body, @@ -63,7 +58,7 @@ export function rawRequest(input: readonly string[]) { } function resolveRequest( - endpoint: Service.Endpoint, + transport: { url: string; headers: RequestInit["headers"] }, input: readonly string[], params: Record, ) { @@ -71,7 +66,7 @@ function resolveRequest( if (raw) return Effect.succeed(raw) if (input.length !== 1) return Effect.fail(new Error("Expected an operation name or an HTTP method and path")) return Effect.tryPromise(async () => { - const response = await fetch(new URL("/openapi.json", endpoint.url), { headers: Service.headers(endpoint) }) + const response = await fetch(new URL("/openapi.json", transport.url), { headers: transport.headers }) if (!response.ok) throw new Error(`Failed to load OpenAPI document: HTTP ${response.status}`) return resolveOperation((await response.json()) as OpenApi, input[0], params) }) diff --git a/packages/cli/src/commands/handlers/console/login.ts b/packages/cli/src/commands/handlers/console/login.ts deleted file mode 100644 index 973fe1a326..0000000000 --- a/packages/cli/src/commands/handlers/console/login.ts +++ /dev/null @@ -1,117 +0,0 @@ -import { Cause, Effect, Exit, Option } from "effect" -import { Service } from "@opencode-ai/client/effect" -import { OpenCode, type OpenCodeClient } from "@opencode-ai/client/promise" -import { AppProcess } from "@opencode-ai/core/process" -import { Commands } from "../../commands" -import { Runtime } from "../../../framework/runtime" -import { ServiceConfig } from "../../../services/service-config" -import { createTimelineHost, type TimelineHost } from "../../../ui/timeline" - -const integrationID = "opencode" -const location = { directory: process.cwd() } - -export default Runtime.handler( - Commands.commands.console.commands.login, - Effect.fn("cli.console.login")(function* (input) { - const timeline = yield* Effect.acquireRelease( - Effect.promise(() => createTimelineHost()), - (value) => request(() => value.close()).pipe(Effect.ignore), - ) - const exit = yield* login(timeline, Option.getOrUndefined(input.url)).pipe( - Effect.raceFirst(AppProcess.waitForAbort(timeline.signal)), - Effect.exit, - ) - if (Exit.isSuccess(exit)) return - - const cancelled = timeline.signal.aborted - yield* request(() => timeline.failure(cancelled ? "Authorization cancelled" : errorMessage(exit.cause))).pipe( - Effect.ignore, - ) - process.exitCode = cancelled ? 130 : 1 - }), -) - -const login = Effect.fn("cli.console.login.run")(function* (timeline: TimelineHost, server?: string) { - yield* request(() => timeline.intro("Log in")) - yield* request(() => timeline.pending("Connecting to OpenCode...")) - - const endpoint = yield* Service.start(yield* ServiceConfig.options()) - const client = OpenCode.make({ baseUrl: endpoint.url, headers: Service.headers(endpoint) }) - const found = yield* request((signal) => client.integration.get({ integrationID, location }, { signal })) - const integration = yield* required(found.data, "OpenCode Console integration is unavailable") - const method = yield* required( - integration.methods.find((candidate) => candidate.type === "oauth"), - "OpenCode Console login is unavailable", - ) - - yield* request(() => timeline.pending("Starting authorization...")) - const started = yield* request((signal) => - client.integration.connect.oauth( - { - integrationID, - methodID: method.id, - inputs: server ? { server } : {}, - location, - }, - { signal }, - ), - ) - const attempt = started.data - yield* Effect.addFinalizer(() => - request(() => - client.integration.attempt.cancel( - { attemptID: attempt.attemptID, location }, - { signal: AbortSignal.timeout(5_000) }, - ), - ).pipe(Effect.ignore), - ) - if (attempt.mode !== "auto") yield* Effect.fail(new Error("OpenCode Console requires a device login")) - - yield* request(() => timeline.item(`Go to: ${attempt.url}`)) - yield* request(() => timeline.item(attempt.instructions)) - yield* request(async () => { - const { default: open } = await import("open") - await open(attempt.url) - }).pipe(Effect.ignore) - yield* request(() => timeline.pending("Waiting for authorization...")) - - const status = yield* waitForConsoleLogin(client, attempt.attemptID) - if (status.status === "failed") yield* Effect.fail(new Error(status.message)) - if (status.status === "expired") yield* Effect.fail(new Error("Device code expired")) - - yield* request(() => timeline.success("Connected to OpenCode Console")) - yield* request(() => timeline.outro("Done")) -}) - -const waitForConsoleLogin = Effect.fn("cli.console.login.wait")(function* ( - client: OpenCodeClient, - attemptID: string, -) { - while (true) { - const response = yield* request((signal) => - client.integration.attempt.status({ attemptID, location }, { signal }), - ) - if (response.data.status !== "pending") return response.data - yield* Effect.sleep(500) - } -}) - -function request(task: (signal: AbortSignal) => Promise) { - return Effect.tryPromise({ - try: task, - catch: (cause) => cause, - }) -} - -function required(value: A | null | undefined, message: string) { - return value === null || value === undefined ? Effect.fail(new Error(message)) : Effect.succeed(value) -} - -function errorMessage(cause: Cause.Cause) { - const error = Cause.squash(cause) - if (error instanceof Error) return error.message - if (typeof error === "object" && error !== null && "message" in error && typeof error.message === "string") { - return error.message - } - return String(error) -} diff --git a/packages/cli/src/commands/handlers/debug/agents.ts b/packages/cli/src/commands/handlers/debug/agents.ts index ec7925fc44..3a0c20cb06 100644 --- a/packages/cli/src/commands/handlers/debug/agents.ts +++ b/packages/cli/src/commands/handlers/debug/agents.ts @@ -1,18 +1,14 @@ import { EOL } from "os" -import { Effect } from "effect" -import { createOpencodeClient } from "@opencode-ai/sdk/v2/client" +import * as Effect from "effect/Effect" import { Commands } from "../../commands" import { Runtime } from "../../../framework/runtime" -import { Service } from "@opencode-ai/client/effect" -import { ServiceConfig } from "../../../services/service-config" +import { Daemon } from "../../../services/daemon" export default Runtime.handler( Commands.commands.debug.commands.agents, Effect.fn("cli.debug.agents")(function* () { - const options = yield* ServiceConfig.options() - const found = yield* Service.discover(options) - const endpoint = found ?? (yield* Service.start(options)) - const client = createOpencodeClient({ baseUrl: endpoint.url, headers: Service.headers(endpoint) }) + const daemon = yield* Daemon.Service + const client = yield* daemon.client() const response = yield* Effect.promise(() => client.v2.agent.list({ location: { directory: process.cwd() } })) process.stdout.write( JSON.stringify( diff --git a/packages/cli/src/commands/handlers/default.ts b/packages/cli/src/commands/handlers/default.ts index 474d2d184d..d0a9968e5d 100644 --- a/packages/cli/src/commands/handlers/default.ts +++ b/packages/cli/src/commands/handlers/default.ts @@ -1,72 +1,13 @@ -import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" -import { Global } from "@opencode-ai/core/global" -import { run } from "@opencode-ai/tui" -import { loadBuiltinPlugins } from "@opencode-ai/tui/builtins" import { Commands } from "../commands" import { Runtime } from "../../framework/runtime" -import { Config } from "../../config" -import { Effect, Option } from "effect" -import { Server } from "../../services/server" -import { Updater } from "../../services/updater" -import { UpdatePreflight } from "../../services/update-preflight" +import { Effect } from "effect" +import { Daemon } from "../../services/daemon" -export default Runtime.handler(Commands, (input) => +export default Runtime.handler(Commands, () => Effect.gen(function* () { - const requestedDirectory = Option.getOrUndefined(input.directory) - if (requestedDirectory !== undefined) process.chdir(requestedDirectory) - const updater = yield* Updater.Service - yield* updater.check().pipe(Effect.forkScoped) - const preflight = UpdatePreflight.make() - yield* Effect.addFinalizer(() => Effect.promise(() => preflight.close())) - const server = yield* Server.resolve({ - server: Option.getOrUndefined(input.server), - standalone: input.standalone, - onStart: (reason, existing) => { - if (reason === "version-mismatch" && preflight.begin(existing?.version)) return - process.stderr.write( - reason === "version-mismatch" - ? "Restarting background server (version mismatch)...\n" - : "Starting background server...\n", - ) - }, - }).pipe( - Effect.tapError(() => - Effect.promise(() => preflight.fail("OpenCode update could not start the new background service")), - ), - ) - preflight.loading() - const config = yield* Config.Service - let disposeSlots: (() => void) | undefined - const context = yield* Effect.context() - const runFork = Effect.runForkWith(context) - const runPromise = Effect.runPromiseWith(context) - yield* run({ - server, - args: { continue: input.continue, sessionID: Option.getOrUndefined(input.session) }, - config: { - get: () => runPromise(config.get()), - update: (update) => runPromise(config.update(update)), - }, - terminalHandoff: () => preflight.finish(), - log: (level, message, tags) => { - const effect = - level === "debug" - ? Effect.logDebug(message, tags) - : level === "warn" - ? Effect.logWarning(message, tags) - : level === "error" - ? Effect.logError(message, tags) - : Effect.logInfo(message, tags) - runFork(effect) - }, - pluginHost: { - async start(pluginInput) { - disposeSlots = await loadBuiltinPlugins(pluginInput.api, pluginInput.runtime) - }, - async dispose() { - disposeSlots?.() - }, - }, - }).pipe(Effect.provide(AppNodeBuilder.build(Global.node))) + const daemon = yield* Daemon.Service + const transport = yield* daemon.transport() + const { runTui } = yield* Effect.promise(() => import("../../tui")) + yield* runTui(transport) }), ) diff --git a/packages/cli/src/commands/handlers/mcp/add.ts b/packages/cli/src/commands/handlers/mcp/add.ts deleted file mode 100644 index 695104b3f2..0000000000 --- a/packages/cli/src/commands/handlers/mcp/add.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { EOL } from "node:os" -import path from "node:path" -import { Effect, Option } from "effect" -import { applyEdits, modify } from "jsonc-parser" -import { Global } from "@opencode-ai/core/global" -import { Commands } from "../../commands" -import { Runtime } from "../../../framework/runtime" - -export default Runtime.handler( - Commands.commands.mcp.commands.add, - Effect.fn("cli.mcp.add")(function* (input) { - const url = Option.getOrUndefined(input.url) - const headers = Option.getOrUndefined(input.header) - const environment = Option.getOrUndefined(input.env) - // The CLI framework strands `--` operands on the root command, so read the local server command - // straight from argv after `--`. This also lets the command carry its own flags (e.g. `npx -y`). - const dash = process.argv.indexOf("--") - const command = dash === -1 ? [...input.command] : process.argv.slice(dash + 1) - - const hasCommand = command.length > 0 - if (url && hasCommand) - return yield* Effect.fail(new Error("Provide either --url or a command after --, not both")) - if (!url && !hasCommand) return yield* Effect.fail(new Error("Provide either --url or a command after --")) - if (url && !URL.canParse(url)) return yield* Effect.fail(new Error(`Invalid URL: ${url}`)) - if (url && environment) return yield* Effect.fail(new Error("--env is only valid for local MCP servers")) - if (hasCommand && headers) return yield* Effect.fail(new Error("--header is only valid for remote MCP servers")) - - const server = url - ? { type: "remote" as const, url, ...(headers ? { headers } : {}) } - : { type: "local" as const, command, ...(environment ? { environment } : {}) } - - const configPath = yield* Effect.promise(() => resolveConfigPath(input.global ? Global.Path.config : process.cwd())) - yield* Effect.promise(() => write(configPath, input.name, server)) - process.stdout.write(`MCP server "${input.name}" added to ${configPath}` + EOL) - }), -) - -async function resolveConfigPath(directory: string) { - const candidates = [ - path.join(directory, "opencode.json"), - path.join(directory, "opencode.jsonc"), - path.join(directory, ".opencode", "opencode.json"), - path.join(directory, ".opencode", "opencode.jsonc"), - ] - for (const candidate of candidates) { - if (await Bun.file(candidate).exists()) return candidate - } - return candidates[0] -} - -async function write(configPath: string, name: string, server: unknown) { - const file = Bun.file(configPath) - const text = (await file.exists()) ? await file.text() : "{}" - const edits = modify(text, ["mcp", "servers", name], server, { - formattingOptions: { tabSize: 2, insertSpaces: true }, - }) - await Bun.write(configPath, applyEdits(text, edits)) -} diff --git a/packages/cli/src/commands/handlers/mcp/auth.ts b/packages/cli/src/commands/handlers/mcp/auth.ts deleted file mode 100644 index 03a5e687fd..0000000000 --- a/packages/cli/src/commands/handlers/mcp/auth.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { EOL } from "node:os" -import { Effect } from "effect" -import { - createOpencodeClient, - type IntegrationAttemptStatus, - type IntegrationOAuthMethod, - type OpencodeClient, -} from "@opencode-ai/sdk/v2/client" -import { Commands } from "../../commands" -import { Runtime } from "../../../framework/runtime" -import { Service } from "@opencode-ai/client/effect" -import { ServiceConfig } from "../../../services/service-config" -import { resolveIntegration } from "./resolve" - -const location = { directory: process.cwd() } - -export default Runtime.handler( - Commands.commands.mcp.commands.auth, - Effect.fn("cli.mcp.auth")(function* (input) { - const options = yield* ServiceConfig.options() - const found = yield* Service.discover(options) - const endpoint = found ?? (yield* Service.start(options)) - const client = createOpencodeClient({ baseUrl: endpoint.url, headers: Service.headers(endpoint) }) - - const integration = yield* resolveIntegration(client, input.name, location) - if (!integration) - return yield* Effect.fail(new Error(`MCP server "${input.name}" is not an OAuth-capable remote server`)) - const method = integration.methods.find( - (candidate): candidate is IntegrationOAuthMethod => candidate.type === "oauth", - ) - if (!method) - return yield* Effect.fail(new Error(`MCP server "${input.name}" is not an OAuth-capable remote server`)) - - const started = yield* Effect.promise(() => - client.v2.integration.connect.oauth({ integrationID: integration.id, methodID: method.id, inputs: {}, location }), - ) - const attempt = started.data?.data - if (!attempt) return yield* Effect.fail(new Error(started.error?.message ?? "Failed to start OAuth attempt")) - if (attempt.mode === "code") - return yield* Effect.fail(new Error("This server requires manual code entry, which the CLI does not support")) - - process.stdout.write(attempt.instructions + EOL + attempt.url + EOL) - - const result = yield* poll(client, attempt.attemptID) - if (result.status === "complete") { - process.stdout.write(`Authenticated with ${input.name}` + EOL) - return - } - const reason = result.status === "failed" ? `: ${result.message}` : "" - return yield* Effect.fail(new Error(`Authentication ${result.status}${reason}`)) - }), -) - -const poll = ( - client: OpencodeClient, - attemptID: string, -): Effect.Effect> => - Effect.gen(function* () { - const response = yield* Effect.promise(() => client.v2.integration.attempt.status({ attemptID, location })) - const status = response.data?.data - if (!status || status.status === "pending") { - yield* Effect.sleep("1 second") - return yield* poll(client, attemptID) - } - return status - }) diff --git a/packages/cli/src/commands/handlers/mcp/list.ts b/packages/cli/src/commands/handlers/mcp/list.ts deleted file mode 100644 index 3c44a839e3..0000000000 --- a/packages/cli/src/commands/handlers/mcp/list.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { EOL } from "node:os" -import { Effect } from "effect" -import { createOpencodeClient, type McpServer } from "@opencode-ai/sdk/v2/client" -import { Commands } from "../../commands" -import { Runtime } from "../../../framework/runtime" -import { Service } from "@opencode-ai/client/effect" -import { ServiceConfig } from "../../../services/service-config" - -export default Runtime.handler( - Commands.commands.mcp.commands.list, - Effect.fn("cli.mcp.list")(function* () { - const options = yield* ServiceConfig.options() - const found = yield* Service.discover(options) - const endpoint = found ?? (yield* Service.start(options)) - const client = createOpencodeClient({ baseUrl: endpoint.url, headers: Service.headers(endpoint) }) - const response = yield* Effect.promise(() => client.v2.mcp.list({ location: { directory: process.cwd() } })) - const servers = (response.data?.data ?? []).toSorted((a, b) => a.name.localeCompare(b.name)) - if (servers.length === 0) { - process.stdout.write("No MCP servers configured" + EOL) - return - } - const width = Math.max(...servers.map((server) => server.name.length)) - const lines = servers.map( - (server) => `${icon(server.status)} ${server.name.padEnd(width)} ${describe(server.status)}`, - ) - process.stdout.write(lines.join(EOL) + EOL) - }), -) - -function icon(status: McpServer["status"]) { - switch (status.status) { - case "connected": - return "✓" - case "needs_auth": - return "⚠" - case "failed": - case "needs_client_registration": - return "✗" - default: - return "○" - } -} - -function describe(status: McpServer["status"]) { - switch (status.status) { - case "needs_auth": - return "needs authentication" - case "needs_client_registration": - return `needs client registration: ${status.error}` - case "failed": - return `failed: ${status.error}` - default: - return status.status - } -} diff --git a/packages/cli/src/commands/handlers/mcp/logout.ts b/packages/cli/src/commands/handlers/mcp/logout.ts deleted file mode 100644 index 271953c306..0000000000 --- a/packages/cli/src/commands/handlers/mcp/logout.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { EOL } from "node:os" -import { Effect } from "effect" -import { createOpencodeClient } from "@opencode-ai/sdk/v2/client" -import { Commands } from "../../commands" -import { Runtime } from "../../../framework/runtime" -import { Service } from "@opencode-ai/client/effect" -import { ServiceConfig } from "../../../services/service-config" -import { resolveIntegration } from "./resolve" - -const location = { directory: process.cwd() } - -export default Runtime.handler( - Commands.commands.mcp.commands.logout, - Effect.fn("cli.mcp.logout")(function* (input) { - const options = yield* ServiceConfig.options() - const found = yield* Service.discover(options) - const endpoint = found ?? (yield* Service.start(options)) - const client = createOpencodeClient({ baseUrl: endpoint.url, headers: Service.headers(endpoint) }) - - const integration = yield* resolveIntegration(client, input.name, location) - if (!integration) { - process.stdout.write(`No stored credentials for ${input.name}` + EOL) - return - } - - const credentials = integration.connections.filter((connection) => connection.type === "credential") - if (credentials.length === 0) { - process.stdout.write(`No stored credentials for ${input.name}` + EOL) - return - } - - yield* Effect.forEach( - credentials, - (connection) => Effect.promise(() => client.v2.credential.remove({ credentialID: connection.id, location })), - { discard: true }, - ) - process.stdout.write(`Removed OAuth credentials for ${input.name}` + EOL) - }), -) diff --git a/packages/cli/src/commands/handlers/mcp/resolve.ts b/packages/cli/src/commands/handlers/mcp/resolve.ts deleted file mode 100644 index 58580e6def..0000000000 --- a/packages/cli/src/commands/handlers/mcp/resolve.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { Effect } from "effect" -import type { OpencodeClient } from "@opencode-ai/sdk/v2/client" - -// Resolve through the MCP-owned integrationID rather than matching integration names: the shared -// integration registry also holds provider/plugin integrations, whose names could collide with a server. -// Fails when the server is unknown; returns undefined when the server has no integration (e.g. a local -// or anonymous server), leaving that case for the caller to interpret. -export const resolveIntegration = (client: OpencodeClient, name: string, location: { directory: string }) => - Effect.gen(function* () { - const servers = yield* Effect.promise(() => client.v2.mcp.list({ location })) - const server = (servers.data?.data ?? []).find((entry) => entry.name === name) - if (!server) return yield* Effect.fail(new Error(`MCP server not found: ${name}`)) - const integrationID = server.integrationID - if (!integrationID) return undefined - const found = yield* Effect.promise(() => client.v2.integration.get({ integrationID, location })) - return found.data?.data - }) diff --git a/packages/cli/src/commands/handlers/migrate.ts b/packages/cli/src/commands/handlers/migrate.ts index 6ff6939aa1..c73c7750df 100644 --- a/packages/cli/src/commands/handlers/migrate.ts +++ b/packages/cli/src/commands/handlers/migrate.ts @@ -1,4 +1,4 @@ -import { Effect } from "effect" +import * as Effect from "effect/Effect" import { Commands } from "../commands" import { Runtime } from "../../framework/runtime" diff --git a/packages/cli/src/commands/handlers/mini.ts b/packages/cli/src/commands/handlers/mini.ts deleted file mode 100644 index 7919b2bc8e..0000000000 --- a/packages/cli/src/commands/handlers/mini.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { Effect, Option } from "effect" -import { Commands } from "../commands" -import { Runtime } from "../../framework/runtime" -import { Server } from "../../services/server" - -export default Runtime.handler(Commands.commands.mini, (input) => - Effect.gen(function* () { - const { runMini, validateMiniTerminal } = yield* Effect.promise(() => import("../../mini")) - yield* Effect.promise(async () => validateMiniTerminal()) - const serverURL = Option.getOrUndefined(input.server) - const server = yield* Server.resolve({ server: serverURL, standalone: input.standalone }) - yield* Effect.promise(() => - runMini({ - server, - continue: input.continue, - session: Option.getOrUndefined(input.session), - fork: input.fork, - model: Option.getOrUndefined(input.model), - agent: Option.getOrUndefined(input.agent), - prompt: Option.getOrUndefined(input.prompt), - replay: input.replay, - replayLimit: Option.getOrUndefined(input.replayLimit), - demo: input.demo, - }), - ) - }), -) diff --git a/packages/cli/src/commands/handlers/pair.ts b/packages/cli/src/commands/handlers/pair.ts deleted file mode 100644 index 5c40a20444..0000000000 --- a/packages/cli/src/commands/handlers/pair.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { EOL } from "os" -import { Effect } from "effect" -import { Service } from "@opencode-ai/client/effect" -import { OpenCode } from "@opencode-ai/client/promise" -import { renderUnicodeCompact } from "uqr" -import { Commands } from "../commands" -import { Runtime } from "../../framework/runtime" -import { ServiceConfig } from "../../services/service-config" - -export default Runtime.handler( - Commands.commands.pair, - Effect.fn("cli.pair")(function* () { - const endpoint = yield* Service.start(yield* ServiceConfig.options()) - const password = yield* ServiceConfig.password() - const server = yield* Effect.tryPromise(() => - OpenCode.make({ baseUrl: endpoint.url, headers: Service.headers(endpoint) }).server.get(), - ) - const info = { urls: server.urls, username: "opencode", password } - process.stdout.write( - [ - "", - ` URLs ${info.urls[0] ?? "(none)"}`, - ...info.urls.slice(1).map((url) => ` ${url}`), - ` Username ${info.username}`, - ` Password ${info.password}`, - "", - " Scan to pair", - "", - renderUnicodeCompact(JSON.stringify(info), { border: 2 }) - .split(EOL) - .map((line) => " " + line) - .join(EOL), - "", - ].join(EOL) + EOL, - ) - - const hostname = new URL(endpoint.url).hostname - if (!["localhost", "127.0.0.1", "[::1]"].includes(hostname)) return - process.stderr.write( - ` Run \`opencode service set hostname 0.0.0.0\` to access the service remotely.${EOL}${EOL}`, - ) - }), -) diff --git a/packages/cli/src/commands/handlers/run.ts b/packages/cli/src/commands/handlers/run.ts deleted file mode 100644 index e8abf0cb1e..0000000000 --- a/packages/cli/src/commands/handlers/run.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { Effect, Option } from "effect" -import { Commands } from "../commands" -import { Runtime } from "../../framework/runtime" -import { Server } from "../../services/server" - -export default Runtime.handler(Commands.commands.run, (input) => - Effect.gen(function* () { - const { runNonInteractive } = yield* Effect.promise(() => import("../../mini")) - const separator = process.argv.indexOf("--", 2) - const server = yield* Server.resolve({ - server: Option.getOrUndefined(input.server), - standalone: input.standalone, - }) - yield* Effect.promise(() => - runNonInteractive({ - server, - message: [...input.message, ...(separator === -1 ? [] : process.argv.slice(separator + 1))], - continue: input.continue, - session: Option.getOrUndefined(input.session), - fork: input.fork, - model: Option.getOrUndefined(input.model), - agent: Option.getOrUndefined(input.agent), - format: input.format, - file: [...input.file], - title: Option.getOrUndefined(input.title), - thinking: input.thinking, - auto: input.auto || input.yolo, - }), - ) - }), -) diff --git a/packages/cli/src/commands/handlers/serve.ts b/packages/cli/src/commands/handlers/serve.ts index bd3e0c1217..19b097453d 100644 --- a/packages/cli/src/commands/handlers/serve.ts +++ b/packages/cli/src/commands/handlers/serve.ts @@ -1,16 +1,46 @@ -import { Effect, Option } from "effect" +import { NodeHttpServer } from "@effect/platform-node" +import { Credential } from "@opencode-ai/core/credential" +import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" +import { LayerNode } from "@opencode-ai/core/effect/layer-node" +import { PermissionSaved } from "@opencode-ai/core/permission/saved" +import { Context, Layer, Option } from "effect" +import * as Effect from "effect/Effect" +import { HttpRouter, HttpServer } from "effect/unstable/http" +import { createServer } from "node:http" +import { createRoutes } from "@opencode-ai/server/routes" import { Commands } from "../commands" import { Runtime } from "../../framework/runtime" -import { ServerProcess } from "../../server-process" +import { Daemon } from "../../services/daemon" export default Runtime.handler( Commands.commands.serve, Effect.fn("cli.serve")(function* (input) { - if (input.service && input.stdio) return yield* Effect.fail(new Error("--service and --stdio cannot be combined")) - return yield* ServerProcess.run({ - mode: input.service ? "service" : input.stdio ? "stdio" : "default", - hostname: Option.getOrUndefined(input.hostname), - port: Option.getOrUndefined(input.port), - }) + return yield* Effect.scoped( + Effect.gen(function* () { + const daemon = yield* Daemon.Service + const address = yield* listen(input.hostname, input.port, yield* daemon.password()) + if (input.register) yield* daemon.register(address) + console.log(`server listening on ${HttpServer.formatAddress(address)}`) + return yield* Effect.never + }), + ) }), ) + +function listen(hostname: string, port: Option.Option, password: string) { + if (Option.isSome(port)) return bind(hostname, port.value, password) + const next = (port: number): ReturnType => + bind(hostname, port, password).pipe( + Effect.catch((error) => (port === 65_535 ? Effect.fail(error) : next(port + 1))), + ) + return next(4096) +} + +function bind(hostname: string, port: number, password: string) { + return Layer.build( + HttpRouter.serve(createRoutes(password), { disableListenLog: true, disableLogger: true }).pipe( + Layer.provideMerge(NodeHttpServer.layer(() => createServer(), { port, host: hostname })), + Layer.provide(AppNodeBuilder.build(LayerNode.group([Credential.node, PermissionSaved.node]))), + ), + ).pipe(Effect.map((context) => Context.get(context, HttpServer.HttpServer).address)) +} diff --git a/packages/cli/src/commands/handlers/service/get.ts b/packages/cli/src/commands/handlers/service/get.ts deleted file mode 100644 index fd4b9af384..0000000000 --- a/packages/cli/src/commands/handlers/service/get.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { EOL } from "os" -import { Effect, Option } from "effect" -import { Commands } from "../../commands" -import { Runtime } from "../../../framework/runtime" -import { ServiceConfig } from "../../../services/service-config" - -export default Runtime.handler( - Commands.commands.service.commands.get, - Effect.fn("cli.service.get")(function* (input) { - process.stdout.write((yield* ServiceConfig.get(Option.getOrUndefined(input.key))) + EOL) - }), -) diff --git a/packages/cli/src/commands/handlers/service/password.ts b/packages/cli/src/commands/handlers/service/password.ts new file mode 100644 index 0000000000..6bf49d50d0 --- /dev/null +++ b/packages/cli/src/commands/handlers/service/password.ts @@ -0,0 +1,16 @@ +import { EOL } from "os" +import { Option } from "effect" +import * as Effect from "effect/Effect" +import { Commands } from "../../commands" +import { Runtime } from "../../../framework/runtime" +import { Daemon } from "../../../services/daemon" + +export default Runtime.handler( + Commands.commands.service.commands.password, + Effect.fn("cli.service.password")(function* (input) { + const daemon = yield* Daemon.Service + const value = Option.getOrUndefined(input.value) + if (value !== undefined) yield* daemon.stop() + process.stdout.write((yield* daemon.password(value)) + EOL) + }), +) diff --git a/packages/cli/src/commands/handlers/service/restart.ts b/packages/cli/src/commands/handlers/service/restart.ts index 93b8836acd..d348987d16 100644 --- a/packages/cli/src/commands/handlers/service/restart.ts +++ b/packages/cli/src/commands/handlers/service/restart.ts @@ -1,16 +1,14 @@ import { EOL } from "os" -import { Effect } from "effect" -import { Service } from "@opencode-ai/client/effect" +import * as Effect from "effect/Effect" import { Commands } from "../../commands" import { Runtime } from "../../../framework/runtime" -import { ServiceConfig } from "../../../services/service-config" +import { Daemon } from "../../../services/daemon" export default Runtime.handler( Commands.commands.service.commands.restart, Effect.fn("cli.service.restart")(function* () { - const options = yield* ServiceConfig.options() - yield* Service.stop(options) - const transport = yield* Service.start(options) - process.stdout.write(transport.url + EOL) + const daemon = yield* Daemon.Service + yield* daemon.stop() + process.stdout.write((yield* daemon.start()) + EOL) }), ) diff --git a/packages/cli/src/commands/handlers/service/set.ts b/packages/cli/src/commands/handlers/service/set.ts deleted file mode 100644 index f761c02411..0000000000 --- a/packages/cli/src/commands/handlers/service/set.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Effect } from "effect" -import { Commands } from "../../commands" -import { Runtime } from "../../../framework/runtime" -import { ServiceConfig } from "../../../services/service-config" - -export default Runtime.handler( - Commands.commands.service.commands.set, - Effect.fn("cli.service.set")(function* (input) { - yield* ServiceConfig.set(input.key, input.value) - }), -) diff --git a/packages/cli/src/commands/handlers/service/start.ts b/packages/cli/src/commands/handlers/service/start.ts index 602a26ecf1..0d6fbaada9 100644 --- a/packages/cli/src/commands/handlers/service/start.ts +++ b/packages/cli/src/commands/handlers/service/start.ts @@ -1,14 +1,12 @@ import { EOL } from "os" -import { Effect } from "effect" -import { Service } from "@opencode-ai/client/effect" +import * as Effect from "effect/Effect" import { Commands } from "../../commands" import { Runtime } from "../../../framework/runtime" -import { ServiceConfig } from "../../../services/service-config" +import { Daemon } from "../../../services/daemon" export default Runtime.handler( Commands.commands.service.commands.start, Effect.fn("cli.service.start")(function* () { - const transport = yield* Service.start(yield* ServiceConfig.options()) - process.stdout.write(transport.url + EOL) + process.stdout.write((yield* (yield* Daemon.Service).start()) + EOL) }), ) diff --git a/packages/cli/src/commands/handlers/service/status.ts b/packages/cli/src/commands/handlers/service/status.ts index bf58968eef..d409970e8b 100644 --- a/packages/cli/src/commands/handlers/service/status.ts +++ b/packages/cli/src/commands/handlers/service/status.ts @@ -1,14 +1,13 @@ import { EOL } from "os" -import { Effect } from "effect" -import { Service } from "@opencode-ai/client/effect" +import * as Effect from "effect/Effect" import { Commands } from "../../commands" import { Runtime } from "../../../framework/runtime" -import { ServiceConfig } from "../../../services/service-config" +import { Daemon } from "../../../services/daemon" export default Runtime.handler( Commands.commands.service.commands.status, Effect.fn("cli.service.status")(function* () { - const found = yield* Service.discover(yield* ServiceConfig.options()) - process.stdout.write((found ? found.url : "stopped") + EOL) + const url = yield* (yield* Daemon.Service).status() + process.stdout.write((url ? `running ${url}` : "stopped") + EOL) }), ) diff --git a/packages/cli/src/commands/handlers/service/stop.ts b/packages/cli/src/commands/handlers/service/stop.ts index 5bf45ccabc..8da9b04cff 100644 --- a/packages/cli/src/commands/handlers/service/stop.ts +++ b/packages/cli/src/commands/handlers/service/stop.ts @@ -1,12 +1,11 @@ -import { Effect } from "effect" -import { Service } from "@opencode-ai/client/effect" +import * as Effect from "effect/Effect" import { Commands } from "../../commands" import { Runtime } from "../../../framework/runtime" -import { ServiceConfig } from "../../../services/service-config" +import { Daemon } from "../../../services/daemon" export default Runtime.handler( Commands.commands.service.commands.stop, Effect.fn("cli.service.stop")(function* () { - yield* Service.stop(yield* ServiceConfig.options()) + yield* (yield* Daemon.Service).stop() }), ) diff --git a/packages/cli/src/commands/handlers/service/unset.ts b/packages/cli/src/commands/handlers/service/unset.ts deleted file mode 100644 index cc738125d3..0000000000 --- a/packages/cli/src/commands/handlers/service/unset.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Effect } from "effect" -import { Commands } from "../../commands" -import { Runtime } from "../../../framework/runtime" -import { ServiceConfig } from "../../../services/service-config" - -export default Runtime.handler( - Commands.commands.service.commands.unset, - Effect.fn("cli.service.unset")(function* (input) { - yield* ServiceConfig.unset(input.key) - }), -) diff --git a/packages/cli/src/config/config.ts b/packages/cli/src/config/config.ts deleted file mode 100644 index d9f01b3fce..0000000000 --- a/packages/cli/src/config/config.ts +++ /dev/null @@ -1,109 +0,0 @@ -export * as Config from "./config" - -import { Global } from "@opencode-ai/core/global" -import { Context, Effect, FileSystem, Layer, Option, Schema, Semaphore } from "effect" -import { produce, type Draft } from "immer" -import { applyEdits, modify, parse, type ParseError } from "jsonc-parser" -import path from "path" -import { ConfigMigration } from "./migrate" -import { Info } from "./schema" - -export * from "./schema" - -export interface Interface { - readonly path: string - readonly get: () => Effect.Effect - readonly update: (update: (draft: Draft) => void) => Effect.Effect -} - -export class Service extends Context.Service()("@opencode/cli/config/Config") {} - -const decode = Schema.decodeUnknownOption(Info) -const decodeRecord = Schema.decodeUnknownOption(Schema.Record(Schema.String, Schema.Any)) -const empty: Info = {} - -export const layer = Layer.effect( - Service, - Effect.gen(function* () { - const fs = yield* FileSystem.FileSystem - const global = yield* Global.Service - const file = path.join(global.config, "cli.json") - const lock = yield* Semaphore.make(1) - - const readJson = Effect.fnUntraced(function* () { - const text = yield* fs.readFileString(file).pipe(Effect.catch(() => Effect.succeed(undefined))) - if (text === undefined) return undefined - const errors: ParseError[] = [] - const value: any = parse(text, errors, { allowTrailingComma: true }) - if (errors.length) return undefined - return Option.getOrUndefined(decodeRecord(value)) - }) - - const write = Effect.fnUntraced(function* (text: string) { - const temp = file + ".tmp" - yield* fs.makeDirectory(path.dirname(file), { recursive: true }) - yield* fs.writeFileString(temp, text, { mode: 0o600 }) - yield* fs.rename(temp, file) - }) - - const migrate = ConfigMigration.run({ file, config: global.config, state: global.state }).pipe( - Effect.provideService(FileSystem.FileSystem, fs), - ) - - const get = Effect.fn("cli.config.get")(function* () { - yield* migrate.pipe(Effect.catchCause((cause) => Effect.logWarning("failed to migrate cli config", { cause }))) - return Option.getOrElse(decode(yield* readJson()), () => empty) - }) - - const update = Effect.fn("cli.config.update")((update: (draft: Draft) => void) => - lock - .withPermits(1)( - Effect.gen(function* () { - yield* migrate - const current = Option.getOrElse(decode(yield* readJson()), () => empty) - const next = produce(current, update) - const edits = changes(current, next) - if (!edits.length) return current - const text = yield* fs.readFileString(file).pipe(Effect.catch(() => Effect.succeed("{}"))) - const updated = edits.reduce( - (text, edit) => - applyEdits( - text, - modify(text, edit.path, edit.value, { formattingOptions: { tabSize: 2, insertSpaces: true } }), - ), - text, - ) - const errors: ParseError[] = [] - const config = Option.getOrUndefined(decode(parse(updated, errors, { allowTrailingComma: true }))) - if (errors.length || config === undefined) return yield* Effect.fail(new Error("Invalid CLI config update")) - yield* write(updated.endsWith("\n") ? updated : updated + "\n") - return config - }), - ) - .pipe(Effect.mapError((cause) => new Error("Failed to update CLI config", { cause }))), - ) - - return Service.of({ path: file, get, update }) - }), -) - -type Edit = { readonly path: (string | number)[]; readonly value: any } - -function changes(before: any, after: any, path: (string | number)[] = []): Edit[] { - if (Object.is(before, after)) return [] - if ( - before !== null && - after !== null && - typeof before === "object" && - typeof after === "object" && - !Array.isArray(before) && - !Array.isArray(after) - ) { - return [...new Set([...Object.keys(before), ...Object.keys(after)])].flatMap((key) => { - if (!(key in after)) return [{ path: [...path, key], value: undefined }] - if (!(key in before)) return [{ path: [...path, key], value: after[key] }] - return changes(before[key], after[key], [...path, key]) - }) - } - return [{ path, value: after }] -} diff --git a/packages/cli/src/config/index.ts b/packages/cli/src/config/index.ts deleted file mode 100644 index 60e39c3163..0000000000 --- a/packages/cli/src/config/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * as Config from "./config" diff --git a/packages/cli/src/config/migrate.ts b/packages/cli/src/config/migrate.ts deleted file mode 100644 index bf57fdb317..0000000000 --- a/packages/cli/src/config/migrate.ts +++ /dev/null @@ -1,142 +0,0 @@ -export * as ConfigMigration from "./migrate" - -import { TuiConfigV1 } from "@opencode-ai/tui/config/v1" -import { Effect, FileSystem, Option, Schema } from "effect" -import { parse, type ParseError } from "jsonc-parser" -import path from "path" -import type { Info } from "./schema" - -const decodeV1 = Schema.decodeUnknownOption(TuiConfigV1.Info) -const decodeRecord = Schema.decodeUnknownOption(Schema.Record(Schema.String, Schema.Any)) - -export const run = Effect.fn("cli.config.migrate")(function* (input: { - readonly file: string - readonly config: string - readonly state: string -}) { - const fs = yield* FileSystem.FileSystem - if (yield* fs.exists(input.file).pipe(Effect.orElseSucceed(() => false))) return - - const legacyValue = yield* readJson(path.join(input.config, "tui.json")) - const legacy = Option.getOrUndefined(decodeV1(legacyValue)) - const kv = yield* readJson(path.join(input.state, "kv.json")) - const migrated = migrateV1(legacy, kv ?? {}) - if (!Object.keys(migrated).length) return - - const temp = input.file + ".tmp" - yield* fs.makeDirectory(path.dirname(input.file), { recursive: true }) - yield* fs.writeFileString(temp, JSON.stringify(migrated, null, 2) + "\n", { mode: 0o600 }) - yield* fs.rename(temp, input.file) - yield* Effect.logInfo("migrated cli config", { - from: [ - legacyValue === undefined ? undefined : path.join(input.config, "tui.json"), - kv === undefined ? undefined : path.join(input.state, "kv.json"), - ].filter(Boolean), - to: input.file, - }) -}) - -export function migrateV1(legacy: TuiConfigV1.Info | undefined, kv: Record): Info { - const plugins = [ - ...(legacy?.plugin?.map((plugin) => - typeof plugin === "string" ? plugin : { package: plugin[0], options: plugin[1] }, - ) ?? []), - ...Object.entries(legacy?.plugin_enabled ?? {}).map(([id, enabled]) => (enabled ? id : `-${id}`)), - ] - const themeName = legacy?.theme ?? kv.theme - const themeMode = kv.theme_mode_lock - const attentionSoundPack = kv.attention_sound_pack - const diffView = kv.diff_viewer_view ?? (legacy?.diff_style === "stacked" ? "unified" : undefined) - const thinking = - kv.thinking_mode ?? - (kv.thinking_visibility === undefined ? undefined : kv.thinking_visibility ? "show" : "hide") - - return { - ...(themeName !== undefined || themeMode !== undefined - ? { theme: { ...(themeName === undefined ? {} : { name: themeName }), ...(themeMode === undefined ? {} : { mode: themeMode }) } } - : {}), - ...(legacy?.keybinds === undefined ? {} : { keybinds: legacy.keybinds }), - ...(plugins.length ? { plugins } : {}), - ...(legacy?.leader_timeout === undefined ? {} : { leader: { timeout: legacy.leader_timeout } }), - ...(legacy?.scroll_speed === undefined && legacy?.scroll_acceleration?.enabled === undefined - ? {} - : { - scroll: { - ...(legacy.scroll_speed === undefined ? {} : { speed: legacy.scroll_speed }), - ...(legacy.scroll_acceleration?.enabled === undefined - ? {} - : { acceleration: legacy.scroll_acceleration.enabled }), - }, - }), - ...(legacy?.attention === undefined && attentionSoundPack === undefined - ? {} - : { - attention: { - ...legacy?.attention, - ...(attentionSoundPack === undefined ? {} : { sound_pack: attentionSoundPack }), - }, - }), - ...(legacy?.diff_style === undefined && - kv.diff_wrap_mode === undefined && - kv.diff_viewer_show_file_tree === undefined && - kv.diff_viewer_single_patch === undefined && - diffView === undefined - ? {} - : { - diffs: { - ...(kv.diff_wrap_mode === undefined ? {} : { wrap: kv.diff_wrap_mode }), - ...(kv.diff_viewer_show_file_tree === undefined ? {} : { tree: kv.diff_viewer_show_file_tree }), - ...(kv.diff_viewer_single_patch === undefined ? {} : { single: kv.diff_viewer_single_patch }), - ...(diffView === undefined ? {} : { view: diffView }), - }, - }), - ...(kv.terminal_title_enabled === undefined ? {} : { terminal: { title: kv.terminal_title_enabled } }), - ...(kv.file_context_enabled === undefined && kv.paste_summary_enabled === undefined - ? {} - : { - prompt: { - ...(kv.file_context_enabled === undefined ? {} : { editor: kv.file_context_enabled }), - ...(kv.paste_summary_enabled === undefined - ? {} - : { paste: kv.paste_summary_enabled ? ("compact" as const) : ("full" as const) }), - }, - }), - ...(kv.sidebar === undefined && - kv.scrollbar_visible === undefined && - thinking === undefined && - kv.exploration_grouping === undefined - ? {} - : { - session: { - ...(kv.sidebar === undefined ? {} : { sidebar: kv.sidebar }), - ...(kv.scrollbar_visible === undefined ? {} : { scrollbar: kv.scrollbar_visible }), - ...(thinking === undefined ? {} : { thinking }), - ...(kv.exploration_grouping === undefined - ? {} - : { grouping: kv.exploration_grouping ? ("auto" as const) : ("none" as const) }), - }, - }), - ...(kv.tips_hidden === undefined && kv.dismissed_getting_started === undefined - ? {} - : { - hints: { - ...(kv.tips_hidden === undefined ? {} : { tips: !kv.tips_hidden }), - ...(kv.dismissed_getting_started === undefined - ? {} - : { onboarding: !kv.dismissed_getting_started }), - }, - }), - ...(kv.animations_enabled === undefined ? {} : { animations: kv.animations_enabled }), - ...(legacy?.mouse === undefined ? {} : { mouse: legacy.mouse }), - } -} - -const readJson = Effect.fnUntraced(function* (target: string) { - const fs = yield* FileSystem.FileSystem - const text = yield* fs.readFileString(target).pipe(Effect.catch(() => Effect.succeed(undefined))) - if (text === undefined) return undefined - const errors: ParseError[] = [] - const value: any = parse(text, errors, { allowTrailingComma: true }) - if (errors.length) return undefined - return Option.getOrUndefined(decodeRecord(value)) -}) diff --git a/packages/cli/src/config/schema.ts b/packages/cli/src/config/schema.ts deleted file mode 100644 index fc6b9724c6..0000000000 --- a/packages/cli/src/config/schema.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { Config } from "@opencode-ai/tui/config" -import { Schema } from "effect" - -export const Info = Schema.Struct({ ...Config.Info.fields }) -export type Info = Schema.Schema.Type diff --git a/packages/cli/src/env.ts b/packages/cli/src/env.ts deleted file mode 100644 index 6cc76b793f..0000000000 --- a/packages/cli/src/env.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Config } from "effect" - -// Every environment variable the CLI reads, in one place. Consumers yield -// these instead of touching process.env so the full surface stays visible, -// typed, and redacted where secret. - -// The opencode server password: sent by clients connecting to an explicit -// --server, and adopted by a manually run or standalone server. The legacy -// name is still honored. -export const password = Config.redacted("OPENCODE_PASSWORD").pipe( - Config.orElse(() => Config.redacted("OPENCODE_SERVER_PASSWORD")), - Config.withDefault(undefined), -) - -export * as Env from "./env" diff --git a/packages/cli/src/framework/runtime.ts b/packages/cli/src/framework/runtime.ts index cffd80e772..97247e4d6b 100644 --- a/packages/cli/src/framework/runtime.ts +++ b/packages/cli/src/framework/runtime.ts @@ -1,9 +1,7 @@ -import { Effect, FileSystem, Scope } from "effect" -import { Command } from "effect/unstable/cli" +import * as Effect from "effect/Effect" +import * as Command from "effect/unstable/cli/Command" import { Spec } from "./spec" -import { Global } from "@opencode-ai/core/global" -import { Updater } from "../services/updater" -import { Config } from "../config" +import { Daemon } from "../services/daemon" export type Input = Value extends Spec.Node @@ -12,29 +10,11 @@ export type Input = ? Input : never -type RuntimeHandler = ( - input: unknown, -) => Effect.Effect< - void, - unknown, - FileSystem.FileSystem | Global.Service | Updater.Service | Config.Service | Scope.Scope -> +type RuntimeHandler = (input: unknown) => Effect.Effect type Loader = () => Promise<{ - default: ( - input: Input, - ) => Effect.Effect< - void, - any, - FileSystem.FileSystem | Global.Service | Updater.Service | Config.Service | Scope.Scope - > + default: (input: Input) => Effect.Effect }> -type ProvidedCommand = Command.Command< - string, - unknown, - unknown, - unknown, - FileSystem.FileSystem | Global.Service | Updater.Service | Config.Service | Scope.Scope -> +type ProvidedCommand = Command.Command export type Handlers = keyof Node["commands"] extends never ? Loader diff --git a/packages/cli/src/framework/spec.ts b/packages/cli/src/framework/spec.ts index 345a0cfa85..3bb47e5e5e 100644 --- a/packages/cli/src/framework/spec.ts +++ b/packages/cli/src/framework/spec.ts @@ -1,4 +1,4 @@ -import { Command } from "effect/unstable/cli" +import * as Command from "effect/unstable/cli/Command" type Options> = { readonly description?: string diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index 2128495526..4b9303f7c3 100755 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -1,17 +1,11 @@ #!/usr/bin/env bun -import { NodeRuntime, NodeServices } from "@effect/platform-node" -import { Effect } from "effect" +import * as NodeRuntime from "@effect/platform-node/NodeRuntime" +import * as NodeServices from "@effect/platform-node/NodeServices" +import * as Effect from "effect/Effect" import { Commands } from "./commands/commands" import { Runtime } from "./framework/runtime" -import { Observability } from "@opencode-ai/core/observability" -import { Updater } from "./services/updater" -import { InstallationChannel, InstallationVersion, InstallationLocal } from "@opencode-ai/core/installation/version" -import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" -import { LayerNode } from "@opencode-ai/core/effect/layer-node" -import { Global } from "@opencode-ai/core/global" -import { AppProcess } from "@opencode-ai/core/process" -import { Config } from "./config" +import { Daemon } from "./services/daemon" const Handlers = Runtime.handlers(Commands, { $: () => import("./commands/handlers/default"), @@ -19,45 +13,20 @@ const Handlers = Runtime.handlers(Commands, { debug: { agents: () => import("./commands/handlers/debug/agents"), }, - console: { - login: () => import("./commands/handlers/console/login"), - }, - mcp: { - list: () => import("./commands/handlers/mcp/list"), - add: () => import("./commands/handlers/mcp/add"), - auth: () => import("./commands/handlers/mcp/auth"), - logout: () => import("./commands/handlers/mcp/logout"), - }, migrate: () => import("./commands/handlers/migrate"), - mini: () => import("./commands/handlers/mini"), - run: () => import("./commands/handlers/run"), - pair: () => import("./commands/handlers/pair"), service: { start: () => import("./commands/handlers/service/start"), restart: () => import("./commands/handlers/service/restart"), status: () => import("./commands/handlers/service/status"), stop: () => import("./commands/handlers/service/stop"), - get: () => import("./commands/handlers/service/get"), - set: () => import("./commands/handlers/service/set"), - unset: () => import("./commands/handlers/service/unset"), + password: () => import("./commands/handlers/service/password"), }, serve: () => import("./commands/handlers/serve"), }) -Effect.logInfo("cli starting", { - version: InstallationVersion, - channel: InstallationChannel, - local: InstallationLocal, - args: process.argv.slice(2), -}).pipe( - Effect.flatMap(() => Runtime.run(Commands, Handlers, { version: InstallationVersion })), - Effect.annotateLogs({ role: "cli" }), - Effect.provide(Config.layer), - Effect.provide(Updater.layer), - Effect.provide(AppNodeBuilder.build(LayerNode.group([Global.node, AppProcess.node]))), - Effect.provide(Observability.layer), +Runtime.run(Commands, Handlers, { version: "local" }).pipe( + Effect.provide(Daemon.layer), Effect.provide(NodeServices.layer), Effect.scoped, - Effect.tap(() => Effect.sync(() => process.exit(process.exitCode ?? 0))), NodeRuntime.runMain, ) diff --git a/packages/cli/src/mini/catalog.shared.ts b/packages/cli/src/mini/catalog.shared.ts deleted file mode 100644 index 0868d3a94b..0000000000 --- a/packages/cli/src/mini/catalog.shared.ts +++ /dev/null @@ -1,159 +0,0 @@ -import type { - AgentListOutput, - CommandListOutput, - ModelListOutput, - OpenCodeClient, - ProviderListOutput, - SkillListOutput, -} from "@opencode-ai/client/promise" -import type { RunAgent, RunCommand, RunProvider, RunReference } from "./types" - -type CurrentAgent = AgentListOutput["data"][number] -type CurrentCommand = CommandListOutput["data"][number] -type CurrentSkill = SkillListOutput["data"][number] -type CurrentProvider = ProviderListOutput["data"][number] -type CurrentModel = ModelListOutput["data"][number] - -function location(directory: string, workspace?: string) { - return { - location: { - directory, - workspace, - }, - } -} - -function defaultCost(model: CurrentModel) { - const picked = model.cost.find((cost) => cost.tier === undefined) ?? model.cost[0] - if (!picked) { - return undefined - } - - return { - ...picked, - input: model.cost.every((cost) => cost.input === 0) ? 0 : picked.input, - } -} - -export function runAgent(input: CurrentAgent): RunAgent { - return { - id: input.id, - name: input.name, - description: input.description, - mode: input.mode, - hidden: input.hidden, - } -} - -export function runCommand(input: CurrentCommand): RunCommand { - return { - name: input.name, - description: input.description, - } -} - -export function runSkill(input: CurrentSkill): RunCommand { - return { - name: input.id, - description: input.description, - source: "skill", - } -} - -export function runProviders(providers: CurrentProvider[], models: CurrentModel[]): RunProvider[] { - const grouped = new Map() - - for (const provider of providers) { - grouped.set(provider.id, { - id: provider.id, - name: provider.name, - models: {}, - }) - } - - for (const model of models) { - const provider = grouped.get(model.providerID) ?? { - id: model.providerID, - name: model.providerID, - models: {}, - } - provider.models[model.id] = { - id: model.id, - providerID: model.providerID, - name: model.name, - capabilities: model.capabilities, - cost: defaultCost(model), - limit: model.limit, - status: model.status, - variants: Object.fromEntries((model.variants ?? []).map((variant) => [variant.id, {}])), - } - grouped.set(provider.id, provider) - } - - return [...grouped.values()] -} - -// A location boots its plugins in a deferred background batch after the layer -// is built, so first-turn model resolution can observe empty catalog state. -// For explicit --model flows, wait for that exact ref to appear before prompt -// admission. On timeout, return and let the real execution error surface. -export async function waitForCatalogReady(input: { - sdk: OpenCodeClient - directory: string - workspace?: string - model: { providerID: string; modelID: string } - timeoutMs?: number -}) { - const deadline = Date.now() + (input.timeoutMs ?? 5_000) - while (Date.now() < deadline) { - const models = await input.sdk.model - .list(location(input.directory, input.workspace)) - .then((result) => result.data) - .catch(() => undefined) - if (models?.some((model) => model.providerID === input.model.providerID && model.id === input.model.modelID)) return - await new Promise((resolve) => setTimeout(resolve, 25)) - } -} - -export async function waitForDefaultModel(input: { - sdk: OpenCodeClient - directory: string - timeoutMs?: number - active?: () => boolean -}): Promise<{ providerID: string; modelID: string } | undefined> { - const deadline = Date.now() + (input.timeoutMs ?? 5_000) - while (Date.now() < deadline && (input.active?.() ?? true)) { - const model = await input.sdk.model - .default(location(input.directory)) - .then((result) => result.data) - .catch(() => undefined) - if (model) return { providerID: model.providerID, modelID: model.id } - await new Promise((resolve) => setTimeout(resolve, 25)) - } -} - -export async function loadRunAgents(sdk: OpenCodeClient, directory: string): Promise { - const result = await sdk.agent.list(location(directory)) - return result.data.map(runAgent) -} - -export async function loadRunCommands(sdk: OpenCodeClient, directory: string): Promise { - const [commands, skills] = await Promise.all([ - sdk.command.list(location(directory)), - sdk.skill.list(location(directory)), - ]) - return [...commands.data.map(runCommand), ...skills.data.filter((skill) => skill.slash !== false).map(runSkill)] -} - -export async function loadRunReferences(sdk: OpenCodeClient, directory: string): Promise { - const result = await sdk.reference.list(location(directory)) - return result.data.filter((reference) => !reference.hidden) -} - -export async function loadRunProviders(sdk: OpenCodeClient, directory: string): Promise { - const [providers, models] = await Promise.all([ - sdk.provider.list(location(directory)), - sdk.model.list(location(directory)), - ]) - return runProviders([...providers.data], [...models.data]) -} diff --git a/packages/cli/src/mini/index.ts b/packages/cli/src/mini/index.ts deleted file mode 100644 index 4a6ef986fa..0000000000 --- a/packages/cli/src/mini/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -export { runMini, validateMiniTerminal, mergeInput as mergeInteractiveInput, type MiniCommandInput } from "./mini" -export { - runNonInteractive, - mergeInput as mergeNonInteractiveInput, - pickRunModel, - parseRunModel, - type RunCommandInput, -} from "./run" diff --git a/packages/cli/src/mini/mini.ts b/packages/cli/src/mini/mini.ts deleted file mode 100644 index 521995b45c..0000000000 --- a/packages/cli/src/mini/mini.ts +++ /dev/null @@ -1,176 +0,0 @@ -import { Service } from "@opencode-ai/client/effect" -import { OpenCode, type OpenCodeClient } from "@opencode-ai/client/promise" -import { Server } from "../services/server" -import { waitForCatalogReady } from "./catalog.shared" -import { INTERACTIVE_INPUT_ERROR, resolveInteractiveStdin } from "./runtime.stdin" -import type { RunInput, RunTuiConfig } from "./types" - -export type MiniCommandInput = { - server: Server.Resolved - continue?: boolean - session?: string - fork?: boolean - model?: string - agent?: string - prompt?: string - replay?: boolean - replayLimit?: number - demo?: boolean - tuiConfig?: RunTuiConfig | Promise -} - -type Session = Awaited> -export async function runMini(input: MiniCommandInput) { - validate(input) - const initialInput = mergeInput(process.stdin.isTTY ? undefined : await Bun.stdin.text(), input.prompt) - const runtimeTask = import("./runtime") - const directory = localDirectory() - - try { - const sdk = OpenCode.make({ - baseUrl: input.server.endpoint.url, - headers: Service.headers(input.server.endpoint), - }) - const model = parseModel(input.model) - let agentTask: Promise | undefined - const resolveAgent = () => { - agentTask ??= validateAgent(sdk, directory, input.agent) - return agentTask - } - const resolveSession = async () => { - const [agent, selected] = await Promise.all([ - resolveAgent(), - selectSession(sdk, directory, input), - ]) - const readyModel = - model ?? (selected?.model ? { providerID: selected.model.providerID, modelID: selected.model.id } : undefined) - if (readyModel) await waitForCatalogReady({ sdk, directory, model: readyModel }) - const session = selected ?? (await createSession(sdk, directory, agent, model)) - return { id: session.id, title: session.title, resume: selected !== undefined } - } - const create = ( - _sdk: OpenCodeClient, - next: { agent: string | undefined; model: RunInput["model"]; variant: string | undefined }, - ) => createSession(sdk, directory, next.agent, next.model, next.variant) - const runtime = await runtimeTask - await runtime.runInteractiveDeferredMode({ - sdk, - directory, - resolveAgent, - session: resolveSession, - createSession: create, - agent: input.agent, - model, - variant: undefined, - files: [], - initialInput, - thinking: true, - replay: input.replay ?? true, - replayLimit: input.replayLimit, - demo: input.demo, - tuiConfig: input.tuiConfig, - }) - } catch (error) { - if (error instanceof Error && error.message === INTERACTIVE_INPUT_ERROR) fail(error.message) - throw error - } -} - -export function validateMiniTerminal() { - if (!process.stdout.isTTY) fail("opencode mini requires a TTY stdout") -} - -/** @internal Exported for testing. */ -export function mergeInput(piped: string | undefined, prompt: string | undefined) { - if (!prompt) return piped || undefined - if (!piped) return prompt - return piped + "\n" + prompt -} - -function validate(input: MiniCommandInput) { - validateMiniTerminal() - if (input.replayLimit !== undefined && (!Number.isInteger(input.replayLimit) || input.replayLimit <= 0)) { - fail("--replay-limit must be a positive integer") - } - if (input.fork && !input.continue && !input.session) fail("--fork requires --continue or --session") - resolveInteractiveStdin().cleanup?.() -} - -function localDirectory(): string { - const root = process.env.PWD ?? process.cwd() - try { - process.chdir(root) - return process.cwd() - } catch { - fail(`Failed to change directory to ${root}`) - } -} - -function parseModel(value?: string): RunInput["model"] { - if (!value) return - const [providerID, ...rest] = value.split("/") - const modelID = rest.join("/") - if (!providerID || !modelID) fail("--model must use the format provider/model") - return { providerID, modelID } -} - -async function validateAgent(sdk: OpenCodeClient, directory: string, name?: string) { - if (!name) return - const deadline = Date.now() + 5_000 - let agents: Awaited> | undefined - while (Date.now() < deadline) { - agents = await sdk.agent.list({ location: { directory } }).catch(() => undefined) - const agent = agents?.data.find((item) => item.id === name) - if (agent?.mode === "subagent") { - warning(`agent "${name}" is a subagent, not a primary agent. Falling back to default agent`) - return - } - if (agent) return name - await Bun.sleep(25) - } - if (!agents) { - warning("failed to list agents. Falling back to default agent") - return - } - warning(`agent "${name}" not found. Falling back to default agent`) -} - -async function selectSession(sdk: OpenCodeClient, directory: string, input: MiniCommandInput, preselected?: Session) { - const selected = - preselected ?? - (input.session - ? await sdk.session.get({ sessionID: input.session }).catch(() => undefined) - : input.continue - ? await sdk.session - .list({ directory, parentID: null, limit: 1, order: "desc" }) - .then((result) => result.data[0]) - : undefined) - if (input.session && !selected) fail("Session not found") - if (!selected) return - if (!input.fork) return selected - return sdk.session.fork({ sessionID: selected.id }) -} - -async function createSession( - sdk: OpenCodeClient, - directory: string, - agent: string | undefined, - model: RunInput["model"], - variant?: string, -): Promise { - if (model) await waitForCatalogReady({ sdk, directory, model }) - return sdk.session.create({ - agent, - model: model ? { providerID: model.providerID, id: model.modelID, variant } : undefined, - location: { directory }, - }) -} - -function warning(message: string) { - process.stderr.write(`\x1b[93m\x1b[1m!\x1b[0m ${message}\n`) -} - -function fail(message: string): never { - process.stderr.write(`\x1b[91m\x1b[1mError: \x1b[0m${message}\n`) - process.exit(1) -} diff --git a/packages/cli/src/mini/noninteractive.ts b/packages/cli/src/mini/noninteractive.ts deleted file mode 100644 index 978bacacc5..0000000000 --- a/packages/cli/src/mini/noninteractive.ts +++ /dev/null @@ -1,488 +0,0 @@ -import type { EventSubscribeOutput, OpenCodeClient } from "@opencode-ai/client/promise" -import type { ReasoningPart, StepFinishPart, StepStartPart, TextPart, ToolPart } from "@opencode-ai/sdk/v2" -import { SessionMessage } from "@opencode-ai/schema/session-message" -import { EOL } from "node:os" -import { UI } from "./ui" - -type Model = { - providerID: string - modelID: string -} - -type File = { - url: string - filename: string - mime: string -} - -type Input = { - client: OpenCodeClient - sessionID: string - message: string - files: File[] - agent?: string - model?: Model - variant?: string - thinking: boolean - format: "default" | "json" - auto: boolean - /** True when the client is attached to a shared server rather than an exclusive in-process one. */ - attached: boolean - renderTool: (part: ToolPart) => Promise - renderToolError: (part: ToolPart) => Promise -} - -type StartedPart = { - id: string - timestamp: number -} - -type ToolState = StartedPart & { - assistantMessageID: string - tool: string - input: Record - raw?: string - provider?: unknown -} - -type V2Event = EventSubscribeOutput -type FormRequest = Extract["data"]["form"] - -// MCP elicitations are temporarily owned by the "global" sentinel instead of a real -// session. An exclusive local process may treat them as this run's blockers; an -// attached client must not cancel input that may belong to another session. -const GLOBAL_FORM_SESSION_ID = "global" - -export async function runNonInteractivePrompt(input: Input) { - const controller = new AbortController() - const stream = input.client.event.subscribe({ signal: controller.signal })[Symbol.asyncIterator]() - const connected = await stream.next() - if (connected.done) throw new Error("Event stream disconnected before prompt admission") - - const messageID = SessionMessage.ID.create() - const starts = new Map() - const tools = new Map() - let submitted = false - let promoted = false - let emittedError = false - let questionRejected = false - let permissionRejected = false - let formCancelled = false - let interrupted = false - let admission: AbortController | undefined - - const emit = (type: string, timestamp: number, data: Record) => { - if (input.format !== "json") return false - process.stdout.write(JSON.stringify({ type, timestamp, sessionID: input.sessionID, ...data }) + EOL) - return true - } - - const writeText = (part: TextPart, timestamp: number) => { - if (emit("text", timestamp, { part })) return - const text = part.text.trim() - if (!text) return - if (!process.stdout.isTTY) { - process.stdout.write(text + EOL) - return - } - UI.empty() - UI.println(text) - UI.empty() - } - - const replyPermission = async (request: { id: string; action: string; resources: ReadonlyArray }) => { - if (!input.auto) { - permissionRejected = true - UI.println( - UI.Style.TEXT_WARNING_BOLD + "!", - UI.Style.TEXT_NORMAL + - `permission requested: ${request.action} (${request.resources.join(", ")}); auto-rejecting`, - ) - } - await input.client.permission - .reply({ - sessionID: input.sessionID, - requestID: request.id, - reply: input.auto ? "once" : "reject", - }) - .catch(() => {}) - if (!input.auto) { - await input.client.session.interrupt({ sessionID: input.sessionID }).catch(() => {}) - } - } - - const rejectQuestion = async (request: { id: string }) => { - questionRejected = true - await input.client.question.reject({ sessionID: input.sessionID, requestID: request.id }).catch(() => {}) - } - - const cancelForm = async (request: Pick) => { - formCancelled = true - await input.client.form.cancel({ sessionID: request.sessionID, formID: request.id }).catch(() => {}) - } - - const consume = async () => { - while (!controller.signal.aborted) { - const next = await stream.next().catch((error) => { - if (!emittedError) throw error - return { done: true as const, value: undefined } - }) - if (next.done) { - if (emittedError) return - throw new Error("Event stream disconnected during prompt execution") - } - const event = next.value - - if (event.type === "permission.v2.asked" && submitted && event.data.sessionID === input.sessionID) { - await replyPermission(event.data) - continue - } - if (event.type === "question.v2.asked" && submitted && event.data.sessionID === input.sessionID) { - await rejectQuestion(event.data) - continue - } - if ( - event.type === "form.created" && - submitted && - (event.data.form.sessionID === input.sessionID || - (!input.attached && event.data.form.sessionID === GLOBAL_FORM_SESSION_ID)) - ) { - await cancelForm(event.data.form) - continue - } - if (!("sessionID" in event.data) || event.data.sessionID !== input.sessionID) continue - const time = toMillis("created" in event ? event.created : undefined) - - if (event.type === "session.input.promoted") { - if (event.data.inputID === messageID) { - promoted = true - continue - } - } - if ( - event.type === "session.execution.interrupted" && - event.data.reason === "user" && - (interrupted || permissionRejected || questionRejected || formCancelled) - ) { - return - } - if (!promoted) continue - - if (event.type === "session.step.started") { - const part: StepStartPart = { - id: partID(event.id), - sessionID: input.sessionID, - messageID: event.data.assistantMessageID, - type: "step-start", - snapshot: event.data.snapshot, - } - if (!emit("step_start", time, { part }) && input.format !== "json") { - UI.empty() - UI.println(`> ${event.data.agent} · ${event.data.model.id}`) - UI.empty() - } - continue - } - - if (event.type === "session.text.started") { - starts.set("text", { id: partID(event.id), timestamp: time }) - continue - } - if (event.type === "session.text.ended") { - const started = starts.get("text") - starts.delete("text") - const part: TextPart = { - id: started?.id ?? partID(event.id), - sessionID: input.sessionID, - messageID: event.data.assistantMessageID, - type: "text", - text: event.data.text, - time: { start: started?.timestamp ?? time, end: time }, - } - writeText(part, time) - continue - } - - if (event.type === "session.reasoning.started") { - starts.set("reasoning", { id: partID(event.id), timestamp: time }) - continue - } - if (event.type === "session.reasoning.ended" && input.thinking) { - const started = starts.get("reasoning") - starts.delete("reasoning") - const part: ReasoningPart = { - id: started?.id ?? partID(event.id), - sessionID: input.sessionID, - messageID: event.data.assistantMessageID, - type: "reasoning", - text: event.data.text, - metadata: event.data.state, - time: { start: started?.timestamp ?? time, end: time }, - } - if (emit("reasoning", time, { part })) continue - const text = part.text.trim() - if (!text) continue - const line = `Thinking: ${text}` - if (!process.stdout.isTTY) { - process.stdout.write(line + EOL) - continue - } - UI.empty() - UI.println(`${UI.Style.TEXT_DIM}\u001b[3m${line}\u001b[0m${UI.Style.TEXT_NORMAL}`) - UI.empty() - continue - } - - if (event.type === "session.tool.input.started") { - tools.set(event.data.callID, { - id: partID(event.id), - timestamp: time, - assistantMessageID: event.data.assistantMessageID, - tool: event.data.name, - input: {}, - }) - continue - } - if (event.type === "session.tool.input.ended") { - const current = tools.get(event.data.callID) - if (current) current.raw = event.data.text - continue - } - if (event.type === "session.tool.called") { - const current = tools.get(event.data.callID) - tools.set(event.data.callID, { - id: current?.id ?? partID(event.id), - timestamp: current?.timestamp ?? time, - assistantMessageID: event.data.assistantMessageID, - tool: current?.tool ?? "tool", - input: event.data.input, - raw: current?.raw, - provider: { executed: event.data.executed, state: event.data.state }, - }) - continue - } - if (event.type === "session.tool.success") { - const current = tools.get(event.data.callID) ?? fallbackTool(event) - const part: ToolPart = { - id: current.id, - sessionID: input.sessionID, - messageID: event.data.assistantMessageID, - type: "tool", - callID: event.data.callID, - tool: current.tool, - state: { - status: "completed", - input: current.input, - output: event.data.content - .filter((item) => item.type === "text") - .map((item) => item.text) - .join("\n"), - title: current.tool, - metadata: { - structured: event.data.structured, - content: event.data.content, - result: event.data.result, - providerCall: current.provider, - providerResult: { executed: event.data.executed, state: event.data.resultState }, - rawInput: current.raw, - }, - time: { start: current.timestamp, end: time }, - }, - } - tools.delete(event.data.callID) - if (!emit("tool_use", time, { part })) await input.renderTool(part) - continue - } - if (event.type === "session.tool.failed") { - const current = tools.get(event.data.callID) ?? fallbackTool(event) - const error = event.data.error.message - const part: ToolPart = { - id: current.id, - sessionID: input.sessionID, - messageID: event.data.assistantMessageID, - type: "tool", - callID: event.data.callID, - tool: current.tool, - state: { - status: "error", - input: current.input, - error, - metadata: { - result: event.data.result, - providerCall: current.provider, - providerResult: { executed: event.data.executed, state: event.data.resultState }, - rawInput: current.raw, - }, - time: { start: current.timestamp, end: time }, - }, - } - tools.delete(event.data.callID) - if (!emit("tool_use", time, { part })) { - await input.renderToolError(part) - UI.error(error) - } - continue - } - - if (event.type === "session.step.ended") { - const part: StepFinishPart = { - id: partID(event.id), - sessionID: input.sessionID, - messageID: event.data.assistantMessageID, - type: "step-finish", - reason: event.data.finish, - snapshot: event.data.snapshot, - cost: event.data.cost, - tokens: event.data.tokens, - } - emit("step_finish", time, { part }) - continue - } - if (event.type === "session.step.failed") { - if (interrupted || permissionRejected || questionRejected || formCancelled) continue - emittedError = true - process.exitCode = 1 - if (!emit("error", time, { error: event.data.error })) UI.error(event.data.error.message) - continue - } - if (event.type === "session.execution.failed") { - if (!emittedError && !questionRejected && !formCancelled) { - emittedError = true - process.exitCode = 1 - if (!emit("error", time, { error: event.data.error })) UI.error(event.data.error.message) - } - return - } - if (event.type === "session.execution.interrupted") { - if (event.data.reason === "user" && interrupted) process.exitCode = 130 - if (event.data.reason !== "user" && !emittedError) { - emittedError = true - process.exitCode = 1 - const error = { type: "aborted" as const, message: `Session interrupted: ${event.data.reason}` } - if (!emit("error", time, { error })) UI.error(error.message) - } - return - } - if (event.type === "session.execution.succeeded") return - } - } - - const interrupt = () => { - if (interrupted) process.exit(130) - interrupted = true - process.exitCode = 130 - admission?.abort() - void input.client.session.interrupt({ sessionID: input.sessionID }).catch(() => {}) - } - process.on("SIGINT", interrupt) - - let completed: Promise | undefined - try { - if (input.agent) { - await input.client.session.switchAgent({ sessionID: input.sessionID, agent: input.agent }) - } - const selected = input.model - ? { providerID: input.model.providerID, id: input.model.modelID, variant: input.variant } - : input.variant - ? await input.client.session - .get({ sessionID: input.sessionID }) - .then((result) => result.model) - .then(async (model) => { - if (model) return { ...model, variant: input.variant } - const result = await input.client.model.default() - const fallback = result.data - return fallback ? { providerID: fallback.providerID, id: fallback.id, variant: input.variant } : undefined - }) - : undefined - if (input.variant && !selected) throw new Error("Cannot select a variant before selecting a model") - if (selected) { - await input.client.session.switchModel({ sessionID: input.sessionID, model: selected }) - } - - const prepared = await Promise.all(input.files.map(prepareFile)) - if (interrupted) return - submitted = true - completed = consume() - admission = new AbortController() - const response = await input.client.session - .prompt( - { - sessionID: input.sessionID, - id: messageID, - text: [input.message, ...prepared.flatMap((file) => (file.text ? [file.text] : []))].join("\n\n"), - files: prepared.flatMap((file) => (file.attachment ? [file.attachment] : [])), - delivery: "steer", - }, - { signal: admission.signal }, - ) - .catch(async (error) => { - if (interrupted) { - await input.client.session.interrupt({ sessionID: input.sessionID }).catch(() => {}) - } - controller.abort() - await completed?.catch(() => {}) - if (interrupted || emittedError) return undefined - throw error - }) - admission = undefined - if (!response) return - if (interrupted) await input.client.session.interrupt({ sessionID: input.sessionID }).catch(() => {}) - - const [permissions, questions, forms] = await Promise.all([ - input.client.permission.list({ sessionID: input.sessionID }).catch(() => undefined), - input.client.question.list({ sessionID: input.sessionID }).catch(() => undefined), - Promise.all( - (input.attached ? [input.sessionID] : [input.sessionID, GLOBAL_FORM_SESSION_ID]).map((sessionID) => - input.client.form.list({ sessionID }).catch(() => undefined), - ), - ), - ]) - await Promise.all([ - ...(permissions ?? []).map(replyPermission), - ...(questions ?? []).map(rejectQuestion), - ...forms.flatMap((response) => response ?? []).map(cancelForm), - ]) - await completed - } finally { - process.off("SIGINT", interrupt) - controller.abort() - await stream.return?.(undefined).catch(() => {}) - } -} - -function partID(eventID: string) { - return `prt_${eventID.replace(/^evt_/, "")}` -} - -function fallbackTool(event: { - id: string - created: number - data: { assistantMessageID: string; callID: string } -}): ToolState { - return { - id: partID(event.id), - timestamp: toMillis(event.created), - assistantMessageID: event.data.assistantMessageID, - tool: "tool", - input: {}, - } -} - -function toMillis(value: unknown) { - if (typeof value === "number") return value - if (typeof value === "string") return new Date(value).getTime() - return Date.now() -} - -async function prepareFile(file: File) { - if (file.mime !== "text/plain") { - const uri = file.url.startsWith("data:") - ? file.url - : `data:${file.mime};base64,${Buffer.from(await Bun.file(new URL(file.url)).arrayBuffer()).toString("base64")}` - return { attachment: { uri, mime: file.mime, name: file.filename } } - } - const content = file.url.startsWith("data:") - ? Buffer.from(file.url.slice(file.url.indexOf(",") + 1), "base64").toString("utf8") - : await Bun.file(new URL(file.url)).text() - return { text: `\n${content}\n` } -} diff --git a/packages/cli/src/mini/run.ts b/packages/cli/src/mini/run.ts deleted file mode 100644 index 6f7e461a8c..0000000000 --- a/packages/cli/src/mini/run.ts +++ /dev/null @@ -1,270 +0,0 @@ -import { Service } from "@opencode-ai/client/effect" -import { OpenCode, type OpenCodeClient } from "@opencode-ai/client/promise" -import { FSUtil } from "@opencode-ai/core/fs-util" -import { Model } from "@opencode-ai/schema/model" -import type { ToolPart } from "@opencode-ai/sdk/v2" -import { open } from "node:fs/promises" -import path from "node:path" -import { Server } from "../services/server" -import { loadRunAgents, waitForCatalogReady } from "./catalog.shared" -import { runNonInteractivePrompt } from "./noninteractive" -import { toolInlineInfo } from "./tool" -import { UI } from "./ui" - -export type RunCommandInput = { - server: Server.Resolved - message: string[] - continue?: boolean - session?: string - fork?: boolean - model?: string - agent?: string - format: "default" | "json" - file: string[] - title?: string - thinking?: boolean - auto?: boolean -} - -type FilePart = { - url: string - filename: string - mime: string -} - -type Prepared = { - directory?: string - message: string - files: FilePart[] -} - -const ATTACH_FILE_MAX_BYTES = 10 * 1024 * 1024 - -export function runNonInteractive(input: RunCommandInput) { - return run(input).catch((error) => reportError(input, error instanceof Error ? error.message : String(error))) -} - -async function run(input: RunCommandInput) { - if (input.fork && !input.continue && !input.session) fail("--fork requires --continue or --session") - const root = process.env.PWD ?? process.cwd() - const directory = localDirectory(root) - const message = mergeInput(formatMessage(input.message), process.stdin.isTTY ? undefined : await Bun.stdin.text()) - if (!message?.trim()) fail("You must provide a message") - const files = await Promise.all(input.file.map((file) => prepareFile(file, root))) - const prepared = { directory, message, files } - return execute(input, prepared, input.server.endpoint) -} - -async function execute(input: RunCommandInput, prepared: Prepared, endpoint: Service.Endpoint) { - const client = OpenCode.make({ baseUrl: endpoint.url, headers: Service.headers(endpoint) }) - const requestedDirectory = prepared.directory ?? (await client.location.get()).directory - if (!requestedDirectory) fail("Failed to resolve server directory") - const session = await selectSession(client, requestedDirectory, input) - const cwd = session?.location.directory ?? requestedDirectory - const workspace = session?.location.workspaceID - const explicit = parseRunModel(input.model) - const explicitModel = explicit?.model - const variant = explicit?.variant - const sessionModel = session?.model ? { providerID: session.model.providerID, modelID: session.model.id } : undefined - const defaultModel = - !explicitModel && !sessionModel - ? await client.model - .default({ location: { directory: cwd, workspace } }) - .then((result) => (result.data ? { providerID: result.data.providerID, modelID: result.data.id } : undefined)) - : undefined - const model = pickRunModel(explicitModel, variant, sessionModel, defaultModel) - if (variant && !model) - return reportError(input, "Cannot select a variant before selecting a model", session?.id) - if (model) { - await waitForCatalogReady({ sdk: client, directory: cwd, workspace, model }) - const available = await client.model.list({ location: { directory: cwd, workspace } }) - if (!available.data.some((item) => item.providerID === model.providerID && item.id === model.modelID)) - return reportError(input, `Model unavailable: ${model.providerID}/${model.modelID}`, session?.id) - } - const agent = await validateAgent(client, cwd, input.agent) - const selected = - session ?? - (await client.session.create({ - agent, - model: model ? { providerID: model.providerID, id: model.modelID, variant } : undefined, - location: { directory: cwd }, - })) - if (!session && input.title !== undefined) { - await client.session.rename({ - sessionID: selected.id, - title: input.title || prepared.message.slice(0, 50) + (prepared.message.length > 50 ? "..." : ""), - }) - } - - await runNonInteractivePrompt({ - client, - sessionID: selected.id, - message: prepared.message, - files: prepared.files, - agent, - model, - variant, - thinking: input.thinking ?? false, - format: input.format, - auto: input.auto ?? false, - attached: true, - renderTool, - renderToolError, - }).catch((error) => reportError(input, error instanceof Error ? error.message : String(error), selected.id)) -} - -export function mergeInput(message: string | undefined, piped: string | undefined) { - if (!message) return piped || undefined - if (!piped) return message - return message + "\n" + piped -} - -export function pickRunModel( - explicit: { providerID: string; modelID: string } | undefined, - variant: string | undefined, - session: { providerID: string; modelID: string } | undefined, - fallback: { providerID: string; modelID: string } | undefined, -) { - if (explicit) return explicit - if (!variant) return - return session ?? fallback -} - -function formatMessage(message: string[]) { - const value = message.map((part) => (part.includes(" ") ? `"${part.replace(/"/g, '\\"')}"` : part)).join(" ") - return value || undefined -} - -function localDirectory(root: string) { - try { - process.chdir(root) - return process.cwd() - } catch { - fail(`Failed to change directory to ${root}`) - } -} - -export function parseRunModel(value?: string) { - if (!value) return - const ref = Model.Ref.parse(value) - return { - model: { providerID: ref.providerID, modelID: ref.id }, - variant: ref.variant, - } -} - -async function validateAgent(client: OpenCodeClient, directory: string, name?: string) { - if (!name) return - const agents = await loadRunAgents(client, directory).catch(() => undefined) - if (!agents) { - warning("failed to list agents. Falling back to default agent") - return - } - const agent = agents.find((item) => item.id === name) - if (!agent) { - warning(`agent "${name}" not found. Falling back to default agent`) - return - } - if (agent.mode === "subagent") { - warning(`agent "${name}" is a subagent, not a primary agent. Falling back to default agent`) - return - } - return name -} - -async function selectSession(client: OpenCodeClient, directory: string, input: RunCommandInput) { - const selected = input.session - ? await client.session.get({ sessionID: input.session }).catch(() => undefined) - : input.continue - ? await client.session - .list({ directory, parentID: null, limit: 1, order: "desc" }) - .then((result) => result.data[0]) - : undefined - if (input.session && !selected) fail("Session not found") - if (!selected || !input.fork) return selected - return client.session.fork({ sessionID: selected.id }) -} - -async function prepareFile(input: string, directory: string): Promise { - const file = path.resolve(directory, input) - const handle = await open(file, "r").catch(() => fail(`File not found: ${input}`)) - try { - const stat = await handle.stat() - if (!stat.isFile() || stat.size > ATTACH_FILE_MAX_BYTES) - fail(`Cannot attach a directory, special file, or file larger than 10 MiB: ${input}`) - const content = Buffer.alloc(Number(stat.size)) - let offset = 0 - while (offset < content.length) { - const read = await handle.read(content, offset, content.length - offset, offset) - if (read.bytesRead === 0) break - offset += read.bytesRead - } - const bytes = content.subarray(0, offset) - const detected = FSUtil.mimeType(file) - const text = bytes.toString("utf8") - const mime = - detected.startsWith("image/") || detected === "application/pdf" - ? detected - : !isBinaryContent(bytes) && Buffer.from(text, "utf8").equals(bytes) - ? "text/plain" - : detected - return { - url: `data:${mime};base64,${bytes.toString("base64")}`, - filename: path.basename(file), - mime, - } - } finally { - await handle.close() - } -} - -function isBinaryContent(bytes: Uint8Array) { - if (bytes.length === 0) return false - if (bytes.includes(0)) return true - return bytes.reduce((count, byte) => count + Number(byte < 9 || (byte > 13 && byte < 32)), 0) / bytes.length > 0.3 -} - -async function renderTool(part: ToolPart) { - const info = toolInlineInfo(part) - if (info.mode === "block") { - UI.empty() - UI.println(UI.Style.TEXT_NORMAL + info.icon, UI.Style.TEXT_NORMAL + info.title) - if (info.body?.trim()) UI.println(info.body) - UI.empty() - return - } - UI.println( - UI.Style.TEXT_NORMAL + info.icon, - UI.Style.TEXT_NORMAL + info.title, - info.description ? UI.Style.TEXT_DIM + info.description + UI.Style.TEXT_NORMAL : "", - ) -} - -async function renderToolError(part: ToolPart) { - const info = toolInlineInfo(part) - UI.println(UI.Style.TEXT_NORMAL + "✗", UI.Style.TEXT_NORMAL + `${info.title} failed`) -} - -function warning(message: string) { - UI.println(UI.Style.TEXT_WARNING_BOLD + "!", UI.Style.TEXT_NORMAL, message) -} - -function reportError(input: RunCommandInput, message: string, sessionID?: string) { - process.exitCode = 1 - if (input.format === "json") { - process.stdout.write( - JSON.stringify({ - type: "error", - timestamp: Date.now(), - sessionID: sessionID ?? "", - error: { type: "unknown", message }, - }) + "\n", - ) - return - } - UI.error(message) -} - -function fail(message: string): never { - throw new Error(message) -} diff --git a/packages/cli/src/mini/stream-v2.subagent.ts b/packages/cli/src/mini/stream-v2.subagent.ts deleted file mode 100644 index 911d2d0d4e..0000000000 --- a/packages/cli/src/mini/stream-v2.subagent.ts +++ /dev/null @@ -1,782 +0,0 @@ -// Current-native subagent (child Session) tracking for the mini transport. -// -// Discovers child Sessions of the active parent from four current sources: -// 1. projected subagent tool output (`structured.sessionID`) during hydration -// 2. the current session list filtered by `parentID` during hydration -// 3. the process-local active-session map during hydration -// 4. live events from unknown sessions whose `parentID` matches the parent -// -// Tracks one footer tab per child and a detail transcript for the selected -// child, reduced from the same current live event stream the parent uses. -// Detail transcripts rebuild from projected messages on discovery, selection, -// and reconnect, then continue from live deltas using the same -// projected-prefix dedup the parent transport uses. -// -// Per-child interruption uses `v2.session.interrupt(childID)`. Per-child -// backgrounding is intentionally absent: subagent jobs block the parent -// session, so only whole-session `v2.session.background(parentID)` exists. -import type { EventSubscribeOutput, OpenCodeClient } from "@opencode-ai/client/promise" -import type { SessionMessageAssistantTool, SessionMessageInfo, ToolPart } from "@opencode-ai/sdk/v2" -import { Locale } from "@opencode-ai/tui/util/locale" -import type { FooterSubagentDetail, FooterSubagentState, FooterSubagentTab, StreamCommit } from "./types" - -const CHILD_MESSAGE_LIMIT = 80 -const CHILD_FRAME_LIMIT = 80 -const CHILD_EVENT_BUFFER_LIMIT = 64 -const FAMILY_LIST_LIMIT = 100 -const FALLBACK_LABEL = "Subagent" - -type V2Event = EventSubscribeOutput - -export function outputText(content: ReadonlyArray<{ type: string; text?: string }>) { - return content.flatMap((item) => (item.type === "text" && item.text ? [item.text] : [])).join("\n") -} - -export function legacyTool(input: { - sessionID: string - messageID: string - tool: SessionMessageAssistantTool -}): ToolPart { - const tool = input.tool - const providerCall = - tool.executed === undefined && tool.providerState === undefined - ? undefined - : { executed: tool.executed, state: tool.providerState } - const providerResult = - tool.executed === undefined && tool.providerResultState === undefined - ? undefined - : { executed: tool.executed, state: tool.providerResultState } - const base = { - id: `prt_${tool.id}`, - sessionID: input.sessionID, - messageID: input.messageID, - type: "tool" as const, - callID: tool.id, - tool: tool.name, - } - if (tool.state.status === "streaming") { - return { - ...base, - state: { status: "pending", input: {}, raw: tool.state.input }, - } - } - if (tool.state.status === "running") { - return { - ...base, - state: { - status: "running", - input: tool.state.input, - title: tool.name, - metadata: { structured: tool.state.structured, content: tool.state.content, providerCall }, - time: { start: tool.time.ran ?? tool.time.created }, - }, - } - } - if (tool.state.status === "completed") { - return { - ...base, - state: { - status: "completed", - input: tool.state.input, - output: outputText(tool.state.content), - title: tool.name, - metadata: { - structured: tool.state.structured, - content: tool.state.content, - result: tool.state.result, - providerCall, - providerResult, - }, - time: { start: tool.time.ran ?? tool.time.created, end: tool.time.completed ?? tool.time.created }, - }, - } - } - return { - ...base, - state: { - status: "error", - input: tool.state.input, - error: tool.state.error.message, - metadata: { - structured: tool.state.structured, - content: tool.state.content, - result: tool.state.result, - providerCall, - providerResult, - }, - time: { start: tool.time.ran ?? tool.time.created, end: tool.time.completed ?? tool.time.created }, - }, - } -} - -export function toolCommit(part: ToolPart, phase: "start" | "progress" | "final"): StreamCommit { - const status = part.state.status - const text = - status === "running" - ? part.tool === "task" - ? "running task" - : `running ${part.tool}` - : status === "completed" - ? part.state.output - : status === "error" - ? part.state.error - : "" - return { - kind: "tool", - source: "tool", - text, - phase, - messageID: part.messageID, - partID: part.id, - tool: part.tool, - part, - toolState: status === "error" ? "error" : status === "completed" ? "completed" : "running", - toolError: status === "error" ? part.state.error : undefined, - } -} - -type Frame = { - key: string - commit: StreamCommit -} - -type ToolTrack = { - name: string - input: Record - started: number - providerState?: Record -} - -type ChildState = { - sessionID: string - label: string - description: string - status: FooterSubagentTab["status"] - background: boolean - title?: string - callIDs: Set - lastUpdatedAt: number - frames: Frame[] - text: Map - projectedText: Map - reasoning: Map - projectedReasoning: Map - tools: Map - finishedTools: Set - messageIDs: Set - prompts: Map - hydrated: boolean -} - -export type SubagentTrackerInput = { - sdk: OpenCodeClient - sessionID: string - thinking: boolean - emit: () => void -} - -export type SubagentTracker = { - main(event: V2Event): void - foreign(sessionID: string, event: V2Event): void - hydrate(next: { messages: SessionMessageInfo[]; active: Record }): Promise - select(sessionID: string | undefined): void - snapshot(): FooterSubagentState -} - -function record(value: unknown): Record | undefined { - if (typeof value === "object" && value !== null && !Array.isArray(value)) return value as Record - return undefined -} - -function text(value: unknown): string | undefined { - if (typeof value !== "string") return undefined - const next = value.trim() - return next || undefined -} - -function childSessionID(structured: Record | undefined) { - const sessionID = text(structured?.sessionID) - if (!sessionID || !sessionID.startsWith("ses")) return undefined - const status = structured?.status - if (status !== "running" && status !== "completed") return undefined - return { sessionID, running: status === "running" } -} - -function tab(child: ChildState): FooterSubagentTab { - return { - sessionID: child.sessionID, - partID: `subagent:${child.sessionID}`, - callID: `subagent:${child.sessionID}`, - label: child.label, - description: child.description || child.title || "", - status: child.status, - background: child.background ? true : undefined, - title: child.title, - toolCalls: child.callIDs.size > 0 ? child.callIDs.size : undefined, - lastUpdatedAt: child.lastUpdatedAt, - } -} - -export function createSubagentTracker(input: SubagentTrackerInput): SubagentTracker { - const children = new Map() - // Live subagent tool calls in the parent, so tool.success structured output - // can be joined with the call's input metadata. - const pendingCalls = new Map>() - // Foreign sessions already resolved through session.get. Non-children stay - // cached so unrelated concurrent sessions are checked at most once. - const checked = new Set() - // Foreign events buffered while a session.get discovery is in flight, so a - // fast child (including its settled event) is not lost mid-discovery. - const pendingEvents = new Map() - const hydrationEvents = new Map() - const hydrationOverflow = new Set() - const hydrations = new Map>() - let selected: string | undefined - const fragmentKey = (messageID: string, partID: string) => `${messageID}\u0000${partID}` - - const ensureChild = (sessionID: string): ChildState => { - const existing = children.get(sessionID) - const child: ChildState = existing ?? { - sessionID, - label: FALLBACK_LABEL, - description: "", - status: "running", - background: false, - callIDs: new Set(), - lastUpdatedAt: Date.now(), - frames: [], - text: new Map(), - projectedText: new Map(), - reasoning: new Map(), - projectedReasoning: new Map(), - tools: new Map(), - finishedTools: new Set(), - messageIDs: new Set(), - prompts: new Map(), - hydrated: false, - } - if (!existing) children.set(sessionID, child) - // Adopting a child while its session.get discovery is still in flight: - // drain the buffered events now. They arrived before whatever the caller - // applies next, so replaying them first preserves bus order, and the - // resolved discovery can no longer replay stale events (e.g. step.started) - // after a terminal settled event was applied directly. - const buffered = pendingEvents.get(sessionID) - if (buffered) { - pendingEvents.delete(sessionID) - for (const event of buffered) reduce(child, event) - } - return child - } - - const touch = (child: ChildState, timestamp?: number) => { - child.lastUpdatedAt = Math.max(child.lastUpdatedAt, timestamp ?? Date.now()) - } - - const notifyDetail = (child: ChildState) => { - if (child.sessionID === selected) input.emit() - } - - const setFrame = (child: ChildState, key: string, commit: StreamCommit) => { - const index = child.frames.findIndex((item) => item.key === key) - if (index === -1) { - child.frames.push({ key, commit }) - if (child.frames.length > CHILD_FRAME_LIMIT) child.frames.splice(0, child.frames.length - CHILD_FRAME_LIMIT) - return - } - child.frames[index] = { key, commit } - } - - const applyMeta = (child: ChildState, meta: Record | undefined) => { - if (!meta) return - const agent = text(meta.agent) - if (agent) child.label = Locale.titlecase(agent) - const description = text(meta.description) - if (description) child.description = description - if (meta.background === true) child.background = true - } - - const userFrame = (child: ChildState, messageID: string, value: string) => { - if (child.messageIDs.has(messageID)) return false - child.messageIDs.add(messageID) - setFrame(child, `user:${messageID}`, { - kind: "user", - source: "system", - text: value, - phase: "start", - messageID, - }) - return true - } - - const childTool = (child: ChildState, item: SessionMessageAssistantTool, messageID: string) => { - const part = legacyTool({ - sessionID: child.sessionID, - messageID, - tool: item, - }) - if (item.state.status === "streaming") return - child.callIDs.add(item.id) - if (item.state.status === "running") { - setFrame(child, `tool:${item.id}`, toolCommit(part, "start")) - return - } - child.finishedTools.add(item.id) - child.tools.delete(item.id) - setFrame(child, `tool:${item.id}`, toolCommit(part, "final")) - } - - const rebuild = (child: ChildState, messages: SessionMessageInfo[]) => { - child.frames = [] - child.text.clear() - child.projectedText.clear() - child.reasoning.clear() - child.projectedReasoning.clear() - child.finishedTools.clear() - child.messageIDs.clear() - child.callIDs.clear() - for (const message of messages) { - if (message.type === "user") { - child.prompts.delete(message.id) - userFrame(child, message.id, message.text) - continue - } - if (message.type !== "assistant") continue - child.messageIDs.add(message.id) - let textOrdinal = 0 - let reasoningOrdinal = 0 - for (const item of message.content) { - if (item.type === "text") { - const id = `text:${textOrdinal++}` - const key = fragmentKey(message.id, id) - child.text.set(key, item.text) - child.projectedText.set(key, item.text) - setFrame(child, key, { - kind: "assistant", - source: "assistant", - text: item.text, - phase: "progress", - messageID: message.id, - partID: id, - }) - continue - } - if (item.type === "reasoning") { - const id = `reasoning:${reasoningOrdinal++}` - const key = fragmentKey(message.id, id) - child.reasoning.set(key, item.text) - child.projectedReasoning.set(key, item.text) - if (input.thinking) - setFrame(child, key, { - kind: "reasoning", - source: "reasoning", - text: `Thinking: ${item.text}`, - phase: "progress", - messageID: message.id, - partID: id, - }) - continue - } - childTool(child, item, message.id) - } - if (message.error) { - setFrame(child, `error:${message.id}`, { - kind: "error", - source: "system", - text: message.error.message, - phase: "start", - messageID: message.id, - }) - } - } - } - - const hydrateChild = (child: ChildState): Promise => { - const existing = hydrations.get(child.sessionID) - if (existing) return existing - const pendingPrompts = new Map(child.prompts) - const pendingTools = new Map(child.tools) - let retry = false - const task = input.sdk.message - .list({ sessionID: child.sessionID, limit: CHILD_MESSAGE_LIMIT, order: "desc" }) - .then((response) => { - const buffered = hydrationEvents.get(child.sessionID) ?? [] - hydrationEvents.delete(child.sessionID) - if (hydrationOverflow.delete(child.sessionID)) { - child.hydrated = false - retry = true - notifyDetail(child) - return - } - for (const [id, prompt] of pendingPrompts) { - if (!child.prompts.has(id)) child.prompts.set(id, prompt) - } - rebuild(child, structuredClone(response.data).toReversed() as SessionMessageInfo[]) - for (const [id, tool] of pendingTools) { - if (!child.finishedTools.has(id) && !child.tools.has(id)) child.tools.set(id, tool) - } - for (const event of buffered) reduce(child, event) - child.hydrated = true - notifyDetail(child) - }) - .catch(() => { - hydrationEvents.delete(child.sessionID) - hydrationOverflow.delete(child.sessionID) - }) - .finally(() => { - hydrations.delete(child.sessionID) - if (retry) queueMicrotask(() => void hydrateChild(child)) - }) - hydrations.set(child.sessionID, task) - return task - } - - const discover = (sessionID: string) => { - if (checked.has(sessionID) || children.has(sessionID) || sessionID === input.sessionID) return - checked.add(sessionID) - if (!pendingEvents.has(sessionID)) pendingEvents.set(sessionID, []) - void input.sdk.session - .get({ sessionID }) - .then((session) => { - const buffered = pendingEvents.get(sessionID) ?? [] - pendingEvents.delete(sessionID) - if (session.parentID !== input.sessionID) return - const child = ensureChild(sessionID) - if (session.agent) child.label = Locale.titlecase(session.agent) - child.title = session.title - for (const event of buffered) reduce(child, event) - touch(child) - input.emit() - void hydrateChild(child) - }) - .catch(() => { - // Allow a later event to retry discovery after transient failures. - pendingEvents.delete(sessionID) - checked.delete(sessionID) - }) - } - - const reduce = (child: ChildState, event: V2Event) => { - if (event.type === "session.input.admitted") { - if (event.data.input.type === "user") child.prompts.set(event.data.inputID, event.data.input.data.text) - return - } - if (event.type === "session.input.promoted") { - const prompt = child.prompts.get(event.data.inputID) - if (prompt === undefined) return - child.prompts.delete(event.data.inputID) - if (userFrame(child, event.data.inputID, prompt)) { - touch(child, event.created) - notifyDetail(child) - } - return - } - if (event.type === "session.step.started") { - touch(child, event.created) - if (child.label === FALLBACK_LABEL && event.data.agent) child.label = Locale.titlecase(event.data.agent) - if (child.status !== "running") child.status = "running" - input.emit() - return - } - if (event.type === "session.text.started") { - return - } - if (event.type === "session.text.delta") { - const id = `text:${event.data.ordinal}` - const key = fragmentKey(event.data.assistantMessageID, id) - const projected = child.projectedText.get(key) - const covered = projected?.indexOf(event.data.delta) ?? -1 - if (projected && covered >= 0) { - child.projectedText.set(key, projected.slice(covered + event.data.delta.length)) - return - } - const next = (child.text.get(key) ?? "") + event.data.delta - child.text.set(key, next) - setFrame(child, key, { - kind: "assistant", - source: "assistant", - text: next, - phase: "progress", - messageID: event.data.assistantMessageID, - partID: id, - }) - touch(child, event.created) - notifyDetail(child) - return - } - if (event.type === "session.text.ended") { - const id = `text:${event.data.ordinal}` - const key = fragmentKey(event.data.assistantMessageID, id) - child.text.set(key, event.data.text) - child.projectedText.delete(key) - setFrame(child, key, { - kind: "assistant", - source: "assistant", - text: event.data.text, - phase: "progress", - messageID: event.data.assistantMessageID, - partID: id, - }) - touch(child, event.created) - notifyDetail(child) - return - } - if (event.type === "session.reasoning.started") { - return - } - if (event.type === "session.reasoning.delta") { - const id = `reasoning:${event.data.ordinal}` - const key = fragmentKey(event.data.assistantMessageID, id) - const projected = child.projectedReasoning.get(key) - const covered = projected?.indexOf(event.data.delta) ?? -1 - if (projected && covered >= 0) { - child.projectedReasoning.set(key, projected.slice(covered + event.data.delta.length)) - return - } - const next = (child.reasoning.get(key) ?? "") + event.data.delta - child.reasoning.set(key, next) - if (!input.thinking) return - setFrame(child, key, { - kind: "reasoning", - source: "reasoning", - text: `Thinking: ${next}`, - phase: "progress", - messageID: event.data.assistantMessageID, - partID: id, - }) - notifyDetail(child) - return - } - if (event.type === "session.reasoning.ended") { - const id = `reasoning:${event.data.ordinal}` - const key = fragmentKey(event.data.assistantMessageID, id) - child.reasoning.set(key, event.data.text) - child.projectedReasoning.delete(key) - if (!input.thinking) return - setFrame(child, key, { - kind: "reasoning", - source: "reasoning", - text: `Thinking: ${event.data.text}`, - phase: "progress", - messageID: event.data.assistantMessageID, - partID: id, - }) - notifyDetail(child) - return - } - if (event.type === "session.tool.input.started") { - if (child.finishedTools.has(event.data.callID)) return - child.tools.set(event.data.callID, { name: event.data.name, input: {}, started: event.created }) - return - } - if (event.type === "session.tool.called") { - if (child.finishedTools.has(event.data.callID)) return - const current = child.tools.get(event.data.callID) - child.tools.set(event.data.callID, { - name: current?.name ?? "tool", - input: event.data.input, - started: current?.started ?? event.created, - providerState: event.data.state, - }) - childTool( - child, - structuredClone({ - type: "tool", - id: event.data.callID, - name: current?.name ?? "tool", - executed: event.data.executed, - providerState: event.data.state, - state: { status: "running", input: event.data.input, structured: {}, content: [] }, - time: { created: current?.started ?? event.created, ran: event.created }, - }) as SessionMessageAssistantTool, - event.data.assistantMessageID, - ) - touch(child, event.created) - notifyDetail(child) - return - } - if (event.type === "session.tool.success" || event.type === "session.tool.failed") { - if (child.finishedTools.has(event.data.callID)) return - const current = child.tools.get(event.data.callID) - const failed = event.type === "session.tool.failed" - childTool( - child, - structuredClone({ - type: "tool", - id: event.data.callID, - name: current?.name ?? "tool", - executed: event.data.executed, - providerState: current?.providerState, - providerResultState: event.data.resultState, - state: failed - ? { - status: "error", - input: current?.input ?? {}, - structured: {}, - content: [], - error: event.data.error, - result: event.data.result, - } - : { - status: "completed", - input: current?.input ?? {}, - structured: event.data.structured, - content: event.data.content, - result: event.data.result, - }, - time: { - created: current?.started ?? event.created, - ran: current?.started, - completed: event.created, - }, - }) as SessionMessageAssistantTool, - event.data.assistantMessageID, - ) - touch(child, event.created) - notifyDetail(child) - return - } - if (event.type === "session.step.ended") return - if (event.type === "session.step.failed") { - setFrame(child, `error:step:${event.data.assistantMessageID}`, { - kind: "error", - source: "system", - text: event.data.error.message, - phase: "start", - messageID: event.data.assistantMessageID, - }) - touch(child, event.created) - notifyDetail(child) - return - } - if (event.type === "session.execution.started") { - child.status = "running" - touch(child, event.created) - input.emit() - return - } - if ( - event.type === "session.execution.succeeded" || - event.type === "session.execution.failed" || - event.type === "session.execution.interrupted" - ) { - child.status = - event.type === "session.execution.succeeded" - ? "completed" - : event.type === "session.execution.interrupted" - ? "cancelled" - : "error" - touch(child, event.created) - input.emit() - } - } - - const mainTool = (item: SessionMessageAssistantTool, active?: Record) => { - if (item.name !== "subagent" || item.state.status !== "completed") return - const found = childSessionID(record(item.state.structured)) - if (!found) return - const child = ensureChild(found.sessionID) - applyMeta(child, record(item.state.input)) - if (found.running) child.background = true - if (child.status === "running") { - const running = found.running && (!active || found.sessionID in active) - child.status = running ? "running" : "completed" - } - touch(child, item.time.completed ?? item.time.created) - } - - return { - main(event) { - if (event.type === "session.tool.input.started") { - if (event.data.name === "subagent") pendingCalls.set(event.data.callID, {}) - return - } - if (event.type === "session.tool.called") { - if (pendingCalls.has(event.data.callID)) pendingCalls.set(event.data.callID, event.data.input) - return - } - if (event.type === "session.tool.failed") { - pendingCalls.delete(event.data.callID) - return - } - if (event.type !== "session.tool.success") return - const pending = pendingCalls.get(event.data.callID) - pendingCalls.delete(event.data.callID) - const found = childSessionID(record(event.data.structured)) - if (!found) return - const child = ensureChild(found.sessionID) - applyMeta(child, pending) - if (found.running) { - child.background = true - child.status = "running" - } - if (!found.running && child.status === "running") child.status = "completed" - touch(child, event.created) - input.emit() - if (!child.hydrated) void hydrateChild(child) - }, - foreign(sessionID, event) { - const child = children.get(sessionID) - if (child) { - if (hydrations.has(sessionID)) { - const buffered = hydrationEvents.get(sessionID) ?? [] - if (buffered.length < CHILD_EVENT_BUFFER_LIMIT) buffered.push(event) - else hydrationOverflow.add(sessionID) - hydrationEvents.set(sessionID, buffered) - } - reduce(child, event) - return - } - discover(sessionID) - const buffered = pendingEvents.get(sessionID) - if (buffered && buffered.length < CHILD_EVENT_BUFFER_LIMIT) buffered.push(event) - }, - async hydrate(next) { - for (const message of next.messages) { - if (message.type !== "assistant") continue - for (const item of message.content) { - if (item.type === "tool") mainTool(item, next.active) - } - } - // Family index: adopt children directly from the current session list so - // historical subagents beyond the projected message window still get tabs. - const family = await input.sdk.session - .list({ parentID: input.sessionID, limit: FAMILY_LIST_LIMIT, order: "desc" }) - .then((response) => response.data) - .catch(() => []) - for (const session of family) { - const child = ensureChild(session.id) - if (session.agent && child.label === FALLBACK_LABEL) child.label = Locale.titlecase(session.agent) - if (!child.title) child.title = session.title - touch(child, session.time.updated) - } - for (const sessionID of Object.keys(next.active)) discover(sessionID) - for (const child of children.values()) { - // Reconnect can miss a child's settled event; the active map is the - // authoritative live signal for still-running children. - if (child.status === "running" && !(child.sessionID in next.active)) child.status = "completed" - } - const current = selected ? children.get(selected) : undefined - if (current) await hydrateChild(current) - if (children.size > 0) input.emit() - }, - select(sessionID) { - selected = sessionID - const child = sessionID ? children.get(sessionID) : undefined - if (child && !child.hydrated) void hydrateChild(child) - input.emit() - }, - snapshot() { - const tabs = [...children.values()].map(tab).toSorted((a, b) => { - const active = Number(b.status === "running") - Number(a.status === "running") - if (active !== 0) return active - return b.lastUpdatedAt - a.lastUpdatedAt - }) - const child = selected ? children.get(selected) : undefined - const details: Record = child - ? { [child.sessionID]: { sessionID: child.sessionID, commits: child.frames.map((item) => item.commit) } } - : {} - return { tabs, details, permissions: [], questions: [] } - }, - } -} diff --git a/packages/cli/src/mini/stream-v2.transport.ts b/packages/cli/src/mini/stream-v2.transport.ts deleted file mode 100644 index 26b8266849..0000000000 --- a/packages/cli/src/mini/stream-v2.transport.ts +++ /dev/null @@ -1,1116 +0,0 @@ -import type { EventSubscribeOutput, OpenCodeClient } from "@opencode-ai/client/promise" -import type { - PermissionRequest, - QuestionRequest, - SessionMessageInfo, - SessionMessageAssistantTool, -} from "@opencode-ai/sdk/v2" -import { Event } from "@opencode-ai/schema/event" -import { blockerStatus, pickBlockerView } from "./session-data" -import { writeSessionOutput } from "./stream" -import { createSubagentTracker, legacyTool, toolCommit } from "./stream-v2.subagent" -import type { - FooterApi, - FooterView, - LocalReplayAnchor, - LocalReplayRow, - RunFilePart, - RunInput, - RunPrompt, - RunPromptPart, - RunProvider, - StreamCommit, -} from "./types" - -type Trace = { - write(type: string, data?: unknown): void -} - -type StreamInput = { - sdk: OpenCodeClient - directory?: string - sessionID: string - thinking: boolean - replay?: boolean - replayLimit?: number - limits: () => Record - providers?: () => RunProvider[] - footer: FooterApi - trace?: Trace - signal?: AbortSignal - onCatalogRefresh?: () => void -} - -export type SessionTurnInput = { - agent: string | undefined - model: RunInput["model"] - variant: string | undefined - prompt: RunPrompt - files: RunFilePart[] - includeFiles: boolean - onVisibleOutput?: (anchor: LocalReplayAnchor) => void - signal?: AbortSignal -} - -export type SessionResizeReplayInput = { - localRows: () => LocalReplayRow[] - reset: () => Promise -} - -export type SessionTransport = { - runPromptTurn(input: SessionTurnInput): Promise - interruptActiveTurn(): Promise - selectSubagent(sessionID: string | undefined): void - replayOnResize(input: SessionResizeReplayInput): Promise - close(): Promise -} - -type Wait = { - messageID: string - promoted: boolean - interrupted: boolean - failureRendered: boolean - resolve: () => void - reject: (error: unknown) => void - onVisibleOutput?: (anchor: LocalReplayAnchor) => void -} - -// One active session.shell call. The HTTP response is the completion signal; -// callID correlates the live shell events once shell.started is observed, and -// abort cancels the blocking request when the user interrupts the turn. -type ShellWait = { - eventID: string - messageID: string - callID?: string - resolve: () => void - abort: () => void -} - -type RunV2Event = EventSubscribeOutput -type PermissionV2Request = Extract["data"] -type QuestionV2Request = Extract["data"] -type PromptFilePart = Extract - -type ToolState = { - messageID: string - name: string - input: Record - started: number - running: boolean - providerState?: Record -} - -type State = { - permissions: PermissionRequest[] - questions: QuestionRequest[] - view: FooterView - messageIDs: Set - text: Map - projectedText: Map - reasoning: Map - projectedReasoning: Map - tools: Map - finishedTools: Set - skillMessages: Set - shellCommands: Map - shellStarted: Set - shellEnded: Set - shellWait?: ShellWait - wait?: Wait - connected: boolean - closed: boolean - initial: boolean - buffered?: RunV2Event[] - errors: Set -} - -const money = new Intl.NumberFormat("en-US", { style: "currency", currency: "USD" }) - -export function formatUnknownError(error: unknown): string { - if (typeof error === "string") return error - if (error instanceof Error) return error.message || error.name - if (error && typeof error === "object") { - const message = Reflect.get(error, "message") - if (typeof message === "string" && message.trim()) return message - const tag = Reflect.get(error, "_tag") - if (typeof tag === "string" && tag.trim()) return tag - } - return "unknown error" -} - -function permission(request: PermissionV2Request): PermissionRequest { - return { - id: request.id, - sessionID: request.sessionID, - permission: request.action, - patterns: [...request.resources], - metadata: request.metadata ?? {}, - always: [...(request.save ?? [])], - tool: request.source?.type === "tool" ? request.source : undefined, - } -} - -function question(request: QuestionV2Request): QuestionRequest { - return { - id: request.id, - sessionID: request.sessionID, - questions: request.questions.map((item) => ({ ...item, options: item.options.map((option) => ({ ...option })) })), - tool: request.tool, - } -} - -function sessionID(event: RunV2Event) { - return "sessionID" in event.data && typeof event.data.sessionID === "string" ? event.data.sessionID : undefined -} - -function errorMessage(error: { message?: string; _tag?: string }) { - return error.message || error._tag || "Session execution failed" -} - -function wait(delay: number, signal: AbortSignal) { - return new Promise((resolve) => { - const timer = setTimeout(done, delay) - signal.addEventListener("abort", done, { once: true }) - function done() { - clearTimeout(timer) - signal.removeEventListener("abort", done) - resolve() - } - }) -} - -async function prepareFile(file: RunFilePart) { - if (file.mime !== "text/plain") return { attachment: { uri: file.url, name: file.filename } } - const content = file.url.startsWith("data:") - ? Buffer.from(file.url.slice(file.url.indexOf(",") + 1), "base64").toString("utf8") - : await Bun.file(new URL(file.url)).text() - return { text: `\n${content}\n` } -} - -function promptFileMention(part: PromptFilePart) { - if (!part.source?.text) return - return { - start: part.source.text.start, - end: part.source.text.end, - text: part.source.text.value, - } -} - -function promptFiles(next: SessionTurnInput) { - return next.prompt.parts.flatMap((part) => - part.type === "file" - ? [ - { - uri: part.url, - name: part.filename, - mention: promptFileMention(part), - }, - ] - : [], - ) -} - -function promptAgents(next: SessionTurnInput) { - return next.prompt.parts.flatMap((part) => - part.type === "agent" - ? [ - { - name: part.name, - mention: part.source - ? { start: part.source.start, end: part.source.end, text: part.source.value } - : undefined, - }, - ] - : [], - ) -} - -function streamPartKey(messageID: string, partID: string) { - return `${messageID}\u0000${partID}` -} - -// Matches the commit shapes the legacy session-data reducer produced for direct -// shell calls: one "start" commit rendering `$ command` and one "progress" -// commit rendering the merged output (see toolEntryBody in tool.ts). -function shellCommit( - callID: string, - command: string, - next: Pick, -): StreamCommit { - return { - kind: "tool", - source: "tool", - partID: `shell:${callID}`, - tool: "bash", - shell: { callID, command }, - ...next, - } -} - -function shellTerminal( - callID: string, - command: string, - shell: { status: string; exit?: number | string }, - output: { output: string; cursor: number; size: number; truncated: boolean }, -) { - const incomplete = output.truncated || output.cursor < output.size - const text = `${output.output}${incomplete ? `${output.output.endsWith("\n") || !output.output ? "" : "\n"}[output truncated]` : ""}` - const error = - shell.status === "exited" && shell.exit === 0 - ? undefined - : shell.status === "exited" - ? `Shell exited with code ${shell.exit ?? "unknown"}` - : `Shell ${shell.status}` - if (!error) return [shellCommit(callID, command, { text, phase: "progress", toolState: "completed" })] - return [ - ...(text ? [shellCommit(callID, command, { text, phase: "progress", toolState: "running" })] : []), - shellCommit(callID, command, { text: error, phase: "final", toolState: "error", toolError: error }), - ] -} - -function messageIDFromEvent(id: string) { - return id.replace(/^evt_/, "msg_") -} - -const catalogEvents = new Set([ - "catalog.updated", - "integration.updated", - "agent.updated", - "command.updated", - "skill.updated", - "reference.updated", -]) - -// session.shell resolves after the command settled server-side; the matching -// live shell.ended event usually lands within the same tick, but hold the turn -// briefly so the output commit renders inside it. -const SHELL_OUTPUT_GRACE_MS = 1500 - -function skillCommit(messageID: string, name: string): StreamCommit { - return { - kind: "system", - source: "system", - messageID, - partID: `skill:${messageID}`, - text: `→ Skill "${name}"`, - phase: "start", - } -} - -async function resolveSelectedModel(input: StreamInput, next: Pick) { - if (next.model) return { providerID: next.model.providerID, id: next.model.modelID, variant: next.variant } - if (!next.variant) return - - const session = await input.sdk.session - .get({ sessionID: input.sessionID }, { signal: next.signal }) - .then((response) => response.model) - if (session) return { ...session, variant: next.variant } - - const fallback = await input.sdk.model.default(undefined, { signal: next.signal }).then((response) => response.data) - if (!fallback) return - return { providerID: fallback.providerID, id: fallback.id, variant: next.variant } -} - -export async function createSessionTransport(input: StreamInput): Promise { - const controller = new AbortController() - input.signal?.addEventListener("abort", () => controller.abort(), { once: true }) - const state: State = { - permissions: [], - questions: [], - view: { type: "prompt" }, - messageIDs: new Set(), - text: new Map(), - projectedText: new Map(), - reasoning: new Map(), - projectedReasoning: new Map(), - tools: new Map(), - finishedTools: new Set(), - skillMessages: new Set(), - shellCommands: new Map(), - shellStarted: new Set(), - shellEnded: new Set(), - connected: false, - closed: false, - initial: true, - errors: new Set(), - } - let readyResolve!: () => void - let readyReject!: (error: unknown) => void - const ready = new Promise((resolve, reject) => { - readyResolve = resolve - readyReject = reject - }) - const abortReady = () => readyReject(new Error("Mini closed before the event stream connected")) - controller.signal.addEventListener("abort", abortReady, { once: true }) - const offFooterClose = input.footer.onClose(() => controller.abort()) - - const subagents = createSubagentTracker({ - sdk: input.sdk, - sessionID: input.sessionID, - thinking: input.thinking, - emit: () => { - if (state.closed || input.footer.isClosed) return - writeSessionOutput( - { footer: input.footer, trace: input.trace }, - { commits: [], footer: { subagent: subagents.snapshot() } }, - ) - }, - }) - - const write = (commits: StreamCommit[], patch?: { phase?: "idle" | "running"; status?: string; usage?: string }) => { - const visible = commits.at(-1) - if (visible) { - state.wait?.onVisibleOutput?.({ - kind: visible.kind, - text: visible.text, - phase: visible.phase, - messageID: visible.messageID, - partID: visible.partID, - toolState: visible.toolState, - }) - } - writeSessionOutput({ footer: input.footer, trace: input.trace }, { commits, footer: patch ? { patch } : undefined }) - } - - const syncBlockers = () => { - const next = pickBlockerView({ permission: state.permissions[0], question: state.questions[0] }) - if (next.type === "prompt" && state.view.type === "prompt") return - if (next.type !== "prompt" && state.view.type === next.type && next.request.id === state.view.request.id) return - state.view = next - writeSessionOutput( - { footer: input.footer, trace: input.trace }, - { commits: [], footer: { view: next, patch: { status: blockerStatus(next) } } }, - ) - } - - const renderTool = (messageID: string, item: SessionMessageAssistantTool) => { - const part = legacyTool({ - sessionID: input.sessionID, - messageID, - tool: item, - }) - if (item.state.status === "streaming") return - if (item.state.status === "running") { - if (state.tools.get(item.id)?.running) return - state.tools.set(item.id, { - messageID, - name: item.name, - input: item.state.input, - started: item.time.ran ?? item.time.created, - running: true, - providerState: item.providerState, - }) - write([toolCommit(part, "start")], { phase: "running", status: `running ${item.name}` }) - return - } - if (state.finishedTools.has(item.id)) return - if (!state.tools.get(item.id)?.running) write([toolCommit(part, "start")]) - state.finishedTools.add(item.id) - state.tools.delete(item.id) - write([ - toolCommit( - part, - item.state.status === "completed" && part.state.status === "completed" && part.state.output - ? "progress" - : "final", - ), - ]) - } - - const renderMessage = (message: SessionMessageInfo, render: boolean, reuseVisibleWait: boolean) => { - if (message.type === "user") { - const waiting = state.wait?.messageID === message.id - if (waiting && state.wait) state.wait.promoted = true - if (!render || state.messageIDs.has(message.id)) return - state.messageIDs.add(message.id) - if (reuseVisibleWait && waiting) return - write([{ kind: "user", source: "system", text: message.text, phase: "start", messageID: message.id }]) - return - } - if (message.type === "skill") { - if (state.wait?.messageID === message.id) state.wait.promoted = true - if (!render || state.skillMessages.has(message.id)) { - state.skillMessages.add(message.id) - return - } - state.skillMessages.add(message.id) - write([skillCommit(message.id, message.name)]) - return - } - if (message.type === "shell") { - state.shellCommands.set(message.shellID, message.command) - if (state.shellWait?.messageID === message.id) state.shellWait.callID = message.shellID - const completed = message.time.completed !== undefined - if (!render) { - // Suppressed history: mark settled shells rendered so live redelivery - // stays silent. A still-running shell stays unmarked and renders in - // full when its live shell.ended event arrives. - if (completed) { - state.shellStarted.add(message.shellID) - state.shellEnded.add(message.shellID) - } - return - } - if (!state.shellStarted.has(message.shellID)) { - state.shellStarted.add(message.shellID) - write([ - shellCommit(message.shellID, message.command, { - text: "running shell", - phase: "start", - toolState: "running", - }), - ]) - } - if (completed && message.output && !state.shellEnded.has(message.shellID)) { - state.shellEnded.add(message.shellID) - write(shellTerminal(message.shellID, message.command, message, message.output)) - } - if (completed && state.shellWait?.callID === message.shellID) state.shellWait.resolve() - return - } - if (message.type !== "assistant") return - state.messageIDs.add(message.id) - let textOrdinal = 0 - let reasoningOrdinal = 0 - for (const item of message.content) { - if (item.type === "text") { - const id = `text:${textOrdinal++}` - const key = streamPartKey(message.id, id) - const sent = state.text.get(key)?.length ?? 0 - state.text.set(key, item.text) - if (render) state.projectedText.set(key, item.text) - if (render && item.text.length > sent) - write([ - { - kind: "assistant", - source: "assistant", - text: item.text.slice(sent), - phase: "progress", - messageID: message.id, - partID: id, - }, - ]) - continue - } - if (item.type === "reasoning") { - const id = `reasoning:${reasoningOrdinal++}` - const key = streamPartKey(message.id, id) - const sent = state.reasoning.get(key)?.length ?? 0 - state.reasoning.set(key, item.text) - if (render) state.projectedReasoning.set(key, item.text) - if (render && input.thinking && item.text.length > sent) - write([ - { - kind: "reasoning", - source: "reasoning", - text: sent === 0 ? `Thinking: ${item.text}` : item.text.slice(sent), - phase: "progress", - messageID: message.id, - partID: id, - }, - ]) - continue - } - if (render) renderTool(message.id, item) - } - if (render && message.error && !state.errors.has(message.id)) { - state.errors.add(message.id) - write([ - { - kind: "error", - source: "system", - text: errorMessage(message.error), - phase: "start", - messageID: message.id, - }, - ]) - } - } - - const hydrate = async (next: { render: boolean; reuseVisibleWait: boolean }) => { - const [messages, permissions, questions, active] = await Promise.all([ - input.sdk.message.list({ sessionID: input.sessionID, limit: input.replayLimit ?? 200, order: "desc" }), - input.sdk.permission.list({ sessionID: input.sessionID }), - input.sdk.question.list({ sessionID: input.sessionID }), - input.sdk.session.active(), - ]) - const projected = structuredClone(messages.data).toReversed() as SessionMessageInfo[] - for (const message of projected) renderMessage(message, next.render, next.reuseVisibleWait) - state.permissions = permissions.map(permission) - state.questions = questions.map(question) - syncBlockers() - await subagents.hydrate({ messages: [...projected], active }) - const running = input.sessionID in active - write([], { phase: running ? "running" : "idle", status: running ? "assistant responding" : "" }) - if (!running && state.wait && (state.wait.promoted || state.wait.interrupted)) { - const current = state.wait - state.wait = undefined - current.resolve() - } - } - - const apply = (event: RunV2Event) => { - if (catalogEvents.has(event.type)) { - if (input.directory && event.location?.directory && event.location.directory !== input.directory) return - input.onCatalogRefresh?.() - return - } - const source = sessionID(event) - if (source !== input.sessionID) { - if (source) subagents.foreign(source, event) - return - } - input.trace?.write("recv.event", event) - subagents.main(event) - if (event.type === "session.input.promoted") { - if (state.wait?.messageID === event.data.inputID) state.wait.promoted = true - state.messageIDs.add(event.data.inputID) - write([], { phase: "running", status: "waiting for assistant" }) - return - } - if (event.type === "session.step.started") { - write([], { phase: "running", status: "assistant responding" }) - return - } - if (event.type === "session.skill.activated") { - const messageID = messageIDFromEvent(event.id) - if (state.wait?.messageID === messageID) state.wait.promoted = true - if (state.skillMessages.has(messageID)) return - state.skillMessages.add(messageID) - write([skillCommit(messageID, event.data.name)]) - return - } - if (event.type === "session.shell.started") { - state.shellCommands.set(event.data.shell.id, event.data.shell.command) - const wait = state.shellWait - if (wait?.eventID === event.id) wait.callID = event.data.shell.id - if (state.shellStarted.has(event.data.shell.id)) return - state.shellStarted.add(event.data.shell.id) - write( - [ - shellCommit(event.data.shell.id, event.data.shell.command, { - text: "running shell", - phase: "start", - toolState: "running", - }), - ], - { - phase: "running", - status: "running shell", - }, - ) - return - } - if (event.type === "session.shell.ended") { - const command = state.shellCommands.get(event.data.shell.id) ?? event.data.shell.command - const commits: StreamCommit[] = [] - if (!state.shellStarted.has(event.data.shell.id)) { - state.shellStarted.add(event.data.shell.id) - if (command) - commits.push( - shellCommit(event.data.shell.id, command, { text: "running shell", phase: "start", toolState: "running" }), - ) - } - if (!state.shellEnded.has(event.data.shell.id)) { - state.shellEnded.add(event.data.shell.id) - commits.push(...shellTerminal(event.data.shell.id, command, event.data.shell, event.data.output)) - } - const wait = state.shellWait - const owned = wait?.callID === event.data.shell.id - write(commits, owned || state.wait || state.shellWait ? undefined : { phase: "idle", status: "" }) - if (owned) wait.resolve() - return - } - if (event.type === "session.text.started") { - return - } - if (event.type === "session.text.delta") { - const id = `text:${event.data.ordinal}` - const key = streamPartKey(event.data.assistantMessageID, id) - const projected = state.projectedText.get(key) - const covered = projected?.indexOf(event.data.delta) ?? -1 - if (projected && covered >= 0) { - state.projectedText.set(key, projected.slice(covered + event.data.delta.length)) - return - } - const previous = state.text.get(key) ?? "" - state.text.set(key, previous + event.data.delta) - write([ - { - kind: "assistant", - source: "assistant", - text: event.data.delta, - phase: "progress", - messageID: event.data.assistantMessageID, - partID: id, - }, - ]) - return - } - if (event.type === "session.text.ended") { - const id = `text:${event.data.ordinal}` - const key = streamPartKey(event.data.assistantMessageID, id) - const previous = state.text.get(key) ?? "" - if (event.data.text.length > previous.length) - write([ - { - kind: "assistant", - source: "assistant", - text: event.data.text.slice(previous.length), - phase: "progress", - messageID: event.data.assistantMessageID, - partID: id, - }, - ]) - state.text.set(key, event.data.text) - state.projectedText.delete(key) - return - } - if (event.type === "session.reasoning.started") { - return - } - if (event.type === "session.reasoning.delta") { - const id = `reasoning:${event.data.ordinal}` - const key = streamPartKey(event.data.assistantMessageID, id) - const projected = state.projectedReasoning.get(key) - const covered = projected?.indexOf(event.data.delta) ?? -1 - if (projected && covered >= 0) { - state.projectedReasoning.set(key, projected.slice(covered + event.data.delta.length)) - return - } - const previous = state.reasoning.get(key) ?? "" - state.reasoning.set(key, previous + event.data.delta) - if (input.thinking) - write([ - { - kind: "reasoning", - source: "reasoning", - text: previous ? event.data.delta : `Thinking: ${event.data.delta}`, - phase: "progress", - messageID: event.data.assistantMessageID, - partID: id, - }, - ]) - return - } - if (event.type === "session.reasoning.ended") { - const id = `reasoning:${event.data.ordinal}` - const key = streamPartKey(event.data.assistantMessageID, id) - const previous = state.reasoning.get(key) ?? "" - if (input.thinking && event.data.text.length > previous.length) - write([ - { - kind: "reasoning", - source: "reasoning", - text: previous ? event.data.text.slice(previous.length) : `Thinking: ${event.data.text}`, - phase: "progress", - messageID: event.data.assistantMessageID, - partID: id, - }, - ]) - state.reasoning.set(key, event.data.text) - state.projectedReasoning.delete(key) - return - } - if (event.type === "session.tool.input.started") { - state.tools.set(event.data.callID, { - messageID: event.data.assistantMessageID, - name: event.data.name, - input: {}, - started: event.created, - running: false, - }) - return - } - if (event.type === "session.tool.called") { - if (state.finishedTools.has(event.data.callID)) return - const current = state.tools.get(event.data.callID) - const item = structuredClone({ - type: "tool", - id: event.data.callID, - name: current?.name ?? "tool", - executed: event.data.executed, - providerState: event.data.state, - state: { status: "running", input: event.data.input, structured: {}, content: [] }, - time: { created: current?.started ?? event.created, ran: event.created }, - }) as SessionMessageAssistantTool - renderTool(event.data.assistantMessageID, item) - return - } - if (event.type === "session.tool.progress") return - if (event.type === "session.tool.success" || event.type === "session.tool.failed") { - const current = state.tools.get(event.data.callID) - const failed = event.type === "session.tool.failed" - const item = structuredClone({ - type: "tool", - id: event.data.callID, - name: current?.name ?? "tool", - executed: event.data.executed, - providerState: current?.providerState, - providerResultState: event.data.resultState, - state: failed - ? { - status: "error", - input: current?.input ?? {}, - structured: {}, - content: [], - error: event.data.error, - result: event.data.result, - } - : { - status: "completed", - input: current?.input ?? {}, - structured: event.data.structured, - content: event.data.content, - result: event.data.result, - }, - time: { created: current?.started ?? event.created, ran: current?.started, completed: event.created }, - }) as SessionMessageAssistantTool - renderTool(event.data.assistantMessageID, item) - return - } - if (event.type === "permission.v2.asked") { - if (!state.permissions.some((item) => item.id === event.data.id)) state.permissions.push(permission(event.data)) - syncBlockers() - return - } - if (event.type === "permission.v2.replied") { - state.permissions = state.permissions.filter((item) => item.id !== event.data.requestID) - syncBlockers() - return - } - if (event.type === "question.v2.asked") { - if (!state.questions.some((item) => item.id === event.data.id)) state.questions.push(question(event.data)) - syncBlockers() - return - } - if (event.type === "question.v2.replied" || event.type === "question.v2.rejected") { - state.questions = state.questions.filter((item) => item.id !== event.data.requestID) - syncBlockers() - return - } - if (event.type === "session.step.ended") { - const total = - event.data.tokens.input + - event.data.tokens.output + - event.data.tokens.reasoning + - event.data.tokens.cache.read + - event.data.tokens.cache.write - const usage = total > 0 ? total.toLocaleString() : "" - write([], { - usage: event.data.cost ? `${usage} · ${money.format(event.data.cost)}` : usage, - }) - return - } - if (event.type === "session.step.failed") { - state.errors.add(event.data.assistantMessageID) - if (state.wait) state.wait.failureRendered = true - write([{ kind: "error", source: "system", text: errorMessage(event.data.error), phase: "start" }]) - return - } - if (event.type === "session.execution.started") { - write([], { phase: "running" }) - return - } - if ( - event.type === "session.execution.succeeded" || - event.type === "session.execution.failed" || - event.type === "session.execution.interrupted" - ) { - write([], { phase: "idle", status: "" }) - const current = state.wait - if (!current || (!current.promoted && !current.interrupted)) return - state.wait = undefined - if (current.interrupted && event.type === "session.execution.interrupted" && event.data.reason === "user") { - current.resolve() - return - } - if (event.type === "session.execution.failed") { - if (current.failureRendered) { - current.resolve() - return - } - current.reject(new Error(errorMessage(event.data.error))) - return - } - if (event.type === "session.execution.interrupted") { - current.reject(new Error(`Session interrupted: ${event.data.reason}`)) - return - } - current.resolve() - } - } - - const receive = (event: RunV2Event) => { - if (state.buffered) { - state.buffered.push(event) - return - } - apply(event) - } - - const connect = async () => { - while (!controller.signal.aborted && !input.footer.isClosed) { - const error = await (async () => { - const connection = new AbortController() - const abortConnection = () => connection.abort() - controller.signal.addEventListener("abort", abortConnection, { once: true }) - const stream = input.sdk.event.subscribe({ signal: connection.signal })[Symbol.asyncIterator]() - try { - const first = await stream.next() - if (first.done || first.value.type !== "server.connected") throw new Error("Event stream disconnected") - const buffered: RunV2Event[] = [] - let booting = true - const consume = (async () => { - while (!connection.signal.aborted) { - const next = await stream.next() - if (next.done) throw new Error("Event stream disconnected") - if (booting) buffered.push(next.value) - else receive(next.value) - } - })() - void consume.catch(() => {}) - await hydrate({ render: state.initial ? input.replay === true : true, reuseVisibleWait: !state.initial }) - input.onCatalogRefresh?.() - state.initial = false - booting = false - for (const event of buffered.splice(0)) apply(event) - state.connected = true - readyResolve() - await consume - } finally { - controller.signal.removeEventListener("abort", abortConnection) - connection.abort() - void stream.return?.(undefined).catch(() => {}) - } - })().catch((error) => error) - state.connected = false - if (controller.signal.aborted || input.footer.isClosed) return - input.trace?.write("recv.reconnect", { error: formatUnknownError(error) }) - write([], { phase: "running", status: "reconnecting" }) - await wait(250, controller.signal) - } - } - const connection = connect() - try { - await ready - } catch (error) { - offFooterClose() - throw error - } finally { - controller.signal.removeEventListener("abort", abortReady) - } - - const runShellTurn = async (next: SessionTurnInput) => { - if (state.wait || state.shellWait) throw new Error("prompt already running") - if (!state.connected) throw new Error("Event stream is reconnecting") - const abort = new AbortController() - const onAbort = () => abort.abort() - next.signal?.addEventListener("abort", onAbort, { once: true }) - let rendered!: () => void - const output = new Promise((resolve) => { - rendered = resolve - }) - const eventID = Event.ID.create() - const active: ShellWait = { - eventID, - messageID: messageIDFromEvent(eventID), - resolve: rendered, - abort: () => abort.abort(), - } - state.shellWait = active - input.trace?.write("send.shell", { sessionID: input.sessionID, id: eventID, command: next.prompt.text }) - write([], { phase: "running", status: "running shell" }) - try { - await input.sdk.session.shell( - { sessionID: input.sessionID, id: eventID, command: next.prompt.text }, - { signal: abort.signal }, - ) - await Promise.race([output, wait(SHELL_OUTPUT_GRACE_MS, abort.signal)]) - } catch (error) { - if (abort.signal.aborted) return - throw error - } finally { - next.signal?.removeEventListener("abort", onAbort) - if (state.shellWait === active) state.shellWait = undefined - } - } - - // Shared settlement scaffolding for prompt-shaped turns: registers the wait, - // wires interruption, sends, then blocks until the live settled event (or a - // hydration pass over an idle session) resolves it. - const runTurnWait = async ( - next: SessionTurnInput, - messageID: string, - turn: { promoted?: boolean; send: () => Promise }, - ) => { - let resolve!: () => void - let reject!: (error: unknown) => void - const done = new Promise((ok, fail) => { - resolve = ok - reject = fail - }) - const active: Wait = { - messageID, - promoted: turn.promoted === true, - interrupted: false, - failureRendered: false, - resolve, - reject, - onVisibleOutput: next.onVisibleOutput, - } - state.wait = active - const interrupt = () => { - active.interrupted = true - void input.sdk.session.interrupt({ sessionID: input.sessionID }).catch(() => {}) - } - next.signal?.addEventListener("abort", interrupt, { once: true }) - try { - await turn.send() - await done - } catch (error) { - if (state.wait === active) state.wait = undefined - if (next.signal?.aborted) return - throw error - } finally { - next.signal?.removeEventListener("abort", interrupt) - } - } - - return { - async runPromptTurn(next) { - if (next.prompt.mode === "shell") { - await runShellTurn(next) - return - } - if (state.wait || state.shellWait) throw new Error("prompt already running") - if (!state.connected) throw new Error("Event stream is reconnecting") - const messageID = next.prompt.messageID - if (!messageID) throw new Error("Prompt message ID is required") - - const command = next.prompt.command - if (command?.source === "skill") { - input.trace?.write("send.skill", { sessionID: input.sessionID, messageID, skill: command.name }) - await runTurnWait(next, messageID, { - send: () => - input.sdk.session.skill( - { sessionID: input.sessionID, id: messageID, skill: command.name }, - { signal: next.signal }, - ), - }) - return - } - if (command) { - const selected = await resolveSelectedModel(input, next) - if (next.variant && !selected) throw new Error("Cannot select a variant before selecting a model") - // Agent and model ride the command payload; the server switches only - // when the command itself does not pin them. - const files = [ - ...(next.includeFiles ? next.files : []).map((file) => ({ uri: file.url, name: file.filename })), - ...promptFiles(next), - ] - const agents = promptAgents(next) - input.trace?.write("send.command", { sessionID: input.sessionID, messageID, command: command.name }) - await runTurnWait(next, messageID, { - send: () => - input.sdk.session.command( - { - sessionID: input.sessionID, - id: messageID, - command: command.name, - arguments: command.arguments, - agent: next.agent, - model: selected, - files: files.length ? files : undefined, - agents: agents.length ? agents : undefined, - delivery: "steer", - }, - { signal: next.signal }, - ), - }) - return - } - - if (next.agent) { - await input.sdk.session.switchAgent({ sessionID: input.sessionID, agent: next.agent }, { signal: next.signal }) - } - const selected = await resolveSelectedModel(input, next) - if (next.variant && !selected) throw new Error("Cannot select a variant before selecting a model") - if (selected) - await input.sdk.session.switchModel({ sessionID: input.sessionID, model: selected }, { signal: next.signal }) - - const prepared = await Promise.all((next.includeFiles ? next.files : []).map(prepareFile)) - const attachments = [ - ...prepared.flatMap((file) => (file.attachment ? [file.attachment] : [])), - ...promptFiles(next), - ] - const agents = promptAgents(next) - input.trace?.write("send.prompt", { sessionID: input.sessionID, messageID }) - await runTurnWait(next, messageID, { - send: () => - input.sdk.session.prompt( - { - sessionID: input.sessionID, - id: messageID, - text: [next.prompt.text, ...prepared.flatMap((file) => (file.text ? [file.text] : []))].join("\n\n"), - files: attachments.length ? attachments : undefined, - agents: agents.length ? agents : undefined, - delivery: "steer", - }, - { signal: next.signal }, - ), - }) - }, - async interruptActiveTurn() { - // A running shell holds no drain, so session.interrupt cannot reach it; - // abort the blocking request instead. The server-side command keeps its - // own lifecycle and simply loses its waiter. - const shell = state.shellWait - if (shell) { - shell.abort() - return - } - if (state.wait) state.wait.interrupted = true - await input.sdk.session.interrupt({ sessionID: input.sessionID }).catch(() => {}) - }, - selectSubagent(sessionID) { - subagents.select(sessionID) - }, - async replayOnResize(next) { - if (!input.replay || state.closed || input.footer.isClosed) return false - const buffered: RunV2Event[] = [] - state.buffered = buffered - try { - await input.footer.idle() - await next.reset() - state.messageIDs.clear() - state.text.clear() - state.projectedText.clear() - state.reasoning.clear() - state.projectedReasoning.clear() - state.tools.clear() - state.finishedTools.clear() - state.skillMessages.clear() - state.shellCommands.clear() - state.shellStarted.clear() - state.shellEnded.clear() - state.errors.clear() - await hydrate({ render: true, reuseVisibleWait: false }) - } finally { - state.buffered = undefined - } - for (const event of buffered) apply(event) - for (const row of next.localRows()) { - if (row.commit.messageID && state.messageIDs.has(row.commit.messageID)) continue - input.footer.append(row.commit) - } - return true - }, - async close() { - state.closed = true - offFooterClose() - controller.abort() - void connection.catch(() => {}) - }, - } -} diff --git a/packages/cli/src/mini/turn-summary.ts b/packages/cli/src/mini/turn-summary.ts deleted file mode 100644 index aa63a8ea0a..0000000000 --- a/packages/cli/src/mini/turn-summary.ts +++ /dev/null @@ -1,21 +0,0 @@ -import type { StreamCommit } from "./types" - -export function turnSummaryCommit(input: { - agent: string - model: string - duration: string - messageID?: string -}): StreamCommit { - return { - kind: "system", - text: `${input.agent} · ${input.model} · ${input.duration}`, - phase: "final", - source: "system", - summary: { - agent: input.agent, - model: input.model, - duration: input.duration, - }, - messageID: input.messageID, - } -} diff --git a/packages/cli/src/mini/ui.ts b/packages/cli/src/mini/ui.ts deleted file mode 100644 index 1caf2674fc..0000000000 --- a/packages/cli/src/mini/ui.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { EOL } from "node:os" - -export const Style = { - TEXT_DIM: "\x1b[90m", - TEXT_NORMAL: "\x1b[0m", - TEXT_WARNING_BOLD: "\x1b[93m\x1b[1m", - TEXT_DANGER_BOLD: "\x1b[91m\x1b[1m", -} - -export function println(...message: string[]) { - process.stderr.write(message.join(" ") + EOL) -} - -let blank = false - -export function empty() { - if (blank) return - println(Style.TEXT_NORMAL) - blank = true -} - -export function error(message: string) { - if (message.startsWith("Error: ")) message = message.slice("Error: ".length) - println(Style.TEXT_DANGER_BOLD + "Error: " + Style.TEXT_NORMAL + message) -} - -export * as UI from "./ui" diff --git a/packages/cli/src/server-process.ts b/packages/cli/src/server-process.ts deleted file mode 100644 index 9f688fb16e..0000000000 --- a/packages/cli/src/server-process.ts +++ /dev/null @@ -1,150 +0,0 @@ -export * as ServerProcess from "./server-process" - -import { NodeServices } from "@effect/platform-node" -import { Service } from "@opencode-ai/client/effect" -import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" -import { LayerNode } from "@opencode-ai/core/effect/layer-node" -import { Global } from "@opencode-ai/core/global" -import { InstallationVersion } from "@opencode-ai/core/installation/version" -import { AppProcess } from "@opencode-ai/core/process" -import { EffectFlock } from "@opencode-ai/core/util/effect-flock" -import { start } from "@opencode-ai/server/process" -import { randomBytes, randomUUID } from "node:crypto" -import path from "node:path" -import { Effect, Exit, FileSystem, Logger, Option, Redacted, Schedule, Schema, Scope } from "effect" -import { HttpServer } from "effect/unstable/http" -import { Env } from "./env" -import { ServiceConfig } from "./services/service-config" -import { Updater } from "./services/updater" - -export type Mode = "default" | "service" | "stdio" - -export type Options = { - readonly mode: Mode - readonly hostname?: string - readonly port?: number -} - -export const run = Effect.fn("cli.server-process.run")((options: Options) => - processEffect(options).pipe( - Effect.provide(Updater.layer), - Effect.provide(AppNodeBuilder.build(LayerNode.group([Global.node, AppProcess.node, EffectFlock.node]))), - Effect.provide(NodeServices.layer), - ), -) - -const processEffect = Effect.fnUntraced(function* (options: Options) { - if (options.mode === "service") yield* Effect.sync(() => process.chdir(Global.Path.home)) - return yield* Effect.scoped( - Effect.gen(function* () { - const serviceOptions = options.mode === "service" ? yield* ServiceConfig.options() : undefined - const lockScope = serviceOptions === undefined ? undefined : yield* acquireServiceLock(serviceOptions.file) - if ( - serviceOptions !== undefined && - lockScope !== undefined && - (yield* Service.discover(serviceOptions)) !== undefined - ) { - yield* Scope.close(lockScope, Exit.void) - return - } - const environmentPassword = yield* Env.password - // Keep the lease credential out of the environment inherited by tools. - if (options.mode === "stdio") { - delete process.env.OPENCODE_PASSWORD - delete process.env.OPENCODE_SERVER_PASSWORD - } - const config = options.mode === "service" ? yield* ServiceConfig.read() : {} - const password = - options.mode === "service" - ? yield* ServiceConfig.password() - : environmentPassword - ? Redacted.value(environmentPassword) - : randomBytes(32).toString("base64url") - if (!password) return yield* Effect.fail(new Error("Missing server password")) - const address = yield* start({ - hostname: options.hostname ?? config.hostname ?? "127.0.0.1", - port: Option.fromNullishOr(options.port ?? config.port), - password, - restartContinuity: options.mode === "service", - }).pipe(Effect.provide(Logger.layer([], { mergeWithExisting: false }))) - if (lockScope !== undefined) { - yield* register(address, password) - yield* Scope.close(lockScope, Exit.void) - } - const url = HttpServer.formatAddress(address) - console.log(options.mode === "stdio" ? JSON.stringify({ url }) : `server listening on ${url}`) - if (options.mode === "default" && !environmentPassword) console.log(`server password ${password}`) - const updater = yield* Updater.Service - yield* updater.check().pipe(Effect.schedule(Schedule.spaced("10 minutes")), Effect.forkScoped) - return yield* options.mode === "stdio" ? waitForStdinClose() : Effect.never - }).pipe(Effect.annotateLogs({ role: "server" })), - ) -}) - -const acquireServiceLock = Effect.fnUntraced(function* (file: string) { - const flock = yield* EffectFlock.Service - const scope = yield* Scope.make() - yield* Effect.addFinalizer((exit) => Scope.close(scope, exit)) - yield* flock - .acquire(`service:${file}`, undefined, { staleMs: 3_000, timeoutMs: 3_000 }) - .pipe(Effect.provideService(Scope.Scope, scope)) - return scope -}) - -// The latest atomic registration wins. A displaced process notices the new id, -// exits, and cannot remove its successor's registration from its finalizer. -const infoJson = Schema.fromJsonString(Service.Info) -const encodeInfo = Schema.encodeEffect(infoJson) -const decodeInfo = Schema.decodeUnknownEffect(infoJson) - -const register = Effect.fnUntraced(function* (address: HttpServer.Address, password: string) { - const fs = yield* FileSystem.FileSystem - const options = yield* ServiceConfig.options() - const id = randomUUID() - const temp = options.file + "." + id + ".tmp" - yield* fs.makeDirectory(path.dirname(options.file), { recursive: true }) - const encoded = yield* encodeInfo({ - id, - version: InstallationVersion, - url: HttpServer.formatAddress(address), - pid: process.pid, - password, - }) - yield* fs.writeFileString(temp, encoded, { mode: 0o600 }) - yield* fs.rename(temp, options.file) - const currentID = fs.readFileString(options.file).pipe( - Effect.flatMap(decodeInfo), - Effect.map((info) => info.id), - Effect.orElseSucceed(() => undefined), - ) - yield* currentID.pipe( - Effect.flatMap((current) => - current === id - ? Effect.void - : Effect.try({ try: () => process.kill(process.pid, "SIGTERM"), catch: (cause) => cause }).pipe(Effect.ignore), - ), - Effect.repeat(Schedule.spaced("10 seconds")), - Effect.forkScoped, - ) - yield* Effect.addFinalizer(() => - currentID.pipe( - Effect.flatMap((current) => (current === id ? fs.remove(options.file) : Effect.void)), - Effect.ignore, - ), - ) -}) - -function waitForStdinClose() { - return Effect.callback((resume) => { - const close = () => resume(Effect.void) - process.stdin.once("end", close) - process.stdin.once("close", close) - process.stdin.resume() - if (process.stdin.readableEnded || process.stdin.destroyed) close() - return Effect.sync(() => { - process.stdin.off("end", close) - process.stdin.off("close", close) - process.stdin.pause() - }) - }) -} diff --git a/packages/cli/src/services/daemon.ts b/packages/cli/src/services/daemon.ts new file mode 100644 index 0000000000..bd30656f55 --- /dev/null +++ b/packages/cli/src/services/daemon.ts @@ -0,0 +1,192 @@ +import { Global } from "@opencode-ai/core/global" +import { InstallationVersion } from "@opencode-ai/core/installation/version" +import { createOpencodeClient } from "@opencode-ai/sdk/v2/client" +import { ServerAuth } from "@opencode-ai/server/auth" +import { Context, Effect, FileSystem, Layer, Option, Schedule, Schema, Scope } from "effect" +import { HttpServer } from "effect/unstable/http" +import { randomBytes, randomUUID } from "crypto" +import { spawn } from "node:child_process" +import path from "path" + +export interface Interface { + readonly client: () => Effect.Effect, unknown> + readonly transport: () => Effect.Effect<{ url: string; headers: RequestInit["headers"] }, unknown> + readonly start: () => Effect.Effect + readonly status: () => Effect.Effect + readonly stop: () => Effect.Effect + readonly password: (value?: string) => Effect.Effect + readonly register: (address: HttpServer.Address) => Effect.Effect +} + +export class Service extends Context.Service()("@opencode/cli/Daemon") {} + +const Registration = Schema.Struct({ + id: Schema.optional(Schema.String), + version: Schema.optional(Schema.String), + url: Schema.String, + pid: Schema.Int.check(Schema.isGreaterThan(0)), +}) +type Registration = typeof Registration.Type + +function sameRegistration(left: Registration, right: Registration) { + return left.id === right.id && left.version === right.version && left.url === right.url && left.pid === right.pid +} + +export const layer = Layer.effect( + Service, + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem + const directory = Global.Path.state + const file = path.join(directory, "server.json") + const passwordFile = path.join(directory, "password") + const decodeRegistration = Schema.decodeUnknownEffect(Schema.fromJsonString(Registration)) + + const password = Effect.fn("cli.daemon.password")(function* (value?: string) { + const existing = yield* fs.readFileString(passwordFile).pipe(Effect.catch(() => Effect.succeed(undefined))) + if (value === undefined && existing) return existing + + // Keep one private credential across server restarts so discovered clients + // can reconnect without exposing a password flag or environment variable. + const generated = value ?? randomBytes(32).toString("base64url") + const temp = passwordFile + ".tmp" + yield* fs.makeDirectory(directory, { recursive: true }) + yield* fs.writeFileString(temp, generated, { mode: 0o600 }) + yield* fs.rename(temp, passwordFile) + return generated + }) + + const registration = Effect.fnUntraced(function* () { + return yield* fs.readFileString(file).pipe(Effect.flatMap(decodeRegistration)) + }) + + const createClient = Effect.fnUntraced(function* (url: string) { + return createOpencodeClient({ baseUrl: url, headers: ServerAuth.headers({ password: yield* password() }) }) + }) + + const healthy = Effect.fnUntraced(function* () { + const info = yield* registration() + const client = yield* createClient(info.url) + const response = yield* Effect.tryPromise(() => client.v2.health.get({ signal: AbortSignal.timeout(2_000) })) + if (response.data?.healthy === true) return info + return yield* Effect.fail(new Error("Registered server is not healthy")) + }) + + const compatible = Effect.fnUntraced(function* () { + const info = yield* healthy() + if (info.version === InstallationVersion) return info + return yield* Effect.fail(new Error("Registered server version does not match the client")) + }) + + const signal = (pid: number, signal: NodeJS.Signals) => + Effect.try({ try: () => process.kill(pid, signal), catch: (cause) => cause }).pipe(Effect.ignore) + + const awaitStopped = Effect.fnUntraced(function* (pid: number) { + const running = yield* Effect.try({ try: () => process.kill(pid, 0), catch: () => false }).pipe( + Effect.orElseSucceed(() => false), + ) + if (!running) return true + return yield* Effect.fail(new Error(`Server process ${pid} is still running`)) + }) + + const stopProcess = Effect.fnUntraced(function* (info: Registration) { + const current = yield* healthy().pipe(Effect.option) + if (Option.isNone(current) || !sameRegistration(current.value, info)) return + + yield* signal(info.pid, "SIGTERM") + const stopped = yield* awaitStopped(info.pid).pipe( + Effect.retry(Schedule.spaced("50 millis").pipe(Schedule.both(Schedule.recurs(100)))), + Effect.option, + ) + if (Option.isSome(stopped)) return + + const latest = yield* healthy().pipe(Effect.option) + if (Option.isNone(latest) || !sameRegistration(latest.value, info)) return + yield* signal(info.pid, "SIGKILL") + yield* awaitStopped(info.pid).pipe( + Effect.retry(Schedule.spaced("50 millis").pipe(Schedule.both(Schedule.recurs(100)))), + ) + }) + + const start = Effect.fn("cli.daemon.start")(function* () { + const existing = yield* healthy().pipe(Effect.option) + const found = Option.getOrUndefined(existing) + const compiled = path.basename(process.execPath).replace(/\.exe$/, "") !== "bun" + if (found?.version === InstallationVersion && compiled) return found.url + if (found) yield* stopProcess(found).pipe(Effect.ignore) + + const entrypoint = compiled ? undefined : process.argv[1] + if (!compiled && entrypoint === undefined) + return yield* Effect.fail(new Error("Failed to resolve CLI entrypoint")) + yield* Effect.try({ + try: () => { + spawn(process.execPath, [...(entrypoint ? [entrypoint] : []), "serve", "--register"], { + detached: true, + stdio: "ignore", + }).unref() + }, + catch: (cause) => new Error("Failed to start server", { cause }), + }) + + return yield* compatible().pipe( + Effect.retry(Schedule.spaced("50 millis").pipe(Schedule.both(Schedule.recurs(100)))), + Effect.map((info) => info.url), + Effect.mapError(() => new Error("Failed to start server")), + ) + }) + + const transport = Effect.fn("cli.daemon.transport")(function* () { + return { url: yield* start(), headers: ServerAuth.headers({ password: yield* password() }) } + }) + + const client = Effect.fn("cli.daemon.client")(function* () { + const connection = yield* transport() + return createOpencodeClient({ baseUrl: connection.url, headers: connection.headers }) + }) + + const status = Effect.fn("cli.daemon.status")(function* () { + const existing = yield* healthy().pipe(Effect.option) + const found = Option.getOrUndefined(existing) + if (found?.version === InstallationVersion) return found.url + if (found) return undefined + yield* fs.remove(file).pipe(Effect.ignore) + return undefined + }) + + const stop = Effect.fn("cli.daemon.stop")(function* () { + const existing = yield* healthy().pipe(Effect.option) + // A stale registration may point at a PID that has since been reused by + // another process. Only signal the PID after authenticating the server. + if (Option.isNone(existing)) return yield* fs.remove(file).pipe(Effect.ignore) + yield* stopProcess(existing.value) + yield* fs.remove(file).pipe(Effect.ignore) + }) + + const register = Effect.fn("cli.daemon.register")(function* (address: HttpServer.Address) { + const id = randomUUID() + const temp = file + "." + id + ".tmp" + yield* fs.makeDirectory(directory, { recursive: true }) + yield* fs.writeFileString( + temp, + JSON.stringify({ id, version: InstallationVersion, url: HttpServer.formatAddress(address), pid: process.pid }), + { mode: 0o600 }, + ) + yield* fs.rename(temp, file) + yield* registration().pipe( + Effect.flatMap((info) => (info.id === id ? Effect.void : signal(process.pid, "SIGTERM"))), + Effect.catch(() => signal(process.pid, "SIGTERM")), + Effect.repeat(Schedule.spaced("10 seconds")), + Effect.forkScoped, + ) + yield* Effect.addFinalizer(() => + registration().pipe( + Effect.flatMap((info) => (info.id === id ? fs.remove(file) : Effect.void)), + Effect.ignore, + ), + ) + }) + + return Service.of({ client, transport, start, status, stop, password, register }) + }), +) + +export * as Daemon from "./daemon" diff --git a/packages/cli/src/services/server.ts b/packages/cli/src/services/server.ts deleted file mode 100644 index 34ddcf965d..0000000000 --- a/packages/cli/src/services/server.ts +++ /dev/null @@ -1,101 +0,0 @@ -import { NodeFileSystem } from "@effect/platform-node" -import { Service } from "@opencode-ai/client/effect" -import { ClientError, isUnauthorizedError, OpenCode } from "@opencode-ai/client/promise" -import { InstallationVersion } from "@opencode-ai/core/installation/version" -import { Effect, Redacted } from "effect" -import { Env } from "../env" -import { ServiceConfig } from "./service-config" -import { Standalone } from "./standalone" - -export type Args = { - readonly server?: string - readonly standalone?: boolean - readonly mismatch?: "replace" | "ignore" | "error" - readonly onStart?: Service.StartOptions["onStart"] -} - -export type Resolved = { - readonly endpoint: Service.Endpoint - readonly reconnect?: (attempt: number) => Promise - readonly reload?: () => Promise -} - -export const resolve = Effect.fn("cli.server.resolve")(function* (args: Args) { - if (args.server !== undefined && args.standalone) - return yield* Effect.fail(new Error("--server and --standalone cannot be combined")) - if (args.server !== undefined) { - const password = yield* Env.password - const endpoint = { - url: args.server, - auth: password - ? { type: "basic" as const, username: "opencode", password: Redacted.value(password) } - : undefined, - } satisfies Service.Endpoint - const client = OpenCode.make({ baseUrl: endpoint.url, headers: Service.headers(endpoint) }) - const health = yield* Effect.tryPromise({ - try: () => client.health.get({ signal: AbortSignal.timeout(5_000) }), - catch: (cause) => connectError(endpoint, cause), - }) - if (health.version !== InstallationVersion) - process.stderr.write( - `Warning: Server at ${endpoint.url} has version ${health.version}; this client is ${InstallationVersion}. Continuing anyway.\n`, - ) - return { endpoint } satisfies Resolved - } - if (args.standalone) { - return { endpoint: yield* Standalone.start() } satisfies Resolved - } - - const options = yield* ServiceConfig.options() - const endpoint = yield* resolveManaged({ ...options, onStart: args.onStart }, args.mismatch ?? "replace") - const reconnectOptions = { ...options, version: undefined } - return { - endpoint, - reconnect: (attempt) => - Effect.runPromise( - Effect.gen(function* () { - if (attempt > 3) return yield* Service.start(reconnectOptions) - const endpoint = yield* Service.discover(reconnectOptions) - if (endpoint !== undefined) return endpoint - return yield* Effect.fail(new Error("Background server is unavailable")) - }).pipe(Effect.provide(NodeFileSystem.layer)), - ), - reload: () => - Effect.runPromise( - Effect.gen(function* () { - yield* Service.stop(options) - yield* Service.start(options) - }).pipe(Effect.provide(NodeFileSystem.layer)), - ), - } satisfies Resolved -}) - -const resolveManaged = Effect.fnUntraced(function* ( - options: Service.StartOptions, - mismatch: NonNullable, -) { - if (mismatch === "replace") return yield* Service.start(options) - if (mismatch === "ignore") return yield* Service.start({ ...options, version: undefined }) - - const compatible = yield* Service.discover(options) - if (compatible !== undefined) return compatible - const existing = yield* Service.discover({ ...options, version: undefined }) - if (existing !== undefined) return yield* Effect.fail(new Error("Background server version does not match this client")) - return yield* Service.start(options) -}) - -function connectError(endpoint: Service.Endpoint, cause: unknown) { - if (isUnauthorizedError(cause)) { - return new Error( - endpoint.auth === undefined - ? `Server at ${endpoint.url} requires a password; set OPENCODE_PASSWORD` - : `Server at ${endpoint.url} rejected the password`, - { cause }, - ) - } - if (cause instanceof ClientError && cause.reason === "Transport") - return new Error(`Could not reach server at ${endpoint.url}`, { cause }) - return new Error(`Server at ${endpoint.url} did not provide a compatible V2 health response`, { cause }) -} - -export * as Server from "./server" diff --git a/packages/cli/src/services/service-config.ts b/packages/cli/src/services/service-config.ts deleted file mode 100644 index ecdd5bf19a..0000000000 --- a/packages/cli/src/services/service-config.ts +++ /dev/null @@ -1,143 +0,0 @@ -import { Global } from "@opencode-ai/core/global" -import { InstallationChannel, InstallationVersion } from "@opencode-ai/core/installation/version" -import { Service } from "@opencode-ai/client/effect" -import { Effect, FileSystem, Schema } from "effect" -import { randomBytes } from "crypto" -import path from "path" - -// The CLI's service configuration file, plus the Service.Options binding that -// points the client package's service operations at this CLI: which -// registration file (by channel), which version, and how to spawn opencode. - -export const Info = Schema.Struct({ - hostname: Schema.optional(Schema.String), - port: Schema.optional(Schema.Int.check(Schema.isGreaterThanOrEqualTo(1), Schema.isLessThanOrEqualTo(65_535))), - password: Schema.optional(Schema.String), -}) -export type Info = typeof Info.Type - -const keys = ["hostname", "port", "password"] as const -type Key = (typeof keys)[number] - -const decodeInfo = Schema.decodeUnknownEffect(Schema.fromJsonString(Info)) - -function configKey(key: string): Key { - if (keys.includes(key as Key)) return key as Key - throw new Error(`Unknown service config key: ${key}`) -} - -const env = Effect.gen(function* () { - const fs = yield* FileSystem.FileSystem - const global = yield* Global.Service - const filename = InstallationChannel === "local" ? "service-local.json" : "service.json" - return { - fs, - file: path.join(global.state, filename), - configFile: path.join(global.config, filename), - } -}) - -export const options = Effect.fnUntraced(function* () { - const { file } = yield* env - const compiled = path.basename(process.execPath).replace(/\.exe$/, "") !== "bun" - const entrypoint = compiled ? undefined : process.argv[1] - if (!compiled && entrypoint === undefined) return yield* Effect.fail(new Error("Failed to resolve CLI entrypoint")) - return { - file, - version: InstallationVersion, - command: [process.execPath, ...(entrypoint ? [entrypoint] : []), "serve", "--service"], - } -}) - -export const read = Effect.fn("cli.service-config.read")(function* () { - const { fs, configFile } = yield* env - return yield* fs.readFileString(configFile).pipe( - Effect.flatMap(decodeInfo), - Effect.catch(() => Effect.succeed({} as Info)), - ) -}) - -const write = Effect.fn("cli.service-config.write")(function* (value: Info) { - const { fs, configFile } = yield* env - const temp = configFile + ".tmp" - yield* fs.makeDirectory(path.dirname(configFile), { recursive: true }) - yield* fs.writeFileString(temp, JSON.stringify(value, null, 2) + "\n", { mode: 0o600 }) - yield* fs.rename(temp, configFile) -}) - -export const password = Effect.fn("cli.service-config.password")(function* (value?: string) { - const existing = yield* read() - if (value === undefined && existing.password) return existing.password - const next = value ?? randomBytes(32).toString("base64url") - - // Keep one private credential across server restarts so discovered clients - // can reconnect without exposing a password flag or environment variable. - yield* write({ ...existing, password: next }) - return next -}) - -export const get = Effect.fn("cli.service-config.get")(function* (key?: string) { - if (key === undefined) { - const { password: _password, ...safe } = yield* read() - return JSON.stringify(safe, null, 2) - } - switch (configKey(key)) { - case "hostname": { - return (yield* read()).hostname ?? "" - } - case "port": { - const port = (yield* read()).port - return port === undefined ? "" : String(port) - } - case "password": { - return yield* password() - } - } -}) - -export const set = Effect.fn("cli.service-config.set")(function* (key: string, value: string) { - switch (configKey(key)) { - case "hostname": { - yield* Service.stop(yield* options()) - yield* write({ ...(yield* read()), hostname: value }) - return - } - case "port": { - const port = Number(value) - if (!Number.isInteger(port) || port < 1 || port > 65_535) throw new Error("Port must be between 1 and 65535") - yield* Service.stop(yield* options()) - yield* write({ ...(yield* read()), port }) - return - } - case "password": { - yield* Service.stop(yield* options()) - yield* password(value) - return - } - } -}) - -export const unset = Effect.fn("cli.service-config.unset")(function* (key: string) { - switch (configKey(key)) { - case "hostname": { - yield* Service.stop(yield* options()) - const { hostname: _hostname, ...next } = yield* read() - yield* write(next) - return - } - case "port": { - yield* Service.stop(yield* options()) - const { port: _port, ...next } = yield* read() - yield* write(next) - return - } - case "password": { - yield* Service.stop(yield* options()) - const { password: _password, ...next } = yield* read() - yield* write(next) - return - } - } -}) - -export * as ServiceConfig from "./service-config" diff --git a/packages/cli/src/services/standalone.ts b/packages/cli/src/services/standalone.ts deleted file mode 100644 index 099b1c9df1..0000000000 --- a/packages/cli/src/services/standalone.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { Service } from "@opencode-ai/client/effect" -import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" -import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" -import { Effect, Schema, Stream } from "effect" -import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process" -import { randomBytes } from "node:crypto" -import path from "node:path" - -const Ready = Schema.Struct({ url: Schema.String }) -const decodeReady = Schema.decodeUnknownPromise(Schema.fromJsonString(Ready)) - -type Options = { - readonly command?: ReadonlyArray -} - -function command(password: string, options: Options) { - const compiled = path.basename(process.execPath).replace(/\.exe$/, "") !== "bun" - const entrypoint = compiled ? [] : process.argv[1] ? [process.argv[1]] : [] - if (!compiled && entrypoint.length === 0) throw new Error("Failed to resolve CLI entrypoint") - const [executable, ...args] = options.command ?? [process.execPath, ...entrypoint, "serve"] - if (!executable) throw new Error("Failed to resolve standalone server command") - return ChildProcess.make(executable, [...args, "--stdio", "--port", "0"], { - cwd: process.cwd(), - // Explicit entry wins over anything inherited, so a user-exported - // OPENCODE_PASSWORD cannot shadow the child's lease credential. - env: { OPENCODE_PASSWORD: password }, - extendEnv: true, - // The server treats EOF on this pipe as the end of its ownership lease. - // The OS closes it even when the TUI is killed before Effect finalizers run. - stdin: "pipe", - stderr: "ignore", - killSignal: "SIGTERM", - forceKillAfter: "3 seconds", - }) -} - -const makeEndpoint = Effect.fn("cli.standalone.endpoint")( - function* (options: Options) { - const password = randomBytes(32).toString("base64url") - const spawner = yield* ChildProcessSpawner.ChildProcessSpawner - const proc = yield* spawner.spawn(command(password, options)) - const output = yield* proc.stdout.pipe(Stream.decodeText(), Stream.splitLines, Stream.take(1), Stream.mkString) - if (!output) return yield* Effect.fail(new Error("Standalone server exited before reporting readiness")) - const ready = yield* Effect.tryPromise(() => decodeReady(output)) - return { - url: ready.url, - auth: { type: "basic" as const, username: "opencode", password }, - pid: proc.pid, - } satisfies Service.Endpoint & { readonly pid: number } - }, - Effect.provide(AppNodeBuilder.build(CrossSpawnSpawner.node)), -) - -export function start(options: Options = {}) { - return makeEndpoint(options) -} - -export * as Standalone from "./standalone" diff --git a/packages/cli/src/services/update-preflight.tsx b/packages/cli/src/services/update-preflight.tsx deleted file mode 100644 index e471e798b0..0000000000 --- a/packages/cli/src/services/update-preflight.tsx +++ /dev/null @@ -1,494 +0,0 @@ -/** @jsxImportSource @opentui/solid */ -// Split-footer status shown while a freshly launched CLI replaces a -// version-mismatched background service before the TUI attaches. -import { createCliRenderer, RGBA, TextAttributes, type CliRenderer, type ThemeMode } from "@opentui/core" -import { render, useTerminalDimensions } from "@opentui/solid" -import { InstallationVersion } from "@opencode-ai/core/installation/version" -import { registerOpencodeSpinner } from "@opencode-ai/tui/component/register-spinner" -import { SPINNER_FRAMES } from "@opencode-ai/tui/component/spinner" -import { go } from "@opencode-ai/tui/logo" -import { - batch, - createEffect, - createMemo, - createSignal, - For, - Index, - on, - onCleanup, - onMount, - Show, - untrack, -} from "solid-js" - -const stages = ["Keeping your session safe", "Starting the new background service", "Loading OpenCode"] as const -const stageFloor = 480 -const transitionDuration = 420 -const completionHold = 650 - -export type Handle = { - readonly begin: (from?: string) => boolean - readonly loading: () => void - readonly finish: () => Promise - readonly fail: (message: string) => Promise - readonly close: () => Promise -} - -export type Handoff = { - readonly renderer: CliRenderer - readonly mode: ThemeMode | null - readonly complete: () => void -} - -export const make = (): Handle => { - let session: Promise | undefined - return { - begin: (from) => { - if (!process.stdout.isTTY || !process.stdin.isTTY) return false - session ??= open(from).catch(() => { - process.stderr.write("Restarting background server (version mismatch)...\n") - return undefined - }) - return true - }, - loading: () => { - void session?.then((active) => active?.loading()) - }, - finish: async () => { - const active = await session - return active?.finish() - }, - fail: async (message) => { - const active = await session - await active?.fail(message) - }, - close: async () => { - const active = await session - await active?.close() - }, - } -} - -type Session = { - readonly loading: () => Promise - readonly finish: () => Promise - readonly fail: (message: string) => Promise - readonly close: () => Promise -} - -async function open(from?: string): Promise { - registerOpencodeSpinner() - const [active, setActive] = createSignal(0) - const [outcome, setOutcome] = createSignal<"running" | "success" | "failure">("running") - const [failure, setFailure] = createSignal("") - const [animating, setAnimating] = createSignal(true) - const [visible, setVisible] = createSignal(true) - let resolveOutcome: (() => void) | undefined - const renderer = await createCliRenderer({ - stdin: process.stdin, - useMouse: false, - autoFocus: false, - openConsoleOnError: false, - exitOnCtrlC: false, - screenMode: "split-footer", - footerHeight: 4, - targetFps: 60, - useKittyKeyboard: {}, - consoleOptions: { - keyBindings: [{ name: "y", ctrl: true, action: "copy-selection" }], - }, - externalOutputMode: "capture-stdout", - consoleMode: "disabled", - }) - const terminalMode = renderer.waitForThemeMode(1000).catch(() => null) - await render( - () => ( - - resolveOutcome?.()} - /> - - ), - renderer, - ).catch((error) => { - if (!renderer.isDestroyed) renderer.destroy() - throw error - }) - let shownAt = performance.now() - const waitForStage = async () => { - const remaining = stageFloor - (performance.now() - shownAt) - if (remaining > 0) await Bun.sleep(remaining) - } - const advance = async (stage: number) => { - await waitForStage() - if (outcome() !== "running") return - setActive(stage) - shownAt = performance.now() - } - // Service.start currently exposes only its start boundary, so this first - // transition is time-based. Finer lifecycle callbacks remain follow-up work. - const auto = advance(1) - const transitionTo = async (next: "success" | "failure", hold: number) => { - const settled = Promise.withResolvers() - resolveOutcome = settled.resolve - setOutcome(next) - const completed = await Promise.race([ - settled.promise.then(() => true), - Bun.sleep(transitionDuration + 500).then(() => false), - ]) - resolveOutcome = undefined - setAnimating(false) - if (completed) await Bun.sleep(hold) - } - let closing: Promise | undefined - let transferred = false - const close = () => - (closing ??= (async () => { - if (transferred) return - setAnimating(false) - if (renderer.isDestroyed) return - renderer.pause() - await Promise.race([renderer.idle(), Bun.sleep(500)]) - renderer.destroy() - })()) - let loading: Promise | undefined - const load = () => - (loading ??= (async () => { - await auto - await advance(2) - })()) - let settled: Promise | undefined - const settle = (task: () => Promise) => (settled ??= task()) - return { - loading: load, - finish: async () => { - await settle(async () => { - await load() - await waitForStage() - await transitionTo("success", completionHold) - }) - const mode = await terminalMode - renderer.externalOutputMode = "passthrough" - renderer.screenMode = "alternate-screen" - renderer.consoleMode = "console-overlay" - renderer.requestRender() - await Promise.race([renderer.idle(), Bun.sleep(500)]) - transferred = true - return { - renderer, - mode, - complete: () => setVisible(false), - } - }, - fail: (message) => - settle(async () => { - setFailure(message) - await transitionTo("failure", 250) - await close() - }), - close, - } -} - -const colors = { - accent: RGBA.fromHex("#a6b8ff"), - accentBright: RGBA.fromHex("#eef1ff"), - accentDim: RGBA.fromHex("#596998"), - error: RGBA.fromHex("#ff8192"), - muted: RGBA.fromHex("#808080"), - success: RGBA.fromHex("#8bd5a5"), - text: RGBA.fromHex("#eeeeee"), -} - -const monogram = go.right.slice(1) -const sweepBlend = 8 -const textDim = RGBA.fromHex("#4c4c4c") -const rampSteps = 32 - -const blend = (from: RGBA, to: RGBA, amount: number) => - RGBA.fromValues( - from.r + (to.r - from.r) * amount, - from.g + (to.g - from.g) * amount, - from.b + (to.b - from.b) * amount, - ) -const ramp = (from: RGBA, to: RGBA) => - Array.from({ length: rampSteps + 1 }, (_, step) => blend(from, to, step / rampSteps)) -const railRamp = ramp(colors.accentDim, colors.accentBright) -const monogramRamp = ramp(colors.muted, colors.accent) -const rampCache = new Map>() -const rampFor = (color: RGBA) => { - const cached = rampCache.get(color) - if (cached) return cached - const result = ramp(textDim, color) - rampCache.set(color, result) - return result -} -const shade = (palette: ReadonlyArray, brightness: number) => - palette[Math.round(Math.max(0, Math.min(1, brightness)) * rampSteps)] - -type Cell = { readonly char: string; readonly color: RGBA; readonly bold?: boolean } -const styled = (text: string, color: RGBA, bold?: boolean): Cell[] => - Array.from(text).map((char) => ({ char, color, bold })) -const phrase = (...segments: ReadonlyArray): Cell[] => - segments.flatMap((segment, index) => [ - ...(index > 0 ? styled(" ", colors.muted) : []), - ...styled(segment[0], segment[1], segment[2]), - ]) - -function Monogram(props: { ink: () => RGBA }) { - const shadow = createMemo(() => { - const ink = props.ink() - return RGBA.fromValues(ink.r * 0.25, ink.g * 0.25, ink.b * 0.25) - }) - return ( - - - {(line) => ( - - - {(char) => - char === "_" ? ( - - {" "} - - ) : ( - - {char} - - ) - } - - - )} - - - ) -} - -type CellTransition = { from: Cell[]; to: Cell[]; done?: () => void } - -function createTransition(render: (transition: CellTransition, progress: number) => Cell[]) { - const [state, setState] = createSignal<{ from: Cell[]; to: Cell[]; done?: () => void } | undefined>() - const [progress, setProgress] = createSignal(0) - let elapsed = 0 - const cells = createMemo(() => { - const transition = state() - if (!transition) return undefined - return render(transition, progress()) - }) - return { - start(from: Cell[], to: Cell[], done?: () => void) { - elapsed = 0 - setProgress(0) - setState({ from, to, done }) - }, - tick(deltaTime: number) { - const transition = state() - if (!transition) return - elapsed = Math.min(transitionDuration, elapsed + deltaTime) - setProgress(elapsed / transitionDuration) - if (elapsed < transitionDuration) return - setState(undefined) - transition.done?.() - }, - cells, - progress, - } -} - -const createSweep = () => - createTransition((transition, progress) => { - const length = Math.max(transition.from.length, transition.to.length) - const front = smoothstep(progress) * (length + 2 * sweepBlend) - sweepBlend - return Array.from({ length }, (_, index) => { - const passed = Math.max(0, Math.min(1, (front - index) / sweepBlend)) - const brightness = smoothstep(Math.abs(passed * 2 - 1)) - const cell = (passed >= 0.5 ? transition.to[index] : transition.from[index]) ?? { - char: " ", - color: colors.text, - } - return { ...cell, color: shade(rampFor(cell.color), brightness) } - }) - }) - -const createFade = () => - createTransition((transition, progress) => { - const entering = progress >= 0.5 - const brightness = smoothstep(entering ? progress * 2 - 1 : 1 - progress * 2) - return (entering ? transition.to : transition.from).map((cell) => ({ - ...cell, - color: shade(rampFor(cell.color), brightness), - })) - }) - -const smoothstep = (value: number) => value * value * (3 - 2 * value) -const frameDone = Promise.resolve() - -function UpdateFooter(props: { - from?: string - active: () => number - outcome: () => "running" | "success" | "failure" - failure: () => string - animating: () => boolean - renderer: CliRenderer - onOutcomeSettled: () => void -}) { - const term = useTerminalDimensions() - const [position, setPosition] = createSignal(0) - const [pulse, setPulse] = createSignal(0) - const headerFade = createFade() - const statusSweep = createSweep() - const runningHeader = () => - phrase( - ["OpenCode", colors.muted, true], - ["is updating", colors.muted], - ...(props.from - ? ([ - ["from", colors.muted], - [props.from, colors.accentDim], - ] as const) - : []), - ["to", colors.muted], - [InstallationVersion, colors.accent], - ) - const completedHeader = phrase( - ["OpenCode", colors.muted, true], - ["updated to", colors.muted], - [InstallationVersion, colors.accent], - ) - const pausedHeader = phrase(["OpenCode", colors.muted, true], ["update paused", colors.muted]) - const outcomeStatus = () => - props.outcome() === "success" - ? [...styled("✓", colors.success), ...styled(" Ready", colors.text)] - : [...styled("!", colors.error), ...styled(" " + props.failure(), colors.text)] - let previousStage: string = stages[0] - createEffect( - on(props.active, (index) => { - if (props.outcome() !== "running") return - const next = stages[index] - if (next === previousStage) return - statusSweep.start(styled(previousStage, colors.text), styled(next, colors.text)) - previousStage = next - }), - ) - createEffect( - on( - props.outcome, - (outcome) => { - if (outcome === "running") return - const visibleStatus = untrack(statusSweep.cells) ?? styled(previousStage, colors.text) - headerFade.start(runningHeader(), outcome === "success" ? completedHeader : pausedHeader) - statusSweep.start([...styled(" ", colors.text), ...visibleStatus], outcomeStatus(), props.onOutcomeSettled) - }, - { defer: true }, - ), - ) - const header = createMemo( - () => - headerFade.cells() ?? - (props.outcome() === "success" - ? completedHeader - : props.outcome() === "failure" - ? pausedHeader - : runningHeader()), - ) - const monogramInk = createMemo(() => - props.outcome() === "success" ? shade(monogramRamp, smoothstep(headerFade.progress())) : colors.muted, - ) - const rail = createMemo(() => { - const width = Math.max(0, Math.min(30, term().width - 39)) - if (width === 0) return [] - const filled = Math.round(position() * width) - const glowRadius = 6 - const span = Math.max(1, filled + glowRadius * 2) - const center = pulse() * span - glowRadius - const success = props.outcome() === "success" - const completion = smoothstep(headerFade.progress()) - return Array.from({ length: width }, (_, index) => { - const color = - index >= filled - ? colors.muted - : shade(railRamp, Math.max(0, 1 - Math.abs(index - center) / glowRadius) ** 2) - return { - char: success || index < filled ? "━" : "·", - color: success ? blend(color, colors.accent, completion) : color, - } - }) - }) - - onMount(() => { - let value = 0 - let velocity = 0 - let phase = 0 - const frame = (deltaTime: number) => { - if (!props.animating()) return frameDone - const elapsed = Math.min(0.032, deltaTime / 1_000) - const stiffness = 110 - const damping = 2 * Math.sqrt(stiffness) - const target = props.outcome() === "success" ? 1 : (props.active() + 1) / stages.length - velocity += (stiffness * (target - value) - damping * velocity) * elapsed - value += velocity * elapsed - phase = (phase + deltaTime / 900) % 1 - batch(() => { - setPosition(Math.max(0, Math.min(1, value))) - setPulse(phase) - }) - headerFade.tick(deltaTime) - statusSweep.tick(deltaTime) - return frameDone - } - props.renderer.setFrameCallback(frame) - onCleanup(() => props.renderer.removeFrameCallback(frame)) - }) - - return ( - - - - - } - > - - - - - - - - - {props.outcome() === "success" ? stages.length : props.active() + 1}/{stages.length} - - - - - ) -} - -function CellLine(props: { cells: ReadonlyArray }) { - return ( - - - {(cell) => ( - - {cell().char} - - )} - - - ) -} - -export * as UpdatePreflight from "./update-preflight" diff --git a/packages/cli/src/services/updater.test.ts b/packages/cli/src/services/updater.test.ts deleted file mode 100644 index e11de2a0d9..0000000000 --- a/packages/cli/src/services/updater.test.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { describe, expect, test } from "bun:test" -import { action, decodePolicy } from "./updater" - -describe("updater", () => { - test("reads autoupdate from JSONC", () => { - expect(decodePolicy('{ // preference\n "autoupdate": "notify",\n}')).toBe("notify") - expect(decodePolicy('{ "autoupdate": false }')).toBe(false) - expect(decodePolicy('{ "autoupdate": "invalid" }')).toBeUndefined() - }) - - test("automatically updates patches and minors", () => { - expect(action("1.2.3", "1.2.4", true)).toBe("upgrade") - expect(action("1.2.3", "1.3.0", true)).toBe("upgrade") - expect(action("1.2.3", "1.2.4", "notify")).toBe("upgrade") - expect(action("1.2.3", "1.3.0", "notify")).toBe("upgrade") - }) - - test("skips when autoupdate is disabled", () => { - expect(action("1.2.3", "1.2.4", false)).toBe("none") - }) - - test("never automatically updates majors", () => { - expect(action("1.2.3", "2.0.0", true)).toBe("none") - }) - - test("reports up-to-date only when versions match", () => { - expect(action("1.2.3", "1.2.3", true)).toBe("none") - }) - - test("upgrades when latest is lower (rollback)", () => { - expect(action("1.2.4", "1.2.3", true)).toBe("upgrade") - }) -}) diff --git a/packages/cli/src/services/updater.ts b/packages/cli/src/services/updater.ts deleted file mode 100644 index 5f3083b7c9..0000000000 --- a/packages/cli/src/services/updater.ts +++ /dev/null @@ -1,156 +0,0 @@ -import { Global } from "@opencode-ai/core/global" -import { Flag } from "@opencode-ai/core/flag/flag" -import { AppProcess } from "@opencode-ai/core/process" -import { - InstallationChannel, - InstallationLocal, - InstallationVersion, -} from "@opencode-ai/core/installation/version" -import { Context, Duration, Effect, FileSystem, Layer } from "effect" -import { ChildProcess } from "effect/unstable/process" -import { parse, type ParseError } from "jsonc-parser" -import path from "node:path" -import semver from "semver" - -export type Policy = boolean | "notify" -export type Action = "none" | "upgrade" -type Method = "npm" | "pnpm" | "bun" | "yarn" - -const packageName = "@opencode-ai/cli" - -export interface Interface { - readonly check: () => Effect.Effect -} - -export class Service extends Context.Service()("@opencode/cli/Updater") {} - -export function decodePolicy(text: string): Policy | undefined { - // The CLI only projects this host-level preference instead of initializing - // the location-scoped server configuration graph. - const errors: ParseError[] = [] - const input: unknown = parse(text, errors, { allowTrailingComma: true }) - if (errors.length || typeof input !== "object" || input === null || !("autoupdate" in input)) return - const value = input.autoupdate - if (typeof value === "boolean" || value === "notify") return value -} - -export function action(current: string, latest: string, policy: Policy): Action { - if (policy === false) return "none" - if (!semver.valid(current) || !semver.valid(latest) || semver.eq(latest, current)) return "none" - // Major upgrades are never installed automatically. - if (semver.major(latest) !== semver.major(current)) return "none" - return "upgrade" -} - -export const layer = Layer.effect( - Service, - Effect.gen(function* () { - const fs = yield* FileSystem.FileSystem - const global = yield* Global.Service - const appProcess = yield* AppProcess.Service - const channel = InstallationChannel.replace(/[^a-zA-Z0-9._-]/g, "-") - - const readPolicy = Effect.fnUntraced(function* () { - const values = yield* Effect.forEach(["config.json", "opencode.json", "opencode.jsonc"], (name) => - fs - .readFileString(path.join(global.config, name)) - .pipe(Effect.map(decodePolicy), Effect.catch(() => Effect.succeed(undefined))), - ) - return values.findLast((value) => value !== undefined) ?? true - }) - - const run = Effect.fnUntraced(function* (command: string[], timeout: Duration.Input = "10 seconds") { - return yield* appProcess - .run(ChildProcess.make(command[0], command.slice(1)), { - timeout, - maxOutputBytes: 100_000, - maxErrorBytes: 100_000, - }) - .pipe( - Effect.map((result) => ({ - code: result.exitCode, - stdout: result.stdout.toString("utf8"), - stderr: result.stderr.toString("utf8"), - })), - Effect.catch(() => Effect.succeed({ code: 1, stdout: "", stderr: "" })), - ) - }) - - const method = Effect.fnUntraced(function* () { - const checks: ReadonlyArray<{ method: Method; command: string[] }> = [ - { method: "npm", command: ["npm", "list", "-g", "--depth=0", packageName] }, - { method: "pnpm", command: ["pnpm", "list", "-g", "--depth=0", packageName] }, - { method: "bun", command: ["bun", "pm", "ls", "-g"] }, - { method: "yarn", command: ["yarn", "global", "list"] }, - ] - const results = yield* Effect.forEach( - checks, - (check) => run(check.command).pipe(Effect.map((result) => ({ check, result }))), - { concurrency: "unbounded" }, - ) - return results.find((result) => result.result.stdout.includes(packageName))?.check.method - }) - - const latest = Effect.fnUntraced(function* () { - const response = yield* Effect.tryPromise({ - try: () => - fetch( - `https://registry.npmjs.org/${encodeURIComponent(packageName)}/${encodeURIComponent(InstallationChannel)}`, - { headers: { "User-Agent": `opencode/${InstallationVersion}` }, signal: AbortSignal.timeout(10_000) }, - ), - catch: (cause) => new Error("Failed to check for updates", { cause }), - }) - if (!response.ok) return yield* Effect.fail(new Error(`Update check failed with status ${response.status}`)) - const data = yield* Effect.tryPromise({ - try: () => response.json(), - catch: (cause) => new Error("Failed to read update information", { cause }), - }) - if (typeof data !== "object" || data === null || !("version" in data) || typeof data.version !== "string") { - return yield* Effect.fail(new Error("Update information did not include a version")) - } - return data.version - }) - - const upgrade = Effect.fnUntraced(function* (method: Method, version: string) { - const target = `${packageName}@${version}` - const commands: Record = { - npm: ["npm", "install", "--global", target], - pnpm: ["pnpm", "install", "--global", target], - bun: ["bun", "install", "--global", target], - yarn: ["yarn", "global", "add", target], - } - const result = yield* run(commands[method], "5 minutes") - if (result.code === 0) return - return yield* Effect.fail(new Error(result.stderr.trim() || `Failed to update with ${method}`)) - }) - - const check = Effect.fn("cli.updater.check")(function* () { - if (InstallationLocal || Flag.OPENCODE_DISABLE_AUTOUPDATE) - return yield* Effect.logInfo("update check skipped", { - reason: InstallationLocal ? "local-install" : "disabled", - version: InstallationVersion, - channel: InstallationChannel, - }) - const policy = yield* readPolicy() - if (policy === false) return yield* Effect.logInfo("update check skipped", { reason: "policy-disabled" }) - - return yield* Effect.gen(function* () { - const version = yield* latest() - yield* Effect.logInfo("update check", { - current: InstallationVersion, - latest: version, - }) - const next = action(InstallationVersion, version, policy) - if (next === "none") return yield* Effect.logInfo("update check done", { action: "up-to-date" }) - const detected = yield* method() - if (!detected) return yield* Effect.logWarning("automatic update skipped: installation method not found") - yield* upgrade(detected, version) - yield* Effect.logInfo("updated OpenCode", { from: InstallationVersion, to: version, method: detected }) - }) - }, Effect.catchCause((cause) => Effect.logWarning("automatic update failed", { cause }))) - - return Service.of({ check }) - }), -) - -export * as Updater from "./updater" diff --git a/packages/cli/src/tui.ts b/packages/cli/src/tui.ts new file mode 100644 index 0000000000..5100e1c99a --- /dev/null +++ b/packages/cli/src/tui.ts @@ -0,0 +1,37 @@ +import { run } from "@opencode-ai/tui" +import { TuiConfig } from "@opencode-ai/tui/config" +import { Effect } from "effect" +import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" +import { Global } from "@opencode-ai/core/global" + +export function runTui(transport: { url: string; headers: RequestInit["headers"] }) { + const config = TuiConfig.resolve({}, { terminalSuspend: false }) + return run({ + ...transport, + args: {}, + config, + fetch: gracefulFetch, + pluginHost: { + async start() {}, + async dispose() {}, + }, + }).pipe(Effect.provide(AppNodeBuilder.build(Global.node))) +} + +const legacyDefaults: Record = { + "/config/providers": { providers: [], default: {} }, + "/provider": { all: [], default: {}, connected: [] }, + "/agent": [], + "/config": {}, +} + +const gracefulFetch = Object.assign( + async (input: RequestInfo | URL, init?: RequestInit) => { + const response = await fetch(input, init) + if (response.status !== 404) return response + const fallback = legacyDefaults[new URL(input instanceof Request ? input.url : input).pathname] + if (fallback === undefined) return response + return Response.json(fallback) + }, + { preconnect: fetch.preconnect }, +) diff --git a/packages/cli/src/ui/timeline.tsx b/packages/cli/src/ui/timeline.tsx deleted file mode 100644 index 8c7e6612c6..0000000000 --- a/packages/cli/src/ui/timeline.tsx +++ /dev/null @@ -1,242 +0,0 @@ -/** @jsxImportSource @opentui/solid */ -import { createCliRenderer, RGBA, type CliRenderer, type ColorInput, type ScrollbackWriter } from "@opentui/core" -import { createScrollbackWriter, render, useKeyboard } from "@opentui/solid" -import { registerOpencodeSpinner } from "@opencode-ai/tui/component/register-spinner" -import { Show, createSignal } from "solid-js" - -registerOpencodeSpinner() - -export type TimelineHost = { - readonly signal: AbortSignal - intro(text: string): Promise - item(text: string): Promise - pending(text: string): Promise - success(text: string): Promise - failure(text: string): Promise - outro(text: string): Promise - close(): Promise -} - -type RowKind = "intro" | "item" | "success" | "failure" | "outro" - -const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"] -const IDLE_TIMEOUT = 1_000 -const COLORS = { - accent: RGBA.fromIndex(6), - error: RGBA.fromIndex(1), - foreground: RGBA.defaultForeground(), - muted: RGBA.fromIndex(8), - success: RGBA.fromIndex(2), -} -const ROWS: Record = { - intro: { marker: "┌", color: COLORS.muted, connector: true }, - item: { marker: "●", color: COLORS.accent, connector: true }, - success: { marker: "◇", color: COLORS.success, connector: true }, - failure: { marker: "■", color: COLORS.error, connector: false }, - outro: { marker: "└", color: COLORS.muted, connector: false }, -} - -function row(kind: RowKind, value: string): ScrollbackWriter { - const style = ROWS[kind] - return createScrollbackWriter( - () => ( - - - - {style.marker} - - - {value} - - - - - - - ), - { startOnNewLine: true, trailingNewline: !style.connector }, - ) -} - -function TimelineFooter(props: { pending: () => string | undefined; cancel: () => void }) { - useKeyboard((event) => { - if (event.name !== "escape" && !(event.ctrl && event.name === "c")) return - event.preventDefault() - props.cancel() - }) - - return ( - - - {(text) => ( - <> - - - {text()} - - - )} - - - ) -} - -function bounded(task: Promise) { - return new Promise((resolve) => { - const timer = setTimeout(resolve, IDLE_TIMEOUT) - timer.unref() - const finish = () => { - clearTimeout(timer) - resolve() - } - void task.then(finish, finish) - }) -} - -async function shutdown(renderer: CliRenderer): Promise { - await bounded(renderer.idle()) - try { - renderer.externalOutputMode = "passthrough" - } finally { - try { - renderer.screenMode = "main-screen" - } finally { - if (!renderer.isDestroyed) renderer.destroy() - } - } -} - -export async function createTimelineHost(): Promise { - const stdout = process.stdout - const controller = new AbortController() - const signals: NodeJS.Signals[] = ["SIGINT", "SIGHUP", "SIGQUIT"] - const cancel = () => { - if (!controller.signal.aborted) controller.abort() - } - signals.forEach((signal) => process.on(signal, cancel)) - - if (!stdout.isTTY || !process.stdin.isTTY) { - let closed = false - let writing = false - let active: Promise | undefined - let closeTask: Promise | undefined - const write = async (kind: RowKind | "pending", text: string) => { - if (closed) throw new Error("timeline closed") - if (writing) throw new Error("timeline write already in progress") - writing = true - try { - const style = kind === "pending" ? undefined : ROWS[kind] - const marker = kind === "pending" ? "." : ROWS[kind].marker - const connector = style?.connector ? "│\n" : "" - active = new Promise((resolve, reject) => { - stdout.write(`${marker} ${text}\n${connector}`, (error) => (error ? reject(error) : resolve())) - }) - await active - } finally { - writing = false - active = undefined - } - } - const close = () => { - if (closeTask) return closeTask - closed = true - closeTask = (async () => { - await active?.catch(() => { }) - signals.forEach((signal) => process.off(signal, cancel)) - })() - return closeTask - } - return { - signal: controller.signal, - intro: (text) => write("intro", text), - item: (text) => write("item", text), - pending: (text) => write("pending", text), - success: (text) => write("success", text), - failure: (text) => write("failure", text), - outro: (text) => write("outro", text), - close, - } - } - - let renderer: CliRenderer | undefined - - try { - // Start on a fresh row so delayed SSH cursor reports cannot make - // split-footer overwrite the shell command. - process.stdout.write("\n") - renderer = await createCliRenderer({ - stdin: process.stdin, - useMouse: false, - autoFocus: false, - openConsoleOnError: false, - exitOnCtrlC: false, - exitSignals: [], - screenMode: "split-footer", - footerHeight: 1, - externalOutputMode: "capture-stdout", - consoleMode: "disabled", - clearOnShutdown: false, - }) - const activeRenderer = renderer - const [pending, setPending] = createSignal() - const renderTask = render(() => , activeRenderer) - void renderTask.catch(cancel) - await bounded(activeRenderer.idle()) - - let closed = false - let writing = false - let active: Promise | undefined - let closeTask: Promise | undefined - const write = (kind: RowKind | "pending", text: string) => { - if (closed) return Promise.reject(new Error("timeline closed")) - if (writing) return Promise.reject(new Error("timeline write already in progress")) - writing = true - active = (async () => { - if (kind === "pending") { - setPending(text) - activeRenderer.requestRender() - } else { - if (kind === "success" || kind === "failure" || kind === "outro") setPending(undefined) - activeRenderer.writeToScrollback(row(kind, text)) - activeRenderer.requestRender() - } - await bounded(activeRenderer.idle()) - })().finally(() => { - writing = false - active = undefined - }) - return active - } - const close = () => { - if (closeTask) return closeTask - closed = true - closeTask = (async () => { - await active?.catch(() => { }) - try { - await shutdown(activeRenderer) - await bounded(renderTask) - } finally { - signals.forEach((signal) => process.off(signal, cancel)) - } - })() - return closeTask - } - return { - signal: controller.signal, - intro: (text) => write("intro", text), - item: (text) => write("item", text), - pending: (text) => write("pending", text), - success: (text) => write("success", text), - failure: (text) => write("failure", text), - outro: (text) => write("outro", text), - close, - } - } catch (error) { - try { - if (renderer) await shutdown(renderer) - } finally { - signals.forEach((signal) => process.off(signal, cancel)) - } - throw error - } -} diff --git a/packages/cli/test/config.test.ts b/packages/cli/test/config.test.ts deleted file mode 100644 index 3c3dd45721..0000000000 --- a/packages/cli/test/config.test.ts +++ /dev/null @@ -1,125 +0,0 @@ -import { NodeFileSystem } from "@effect/platform-node" -import { Global } from "@opencode-ai/core/global" -import { Effect } from "effect" -import { expect, test } from "bun:test" -import path from "path" -import { Config } from "../src/config" - -function run(directory: string, effect: Effect.Effect) { - return Effect.runPromise( - effect.pipe( - Effect.provide(Config.layer), - Effect.provide(Global.layerWith({ config: directory, state: directory })), - Effect.provide(NodeFileSystem.layer), - ), - ) -} - -test("migrates tui and kv config into cli.json", async () => { - const directory = await Bun.$`mktemp -d`.text().then((value) => value.trim()) - await Bun.write( - path.join(directory, "tui.json"), - JSON.stringify({ - theme: "legacy", - keybinds: { leader: "ctrl+o" }, - plugin: [["example", { mode: "safe" }]], - plugin_enabled: { disabled: false }, - leader_timeout: 500, - scroll_speed: 2, - scroll_acceleration: { enabled: true }, - diff_style: "stacked", - mouse: false, - }), - ) - await Bun.write( - path.join(directory, "kv.json"), - JSON.stringify({ - theme_mode_lock: "light", - paste_summary_enabled: false, - exploration_grouping: false, - tips_hidden: true, - }), - ) - - try { - const config = await run( - directory, - Effect.gen(function* () { - const service = yield* Config.Service - return yield* service.get() - }), - ) - - expect(config).toMatchObject({ - theme: { name: "legacy", mode: "light" }, - keybinds: { leader: "ctrl+o" }, - plugins: [{ package: "example", options: { mode: "safe" } }, "-disabled"], - leader: { timeout: 500 }, - scroll: { speed: 2, acceleration: true }, - diffs: { view: "unified" }, - prompt: { paste: "full" }, - session: { grouping: "none" }, - hints: { tips: false }, - mouse: false, - }) - expect((await Bun.file(path.join(directory, "cli.json")).json()).keybinds).toEqual({ leader: "ctrl+o" }) - expect(await Bun.file(path.join(directory, "cli.json")).exists()).toBe(true) - expect(await Bun.file(path.join(directory, "tui.json")).exists()).toBe(true) - expect(await Bun.file(path.join(directory, "kv.json")).exists()).toBe(true) - } finally { - await Bun.$`rm -rf ${directory}` - } -}) - -test("migrates before the first update and does not remigrate afterward", async () => { - const directory = await Bun.$`mktemp -d`.text().then((value) => value.trim()) - await Bun.write(path.join(directory, "tui.json"), JSON.stringify({ theme: "legacy" })) - - try { - const config = await run( - directory, - Effect.gen(function* () { - const service = yield* Config.Service - yield* service.update((draft) => { - draft.animations = false - draft.mouse = false - }) - yield* Effect.promise(() => - Bun.write(path.join(directory, "tui.json"), JSON.stringify({ theme: "changed" })), - ) - return yield* service.get() - }), - ) - - expect(config).toEqual({ theme: { name: "legacy" }, animations: false, mouse: false }) - expect(await Bun.file(path.join(directory, "cli.json")).json()).toEqual({ - theme: { name: "legacy" }, - animations: false, - mouse: false, - }) - } finally { - await Bun.$`rm -rf ${directory}` - } -}) - -test("updates a config draft while preserving JSONC comments", async () => { - const directory = await Bun.$`mktemp -d`.text().then((value) => value.trim()) - await Bun.write(path.join(directory, "cli.json"), "{\n // Keep this comment\n \"animations\": true\n}\n") - - try { - const config = await run( - directory, - Effect.gen(function* () { - const service = yield* Config.Service - return yield* service.update((draft) => { - draft.prompt = { paste: "compact" } - }) - }), - ) - - expect(config).toEqual({ animations: true, prompt: { paste: "compact" } }) - expect(await Bun.file(path.join(directory, "cli.json")).text()).toContain("// Keep this comment") - } finally { - await Bun.$`rm -rf ${directory}` - } -}) diff --git a/packages/cli/test/fixture/standalone-owner.ts b/packages/cli/test/fixture/standalone-owner.ts deleted file mode 100644 index 7f92ac0636..0000000000 --- a/packages/cli/test/fixture/standalone-owner.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { Effect } from "effect" -import { Service } from "@opencode-ai/client/effect" -import path from "node:path" -import { Standalone } from "../../src/services/standalone" - -process.argv[1] = path.join(import.meta.dir, "../../src/index.ts") - -await Effect.runPromise( - Effect.scoped( - Effect.gen(function* () { - const endpoint = yield* Standalone.start() - const response = yield* Effect.promise(() => - fetch(new URL("/api/health", endpoint.url), { headers: Service.headers(endpoint) }), - ) - console.log(`${endpoint.pid} ${endpoint.url} ${response.status}`) - return yield* Effect.never - }), - ), -) diff --git a/packages/cli/test/footer-keymap.test.tsx b/packages/cli/test/footer-keymap.test.tsx deleted file mode 100644 index 1aa0e3270e..0000000000 --- a/packages/cli/test/footer-keymap.test.tsx +++ /dev/null @@ -1,106 +0,0 @@ -/** @jsxImportSource @opentui/solid */ -import { createDefaultOpenTuiKeymap } from "@opentui/keymap/opentui" -import { testRender, useRenderer } from "@opentui/solid" -import { OpencodeKeymapProvider, registerOpencodeKeymap } from "@opencode-ai/tui/keymap" -import { resolve } from "@opencode-ai/tui/config/v1" -import { expect, test } from "bun:test" -import { createComponent, createSignal } from "solid-js" -import { RunFooterView } from "../src/mini/footer.view" -import { RUN_THEME_FALLBACK } from "../src/mini/theme" -import type { FooterState, FooterSubagentState, FooterView } from "../src/mini/types" - -test("down opens subagents from an empty prompt", async () => { - const [state] = createSignal({ - phase: "idle", - status: "", - queue: 0, - model: "gpt-5", - duration: "", - usage: "", - first: false, - interrupt: 0, - exit: 0, - }) - const [view] = createSignal({ type: "prompt" }) - const [subagents] = createSignal({ - tabs: [ - { - sessionID: "subagent-1", - partID: "part-1", - callID: "call-1", - label: "Explore", - description: "Inspect the keymap", - status: "running", - lastUpdatedAt: 1, - }, - ], - details: {}, - permissions: [], - questions: [], - }) - const config = resolve( - { keybinds: { editor_open: "none", session_queued_prompts: "none" } }, - { terminalSuspend: true }, - ) - let offKeymap: (() => void) | undefined - - function Harness() { - const renderer = useRenderer() - const keymap = createDefaultOpenTuiKeymap(renderer) - offKeymap = registerOpencodeKeymap(keymap, renderer, config) - - return createComponent(OpencodeKeymapProvider, { - keymap, - get children() { - return ( - []} - agents={() => []} - references={() => []} - commands={() => []} - providers={() => undefined} - currentModel={() => undefined} - variants={() => []} - currentVariant={() => undefined} - state={state} - view={view} - subagent={subagents} - theme={() => RUN_THEME_FALLBACK} - tuiConfig={config} - agent="opencode" - onSubmit={() => true} - onPermissionReply={() => {}} - onQuestionReply={() => {}} - onQuestionReject={() => {}} - onCycle={() => {}} - onInterrupt={() => false} - onEditorOpen={async () => undefined} - onInputClear={() => {}} - onExit={() => {}} - onModelSelect={() => {}} - onVariantSelect={() => {}} - onRows={() => {}} - onLayout={() => {}} - onStatus={() => {}} - onQueuedRemove={async () => true} - /> - ) - }, - }) - } - - const app = await testRender(() => , { width: 100, height: 8, kittyKeyboard: true }) - try { - await app.renderOnce() - expect(app.renderer.currentFocusedEditor?.plainText).toBe("") - app.mockInput.pressArrow("down") - await app.renderOnce() - expect(app.captureCharFrame()).toContain("Select subagent") - } finally { - app.renderer.currentFocusedRenderable?.blur() - app.renderer.currentFocusedEditor?.blur() - offKeymap?.() - app.renderer.destroy() - } -}) diff --git a/packages/cli/test/mini.test.ts b/packages/cli/test/mini.test.ts deleted file mode 100644 index 17ff0177c7..0000000000 --- a/packages/cli/test/mini.test.ts +++ /dev/null @@ -1,150 +0,0 @@ -import { describe, expect, test } from "bun:test" -import { InstallationVersion } from "@opencode-ai/core/installation/version" -import path from "node:path" -import { mergeInteractiveInput, mergeNonInteractiveInput, parseRunModel, pickRunModel } from "../src/mini" - -async function cli(args: string[]) { - const child = Bun.spawn([process.execPath, "run", "src/index.ts", ...args], { - cwd: path.join(import.meta.dir, ".."), - stdout: "pipe", - stderr: "pipe", - }) - const [stdout, stderr, exitCode] = await Promise.all([ - new Response(child.stdout).text(), - new Response(child.stderr).text(), - child.exited, - ]) - return { stdout, stderr, exitCode } -} - -describe("mini command", () => { - test("uses piped stdin as the initial prompt", () => { - expect(mergeInteractiveInput("from stdin", undefined)).toBe("from stdin") - expect(mergeInteractiveInput("from stdin", "from flag")).toBe("from stdin\nfrom flag") - }) - - test("keeps run as mini's non-interactive input mode", () => { - expect(mergeNonInteractiveInput("from args", "from stdin")).toBe("from args\nfrom stdin") - expect(mergeNonInteractiveInput(undefined, "from stdin")).toBe("from stdin") - }) - - test("applies a variant to a resumed session's model", () => { - expect( - pickRunModel( - undefined, - "high", - { providerID: "session-provider", modelID: "session-model" }, - { providerID: "default-provider", modelID: "default-model" }, - ), - ).toEqual({ providerID: "session-provider", modelID: "session-model" }) - }) - - test("parses model variants from the model reference", () => { - expect(JSON.stringify(parseRunModel("openrouter/openai/gpt-5#high"))).toBe( - JSON.stringify({ model: { providerID: "openrouter", modelID: "openai/gpt-5" }, variant: "high" }), - ) - }) - - test("is registered in the preview CLI", async () => { - const result = await cli(["--help"]) - - expect(result.exitCode).toBe(0) - expect(result.stdout).toContain("mini Start the minimal interactive interface") - expect(result.stdout).toContain("run Run OpenCode with a message") - }) - - test("exposes run without legacy attach or command modes", async () => { - const result = await cli(["run", "--help"]) - - expect(result.exitCode).toBe(0) - expect(result.stdout).toContain("--server string") - expect(result.stdout).not.toContain("--variant") - expect(result.stdout).not.toContain("--attach") - expect(result.stdout).not.toContain("--command") - }) - - test("keeps option-like prompt text after the argument separator", async () => { - const result = await cli(["run", "--server", "http://127.0.0.1:1", "--", "--foo"]) - - expect(result.exitCode).toBe(1) - expect(result.stderr).not.toContain("You must provide a message") - }) - - test("preserves a run failure exit code", async () => { - let modelRequests = 0 - const server = Bun.serve({ - port: 0, - fetch(request) { - const url = new URL(request.url) - if (url.pathname === "/api/health") - return Response.json({ healthy: true, version: InstallationVersion, pid: process.pid }) - if (url.pathname === "/api/location") - return Response.json({ directory: process.cwd(), project: { id: "global", directory: process.cwd() } }) - if (url.pathname === "/api/model") { - modelRequests++ - return Response.json({ - location: { directory: process.cwd(), project: { id: "global", directory: process.cwd() } }, - data: modelRequests === 1 ? [{ id: "missing", providerID: "definitely" }] : [], - }) - } - return new Response(undefined, { status: 404 }) - }, - }) - - try { - const result = await cli([ - "run", - "--server", - server.url.toString(), - "--model", - "definitely/missing", - "hi", - ]) - - expect(result.exitCode).toBe(1) - expect(result.stderr).toContain("Model unavailable: definitely/missing") - } finally { - server.stop(true) - } - }) - - test("reports pre-admission errors as JSON", async () => { - const server = Bun.serve({ - port: 0, - fetch(request) { - if (new URL(request.url).pathname === "/api/session") return new Response("boom", { status: 500 }) - return Response.json({ healthy: true, version: "incompatible", pid: process.pid }) - }, - }) - - try { - const result = await cli(["run", "--format", "json", "--server", server.url.toString(), "hi"]) - - expect(result.exitCode).toBe(1) - expect(JSON.parse(result.stdout)).toMatchObject({ - type: "error", - sessionID: "", - error: { type: "unknown", message: "UnexpectedStatus" }, - }) - } finally { - server.stop(true) - } - }) - - test("uses the shared V2 server option instead of an attach command", async () => { - const result = await cli(["mini", "--help"]) - - expect(result.exitCode).toBe(0) - expect(result.stdout).toContain("--server string") - expect(result.stdout).not.toContain("SUBCOMMANDS") - }) - - test("routes local and explicit-server invocations into mini", async () => { - for (const args of [["mini"], ["mini", "--server", "http://127.0.0.1:1"]]) { - const result = await cli(args) - - expect(result.exitCode).toBe(1) - expect(result.stderr).toContain("opencode mini requires a TTY stdout") - } - }) -}) diff --git a/packages/cli/test/service.test.ts b/packages/cli/test/service.test.ts deleted file mode 100644 index a793ce0257..0000000000 --- a/packages/cli/test/service.test.ts +++ /dev/null @@ -1,154 +0,0 @@ -import { NodeFileSystem } from "@effect/platform-node" -import { Service } from "@opencode-ai/client/effect" -import { Database } from "@opencode-ai/core/database/database" -import { EventV2 } from "@opencode-ai/core/event" -import { EventTable } from "@opencode-ai/core/event/sql" -import { Global } from "@opencode-ai/core/global" -import { Project } from "@opencode-ai/core/project" -import { ProjectTable } from "@opencode-ai/core/project/sql" -import { AbsolutePath } from "@opencode-ai/core/schema" -import { SessionV2 } from "@opencode-ai/core/session" -import { SessionEvent } from "@opencode-ai/core/session/event" -import { SessionTable } from "@opencode-ai/core/session/sql" -import { expect, test } from "bun:test" -import { Effect, Schedule, Schema } from "effect" -import fs from "node:fs/promises" -import os from "node:os" -import path from "node:path" -import { ServiceConfig } from "../src/services/service-config" - -test("local channel stores service config with the local service filename", async () => { - const root = await fs.mkdtemp(path.join(os.tmpdir(), "opencode-service-")) - try { - await Effect.runPromise( - ServiceConfig.set("hostname", "127.0.0.2").pipe( - Effect.provide(Global.layerWith({ config: path.join(root, "config"), state: path.join(root, "state") })), - Effect.provide(NodeFileSystem.layer), - ), - ) - expect(await Bun.file(path.join(root, "config", "service-local.json")).json()).toEqual({ - hostname: "127.0.0.2", - }) - expect(await Bun.file(path.join(root, "config", "service.json")).exists()).toBe(false) - } finally { - await fs.rm(root, { recursive: true, force: true }) - } -}) - -test("concurrent service processes elect one server", async () => { - const root = await fs.mkdtemp(path.join(os.tmpdir(), "opencode-service-election-")) - const database = path.join(root, "opencode.db") - const env = { - ...process.env, - HOME: root, - OPENCODE_DB: database, - OPENCODE_TEST_HOME: root, - XDG_CACHE_HOME: path.join(root, "cache"), - XDG_CONFIG_HOME: path.join(root, "config"), - XDG_DATA_HOME: path.join(root, "data"), - XDG_STATE_HOME: path.join(root, "state"), - } - const sessionID = SessionV2.ID.make("ses_service_recovery") - await withDatabase( - database, - Effect.gen(function* () { - const { db } = yield* Database.Service - yield* db - .insert(ProjectTable) - .values({ id: Project.ID.global, worktree: AbsolutePath.make(root), sandboxes: [] }) - .run() - .pipe(Effect.orDie) - yield* db - .insert(SessionTable) - .values({ - id: sessionID, - project_id: Project.ID.global, - slug: "recovery", - directory: root, - title: "recovery", - version: "test", - time_suspended: Date.now(), - }) - .run() - .pipe(Effect.orDie) - }), - ) - const command = [process.execPath, path.join(import.meta.dir, "../src/index.ts"), "serve", "--service"] - const first = Bun.spawn(command, { env, stderr: "pipe", stdout: "ignore" }) - const second = Bun.spawn(command, { env, stderr: "pipe", stdout: "ignore" }) - - try { - const registration = path.join(root, "state", "opencode", "service-local.json") - const info = await waitForInfo(registration) - const winner = info.pid === first.pid ? first : second - const loser = info.pid === first.pid ? second : first - const exited = await Promise.race([loser.exited.then(() => true), Bun.sleep(10_000).then(() => false)]) - - expect(exited).toBe(true) - expect(winner.exitCode).toBe(null) - expect( - await withDatabase( - database, - Effect.gen(function* () { - const { db } = yield* Database.Service - return yield* db - .select({ timeSuspended: SessionTable.time_suspended }) - .from(SessionTable) - .get() - .pipe(Effect.orDie) - }), - ), - ).toEqual({ timeSuspended: null }) - expect(await waitForExecutionStart(database, sessionID)).toBe(1) - } finally { - first.kill("SIGTERM") - second.kill("SIGTERM") - await Promise.all([first.exited, second.exited]) - await fs.rm(root, { recursive: true, force: true }) - } -}) - -function withDatabase(file: string, effect: Effect.Effect) { - return Effect.runPromise(effect.pipe(Effect.provide(Database.layerFromPath(file)), Effect.scoped)) -} - -function waitForExecutionStart(file: string, sessionID: SessionV2.ID) { - return withDatabase( - file, - Effect.gen(function* () { - const { db } = yield* Database.Service - return yield* db - .select({ id: EventTable.id, sessionID: EventTable.aggregate_id, type: EventTable.type }) - .from(EventTable) - .all() - .pipe( - Effect.orDie, - Effect.map((rows) => - rows.filter( - (row) => - row.sessionID === sessionID && - row.type === - EventV2.versionedType( - SessionEvent.Execution.Started.type, - SessionEvent.Execution.Started.durable.version, - ), - ), - ), - Effect.filterOrFail((rows) => rows.length > 0), - Effect.map((rows) => rows.length), - Effect.retry(Schedule.spaced("50 millis").pipe(Schedule.both(Schedule.recurs(200)))), - ) - }), - ) -} - -async function waitForInfo(file: string) { - for (let attempt = 0; attempt < 200; attempt++) { - const value = await Bun.file(file) - .json() - .catch(() => undefined) - if (value !== undefined) return Schema.decodeUnknownPromise(Service.Info)(value) - await Bun.sleep(50) - } - throw new Error("Timed out waiting for service registration") -} diff --git a/packages/cli/test/standalone.test.ts b/packages/cli/test/standalone.test.ts deleted file mode 100644 index 3cfe25855f..0000000000 --- a/packages/cli/test/standalone.test.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { expect, test } from "bun:test" -import path from "node:path" - -test("standalone server exits when its owner is killed", async () => { - const owner = Bun.spawn([process.execPath, path.join(import.meta.dir, "fixture/standalone-owner.ts")], { - cwd: path.join(import.meta.dir, ".."), - env: { ...process.env, OPENCODE_SERVER_USERNAME: "custom" }, - stdin: "ignore", - stdout: "pipe", - stderr: "pipe", - }) - const line = await Promise.race([readLine(owner.stdout), Bun.sleep(10_000).then(() => undefined)]) - const [rawPID, url, status] = line?.split(" ") ?? [] - const pid = Number(rawPID) - - try { - expect(pid).toBeGreaterThan(0) - expect(url).toStartWith("http://127.0.0.1:") - expect(status).toBe("200") - expect(running(pid)).toBe(true) - - owner.kill("SIGKILL") - await owner.exited - - expect(await waitForExit(pid)).toBe(true) - } finally { - owner.kill("SIGKILL") - if (running(pid)) process.kill(pid, "SIGKILL") - } -}) - -async function readLine(stream: ReadableStream) { - const reader = stream.getReader() - const decoder = new TextDecoder() - const chunks: string[] = [] - while (true) { - const result = await reader.read() - if (result.done) break - chunks.push(decoder.decode(result.value, { stream: true })) - const output = chunks.join("") - const newline = output.indexOf("\n") - if (newline !== -1) { - reader.releaseLock() - return output.slice(0, newline) - } - } - reader.releaseLock() - return chunks.join("") + decoder.decode() -} - -async function waitForExit(pid: number, attempts = 100): Promise { - if (!running(pid)) return true - if (attempts === 0) return false - await Bun.sleep(50) - return waitForExit(pid, attempts - 1) -} - -function running(pid: number) { - if (!Number.isSafeInteger(pid) || pid <= 0) return false - try { - process.kill(pid, 0) - return true - } catch { - return false - } -} diff --git a/packages/client/package.json b/packages/client/package.json index 531e28caaa..4f2445ca9d 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -1,31 +1,16 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/client", - "version": "1.17.13", + "private": true, "type": "module", "license": "MIT", - "repository": { - "type": "git", - "url": "git+https://github.com/anomalyco/opencode.git", - "directory": "packages/client" - }, - "publishConfig": { - "access": "public" - }, - "files": [ - "dist" - ], "exports": { - ".": "./src/promise/index.ts", - "./promise": "./src/promise/index.ts", - "./promise/api": "./src/promise/api.ts", - "./effect": "./src/effect/index.ts", - "./effect/api": "./src/effect/api.ts" + ".": "./src/index.ts", + "./effect": "./src/effect.ts" }, "scripts": { - "build": "bun run script/build-package.ts", "generate": "bun run script/build.ts", - "check:generated": "bun run generate && git diff --exit-code -- src/promise/generated src/effect/generated src/effect/api", + "check:generated": "bun run generate && git diff --exit-code -- src/generated src/generated-effect", "test": "bun test --timeout 5000", "typecheck": "tsgo --noEmit" }, @@ -43,7 +28,9 @@ }, "devDependencies": { "@effect/platform-node": "catalog:", + "@opencode-ai/core": "workspace:*", "@opencode-ai/httpapi-codegen": "workspace:*", + "@opencode-ai/server": "workspace:*", "@tsconfig/bun": "catalog:", "@types/bun": "catalog:", "@typescript/native-preview": "catalog:", diff --git a/packages/client/script/build-package.ts b/packages/client/script/build-package.ts deleted file mode 100644 index 323a63ddf9..0000000000 --- a/packages/client/script/build-package.ts +++ /dev/null @@ -1,9 +0,0 @@ -#!/usr/bin/env bun - -import { $ } from "bun" -import { fileURLToPath } from "node:url" - -process.chdir(fileURLToPath(new URL("..", import.meta.url))) - -await $`rm -rf dist` -await $`bun tsc -p tsconfig.build.json` diff --git a/packages/client/script/build.ts b/packages/client/script/build.ts index 8e432b0a17..aeec4b3e34 100644 --- a/packages/client/script/build.ts +++ b/packages/client/script/build.ts @@ -1,35 +1,30 @@ import { NodeFileSystem } from "@effect/platform-node" -import { compile, emitEffectImported, emitEffectShape, emitPromise, write } from "@opencode-ai/httpapi-codegen" -import { - ClientApi, - effectOmitEndpoints, - groupNames, - promiseOmitEndpoints, -} from "@opencode-ai/protocol/client" +import { compile, emitEffectImported, emitPromise, write } from "@opencode-ai/httpapi-codegen" +import { ClientApi, endpointNames, groupNames, omitEndpoints } from "../src/contract" import { Effect } from "effect" import { fileURLToPath } from "url" -const promiseContract = compile(ClientApi, { groupNames, omitEndpoints: promiseOmitEndpoints }) -const effectContract = compile(ClientApi, { groupNames, omitEndpoints: effectOmitEndpoints }) +const contract = compile(ClientApi, { groupNames, endpointNames, omitEndpoints }) await Effect.runPromise( Effect.all( [ write( - emitPromise(promiseContract, { - mutableOutputs: true, + emitPromise(contract, { + outputTypes: { + "events.subscribe": { + name: "OpenCodeEventEncoded", + import: 'import type { OpenCodeEventEncoded } from "@opencode-ai/protocol/groups/event"', + }, + }, }), - fileURLToPath(new URL("../src/promise/generated", import.meta.url)), + fileURLToPath(new URL("../src/generated", import.meta.url)), ), write( - emitEffectImported(effectContract, { module: "../../contract", api: "ClientApi" }), - fileURLToPath(new URL("../src/effect/generated", import.meta.url)), - ), - write( - emitEffectShape(effectContract, { module: "../../contract", api: "ClientApi" }), - fileURLToPath(new URL("../src/effect/api", import.meta.url)), + emitEffectImported(contract, { module: "../contract", api: "ClientApi" }), + fileURLToPath(new URL("../src/generated-effect", import.meta.url)), ), ], - { concurrency: 3, discard: true }, + { concurrency: 2, discard: true }, ).pipe(Effect.provide(NodeFileSystem.layer)), ) diff --git a/packages/client/script/publish.ts b/packages/client/script/publish.ts deleted file mode 100644 index 8e37674b64..0000000000 --- a/packages/client/script/publish.ts +++ /dev/null @@ -1,45 +0,0 @@ -#!/usr/bin/env bun - -import { Script } from "@opencode-ai/script" -import { $ } from "bun" -import { rm } from "node:fs/promises" -import { fileURLToPath } from "node:url" - -process.chdir(fileURLToPath(new URL("..", import.meta.url))) - -const originalText = await Bun.file("package.json").text() -const pkg = JSON.parse(originalText) as { - name: string - version: string - exports: Record -} -const tarball = `${pkg.name.replace("@", "").replace("/", "-")}-${pkg.version}.tgz` - -if ((await $`npm view ${pkg.name}@${pkg.version} version`.nothrow()).exitCode === 0) { - console.log(`already published ${pkg.name}@${pkg.version}`) - process.exit(0) -} - -try { - await $`bun run typecheck` - await $`bun run build` - pkg.exports = Object.fromEntries( - Object.entries(pkg.exports).map(([key, value]) => { - if (typeof value !== "string") return [key, value] - return [ - key, - { - import: value.replace("./src/", "./dist/").replace(/\.ts$/, ".js"), - types: value.replace("./src/", "./dist/").replace(/\.ts$/, ".d.ts"), - }, - ] - }), - ) - await Bun.write("package.json", JSON.stringify(pkg, null, 2) + "\n") - await rm(tarball, { force: true }) - await $`bun pm pack` - await $`npm publish ${tarball} --tag ${Script.channel} --access public` -} finally { - await Bun.write("package.json", originalText) - await rm(tarball, { force: true }) -} diff --git a/packages/client/src/contract.ts b/packages/client/src/contract.ts index 8fd9994f5c..413fea9dc3 100644 --- a/packages/client/src/contract.ts +++ b/packages/client/src/contract.ts @@ -1,6 +1,53 @@ -export { - ClientApi, - effectOmitEndpoints, - groupNames, - promiseOmitEndpoints, -} from "@opencode-ai/protocol/client" +import { makeDefaultApi } from "@opencode-ai/protocol/api" +import { InvalidRequestError, SessionNotFoundError } from "@opencode-ai/protocol/errors" +import { HttpApiMiddleware } from "effect/unstable/httpapi" + +class LocationMiddleware extends HttpApiMiddleware.Service()( + "@opencode-ai/client/LocationMiddleware", +) {} + +class SessionLocationMiddleware extends HttpApiMiddleware.Service()( + "@opencode-ai/client/SessionLocationMiddleware", + { error: [InvalidRequestError, SessionNotFoundError] }, +) {} + +export const ClientApi = makeDefaultApi({ + locationMiddleware: LocationMiddleware, + sessionLocationMiddleware: SessionLocationMiddleware, +}) + +export const groupNames = { + "server.health": "health", + "server.location": "location", + "server.agent": "agents", + "server.session": "sessions", + "server.message": "messages", + "server.model": "models", + "server.provider": "providers", + "server.integration": "integrations", + "server.credential": "credentials", + "server.permission": "permissions", + "server.fs": "files", + "server.command": "commands", + "server.skill": "skills", + "server.event": "events", + "server.pty": "ptys", + "server.question": "questions", + "server.reference": "references", + "server.projectCopy": "projectCopies", +} as const + +export const endpointNames = { + "session.messages": "list", + "integration.connect.key": "connectKey", + "integration.connect.oauth": "connectOauth", + "integration.attempt.status": "attemptStatus", + "integration.attempt.complete": "attemptComplete", + "integration.attempt.cancel": "attemptCancel", + "permission.request.list": "listRequests", + "permission.saved.list": "listSaved", + "permission.saved.remove": "removeSaved", + "question.request.list": "listRequests", +} as const + +export const omitEndpoints = new Set(["fs.read", "pty.connect", "pty.connectToken"]) diff --git a/packages/client/src/effect/index.ts b/packages/client/src/effect.ts similarity index 68% rename from packages/client/src/effect/index.ts rename to packages/client/src/effect.ts index 67c2c42142..b580c7f48a 100644 --- a/packages/client/src/effect/index.ts +++ b/packages/client/src/effect.ts @@ -1,30 +1,10 @@ // TODO: Keep additional network capabilities inside Schema and Protocol as the client grows; /effect must never import // Core or Server. Preserve these datatype exports so internal model reorganizations do not require caller migrations. -import type { Effect } from "effect" - -export * from "./generated/index" -export type { - AgentApi, - AppApi, - CatalogApi, - CommandApi, - EventApi, - IntegrationApi, - ModelApi, - PluginApi, - ProviderApi, - ReferenceApi, - SessionApi, - SkillApi, -} from "./api.js" -export { Service } from "./service.js" +export * from "./generated-effect/index" export { Agent } from "@opencode-ai/schema/agent" export { Command } from "@opencode-ai/schema/command" export { Credential } from "@opencode-ai/schema/credential" -export { Event } from "@opencode-ai/schema/event" -export { EventLog } from "@opencode-ai/schema/event-log" export { FileSystem } from "@opencode-ai/schema/filesystem" -export { Form } from "@opencode-ai/schema/form" export { Integration } from "@opencode-ai/schema/integration" export { Location } from "@opencode-ai/schema/location" export { Model } from "@opencode-ai/schema/model" @@ -38,10 +18,8 @@ export { Question } from "@opencode-ai/schema/question" export { Reference } from "@opencode-ai/schema/reference" export { AbsolutePath, RelativePath } from "@opencode-ai/schema/schema" export { Session } from "@opencode-ai/schema/session" -export { SessionPending } from "@opencode-ai/schema/session-pending" +export { SessionInput } from "@opencode-ai/schema/session-input" export { SessionMessage } from "@opencode-ai/schema/session-message" export { Skill } from "@opencode-ai/schema/skill" export { Prompt } from "@opencode-ai/schema/prompt" -export { PromptInput } from "@opencode-ai/schema/prompt-input" export type { OpenCodeEvent } from "@opencode-ai/protocol/groups/event" -export type OpenCodeClient = Effect.Success> diff --git a/packages/client/src/effect/api.ts b/packages/client/src/effect/api.ts deleted file mode 100644 index e7cb2012f5..0000000000 --- a/packages/client/src/effect/api.ts +++ /dev/null @@ -1,8 +0,0 @@ -import type { ModelApi, ProviderApi } from "./api/api.js" - -export type * from "./api/api.js" - -export interface CatalogApi { - readonly provider: ProviderApi - readonly model: ModelApi -} diff --git a/packages/client/src/effect/api/.httpapi-codegen.json b/packages/client/src/effect/api/.httpapi-codegen.json deleted file mode 100644 index d1d5afdb85..0000000000 --- a/packages/client/src/effect/api/.httpapi-codegen.json +++ /dev/null @@ -1,3 +0,0 @@ -[ - "api.ts" -] diff --git a/packages/client/src/effect/api/api.ts b/packages/client/src/effect/api/api.ts deleted file mode 100644 index 28c06cbf04..0000000000 --- a/packages/client/src/effect/api/api.ts +++ /dev/null @@ -1,972 +0,0 @@ -// Generated by @opencode-ai/httpapi-codegen. Do not edit. -import type { Effect, Stream } from "effect" -import type { HttpApiClient } from "effect/unstable/httpapi" -import type { ClientApi } from "../../contract" - -type RawClient = HttpApiClient.ForApi -type EffectValue = A extends Effect.Effect ? Success : never -type StreamValue = A extends Stream.Stream ? Success : never - -export type Endpoint0_0Output = EffectValue> -export type HealthGetOperation = () => Effect.Effect - -export interface HealthApi { - readonly get: HealthGetOperation -} - -export type Endpoint1_0Output = EffectValue> -export type ServerGetOperation = () => Effect.Effect - -export interface ServerApi { - readonly get: ServerGetOperation -} - -type Endpoint2_0Request = Parameters[0] -export type Endpoint2_0Input = { readonly location?: Endpoint2_0Request["query"]["location"] } -export type Endpoint2_0Output = EffectValue> -export type LocationGetOperation = (input?: Endpoint2_0Input) => Effect.Effect - -export interface LocationApi { - readonly get: LocationGetOperation -} - -type Endpoint3_0Request = Parameters[0] -export type Endpoint3_0Input = { readonly location?: Endpoint3_0Request["query"]["location"] } -export type Endpoint3_0Output = EffectValue> -export type AgentListOperation = (input?: Endpoint3_0Input) => Effect.Effect - -export interface AgentApi { - readonly list: AgentListOperation -} - -type Endpoint4_0Request = Parameters[0] -export type Endpoint4_0Input = { readonly location?: Endpoint4_0Request["query"]["location"] } -export type Endpoint4_0Output = EffectValue> -export type PluginListOperation = (input?: Endpoint4_0Input) => Effect.Effect - -export interface PluginApi { - readonly list: PluginListOperation -} - -type Endpoint5_0Request = Parameters[0] -export type Endpoint5_0Input = { - readonly workspace?: Endpoint5_0Request["query"]["workspace"] - readonly limit?: Endpoint5_0Request["query"]["limit"] - readonly order?: Endpoint5_0Request["query"]["order"] - readonly search?: Endpoint5_0Request["query"]["search"] - readonly parentID?: Endpoint5_0Request["query"]["parentID"] - readonly directory?: Endpoint5_0Request["query"]["directory"] - readonly project?: Endpoint5_0Request["query"]["project"] - readonly subpath?: Endpoint5_0Request["query"]["subpath"] - readonly cursor?: Endpoint5_0Request["query"]["cursor"] -} -export type Endpoint5_0Output = EffectValue> -export type SessionListOperation = (input?: Endpoint5_0Input) => Effect.Effect - -type Endpoint5_1Request = Parameters[0] -export type Endpoint5_1Input = { - readonly id?: Endpoint5_1Request["payload"]["id"] - readonly agent?: Endpoint5_1Request["payload"]["agent"] - readonly model?: Endpoint5_1Request["payload"]["model"] - readonly location?: Endpoint5_1Request["payload"]["location"] -} -export type Endpoint5_1Output = EffectValue>["data"] -export type SessionCreateOperation = (input?: Endpoint5_1Input) => Effect.Effect - -export type Endpoint5_2Output = EffectValue>["data"] -export type SessionActiveOperation = () => Effect.Effect - -type Endpoint5_3Request = Parameters[0] -export type Endpoint5_3Input = { readonly sessionID: Endpoint5_3Request["params"]["sessionID"] } -export type Endpoint5_3Output = EffectValue>["data"] -export type SessionGetOperation = (input: Endpoint5_3Input) => Effect.Effect - -type Endpoint5_4Request = Parameters[0] -export type Endpoint5_4Input = { readonly sessionID: Endpoint5_4Request["params"]["sessionID"] } -export type Endpoint5_4Output = EffectValue> -export type SessionRemoveOperation = (input: Endpoint5_4Input) => Effect.Effect - -type Endpoint5_5Request = Parameters[0] -export type Endpoint5_5Input = { - readonly sessionID: Endpoint5_5Request["params"]["sessionID"] - readonly messageID?: Endpoint5_5Request["payload"]["messageID"] -} -export type Endpoint5_5Output = EffectValue>["data"] -export type SessionForkOperation = (input: Endpoint5_5Input) => Effect.Effect - -type Endpoint5_6Request = Parameters[0] -export type Endpoint5_6Input = { - readonly sessionID: Endpoint5_6Request["params"]["sessionID"] - readonly agent: Endpoint5_6Request["payload"]["agent"] -} -export type Endpoint5_6Output = EffectValue> -export type SessionSwitchAgentOperation = (input: Endpoint5_6Input) => Effect.Effect - -type Endpoint5_7Request = Parameters[0] -export type Endpoint5_7Input = { - readonly sessionID: Endpoint5_7Request["params"]["sessionID"] - readonly model: Endpoint5_7Request["payload"]["model"] -} -export type Endpoint5_7Output = EffectValue> -export type SessionSwitchModelOperation = (input: Endpoint5_7Input) => Effect.Effect - -type Endpoint5_8Request = Parameters[0] -export type Endpoint5_8Input = { - readonly sessionID: Endpoint5_8Request["params"]["sessionID"] - readonly title: Endpoint5_8Request["payload"]["title"] -} -export type Endpoint5_8Output = EffectValue> -export type SessionRenameOperation = (input: Endpoint5_8Input) => Effect.Effect - -type Endpoint5_9Request = Parameters[0] -export type Endpoint5_9Input = { - readonly sessionID: Endpoint5_9Request["params"]["sessionID"] - readonly destination: Endpoint5_9Request["payload"]["destination"] - readonly moveChanges?: Endpoint5_9Request["payload"]["moveChanges"] -} -export type Endpoint5_9Output = EffectValue> -export type SessionMoveOperation = (input: Endpoint5_9Input) => Effect.Effect - -type Endpoint5_10Request = Parameters[0] -export type Endpoint5_10Input = { - readonly sessionID: Endpoint5_10Request["params"]["sessionID"] - readonly id?: Endpoint5_10Request["payload"]["id"] - readonly text: Endpoint5_10Request["payload"]["text"] - readonly files?: Endpoint5_10Request["payload"]["files"] - readonly agents?: Endpoint5_10Request["payload"]["agents"] - readonly metadata?: Endpoint5_10Request["payload"]["metadata"] - readonly delivery?: Endpoint5_10Request["payload"]["delivery"] - readonly resume?: Endpoint5_10Request["payload"]["resume"] -} -export type Endpoint5_10Output = EffectValue>["data"] -export type SessionPromptOperation = (input: Endpoint5_10Input) => Effect.Effect - -type Endpoint5_11Request = Parameters[0] -export type Endpoint5_11Input = { - readonly sessionID: Endpoint5_11Request["params"]["sessionID"] - readonly id?: Endpoint5_11Request["payload"]["id"] - readonly command: Endpoint5_11Request["payload"]["command"] - readonly arguments?: Endpoint5_11Request["payload"]["arguments"] - readonly agent?: Endpoint5_11Request["payload"]["agent"] - readonly model?: Endpoint5_11Request["payload"]["model"] - readonly files?: Endpoint5_11Request["payload"]["files"] - readonly agents?: Endpoint5_11Request["payload"]["agents"] - readonly delivery?: Endpoint5_11Request["payload"]["delivery"] - readonly resume?: Endpoint5_11Request["payload"]["resume"] -} -export type Endpoint5_11Output = EffectValue>["data"] -export type SessionCommandOperation = (input: Endpoint5_11Input) => Effect.Effect - -type Endpoint5_12Request = Parameters[0] -export type Endpoint5_12Input = { - readonly sessionID: Endpoint5_12Request["params"]["sessionID"] - readonly id?: Endpoint5_12Request["payload"]["id"] - readonly skill: Endpoint5_12Request["payload"]["skill"] - readonly resume?: Endpoint5_12Request["payload"]["resume"] -} -export type Endpoint5_12Output = EffectValue> -export type SessionSkillOperation = (input: Endpoint5_12Input) => Effect.Effect - -type Endpoint5_13Request = Parameters[0] -export type Endpoint5_13Input = { - readonly sessionID: Endpoint5_13Request["params"]["sessionID"] - readonly id?: Endpoint5_13Request["payload"]["id"] - readonly text: Endpoint5_13Request["payload"]["text"] - readonly description?: Endpoint5_13Request["payload"]["description"] - readonly metadata?: Endpoint5_13Request["payload"]["metadata"] - readonly delivery?: Endpoint5_13Request["payload"]["delivery"] - readonly resume?: Endpoint5_13Request["payload"]["resume"] -} -export type Endpoint5_13Output = EffectValue>["data"] -export type SessionSyntheticOperation = (input: Endpoint5_13Input) => Effect.Effect - -type Endpoint5_14Request = Parameters[0] -export type Endpoint5_14Input = { - readonly sessionID: Endpoint5_14Request["params"]["sessionID"] - readonly id?: Endpoint5_14Request["payload"]["id"] - readonly command: Endpoint5_14Request["payload"]["command"] -} -export type Endpoint5_14Output = EffectValue> -export type SessionShellOperation = (input: Endpoint5_14Input) => Effect.Effect - -type Endpoint5_15Request = Parameters[0] -export type Endpoint5_15Input = { - readonly sessionID: Endpoint5_15Request["params"]["sessionID"] - readonly id?: Endpoint5_15Request["payload"]["id"] -} -export type Endpoint5_15Output = EffectValue>["data"] -export type SessionCompactOperation = (input: Endpoint5_15Input) => Effect.Effect - -type Endpoint5_16Request = Parameters[0] -export type Endpoint5_16Input = { readonly sessionID: Endpoint5_16Request["params"]["sessionID"] } -export type Endpoint5_16Output = EffectValue> -export type SessionWaitOperation = (input: Endpoint5_16Input) => Effect.Effect - -type Endpoint5_17Request = Parameters[0] -export type Endpoint5_17Input = { - readonly sessionID: Endpoint5_17Request["params"]["sessionID"] - readonly messageID: Endpoint5_17Request["payload"]["messageID"] - readonly files?: Endpoint5_17Request["payload"]["files"] -} -export type Endpoint5_17Output = EffectValue>["data"] -export type SessionRevertStageOperation = (input: Endpoint5_17Input) => Effect.Effect - -type Endpoint5_18Request = Parameters[0] -export type Endpoint5_18Input = { readonly sessionID: Endpoint5_18Request["params"]["sessionID"] } -export type Endpoint5_18Output = EffectValue> -export type SessionRevertClearOperation = (input: Endpoint5_18Input) => Effect.Effect - -type Endpoint5_19Request = Parameters[0] -export type Endpoint5_19Input = { readonly sessionID: Endpoint5_19Request["params"]["sessionID"] } -export type Endpoint5_19Output = EffectValue> -export type SessionRevertCommitOperation = (input: Endpoint5_19Input) => Effect.Effect - -type Endpoint5_20Request = Parameters[0] -export type Endpoint5_20Input = { readonly sessionID: Endpoint5_20Request["params"]["sessionID"] } -export type Endpoint5_20Output = EffectValue>["data"] -export type SessionContextOperation = (input: Endpoint5_20Input) => Effect.Effect - -type Endpoint5_21Request = Parameters[0] -export type Endpoint5_21Input = { readonly sessionID: Endpoint5_21Request["params"]["sessionID"] } -export type Endpoint5_21Output = EffectValue>["data"] -export type SessionPendingListOperation = (input: Endpoint5_21Input) => Effect.Effect - -type Endpoint5_22Request = Parameters[0] -export type Endpoint5_22Input = { readonly sessionID: Endpoint5_22Request["params"]["sessionID"] } -export type Endpoint5_22Output = EffectValue< - ReturnType ->["data"] -export type SessionInstructionsEntryListOperation = ( - input: Endpoint5_22Input, -) => Effect.Effect - -type Endpoint5_23Request = Parameters[0] -export type Endpoint5_23Input = { - readonly sessionID: Endpoint5_23Request["params"]["sessionID"] - readonly key: Endpoint5_23Request["params"]["key"] - readonly value: Endpoint5_23Request["payload"]["value"] -} -export type Endpoint5_23Output = EffectValue> -export type SessionInstructionsEntryPutOperation = ( - input: Endpoint5_23Input, -) => Effect.Effect - -type Endpoint5_24Request = Parameters[0] -export type Endpoint5_24Input = { - readonly sessionID: Endpoint5_24Request["params"]["sessionID"] - readonly key: Endpoint5_24Request["params"]["key"] -} -export type Endpoint5_24Output = EffectValue< - ReturnType -> -export type SessionInstructionsEntryRemoveOperation = ( - input: Endpoint5_24Input, -) => Effect.Effect - -type Endpoint5_25Request = Parameters[0] -export type Endpoint5_25Input = { - readonly sessionID: Endpoint5_25Request["params"]["sessionID"] - readonly after?: Endpoint5_25Request["query"]["after"] - readonly follow?: Endpoint5_25Request["query"]["follow"] -} -export type Endpoint5_25Output = StreamValue>> -export type SessionLogOperation = (input: Endpoint5_25Input) => Stream.Stream - -type Endpoint5_26Request = Parameters[0] -export type Endpoint5_26Input = { readonly sessionID: Endpoint5_26Request["params"]["sessionID"] } -export type Endpoint5_26Output = EffectValue> -export type SessionInterruptOperation = (input: Endpoint5_26Input) => Effect.Effect - -type Endpoint5_27Request = Parameters[0] -export type Endpoint5_27Input = { readonly sessionID: Endpoint5_27Request["params"]["sessionID"] } -export type Endpoint5_27Output = EffectValue> -export type SessionBackgroundOperation = (input: Endpoint5_27Input) => Effect.Effect - -type Endpoint5_28Request = Parameters[0] -export type Endpoint5_28Input = { - readonly sessionID: Endpoint5_28Request["params"]["sessionID"] - readonly messageID: Endpoint5_28Request["params"]["messageID"] -} -export type Endpoint5_28Output = EffectValue>["data"] -export type SessionMessageOperation = (input: Endpoint5_28Input) => Effect.Effect - -export interface SessionApi { - readonly list: SessionListOperation - readonly create: SessionCreateOperation - readonly active: SessionActiveOperation - readonly get: SessionGetOperation - readonly remove: SessionRemoveOperation - readonly fork: SessionForkOperation - readonly switchAgent: SessionSwitchAgentOperation - readonly switchModel: SessionSwitchModelOperation - readonly rename: SessionRenameOperation - readonly move: SessionMoveOperation - readonly prompt: SessionPromptOperation - readonly command: SessionCommandOperation - readonly skill: SessionSkillOperation - readonly synthetic: SessionSyntheticOperation - readonly shell: SessionShellOperation - readonly compact: SessionCompactOperation - readonly wait: SessionWaitOperation - readonly revert: { - readonly stage: SessionRevertStageOperation - readonly clear: SessionRevertClearOperation - readonly commit: SessionRevertCommitOperation - } - readonly context: SessionContextOperation - readonly pending: { readonly list: SessionPendingListOperation } - readonly instructions: { - readonly entry: { - readonly list: SessionInstructionsEntryListOperation - readonly put: SessionInstructionsEntryPutOperation - readonly remove: SessionInstructionsEntryRemoveOperation - } - } - readonly log: SessionLogOperation - readonly interrupt: SessionInterruptOperation - readonly background: SessionBackgroundOperation - readonly message: SessionMessageOperation -} - -type Endpoint6_0Request = Parameters[0] -export type Endpoint6_0Input = { - readonly sessionID: Endpoint6_0Request["params"]["sessionID"] - readonly limit?: Endpoint6_0Request["query"]["limit"] - readonly order?: Endpoint6_0Request["query"]["order"] - readonly cursor?: Endpoint6_0Request["query"]["cursor"] -} -export type Endpoint6_0Output = EffectValue> -export type MessageListOperation = (input: Endpoint6_0Input) => Effect.Effect - -export interface MessageApi { - readonly list: MessageListOperation -} - -type Endpoint7_0Request = Parameters[0] -export type Endpoint7_0Input = { readonly location?: Endpoint7_0Request["query"]["location"] } -export type Endpoint7_0Output = EffectValue> -export type ModelListOperation = (input?: Endpoint7_0Input) => Effect.Effect - -type Endpoint7_1Request = Parameters[0] -export type Endpoint7_1Input = { readonly location?: Endpoint7_1Request["query"]["location"] } -export type Endpoint7_1Output = EffectValue> -export type ModelDefaultOperation = (input?: Endpoint7_1Input) => Effect.Effect - -export interface ModelApi { - readonly list: ModelListOperation - readonly default: ModelDefaultOperation -} - -type Endpoint8_0Request = Parameters[0] -export type Endpoint8_0Input = { - readonly location?: Endpoint8_0Request["query"]["location"] - readonly prompt: Endpoint8_0Request["payload"]["prompt"] - readonly model?: Endpoint8_0Request["payload"]["model"] -} -export type Endpoint8_0Output = EffectValue>["data"] -export type GenerateTextOperation = (input: Endpoint8_0Input) => Effect.Effect - -export interface GenerateApi { - readonly text: GenerateTextOperation -} - -type Endpoint9_0Request = Parameters[0] -export type Endpoint9_0Input = { readonly location?: Endpoint9_0Request["query"]["location"] } -export type Endpoint9_0Output = EffectValue> -export type ProviderListOperation = (input?: Endpoint9_0Input) => Effect.Effect - -type Endpoint9_1Request = Parameters[0] -export type Endpoint9_1Input = { - readonly providerID: Endpoint9_1Request["params"]["providerID"] - readonly location?: Endpoint9_1Request["query"]["location"] -} -export type Endpoint9_1Output = EffectValue> -export type ProviderGetOperation = (input: Endpoint9_1Input) => Effect.Effect - -export interface ProviderApi { - readonly list: ProviderListOperation - readonly get: ProviderGetOperation -} - -type Endpoint10_0Request = Parameters[0] -export type Endpoint10_0Input = { readonly location?: Endpoint10_0Request["query"]["location"] } -export type Endpoint10_0Output = EffectValue> -export type IntegrationListOperation = (input?: Endpoint10_0Input) => Effect.Effect - -type Endpoint10_1Request = Parameters[0] -export type Endpoint10_1Input = { - readonly integrationID: Endpoint10_1Request["params"]["integrationID"] - readonly location?: Endpoint10_1Request["query"]["location"] -} -export type Endpoint10_1Output = EffectValue> -export type IntegrationGetOperation = (input: Endpoint10_1Input) => Effect.Effect - -type Endpoint10_2Request = Parameters[0] -export type Endpoint10_2Input = { - readonly integrationID: Endpoint10_2Request["params"]["integrationID"] - readonly location?: Endpoint10_2Request["query"]["location"] - readonly key: Endpoint10_2Request["payload"]["key"] - readonly label?: Endpoint10_2Request["payload"]["label"] -} -export type Endpoint10_2Output = EffectValue> -export type IntegrationConnectKeyOperation = ( - input: Endpoint10_2Input, -) => Effect.Effect - -type Endpoint10_3Request = Parameters[0] -export type Endpoint10_3Input = { - readonly integrationID: Endpoint10_3Request["params"]["integrationID"] - readonly location?: Endpoint10_3Request["query"]["location"] - readonly methodID: Endpoint10_3Request["payload"]["methodID"] - readonly inputs: Endpoint10_3Request["payload"]["inputs"] - readonly label?: Endpoint10_3Request["payload"]["label"] -} -export type Endpoint10_3Output = EffectValue> -export type IntegrationConnectOauthOperation = ( - input: Endpoint10_3Input, -) => Effect.Effect - -type Endpoint10_4Request = Parameters[0] -export type Endpoint10_4Input = { - readonly attemptID: Endpoint10_4Request["params"]["attemptID"] - readonly location?: Endpoint10_4Request["query"]["location"] -} -export type Endpoint10_4Output = EffectValue> -export type IntegrationAttemptStatusOperation = ( - input: Endpoint10_4Input, -) => Effect.Effect - -type Endpoint10_5Request = Parameters[0] -export type Endpoint10_5Input = { - readonly attemptID: Endpoint10_5Request["params"]["attemptID"] - readonly location?: Endpoint10_5Request["query"]["location"] - readonly code?: Endpoint10_5Request["payload"]["code"] -} -export type Endpoint10_5Output = EffectValue< - ReturnType -> -export type IntegrationAttemptCompleteOperation = ( - input: Endpoint10_5Input, -) => Effect.Effect - -type Endpoint10_6Request = Parameters[0] -export type Endpoint10_6Input = { - readonly attemptID: Endpoint10_6Request["params"]["attemptID"] - readonly location?: Endpoint10_6Request["query"]["location"] -} -export type Endpoint10_6Output = EffectValue> -export type IntegrationAttemptCancelOperation = ( - input: Endpoint10_6Input, -) => Effect.Effect - -export interface IntegrationApi { - readonly list: IntegrationListOperation - readonly get: IntegrationGetOperation - readonly connect: { - readonly key: IntegrationConnectKeyOperation - readonly oauth: IntegrationConnectOauthOperation - } - readonly attempt: { - readonly status: IntegrationAttemptStatusOperation - readonly complete: IntegrationAttemptCompleteOperation - readonly cancel: IntegrationAttemptCancelOperation - } -} - -type Endpoint11_0Request = Parameters[0] -export type Endpoint11_0Input = { readonly location?: Endpoint11_0Request["query"]["location"] } -export type Endpoint11_0Output = EffectValue> -export type McpListOperation = (input?: Endpoint11_0Input) => Effect.Effect - -type Endpoint11_1Request = Parameters[0] -export type Endpoint11_1Input = { readonly location?: Endpoint11_1Request["query"]["location"] } -export type Endpoint11_1Output = EffectValue> -export type McpResourceCatalogOperation = (input?: Endpoint11_1Input) => Effect.Effect - -export interface McpApi { - readonly list: McpListOperation - readonly resource: { readonly catalog: McpResourceCatalogOperation } -} - -type Endpoint12_0Request = Parameters[0] -export type Endpoint12_0Input = { - readonly credentialID: Endpoint12_0Request["params"]["credentialID"] - readonly location?: Endpoint12_0Request["query"]["location"] - readonly label: Endpoint12_0Request["payload"]["label"] -} -export type Endpoint12_0Output = EffectValue> -export type CredentialUpdateOperation = (input: Endpoint12_0Input) => Effect.Effect - -type Endpoint12_1Request = Parameters[0] -export type Endpoint12_1Input = { - readonly credentialID: Endpoint12_1Request["params"]["credentialID"] - readonly location?: Endpoint12_1Request["query"]["location"] -} -export type Endpoint12_1Output = EffectValue> -export type CredentialRemoveOperation = (input: Endpoint12_1Input) => Effect.Effect - -export interface CredentialApi { - readonly update: CredentialUpdateOperation - readonly remove: CredentialRemoveOperation -} - -export type Endpoint13_0Output = EffectValue> -export type ProjectListOperation = () => Effect.Effect - -type Endpoint13_1Request = Parameters[0] -export type Endpoint13_1Input = { readonly location?: Endpoint13_1Request["query"]["location"] } -export type Endpoint13_1Output = EffectValue> -export type ProjectCurrentOperation = (input?: Endpoint13_1Input) => Effect.Effect - -type Endpoint13_2Request = Parameters[0] -export type Endpoint13_2Input = { - readonly projectID: Endpoint13_2Request["params"]["projectID"] - readonly location?: Endpoint13_2Request["query"]["location"] -} -export type Endpoint13_2Output = EffectValue> -export type ProjectDirectoriesOperation = (input: Endpoint13_2Input) => Effect.Effect - -export interface ProjectApi { - readonly list: ProjectListOperation - readonly current: ProjectCurrentOperation - readonly directories: ProjectDirectoriesOperation -} - -type Endpoint14_0Request = Parameters[0] -export type Endpoint14_0Input = { readonly location?: Endpoint14_0Request["query"]["location"] } -export type Endpoint14_0Output = EffectValue> -export type FormRequestListOperation = (input?: Endpoint14_0Input) => Effect.Effect - -type Endpoint14_1Request = Parameters[0] -export type Endpoint14_1Input = { readonly sessionID: Endpoint14_1Request["params"]["sessionID"] } -export type Endpoint14_1Output = EffectValue>["data"] -export type FormListOperation = (input: Endpoint14_1Input) => Effect.Effect - -type Endpoint14_2Request = Parameters[0] -export type Endpoint14_2Input = { - readonly sessionID: Endpoint14_2Request["params"]["sessionID"] - readonly id?: Endpoint14_2Request["payload"]["id"] - readonly title: Endpoint14_2Request["payload"]["title"] - readonly metadata?: Endpoint14_2Request["payload"]["metadata"] - readonly fields: Endpoint14_2Request["payload"]["fields"] -} -export type Endpoint14_2Output = EffectValue>["data"] -export type FormCreateOperation = (input: Endpoint14_2Input) => Effect.Effect - -type Endpoint14_3Request = Parameters[0] -export type Endpoint14_3Input = { - readonly sessionID: Endpoint14_3Request["params"]["sessionID"] - readonly formID: Endpoint14_3Request["params"]["formID"] -} -export type Endpoint14_3Output = EffectValue>["data"] -export type FormGetOperation = (input: Endpoint14_3Input) => Effect.Effect - -type Endpoint14_4Request = Parameters[0] -export type Endpoint14_4Input = { - readonly sessionID: Endpoint14_4Request["params"]["sessionID"] - readonly formID: Endpoint14_4Request["params"]["formID"] -} -export type Endpoint14_4Output = EffectValue>["data"] -export type FormStateOperation = (input: Endpoint14_4Input) => Effect.Effect - -type Endpoint14_5Request = Parameters[0] -export type Endpoint14_5Input = { - readonly sessionID: Endpoint14_5Request["params"]["sessionID"] - readonly formID: Endpoint14_5Request["params"]["formID"] - readonly answer: Endpoint14_5Request["payload"]["answer"] -} -export type Endpoint14_5Output = EffectValue> -export type FormReplyOperation = (input: Endpoint14_5Input) => Effect.Effect - -type Endpoint14_6Request = Parameters[0] -export type Endpoint14_6Input = { - readonly sessionID: Endpoint14_6Request["params"]["sessionID"] - readonly formID: Endpoint14_6Request["params"]["formID"] -} -export type Endpoint14_6Output = EffectValue> -export type FormCancelOperation = (input: Endpoint14_6Input) => Effect.Effect - -export interface FormApi { - readonly request: { readonly list: FormRequestListOperation } - readonly list: FormListOperation - readonly create: FormCreateOperation - readonly get: FormGetOperation - readonly state: FormStateOperation - readonly reply: FormReplyOperation - readonly cancel: FormCancelOperation -} - -type Endpoint15_0Request = Parameters[0] -export type Endpoint15_0Input = { readonly location?: Endpoint15_0Request["query"]["location"] } -export type Endpoint15_0Output = EffectValue> -export type PermissionRequestListOperation = ( - input?: Endpoint15_0Input, -) => Effect.Effect - -type Endpoint15_1Request = Parameters[0] -export type Endpoint15_1Input = { readonly projectID?: Endpoint15_1Request["query"]["projectID"] } -export type Endpoint15_1Output = EffectValue< - ReturnType ->["data"] -export type PermissionSavedListOperation = ( - input?: Endpoint15_1Input, -) => Effect.Effect - -type Endpoint15_2Request = Parameters[0] -export type Endpoint15_2Input = { readonly id: Endpoint15_2Request["params"]["id"] } -export type Endpoint15_2Output = EffectValue> -export type PermissionSavedRemoveOperation = ( - input: Endpoint15_2Input, -) => Effect.Effect - -type Endpoint15_3Request = Parameters[0] -export type Endpoint15_3Input = { - readonly sessionID: Endpoint15_3Request["params"]["sessionID"] - readonly id?: Endpoint15_3Request["payload"]["id"] - readonly action: Endpoint15_3Request["payload"]["action"] - readonly resources: Endpoint15_3Request["payload"]["resources"] - readonly save?: Endpoint15_3Request["payload"]["save"] - readonly metadata?: Endpoint15_3Request["payload"]["metadata"] - readonly source?: Endpoint15_3Request["payload"]["source"] - readonly agent?: Endpoint15_3Request["payload"]["agent"] -} -export type Endpoint15_3Output = EffectValue< - ReturnType ->["data"] -export type PermissionCreateOperation = (input: Endpoint15_3Input) => Effect.Effect - -type Endpoint15_4Request = Parameters[0] -export type Endpoint15_4Input = { readonly sessionID: Endpoint15_4Request["params"]["sessionID"] } -export type Endpoint15_4Output = EffectValue< - ReturnType ->["data"] -export type PermissionListOperation = (input: Endpoint15_4Input) => Effect.Effect - -type Endpoint15_5Request = Parameters[0] -export type Endpoint15_5Input = { - readonly sessionID: Endpoint15_5Request["params"]["sessionID"] - readonly requestID: Endpoint15_5Request["params"]["requestID"] -} -export type Endpoint15_5Output = EffectValue< - ReturnType ->["data"] -export type PermissionGetOperation = (input: Endpoint15_5Input) => Effect.Effect - -type Endpoint15_6Request = Parameters[0] -export type Endpoint15_6Input = { - readonly sessionID: Endpoint15_6Request["params"]["sessionID"] - readonly requestID: Endpoint15_6Request["params"]["requestID"] - readonly reply: Endpoint15_6Request["payload"]["reply"] - readonly message?: Endpoint15_6Request["payload"]["message"] -} -export type Endpoint15_6Output = EffectValue> -export type PermissionReplyOperation = (input: Endpoint15_6Input) => Effect.Effect - -export interface PermissionApi { - readonly request: { readonly list: PermissionRequestListOperation } - readonly saved: { readonly list: PermissionSavedListOperation; readonly remove: PermissionSavedRemoveOperation } - readonly create: PermissionCreateOperation - readonly list: PermissionListOperation - readonly get: PermissionGetOperation - readonly reply: PermissionReplyOperation -} - -type Endpoint16_0Request = Parameters[0] -export type Endpoint16_0Input = { - readonly location?: Endpoint16_0Request["query"]["location"] - readonly path?: Endpoint16_0Request["query"]["path"] -} -export type Endpoint16_0Output = EffectValue> -export type FileListOperation = (input?: Endpoint16_0Input) => Effect.Effect - -type Endpoint16_1Request = Parameters[0] -export type Endpoint16_1Input = { - readonly location?: Endpoint16_1Request["query"]["location"] - readonly query: Endpoint16_1Request["query"]["query"] - readonly type?: Endpoint16_1Request["query"]["type"] - readonly limit?: Endpoint16_1Request["query"]["limit"] -} -export type Endpoint16_1Output = EffectValue> -export type FileFindOperation = (input: Endpoint16_1Input) => Effect.Effect - -export interface FileApi { - readonly list: FileListOperation - readonly find: FileFindOperation -} - -type Endpoint17_0Request = Parameters[0] -export type Endpoint17_0Input = { readonly location?: Endpoint17_0Request["query"]["location"] } -export type Endpoint17_0Output = EffectValue> -export type CommandListOperation = (input?: Endpoint17_0Input) => Effect.Effect - -export interface CommandApi { - readonly list: CommandListOperation -} - -type Endpoint18_0Request = Parameters[0] -export type Endpoint18_0Input = { readonly location?: Endpoint18_0Request["query"]["location"] } -export type Endpoint18_0Output = EffectValue> -export type SkillListOperation = (input?: Endpoint18_0Input) => Effect.Effect - -export interface SkillApi { - readonly list: SkillListOperation -} - -export type Endpoint19_0Output = StreamValue>> -export type EventSubscribeOperation = () => Stream.Stream - -export interface EventApi { - readonly subscribe: EventSubscribeOperation -} - -type Endpoint20_0Request = Parameters[0] -export type Endpoint20_0Input = { readonly location?: Endpoint20_0Request["query"]["location"] } -export type Endpoint20_0Output = EffectValue> -export type PtyListOperation = (input?: Endpoint20_0Input) => Effect.Effect - -type Endpoint20_1Request = Parameters[0] -export type Endpoint20_1Input = { - readonly location?: Endpoint20_1Request["query"]["location"] - readonly command?: Endpoint20_1Request["payload"]["command"] - readonly args?: Endpoint20_1Request["payload"]["args"] - readonly cwd?: Endpoint20_1Request["payload"]["cwd"] - readonly title?: Endpoint20_1Request["payload"]["title"] - readonly env?: Endpoint20_1Request["payload"]["env"] -} -export type Endpoint20_1Output = EffectValue> -export type PtyCreateOperation = (input?: Endpoint20_1Input) => Effect.Effect - -type Endpoint20_2Request = Parameters[0] -export type Endpoint20_2Input = { - readonly ptyID: Endpoint20_2Request["params"]["ptyID"] - readonly location?: Endpoint20_2Request["query"]["location"] -} -export type Endpoint20_2Output = EffectValue> -export type PtyGetOperation = (input: Endpoint20_2Input) => Effect.Effect - -type Endpoint20_3Request = Parameters[0] -export type Endpoint20_3Input = { - readonly ptyID: Endpoint20_3Request["params"]["ptyID"] - readonly location?: Endpoint20_3Request["query"]["location"] - readonly title?: Endpoint20_3Request["payload"]["title"] - readonly size?: Endpoint20_3Request["payload"]["size"] -} -export type Endpoint20_3Output = EffectValue> -export type PtyUpdateOperation = (input: Endpoint20_3Input) => Effect.Effect - -type Endpoint20_4Request = Parameters[0] -export type Endpoint20_4Input = { - readonly ptyID: Endpoint20_4Request["params"]["ptyID"] - readonly location?: Endpoint20_4Request["query"]["location"] -} -export type Endpoint20_4Output = EffectValue> -export type PtyRemoveOperation = (input: Endpoint20_4Input) => Effect.Effect - -export interface PtyApi { - readonly list: PtyListOperation - readonly create: PtyCreateOperation - readonly get: PtyGetOperation - readonly update: PtyUpdateOperation - readonly remove: PtyRemoveOperation -} - -type Endpoint21_0Request = Parameters[0] -export type Endpoint21_0Input = { readonly location?: Endpoint21_0Request["query"]["location"] } -export type Endpoint21_0Output = EffectValue> -export type ShellListOperation = (input?: Endpoint21_0Input) => Effect.Effect - -type Endpoint21_1Request = Parameters[0] -export type Endpoint21_1Input = { - readonly location?: Endpoint21_1Request["query"]["location"] - readonly command: Endpoint21_1Request["payload"]["command"] - readonly cwd?: Endpoint21_1Request["payload"]["cwd"] - readonly timeout: Endpoint21_1Request["payload"]["timeout"] - readonly metadata?: Endpoint21_1Request["payload"]["metadata"] -} -export type Endpoint21_1Output = EffectValue> -export type ShellCreateOperation = (input: Endpoint21_1Input) => Effect.Effect - -type Endpoint21_2Request = Parameters[0] -export type Endpoint21_2Input = { - readonly id: Endpoint21_2Request["params"]["id"] - readonly location?: Endpoint21_2Request["query"]["location"] -} -export type Endpoint21_2Output = EffectValue> -export type ShellGetOperation = (input: Endpoint21_2Input) => Effect.Effect - -type Endpoint21_3Request = Parameters[0] -export type Endpoint21_3Input = { - readonly id: Endpoint21_3Request["params"]["id"] - readonly location?: Endpoint21_3Request["query"]["location"] - readonly timeout: Endpoint21_3Request["payload"]["timeout"] -} -export type Endpoint21_3Output = EffectValue> -export type ShellTimeoutOperation = (input: Endpoint21_3Input) => Effect.Effect - -type Endpoint21_4Request = Parameters[0] -export type Endpoint21_4Input = { - readonly id: Endpoint21_4Request["params"]["id"] - readonly location?: Endpoint21_4Request["query"]["location"] - readonly cursor?: Endpoint21_4Request["query"]["cursor"] - readonly limit?: Endpoint21_4Request["query"]["limit"] -} -export type Endpoint21_4Output = EffectValue> -export type ShellOutputOperation = (input: Endpoint21_4Input) => Effect.Effect - -type Endpoint21_5Request = Parameters[0] -export type Endpoint21_5Input = { - readonly id: Endpoint21_5Request["params"]["id"] - readonly location?: Endpoint21_5Request["query"]["location"] -} -export type Endpoint21_5Output = EffectValue> -export type ShellRemoveOperation = (input: Endpoint21_5Input) => Effect.Effect - -export interface ShellApi { - readonly list: ShellListOperation - readonly create: ShellCreateOperation - readonly get: ShellGetOperation - readonly timeout: ShellTimeoutOperation - readonly output: ShellOutputOperation - readonly remove: ShellRemoveOperation -} - -type Endpoint22_0Request = Parameters[0] -export type Endpoint22_0Input = { readonly location?: Endpoint22_0Request["query"]["location"] } -export type Endpoint22_0Output = EffectValue> -export type QuestionRequestListOperation = ( - input?: Endpoint22_0Input, -) => Effect.Effect - -type Endpoint22_1Request = Parameters[0] -export type Endpoint22_1Input = { readonly sessionID: Endpoint22_1Request["params"]["sessionID"] } -export type Endpoint22_1Output = EffectValue>["data"] -export type QuestionListOperation = (input: Endpoint22_1Input) => Effect.Effect - -type Endpoint22_2Request = Parameters[0] -export type Endpoint22_2Input = { - readonly sessionID: Endpoint22_2Request["params"]["sessionID"] - readonly requestID: Endpoint22_2Request["params"]["requestID"] - readonly answers: Endpoint22_2Request["payload"]["answers"] -} -export type Endpoint22_2Output = EffectValue> -export type QuestionReplyOperation = (input: Endpoint22_2Input) => Effect.Effect - -type Endpoint22_3Request = Parameters[0] -export type Endpoint22_3Input = { - readonly sessionID: Endpoint22_3Request["params"]["sessionID"] - readonly requestID: Endpoint22_3Request["params"]["requestID"] -} -export type Endpoint22_3Output = EffectValue> -export type QuestionRejectOperation = (input: Endpoint22_3Input) => Effect.Effect - -export interface QuestionApi { - readonly request: { readonly list: QuestionRequestListOperation } - readonly list: QuestionListOperation - readonly reply: QuestionReplyOperation - readonly reject: QuestionRejectOperation -} - -type Endpoint23_0Request = Parameters[0] -export type Endpoint23_0Input = { readonly location?: Endpoint23_0Request["query"]["location"] } -export type Endpoint23_0Output = EffectValue> -export type ReferenceListOperation = (input?: Endpoint23_0Input) => Effect.Effect - -export interface ReferenceApi { - readonly list: ReferenceListOperation -} - -type Endpoint24_0Request = Parameters[0] -export type Endpoint24_0Input = { - readonly projectID: Endpoint24_0Request["params"]["projectID"] - readonly location?: Endpoint24_0Request["query"]["location"] - readonly strategy: Endpoint24_0Request["payload"]["strategy"] - readonly directory: Endpoint24_0Request["payload"]["directory"] - readonly name?: Endpoint24_0Request["payload"]["name"] -} -export type Endpoint24_0Output = EffectValue> -export type ProjectCopyCreateOperation = (input: Endpoint24_0Input) => Effect.Effect - -type Endpoint24_1Request = Parameters[0] -export type Endpoint24_1Input = { - readonly projectID: Endpoint24_1Request["params"]["projectID"] - readonly location?: Endpoint24_1Request["query"]["location"] - readonly directory: Endpoint24_1Request["payload"]["directory"] - readonly force: Endpoint24_1Request["payload"]["force"] -} -export type Endpoint24_1Output = EffectValue> -export type ProjectCopyRemoveOperation = (input: Endpoint24_1Input) => Effect.Effect - -type Endpoint24_2Request = Parameters[0] -export type Endpoint24_2Input = { - readonly projectID: Endpoint24_2Request["params"]["projectID"] - readonly location?: Endpoint24_2Request["query"]["location"] -} -export type Endpoint24_2Output = EffectValue> -export type ProjectCopyRefreshOperation = (input: Endpoint24_2Input) => Effect.Effect - -export interface ProjectCopyApi { - readonly create: ProjectCopyCreateOperation - readonly remove: ProjectCopyRemoveOperation - readonly refresh: ProjectCopyRefreshOperation -} - -type Endpoint25_0Request = Parameters[0] -export type Endpoint25_0Input = { readonly location?: Endpoint25_0Request["query"]["location"] } -export type Endpoint25_0Output = EffectValue> -export type VcsStatusOperation = (input?: Endpoint25_0Input) => Effect.Effect - -type Endpoint25_1Request = Parameters[0] -export type Endpoint25_1Input = { - readonly location?: Endpoint25_1Request["query"]["location"] - readonly mode: Endpoint25_1Request["query"]["mode"] - readonly context?: Endpoint25_1Request["query"]["context"] -} -export type Endpoint25_1Output = EffectValue> -export type VcsDiffOperation = (input: Endpoint25_1Input) => Effect.Effect - -export interface VcsApi { - readonly status: VcsStatusOperation - readonly diff: VcsDiffOperation -} - -export type Endpoint26_0Output = EffectValue> -export type DebugLocationListOperation = () => Effect.Effect - -type Endpoint26_1Request = Parameters[0] -export type Endpoint26_1Input = { readonly location?: Endpoint26_1Request["query"]["location"] } -export type Endpoint26_1Output = EffectValue> -export type DebugLocationEvictOperation = (input?: Endpoint26_1Input) => Effect.Effect - -export interface DebugApi { - readonly location: { readonly list: DebugLocationListOperation; readonly evict: DebugLocationEvictOperation } -} - -export interface AppApi { - readonly health: HealthApi - readonly server: ServerApi - readonly location: LocationApi - readonly agent: AgentApi - readonly plugin: PluginApi - readonly session: SessionApi - readonly message: MessageApi - readonly model: ModelApi - readonly generate: GenerateApi - readonly provider: ProviderApi - readonly integration: IntegrationApi - readonly mcp: McpApi - readonly credential: CredentialApi - readonly project: ProjectApi - readonly form: FormApi - readonly permission: PermissionApi - readonly file: FileApi - readonly command: CommandApi - readonly skill: SkillApi - readonly event: EventApi - readonly pty: PtyApi - readonly shell: ShellApi - readonly question: QuestionApi - readonly reference: ReferenceApi - readonly projectCopy: ProjectCopyApi - readonly vcs: VcsApi - readonly debug: DebugApi -} diff --git a/packages/client/src/effect/generated/client.ts b/packages/client/src/effect/generated/client.ts deleted file mode 100644 index 1f58717113..0000000000 --- a/packages/client/src/effect/generated/client.ts +++ /dev/null @@ -1,1156 +0,0 @@ -// Generated by @opencode-ai/httpapi-codegen. Do not edit. -import { Effect, Stream, Schema } from "effect" -import { Sse } from "effect/unstable/encoding" -import { HttpClientError } from "effect/unstable/http" -import { HttpApiClient } from "effect/unstable/httpapi" -import { ClientApi } from "../../contract" -import { ClientError } from "./client-error" - -type RawClient = HttpApiClient.ForApi - -const mapClientError = (error: E) => - HttpClientError.isHttpClientError(error) || Schema.isSchemaError(error) || Sse.Retry.is(error) - ? new ClientError({ cause: error }) - : error - -const Endpoint0_0 = (raw: RawClient["server.health"]) => () => - raw["health.get"]({}).pipe(Effect.mapError(mapClientError)) - -const adaptGroup0 = (raw: RawClient["server.health"]) => ({ get: Endpoint0_0(raw) }) - -const Endpoint1_0 = (raw: RawClient["server.server"]) => () => - raw["server.get"]({}).pipe(Effect.mapError(mapClientError)) - -const adaptGroup1 = (raw: RawClient["server.server"]) => ({ get: Endpoint1_0(raw) }) - -type Endpoint2_0Request = Parameters[0] -type Endpoint2_0Input = { readonly location?: Endpoint2_0Request["query"]["location"] } -const Endpoint2_0 = (raw: RawClient["server.location"]) => (input?: Endpoint2_0Input) => - raw["location.get"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)) - -const adaptGroup2 = (raw: RawClient["server.location"]) => ({ get: Endpoint2_0(raw) }) - -type Endpoint3_0Request = Parameters[0] -type Endpoint3_0Input = { readonly location?: Endpoint3_0Request["query"]["location"] } -const Endpoint3_0 = (raw: RawClient["server.agent"]) => (input?: Endpoint3_0Input) => - raw["agent.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)) - -const adaptGroup3 = (raw: RawClient["server.agent"]) => ({ list: Endpoint3_0(raw) }) - -type Endpoint4_0Request = Parameters[0] -type Endpoint4_0Input = { readonly location?: Endpoint4_0Request["query"]["location"] } -const Endpoint4_0 = (raw: RawClient["server.plugin"]) => (input?: Endpoint4_0Input) => - raw["plugin.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)) - -const adaptGroup4 = (raw: RawClient["server.plugin"]) => ({ list: Endpoint4_0(raw) }) - -type Endpoint5_0Request = Parameters[0] -type Endpoint5_0Input = { - readonly workspace?: Endpoint5_0Request["query"]["workspace"] - readonly limit?: Endpoint5_0Request["query"]["limit"] - readonly order?: Endpoint5_0Request["query"]["order"] - readonly search?: Endpoint5_0Request["query"]["search"] - readonly parentID?: Endpoint5_0Request["query"]["parentID"] - readonly directory?: Endpoint5_0Request["query"]["directory"] - readonly project?: Endpoint5_0Request["query"]["project"] - readonly subpath?: Endpoint5_0Request["query"]["subpath"] - readonly cursor?: Endpoint5_0Request["query"]["cursor"] -} -const Endpoint5_0 = (raw: RawClient["server.session"]) => (input?: Endpoint5_0Input) => - raw["session.list"]({ - query: { - workspace: input?.["workspace"], - limit: input?.["limit"], - order: input?.["order"], - search: input?.["search"], - parentID: input?.["parentID"], - directory: input?.["directory"], - project: input?.["project"], - subpath: input?.["subpath"], - cursor: input?.["cursor"], - }, - }).pipe(Effect.mapError(mapClientError)) - -type Endpoint5_1Request = Parameters[0] -type Endpoint5_1Input = { - readonly id?: Endpoint5_1Request["payload"]["id"] - readonly agent?: Endpoint5_1Request["payload"]["agent"] - readonly model?: Endpoint5_1Request["payload"]["model"] - readonly location?: Endpoint5_1Request["payload"]["location"] -} -const Endpoint5_1 = (raw: RawClient["server.session"]) => (input?: Endpoint5_1Input) => - raw["session.create"]({ - payload: { id: input?.["id"], agent: input?.["agent"], model: input?.["model"], location: input?.["location"] }, - }).pipe( - Effect.mapError(mapClientError), - Effect.map((value) => value.data), - ) - -const Endpoint5_2 = (raw: RawClient["server.session"]) => () => - raw["session.active"]({}).pipe( - Effect.mapError(mapClientError), - Effect.map((value) => value.data), - ) - -type Endpoint5_3Request = Parameters[0] -type Endpoint5_3Input = { readonly sessionID: Endpoint5_3Request["params"]["sessionID"] } -const Endpoint5_3 = (raw: RawClient["server.session"]) => (input: Endpoint5_3Input) => - raw["session.get"]({ params: { sessionID: input["sessionID"] } }).pipe( - Effect.mapError(mapClientError), - Effect.map((value) => value.data), - ) - -type Endpoint5_4Request = Parameters[0] -type Endpoint5_4Input = { readonly sessionID: Endpoint5_4Request["params"]["sessionID"] } -const Endpoint5_4 = (raw: RawClient["server.session"]) => (input: Endpoint5_4Input) => - raw["session.remove"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError)) - -type Endpoint5_5Request = Parameters[0] -type Endpoint5_5Input = { - readonly sessionID: Endpoint5_5Request["params"]["sessionID"] - readonly messageID?: Endpoint5_5Request["payload"]["messageID"] -} -const Endpoint5_5 = (raw: RawClient["server.session"]) => (input: Endpoint5_5Input) => - raw["session.fork"]({ params: { sessionID: input["sessionID"] }, payload: { messageID: input["messageID"] } }).pipe( - Effect.mapError(mapClientError), - Effect.map((value) => value.data), - ) - -type Endpoint5_6Request = Parameters[0] -type Endpoint5_6Input = { - readonly sessionID: Endpoint5_6Request["params"]["sessionID"] - readonly agent: Endpoint5_6Request["payload"]["agent"] -} -const Endpoint5_6 = (raw: RawClient["server.session"]) => (input: Endpoint5_6Input) => - raw["session.switchAgent"]({ params: { sessionID: input["sessionID"] }, payload: { agent: input["agent"] } }).pipe( - Effect.mapError(mapClientError), - ) - -type Endpoint5_7Request = Parameters[0] -type Endpoint5_7Input = { - readonly sessionID: Endpoint5_7Request["params"]["sessionID"] - readonly model: Endpoint5_7Request["payload"]["model"] -} -const Endpoint5_7 = (raw: RawClient["server.session"]) => (input: Endpoint5_7Input) => - raw["session.switchModel"]({ params: { sessionID: input["sessionID"] }, payload: { model: input["model"] } }).pipe( - Effect.mapError(mapClientError), - ) - -type Endpoint5_8Request = Parameters[0] -type Endpoint5_8Input = { - readonly sessionID: Endpoint5_8Request["params"]["sessionID"] - readonly title: Endpoint5_8Request["payload"]["title"] -} -const Endpoint5_8 = (raw: RawClient["server.session"]) => (input: Endpoint5_8Input) => - raw["session.rename"]({ params: { sessionID: input["sessionID"] }, payload: { title: input["title"] } }).pipe( - Effect.mapError(mapClientError), - ) - -type Endpoint5_9Request = Parameters[0] -type Endpoint5_9Input = { - readonly sessionID: Endpoint5_9Request["params"]["sessionID"] - readonly destination: Endpoint5_9Request["payload"]["destination"] - readonly moveChanges?: Endpoint5_9Request["payload"]["moveChanges"] -} -const Endpoint5_9 = (raw: RawClient["server.session"]) => (input: Endpoint5_9Input) => - raw["session.move"]({ - params: { sessionID: input["sessionID"] }, - payload: { destination: input["destination"], moveChanges: input["moveChanges"] }, - }).pipe(Effect.mapError(mapClientError)) - -type Endpoint5_10Request = Parameters[0] -type Endpoint5_10Input = { - readonly sessionID: Endpoint5_10Request["params"]["sessionID"] - readonly id?: Endpoint5_10Request["payload"]["id"] - readonly text: Endpoint5_10Request["payload"]["text"] - readonly files?: Endpoint5_10Request["payload"]["files"] - readonly agents?: Endpoint5_10Request["payload"]["agents"] - readonly metadata?: Endpoint5_10Request["payload"]["metadata"] - readonly delivery?: Endpoint5_10Request["payload"]["delivery"] - readonly resume?: Endpoint5_10Request["payload"]["resume"] -} -const Endpoint5_10 = (raw: RawClient["server.session"]) => (input: Endpoint5_10Input) => - raw["session.prompt"]({ - params: { sessionID: input["sessionID"] }, - payload: { - id: input["id"], - text: input["text"], - files: input["files"], - agents: input["agents"], - metadata: input["metadata"], - delivery: input["delivery"], - resume: input["resume"], - }, - }).pipe( - Effect.mapError(mapClientError), - Effect.map((value) => value.data), - ) - -type Endpoint5_11Request = Parameters[0] -type Endpoint5_11Input = { - readonly sessionID: Endpoint5_11Request["params"]["sessionID"] - readonly id?: Endpoint5_11Request["payload"]["id"] - readonly command: Endpoint5_11Request["payload"]["command"] - readonly arguments?: Endpoint5_11Request["payload"]["arguments"] - readonly agent?: Endpoint5_11Request["payload"]["agent"] - readonly model?: Endpoint5_11Request["payload"]["model"] - readonly files?: Endpoint5_11Request["payload"]["files"] - readonly agents?: Endpoint5_11Request["payload"]["agents"] - readonly delivery?: Endpoint5_11Request["payload"]["delivery"] - readonly resume?: Endpoint5_11Request["payload"]["resume"] -} -const Endpoint5_11 = (raw: RawClient["server.session"]) => (input: Endpoint5_11Input) => - raw["session.command"]({ - params: { sessionID: input["sessionID"] }, - payload: { - id: input["id"], - command: input["command"], - arguments: input["arguments"], - agent: input["agent"], - model: input["model"], - files: input["files"], - agents: input["agents"], - delivery: input["delivery"], - resume: input["resume"], - }, - }).pipe( - Effect.mapError(mapClientError), - Effect.map((value) => value.data), - ) - -type Endpoint5_12Request = Parameters[0] -type Endpoint5_12Input = { - readonly sessionID: Endpoint5_12Request["params"]["sessionID"] - readonly id?: Endpoint5_12Request["payload"]["id"] - readonly skill: Endpoint5_12Request["payload"]["skill"] - readonly resume?: Endpoint5_12Request["payload"]["resume"] -} -const Endpoint5_12 = (raw: RawClient["server.session"]) => (input: Endpoint5_12Input) => - raw["session.skill"]({ - params: { sessionID: input["sessionID"] }, - payload: { id: input["id"], skill: input["skill"], resume: input["resume"] }, - }).pipe(Effect.mapError(mapClientError)) - -type Endpoint5_13Request = Parameters[0] -type Endpoint5_13Input = { - readonly sessionID: Endpoint5_13Request["params"]["sessionID"] - readonly id?: Endpoint5_13Request["payload"]["id"] - readonly text: Endpoint5_13Request["payload"]["text"] - readonly description?: Endpoint5_13Request["payload"]["description"] - readonly metadata?: Endpoint5_13Request["payload"]["metadata"] - readonly delivery?: Endpoint5_13Request["payload"]["delivery"] - readonly resume?: Endpoint5_13Request["payload"]["resume"] -} -const Endpoint5_13 = (raw: RawClient["server.session"]) => (input: Endpoint5_13Input) => - raw["session.synthetic"]({ - params: { sessionID: input["sessionID"] }, - payload: { - id: input["id"], - text: input["text"], - description: input["description"], - metadata: input["metadata"], - delivery: input["delivery"], - resume: input["resume"], - }, - }).pipe( - Effect.mapError(mapClientError), - Effect.map((value) => value.data), - ) - -type Endpoint5_14Request = Parameters[0] -type Endpoint5_14Input = { - readonly sessionID: Endpoint5_14Request["params"]["sessionID"] - readonly id?: Endpoint5_14Request["payload"]["id"] - readonly command: Endpoint5_14Request["payload"]["command"] -} -const Endpoint5_14 = (raw: RawClient["server.session"]) => (input: Endpoint5_14Input) => - raw["session.shell"]({ - params: { sessionID: input["sessionID"] }, - payload: { id: input["id"], command: input["command"] }, - }).pipe(Effect.mapError(mapClientError)) - -type Endpoint5_15Request = Parameters[0] -type Endpoint5_15Input = { - readonly sessionID: Endpoint5_15Request["params"]["sessionID"] - readonly id?: Endpoint5_15Request["payload"]["id"] -} -const Endpoint5_15 = (raw: RawClient["server.session"]) => (input: Endpoint5_15Input) => - raw["session.compact"]({ params: { sessionID: input["sessionID"] }, payload: { id: input["id"] } }).pipe( - Effect.mapError(mapClientError), - Effect.map((value) => value.data), - ) - -type Endpoint5_16Request = Parameters[0] -type Endpoint5_16Input = { readonly sessionID: Endpoint5_16Request["params"]["sessionID"] } -const Endpoint5_16 = (raw: RawClient["server.session"]) => (input: Endpoint5_16Input) => - raw["session.wait"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError)) - -type Endpoint5_17Request = Parameters[0] -type Endpoint5_17Input = { - readonly sessionID: Endpoint5_17Request["params"]["sessionID"] - readonly messageID: Endpoint5_17Request["payload"]["messageID"] - readonly files?: Endpoint5_17Request["payload"]["files"] -} -const Endpoint5_17 = (raw: RawClient["server.session"]) => (input: Endpoint5_17Input) => - raw["session.revert.stage"]({ - params: { sessionID: input["sessionID"] }, - payload: { messageID: input["messageID"], files: input["files"] }, - }).pipe( - Effect.mapError(mapClientError), - Effect.map((value) => value.data), - ) - -type Endpoint5_18Request = Parameters[0] -type Endpoint5_18Input = { readonly sessionID: Endpoint5_18Request["params"]["sessionID"] } -const Endpoint5_18 = (raw: RawClient["server.session"]) => (input: Endpoint5_18Input) => - raw["session.revert.clear"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError)) - -type Endpoint5_19Request = Parameters[0] -type Endpoint5_19Input = { readonly sessionID: Endpoint5_19Request["params"]["sessionID"] } -const Endpoint5_19 = (raw: RawClient["server.session"]) => (input: Endpoint5_19Input) => - raw["session.revert.commit"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError)) - -type Endpoint5_20Request = Parameters[0] -type Endpoint5_20Input = { readonly sessionID: Endpoint5_20Request["params"]["sessionID"] } -const Endpoint5_20 = (raw: RawClient["server.session"]) => (input: Endpoint5_20Input) => - raw["session.context"]({ params: { sessionID: input["sessionID"] } }).pipe( - Effect.mapError(mapClientError), - Effect.map((value) => value.data), - ) - -type Endpoint5_21Request = Parameters[0] -type Endpoint5_21Input = { readonly sessionID: Endpoint5_21Request["params"]["sessionID"] } -const Endpoint5_21 = (raw: RawClient["server.session"]) => (input: Endpoint5_21Input) => - raw["session.pending.list"]({ params: { sessionID: input["sessionID"] } }).pipe( - Effect.mapError(mapClientError), - Effect.map((value) => value.data), - ) - -type Endpoint5_22Request = Parameters[0] -type Endpoint5_22Input = { readonly sessionID: Endpoint5_22Request["params"]["sessionID"] } -const Endpoint5_22 = (raw: RawClient["server.session"]) => (input: Endpoint5_22Input) => - raw["session.instructions.entry.list"]({ params: { sessionID: input["sessionID"] } }).pipe( - Effect.mapError(mapClientError), - Effect.map((value) => value.data), - ) - -type Endpoint5_23Request = Parameters[0] -type Endpoint5_23Input = { - readonly sessionID: Endpoint5_23Request["params"]["sessionID"] - readonly key: Endpoint5_23Request["params"]["key"] - readonly value: Endpoint5_23Request["payload"]["value"] -} -const Endpoint5_23 = (raw: RawClient["server.session"]) => (input: Endpoint5_23Input) => - raw["session.instructions.entry.put"]({ - params: { sessionID: input["sessionID"], key: input["key"] }, - payload: { value: input["value"] }, - }).pipe(Effect.mapError(mapClientError)) - -type Endpoint5_24Request = Parameters[0] -type Endpoint5_24Input = { - readonly sessionID: Endpoint5_24Request["params"]["sessionID"] - readonly key: Endpoint5_24Request["params"]["key"] -} -const Endpoint5_24 = (raw: RawClient["server.session"]) => (input: Endpoint5_24Input) => - raw["session.instructions.entry.remove"]({ params: { sessionID: input["sessionID"], key: input["key"] } }).pipe( - Effect.mapError(mapClientError), - ) - -type Endpoint5_25Request = Parameters[0] -type Endpoint5_25Input = { - readonly sessionID: Endpoint5_25Request["params"]["sessionID"] - readonly after?: Endpoint5_25Request["query"]["after"] - readonly follow?: Endpoint5_25Request["query"]["follow"] -} -const Endpoint5_25 = (raw: RawClient["server.session"]) => (input: Endpoint5_25Input) => - Stream.unwrap( - raw["session.log"]({ - params: { sessionID: input["sessionID"] }, - query: { after: input["after"], follow: input["follow"] }, - }).pipe( - Effect.mapError(mapClientError), - Effect.map((stream) => stream.pipe(Stream.mapError(mapClientError))), - ), - ) - -type Endpoint5_26Request = Parameters[0] -type Endpoint5_26Input = { readonly sessionID: Endpoint5_26Request["params"]["sessionID"] } -const Endpoint5_26 = (raw: RawClient["server.session"]) => (input: Endpoint5_26Input) => - raw["session.interrupt"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError)) - -type Endpoint5_27Request = Parameters[0] -type Endpoint5_27Input = { readonly sessionID: Endpoint5_27Request["params"]["sessionID"] } -const Endpoint5_27 = (raw: RawClient["server.session"]) => (input: Endpoint5_27Input) => - raw["session.background"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError)) - -type Endpoint5_28Request = Parameters[0] -type Endpoint5_28Input = { - readonly sessionID: Endpoint5_28Request["params"]["sessionID"] - readonly messageID: Endpoint5_28Request["params"]["messageID"] -} -const Endpoint5_28 = (raw: RawClient["server.session"]) => (input: Endpoint5_28Input) => - raw["session.message"]({ params: { sessionID: input["sessionID"], messageID: input["messageID"] } }).pipe( - Effect.mapError(mapClientError), - Effect.map((value) => value.data), - ) - -const adaptGroup5 = (raw: RawClient["server.session"]) => ({ - list: Endpoint5_0(raw), - create: Endpoint5_1(raw), - active: Endpoint5_2(raw), - get: Endpoint5_3(raw), - remove: Endpoint5_4(raw), - fork: Endpoint5_5(raw), - switchAgent: Endpoint5_6(raw), - switchModel: Endpoint5_7(raw), - rename: Endpoint5_8(raw), - move: Endpoint5_9(raw), - prompt: Endpoint5_10(raw), - command: Endpoint5_11(raw), - skill: Endpoint5_12(raw), - synthetic: Endpoint5_13(raw), - shell: Endpoint5_14(raw), - compact: Endpoint5_15(raw), - wait: Endpoint5_16(raw), - revert: { stage: Endpoint5_17(raw), clear: Endpoint5_18(raw), commit: Endpoint5_19(raw) }, - context: Endpoint5_20(raw), - pending: { list: Endpoint5_21(raw) }, - instructions: { entry: { list: Endpoint5_22(raw), put: Endpoint5_23(raw), remove: Endpoint5_24(raw) } }, - log: Endpoint5_25(raw), - interrupt: Endpoint5_26(raw), - background: Endpoint5_27(raw), - message: Endpoint5_28(raw), -}) - -type Endpoint6_0Request = Parameters[0] -type Endpoint6_0Input = { - readonly sessionID: Endpoint6_0Request["params"]["sessionID"] - readonly limit?: Endpoint6_0Request["query"]["limit"] - readonly order?: Endpoint6_0Request["query"]["order"] - readonly cursor?: Endpoint6_0Request["query"]["cursor"] -} -const Endpoint6_0 = (raw: RawClient["server.message"]) => (input: Endpoint6_0Input) => - raw["session.messages"]({ - params: { sessionID: input["sessionID"] }, - query: { limit: input["limit"], order: input["order"], cursor: input["cursor"] }, - }).pipe(Effect.mapError(mapClientError)) - -const adaptGroup6 = (raw: RawClient["server.message"]) => ({ list: Endpoint6_0(raw) }) - -type Endpoint7_0Request = Parameters[0] -type Endpoint7_0Input = { readonly location?: Endpoint7_0Request["query"]["location"] } -const Endpoint7_0 = (raw: RawClient["server.model"]) => (input?: Endpoint7_0Input) => - raw["model.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)) - -type Endpoint7_1Request = Parameters[0] -type Endpoint7_1Input = { readonly location?: Endpoint7_1Request["query"]["location"] } -const Endpoint7_1 = (raw: RawClient["server.model"]) => (input?: Endpoint7_1Input) => - raw["model.default"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)) - -const adaptGroup7 = (raw: RawClient["server.model"]) => ({ list: Endpoint7_0(raw), default: Endpoint7_1(raw) }) - -type Endpoint8_0Request = Parameters[0] -type Endpoint8_0Input = { - readonly location?: Endpoint8_0Request["query"]["location"] - readonly prompt: Endpoint8_0Request["payload"]["prompt"] - readonly model?: Endpoint8_0Request["payload"]["model"] -} -const Endpoint8_0 = (raw: RawClient["server.generate"]) => (input: Endpoint8_0Input) => - raw["generate.text"]({ - query: { location: input["location"] }, - payload: { prompt: input["prompt"], model: input["model"] }, - }).pipe( - Effect.mapError(mapClientError), - Effect.map((value) => value.data), - ) - -const adaptGroup8 = (raw: RawClient["server.generate"]) => ({ text: Endpoint8_0(raw) }) - -type Endpoint9_0Request = Parameters[0] -type Endpoint9_0Input = { readonly location?: Endpoint9_0Request["query"]["location"] } -const Endpoint9_0 = (raw: RawClient["server.provider"]) => (input?: Endpoint9_0Input) => - raw["provider.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)) - -type Endpoint9_1Request = Parameters[0] -type Endpoint9_1Input = { - readonly providerID: Endpoint9_1Request["params"]["providerID"] - readonly location?: Endpoint9_1Request["query"]["location"] -} -const Endpoint9_1 = (raw: RawClient["server.provider"]) => (input: Endpoint9_1Input) => - raw["provider.get"]({ params: { providerID: input["providerID"] }, query: { location: input["location"] } }).pipe( - Effect.mapError(mapClientError), - ) - -const adaptGroup9 = (raw: RawClient["server.provider"]) => ({ list: Endpoint9_0(raw), get: Endpoint9_1(raw) }) - -type Endpoint10_0Request = Parameters[0] -type Endpoint10_0Input = { readonly location?: Endpoint10_0Request["query"]["location"] } -const Endpoint10_0 = (raw: RawClient["server.integration"]) => (input?: Endpoint10_0Input) => - raw["integration.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)) - -type Endpoint10_1Request = Parameters[0] -type Endpoint10_1Input = { - readonly integrationID: Endpoint10_1Request["params"]["integrationID"] - readonly location?: Endpoint10_1Request["query"]["location"] -} -const Endpoint10_1 = (raw: RawClient["server.integration"]) => (input: Endpoint10_1Input) => - raw["integration.get"]({ - params: { integrationID: input["integrationID"] }, - query: { location: input["location"] }, - }).pipe(Effect.mapError(mapClientError)) - -type Endpoint10_2Request = Parameters[0] -type Endpoint10_2Input = { - readonly integrationID: Endpoint10_2Request["params"]["integrationID"] - readonly location?: Endpoint10_2Request["query"]["location"] - readonly key: Endpoint10_2Request["payload"]["key"] - readonly label?: Endpoint10_2Request["payload"]["label"] -} -const Endpoint10_2 = (raw: RawClient["server.integration"]) => (input: Endpoint10_2Input) => - raw["integration.connect.key"]({ - params: { integrationID: input["integrationID"] }, - query: { location: input["location"] }, - payload: { key: input["key"], label: input["label"] }, - }).pipe(Effect.mapError(mapClientError)) - -type Endpoint10_3Request = Parameters[0] -type Endpoint10_3Input = { - readonly integrationID: Endpoint10_3Request["params"]["integrationID"] - readonly location?: Endpoint10_3Request["query"]["location"] - readonly methodID: Endpoint10_3Request["payload"]["methodID"] - readonly inputs: Endpoint10_3Request["payload"]["inputs"] - readonly label?: Endpoint10_3Request["payload"]["label"] -} -const Endpoint10_3 = (raw: RawClient["server.integration"]) => (input: Endpoint10_3Input) => - raw["integration.connect.oauth"]({ - params: { integrationID: input["integrationID"] }, - query: { location: input["location"] }, - payload: { methodID: input["methodID"], inputs: input["inputs"], label: input["label"] }, - }).pipe(Effect.mapError(mapClientError)) - -type Endpoint10_4Request = Parameters[0] -type Endpoint10_4Input = { - readonly attemptID: Endpoint10_4Request["params"]["attemptID"] - readonly location?: Endpoint10_4Request["query"]["location"] -} -const Endpoint10_4 = (raw: RawClient["server.integration"]) => (input: Endpoint10_4Input) => - raw["integration.attempt.status"]({ - params: { attemptID: input["attemptID"] }, - query: { location: input["location"] }, - }).pipe(Effect.mapError(mapClientError)) - -type Endpoint10_5Request = Parameters[0] -type Endpoint10_5Input = { - readonly attemptID: Endpoint10_5Request["params"]["attemptID"] - readonly location?: Endpoint10_5Request["query"]["location"] - readonly code?: Endpoint10_5Request["payload"]["code"] -} -const Endpoint10_5 = (raw: RawClient["server.integration"]) => (input: Endpoint10_5Input) => - raw["integration.attempt.complete"]({ - params: { attemptID: input["attemptID"] }, - query: { location: input["location"] }, - payload: { code: input["code"] }, - }).pipe(Effect.mapError(mapClientError)) - -type Endpoint10_6Request = Parameters[0] -type Endpoint10_6Input = { - readonly attemptID: Endpoint10_6Request["params"]["attemptID"] - readonly location?: Endpoint10_6Request["query"]["location"] -} -const Endpoint10_6 = (raw: RawClient["server.integration"]) => (input: Endpoint10_6Input) => - raw["integration.attempt.cancel"]({ - params: { attemptID: input["attemptID"] }, - query: { location: input["location"] }, - }).pipe(Effect.mapError(mapClientError)) - -const adaptGroup10 = (raw: RawClient["server.integration"]) => ({ - list: Endpoint10_0(raw), - get: Endpoint10_1(raw), - connect: { key: Endpoint10_2(raw), oauth: Endpoint10_3(raw) }, - attempt: { status: Endpoint10_4(raw), complete: Endpoint10_5(raw), cancel: Endpoint10_6(raw) }, -}) - -type Endpoint11_0Request = Parameters[0] -type Endpoint11_0Input = { readonly location?: Endpoint11_0Request["query"]["location"] } -const Endpoint11_0 = (raw: RawClient["server.mcp"]) => (input?: Endpoint11_0Input) => - raw["mcp.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)) - -type Endpoint11_1Request = Parameters[0] -type Endpoint11_1Input = { readonly location?: Endpoint11_1Request["query"]["location"] } -const Endpoint11_1 = (raw: RawClient["server.mcp"]) => (input?: Endpoint11_1Input) => - raw["mcp.resource.catalog"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)) - -const adaptGroup11 = (raw: RawClient["server.mcp"]) => ({ - list: Endpoint11_0(raw), - resource: { catalog: Endpoint11_1(raw) }, -}) - -type Endpoint12_0Request = Parameters[0] -type Endpoint12_0Input = { - readonly credentialID: Endpoint12_0Request["params"]["credentialID"] - readonly location?: Endpoint12_0Request["query"]["location"] - readonly label: Endpoint12_0Request["payload"]["label"] -} -const Endpoint12_0 = (raw: RawClient["server.credential"]) => (input: Endpoint12_0Input) => - raw["credential.update"]({ - params: { credentialID: input["credentialID"] }, - query: { location: input["location"] }, - payload: { label: input["label"] }, - }).pipe(Effect.mapError(mapClientError)) - -type Endpoint12_1Request = Parameters[0] -type Endpoint12_1Input = { - readonly credentialID: Endpoint12_1Request["params"]["credentialID"] - readonly location?: Endpoint12_1Request["query"]["location"] -} -const Endpoint12_1 = (raw: RawClient["server.credential"]) => (input: Endpoint12_1Input) => - raw["credential.remove"]({ - params: { credentialID: input["credentialID"] }, - query: { location: input["location"] }, - }).pipe(Effect.mapError(mapClientError)) - -const adaptGroup12 = (raw: RawClient["server.credential"]) => ({ update: Endpoint12_0(raw), remove: Endpoint12_1(raw) }) - -const Endpoint13_0 = (raw: RawClient["server.project"]) => () => - raw["project.list"]({}).pipe(Effect.mapError(mapClientError)) - -type Endpoint13_1Request = Parameters[0] -type Endpoint13_1Input = { readonly location?: Endpoint13_1Request["query"]["location"] } -const Endpoint13_1 = (raw: RawClient["server.project"]) => (input?: Endpoint13_1Input) => - raw["project.current"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)) - -type Endpoint13_2Request = Parameters[0] -type Endpoint13_2Input = { - readonly projectID: Endpoint13_2Request["params"]["projectID"] - readonly location?: Endpoint13_2Request["query"]["location"] -} -const Endpoint13_2 = (raw: RawClient["server.project"]) => (input: Endpoint13_2Input) => - raw["project.directories"]({ - params: { projectID: input["projectID"] }, - query: { location: input["location"] }, - }).pipe(Effect.mapError(mapClientError)) - -const adaptGroup13 = (raw: RawClient["server.project"]) => ({ - list: Endpoint13_0(raw), - current: Endpoint13_1(raw), - directories: Endpoint13_2(raw), -}) - -type Endpoint14_0Request = Parameters[0] -type Endpoint14_0Input = { readonly location?: Endpoint14_0Request["query"]["location"] } -const Endpoint14_0 = (raw: RawClient["server.form"]) => (input?: Endpoint14_0Input) => - raw["form.request.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)) - -type Endpoint14_1Request = Parameters[0] -type Endpoint14_1Input = { readonly sessionID: Endpoint14_1Request["params"]["sessionID"] } -const Endpoint14_1 = (raw: RawClient["server.form"]) => (input: Endpoint14_1Input) => - raw["session.form.list"]({ params: { sessionID: input["sessionID"] } }).pipe( - Effect.mapError(mapClientError), - Effect.map((value) => value.data), - ) - -type Endpoint14_2Request = Parameters[0] -type Endpoint14_2Input = { - readonly sessionID: Endpoint14_2Request["params"]["sessionID"] - readonly id?: Endpoint14_2Request["payload"]["id"] - readonly title: Endpoint14_2Request["payload"]["title"] - readonly metadata?: Endpoint14_2Request["payload"]["metadata"] - readonly fields: Endpoint14_2Request["payload"]["fields"] -} -const Endpoint14_2 = (raw: RawClient["server.form"]) => (input: Endpoint14_2Input) => - raw["session.form.create"]({ - params: { sessionID: input["sessionID"] }, - payload: { id: input["id"], title: input["title"], metadata: input["metadata"], fields: input["fields"] }, - }).pipe( - Effect.mapError(mapClientError), - Effect.map((value) => value.data), - ) - -type Endpoint14_3Request = Parameters[0] -type Endpoint14_3Input = { - readonly sessionID: Endpoint14_3Request["params"]["sessionID"] - readonly formID: Endpoint14_3Request["params"]["formID"] -} -const Endpoint14_3 = (raw: RawClient["server.form"]) => (input: Endpoint14_3Input) => - raw["session.form.get"]({ params: { sessionID: input["sessionID"], formID: input["formID"] } }).pipe( - Effect.mapError(mapClientError), - Effect.map((value) => value.data), - ) - -type Endpoint14_4Request = Parameters[0] -type Endpoint14_4Input = { - readonly sessionID: Endpoint14_4Request["params"]["sessionID"] - readonly formID: Endpoint14_4Request["params"]["formID"] -} -const Endpoint14_4 = (raw: RawClient["server.form"]) => (input: Endpoint14_4Input) => - raw["session.form.state"]({ params: { sessionID: input["sessionID"], formID: input["formID"] } }).pipe( - Effect.mapError(mapClientError), - Effect.map((value) => value.data), - ) - -type Endpoint14_5Request = Parameters[0] -type Endpoint14_5Input = { - readonly sessionID: Endpoint14_5Request["params"]["sessionID"] - readonly formID: Endpoint14_5Request["params"]["formID"] - readonly answer: Endpoint14_5Request["payload"]["answer"] -} -const Endpoint14_5 = (raw: RawClient["server.form"]) => (input: Endpoint14_5Input) => - raw["session.form.reply"]({ - params: { sessionID: input["sessionID"], formID: input["formID"] }, - payload: { answer: input["answer"] }, - }).pipe(Effect.mapError(mapClientError)) - -type Endpoint14_6Request = Parameters[0] -type Endpoint14_6Input = { - readonly sessionID: Endpoint14_6Request["params"]["sessionID"] - readonly formID: Endpoint14_6Request["params"]["formID"] -} -const Endpoint14_6 = (raw: RawClient["server.form"]) => (input: Endpoint14_6Input) => - raw["session.form.cancel"]({ params: { sessionID: input["sessionID"], formID: input["formID"] } }).pipe( - Effect.mapError(mapClientError), - ) - -const adaptGroup14 = (raw: RawClient["server.form"]) => ({ - request: { list: Endpoint14_0(raw) }, - list: Endpoint14_1(raw), - create: Endpoint14_2(raw), - get: Endpoint14_3(raw), - state: Endpoint14_4(raw), - reply: Endpoint14_5(raw), - cancel: Endpoint14_6(raw), -}) - -type Endpoint15_0Request = Parameters[0] -type Endpoint15_0Input = { readonly location?: Endpoint15_0Request["query"]["location"] } -const Endpoint15_0 = (raw: RawClient["server.permission"]) => (input?: Endpoint15_0Input) => - raw["permission.request.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)) - -type Endpoint15_1Request = Parameters[0] -type Endpoint15_1Input = { readonly projectID?: Endpoint15_1Request["query"]["projectID"] } -const Endpoint15_1 = (raw: RawClient["server.permission"]) => (input?: Endpoint15_1Input) => - raw["permission.saved.list"]({ query: { projectID: input?.["projectID"] } }).pipe( - Effect.mapError(mapClientError), - Effect.map((value) => value.data), - ) - -type Endpoint15_2Request = Parameters[0] -type Endpoint15_2Input = { readonly id: Endpoint15_2Request["params"]["id"] } -const Endpoint15_2 = (raw: RawClient["server.permission"]) => (input: Endpoint15_2Input) => - raw["permission.saved.remove"]({ params: { id: input["id"] } }).pipe(Effect.mapError(mapClientError)) - -type Endpoint15_3Request = Parameters[0] -type Endpoint15_3Input = { - readonly sessionID: Endpoint15_3Request["params"]["sessionID"] - readonly id?: Endpoint15_3Request["payload"]["id"] - readonly action: Endpoint15_3Request["payload"]["action"] - readonly resources: Endpoint15_3Request["payload"]["resources"] - readonly save?: Endpoint15_3Request["payload"]["save"] - readonly metadata?: Endpoint15_3Request["payload"]["metadata"] - readonly source?: Endpoint15_3Request["payload"]["source"] - readonly agent?: Endpoint15_3Request["payload"]["agent"] -} -const Endpoint15_3 = (raw: RawClient["server.permission"]) => (input: Endpoint15_3Input) => - raw["session.permission.create"]({ - params: { sessionID: input["sessionID"] }, - payload: { - id: input["id"], - action: input["action"], - resources: input["resources"], - save: input["save"], - metadata: input["metadata"], - source: input["source"], - agent: input["agent"], - }, - }).pipe( - Effect.mapError(mapClientError), - Effect.map((value) => value.data), - ) - -type Endpoint15_4Request = Parameters[0] -type Endpoint15_4Input = { readonly sessionID: Endpoint15_4Request["params"]["sessionID"] } -const Endpoint15_4 = (raw: RawClient["server.permission"]) => (input: Endpoint15_4Input) => - raw["session.permission.list"]({ params: { sessionID: input["sessionID"] } }).pipe( - Effect.mapError(mapClientError), - Effect.map((value) => value.data), - ) - -type Endpoint15_5Request = Parameters[0] -type Endpoint15_5Input = { - readonly sessionID: Endpoint15_5Request["params"]["sessionID"] - readonly requestID: Endpoint15_5Request["params"]["requestID"] -} -const Endpoint15_5 = (raw: RawClient["server.permission"]) => (input: Endpoint15_5Input) => - raw["session.permission.get"]({ params: { sessionID: input["sessionID"], requestID: input["requestID"] } }).pipe( - Effect.mapError(mapClientError), - Effect.map((value) => value.data), - ) - -type Endpoint15_6Request = Parameters[0] -type Endpoint15_6Input = { - readonly sessionID: Endpoint15_6Request["params"]["sessionID"] - readonly requestID: Endpoint15_6Request["params"]["requestID"] - readonly reply: Endpoint15_6Request["payload"]["reply"] - readonly message?: Endpoint15_6Request["payload"]["message"] -} -const Endpoint15_6 = (raw: RawClient["server.permission"]) => (input: Endpoint15_6Input) => - raw["session.permission.reply"]({ - params: { sessionID: input["sessionID"], requestID: input["requestID"] }, - payload: { reply: input["reply"], message: input["message"] }, - }).pipe(Effect.mapError(mapClientError)) - -const adaptGroup15 = (raw: RawClient["server.permission"]) => ({ - request: { list: Endpoint15_0(raw) }, - saved: { list: Endpoint15_1(raw), remove: Endpoint15_2(raw) }, - create: Endpoint15_3(raw), - list: Endpoint15_4(raw), - get: Endpoint15_5(raw), - reply: Endpoint15_6(raw), -}) - -type Endpoint16_0Request = Parameters[0] -type Endpoint16_0Input = { - readonly location?: Endpoint16_0Request["query"]["location"] - readonly path?: Endpoint16_0Request["query"]["path"] -} -const Endpoint16_0 = (raw: RawClient["server.fs"]) => (input?: Endpoint16_0Input) => - raw["fs.list"]({ query: { location: input?.["location"], path: input?.["path"] } }).pipe( - Effect.mapError(mapClientError), - ) - -type Endpoint16_1Request = Parameters[0] -type Endpoint16_1Input = { - readonly location?: Endpoint16_1Request["query"]["location"] - readonly query: Endpoint16_1Request["query"]["query"] - readonly type?: Endpoint16_1Request["query"]["type"] - readonly limit?: Endpoint16_1Request["query"]["limit"] -} -const Endpoint16_1 = (raw: RawClient["server.fs"]) => (input: Endpoint16_1Input) => - raw["fs.find"]({ - query: { location: input["location"], query: input["query"], type: input["type"], limit: input["limit"] }, - }).pipe(Effect.mapError(mapClientError)) - -const adaptGroup16 = (raw: RawClient["server.fs"]) => ({ list: Endpoint16_0(raw), find: Endpoint16_1(raw) }) - -type Endpoint17_0Request = Parameters[0] -type Endpoint17_0Input = { readonly location?: Endpoint17_0Request["query"]["location"] } -const Endpoint17_0 = (raw: RawClient["server.command"]) => (input?: Endpoint17_0Input) => - raw["command.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)) - -const adaptGroup17 = (raw: RawClient["server.command"]) => ({ list: Endpoint17_0(raw) }) - -type Endpoint18_0Request = Parameters[0] -type Endpoint18_0Input = { readonly location?: Endpoint18_0Request["query"]["location"] } -const Endpoint18_0 = (raw: RawClient["server.skill"]) => (input?: Endpoint18_0Input) => - raw["skill.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)) - -const adaptGroup18 = (raw: RawClient["server.skill"]) => ({ list: Endpoint18_0(raw) }) - -const Endpoint19_0 = (raw: RawClient["server.event"]) => () => - Stream.unwrap( - raw["event.subscribe"]({}).pipe( - Effect.mapError(mapClientError), - Effect.map((stream) => stream.pipe(Stream.mapError(mapClientError))), - ), - ) - -const adaptGroup19 = (raw: RawClient["server.event"]) => ({ subscribe: Endpoint19_0(raw) }) - -type Endpoint20_0Request = Parameters[0] -type Endpoint20_0Input = { readonly location?: Endpoint20_0Request["query"]["location"] } -const Endpoint20_0 = (raw: RawClient["server.pty"]) => (input?: Endpoint20_0Input) => - raw["pty.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)) - -type Endpoint20_1Request = Parameters[0] -type Endpoint20_1Input = { - readonly location?: Endpoint20_1Request["query"]["location"] - readonly command?: Endpoint20_1Request["payload"]["command"] - readonly args?: Endpoint20_1Request["payload"]["args"] - readonly cwd?: Endpoint20_1Request["payload"]["cwd"] - readonly title?: Endpoint20_1Request["payload"]["title"] - readonly env?: Endpoint20_1Request["payload"]["env"] -} -const Endpoint20_1 = (raw: RawClient["server.pty"]) => (input?: Endpoint20_1Input) => - raw["pty.create"]({ - query: { location: input?.["location"] }, - payload: { - command: input?.["command"], - args: input?.["args"], - cwd: input?.["cwd"], - title: input?.["title"], - env: input?.["env"], - }, - }).pipe(Effect.mapError(mapClientError)) - -type Endpoint20_2Request = Parameters[0] -type Endpoint20_2Input = { - readonly ptyID: Endpoint20_2Request["params"]["ptyID"] - readonly location?: Endpoint20_2Request["query"]["location"] -} -const Endpoint20_2 = (raw: RawClient["server.pty"]) => (input: Endpoint20_2Input) => - raw["pty.get"]({ params: { ptyID: input["ptyID"] }, query: { location: input["location"] } }).pipe( - Effect.mapError(mapClientError), - ) - -type Endpoint20_3Request = Parameters[0] -type Endpoint20_3Input = { - readonly ptyID: Endpoint20_3Request["params"]["ptyID"] - readonly location?: Endpoint20_3Request["query"]["location"] - readonly title?: Endpoint20_3Request["payload"]["title"] - readonly size?: Endpoint20_3Request["payload"]["size"] -} -const Endpoint20_3 = (raw: RawClient["server.pty"]) => (input: Endpoint20_3Input) => - raw["pty.update"]({ - params: { ptyID: input["ptyID"] }, - query: { location: input["location"] }, - payload: { title: input["title"], size: input["size"] }, - }).pipe(Effect.mapError(mapClientError)) - -type Endpoint20_4Request = Parameters[0] -type Endpoint20_4Input = { - readonly ptyID: Endpoint20_4Request["params"]["ptyID"] - readonly location?: Endpoint20_4Request["query"]["location"] -} -const Endpoint20_4 = (raw: RawClient["server.pty"]) => (input: Endpoint20_4Input) => - raw["pty.remove"]({ params: { ptyID: input["ptyID"] }, query: { location: input["location"] } }).pipe( - Effect.mapError(mapClientError), - ) - -const adaptGroup20 = (raw: RawClient["server.pty"]) => ({ - list: Endpoint20_0(raw), - create: Endpoint20_1(raw), - get: Endpoint20_2(raw), - update: Endpoint20_3(raw), - remove: Endpoint20_4(raw), -}) - -type Endpoint21_0Request = Parameters[0] -type Endpoint21_0Input = { readonly location?: Endpoint21_0Request["query"]["location"] } -const Endpoint21_0 = (raw: RawClient["server.shell"]) => (input?: Endpoint21_0Input) => - raw["shell.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)) - -type Endpoint21_1Request = Parameters[0] -type Endpoint21_1Input = { - readonly location?: Endpoint21_1Request["query"]["location"] - readonly command: Endpoint21_1Request["payload"]["command"] - readonly cwd?: Endpoint21_1Request["payload"]["cwd"] - readonly timeout: Endpoint21_1Request["payload"]["timeout"] - readonly metadata?: Endpoint21_1Request["payload"]["metadata"] -} -const Endpoint21_1 = (raw: RawClient["server.shell"]) => (input: Endpoint21_1Input) => - raw["shell.create"]({ - query: { location: input["location"] }, - payload: { command: input["command"], cwd: input["cwd"], timeout: input["timeout"], metadata: input["metadata"] }, - }).pipe(Effect.mapError(mapClientError)) - -type Endpoint21_2Request = Parameters[0] -type Endpoint21_2Input = { - readonly id: Endpoint21_2Request["params"]["id"] - readonly location?: Endpoint21_2Request["query"]["location"] -} -const Endpoint21_2 = (raw: RawClient["server.shell"]) => (input: Endpoint21_2Input) => - raw["shell.get"]({ params: { id: input["id"] }, query: { location: input["location"] } }).pipe( - Effect.mapError(mapClientError), - ) - -type Endpoint21_3Request = Parameters[0] -type Endpoint21_3Input = { - readonly id: Endpoint21_3Request["params"]["id"] - readonly location?: Endpoint21_3Request["query"]["location"] - readonly timeout: Endpoint21_3Request["payload"]["timeout"] -} -const Endpoint21_3 = (raw: RawClient["server.shell"]) => (input: Endpoint21_3Input) => - raw["shell.timeout"]({ - params: { id: input["id"] }, - query: { location: input["location"] }, - payload: { timeout: input["timeout"] }, - }).pipe(Effect.mapError(mapClientError)) - -type Endpoint21_4Request = Parameters[0] -type Endpoint21_4Input = { - readonly id: Endpoint21_4Request["params"]["id"] - readonly location?: Endpoint21_4Request["query"]["location"] - readonly cursor?: Endpoint21_4Request["query"]["cursor"] - readonly limit?: Endpoint21_4Request["query"]["limit"] -} -const Endpoint21_4 = (raw: RawClient["server.shell"]) => (input: Endpoint21_4Input) => - raw["shell.output"]({ - params: { id: input["id"] }, - query: { location: input["location"], cursor: input["cursor"], limit: input["limit"] }, - }).pipe(Effect.mapError(mapClientError)) - -type Endpoint21_5Request = Parameters[0] -type Endpoint21_5Input = { - readonly id: Endpoint21_5Request["params"]["id"] - readonly location?: Endpoint21_5Request["query"]["location"] -} -const Endpoint21_5 = (raw: RawClient["server.shell"]) => (input: Endpoint21_5Input) => - raw["shell.remove"]({ params: { id: input["id"] }, query: { location: input["location"] } }).pipe( - Effect.mapError(mapClientError), - ) - -const adaptGroup21 = (raw: RawClient["server.shell"]) => ({ - list: Endpoint21_0(raw), - create: Endpoint21_1(raw), - get: Endpoint21_2(raw), - timeout: Endpoint21_3(raw), - output: Endpoint21_4(raw), - remove: Endpoint21_5(raw), -}) - -type Endpoint22_0Request = Parameters[0] -type Endpoint22_0Input = { readonly location?: Endpoint22_0Request["query"]["location"] } -const Endpoint22_0 = (raw: RawClient["server.question"]) => (input?: Endpoint22_0Input) => - raw["question.request.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)) - -type Endpoint22_1Request = Parameters[0] -type Endpoint22_1Input = { readonly sessionID: Endpoint22_1Request["params"]["sessionID"] } -const Endpoint22_1 = (raw: RawClient["server.question"]) => (input: Endpoint22_1Input) => - raw["session.question.list"]({ params: { sessionID: input["sessionID"] } }).pipe( - Effect.mapError(mapClientError), - Effect.map((value) => value.data), - ) - -type Endpoint22_2Request = Parameters[0] -type Endpoint22_2Input = { - readonly sessionID: Endpoint22_2Request["params"]["sessionID"] - readonly requestID: Endpoint22_2Request["params"]["requestID"] - readonly answers: Endpoint22_2Request["payload"]["answers"] -} -const Endpoint22_2 = (raw: RawClient["server.question"]) => (input: Endpoint22_2Input) => - raw["session.question.reply"]({ - params: { sessionID: input["sessionID"], requestID: input["requestID"] }, - payload: { answers: input["answers"] }, - }).pipe(Effect.mapError(mapClientError)) - -type Endpoint22_3Request = Parameters[0] -type Endpoint22_3Input = { - readonly sessionID: Endpoint22_3Request["params"]["sessionID"] - readonly requestID: Endpoint22_3Request["params"]["requestID"] -} -const Endpoint22_3 = (raw: RawClient["server.question"]) => (input: Endpoint22_3Input) => - raw["session.question.reject"]({ params: { sessionID: input["sessionID"], requestID: input["requestID"] } }).pipe( - Effect.mapError(mapClientError), - ) - -const adaptGroup22 = (raw: RawClient["server.question"]) => ({ - request: { list: Endpoint22_0(raw) }, - list: Endpoint22_1(raw), - reply: Endpoint22_2(raw), - reject: Endpoint22_3(raw), -}) - -type Endpoint23_0Request = Parameters[0] -type Endpoint23_0Input = { readonly location?: Endpoint23_0Request["query"]["location"] } -const Endpoint23_0 = (raw: RawClient["server.reference"]) => (input?: Endpoint23_0Input) => - raw["reference.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)) - -const adaptGroup23 = (raw: RawClient["server.reference"]) => ({ list: Endpoint23_0(raw) }) - -type Endpoint24_0Request = Parameters[0] -type Endpoint24_0Input = { - readonly projectID: Endpoint24_0Request["params"]["projectID"] - readonly location?: Endpoint24_0Request["query"]["location"] - readonly strategy: Endpoint24_0Request["payload"]["strategy"] - readonly directory: Endpoint24_0Request["payload"]["directory"] - readonly name?: Endpoint24_0Request["payload"]["name"] -} -const Endpoint24_0 = (raw: RawClient["server.projectCopy"]) => (input: Endpoint24_0Input) => - raw["projectCopy.create"]({ - params: { projectID: input["projectID"] }, - query: { location: input["location"] }, - payload: { strategy: input["strategy"], directory: input["directory"], name: input["name"] }, - }).pipe(Effect.mapError(mapClientError)) - -type Endpoint24_1Request = Parameters[0] -type Endpoint24_1Input = { - readonly projectID: Endpoint24_1Request["params"]["projectID"] - readonly location?: Endpoint24_1Request["query"]["location"] - readonly directory: Endpoint24_1Request["payload"]["directory"] - readonly force: Endpoint24_1Request["payload"]["force"] -} -const Endpoint24_1 = (raw: RawClient["server.projectCopy"]) => (input: Endpoint24_1Input) => - raw["projectCopy.remove"]({ - params: { projectID: input["projectID"] }, - query: { location: input["location"] }, - payload: { directory: input["directory"], force: input["force"] }, - }).pipe(Effect.mapError(mapClientError)) - -type Endpoint24_2Request = Parameters[0] -type Endpoint24_2Input = { - readonly projectID: Endpoint24_2Request["params"]["projectID"] - readonly location?: Endpoint24_2Request["query"]["location"] -} -const Endpoint24_2 = (raw: RawClient["server.projectCopy"]) => (input: Endpoint24_2Input) => - raw["projectCopy.refresh"]({ - params: { projectID: input["projectID"] }, - query: { location: input["location"] }, - }).pipe(Effect.mapError(mapClientError)) - -const adaptGroup24 = (raw: RawClient["server.projectCopy"]) => ({ - create: Endpoint24_0(raw), - remove: Endpoint24_1(raw), - refresh: Endpoint24_2(raw), -}) - -type Endpoint25_0Request = Parameters[0] -type Endpoint25_0Input = { readonly location?: Endpoint25_0Request["query"]["location"] } -const Endpoint25_0 = (raw: RawClient["server.vcs"]) => (input?: Endpoint25_0Input) => - raw["vcs.status"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)) - -type Endpoint25_1Request = Parameters[0] -type Endpoint25_1Input = { - readonly location?: Endpoint25_1Request["query"]["location"] - readonly mode: Endpoint25_1Request["query"]["mode"] - readonly context?: Endpoint25_1Request["query"]["context"] -} -const Endpoint25_1 = (raw: RawClient["server.vcs"]) => (input: Endpoint25_1Input) => - raw["vcs.diff"]({ query: { location: input["location"], mode: input["mode"], context: input["context"] } }).pipe( - Effect.mapError(mapClientError), - ) - -const adaptGroup25 = (raw: RawClient["server.vcs"]) => ({ status: Endpoint25_0(raw), diff: Endpoint25_1(raw) }) - -const Endpoint26_0 = (raw: RawClient["server.debug"]) => () => - raw["debug.location"]({}).pipe(Effect.mapError(mapClientError)) - -type Endpoint26_1Request = Parameters[0] -type Endpoint26_1Input = { readonly location?: Endpoint26_1Request["query"]["location"] } -const Endpoint26_1 = (raw: RawClient["server.debug"]) => (input?: Endpoint26_1Input) => - raw["debug.location.evict"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)) - -const adaptGroup26 = (raw: RawClient["server.debug"]) => ({ - location: { list: Endpoint26_0(raw), evict: Endpoint26_1(raw) }, -}) - -const adaptClient = (raw: RawClient) => ({ - health: adaptGroup0(raw["server.health"]), - server: adaptGroup1(raw["server.server"]), - location: adaptGroup2(raw["server.location"]), - agent: adaptGroup3(raw["server.agent"]), - plugin: adaptGroup4(raw["server.plugin"]), - session: adaptGroup5(raw["server.session"]), - message: adaptGroup6(raw["server.message"]), - model: adaptGroup7(raw["server.model"]), - generate: adaptGroup8(raw["server.generate"]), - provider: adaptGroup9(raw["server.provider"]), - integration: adaptGroup10(raw["server.integration"]), - mcp: adaptGroup11(raw["server.mcp"]), - credential: adaptGroup12(raw["server.credential"]), - project: adaptGroup13(raw["server.project"]), - form: adaptGroup14(raw["server.form"]), - permission: adaptGroup15(raw["server.permission"]), - file: adaptGroup16(raw["server.fs"]), - command: adaptGroup17(raw["server.command"]), - skill: adaptGroup18(raw["server.skill"]), - event: adaptGroup19(raw["server.event"]), - pty: adaptGroup20(raw["server.pty"]), - shell: adaptGroup21(raw["server.shell"]), - question: adaptGroup22(raw["server.question"]), - reference: adaptGroup23(raw["server.reference"]), - projectCopy: adaptGroup24(raw["server.projectCopy"]), - vcs: adaptGroup25(raw["server.vcs"]), - debug: adaptGroup26(raw["server.debug"]), -}) - -export const make = (options?: { readonly baseUrl?: URL | string }) => - HttpApiClient.make(ClientApi, options).pipe(Effect.map(adaptClient)) diff --git a/packages/client/src/effect/service.ts b/packages/client/src/effect/service.ts deleted file mode 100644 index f047d462bf..0000000000 --- a/packages/client/src/effect/service.ts +++ /dev/null @@ -1,217 +0,0 @@ -import { Effect, FileSystem, Option, Schedule, Schema } from "effect" -import { spawn } from "node:child_process" -import { homedir } from "node:os" -import { join } from "node:path" - -// Find, start, and stop the local opencode background service. -// -// The service daemon advertises itself through a registration file in the -// user's state directory: url, pid, version, and the private password, with -// 0600 permissions. That file is the complete discovery contract — reading it -// is all a client needs to connect. The daemon's own configuration (port, -// persisted password) is CLI-owned and never read here. - -export type Endpoint = { - readonly url: string - readonly auth?: { - readonly type: "basic" - readonly username: string - readonly password: string - } -} - -export type Options = { - // Absolute path to the service registration file. Defaults to - // opencode/service.json in the XDG state directory. - readonly file?: string - // When set, discovery only returns a server reporting this exact version, - // and start() replaces a healthy server whose version differs. - readonly version?: string - // Argv used to spawn the service. Defaults to ["opencode", "serve", - // "--service"] resolved from PATH. - readonly command?: ReadonlyArray -} - -export type StartReason = "missing" | "version-mismatch" - -export type StartOptions = Options & { - // Called once when start() decides it must spawn: either no service was - // found, or a healthy service with a different version is being replaced. - // `existing` carries the registration of the service being replaced. - readonly onStart?: (reason: StartReason, existing?: Info) => void -} - -// Read-only lookup: registration file plus health check and version gate. -// Never spawns; escalation to start() is the caller's policy. -export const discover = Effect.fn("service.discover")(function* (options: Options = {}) { - return (yield* discoverLocal(options))?.endpoint -}) - -const discoverLocal = Effect.fnUntraced(function* (options: Options) { - const info = yield* read(options.file) - if (info === undefined) return undefined - if (options.version !== undefined && info.version !== options.version) return undefined - return yield* probe(info, options.version) -}) - -// Idempotent ensure-running: reuses a healthy compatible server, replaces a -// version-mismatched one, and otherwise spawns the service command detached. -export const start = Effect.fn("service.start")(function* (options: StartOptions = {}) { - const compatible = yield* discover(options) - if (compatible !== undefined) return compatible - const mismatched = yield* find(options) - yield* Effect.sync(() => - options.onStart?.(mismatched === undefined ? "missing" : "version-mismatch", mismatched?.info), - ) - if (mismatched !== undefined) yield* kill(mismatched.info, options).pipe(Effect.ignore) - - const [command, ...args] = options.command ?? ["opencode", "serve", "--service"] - if (command === undefined) return yield* Effect.fail(new Error("Missing service command")) - const child = yield* Effect.try({ - try: () => { - const child = spawn(command, args, { detached: true, stdio: "ignore" }) - child.unref() - return child - }, - catch: (cause) => new Error("Failed to start server", { cause }), - }) - - return yield* discoverLocal(options).pipe( - Effect.flatMap((found) => - found === undefined ? Effect.fail(new Error("Server is not ready")) : Effect.succeed(found), - ), - Effect.retry(poll), - Effect.tap((found) => - found.info.pid === child.pid - ? Effect.void - : Effect.sync(() => { - child.kill("SIGTERM") - }), - ), - Effect.map((found) => found.endpoint), - Effect.tapError(() => Effect.try({ try: () => child.kill("SIGTERM"), catch: () => undefined }).pipe(Effect.ignore)), - Effect.mapError(() => new Error("Failed to start server")), - ) -}) - -export const stop = Effect.fn("service.stop")(function* (options: Options = {}) { - const fs = yield* FileSystem.FileSystem - const existing = yield* find(options) - if (existing !== undefined) yield* kill(existing.info, options) - yield* fs.remove(options.file ?? fallback()).pipe(Effect.ignore) -}) - -function fallback() { - const state = process.env["XDG_STATE_HOME"] ?? join(homedir(), ".local", "state") - return join(state, "opencode", "service.json") -} - -export function headers(endpoint: Endpoint): RequestInit["headers"] { - if (endpoint.auth === undefined) return undefined - return { authorization: "Basic " + btoa(endpoint.auth.username + ":" + endpoint.auth.password) } -} - -export const Info = Schema.Struct({ - id: Schema.optional(Schema.String), - version: Schema.optional(Schema.String), - url: Schema.String, - pid: Schema.Int.check(Schema.isGreaterThan(0)), - password: Schema.optional(Schema.String), -}) -export type Info = typeof Info.Type - -const decode = Schema.decodeUnknownEffect(Schema.fromJsonString(Info)) -const decodeHealth = Schema.decodeUnknownOption( - Schema.Struct({ healthy: Schema.Literal(true), version: Schema.String, pid: Schema.Int }), -) -const decodeLegacyHealth = Schema.decodeUnknownOption(Schema.Struct({ healthy: Schema.Literal(true) })) - -// A missing or corrupt file means no valid info; callers treat both -// the same (the registering server self-evicts, clients rediscover). -const read = Effect.fnUntraced(function* (file?: string) { - const fs = yield* FileSystem.FileSystem - const text = yield* fs.readFileString(file ?? fallback()).pipe(Effect.option) - if (Option.isNone(text)) return undefined - return yield* decode(text.value).pipe(Effect.option, Effect.map(Option.getOrUndefined)) -}) - -type LocalService = { - readonly info: Info - readonly endpoint: Endpoint -} - -const probe = Effect.fnUntraced(function* (info: Info, version?: string, allowLegacy = false) { - const endpoint = { - url: info.url, - auth: - info.password === undefined - ? undefined - : { type: "basic" as const, username: "opencode", password: info.password }, - } satisfies Endpoint - const response = yield* Effect.tryPromise(() => - fetch(new URL("/api/health", info.url), { - headers: headers(endpoint), - signal: AbortSignal.timeout(2_000), - }), - ).pipe(Effect.option, Effect.map(Option.getOrUndefined)) - if (response === undefined || !response.ok) return undefined - const body = yield* Effect.tryPromise(() => response.json()).pipe(Effect.option, Effect.map(Option.getOrUndefined)) - const health = decodeHealth(body) - if (Option.isSome(health)) { - if (health.value.pid !== info.pid) return undefined - if (info.version !== undefined && health.value.version !== info.version) return undefined - if (version !== undefined && health.value.version !== version) return undefined - return { info, endpoint } satisfies LocalService - } - if ( - !allowLegacy || - Option.isNone(decodeLegacyHealth(body)) || - (typeof body === "object" && body !== null && ("version" in body || "pid" in body)) - ) - return undefined - return { info, endpoint } satisfies LocalService -}) - -// Health-checked lookup without the version gate: lifecycle operations must be -// able to see (and replace or stop) a server from a different version. -const find = Effect.fnUntraced(function* (options: Options) { - const info = yield* read(options.file) - if (info === undefined) return undefined - return yield* probe(info, undefined, true) -}) - -// 50ms cadence bounded at ~5s, shared by stop escalation and start readiness. -const poll = Schedule.spaced("50 millis").pipe(Schedule.both(Schedule.recurs(100))) - -const signal = (pid: number, name: NodeJS.Signals) => - Effect.try({ try: () => process.kill(pid, name), catch: (cause) => cause }).pipe(Effect.ignore) - -const stopped = Effect.fnUntraced(function* (pid: number) { - const running = yield* Effect.try({ try: () => process.kill(pid, 0), catch: () => false }).pipe( - Effect.orElseSucceed(() => false), - ) - if (!running) return true - return yield* Effect.fail(new Error(`Server process ${pid} is still running`)) -}) - -function same(left: Info, right: Info) { - return left.id === right.id && left.version === right.version && left.url === right.url && left.pid === right.pid -} - -const kill = Effect.fnUntraced(function* (info: Info, options: Options) { - // A stale registration may point at a PID that has since been reused by - // another process. Only signal the PID after authenticating the server. - const current = yield* find(options) - if (current === undefined || !same(current.info, info)) return - - yield* signal(info.pid, "SIGTERM") - const done = yield* stopped(info.pid).pipe(Effect.retry(poll), Effect.option) - if (Option.isSome(done)) return - - const latest = yield* find(options) - if (latest === undefined || !same(latest.info, info)) return - yield* signal(info.pid, "SIGKILL") - yield* stopped(info.pid).pipe(Effect.retry(poll)) -}) - -export * as Service from "./service.js" diff --git a/packages/client/src/effect/generated/.httpapi-codegen.json b/packages/client/src/generated-effect/.httpapi-codegen.json similarity index 100% rename from packages/client/src/effect/generated/.httpapi-codegen.json rename to packages/client/src/generated-effect/.httpapi-codegen.json diff --git a/packages/client/src/effect/generated/client-error.ts b/packages/client/src/generated-effect/client-error.ts similarity index 100% rename from packages/client/src/effect/generated/client-error.ts rename to packages/client/src/generated-effect/client-error.ts diff --git a/packages/client/src/generated-effect/client.ts b/packages/client/src/generated-effect/client.ts new file mode 100644 index 0000000000..024c978280 --- /dev/null +++ b/packages/client/src/generated-effect/client.ts @@ -0,0 +1,706 @@ +// Generated by @opencode-ai/httpapi-codegen. Do not edit. +import { Effect, Stream, Schema } from "effect" +import { Sse } from "effect/unstable/encoding" +import { HttpClientError } from "effect/unstable/http" +import { HttpApiClient } from "effect/unstable/httpapi" +import { ClientApi } from "../contract" +import { ClientError } from "./client-error" + +type RawClient = HttpApiClient.ForApi + +const mapClientError = (error: E) => + HttpClientError.isHttpClientError(error) || Schema.isSchemaError(error) || Sse.Retry.is(error) + ? new ClientError({ cause: error }) + : error + +const Endpoint0_0 = (raw: RawClient["server.health"]) => () => + raw["health.get"]({}).pipe(Effect.mapError(mapClientError)) + +const adaptGroup0 = (raw: RawClient["server.health"]) => ({ get: Endpoint0_0(raw) }) + +type Endpoint1_0Request = Parameters[0] +type Endpoint1_0Input = { readonly location?: Endpoint1_0Request["query"]["location"] } +const Endpoint1_0 = (raw: RawClient["server.location"]) => (input?: Endpoint1_0Input) => + raw["location.get"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)) + +const adaptGroup1 = (raw: RawClient["server.location"]) => ({ get: Endpoint1_0(raw) }) + +type Endpoint2_0Request = Parameters[0] +type Endpoint2_0Input = { readonly location?: Endpoint2_0Request["query"]["location"] } +const Endpoint2_0 = (raw: RawClient["server.agent"]) => (input?: Endpoint2_0Input) => + raw["agent.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)) + +const adaptGroup2 = (raw: RawClient["server.agent"]) => ({ list: Endpoint2_0(raw) }) + +type Endpoint3_0Request = Parameters[0] +type Endpoint3_0Input = { + readonly workspace?: Endpoint3_0Request["query"]["workspace"] + readonly limit?: Endpoint3_0Request["query"]["limit"] + readonly order?: Endpoint3_0Request["query"]["order"] + readonly search?: Endpoint3_0Request["query"]["search"] + readonly directory?: Endpoint3_0Request["query"]["directory"] + readonly project?: Endpoint3_0Request["query"]["project"] + readonly subpath?: Endpoint3_0Request["query"]["subpath"] + readonly cursor?: Endpoint3_0Request["query"]["cursor"] +} +const Endpoint3_0 = (raw: RawClient["server.session"]) => (input?: Endpoint3_0Input) => + raw["session.list"]({ + query: { + workspace: input?.["workspace"], + limit: input?.["limit"], + order: input?.["order"], + search: input?.["search"], + directory: input?.["directory"], + project: input?.["project"], + subpath: input?.["subpath"], + cursor: input?.["cursor"], + }, + }).pipe(Effect.mapError(mapClientError)) + +type Endpoint3_1Request = Parameters[0] +type Endpoint3_1Input = { + readonly id?: Endpoint3_1Request["payload"]["id"] + readonly agent?: Endpoint3_1Request["payload"]["agent"] + readonly model?: Endpoint3_1Request["payload"]["model"] + readonly location?: Endpoint3_1Request["payload"]["location"] +} +const Endpoint3_1 = (raw: RawClient["server.session"]) => (input?: Endpoint3_1Input) => + raw["session.create"]({ + payload: { id: input?.["id"], agent: input?.["agent"], model: input?.["model"], location: input?.["location"] }, + }).pipe( + Effect.mapError(mapClientError), + Effect.map((value) => value.data), + ) + +const Endpoint3_2 = (raw: RawClient["server.session"]) => () => + raw["session.active"]({}).pipe( + Effect.mapError(mapClientError), + Effect.map((value) => value.data), + ) + +type Endpoint3_3Request = Parameters[0] +type Endpoint3_3Input = { readonly sessionID: Endpoint3_3Request["params"]["sessionID"] } +const Endpoint3_3 = (raw: RawClient["server.session"]) => (input: Endpoint3_3Input) => + raw["session.get"]({ params: { sessionID: input["sessionID"] } }).pipe( + Effect.mapError(mapClientError), + Effect.map((value) => value.data), + ) + +type Endpoint3_4Request = Parameters[0] +type Endpoint3_4Input = { + readonly sessionID: Endpoint3_4Request["params"]["sessionID"] + readonly agent: Endpoint3_4Request["payload"]["agent"] +} +const Endpoint3_4 = (raw: RawClient["server.session"]) => (input: Endpoint3_4Input) => + raw["session.switchAgent"]({ params: { sessionID: input["sessionID"] }, payload: { agent: input["agent"] } }).pipe( + Effect.mapError(mapClientError), + ) + +type Endpoint3_5Request = Parameters[0] +type Endpoint3_5Input = { + readonly sessionID: Endpoint3_5Request["params"]["sessionID"] + readonly model: Endpoint3_5Request["payload"]["model"] +} +const Endpoint3_5 = (raw: RawClient["server.session"]) => (input: Endpoint3_5Input) => + raw["session.switchModel"]({ params: { sessionID: input["sessionID"] }, payload: { model: input["model"] } }).pipe( + Effect.mapError(mapClientError), + ) + +type Endpoint3_6Request = Parameters[0] +type Endpoint3_6Input = { + readonly sessionID: Endpoint3_6Request["params"]["sessionID"] + readonly id?: Endpoint3_6Request["payload"]["id"] + readonly prompt: Endpoint3_6Request["payload"]["prompt"] + readonly delivery?: Endpoint3_6Request["payload"]["delivery"] + readonly resume?: Endpoint3_6Request["payload"]["resume"] +} +const Endpoint3_6 = (raw: RawClient["server.session"]) => (input: Endpoint3_6Input) => + raw["session.prompt"]({ + params: { sessionID: input["sessionID"] }, + payload: { id: input["id"], prompt: input["prompt"], delivery: input["delivery"], resume: input["resume"] }, + }).pipe( + Effect.mapError(mapClientError), + Effect.map((value) => value.data), + ) + +type Endpoint3_7Request = Parameters[0] +type Endpoint3_7Input = { readonly sessionID: Endpoint3_7Request["params"]["sessionID"] } +const Endpoint3_7 = (raw: RawClient["server.session"]) => (input: Endpoint3_7Input) => + raw["session.compact"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError)) + +type Endpoint3_8Request = Parameters[0] +type Endpoint3_8Input = { readonly sessionID: Endpoint3_8Request["params"]["sessionID"] } +const Endpoint3_8 = (raw: RawClient["server.session"]) => (input: Endpoint3_8Input) => + raw["session.wait"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError)) + +type Endpoint3_9Request = Parameters[0] +type Endpoint3_9Input = { + readonly sessionID: Endpoint3_9Request["params"]["sessionID"] + readonly messageID: Endpoint3_9Request["payload"]["messageID"] + readonly files?: Endpoint3_9Request["payload"]["files"] +} +const Endpoint3_9 = (raw: RawClient["server.session"]) => (input: Endpoint3_9Input) => + raw["session.revert.stage"]({ + params: { sessionID: input["sessionID"] }, + payload: { messageID: input["messageID"], files: input["files"] }, + }).pipe( + Effect.mapError(mapClientError), + Effect.map((value) => value.data), + ) + +type Endpoint3_10Request = Parameters[0] +type Endpoint3_10Input = { readonly sessionID: Endpoint3_10Request["params"]["sessionID"] } +const Endpoint3_10 = (raw: RawClient["server.session"]) => (input: Endpoint3_10Input) => + raw["session.revert.clear"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError)) + +type Endpoint3_11Request = Parameters[0] +type Endpoint3_11Input = { readonly sessionID: Endpoint3_11Request["params"]["sessionID"] } +const Endpoint3_11 = (raw: RawClient["server.session"]) => (input: Endpoint3_11Input) => + raw["session.revert.commit"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError)) + +type Endpoint3_12Request = Parameters[0] +type Endpoint3_12Input = { readonly sessionID: Endpoint3_12Request["params"]["sessionID"] } +const Endpoint3_12 = (raw: RawClient["server.session"]) => (input: Endpoint3_12Input) => + raw["session.context"]({ params: { sessionID: input["sessionID"] } }).pipe( + Effect.mapError(mapClientError), + Effect.map((value) => value.data), + ) + +type Endpoint3_13Request = Parameters[0] +type Endpoint3_13Input = { + readonly sessionID: Endpoint3_13Request["params"]["sessionID"] + readonly limit?: Endpoint3_13Request["query"]["limit"] + readonly after?: Endpoint3_13Request["query"]["after"] +} +const Endpoint3_13 = (raw: RawClient["server.session"]) => (input: Endpoint3_13Input) => + raw["session.history"]({ + params: { sessionID: input["sessionID"] }, + query: { limit: input["limit"], after: input["after"] }, + }).pipe(Effect.mapError(mapClientError)) + +type Endpoint3_14Request = Parameters[0] +type Endpoint3_14Input = { + readonly sessionID: Endpoint3_14Request["params"]["sessionID"] + readonly after?: Endpoint3_14Request["query"]["after"] +} +const Endpoint3_14 = (raw: RawClient["server.session"]) => (input: Endpoint3_14Input) => + Stream.unwrap( + raw["session.events"]({ params: { sessionID: input["sessionID"] }, query: { after: input["after"] } }).pipe( + Effect.mapError(mapClientError), + Effect.map((stream) => stream.pipe(Stream.mapError(mapClientError))), + ), + ) + +type Endpoint3_15Request = Parameters[0] +type Endpoint3_15Input = { readonly sessionID: Endpoint3_15Request["params"]["sessionID"] } +const Endpoint3_15 = (raw: RawClient["server.session"]) => (input: Endpoint3_15Input) => + raw["session.interrupt"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError)) + +type Endpoint3_16Request = Parameters[0] +type Endpoint3_16Input = { + readonly sessionID: Endpoint3_16Request["params"]["sessionID"] + readonly messageID: Endpoint3_16Request["params"]["messageID"] +} +const Endpoint3_16 = (raw: RawClient["server.session"]) => (input: Endpoint3_16Input) => + raw["session.message"]({ params: { sessionID: input["sessionID"], messageID: input["messageID"] } }).pipe( + Effect.mapError(mapClientError), + Effect.map((value) => value.data), + ) + +const adaptGroup3 = (raw: RawClient["server.session"]) => ({ + list: Endpoint3_0(raw), + create: Endpoint3_1(raw), + active: Endpoint3_2(raw), + get: Endpoint3_3(raw), + switchAgent: Endpoint3_4(raw), + switchModel: Endpoint3_5(raw), + prompt: Endpoint3_6(raw), + compact: Endpoint3_7(raw), + wait: Endpoint3_8(raw), + stage: Endpoint3_9(raw), + clear: Endpoint3_10(raw), + commit: Endpoint3_11(raw), + context: Endpoint3_12(raw), + history: Endpoint3_13(raw), + events: Endpoint3_14(raw), + interrupt: Endpoint3_15(raw), + message: Endpoint3_16(raw), +}) + +type Endpoint4_0Request = Parameters[0] +type Endpoint4_0Input = { + readonly sessionID: Endpoint4_0Request["params"]["sessionID"] + readonly limit?: Endpoint4_0Request["query"]["limit"] + readonly order?: Endpoint4_0Request["query"]["order"] + readonly cursor?: Endpoint4_0Request["query"]["cursor"] +} +const Endpoint4_0 = (raw: RawClient["server.message"]) => (input: Endpoint4_0Input) => + raw["session.messages"]({ + params: { sessionID: input["sessionID"] }, + query: { limit: input["limit"], order: input["order"], cursor: input["cursor"] }, + }).pipe(Effect.mapError(mapClientError)) + +const adaptGroup4 = (raw: RawClient["server.message"]) => ({ list: Endpoint4_0(raw) }) + +type Endpoint5_0Request = Parameters[0] +type Endpoint5_0Input = { readonly location?: Endpoint5_0Request["query"]["location"] } +const Endpoint5_0 = (raw: RawClient["server.model"]) => (input?: Endpoint5_0Input) => + raw["model.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)) + +const adaptGroup5 = (raw: RawClient["server.model"]) => ({ list: Endpoint5_0(raw) }) + +type Endpoint6_0Request = Parameters[0] +type Endpoint6_0Input = { readonly location?: Endpoint6_0Request["query"]["location"] } +const Endpoint6_0 = (raw: RawClient["server.provider"]) => (input?: Endpoint6_0Input) => + raw["provider.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)) + +type Endpoint6_1Request = Parameters[0] +type Endpoint6_1Input = { + readonly providerID: Endpoint6_1Request["params"]["providerID"] + readonly location?: Endpoint6_1Request["query"]["location"] +} +const Endpoint6_1 = (raw: RawClient["server.provider"]) => (input: Endpoint6_1Input) => + raw["provider.get"]({ params: { providerID: input["providerID"] }, query: { location: input["location"] } }).pipe( + Effect.mapError(mapClientError), + ) + +const adaptGroup6 = (raw: RawClient["server.provider"]) => ({ list: Endpoint6_0(raw), get: Endpoint6_1(raw) }) + +type Endpoint7_0Request = Parameters[0] +type Endpoint7_0Input = { readonly location?: Endpoint7_0Request["query"]["location"] } +const Endpoint7_0 = (raw: RawClient["server.integration"]) => (input?: Endpoint7_0Input) => + raw["integration.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)) + +type Endpoint7_1Request = Parameters[0] +type Endpoint7_1Input = { + readonly integrationID: Endpoint7_1Request["params"]["integrationID"] + readonly location?: Endpoint7_1Request["query"]["location"] +} +const Endpoint7_1 = (raw: RawClient["server.integration"]) => (input: Endpoint7_1Input) => + raw["integration.get"]({ + params: { integrationID: input["integrationID"] }, + query: { location: input["location"] }, + }).pipe(Effect.mapError(mapClientError)) + +type Endpoint7_2Request = Parameters[0] +type Endpoint7_2Input = { + readonly integrationID: Endpoint7_2Request["params"]["integrationID"] + readonly location?: Endpoint7_2Request["query"]["location"] + readonly key: Endpoint7_2Request["payload"]["key"] + readonly label?: Endpoint7_2Request["payload"]["label"] +} +const Endpoint7_2 = (raw: RawClient["server.integration"]) => (input: Endpoint7_2Input) => + raw["integration.connect.key"]({ + params: { integrationID: input["integrationID"] }, + query: { location: input["location"] }, + payload: { key: input["key"], label: input["label"] }, + }).pipe(Effect.mapError(mapClientError)) + +type Endpoint7_3Request = Parameters[0] +type Endpoint7_3Input = { + readonly integrationID: Endpoint7_3Request["params"]["integrationID"] + readonly location?: Endpoint7_3Request["query"]["location"] + readonly methodID: Endpoint7_3Request["payload"]["methodID"] + readonly inputs: Endpoint7_3Request["payload"]["inputs"] + readonly label?: Endpoint7_3Request["payload"]["label"] +} +const Endpoint7_3 = (raw: RawClient["server.integration"]) => (input: Endpoint7_3Input) => + raw["integration.connect.oauth"]({ + params: { integrationID: input["integrationID"] }, + query: { location: input["location"] }, + payload: { methodID: input["methodID"], inputs: input["inputs"], label: input["label"] }, + }).pipe(Effect.mapError(mapClientError)) + +type Endpoint7_4Request = Parameters[0] +type Endpoint7_4Input = { + readonly attemptID: Endpoint7_4Request["params"]["attemptID"] + readonly location?: Endpoint7_4Request["query"]["location"] +} +const Endpoint7_4 = (raw: RawClient["server.integration"]) => (input: Endpoint7_4Input) => + raw["integration.attempt.status"]({ + params: { attemptID: input["attemptID"] }, + query: { location: input["location"] }, + }).pipe(Effect.mapError(mapClientError)) + +type Endpoint7_5Request = Parameters[0] +type Endpoint7_5Input = { + readonly attemptID: Endpoint7_5Request["params"]["attemptID"] + readonly location?: Endpoint7_5Request["query"]["location"] + readonly code?: Endpoint7_5Request["payload"]["code"] +} +const Endpoint7_5 = (raw: RawClient["server.integration"]) => (input: Endpoint7_5Input) => + raw["integration.attempt.complete"]({ + params: { attemptID: input["attemptID"] }, + query: { location: input["location"] }, + payload: { code: input["code"] }, + }).pipe(Effect.mapError(mapClientError)) + +type Endpoint7_6Request = Parameters[0] +type Endpoint7_6Input = { + readonly attemptID: Endpoint7_6Request["params"]["attemptID"] + readonly location?: Endpoint7_6Request["query"]["location"] +} +const Endpoint7_6 = (raw: RawClient["server.integration"]) => (input: Endpoint7_6Input) => + raw["integration.attempt.cancel"]({ + params: { attemptID: input["attemptID"] }, + query: { location: input["location"] }, + }).pipe(Effect.mapError(mapClientError)) + +const adaptGroup7 = (raw: RawClient["server.integration"]) => ({ + list: Endpoint7_0(raw), + get: Endpoint7_1(raw), + connectKey: Endpoint7_2(raw), + connectOauth: Endpoint7_3(raw), + attemptStatus: Endpoint7_4(raw), + attemptComplete: Endpoint7_5(raw), + attemptCancel: Endpoint7_6(raw), +}) + +type Endpoint8_0Request = Parameters[0] +type Endpoint8_0Input = { + readonly credentialID: Endpoint8_0Request["params"]["credentialID"] + readonly location?: Endpoint8_0Request["query"]["location"] + readonly label: Endpoint8_0Request["payload"]["label"] +} +const Endpoint8_0 = (raw: RawClient["server.credential"]) => (input: Endpoint8_0Input) => + raw["credential.update"]({ + params: { credentialID: input["credentialID"] }, + query: { location: input["location"] }, + payload: { label: input["label"] }, + }).pipe(Effect.mapError(mapClientError)) + +type Endpoint8_1Request = Parameters[0] +type Endpoint8_1Input = { + readonly credentialID: Endpoint8_1Request["params"]["credentialID"] + readonly location?: Endpoint8_1Request["query"]["location"] +} +const Endpoint8_1 = (raw: RawClient["server.credential"]) => (input: Endpoint8_1Input) => + raw["credential.remove"]({ + params: { credentialID: input["credentialID"] }, + query: { location: input["location"] }, + }).pipe(Effect.mapError(mapClientError)) + +const adaptGroup8 = (raw: RawClient["server.credential"]) => ({ update: Endpoint8_0(raw), remove: Endpoint8_1(raw) }) + +type Endpoint9_0Request = Parameters[0] +type Endpoint9_0Input = { readonly location?: Endpoint9_0Request["query"]["location"] } +const Endpoint9_0 = (raw: RawClient["server.permission"]) => (input?: Endpoint9_0Input) => + raw["permission.request.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)) + +type Endpoint9_1Request = Parameters[0] +type Endpoint9_1Input = { readonly projectID?: Endpoint9_1Request["query"]["projectID"] } +const Endpoint9_1 = (raw: RawClient["server.permission"]) => (input?: Endpoint9_1Input) => + raw["permission.saved.list"]({ query: { projectID: input?.["projectID"] } }).pipe( + Effect.mapError(mapClientError), + Effect.map((value) => value.data), + ) + +type Endpoint9_2Request = Parameters[0] +type Endpoint9_2Input = { readonly id: Endpoint9_2Request["params"]["id"] } +const Endpoint9_2 = (raw: RawClient["server.permission"]) => (input: Endpoint9_2Input) => + raw["permission.saved.remove"]({ params: { id: input["id"] } }).pipe(Effect.mapError(mapClientError)) + +type Endpoint9_3Request = Parameters[0] +type Endpoint9_3Input = { + readonly sessionID: Endpoint9_3Request["params"]["sessionID"] + readonly id?: Endpoint9_3Request["payload"]["id"] + readonly action: Endpoint9_3Request["payload"]["action"] + readonly resources: Endpoint9_3Request["payload"]["resources"] + readonly save?: Endpoint9_3Request["payload"]["save"] + readonly metadata?: Endpoint9_3Request["payload"]["metadata"] + readonly source?: Endpoint9_3Request["payload"]["source"] + readonly agent?: Endpoint9_3Request["payload"]["agent"] +} +const Endpoint9_3 = (raw: RawClient["server.permission"]) => (input: Endpoint9_3Input) => + raw["session.permission.create"]({ + params: { sessionID: input["sessionID"] }, + payload: { + id: input["id"], + action: input["action"], + resources: input["resources"], + save: input["save"], + metadata: input["metadata"], + source: input["source"], + agent: input["agent"], + }, + }).pipe( + Effect.mapError(mapClientError), + Effect.map((value) => value.data), + ) + +type Endpoint9_4Request = Parameters[0] +type Endpoint9_4Input = { readonly sessionID: Endpoint9_4Request["params"]["sessionID"] } +const Endpoint9_4 = (raw: RawClient["server.permission"]) => (input: Endpoint9_4Input) => + raw["session.permission.list"]({ params: { sessionID: input["sessionID"] } }).pipe( + Effect.mapError(mapClientError), + Effect.map((value) => value.data), + ) + +type Endpoint9_5Request = Parameters[0] +type Endpoint9_5Input = { + readonly sessionID: Endpoint9_5Request["params"]["sessionID"] + readonly requestID: Endpoint9_5Request["params"]["requestID"] +} +const Endpoint9_5 = (raw: RawClient["server.permission"]) => (input: Endpoint9_5Input) => + raw["session.permission.get"]({ params: { sessionID: input["sessionID"], requestID: input["requestID"] } }).pipe( + Effect.mapError(mapClientError), + Effect.map((value) => value.data), + ) + +type Endpoint9_6Request = Parameters[0] +type Endpoint9_6Input = { + readonly sessionID: Endpoint9_6Request["params"]["sessionID"] + readonly requestID: Endpoint9_6Request["params"]["requestID"] + readonly reply: Endpoint9_6Request["payload"]["reply"] + readonly message?: Endpoint9_6Request["payload"]["message"] +} +const Endpoint9_6 = (raw: RawClient["server.permission"]) => (input: Endpoint9_6Input) => + raw["session.permission.reply"]({ + params: { sessionID: input["sessionID"], requestID: input["requestID"] }, + payload: { reply: input["reply"], message: input["message"] }, + }).pipe(Effect.mapError(mapClientError)) + +const adaptGroup9 = (raw: RawClient["server.permission"]) => ({ + listRequests: Endpoint9_0(raw), + listSaved: Endpoint9_1(raw), + removeSaved: Endpoint9_2(raw), + create: Endpoint9_3(raw), + list: Endpoint9_4(raw), + get: Endpoint9_5(raw), + reply: Endpoint9_6(raw), +}) + +type Endpoint10_0Request = Parameters[0] +type Endpoint10_0Input = { + readonly location?: Endpoint10_0Request["query"]["location"] + readonly path?: Endpoint10_0Request["query"]["path"] +} +const Endpoint10_0 = (raw: RawClient["server.fs"]) => (input?: Endpoint10_0Input) => + raw["fs.list"]({ query: { location: input?.["location"], path: input?.["path"] } }).pipe( + Effect.mapError(mapClientError), + ) + +type Endpoint10_1Request = Parameters[0] +type Endpoint10_1Input = { + readonly location?: Endpoint10_1Request["query"]["location"] + readonly query: Endpoint10_1Request["query"]["query"] + readonly type?: Endpoint10_1Request["query"]["type"] + readonly limit?: Endpoint10_1Request["query"]["limit"] +} +const Endpoint10_1 = (raw: RawClient["server.fs"]) => (input: Endpoint10_1Input) => + raw["fs.find"]({ + query: { location: input["location"], query: input["query"], type: input["type"], limit: input["limit"] }, + }).pipe(Effect.mapError(mapClientError)) + +const adaptGroup10 = (raw: RawClient["server.fs"]) => ({ list: Endpoint10_0(raw), find: Endpoint10_1(raw) }) + +type Endpoint11_0Request = Parameters[0] +type Endpoint11_0Input = { readonly location?: Endpoint11_0Request["query"]["location"] } +const Endpoint11_0 = (raw: RawClient["server.command"]) => (input?: Endpoint11_0Input) => + raw["command.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)) + +const adaptGroup11 = (raw: RawClient["server.command"]) => ({ list: Endpoint11_0(raw) }) + +type Endpoint12_0Request = Parameters[0] +type Endpoint12_0Input = { readonly location?: Endpoint12_0Request["query"]["location"] } +const Endpoint12_0 = (raw: RawClient["server.skill"]) => (input?: Endpoint12_0Input) => + raw["skill.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)) + +const adaptGroup12 = (raw: RawClient["server.skill"]) => ({ list: Endpoint12_0(raw) }) + +const Endpoint13_0 = (raw: RawClient["server.event"]) => () => + Stream.unwrap( + raw["event.subscribe"]({}).pipe( + Effect.mapError(mapClientError), + Effect.map((stream) => stream.pipe(Stream.mapError(mapClientError))), + ), + ) + +const adaptGroup13 = (raw: RawClient["server.event"]) => ({ subscribe: Endpoint13_0(raw) }) + +type Endpoint14_0Request = Parameters[0] +type Endpoint14_0Input = { readonly location?: Endpoint14_0Request["query"]["location"] } +const Endpoint14_0 = (raw: RawClient["server.pty"]) => (input?: Endpoint14_0Input) => + raw["pty.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)) + +type Endpoint14_1Request = Parameters[0] +type Endpoint14_1Input = { + readonly location?: Endpoint14_1Request["query"]["location"] + readonly command?: Endpoint14_1Request["payload"]["command"] + readonly args?: Endpoint14_1Request["payload"]["args"] + readonly cwd?: Endpoint14_1Request["payload"]["cwd"] + readonly title?: Endpoint14_1Request["payload"]["title"] + readonly env?: Endpoint14_1Request["payload"]["env"] +} +const Endpoint14_1 = (raw: RawClient["server.pty"]) => (input?: Endpoint14_1Input) => + raw["pty.create"]({ + query: { location: input?.["location"] }, + payload: { + command: input?.["command"], + args: input?.["args"], + cwd: input?.["cwd"], + title: input?.["title"], + env: input?.["env"], + }, + }).pipe(Effect.mapError(mapClientError)) + +type Endpoint14_2Request = Parameters[0] +type Endpoint14_2Input = { + readonly ptyID: Endpoint14_2Request["params"]["ptyID"] + readonly location?: Endpoint14_2Request["query"]["location"] +} +const Endpoint14_2 = (raw: RawClient["server.pty"]) => (input: Endpoint14_2Input) => + raw["pty.get"]({ params: { ptyID: input["ptyID"] }, query: { location: input["location"] } }).pipe( + Effect.mapError(mapClientError), + ) + +type Endpoint14_3Request = Parameters[0] +type Endpoint14_3Input = { + readonly ptyID: Endpoint14_3Request["params"]["ptyID"] + readonly location?: Endpoint14_3Request["query"]["location"] + readonly title?: Endpoint14_3Request["payload"]["title"] + readonly size?: Endpoint14_3Request["payload"]["size"] +} +const Endpoint14_3 = (raw: RawClient["server.pty"]) => (input: Endpoint14_3Input) => + raw["pty.update"]({ + params: { ptyID: input["ptyID"] }, + query: { location: input["location"] }, + payload: { title: input["title"], size: input["size"] }, + }).pipe(Effect.mapError(mapClientError)) + +type Endpoint14_4Request = Parameters[0] +type Endpoint14_4Input = { + readonly ptyID: Endpoint14_4Request["params"]["ptyID"] + readonly location?: Endpoint14_4Request["query"]["location"] +} +const Endpoint14_4 = (raw: RawClient["server.pty"]) => (input: Endpoint14_4Input) => + raw["pty.remove"]({ params: { ptyID: input["ptyID"] }, query: { location: input["location"] } }).pipe( + Effect.mapError(mapClientError), + ) + +const adaptGroup14 = (raw: RawClient["server.pty"]) => ({ + list: Endpoint14_0(raw), + create: Endpoint14_1(raw), + get: Endpoint14_2(raw), + update: Endpoint14_3(raw), + remove: Endpoint14_4(raw), +}) + +type Endpoint15_0Request = Parameters[0] +type Endpoint15_0Input = { readonly location?: Endpoint15_0Request["query"]["location"] } +const Endpoint15_0 = (raw: RawClient["server.question"]) => (input?: Endpoint15_0Input) => + raw["question.request.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)) + +type Endpoint15_1Request = Parameters[0] +type Endpoint15_1Input = { readonly sessionID: Endpoint15_1Request["params"]["sessionID"] } +const Endpoint15_1 = (raw: RawClient["server.question"]) => (input: Endpoint15_1Input) => + raw["session.question.list"]({ params: { sessionID: input["sessionID"] } }).pipe( + Effect.mapError(mapClientError), + Effect.map((value) => value.data), + ) + +type Endpoint15_2Request = Parameters[0] +type Endpoint15_2Input = { + readonly sessionID: Endpoint15_2Request["params"]["sessionID"] + readonly requestID: Endpoint15_2Request["params"]["requestID"] + readonly answers: Endpoint15_2Request["payload"]["answers"] +} +const Endpoint15_2 = (raw: RawClient["server.question"]) => (input: Endpoint15_2Input) => + raw["session.question.reply"]({ + params: { sessionID: input["sessionID"], requestID: input["requestID"] }, + payload: { answers: input["answers"] }, + }).pipe(Effect.mapError(mapClientError)) + +type Endpoint15_3Request = Parameters[0] +type Endpoint15_3Input = { + readonly sessionID: Endpoint15_3Request["params"]["sessionID"] + readonly requestID: Endpoint15_3Request["params"]["requestID"] +} +const Endpoint15_3 = (raw: RawClient["server.question"]) => (input: Endpoint15_3Input) => + raw["session.question.reject"]({ params: { sessionID: input["sessionID"], requestID: input["requestID"] } }).pipe( + Effect.mapError(mapClientError), + ) + +const adaptGroup15 = (raw: RawClient["server.question"]) => ({ + listRequests: Endpoint15_0(raw), + list: Endpoint15_1(raw), + reply: Endpoint15_2(raw), + reject: Endpoint15_3(raw), +}) + +type Endpoint16_0Request = Parameters[0] +type Endpoint16_0Input = { readonly location?: Endpoint16_0Request["query"]["location"] } +const Endpoint16_0 = (raw: RawClient["server.reference"]) => (input?: Endpoint16_0Input) => + raw["reference.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)) + +const adaptGroup16 = (raw: RawClient["server.reference"]) => ({ list: Endpoint16_0(raw) }) + +type Endpoint17_0Request = Parameters[0] +type Endpoint17_0Input = { + readonly projectID: Endpoint17_0Request["params"]["projectID"] + readonly location?: Endpoint17_0Request["query"]["location"] + readonly strategy: Endpoint17_0Request["payload"]["strategy"] + readonly directory: Endpoint17_0Request["payload"]["directory"] + readonly name?: Endpoint17_0Request["payload"]["name"] +} +const Endpoint17_0 = (raw: RawClient["server.projectCopy"]) => (input: Endpoint17_0Input) => + raw["projectCopy.create"]({ + params: { projectID: input["projectID"] }, + query: { location: input["location"] }, + payload: { strategy: input["strategy"], directory: input["directory"], name: input["name"] }, + }).pipe(Effect.mapError(mapClientError)) + +type Endpoint17_1Request = Parameters[0] +type Endpoint17_1Input = { + readonly projectID: Endpoint17_1Request["params"]["projectID"] + readonly location?: Endpoint17_1Request["query"]["location"] + readonly directory: Endpoint17_1Request["payload"]["directory"] + readonly force: Endpoint17_1Request["payload"]["force"] +} +const Endpoint17_1 = (raw: RawClient["server.projectCopy"]) => (input: Endpoint17_1Input) => + raw["projectCopy.remove"]({ + params: { projectID: input["projectID"] }, + query: { location: input["location"] }, + payload: { directory: input["directory"], force: input["force"] }, + }).pipe(Effect.mapError(mapClientError)) + +type Endpoint17_2Request = Parameters[0] +type Endpoint17_2Input = { + readonly projectID: Endpoint17_2Request["params"]["projectID"] + readonly location?: Endpoint17_2Request["query"]["location"] +} +const Endpoint17_2 = (raw: RawClient["server.projectCopy"]) => (input: Endpoint17_2Input) => + raw["projectCopy.refresh"]({ + params: { projectID: input["projectID"] }, + query: { location: input["location"] }, + }).pipe(Effect.mapError(mapClientError)) + +const adaptGroup17 = (raw: RawClient["server.projectCopy"]) => ({ + create: Endpoint17_0(raw), + remove: Endpoint17_1(raw), + refresh: Endpoint17_2(raw), +}) + +const adaptClient = (raw: RawClient) => ({ + health: adaptGroup0(raw["server.health"]), + location: adaptGroup1(raw["server.location"]), + agents: adaptGroup2(raw["server.agent"]), + sessions: adaptGroup3(raw["server.session"]), + messages: adaptGroup4(raw["server.message"]), + models: adaptGroup5(raw["server.model"]), + providers: adaptGroup6(raw["server.provider"]), + integrations: adaptGroup7(raw["server.integration"]), + credentials: adaptGroup8(raw["server.credential"]), + permissions: adaptGroup9(raw["server.permission"]), + files: adaptGroup10(raw["server.fs"]), + commands: adaptGroup11(raw["server.command"]), + skills: adaptGroup12(raw["server.skill"]), + events: adaptGroup13(raw["server.event"]), + ptys: adaptGroup14(raw["server.pty"]), + questions: adaptGroup15(raw["server.question"]), + references: adaptGroup16(raw["server.reference"]), + projectCopies: adaptGroup17(raw["server.projectCopy"]), +}) + +export const make = (options?: { readonly baseUrl?: URL | string }) => + HttpApiClient.make(ClientApi, options).pipe(Effect.map(adaptClient)) diff --git a/packages/client/src/effect/generated/index.ts b/packages/client/src/generated-effect/index.ts similarity index 100% rename from packages/client/src/effect/generated/index.ts rename to packages/client/src/generated-effect/index.ts diff --git a/packages/client/src/promise/generated/.httpapi-codegen.json b/packages/client/src/generated/.httpapi-codegen.json similarity index 100% rename from packages/client/src/promise/generated/.httpapi-codegen.json rename to packages/client/src/generated/.httpapi-codegen.json diff --git a/packages/client/src/promise/generated/client-error.ts b/packages/client/src/generated/client-error.ts similarity index 58% rename from packages/client/src/promise/generated/client-error.ts rename to packages/client/src/generated/client-error.ts index 930b612383..c278f0ddc8 100644 --- a/packages/client/src/promise/generated/client-error.ts +++ b/packages/client/src/generated/client-error.ts @@ -1,9 +1,4 @@ -export type ClientErrorReason = - | "Transport" - | "UnexpectedStatus" - | "UnsupportedContentType" - | "MalformedResponse" - | "SseEventTooLarge" +export type ClientErrorReason = "Transport" | "UnexpectedStatus" | "UnsupportedContentType" | "MalformedResponse" export class ClientError extends Error { override readonly name = "ClientError" diff --git a/packages/client/src/generated/client.ts b/packages/client/src/generated/client.ts new file mode 100644 index 0000000000..27ec3d81ba --- /dev/null +++ b/packages/client/src/generated/client.ts @@ -0,0 +1,1029 @@ +import type { + HealthGetOutput, + LocationGetInput, + LocationGetOutput, + AgentsListInput, + AgentsListOutput, + SessionsListInput, + SessionsListOutput, + SessionsCreateInput, + SessionsCreateOutput, + SessionsActiveOutput, + SessionsGetInput, + SessionsGetOutput, + SessionsSwitchAgentInput, + SessionsSwitchAgentOutput, + SessionsSwitchModelInput, + SessionsSwitchModelOutput, + SessionsPromptInput, + SessionsPromptOutput, + SessionsCompactInput, + SessionsCompactOutput, + SessionsWaitInput, + SessionsWaitOutput, + SessionsStageInput, + SessionsStageOutput, + SessionsClearInput, + SessionsClearOutput, + SessionsCommitInput, + SessionsCommitOutput, + SessionsContextInput, + SessionsContextOutput, + SessionsHistoryInput, + SessionsHistoryOutput, + SessionsEventsInput, + SessionsEventsOutput, + SessionsInterruptInput, + SessionsInterruptOutput, + SessionsMessageInput, + SessionsMessageOutput, + MessagesListInput, + MessagesListOutput, + ModelsListInput, + ModelsListOutput, + ProvidersListInput, + ProvidersListOutput, + ProvidersGetInput, + ProvidersGetOutput, + IntegrationsListInput, + IntegrationsListOutput, + IntegrationsGetInput, + IntegrationsGetOutput, + IntegrationsConnectKeyInput, + IntegrationsConnectKeyOutput, + IntegrationsConnectOauthInput, + IntegrationsConnectOauthOutput, + IntegrationsAttemptStatusInput, + IntegrationsAttemptStatusOutput, + IntegrationsAttemptCompleteInput, + IntegrationsAttemptCompleteOutput, + IntegrationsAttemptCancelInput, + IntegrationsAttemptCancelOutput, + CredentialsUpdateInput, + CredentialsUpdateOutput, + CredentialsRemoveInput, + CredentialsRemoveOutput, + PermissionsListRequestsInput, + PermissionsListRequestsOutput, + PermissionsListSavedInput, + PermissionsListSavedOutput, + PermissionsRemoveSavedInput, + PermissionsRemoveSavedOutput, + PermissionsCreateInput, + PermissionsCreateOutput, + PermissionsListInput, + PermissionsListOutput, + PermissionsGetInput, + PermissionsGetOutput, + PermissionsReplyInput, + PermissionsReplyOutput, + FilesListInput, + FilesListOutput, + FilesFindInput, + FilesFindOutput, + CommandsListInput, + CommandsListOutput, + SkillsListInput, + SkillsListOutput, + EventsSubscribeOutput, + PtysListInput, + PtysListOutput, + PtysCreateInput, + PtysCreateOutput, + PtysGetInput, + PtysGetOutput, + PtysUpdateInput, + PtysUpdateOutput, + PtysRemoveInput, + PtysRemoveOutput, + QuestionsListRequestsInput, + QuestionsListRequestsOutput, + QuestionsListInput, + QuestionsListOutput, + QuestionsReplyInput, + QuestionsReplyOutput, + QuestionsRejectInput, + QuestionsRejectOutput, + ReferencesListInput, + ReferencesListOutput, + ProjectCopiesCreateInput, + ProjectCopiesCreateOutput, + ProjectCopiesRemoveInput, + ProjectCopiesRemoveOutput, + ProjectCopiesRefreshInput, + ProjectCopiesRefreshOutput, +} from "./types" +import { ClientError } from "./client-error" + +export interface ClientOptions { + readonly baseUrl: string + readonly fetch?: typeof globalThis.fetch + readonly headers?: HeadersInit +} + +export interface RequestOptions { + readonly signal?: AbortSignal + readonly headers?: HeadersInit +} + +interface RequestDescriptor { + readonly method: string + readonly path: string + readonly query?: Record + readonly headers?: Record + readonly body?: unknown + readonly successStatus: number + readonly declaredStatuses: ReadonlyArray + readonly empty: boolean +} + +export function make(options: ClientOptions) { + const fetch = options.fetch ?? globalThis.fetch + + const prepare = (descriptor: RequestDescriptor, requestOptions?: RequestOptions) => { + const url = new URL(descriptor.path, options.baseUrl) + for (const [key, value] of Object.entries(descriptor.query ?? {})) appendQuery(url.searchParams, key, value) + const headers = new Headers(options.headers) + for (const [key, value] of Object.entries(descriptor.headers ?? {})) { + if (value !== undefined && value !== null) headers.set(key, String(value)) + } + for (const [key, value] of new Headers(requestOptions?.headers)) headers.set(key, value) + if (descriptor.body !== undefined && !headers.has("content-type")) headers.set("content-type", "application/json") + return { + url, + init: { + method: descriptor.method, + signal: requestOptions?.signal, + headers, + body: descriptor.body === undefined ? undefined : JSON.stringify(descriptor.body), + } satisfies RequestInit, + } + } + + const execute = async (descriptor: RequestDescriptor, requestOptions?: RequestOptions) => { + try { + const prepared = prepare(descriptor, requestOptions) + return await fetch(prepared.url, prepared.init) + } catch (cause) { + throw new ClientError("Transport", { cause }) + } + } + + const responseError = async (response: Response, descriptor: RequestDescriptor): Promise => { + if (descriptor.declaredStatuses.includes(response.status)) throw await json(response) + try { + await response.body?.cancel() + } catch {} + throw new ClientError("UnexpectedStatus", { cause: { status: response.status } }) + } + + const request = async (descriptor: RequestDescriptor, requestOptions?: RequestOptions): Promise => { + const response = await execute(descriptor, requestOptions) + if (response.status !== descriptor.successStatus) return responseError(response, descriptor) + if (descriptor.empty) { + try { + await response.body?.cancel() + } catch {} + return undefined as A + } + return (await json(response)) as A + } + + const sse = (descriptor: RequestDescriptor, requestOptions?: RequestOptions): AsyncIterable => ({ + async *[Symbol.asyncIterator]() { + const response = await execute(descriptor, requestOptions) + if (response.status !== descriptor.successStatus) await responseError(response, descriptor) + if (!isContentType(response, "text/event-stream")) { + try { + await response.body?.cancel() + } catch {} + throw new ClientError("UnsupportedContentType") + } + if (response.body === null) throw new ClientError("MalformedResponse") + const reader = response.body.getReader() + const decoder = new TextDecoder() + let buffer = "" + try { + while (true) { + let next + try { + next = await reader.read() + } catch (cause) { + throw new ClientError("Transport", { cause }) + } + buffer += decoder.decode(next.value, { stream: !next.done }) + if (buffer.length > 1_048_576) throw new ClientError("MalformedResponse") + const trailingCarriageReturn = !next.done && buffer.endsWith("\r") + if (trailingCarriageReturn) buffer = buffer.slice(0, -1) + buffer = buffer.replaceAll("\r\n", "\n").replaceAll("\r", "\n") + if (trailingCarriageReturn) buffer += "\r" + if (next.done && buffer !== "") buffer += "\n\n" + let boundary = buffer.indexOf("\n\n") + while (boundary >= 0) { + const block = buffer.slice(0, boundary) + buffer = buffer.slice(boundary + 2) + const data = block + .split("\n") + .flatMap((line) => (line.startsWith("data:") ? [line.slice(5).trimStart()] : [])) + .join("\n") + if (data !== "") { + try { + yield JSON.parse(data) as A + } catch (cause) { + throw new ClientError("MalformedResponse", { cause }) + } + } + boundary = buffer.indexOf("\n\n") + } + if (next.done) return + } + } finally { + try { + await reader.cancel() + } catch {} + reader.releaseLock() + } + }, + }) + + return { + health: { + get: (requestOptions?: RequestOptions) => + request( + { method: "GET", path: `/api/health`, successStatus: 200, declaredStatuses: [401, 400], empty: false }, + requestOptions, + ), + }, + location: { + get: (input?: LocationGetInput, requestOptions?: RequestOptions) => + request( + { + method: "GET", + path: `/api/location`, + query: { location: input?.["location"] }, + successStatus: 200, + declaredStatuses: [401, 400], + empty: false, + }, + requestOptions, + ), + }, + agents: { + list: (input?: AgentsListInput, requestOptions?: RequestOptions) => + request( + { + method: "GET", + path: `/api/agent`, + query: { location: input?.["location"] }, + successStatus: 200, + declaredStatuses: [401, 400], + empty: false, + }, + requestOptions, + ), + }, + sessions: { + list: (input?: SessionsListInput, requestOptions?: RequestOptions) => + request( + { + method: "GET", + path: `/api/session`, + query: { + workspace: input?.["workspace"], + limit: input?.["limit"], + order: input?.["order"], + search: input?.["search"], + directory: input?.["directory"], + project: input?.["project"], + subpath: input?.["subpath"], + cursor: input?.["cursor"], + }, + successStatus: 200, + declaredStatuses: [400, 401], + empty: false, + }, + requestOptions, + ), + create: (input?: SessionsCreateInput, requestOptions?: RequestOptions) => + request<{ readonly data: SessionsCreateOutput }>( + { + method: "POST", + path: `/api/session`, + body: { + id: input?.["id"], + agent: input?.["agent"], + model: input?.["model"], + location: input?.["location"], + }, + successStatus: 200, + declaredStatuses: [401, 400], + empty: false, + }, + requestOptions, + ).then((value) => value.data), + active: (requestOptions?: RequestOptions) => + request<{ readonly data: SessionsActiveOutput }>( + { + method: "GET", + path: `/api/session/active`, + successStatus: 200, + declaredStatuses: [401, 400], + empty: false, + }, + requestOptions, + ).then((value) => value.data), + get: (input: SessionsGetInput, requestOptions?: RequestOptions) => + request<{ readonly data: SessionsGetOutput }>( + { + method: "GET", + path: `/api/session/${encodeURIComponent(input.sessionID)}`, + successStatus: 200, + declaredStatuses: [404, 400, 401], + empty: false, + }, + requestOptions, + ).then((value) => value.data), + switchAgent: (input: SessionsSwitchAgentInput, requestOptions?: RequestOptions) => + request( + { + method: "POST", + path: `/api/session/${encodeURIComponent(input.sessionID)}/agent`, + body: { agent: input["agent"] }, + successStatus: 204, + declaredStatuses: [404, 400, 401], + empty: true, + }, + requestOptions, + ), + switchModel: (input: SessionsSwitchModelInput, requestOptions?: RequestOptions) => + request( + { + method: "POST", + path: `/api/session/${encodeURIComponent(input.sessionID)}/model`, + body: { model: input["model"] }, + successStatus: 204, + declaredStatuses: [404, 400, 401], + empty: true, + }, + requestOptions, + ), + prompt: (input: SessionsPromptInput, requestOptions?: RequestOptions) => + request<{ readonly data: SessionsPromptOutput }>( + { + method: "POST", + path: `/api/session/${encodeURIComponent(input.sessionID)}/prompt`, + body: { id: input["id"], prompt: input["prompt"], delivery: input["delivery"], resume: input["resume"] }, + successStatus: 200, + declaredStatuses: [409, 404, 400, 401], + empty: false, + }, + requestOptions, + ).then((value) => value.data), + compact: (input: SessionsCompactInput, requestOptions?: RequestOptions) => + request( + { + method: "POST", + path: `/api/session/${encodeURIComponent(input.sessionID)}/compact`, + successStatus: 204, + declaredStatuses: [404, 503, 400, 401], + empty: true, + }, + requestOptions, + ), + wait: (input: SessionsWaitInput, requestOptions?: RequestOptions) => + request( + { + method: "POST", + path: `/api/session/${encodeURIComponent(input.sessionID)}/wait`, + successStatus: 204, + declaredStatuses: [404, 503, 400, 401], + empty: true, + }, + requestOptions, + ), + stage: (input: SessionsStageInput, requestOptions?: RequestOptions) => + request<{ readonly data: SessionsStageOutput }>( + { + method: "POST", + path: `/api/session/${encodeURIComponent(input.sessionID)}/revert/stage`, + body: { messageID: input["messageID"], files: input["files"] }, + successStatus: 200, + declaredStatuses: [404, 500, 400, 401], + empty: false, + }, + requestOptions, + ).then((value) => value.data), + clear: (input: SessionsClearInput, requestOptions?: RequestOptions) => + request( + { + method: "POST", + path: `/api/session/${encodeURIComponent(input.sessionID)}/revert/clear`, + successStatus: 204, + declaredStatuses: [404, 500, 400, 401], + empty: true, + }, + requestOptions, + ), + commit: (input: SessionsCommitInput, requestOptions?: RequestOptions) => + request( + { + method: "POST", + path: `/api/session/${encodeURIComponent(input.sessionID)}/revert/commit`, + successStatus: 204, + declaredStatuses: [404, 400, 401], + empty: true, + }, + requestOptions, + ), + context: (input: SessionsContextInput, requestOptions?: RequestOptions) => + request<{ readonly data: SessionsContextOutput }>( + { + method: "GET", + path: `/api/session/${encodeURIComponent(input.sessionID)}/context`, + successStatus: 200, + declaredStatuses: [404, 500, 400, 401], + empty: false, + }, + requestOptions, + ).then((value) => value.data), + history: (input: SessionsHistoryInput, requestOptions?: RequestOptions) => + request( + { + method: "GET", + path: `/api/session/${encodeURIComponent(input.sessionID)}/history`, + query: { limit: input["limit"], after: input["after"] }, + successStatus: 200, + declaredStatuses: [404, 400, 401], + empty: false, + }, + requestOptions, + ), + events: (input: SessionsEventsInput, requestOptions?: RequestOptions): AsyncIterable => + sse( + { + method: "GET", + path: `/api/session/${encodeURIComponent(input.sessionID)}/event`, + query: { after: input["after"] }, + successStatus: 200, + declaredStatuses: [404, 400, 401], + empty: false, + }, + requestOptions, + ), + interrupt: (input: SessionsInterruptInput, requestOptions?: RequestOptions) => + request( + { + method: "POST", + path: `/api/session/${encodeURIComponent(input.sessionID)}/interrupt`, + successStatus: 204, + declaredStatuses: [404, 400, 401], + empty: true, + }, + requestOptions, + ), + message: (input: SessionsMessageInput, requestOptions?: RequestOptions) => + request<{ readonly data: SessionsMessageOutput }>( + { + method: "GET", + path: `/api/session/${encodeURIComponent(input.sessionID)}/message/${encodeURIComponent(input.messageID)}`, + successStatus: 200, + declaredStatuses: [404, 400, 401], + empty: false, + }, + requestOptions, + ).then((value) => value.data), + }, + messages: { + list: (input: MessagesListInput, requestOptions?: RequestOptions) => + request( + { + method: "GET", + path: `/api/session/${encodeURIComponent(input.sessionID)}/message`, + query: { limit: input["limit"], order: input["order"], cursor: input["cursor"] }, + successStatus: 200, + declaredStatuses: [400, 404, 500, 401], + empty: false, + }, + requestOptions, + ), + }, + models: { + list: (input?: ModelsListInput, requestOptions?: RequestOptions) => + request( + { + method: "GET", + path: `/api/model`, + query: { location: input?.["location"] }, + successStatus: 200, + declaredStatuses: [503, 401, 400], + empty: false, + }, + requestOptions, + ), + }, + providers: { + list: (input?: ProvidersListInput, requestOptions?: RequestOptions) => + request( + { + method: "GET", + path: `/api/provider`, + query: { location: input?.["location"] }, + successStatus: 200, + declaredStatuses: [503, 401, 400], + empty: false, + }, + requestOptions, + ), + get: (input: ProvidersGetInput, requestOptions?: RequestOptions) => + request( + { + method: "GET", + path: `/api/provider/${encodeURIComponent(input.providerID)}`, + query: { location: input["location"] }, + successStatus: 200, + declaredStatuses: [404, 503, 401, 400], + empty: false, + }, + requestOptions, + ), + }, + integrations: { + list: (input?: IntegrationsListInput, requestOptions?: RequestOptions) => + request( + { + method: "GET", + path: `/api/integration`, + query: { location: input?.["location"] }, + successStatus: 200, + declaredStatuses: [401, 400], + empty: false, + }, + requestOptions, + ), + get: (input: IntegrationsGetInput, requestOptions?: RequestOptions) => + request( + { + method: "GET", + path: `/api/integration/${encodeURIComponent(input.integrationID)}`, + query: { location: input["location"] }, + successStatus: 200, + declaredStatuses: [401, 400], + empty: false, + }, + requestOptions, + ), + connectKey: (input: IntegrationsConnectKeyInput, requestOptions?: RequestOptions) => + request( + { + method: "POST", + path: `/api/integration/${encodeURIComponent(input.integrationID)}/connect/key`, + query: { location: input["location"] }, + body: { key: input["key"], label: input["label"] }, + successStatus: 204, + declaredStatuses: [400, 401], + empty: true, + }, + requestOptions, + ), + connectOauth: (input: IntegrationsConnectOauthInput, requestOptions?: RequestOptions) => + request( + { + method: "POST", + path: `/api/integration/${encodeURIComponent(input.integrationID)}/connect/oauth`, + query: { location: input["location"] }, + body: { methodID: input["methodID"], inputs: input["inputs"], label: input["label"] }, + successStatus: 200, + declaredStatuses: [400, 401], + empty: false, + }, + requestOptions, + ), + attemptStatus: (input: IntegrationsAttemptStatusInput, requestOptions?: RequestOptions) => + request( + { + method: "GET", + path: `/api/integration/attempt/${encodeURIComponent(input.attemptID)}`, + query: { location: input["location"] }, + successStatus: 200, + declaredStatuses: [401, 400], + empty: false, + }, + requestOptions, + ), + attemptComplete: (input: IntegrationsAttemptCompleteInput, requestOptions?: RequestOptions) => + request( + { + method: "POST", + path: `/api/integration/attempt/${encodeURIComponent(input.attemptID)}/complete`, + query: { location: input["location"] }, + body: { code: input["code"] }, + successStatus: 204, + declaredStatuses: [400, 401], + empty: true, + }, + requestOptions, + ), + attemptCancel: (input: IntegrationsAttemptCancelInput, requestOptions?: RequestOptions) => + request( + { + method: "DELETE", + path: `/api/integration/attempt/${encodeURIComponent(input.attemptID)}`, + query: { location: input["location"] }, + successStatus: 204, + declaredStatuses: [401, 400], + empty: true, + }, + requestOptions, + ), + }, + credentials: { + update: (input: CredentialsUpdateInput, requestOptions?: RequestOptions) => + request( + { + method: "PATCH", + path: `/api/credential/${encodeURIComponent(input.credentialID)}`, + query: { location: input["location"] }, + body: { label: input["label"] }, + successStatus: 204, + declaredStatuses: [401, 400], + empty: true, + }, + requestOptions, + ), + remove: (input: CredentialsRemoveInput, requestOptions?: RequestOptions) => + request( + { + method: "DELETE", + path: `/api/credential/${encodeURIComponent(input.credentialID)}`, + query: { location: input["location"] }, + successStatus: 204, + declaredStatuses: [401, 400], + empty: true, + }, + requestOptions, + ), + }, + permissions: { + listRequests: (input?: PermissionsListRequestsInput, requestOptions?: RequestOptions) => + request( + { + method: "GET", + path: `/api/permission/request`, + query: { location: input?.["location"] }, + successStatus: 200, + declaredStatuses: [401, 400], + empty: false, + }, + requestOptions, + ), + listSaved: (input?: PermissionsListSavedInput, requestOptions?: RequestOptions) => + request<{ readonly data: PermissionsListSavedOutput }>( + { + method: "GET", + path: `/api/permission/saved`, + query: { projectID: input?.["projectID"] }, + successStatus: 200, + declaredStatuses: [401, 400], + empty: false, + }, + requestOptions, + ).then((value) => value.data), + removeSaved: (input: PermissionsRemoveSavedInput, requestOptions?: RequestOptions) => + request( + { + method: "DELETE", + path: `/api/permission/saved/${encodeURIComponent(input.id)}`, + successStatus: 204, + declaredStatuses: [401, 400], + empty: true, + }, + requestOptions, + ), + create: (input: PermissionsCreateInput, requestOptions?: RequestOptions) => + request<{ readonly data: PermissionsCreateOutput }>( + { + method: "POST", + path: `/api/session/${encodeURIComponent(input.sessionID)}/permission`, + body: { + id: input["id"], + action: input["action"], + resources: input["resources"], + save: input["save"], + metadata: input["metadata"], + source: input["source"], + agent: input["agent"], + }, + successStatus: 200, + declaredStatuses: [404, 400, 401], + empty: false, + }, + requestOptions, + ).then((value) => value.data), + list: (input: PermissionsListInput, requestOptions?: RequestOptions) => + request<{ readonly data: PermissionsListOutput }>( + { + method: "GET", + path: `/api/session/${encodeURIComponent(input.sessionID)}/permission`, + successStatus: 200, + declaredStatuses: [404, 400, 401], + empty: false, + }, + requestOptions, + ).then((value) => value.data), + get: (input: PermissionsGetInput, requestOptions?: RequestOptions) => + request<{ readonly data: PermissionsGetOutput }>( + { + method: "GET", + path: `/api/session/${encodeURIComponent(input.sessionID)}/permission/${encodeURIComponent(input.requestID)}`, + successStatus: 200, + declaredStatuses: [404, 400, 401], + empty: false, + }, + requestOptions, + ).then((value) => value.data), + reply: (input: PermissionsReplyInput, requestOptions?: RequestOptions) => + request( + { + method: "POST", + path: `/api/session/${encodeURIComponent(input.sessionID)}/permission/${encodeURIComponent(input.requestID)}/reply`, + body: { reply: input["reply"], message: input["message"] }, + successStatus: 204, + declaredStatuses: [404, 400, 401], + empty: true, + }, + requestOptions, + ), + }, + files: { + list: (input?: FilesListInput, requestOptions?: RequestOptions) => + request( + { + method: "GET", + path: `/api/fs/list`, + query: { location: input?.["location"], path: input?.["path"] }, + successStatus: 200, + declaredStatuses: [401, 400], + empty: false, + }, + requestOptions, + ), + find: (input: FilesFindInput, requestOptions?: RequestOptions) => + request( + { + method: "GET", + path: `/api/fs/find`, + query: { location: input["location"], query: input["query"], type: input["type"], limit: input["limit"] }, + successStatus: 200, + declaredStatuses: [401, 400], + empty: false, + }, + requestOptions, + ), + }, + commands: { + list: (input?: CommandsListInput, requestOptions?: RequestOptions) => + request( + { + method: "GET", + path: `/api/command`, + query: { location: input?.["location"] }, + successStatus: 200, + declaredStatuses: [401, 400], + empty: false, + }, + requestOptions, + ), + }, + skills: { + list: (input?: SkillsListInput, requestOptions?: RequestOptions) => + request( + { + method: "GET", + path: `/api/skill`, + query: { location: input?.["location"] }, + successStatus: 200, + declaredStatuses: [401, 400], + empty: false, + }, + requestOptions, + ), + }, + events: { + subscribe: (requestOptions?: RequestOptions): AsyncIterable => + sse( + { method: "GET", path: `/api/event`, successStatus: 200, declaredStatuses: [401, 400], empty: false }, + requestOptions, + ), + }, + ptys: { + list: (input?: PtysListInput, requestOptions?: RequestOptions) => + request( + { + method: "GET", + path: `/api/pty`, + query: { location: input?.["location"] }, + successStatus: 200, + declaredStatuses: [401, 400], + empty: false, + }, + requestOptions, + ), + create: (input?: PtysCreateInput, requestOptions?: RequestOptions) => + request( + { + method: "POST", + path: `/api/pty`, + query: { location: input?.["location"] }, + body: { + command: input?.["command"], + args: input?.["args"], + cwd: input?.["cwd"], + title: input?.["title"], + env: input?.["env"], + }, + successStatus: 200, + declaredStatuses: [401, 400], + empty: false, + }, + requestOptions, + ), + get: (input: PtysGetInput, requestOptions?: RequestOptions) => + request( + { + method: "GET", + path: `/api/pty/${encodeURIComponent(input.ptyID)}`, + query: { location: input["location"] }, + successStatus: 200, + declaredStatuses: [404, 401, 400], + empty: false, + }, + requestOptions, + ), + update: (input: PtysUpdateInput, requestOptions?: RequestOptions) => + request( + { + method: "PUT", + path: `/api/pty/${encodeURIComponent(input.ptyID)}`, + query: { location: input["location"] }, + body: { title: input["title"], size: input["size"] }, + successStatus: 200, + declaredStatuses: [404, 401, 400], + empty: false, + }, + requestOptions, + ), + remove: (input: PtysRemoveInput, requestOptions?: RequestOptions) => + request( + { + method: "DELETE", + path: `/api/pty/${encodeURIComponent(input.ptyID)}`, + query: { location: input["location"] }, + successStatus: 204, + declaredStatuses: [404, 401, 400], + empty: true, + }, + requestOptions, + ), + }, + questions: { + listRequests: (input?: QuestionsListRequestsInput, requestOptions?: RequestOptions) => + request( + { + method: "GET", + path: `/api/question/request`, + query: { location: input?.["location"] }, + successStatus: 200, + declaredStatuses: [401, 400], + empty: false, + }, + requestOptions, + ), + list: (input: QuestionsListInput, requestOptions?: RequestOptions) => + request<{ readonly data: QuestionsListOutput }>( + { + method: "GET", + path: `/api/session/${encodeURIComponent(input.sessionID)}/question`, + successStatus: 200, + declaredStatuses: [404, 400, 401], + empty: false, + }, + requestOptions, + ).then((value) => value.data), + reply: (input: QuestionsReplyInput, requestOptions?: RequestOptions) => + request( + { + method: "POST", + path: `/api/session/${encodeURIComponent(input.sessionID)}/question/${encodeURIComponent(input.requestID)}/reply`, + body: { answers: input["answers"] }, + successStatus: 204, + declaredStatuses: [404, 400, 401], + empty: true, + }, + requestOptions, + ), + reject: (input: QuestionsRejectInput, requestOptions?: RequestOptions) => + request( + { + method: "POST", + path: `/api/session/${encodeURIComponent(input.sessionID)}/question/${encodeURIComponent(input.requestID)}/reject`, + successStatus: 204, + declaredStatuses: [404, 400, 401], + empty: true, + }, + requestOptions, + ), + }, + references: { + list: (input?: ReferencesListInput, requestOptions?: RequestOptions) => + request( + { + method: "GET", + path: `/api/reference`, + query: { location: input?.["location"] }, + successStatus: 200, + declaredStatuses: [401, 400], + empty: false, + }, + requestOptions, + ), + }, + projectCopies: { + create: (input: ProjectCopiesCreateInput, requestOptions?: RequestOptions) => + request( + { + method: "POST", + path: `/experimental/project/${encodeURIComponent(input.projectID)}/copy`, + query: { location: input["location"] }, + body: { strategy: input["strategy"], directory: input["directory"], name: input["name"] }, + successStatus: 200, + declaredStatuses: [400, 401], + empty: false, + }, + requestOptions, + ), + remove: (input: ProjectCopiesRemoveInput, requestOptions?: RequestOptions) => + request( + { + method: "DELETE", + path: `/experimental/project/${encodeURIComponent(input.projectID)}/copy`, + query: { location: input["location"] }, + body: { directory: input["directory"], force: input["force"] }, + successStatus: 204, + declaredStatuses: [400, 401], + empty: true, + }, + requestOptions, + ), + refresh: (input: ProjectCopiesRefreshInput, requestOptions?: RequestOptions) => + request( + { + method: "POST", + path: `/experimental/project/${encodeURIComponent(input.projectID)}/copy/refresh`, + query: { location: input["location"] }, + successStatus: 204, + declaredStatuses: [400, 401], + empty: true, + }, + requestOptions, + ), + }, + } +} + +function appendQuery(params: URLSearchParams, key: string, value: unknown): void { + if (value === undefined || value === null) return + if (Array.isArray(value)) { + for (const item of value) appendQuery(params, key, item) + return + } + if (typeof value === "object") { + for (const [child, item] of Object.entries(value)) appendQuery(params, `${key}[${child}]`, item) + return + } + params.append(key, String(value)) +} + +async function json(response: Response): Promise { + if (!isContentType(response, "application/json") && !response.headers.get("content-type")?.includes("+json")) { + try { + await response.body?.cancel() + } catch {} + throw new ClientError("UnsupportedContentType") + } + let text: string + try { + text = await response.text() + } catch (cause) { + throw new ClientError("Transport", { cause }) + } + if (text === "") throw new ClientError("MalformedResponse") + try { + return JSON.parse(text) + } catch (cause) { + throw new ClientError("MalformedResponse", { cause }) + } +} + +function isContentType(response: Response, expected: string) { + return response.headers.get("content-type")?.split(";", 1)[0]?.trim().toLowerCase() === expected +} diff --git a/packages/client/src/promise/generated/index.ts b/packages/client/src/generated/index.ts similarity index 100% rename from packages/client/src/promise/generated/index.ts rename to packages/client/src/generated/index.ts diff --git a/packages/client/src/generated/types.ts b/packages/client/src/generated/types.ts new file mode 100644 index 0000000000..3b3188c874 --- /dev/null +++ b/packages/client/src/generated/types.ts @@ -0,0 +1,2807 @@ +import type { OpenCodeEventEncoded } from "@opencode-ai/protocol/groups/event" + +export type JsonValue = + | null + | boolean + | number + | string + | ReadonlyArray + | { readonly [key: string]: JsonValue } + +export type UnauthorizedError = { readonly _tag: "UnauthorizedError"; readonly message: string } +export const isUnauthorizedError = (value: unknown): value is UnauthorizedError => + typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "UnauthorizedError" + +export type InvalidRequestError = { + readonly _tag: "InvalidRequestError" + readonly message: string + readonly kind?: string | undefined + readonly field?: string | undefined +} +export const isInvalidRequestError = (value: unknown): value is InvalidRequestError => + typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "InvalidRequestError" + +export type InvalidCursorError = { readonly _tag: "InvalidCursorError"; readonly message: string } +export const isInvalidCursorError = (value: unknown): value is InvalidCursorError => + typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "InvalidCursorError" + +export type SessionNotFoundError = { + readonly _tag: "SessionNotFoundError" + readonly sessionID: string + readonly message: string +} +export const isSessionNotFoundError = (value: unknown): value is SessionNotFoundError => + typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "SessionNotFoundError" + +export type ConflictError = { + readonly _tag: "ConflictError" + readonly message: string + readonly resource?: string | undefined +} +export const isConflictError = (value: unknown): value is ConflictError => + typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "ConflictError" + +export type ServiceUnavailableError = { + readonly _tag: "ServiceUnavailableError" + readonly message: string + readonly service?: string | undefined +} +export const isServiceUnavailableError = (value: unknown): value is ServiceUnavailableError => + typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "ServiceUnavailableError" + +export type MessageNotFoundError = { + readonly _tag: "MessageNotFoundError" + readonly sessionID: string + readonly messageID: string + readonly message: string +} +export const isMessageNotFoundError = (value: unknown): value is MessageNotFoundError => + typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "MessageNotFoundError" + +export type UnknownError = { + readonly _tag: "UnknownError" + readonly message: string + readonly ref?: string | undefined +} +export const isUnknownError = (value: unknown): value is UnknownError => + typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "UnknownError" + +export type ProviderNotFoundError = { + readonly _tag: "ProviderNotFoundError" + readonly providerID: string + readonly message: string +} +export const isProviderNotFoundError = (value: unknown): value is ProviderNotFoundError => + typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "ProviderNotFoundError" + +export type PermissionNotFoundError = { + readonly _tag: "PermissionNotFoundError" + readonly requestID: string + readonly message: string +} +export const isPermissionNotFoundError = (value: unknown): value is PermissionNotFoundError => + typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "PermissionNotFoundError" + +export type PtyNotFoundError = { readonly _tag: "PtyNotFoundError"; readonly ptyID: string; readonly message: string } +export const isPtyNotFoundError = (value: unknown): value is PtyNotFoundError => + typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "PtyNotFoundError" + +export type QuestionNotFoundError = { + readonly _tag: "QuestionNotFoundError" + readonly requestID: string + readonly message: string +} +export const isQuestionNotFoundError = (value: unknown): value is QuestionNotFoundError => + typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "QuestionNotFoundError" + +export type ProjectCopyError = { + readonly name: "ProjectCopyError" + readonly data: { readonly message: string; readonly forceRequired?: boolean | undefined } +} +export const isProjectCopyError = (value: unknown): value is ProjectCopyError => + typeof value === "object" && value !== null && "name" in value && value["name"] === "ProjectCopyError" + +export type HealthGetOutput = { readonly healthy: true } + +export type LocationGetInput = { + readonly location?: { + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined + }["location"] +} + +export type LocationGetOutput = { + readonly directory: string + readonly workspaceID?: string + readonly project: { readonly id: string; readonly directory: string } +} + +export type AgentsListInput = { + readonly location?: { + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined + }["location"] +} + +export type AgentsListOutput = { + readonly location: { + readonly directory: string + readonly workspaceID?: string + readonly project: { readonly id: string; readonly directory: string } + } + readonly data: ReadonlyArray<{ + readonly id: string + readonly model?: { readonly id: string; readonly providerID: string; readonly variant?: string } + readonly request: { + readonly headers: { readonly [x: string]: string } + readonly body: { readonly [x: string]: JsonValue } + } + readonly system?: string + readonly description?: string + readonly mode: "subagent" | "primary" | "all" + readonly hidden: boolean + readonly color?: string | "primary" | "secondary" | "accent" | "success" | "warning" | "error" | "info" + readonly steps?: number + readonly permissions: ReadonlyArray<{ + readonly action: string + readonly resource: string + readonly effect: "allow" | "deny" | "ask" + }> + }> +} + +export type SessionsListInput = { + readonly workspace?: { + readonly workspace?: string | undefined + readonly limit?: number | undefined + readonly order?: "asc" | "desc" | undefined + readonly search?: string | undefined + readonly directory?: string | undefined + readonly project?: string | undefined + readonly subpath?: string | undefined + readonly cursor?: string | undefined + }["workspace"] + readonly limit?: { + readonly workspace?: string | undefined + readonly limit?: number | undefined + readonly order?: "asc" | "desc" | undefined + readonly search?: string | undefined + readonly directory?: string | undefined + readonly project?: string | undefined + readonly subpath?: string | undefined + readonly cursor?: string | undefined + }["limit"] + readonly order?: { + readonly workspace?: string | undefined + readonly limit?: number | undefined + readonly order?: "asc" | "desc" | undefined + readonly search?: string | undefined + readonly directory?: string | undefined + readonly project?: string | undefined + readonly subpath?: string | undefined + readonly cursor?: string | undefined + }["order"] + readonly search?: { + readonly workspace?: string | undefined + readonly limit?: number | undefined + readonly order?: "asc" | "desc" | undefined + readonly search?: string | undefined + readonly directory?: string | undefined + readonly project?: string | undefined + readonly subpath?: string | undefined + readonly cursor?: string | undefined + }["search"] + readonly directory?: { + readonly workspace?: string | undefined + readonly limit?: number | undefined + readonly order?: "asc" | "desc" | undefined + readonly search?: string | undefined + readonly directory?: string | undefined + readonly project?: string | undefined + readonly subpath?: string | undefined + readonly cursor?: string | undefined + }["directory"] + readonly project?: { + readonly workspace?: string | undefined + readonly limit?: number | undefined + readonly order?: "asc" | "desc" | undefined + readonly search?: string | undefined + readonly directory?: string | undefined + readonly project?: string | undefined + readonly subpath?: string | undefined + readonly cursor?: string | undefined + }["project"] + readonly subpath?: { + readonly workspace?: string | undefined + readonly limit?: number | undefined + readonly order?: "asc" | "desc" | undefined + readonly search?: string | undefined + readonly directory?: string | undefined + readonly project?: string | undefined + readonly subpath?: string | undefined + readonly cursor?: string | undefined + }["subpath"] + readonly cursor?: { + readonly workspace?: string | undefined + readonly limit?: number | undefined + readonly order?: "asc" | "desc" | undefined + readonly search?: string | undefined + readonly directory?: string | undefined + readonly project?: string | undefined + readonly subpath?: string | undefined + readonly cursor?: string | undefined + }["cursor"] +} + +export type SessionsListOutput = { + readonly data: ReadonlyArray<{ + readonly id: string + readonly parentID?: string + readonly projectID: string + readonly agent?: string + readonly model?: { readonly id: string; readonly providerID: string; readonly variant?: string } + readonly cost: number + readonly tokens: { + readonly input: number + readonly output: number + readonly reasoning: number + readonly cache: { readonly read: number; readonly write: number } + } + readonly time: { readonly created: number; readonly updated: number; readonly archived?: number } + readonly title: string + readonly location: { readonly directory: string; readonly workspaceID?: string } + readonly subpath?: string + readonly revert?: { + readonly messageID: string + readonly partID?: string + readonly snapshot?: string + readonly diff?: string + readonly files?: ReadonlyArray<{ + readonly path: string + readonly status: "added" | "modified" | "deleted" + readonly additions: number + readonly deletions: number + readonly patch: string + }> + } + }> + readonly cursor: { readonly previous?: string | null; readonly next?: string | null } +} + +export type SessionsCreateInput = { + readonly id?: { + readonly id?: string | null + readonly agent?: string | null + readonly model?: { readonly id: string; readonly providerID: string; readonly variant?: string } | null + readonly location?: { readonly directory: string; readonly workspaceID?: string } | null + }["id"] + readonly agent?: { + readonly id?: string | null + readonly agent?: string | null + readonly model?: { readonly id: string; readonly providerID: string; readonly variant?: string } | null + readonly location?: { readonly directory: string; readonly workspaceID?: string } | null + }["agent"] + readonly model?: { + readonly id?: string | null + readonly agent?: string | null + readonly model?: { readonly id: string; readonly providerID: string; readonly variant?: string } | null + readonly location?: { readonly directory: string; readonly workspaceID?: string } | null + }["model"] + readonly location?: { + readonly id?: string | null + readonly agent?: string | null + readonly model?: { readonly id: string; readonly providerID: string; readonly variant?: string } | null + readonly location?: { readonly directory: string; readonly workspaceID?: string } | null + }["location"] +} + +export type SessionsCreateOutput = { + readonly data: { + readonly id: string + readonly parentID?: string + readonly projectID: string + readonly agent?: string + readonly model?: { readonly id: string; readonly providerID: string; readonly variant?: string } + readonly cost: number + readonly tokens: { + readonly input: number + readonly output: number + readonly reasoning: number + readonly cache: { readonly read: number; readonly write: number } + } + readonly time: { readonly created: number; readonly updated: number; readonly archived?: number } + readonly title: string + readonly location: { readonly directory: string; readonly workspaceID?: string } + readonly subpath?: string + readonly revert?: { + readonly messageID: string + readonly partID?: string + readonly snapshot?: string + readonly diff?: string + readonly files?: ReadonlyArray<{ + readonly path: string + readonly status: "added" | "modified" | "deleted" + readonly additions: number + readonly deletions: number + readonly patch: string + }> + } + } +}["data"] + +export type SessionsActiveOutput = { readonly data: { readonly [x: string]: { readonly type: "running" } } }["data"] + +export type SessionsGetInput = { readonly sessionID: { readonly sessionID: string }["sessionID"] } + +export type SessionsGetOutput = { + readonly data: { + readonly id: string + readonly parentID?: string + readonly projectID: string + readonly agent?: string + readonly model?: { readonly id: string; readonly providerID: string; readonly variant?: string } + readonly cost: number + readonly tokens: { + readonly input: number + readonly output: number + readonly reasoning: number + readonly cache: { readonly read: number; readonly write: number } + } + readonly time: { readonly created: number; readonly updated: number; readonly archived?: number } + readonly title: string + readonly location: { readonly directory: string; readonly workspaceID?: string } + readonly subpath?: string + readonly revert?: { + readonly messageID: string + readonly partID?: string + readonly snapshot?: string + readonly diff?: string + readonly files?: ReadonlyArray<{ + readonly path: string + readonly status: "added" | "modified" | "deleted" + readonly additions: number + readonly deletions: number + readonly patch: string + }> + } + } +}["data"] + +export type SessionsSwitchAgentInput = { + readonly sessionID: { readonly sessionID: string }["sessionID"] + readonly agent: { readonly agent: string }["agent"] +} + +export type SessionsSwitchAgentOutput = void + +export type SessionsSwitchModelInput = { + readonly sessionID: { readonly sessionID: string }["sessionID"] + readonly model: { + readonly model: { readonly id: string; readonly providerID: string; readonly variant?: string } + }["model"] +} + +export type SessionsSwitchModelOutput = void + +export type SessionsPromptInput = { + readonly sessionID: { readonly sessionID: string }["sessionID"] + readonly id?: { + readonly id?: string | null + readonly prompt: { + readonly text: string + readonly files?: ReadonlyArray<{ + readonly uri: string + readonly name?: string + readonly description?: string + readonly source?: { readonly start: number; readonly end: number; readonly text: string } + }> + readonly agents?: ReadonlyArray<{ + readonly name: string + readonly source?: { readonly start: number; readonly end: number; readonly text: string } + }> + } + readonly delivery?: "steer" | "queue" | null + readonly resume?: boolean | null + }["id"] + readonly prompt: { + readonly id?: string | null + readonly prompt: { + readonly text: string + readonly files?: ReadonlyArray<{ + readonly uri: string + readonly name?: string + readonly description?: string + readonly source?: { readonly start: number; readonly end: number; readonly text: string } + }> + readonly agents?: ReadonlyArray<{ + readonly name: string + readonly source?: { readonly start: number; readonly end: number; readonly text: string } + }> + } + readonly delivery?: "steer" | "queue" | null + readonly resume?: boolean | null + }["prompt"] + readonly delivery?: { + readonly id?: string | null + readonly prompt: { + readonly text: string + readonly files?: ReadonlyArray<{ + readonly uri: string + readonly name?: string + readonly description?: string + readonly source?: { readonly start: number; readonly end: number; readonly text: string } + }> + readonly agents?: ReadonlyArray<{ + readonly name: string + readonly source?: { readonly start: number; readonly end: number; readonly text: string } + }> + } + readonly delivery?: "steer" | "queue" | null + readonly resume?: boolean | null + }["delivery"] + readonly resume?: { + readonly id?: string | null + readonly prompt: { + readonly text: string + readonly files?: ReadonlyArray<{ + readonly uri: string + readonly name?: string + readonly description?: string + readonly source?: { readonly start: number; readonly end: number; readonly text: string } + }> + readonly agents?: ReadonlyArray<{ + readonly name: string + readonly source?: { readonly start: number; readonly end: number; readonly text: string } + }> + } + readonly delivery?: "steer" | "queue" | null + readonly resume?: boolean | null + }["resume"] +} + +export type SessionsPromptOutput = { + readonly data: { + readonly admittedSeq: number + readonly id: string + readonly sessionID: string + readonly prompt: { + readonly text: string + readonly files?: ReadonlyArray<{ + readonly uri: string + readonly mime: string + readonly name?: string + readonly description?: string + readonly source?: { readonly start: number; readonly end: number; readonly text: string } + }> + readonly agents?: ReadonlyArray<{ + readonly name: string + readonly source?: { readonly start: number; readonly end: number; readonly text: string } + }> + } + readonly delivery: "steer" | "queue" + readonly timeCreated: number + readonly promotedSeq?: number + } +}["data"] + +export type SessionsCompactInput = { readonly sessionID: { readonly sessionID: string }["sessionID"] } + +export type SessionsCompactOutput = void + +export type SessionsWaitInput = { readonly sessionID: { readonly sessionID: string }["sessionID"] } + +export type SessionsWaitOutput = void + +export type SessionsStageInput = { + readonly sessionID: { readonly sessionID: string }["sessionID"] + readonly messageID: { readonly messageID: string; readonly files?: boolean | undefined }["messageID"] + readonly files?: { readonly messageID: string; readonly files?: boolean | undefined }["files"] +} + +export type SessionsStageOutput = { + readonly data: { + readonly messageID: string + readonly partID?: string + readonly snapshot?: string + readonly diff?: string + readonly files?: ReadonlyArray<{ + readonly path: string + readonly status: "added" | "modified" | "deleted" + readonly additions: number + readonly deletions: number + readonly patch: string + }> + } +}["data"] + +export type SessionsClearInput = { readonly sessionID: { readonly sessionID: string }["sessionID"] } + +export type SessionsClearOutput = void + +export type SessionsCommitInput = { readonly sessionID: { readonly sessionID: string }["sessionID"] } + +export type SessionsCommitOutput = void + +export type SessionsContextInput = { readonly sessionID: { readonly sessionID: string }["sessionID"] } + +export type SessionsContextOutput = { + readonly data: ReadonlyArray< + | { + readonly id: string + readonly metadata?: { readonly [x: string]: JsonValue } + readonly time: { readonly created: number } + readonly type: "agent-switched" + readonly agent: string + } + | { + readonly id: string + readonly metadata?: { readonly [x: string]: JsonValue } + readonly time: { readonly created: number } + readonly type: "model-switched" + readonly model: { readonly id: string; readonly providerID: string; readonly variant?: string } + } + | { + readonly id: string + readonly metadata?: { readonly [x: string]: JsonValue } + readonly time: { readonly created: number } + readonly text: string + readonly files?: ReadonlyArray<{ + readonly uri: string + readonly mime: string + readonly name?: string + readonly description?: string + readonly source?: { readonly start: number; readonly end: number; readonly text: string } + }> + readonly agents?: ReadonlyArray<{ + readonly name: string + readonly source?: { readonly start: number; readonly end: number; readonly text: string } + }> + readonly type: "user" + } + | { + readonly id: string + readonly metadata?: { readonly [x: string]: JsonValue } + readonly time: { readonly created: number } + readonly sessionID: string + readonly text: string + readonly type: "synthetic" + } + | { + readonly id: string + readonly metadata?: { readonly [x: string]: JsonValue } + readonly time: { readonly created: number } + readonly type: "system" + readonly text: string + } + | { + readonly id: string + readonly metadata?: { readonly [x: string]: JsonValue } + readonly time: { readonly created: number; readonly completed?: number } + readonly type: "shell" + readonly callID: string + readonly command: string + readonly output: string + } + | { + readonly id: string + readonly metadata?: { readonly [x: string]: JsonValue } + readonly time: { readonly created: number; readonly completed?: number } + readonly type: "assistant" + readonly agent: string + readonly model: { readonly id: string; readonly providerID: string; readonly variant?: string } + readonly content: ReadonlyArray< + | { readonly type: "text"; readonly id: string; readonly text: string } + | { + readonly type: "reasoning" + readonly id: string + readonly text: string + readonly providerMetadata?: { readonly [x: string]: { readonly [x: string]: JsonValue } } + readonly time?: { readonly created: number; readonly completed?: number } + } + | { + readonly type: "tool" + readonly id: string + readonly name: string + readonly provider?: { + readonly executed: boolean + readonly metadata?: { readonly [x: string]: { readonly [x: string]: JsonValue } } + readonly resultMetadata?: { readonly [x: string]: { readonly [x: string]: JsonValue } } + } + readonly state: + | { readonly status: "pending"; readonly input: string } + | { + readonly status: "running" + readonly input: { readonly [x: string]: JsonValue } + readonly structured: { readonly [x: string]: JsonValue } + readonly content: ReadonlyArray< + | { readonly type: "text"; readonly text: string } + | { readonly type: "file"; readonly uri: string; readonly mime: string; readonly name?: string } + > + } + | { + readonly status: "completed" + readonly input: { readonly [x: string]: JsonValue } + readonly attachments?: ReadonlyArray<{ + readonly uri: string + readonly mime: string + readonly name?: string + readonly description?: string + readonly source?: { readonly start: number; readonly end: number; readonly text: string } + }> + readonly content: ReadonlyArray< + | { readonly type: "text"; readonly text: string } + | { readonly type: "file"; readonly uri: string; readonly mime: string; readonly name?: string } + > + readonly outputPaths?: ReadonlyArray + readonly structured: { readonly [x: string]: JsonValue } + readonly result?: JsonValue + } + | { + readonly status: "error" + readonly input: { readonly [x: string]: JsonValue } + readonly content: ReadonlyArray< + | { readonly type: "text"; readonly text: string } + | { readonly type: "file"; readonly uri: string; readonly mime: string; readonly name?: string } + > + readonly structured: { readonly [x: string]: JsonValue } + readonly error: { readonly type: "unknown"; readonly message: string } + readonly result?: JsonValue + } + readonly time: { + readonly created: number + readonly ran?: number + readonly completed?: number + readonly pruned?: number + } + } + > + readonly snapshot?: { readonly start?: string; readonly end?: string; readonly files?: ReadonlyArray } + readonly finish?: string + readonly cost?: number + readonly tokens?: { + readonly input: number + readonly output: number + readonly reasoning: number + readonly cache: { readonly read: number; readonly write: number } + } + readonly error?: { readonly type: "unknown"; readonly message: string } + } + | { + readonly type: "compaction" + readonly reason: "auto" | "manual" + readonly summary: string + readonly recent: string + readonly id: string + readonly metadata?: { readonly [x: string]: JsonValue } + readonly time: { readonly created: number } + } + > +}["data"] + +export type SessionsHistoryInput = { + readonly sessionID: { readonly sessionID: string }["sessionID"] + readonly limit?: { readonly limit?: number | undefined; readonly after?: number | undefined }["limit"] + readonly after?: { readonly limit?: number | undefined; readonly after?: number | undefined }["after"] +} + +export type SessionsHistoryOutput = { + readonly data: ReadonlyArray< + | { + readonly id: string + readonly metadata?: { readonly [x: string]: JsonValue } + readonly type: "session.next.agent.switched" + readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number } + readonly location?: { readonly directory: string; readonly workspaceID?: string } + readonly data: { + readonly timestamp: number + readonly sessionID: string + readonly messageID: string + readonly agent: string + } + } + | { + readonly id: string + readonly metadata?: { readonly [x: string]: JsonValue } + readonly type: "session.next.model.switched" + readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number } + readonly location?: { readonly directory: string; readonly workspaceID?: string } + readonly data: { + readonly timestamp: number + readonly sessionID: string + readonly messageID: string + readonly model: { readonly id: string; readonly providerID: string; readonly variant?: string } + } + } + | { + readonly id: string + readonly metadata?: { readonly [x: string]: JsonValue } + readonly type: "session.next.moved" + readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number } + readonly location?: { readonly directory: string; readonly workspaceID?: string } + readonly data: { + readonly timestamp: number + readonly sessionID: string + readonly location: { readonly directory: string; readonly workspaceID?: string } + readonly subdirectory?: string + } + } + | { + readonly id: string + readonly metadata?: { readonly [x: string]: JsonValue } + readonly type: "session.next.prompted" + readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number } + readonly location?: { readonly directory: string; readonly workspaceID?: string } + readonly data: { + readonly timestamp: number + readonly sessionID: string + readonly messageID: string + readonly prompt: { + readonly text: string + readonly files?: ReadonlyArray<{ + readonly uri: string + readonly mime: string + readonly name?: string + readonly description?: string + readonly source?: { readonly start: number; readonly end: number; readonly text: string } + }> + readonly agents?: ReadonlyArray<{ + readonly name: string + readonly source?: { readonly start: number; readonly end: number; readonly text: string } + }> + } + readonly delivery: "steer" | "queue" + } + } + | { + readonly id: string + readonly metadata?: { readonly [x: string]: JsonValue } + readonly type: "session.next.prompt.admitted" + readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number } + readonly location?: { readonly directory: string; readonly workspaceID?: string } + readonly data: { + readonly timestamp: number + readonly sessionID: string + readonly messageID: string + readonly prompt: { + readonly text: string + readonly files?: ReadonlyArray<{ + readonly uri: string + readonly mime: string + readonly name?: string + readonly description?: string + readonly source?: { readonly start: number; readonly end: number; readonly text: string } + }> + readonly agents?: ReadonlyArray<{ + readonly name: string + readonly source?: { readonly start: number; readonly end: number; readonly text: string } + }> + } + readonly delivery: "steer" | "queue" + } + } + | { + readonly id: string + readonly metadata?: { readonly [x: string]: JsonValue } + readonly type: "session.next.context.updated" + readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number } + readonly location?: { readonly directory: string; readonly workspaceID?: string } + readonly data: { + readonly timestamp: number + readonly sessionID: string + readonly messageID: string + readonly text: string + } + } + | { + readonly id: string + readonly metadata?: { readonly [x: string]: JsonValue } + readonly type: "session.next.synthetic" + readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number } + readonly location?: { readonly directory: string; readonly workspaceID?: string } + readonly data: { + readonly timestamp: number + readonly sessionID: string + readonly messageID: string + readonly text: string + } + } + | { + readonly id: string + readonly metadata?: { readonly [x: string]: JsonValue } + readonly type: "session.next.shell.started" + readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number } + readonly location?: { readonly directory: string; readonly workspaceID?: string } + readonly data: { + readonly timestamp: number + readonly sessionID: string + readonly messageID: string + readonly callID: string + readonly command: string + } + } + | { + readonly id: string + readonly metadata?: { readonly [x: string]: JsonValue } + readonly type: "session.next.shell.ended" + readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number } + readonly location?: { readonly directory: string; readonly workspaceID?: string } + readonly data: { + readonly timestamp: number + readonly sessionID: string + readonly callID: string + readonly output: string + } + } + | { + readonly id: string + readonly metadata?: { readonly [x: string]: JsonValue } + readonly type: "session.next.step.started" + readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number } + readonly location?: { readonly directory: string; readonly workspaceID?: string } + readonly data: { + readonly timestamp: number + readonly sessionID: string + readonly assistantMessageID: string + readonly agent: string + readonly model: { readonly id: string; readonly providerID: string; readonly variant?: string } + readonly snapshot?: string + } + } + | { + readonly id: string + readonly metadata?: { readonly [x: string]: JsonValue } + readonly type: "session.next.step.ended" + readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number } + readonly location?: { readonly directory: string; readonly workspaceID?: string } + readonly data: { + readonly timestamp: number + readonly sessionID: string + readonly assistantMessageID: string + readonly finish: string + readonly cost: number + readonly tokens: { + readonly input: number + readonly output: number + readonly reasoning: number + readonly cache: { readonly read: number; readonly write: number } + } + readonly snapshot?: string + readonly files?: ReadonlyArray + } + } + | { + readonly id: string + readonly metadata?: { readonly [x: string]: JsonValue } + readonly type: "session.next.step.failed" + readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number } + readonly location?: { readonly directory: string; readonly workspaceID?: string } + readonly data: { + readonly timestamp: number + readonly sessionID: string + readonly assistantMessageID: string + readonly error: { readonly type: "unknown"; readonly message: string } + } + } + | { + readonly id: string + readonly metadata?: { readonly [x: string]: JsonValue } + readonly type: "session.next.text.started" + readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number } + readonly location?: { readonly directory: string; readonly workspaceID?: string } + readonly data: { + readonly timestamp: number + readonly sessionID: string + readonly assistantMessageID: string + readonly textID: string + } + } + | { + readonly id: string + readonly metadata?: { readonly [x: string]: JsonValue } + readonly type: "session.next.text.ended" + readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number } + readonly location?: { readonly directory: string; readonly workspaceID?: string } + readonly data: { + readonly timestamp: number + readonly sessionID: string + readonly assistantMessageID: string + readonly textID: string + readonly text: string + } + } + | { + readonly id: string + readonly metadata?: { readonly [x: string]: JsonValue } + readonly type: "session.next.tool.input.started" + readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number } + readonly location?: { readonly directory: string; readonly workspaceID?: string } + readonly data: { + readonly timestamp: number + readonly sessionID: string + readonly assistantMessageID: string + readonly callID: string + readonly name: string + } + } + | { + readonly id: string + readonly metadata?: { readonly [x: string]: JsonValue } + readonly type: "session.next.tool.input.ended" + readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number } + readonly location?: { readonly directory: string; readonly workspaceID?: string } + readonly data: { + readonly timestamp: number + readonly sessionID: string + readonly assistantMessageID: string + readonly callID: string + readonly text: string + } + } + | { + readonly id: string + readonly metadata?: { readonly [x: string]: JsonValue } + readonly type: "session.next.tool.called" + readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number } + readonly location?: { readonly directory: string; readonly workspaceID?: string } + readonly data: { + readonly timestamp: number + readonly sessionID: string + readonly assistantMessageID: string + readonly callID: string + readonly tool: string + readonly input: { readonly [x: string]: JsonValue } + readonly provider: { + readonly executed: boolean + readonly metadata?: { readonly [x: string]: { readonly [x: string]: JsonValue } } + } + } + } + | { + readonly id: string + readonly metadata?: { readonly [x: string]: JsonValue } + readonly type: "session.next.tool.progress" + readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number } + readonly location?: { readonly directory: string; readonly workspaceID?: string } + readonly data: { + readonly timestamp: number + readonly sessionID: string + readonly assistantMessageID: string + readonly callID: string + readonly structured: { readonly [x: string]: JsonValue } + readonly content: ReadonlyArray< + | { readonly type: "text"; readonly text: string } + | { readonly type: "file"; readonly uri: string; readonly mime: string; readonly name?: string } + > + } + } + | { + readonly id: string + readonly metadata?: { readonly [x: string]: JsonValue } + readonly type: "session.next.tool.success" + readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number } + readonly location?: { readonly directory: string; readonly workspaceID?: string } + readonly data: { + readonly timestamp: number + readonly sessionID: string + readonly assistantMessageID: string + readonly callID: string + readonly structured: { readonly [x: string]: JsonValue } + readonly content: ReadonlyArray< + | { readonly type: "text"; readonly text: string } + | { readonly type: "file"; readonly uri: string; readonly mime: string; readonly name?: string } + > + readonly outputPaths?: ReadonlyArray + readonly result?: JsonValue + readonly provider: { + readonly executed: boolean + readonly metadata?: { readonly [x: string]: { readonly [x: string]: JsonValue } } + } + } + } + | { + readonly id: string + readonly metadata?: { readonly [x: string]: JsonValue } + readonly type: "session.next.tool.failed" + readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number } + readonly location?: { readonly directory: string; readonly workspaceID?: string } + readonly data: { + readonly timestamp: number + readonly sessionID: string + readonly assistantMessageID: string + readonly callID: string + readonly error: { readonly type: "unknown"; readonly message: string } + readonly result?: JsonValue + readonly provider: { + readonly executed: boolean + readonly metadata?: { readonly [x: string]: { readonly [x: string]: JsonValue } } + } + } + } + | { + readonly id: string + readonly metadata?: { readonly [x: string]: JsonValue } + readonly type: "session.next.reasoning.started" + readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number } + readonly location?: { readonly directory: string; readonly workspaceID?: string } + readonly data: { + readonly timestamp: number + readonly sessionID: string + readonly assistantMessageID: string + readonly reasoningID: string + readonly providerMetadata?: { readonly [x: string]: { readonly [x: string]: JsonValue } } + } + } + | { + readonly id: string + readonly metadata?: { readonly [x: string]: JsonValue } + readonly type: "session.next.reasoning.ended" + readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number } + readonly location?: { readonly directory: string; readonly workspaceID?: string } + readonly data: { + readonly timestamp: number + readonly sessionID: string + readonly assistantMessageID: string + readonly reasoningID: string + readonly text: string + readonly providerMetadata?: { readonly [x: string]: { readonly [x: string]: JsonValue } } + } + } + | { + readonly id: string + readonly metadata?: { readonly [x: string]: JsonValue } + readonly type: "session.next.retried" + readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number } + readonly location?: { readonly directory: string; readonly workspaceID?: string } + readonly data: { + readonly timestamp: number + readonly sessionID: string + readonly attempt: number + readonly error: { + readonly message: string + readonly statusCode?: number + readonly isRetryable: boolean + readonly responseHeaders?: { readonly [x: string]: string } + readonly responseBody?: string + readonly metadata?: { readonly [x: string]: string } + } + } + } + | { + readonly id: string + readonly metadata?: { readonly [x: string]: JsonValue } + readonly type: "session.next.compaction.started" + readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number } + readonly location?: { readonly directory: string; readonly workspaceID?: string } + readonly data: { + readonly timestamp: number + readonly sessionID: string + readonly messageID: string + readonly reason: "auto" | "manual" + } + } + | { + readonly id: string + readonly metadata?: { readonly [x: string]: JsonValue } + readonly type: "session.next.compaction.ended" + readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number } + readonly location?: { readonly directory: string; readonly workspaceID?: string } + readonly data: { + readonly timestamp: number + readonly sessionID: string + readonly messageID: string + readonly reason: "auto" | "manual" + readonly text: string + readonly recent: string + } + } + | { + readonly id: string + readonly metadata?: { readonly [x: string]: JsonValue } + readonly type: "session.next.revert.staged" + readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number } + readonly location?: { readonly directory: string; readonly workspaceID?: string } + readonly data: { + readonly timestamp: number + readonly sessionID: string + readonly revert: { + readonly messageID: string + readonly partID?: string + readonly snapshot?: string + readonly diff?: string + readonly files?: ReadonlyArray<{ + readonly path: string + readonly status: "added" | "modified" | "deleted" + readonly additions: number + readonly deletions: number + readonly patch: string + }> + } + } + } + | { + readonly id: string + readonly metadata?: { readonly [x: string]: JsonValue } + readonly type: "session.next.revert.cleared" + readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number } + readonly location?: { readonly directory: string; readonly workspaceID?: string } + readonly data: { readonly timestamp: number; readonly sessionID: string } + } + | { + readonly id: string + readonly metadata?: { readonly [x: string]: JsonValue } + readonly type: "session.next.revert.committed" + readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number } + readonly location?: { readonly directory: string; readonly workspaceID?: string } + readonly data: { readonly timestamp: number; readonly sessionID: string; readonly messageID: string } + } + > + readonly hasMore: boolean +} + +export type SessionsEventsInput = { + readonly sessionID: { readonly sessionID: string }["sessionID"] + readonly after?: { readonly after?: number | undefined }["after"] +} + +export type SessionsEventsOutput = + | { + readonly id: string + readonly metadata?: { readonly [x: string]: unknown } + readonly type: "session.next.agent.switched" + readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number } + readonly location?: { readonly directory: string; readonly workspaceID?: string } + readonly data: { + readonly timestamp: number + readonly sessionID: string + readonly messageID: string + readonly agent: string + } + } + | { + readonly id: string + readonly metadata?: { readonly [x: string]: unknown } + readonly type: "session.next.model.switched" + readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number } + readonly location?: { readonly directory: string; readonly workspaceID?: string } + readonly data: { + readonly timestamp: number + readonly sessionID: string + readonly messageID: string + readonly model: { readonly id: string; readonly providerID: string; readonly variant?: string } + } + } + | { + readonly id: string + readonly metadata?: { readonly [x: string]: unknown } + readonly type: "session.next.moved" + readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number } + readonly location?: { readonly directory: string; readonly workspaceID?: string } + readonly data: { + readonly timestamp: number + readonly sessionID: string + readonly location: { readonly directory: string; readonly workspaceID?: string } + readonly subdirectory?: string + } + } + | { + readonly id: string + readonly metadata?: { readonly [x: string]: unknown } + readonly type: "session.next.prompted" + readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number } + readonly location?: { readonly directory: string; readonly workspaceID?: string } + readonly data: { + readonly timestamp: number + readonly sessionID: string + readonly messageID: string + readonly prompt: { + readonly text: string + readonly files?: ReadonlyArray<{ + readonly uri: string + readonly mime: string + readonly name?: string + readonly description?: string + readonly source?: { readonly start: number; readonly end: number; readonly text: string } + }> + readonly agents?: ReadonlyArray<{ + readonly name: string + readonly source?: { readonly start: number; readonly end: number; readonly text: string } + }> + } + readonly delivery: "steer" | "queue" + } + } + | { + readonly id: string + readonly metadata?: { readonly [x: string]: unknown } + readonly type: "session.next.prompt.admitted" + readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number } + readonly location?: { readonly directory: string; readonly workspaceID?: string } + readonly data: { + readonly timestamp: number + readonly sessionID: string + readonly messageID: string + readonly prompt: { + readonly text: string + readonly files?: ReadonlyArray<{ + readonly uri: string + readonly mime: string + readonly name?: string + readonly description?: string + readonly source?: { readonly start: number; readonly end: number; readonly text: string } + }> + readonly agents?: ReadonlyArray<{ + readonly name: string + readonly source?: { readonly start: number; readonly end: number; readonly text: string } + }> + } + readonly delivery: "steer" | "queue" + } + } + | { + readonly id: string + readonly metadata?: { readonly [x: string]: unknown } + readonly type: "session.next.context.updated" + readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number } + readonly location?: { readonly directory: string; readonly workspaceID?: string } + readonly data: { + readonly timestamp: number + readonly sessionID: string + readonly messageID: string + readonly text: string + } + } + | { + readonly id: string + readonly metadata?: { readonly [x: string]: unknown } + readonly type: "session.next.synthetic" + readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number } + readonly location?: { readonly directory: string; readonly workspaceID?: string } + readonly data: { + readonly timestamp: number + readonly sessionID: string + readonly messageID: string + readonly text: string + } + } + | { + readonly id: string + readonly metadata?: { readonly [x: string]: unknown } + readonly type: "session.next.shell.started" + readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number } + readonly location?: { readonly directory: string; readonly workspaceID?: string } + readonly data: { + readonly timestamp: number + readonly sessionID: string + readonly messageID: string + readonly callID: string + readonly command: string + } + } + | { + readonly id: string + readonly metadata?: { readonly [x: string]: unknown } + readonly type: "session.next.shell.ended" + readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number } + readonly location?: { readonly directory: string; readonly workspaceID?: string } + readonly data: { + readonly timestamp: number + readonly sessionID: string + readonly callID: string + readonly output: string + } + } + | { + readonly id: string + readonly metadata?: { readonly [x: string]: unknown } + readonly type: "session.next.step.started" + readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number } + readonly location?: { readonly directory: string; readonly workspaceID?: string } + readonly data: { + readonly timestamp: number + readonly sessionID: string + readonly assistantMessageID: string + readonly agent: string + readonly model: { readonly id: string; readonly providerID: string; readonly variant?: string } + readonly snapshot?: string + } + } + | { + readonly id: string + readonly metadata?: { readonly [x: string]: unknown } + readonly type: "session.next.step.ended" + readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number } + readonly location?: { readonly directory: string; readonly workspaceID?: string } + readonly data: { + readonly timestamp: number + readonly sessionID: string + readonly assistantMessageID: string + readonly finish: string + readonly cost: number + readonly tokens: { + readonly input: number + readonly output: number + readonly reasoning: number + readonly cache: { readonly read: number; readonly write: number } + } + readonly snapshot?: string + readonly files?: ReadonlyArray + } + } + | { + readonly id: string + readonly metadata?: { readonly [x: string]: unknown } + readonly type: "session.next.step.failed" + readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number } + readonly location?: { readonly directory: string; readonly workspaceID?: string } + readonly data: { + readonly timestamp: number + readonly sessionID: string + readonly assistantMessageID: string + readonly error: { readonly type: "unknown"; readonly message: string } + } + } + | { + readonly id: string + readonly metadata?: { readonly [x: string]: unknown } + readonly type: "session.next.text.started" + readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number } + readonly location?: { readonly directory: string; readonly workspaceID?: string } + readonly data: { + readonly timestamp: number + readonly sessionID: string + readonly assistantMessageID: string + readonly textID: string + } + } + | { + readonly id: string + readonly metadata?: { readonly [x: string]: unknown } + readonly type: "session.next.text.ended" + readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number } + readonly location?: { readonly directory: string; readonly workspaceID?: string } + readonly data: { + readonly timestamp: number + readonly sessionID: string + readonly assistantMessageID: string + readonly textID: string + readonly text: string + } + } + | { + readonly id: string + readonly metadata?: { readonly [x: string]: unknown } + readonly type: "session.next.tool.input.started" + readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number } + readonly location?: { readonly directory: string; readonly workspaceID?: string } + readonly data: { + readonly timestamp: number + readonly sessionID: string + readonly assistantMessageID: string + readonly callID: string + readonly name: string + } + } + | { + readonly id: string + readonly metadata?: { readonly [x: string]: unknown } + readonly type: "session.next.tool.input.ended" + readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number } + readonly location?: { readonly directory: string; readonly workspaceID?: string } + readonly data: { + readonly timestamp: number + readonly sessionID: string + readonly assistantMessageID: string + readonly callID: string + readonly text: string + } + } + | { + readonly id: string + readonly metadata?: { readonly [x: string]: unknown } + readonly type: "session.next.tool.called" + readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number } + readonly location?: { readonly directory: string; readonly workspaceID?: string } + readonly data: { + readonly timestamp: number + readonly sessionID: string + readonly assistantMessageID: string + readonly callID: string + readonly tool: string + readonly input: { readonly [x: string]: unknown } + readonly provider: { + readonly executed: boolean + readonly metadata?: { readonly [x: string]: { readonly [x: string]: unknown } } + } + } + } + | { + readonly id: string + readonly metadata?: { readonly [x: string]: unknown } + readonly type: "session.next.tool.progress" + readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number } + readonly location?: { readonly directory: string; readonly workspaceID?: string } + readonly data: { + readonly timestamp: number + readonly sessionID: string + readonly assistantMessageID: string + readonly callID: string + readonly structured: { readonly [x: string]: unknown } + readonly content: ReadonlyArray< + | { readonly type: "text"; readonly text: string } + | { readonly type: "file"; readonly uri: string; readonly mime: string; readonly name?: string } + > + } + } + | { + readonly id: string + readonly metadata?: { readonly [x: string]: unknown } + readonly type: "session.next.tool.success" + readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number } + readonly location?: { readonly directory: string; readonly workspaceID?: string } + readonly data: { + readonly timestamp: number + readonly sessionID: string + readonly assistantMessageID: string + readonly callID: string + readonly structured: { readonly [x: string]: unknown } + readonly content: ReadonlyArray< + | { readonly type: "text"; readonly text: string } + | { readonly type: "file"; readonly uri: string; readonly mime: string; readonly name?: string } + > + readonly outputPaths?: ReadonlyArray + readonly result?: unknown + readonly provider: { + readonly executed: boolean + readonly metadata?: { readonly [x: string]: { readonly [x: string]: unknown } } + } + } + } + | { + readonly id: string + readonly metadata?: { readonly [x: string]: unknown } + readonly type: "session.next.tool.failed" + readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number } + readonly location?: { readonly directory: string; readonly workspaceID?: string } + readonly data: { + readonly timestamp: number + readonly sessionID: string + readonly assistantMessageID: string + readonly callID: string + readonly error: { readonly type: "unknown"; readonly message: string } + readonly result?: unknown + readonly provider: { + readonly executed: boolean + readonly metadata?: { readonly [x: string]: { readonly [x: string]: unknown } } + } + } + } + | { + readonly id: string + readonly metadata?: { readonly [x: string]: unknown } + readonly type: "session.next.reasoning.started" + readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number } + readonly location?: { readonly directory: string; readonly workspaceID?: string } + readonly data: { + readonly timestamp: number + readonly sessionID: string + readonly assistantMessageID: string + readonly reasoningID: string + readonly providerMetadata?: { readonly [x: string]: { readonly [x: string]: unknown } } + } + } + | { + readonly id: string + readonly metadata?: { readonly [x: string]: unknown } + readonly type: "session.next.reasoning.ended" + readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number } + readonly location?: { readonly directory: string; readonly workspaceID?: string } + readonly data: { + readonly timestamp: number + readonly sessionID: string + readonly assistantMessageID: string + readonly reasoningID: string + readonly text: string + readonly providerMetadata?: { readonly [x: string]: { readonly [x: string]: unknown } } + } + } + | { + readonly id: string + readonly metadata?: { readonly [x: string]: unknown } + readonly type: "session.next.retried" + readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number } + readonly location?: { readonly directory: string; readonly workspaceID?: string } + readonly data: { + readonly timestamp: number + readonly sessionID: string + readonly attempt: number + readonly error: { + readonly message: string + readonly statusCode?: number + readonly isRetryable: boolean + readonly responseHeaders?: { readonly [x: string]: string } + readonly responseBody?: string + readonly metadata?: { readonly [x: string]: string } + } + } + } + | { + readonly id: string + readonly metadata?: { readonly [x: string]: unknown } + readonly type: "session.next.compaction.started" + readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number } + readonly location?: { readonly directory: string; readonly workspaceID?: string } + readonly data: { + readonly timestamp: number + readonly sessionID: string + readonly messageID: string + readonly reason: "auto" | "manual" + } + } + | { + readonly id: string + readonly metadata?: { readonly [x: string]: unknown } + readonly type: "session.next.compaction.ended" + readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number } + readonly location?: { readonly directory: string; readonly workspaceID?: string } + readonly data: { + readonly timestamp: number + readonly sessionID: string + readonly messageID: string + readonly reason: "auto" | "manual" + readonly text: string + readonly recent: string + } + } + | { + readonly id: string + readonly metadata?: { readonly [x: string]: unknown } + readonly type: "session.next.revert.staged" + readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number } + readonly location?: { readonly directory: string; readonly workspaceID?: string } + readonly data: { + readonly timestamp: number + readonly sessionID: string + readonly revert: { + readonly messageID: string + readonly partID?: string + readonly snapshot?: string + readonly diff?: string + readonly files?: ReadonlyArray<{ + readonly path: string + readonly status: "added" | "modified" | "deleted" + readonly additions: number + readonly deletions: number + readonly patch: string + }> + } + } + } + | { + readonly id: string + readonly metadata?: { readonly [x: string]: unknown } + readonly type: "session.next.revert.cleared" + readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number } + readonly location?: { readonly directory: string; readonly workspaceID?: string } + readonly data: { readonly timestamp: number; readonly sessionID: string } + } + | { + readonly id: string + readonly metadata?: { readonly [x: string]: unknown } + readonly type: "session.next.revert.committed" + readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number } + readonly location?: { readonly directory: string; readonly workspaceID?: string } + readonly data: { readonly timestamp: number; readonly sessionID: string; readonly messageID: string } + } + +export type SessionsInterruptInput = { readonly sessionID: { readonly sessionID: string }["sessionID"] } + +export type SessionsInterruptOutput = void + +export type SessionsMessageInput = { + readonly sessionID: { readonly sessionID: string; readonly messageID: string }["sessionID"] + readonly messageID: { readonly sessionID: string; readonly messageID: string }["messageID"] +} + +export type SessionsMessageOutput = { + readonly data: + | { + readonly id: string + readonly metadata?: { readonly [x: string]: JsonValue } + readonly time: { readonly created: number } + readonly type: "agent-switched" + readonly agent: string + } + | { + readonly id: string + readonly metadata?: { readonly [x: string]: JsonValue } + readonly time: { readonly created: number } + readonly type: "model-switched" + readonly model: { readonly id: string; readonly providerID: string; readonly variant?: string } + } + | { + readonly id: string + readonly metadata?: { readonly [x: string]: JsonValue } + readonly time: { readonly created: number } + readonly text: string + readonly files?: ReadonlyArray<{ + readonly uri: string + readonly mime: string + readonly name?: string + readonly description?: string + readonly source?: { readonly start: number; readonly end: number; readonly text: string } + }> + readonly agents?: ReadonlyArray<{ + readonly name: string + readonly source?: { readonly start: number; readonly end: number; readonly text: string } + }> + readonly type: "user" + } + | { + readonly id: string + readonly metadata?: { readonly [x: string]: JsonValue } + readonly time: { readonly created: number } + readonly sessionID: string + readonly text: string + readonly type: "synthetic" + } + | { + readonly id: string + readonly metadata?: { readonly [x: string]: JsonValue } + readonly time: { readonly created: number } + readonly type: "system" + readonly text: string + } + | { + readonly id: string + readonly metadata?: { readonly [x: string]: JsonValue } + readonly time: { readonly created: number; readonly completed?: number } + readonly type: "shell" + readonly callID: string + readonly command: string + readonly output: string + } + | { + readonly id: string + readonly metadata?: { readonly [x: string]: JsonValue } + readonly time: { readonly created: number; readonly completed?: number } + readonly type: "assistant" + readonly agent: string + readonly model: { readonly id: string; readonly providerID: string; readonly variant?: string } + readonly content: ReadonlyArray< + | { readonly type: "text"; readonly id: string; readonly text: string } + | { + readonly type: "reasoning" + readonly id: string + readonly text: string + readonly providerMetadata?: { readonly [x: string]: { readonly [x: string]: JsonValue } } + readonly time?: { readonly created: number; readonly completed?: number } + } + | { + readonly type: "tool" + readonly id: string + readonly name: string + readonly provider?: { + readonly executed: boolean + readonly metadata?: { readonly [x: string]: { readonly [x: string]: JsonValue } } + readonly resultMetadata?: { readonly [x: string]: { readonly [x: string]: JsonValue } } + } + readonly state: + | { readonly status: "pending"; readonly input: string } + | { + readonly status: "running" + readonly input: { readonly [x: string]: JsonValue } + readonly structured: { readonly [x: string]: JsonValue } + readonly content: ReadonlyArray< + | { readonly type: "text"; readonly text: string } + | { readonly type: "file"; readonly uri: string; readonly mime: string; readonly name?: string } + > + } + | { + readonly status: "completed" + readonly input: { readonly [x: string]: JsonValue } + readonly attachments?: ReadonlyArray<{ + readonly uri: string + readonly mime: string + readonly name?: string + readonly description?: string + readonly source?: { readonly start: number; readonly end: number; readonly text: string } + }> + readonly content: ReadonlyArray< + | { readonly type: "text"; readonly text: string } + | { readonly type: "file"; readonly uri: string; readonly mime: string; readonly name?: string } + > + readonly outputPaths?: ReadonlyArray + readonly structured: { readonly [x: string]: JsonValue } + readonly result?: JsonValue + } + | { + readonly status: "error" + readonly input: { readonly [x: string]: JsonValue } + readonly content: ReadonlyArray< + | { readonly type: "text"; readonly text: string } + | { readonly type: "file"; readonly uri: string; readonly mime: string; readonly name?: string } + > + readonly structured: { readonly [x: string]: JsonValue } + readonly error: { readonly type: "unknown"; readonly message: string } + readonly result?: JsonValue + } + readonly time: { + readonly created: number + readonly ran?: number + readonly completed?: number + readonly pruned?: number + } + } + > + readonly snapshot?: { readonly start?: string; readonly end?: string; readonly files?: ReadonlyArray } + readonly finish?: string + readonly cost?: number + readonly tokens?: { + readonly input: number + readonly output: number + readonly reasoning: number + readonly cache: { readonly read: number; readonly write: number } + } + readonly error?: { readonly type: "unknown"; readonly message: string } + } + | { + readonly type: "compaction" + readonly reason: "auto" | "manual" + readonly summary: string + readonly recent: string + readonly id: string + readonly metadata?: { readonly [x: string]: JsonValue } + readonly time: { readonly created: number } + } +}["data"] + +export type MessagesListInput = { + readonly sessionID: { readonly sessionID: string }["sessionID"] + readonly limit?: { + readonly limit?: number | undefined + readonly order?: "asc" | "desc" | undefined + readonly cursor?: string | undefined + }["limit"] + readonly order?: { + readonly limit?: number | undefined + readonly order?: "asc" | "desc" | undefined + readonly cursor?: string | undefined + }["order"] + readonly cursor?: { + readonly limit?: number | undefined + readonly order?: "asc" | "desc" | undefined + readonly cursor?: string | undefined + }["cursor"] +} + +export type MessagesListOutput = { + readonly data: ReadonlyArray< + | { + readonly id: string + readonly metadata?: { readonly [x: string]: JsonValue } + readonly time: { readonly created: number } + readonly type: "agent-switched" + readonly agent: string + } + | { + readonly id: string + readonly metadata?: { readonly [x: string]: JsonValue } + readonly time: { readonly created: number } + readonly type: "model-switched" + readonly model: { readonly id: string; readonly providerID: string; readonly variant?: string } + } + | { + readonly id: string + readonly metadata?: { readonly [x: string]: JsonValue } + readonly time: { readonly created: number } + readonly text: string + readonly files?: ReadonlyArray<{ + readonly uri: string + readonly mime: string + readonly name?: string + readonly description?: string + readonly source?: { readonly start: number; readonly end: number; readonly text: string } + }> + readonly agents?: ReadonlyArray<{ + readonly name: string + readonly source?: { readonly start: number; readonly end: number; readonly text: string } + }> + readonly type: "user" + } + | { + readonly id: string + readonly metadata?: { readonly [x: string]: JsonValue } + readonly time: { readonly created: number } + readonly sessionID: string + readonly text: string + readonly type: "synthetic" + } + | { + readonly id: string + readonly metadata?: { readonly [x: string]: JsonValue } + readonly time: { readonly created: number } + readonly type: "system" + readonly text: string + } + | { + readonly id: string + readonly metadata?: { readonly [x: string]: JsonValue } + readonly time: { readonly created: number; readonly completed?: number } + readonly type: "shell" + readonly callID: string + readonly command: string + readonly output: string + } + | { + readonly id: string + readonly metadata?: { readonly [x: string]: JsonValue } + readonly time: { readonly created: number; readonly completed?: number } + readonly type: "assistant" + readonly agent: string + readonly model: { readonly id: string; readonly providerID: string; readonly variant?: string } + readonly content: ReadonlyArray< + | { readonly type: "text"; readonly id: string; readonly text: string } + | { + readonly type: "reasoning" + readonly id: string + readonly text: string + readonly providerMetadata?: { readonly [x: string]: { readonly [x: string]: JsonValue } } + readonly time?: { readonly created: number; readonly completed?: number } + } + | { + readonly type: "tool" + readonly id: string + readonly name: string + readonly provider?: { + readonly executed: boolean + readonly metadata?: { readonly [x: string]: { readonly [x: string]: JsonValue } } + readonly resultMetadata?: { readonly [x: string]: { readonly [x: string]: JsonValue } } + } + readonly state: + | { readonly status: "pending"; readonly input: string } + | { + readonly status: "running" + readonly input: { readonly [x: string]: JsonValue } + readonly structured: { readonly [x: string]: JsonValue } + readonly content: ReadonlyArray< + | { readonly type: "text"; readonly text: string } + | { readonly type: "file"; readonly uri: string; readonly mime: string; readonly name?: string } + > + } + | { + readonly status: "completed" + readonly input: { readonly [x: string]: JsonValue } + readonly attachments?: ReadonlyArray<{ + readonly uri: string + readonly mime: string + readonly name?: string + readonly description?: string + readonly source?: { readonly start: number; readonly end: number; readonly text: string } + }> + readonly content: ReadonlyArray< + | { readonly type: "text"; readonly text: string } + | { readonly type: "file"; readonly uri: string; readonly mime: string; readonly name?: string } + > + readonly outputPaths?: ReadonlyArray + readonly structured: { readonly [x: string]: JsonValue } + readonly result?: JsonValue + } + | { + readonly status: "error" + readonly input: { readonly [x: string]: JsonValue } + readonly content: ReadonlyArray< + | { readonly type: "text"; readonly text: string } + | { readonly type: "file"; readonly uri: string; readonly mime: string; readonly name?: string } + > + readonly structured: { readonly [x: string]: JsonValue } + readonly error: { readonly type: "unknown"; readonly message: string } + readonly result?: JsonValue + } + readonly time: { + readonly created: number + readonly ran?: number + readonly completed?: number + readonly pruned?: number + } + } + > + readonly snapshot?: { readonly start?: string; readonly end?: string; readonly files?: ReadonlyArray } + readonly finish?: string + readonly cost?: number + readonly tokens?: { + readonly input: number + readonly output: number + readonly reasoning: number + readonly cache: { readonly read: number; readonly write: number } + } + readonly error?: { readonly type: "unknown"; readonly message: string } + } + | { + readonly type: "compaction" + readonly reason: "auto" | "manual" + readonly summary: string + readonly recent: string + readonly id: string + readonly metadata?: { readonly [x: string]: JsonValue } + readonly time: { readonly created: number } + } + > + readonly cursor: { readonly previous?: string | null; readonly next?: string | null } +} + +export type ModelsListInput = { + readonly location?: { + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined + }["location"] +} + +export type ModelsListOutput = { + readonly location: { + readonly directory: string + readonly workspaceID?: string + readonly project: { readonly id: string; readonly directory: string } + } + readonly data: ReadonlyArray<{ + readonly id: string + readonly providerID: string + readonly family?: string + readonly name: string + readonly api: + | { + readonly id: string + readonly type: "aisdk" + readonly package: string + readonly url?: string + readonly settings?: { readonly [x: string]: JsonValue } + } + | { + readonly id: string + readonly type: "native" + readonly url?: string + readonly settings: { readonly [x: string]: JsonValue } + } + readonly capabilities: { + readonly tools: boolean + readonly input: ReadonlyArray + readonly output: ReadonlyArray + } + readonly request: { + readonly headers: { readonly [x: string]: string } + readonly body: { readonly [x: string]: JsonValue } + readonly variant?: string + } + readonly variants: ReadonlyArray<{ + readonly id: string + readonly headers: { readonly [x: string]: string } + readonly body: { readonly [x: string]: JsonValue } + }> + readonly time: { readonly released: number } + readonly cost: ReadonlyArray<{ + readonly tier?: { readonly type: "context"; readonly size: number } + readonly input: number + readonly output: number + readonly cache: { readonly read: number; readonly write: number } + }> + readonly status: "alpha" | "beta" | "deprecated" | "active" + readonly enabled: boolean + readonly limit: { readonly context: number; readonly input?: number; readonly output: number } + }> +} + +export type ProvidersListInput = { + readonly location?: { + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined + }["location"] +} + +export type ProvidersListOutput = { + readonly location: { + readonly directory: string + readonly workspaceID?: string + readonly project: { readonly id: string; readonly directory: string } + } + readonly data: ReadonlyArray<{ + readonly id: string + readonly integrationID?: string + readonly name: string + readonly disabled?: boolean + readonly api: + | { + readonly type: "aisdk" + readonly package: string + readonly url?: string + readonly settings?: { readonly [x: string]: JsonValue } + } + | { readonly type: "native"; readonly url?: string; readonly settings: { readonly [x: string]: JsonValue } } + readonly request: { + readonly headers: { readonly [x: string]: string } + readonly body: { readonly [x: string]: JsonValue } + } + }> +} + +export type ProvidersGetInput = { + readonly providerID: { readonly providerID: string }["providerID"] + readonly location?: { + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined + }["location"] +} + +export type ProvidersGetOutput = { + readonly location: { + readonly directory: string + readonly workspaceID?: string + readonly project: { readonly id: string; readonly directory: string } + } + readonly data: { + readonly id: string + readonly integrationID?: string + readonly name: string + readonly disabled?: boolean + readonly api: + | { + readonly type: "aisdk" + readonly package: string + readonly url?: string + readonly settings?: { readonly [x: string]: JsonValue } + } + | { readonly type: "native"; readonly url?: string; readonly settings: { readonly [x: string]: JsonValue } } + readonly request: { + readonly headers: { readonly [x: string]: string } + readonly body: { readonly [x: string]: JsonValue } + } + } +} + +export type IntegrationsListInput = { + readonly location?: { + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined + }["location"] +} + +export type IntegrationsListOutput = { + readonly location: { + readonly directory: string + readonly workspaceID?: string + readonly project: { readonly id: string; readonly directory: string } + } + readonly data: ReadonlyArray<{ + readonly id: string + readonly name: string + readonly methods: ReadonlyArray< + | { + readonly id: string + readonly type: "oauth" + readonly label: string + readonly prompts?: ReadonlyArray< + | { + readonly type: "text" + readonly key: string + readonly message: string + readonly placeholder?: string + readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string } + } + | { + readonly type: "select" + readonly key: string + readonly message: string + readonly options: ReadonlyArray<{ + readonly label: string + readonly value: string + readonly hint?: string + }> + readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string } + } + > + } + | { readonly type: "key"; readonly label?: string } + | { readonly type: "env"; readonly names: ReadonlyArray } + > + readonly connections: ReadonlyArray< + | { readonly type: "credential"; readonly id: string; readonly label: string } + | { readonly type: "env"; readonly name: string } + > + }> +} + +export type IntegrationsGetInput = { + readonly integrationID: { readonly integrationID: string }["integrationID"] + readonly location?: { + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined + }["location"] +} + +export type IntegrationsGetOutput = { + readonly location: { + readonly directory: string + readonly workspaceID?: string + readonly project: { readonly id: string; readonly directory: string } + } + readonly data: { + readonly id: string + readonly name: string + readonly methods: ReadonlyArray< + | { + readonly id: string + readonly type: "oauth" + readonly label: string + readonly prompts?: ReadonlyArray< + | { + readonly type: "text" + readonly key: string + readonly message: string + readonly placeholder?: string + readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string } + } + | { + readonly type: "select" + readonly key: string + readonly message: string + readonly options: ReadonlyArray<{ + readonly label: string + readonly value: string + readonly hint?: string + }> + readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string } + } + > + } + | { readonly type: "key"; readonly label?: string } + | { readonly type: "env"; readonly names: ReadonlyArray } + > + readonly connections: ReadonlyArray< + | { readonly type: "credential"; readonly id: string; readonly label: string } + | { readonly type: "env"; readonly name: string } + > + } | null +} + +export type IntegrationsConnectKeyInput = { + readonly integrationID: { readonly integrationID: string }["integrationID"] + readonly location?: { + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined + }["location"] + readonly key: { readonly key: string; readonly label?: string | undefined }["key"] + readonly label?: { readonly key: string; readonly label?: string | undefined }["label"] +} + +export type IntegrationsConnectKeyOutput = void + +export type IntegrationsConnectOauthInput = { + readonly integrationID: { readonly integrationID: string }["integrationID"] + readonly location?: { + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined + }["location"] + readonly methodID: { + readonly methodID: string + readonly inputs: { readonly [x: string]: string } + readonly label?: string | undefined + }["methodID"] + readonly inputs: { + readonly methodID: string + readonly inputs: { readonly [x: string]: string } + readonly label?: string | undefined + }["inputs"] + readonly label?: { + readonly methodID: string + readonly inputs: { readonly [x: string]: string } + readonly label?: string | undefined + }["label"] +} + +export type IntegrationsConnectOauthOutput = { + readonly location: { + readonly directory: string + readonly workspaceID?: string + readonly project: { readonly id: string; readonly directory: string } + } + readonly data: { + readonly attemptID: string + readonly url: string + readonly instructions: string + readonly mode: "auto" | "code" + readonly time: { + readonly created: number | "Infinity" | "-Infinity" | "NaN" + readonly expires: number | "Infinity" | "-Infinity" | "NaN" + } + } +} + +export type IntegrationsAttemptStatusInput = { + readonly attemptID: { readonly attemptID: string }["attemptID"] + readonly location?: { + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined + }["location"] +} + +export type IntegrationsAttemptStatusOutput = { + readonly location: { + readonly directory: string + readonly workspaceID?: string + readonly project: { readonly id: string; readonly directory: string } + } + readonly data: + | { + readonly status: "pending" + readonly time: { + readonly created: number | "Infinity" | "-Infinity" | "NaN" + readonly expires: number | "Infinity" | "-Infinity" | "NaN" + } + } + | { + readonly status: "complete" + readonly time: { + readonly created: number | "Infinity" | "-Infinity" | "NaN" + readonly expires: number | "Infinity" | "-Infinity" | "NaN" + } + } + | { + readonly status: "failed" + readonly message: string + readonly time: { + readonly created: number | "Infinity" | "-Infinity" | "NaN" + readonly expires: number | "Infinity" | "-Infinity" | "NaN" + } + } + | { + readonly status: "expired" + readonly time: { + readonly created: number | "Infinity" | "-Infinity" | "NaN" + readonly expires: number | "Infinity" | "-Infinity" | "NaN" + } + } +} + +export type IntegrationsAttemptCompleteInput = { + readonly attemptID: { readonly attemptID: string }["attemptID"] + readonly location?: { + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined + }["location"] + readonly code?: { readonly code?: string | undefined }["code"] +} + +export type IntegrationsAttemptCompleteOutput = void + +export type IntegrationsAttemptCancelInput = { + readonly attemptID: { readonly attemptID: string }["attemptID"] + readonly location?: { + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined + }["location"] +} + +export type IntegrationsAttemptCancelOutput = void + +export type CredentialsUpdateInput = { + readonly credentialID: { readonly credentialID: string }["credentialID"] + readonly location?: { + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined + }["location"] + readonly label: { readonly label: string }["label"] +} + +export type CredentialsUpdateOutput = void + +export type CredentialsRemoveInput = { + readonly credentialID: { readonly credentialID: string }["credentialID"] + readonly location?: { + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined + }["location"] +} + +export type CredentialsRemoveOutput = void + +export type PermissionsListRequestsInput = { + readonly location?: { + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined + }["location"] +} + +export type PermissionsListRequestsOutput = { + readonly location: { + readonly directory: string + readonly workspaceID?: string + readonly project: { readonly id: string; readonly directory: string } + } + readonly data: ReadonlyArray<{ + readonly id: string + readonly sessionID: string + readonly action: string + readonly resources: ReadonlyArray + readonly save?: ReadonlyArray + readonly metadata?: { readonly [x: string]: JsonValue } + readonly source?: { readonly type: "tool"; readonly messageID: string; readonly callID: string } + }> +} + +export type PermissionsListSavedInput = { + readonly projectID?: { readonly projectID?: string | undefined }["projectID"] +} + +export type PermissionsListSavedOutput = { + readonly data: ReadonlyArray<{ + readonly id: string + readonly projectID: string + readonly action: string + readonly resource: string + }> +}["data"] + +export type PermissionsRemoveSavedInput = { readonly id: { readonly id: string }["id"] } + +export type PermissionsRemoveSavedOutput = void + +export type PermissionsCreateInput = { + readonly sessionID: { readonly sessionID: string }["sessionID"] + readonly id?: { + readonly id?: string | null + readonly action: string + readonly resources: ReadonlyArray + readonly save?: ReadonlyArray + readonly metadata?: { readonly [x: string]: JsonValue } + readonly source?: { readonly type: "tool"; readonly messageID: string; readonly callID: string } + readonly agent?: string | null + }["id"] + readonly action: { + readonly id?: string | null + readonly action: string + readonly resources: ReadonlyArray + readonly save?: ReadonlyArray + readonly metadata?: { readonly [x: string]: JsonValue } + readonly source?: { readonly type: "tool"; readonly messageID: string; readonly callID: string } + readonly agent?: string | null + }["action"] + readonly resources: { + readonly id?: string | null + readonly action: string + readonly resources: ReadonlyArray + readonly save?: ReadonlyArray + readonly metadata?: { readonly [x: string]: JsonValue } + readonly source?: { readonly type: "tool"; readonly messageID: string; readonly callID: string } + readonly agent?: string | null + }["resources"] + readonly save?: { + readonly id?: string | null + readonly action: string + readonly resources: ReadonlyArray + readonly save?: ReadonlyArray + readonly metadata?: { readonly [x: string]: JsonValue } + readonly source?: { readonly type: "tool"; readonly messageID: string; readonly callID: string } + readonly agent?: string | null + }["save"] + readonly metadata?: { + readonly id?: string | null + readonly action: string + readonly resources: ReadonlyArray + readonly save?: ReadonlyArray + readonly metadata?: { readonly [x: string]: JsonValue } + readonly source?: { readonly type: "tool"; readonly messageID: string; readonly callID: string } + readonly agent?: string | null + }["metadata"] + readonly source?: { + readonly id?: string | null + readonly action: string + readonly resources: ReadonlyArray + readonly save?: ReadonlyArray + readonly metadata?: { readonly [x: string]: JsonValue } + readonly source?: { readonly type: "tool"; readonly messageID: string; readonly callID: string } + readonly agent?: string | null + }["source"] + readonly agent?: { + readonly id?: string | null + readonly action: string + readonly resources: ReadonlyArray + readonly save?: ReadonlyArray + readonly metadata?: { readonly [x: string]: JsonValue } + readonly source?: { readonly type: "tool"; readonly messageID: string; readonly callID: string } + readonly agent?: string | null + }["agent"] +} + +export type PermissionsCreateOutput = { + readonly data: { readonly id: string; readonly effect: "allow" | "deny" | "ask" } +}["data"] + +export type PermissionsListInput = { readonly sessionID: { readonly sessionID: string }["sessionID"] } + +export type PermissionsListOutput = { + readonly data: ReadonlyArray<{ + readonly id: string + readonly sessionID: string + readonly action: string + readonly resources: ReadonlyArray + readonly save?: ReadonlyArray + readonly metadata?: { readonly [x: string]: JsonValue } + readonly source?: { readonly type: "tool"; readonly messageID: string; readonly callID: string } + }> +}["data"] + +export type PermissionsGetInput = { + readonly sessionID: { readonly sessionID: string; readonly requestID: string }["sessionID"] + readonly requestID: { readonly sessionID: string; readonly requestID: string }["requestID"] +} + +export type PermissionsGetOutput = { + readonly data: { + readonly id: string + readonly sessionID: string + readonly action: string + readonly resources: ReadonlyArray + readonly save?: ReadonlyArray + readonly metadata?: { readonly [x: string]: JsonValue } + readonly source?: { readonly type: "tool"; readonly messageID: string; readonly callID: string } + } +}["data"] + +export type PermissionsReplyInput = { + readonly sessionID: { readonly sessionID: string; readonly requestID: string }["sessionID"] + readonly requestID: { readonly sessionID: string; readonly requestID: string }["requestID"] + readonly reply: { readonly reply: "once" | "always" | "reject"; readonly message?: string | undefined }["reply"] + readonly message?: { readonly reply: "once" | "always" | "reject"; readonly message?: string | undefined }["message"] +} + +export type PermissionsReplyOutput = void + +export type FilesListInput = { + readonly location?: { + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined + readonly path?: string | undefined + }["location"] + readonly path?: { + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined + readonly path?: string | undefined + }["path"] +} + +export type FilesListOutput = { + readonly location: { + readonly directory: string + readonly workspaceID?: string + readonly project: { readonly id: string; readonly directory: string } + } + readonly data: ReadonlyArray<{ readonly path: string; readonly type: "file" | "directory" }> +} + +export type FilesFindInput = { + readonly location?: { + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined + readonly query: string + readonly type?: "file" | "directory" | undefined + readonly limit?: number | undefined + }["location"] + readonly query: { + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined + readonly query: string + readonly type?: "file" | "directory" | undefined + readonly limit?: number | undefined + }["query"] + readonly type?: { + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined + readonly query: string + readonly type?: "file" | "directory" | undefined + readonly limit?: number | undefined + }["type"] + readonly limit?: { + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined + readonly query: string + readonly type?: "file" | "directory" | undefined + readonly limit?: number | undefined + }["limit"] +} + +export type FilesFindOutput = { + readonly location: { + readonly directory: string + readonly workspaceID?: string + readonly project: { readonly id: string; readonly directory: string } + } + readonly data: ReadonlyArray<{ readonly path: string; readonly type: "file" | "directory" }> +} + +export type CommandsListInput = { + readonly location?: { + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined + }["location"] +} + +export type CommandsListOutput = { + readonly location: { + readonly directory: string + readonly workspaceID?: string + readonly project: { readonly id: string; readonly directory: string } + } + readonly data: ReadonlyArray<{ + readonly name: string + readonly template: string + readonly description?: string + readonly agent?: string + readonly model?: { readonly id: string; readonly providerID: string; readonly variant?: string } + readonly subtask?: boolean + }> +} + +export type SkillsListInput = { + readonly location?: { + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined + }["location"] +} + +export type SkillsListOutput = { + readonly location: { + readonly directory: string + readonly workspaceID?: string + readonly project: { readonly id: string; readonly directory: string } + } + readonly data: ReadonlyArray<{ + readonly name: string + readonly description?: string + readonly slash?: boolean + readonly location: string + readonly content: string + }> +} + +export type EventsSubscribeOutput = OpenCodeEventEncoded + +export type PtysListInput = { + readonly location?: { + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined + }["location"] +} + +export type PtysListOutput = { + readonly location: { + readonly directory: string + readonly workspaceID?: string + readonly project: { readonly id: string; readonly directory: string } + } + readonly data: ReadonlyArray<{ + readonly id: string + readonly title: string + readonly command: string + readonly args: ReadonlyArray + readonly cwd: string + readonly status: "running" | "exited" + readonly pid: number + readonly exitCode?: number + }> +} + +export type PtysCreateInput = { + readonly location?: { + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined + }["location"] + readonly command?: { + readonly command?: string + readonly args?: ReadonlyArray + readonly cwd?: string + readonly title?: string + readonly env?: { readonly [x: string]: string } + }["command"] + readonly args?: { + readonly command?: string + readonly args?: ReadonlyArray + readonly cwd?: string + readonly title?: string + readonly env?: { readonly [x: string]: string } + }["args"] + readonly cwd?: { + readonly command?: string + readonly args?: ReadonlyArray + readonly cwd?: string + readonly title?: string + readonly env?: { readonly [x: string]: string } + }["cwd"] + readonly title?: { + readonly command?: string + readonly args?: ReadonlyArray + readonly cwd?: string + readonly title?: string + readonly env?: { readonly [x: string]: string } + }["title"] + readonly env?: { + readonly command?: string + readonly args?: ReadonlyArray + readonly cwd?: string + readonly title?: string + readonly env?: { readonly [x: string]: string } + }["env"] +} + +export type PtysCreateOutput = { + readonly location: { + readonly directory: string + readonly workspaceID?: string + readonly project: { readonly id: string; readonly directory: string } + } + readonly data: { + readonly id: string + readonly title: string + readonly command: string + readonly args: ReadonlyArray + readonly cwd: string + readonly status: "running" | "exited" + readonly pid: number + readonly exitCode?: number + } +} + +export type PtysGetInput = { + readonly ptyID: { readonly ptyID: string }["ptyID"] + readonly location?: { + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined + }["location"] +} + +export type PtysGetOutput = { + readonly location: { + readonly directory: string + readonly workspaceID?: string + readonly project: { readonly id: string; readonly directory: string } + } + readonly data: { + readonly id: string + readonly title: string + readonly command: string + readonly args: ReadonlyArray + readonly cwd: string + readonly status: "running" | "exited" + readonly pid: number + readonly exitCode?: number + } +} + +export type PtysUpdateInput = { + readonly ptyID: { readonly ptyID: string }["ptyID"] + readonly location?: { + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined + }["location"] + readonly title?: { + readonly title?: string + readonly size?: { readonly rows: number; readonly cols: number } + }["title"] + readonly size?: { readonly title?: string; readonly size?: { readonly rows: number; readonly cols: number } }["size"] +} + +export type PtysUpdateOutput = { + readonly location: { + readonly directory: string + readonly workspaceID?: string + readonly project: { readonly id: string; readonly directory: string } + } + readonly data: { + readonly id: string + readonly title: string + readonly command: string + readonly args: ReadonlyArray + readonly cwd: string + readonly status: "running" | "exited" + readonly pid: number + readonly exitCode?: number + } +} + +export type PtysRemoveInput = { + readonly ptyID: { readonly ptyID: string }["ptyID"] + readonly location?: { + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined + }["location"] +} + +export type PtysRemoveOutput = void + +export type QuestionsListRequestsInput = { + readonly location?: { + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined + }["location"] +} + +export type QuestionsListRequestsOutput = { + readonly location: { + readonly directory: string + readonly workspaceID?: string + readonly project: { readonly id: string; readonly directory: string } + } + readonly data: ReadonlyArray<{ + readonly id: string + readonly sessionID: string + readonly questions: ReadonlyArray<{ + readonly question: string + readonly header: string + readonly options: ReadonlyArray<{ readonly label: string; readonly description: string }> + readonly multiple?: boolean + readonly custom?: boolean + }> + readonly tool?: { readonly messageID: string; readonly callID: string } + }> +} + +export type QuestionsListInput = { readonly sessionID: { readonly sessionID: string }["sessionID"] } + +export type QuestionsListOutput = { + readonly data: ReadonlyArray<{ + readonly id: string + readonly sessionID: string + readonly questions: ReadonlyArray<{ + readonly question: string + readonly header: string + readonly options: ReadonlyArray<{ readonly label: string; readonly description: string }> + readonly multiple?: boolean + readonly custom?: boolean + }> + readonly tool?: { readonly messageID: string; readonly callID: string } + }> +}["data"] + +export type QuestionsReplyInput = { + readonly sessionID: { readonly sessionID: string; readonly requestID: string }["sessionID"] + readonly requestID: { readonly sessionID: string; readonly requestID: string }["requestID"] + readonly answers: { readonly answers: ReadonlyArray> }["answers"] +} + +export type QuestionsReplyOutput = void + +export type QuestionsRejectInput = { + readonly sessionID: { readonly sessionID: string; readonly requestID: string }["sessionID"] + readonly requestID: { readonly sessionID: string; readonly requestID: string }["requestID"] +} + +export type QuestionsRejectOutput = void + +export type ReferencesListInput = { + readonly location?: { + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined + }["location"] +} + +export type ReferencesListOutput = { + readonly location: { + readonly directory: string + readonly workspaceID?: string + readonly project: { readonly id: string; readonly directory: string } + } + readonly data: ReadonlyArray<{ + readonly name: string + readonly path: string + readonly description?: string + readonly hidden?: boolean + readonly source: + | { readonly type: "local"; readonly path: string; readonly description?: string; readonly hidden?: boolean } + | { + readonly type: "git" + readonly repository: string + readonly branch?: string + readonly description?: string + readonly hidden?: boolean + } + }> +} + +export type ProjectCopiesCreateInput = { + readonly projectID: { readonly projectID: string }["projectID"] + readonly location?: { + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined + }["location"] + readonly strategy: { readonly strategy: string; readonly directory: string; readonly name?: string }["strategy"] + readonly directory: { readonly strategy: string; readonly directory: string; readonly name?: string }["directory"] + readonly name?: { readonly strategy: string; readonly directory: string; readonly name?: string }["name"] +} + +export type ProjectCopiesCreateOutput = { readonly directory: string } + +export type ProjectCopiesRemoveInput = { + readonly projectID: { readonly projectID: string }["projectID"] + readonly location?: { + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined + }["location"] + readonly directory: { readonly directory: string; readonly force: boolean }["directory"] + readonly force: { readonly directory: string; readonly force: boolean }["force"] +} + +export type ProjectCopiesRemoveOutput = void + +export type ProjectCopiesRefreshInput = { + readonly projectID: { readonly projectID: string }["projectID"] + readonly location?: { + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined + }["location"] +} + +export type ProjectCopiesRefreshOutput = void diff --git a/packages/client/src/index.ts b/packages/client/src/index.ts new file mode 100644 index 0000000000..6955d7d8c5 --- /dev/null +++ b/packages/client/src/index.ts @@ -0,0 +1,2 @@ +export * from "./generated/index" +export type { EventsSubscribeOutput as OpenCodeEvent } from "./generated/types" diff --git a/packages/client/src/promise/api.ts b/packages/client/src/promise/api.ts deleted file mode 100644 index b4bc8635b2..0000000000 --- a/packages/client/src/promise/api.ts +++ /dev/null @@ -1,17 +0,0 @@ -type Client = ReturnType - -export type AgentApi = Client["agent"] -export type CommandApi = Client["command"] -export type EventApi = Client["event"] -export type IntegrationApi = Client["integration"] -export type ModelApi = Client["model"] -export type PluginApi = Client["plugin"] -export type ProviderApi = Client["provider"] -export type ReferenceApi = Client["reference"] -export type SessionApi = Client["session"] -export type SkillApi = Client["skill"] - -export interface CatalogApi { - readonly provider: ProviderApi - readonly model: ModelApi -} diff --git a/packages/client/src/promise/generated/client.ts b/packages/client/src/promise/generated/client.ts deleted file mode 100644 index bed325d976..0000000000 --- a/packages/client/src/promise/generated/client.ts +++ /dev/null @@ -1,1638 +0,0 @@ -import type { - HealthGetOutput, - ServerGetOutput, - LocationGetInput, - LocationGetOutput, - AgentListInput, - AgentListOutput, - PluginListInput, - PluginListOutput, - SessionListInput, - SessionListOutput, - SessionCreateInput, - SessionCreateOutput, - SessionActiveOutput, - SessionGetInput, - SessionGetOutput, - SessionRemoveInput, - SessionRemoveOutput, - SessionForkInput, - SessionForkOutput, - SessionSwitchAgentInput, - SessionSwitchAgentOutput, - SessionSwitchModelInput, - SessionSwitchModelOutput, - SessionRenameInput, - SessionRenameOutput, - SessionMoveInput, - SessionMoveOutput, - SessionPromptInput, - SessionPromptOutput, - SessionCommandInput, - SessionCommandOutput, - SessionSkillInput, - SessionSkillOutput, - SessionSyntheticInput, - SessionSyntheticOutput, - SessionShellInput, - SessionShellOutput, - SessionCompactInput, - SessionCompactOutput, - SessionWaitInput, - SessionWaitOutput, - SessionRevertStageInput, - SessionRevertStageOutput, - SessionRevertClearInput, - SessionRevertClearOutput, - SessionRevertCommitInput, - SessionRevertCommitOutput, - SessionContextInput, - SessionContextOutput, - SessionPendingListInput, - SessionPendingListOutput, - SessionInstructionsEntryListInput, - SessionInstructionsEntryListOutput, - SessionInstructionsEntryPutInput, - SessionInstructionsEntryPutOutput, - SessionInstructionsEntryRemoveInput, - SessionInstructionsEntryRemoveOutput, - SessionLogInput, - SessionLogOutput, - SessionInterruptInput, - SessionInterruptOutput, - SessionBackgroundInput, - SessionBackgroundOutput, - SessionMessageInput, - SessionMessageOutput, - MessageListInput, - MessageListOutput, - ModelListInput, - ModelListOutput, - ModelDefaultInput, - ModelDefaultOutput, - GenerateTextInput, - GenerateTextOutput, - ProviderListInput, - ProviderListOutput, - ProviderGetInput, - ProviderGetOutput, - IntegrationListInput, - IntegrationListOutput, - IntegrationGetInput, - IntegrationGetOutput, - IntegrationConnectKeyInput, - IntegrationConnectKeyOutput, - IntegrationConnectOauthInput, - IntegrationConnectOauthOutput, - IntegrationAttemptStatusInput, - IntegrationAttemptStatusOutput, - IntegrationAttemptCompleteInput, - IntegrationAttemptCompleteOutput, - IntegrationAttemptCancelInput, - IntegrationAttemptCancelOutput, - McpListInput, - McpListOutput, - McpResourceCatalogInput, - McpResourceCatalogOutput, - CredentialUpdateInput, - CredentialUpdateOutput, - CredentialRemoveInput, - CredentialRemoveOutput, - ProjectListOutput, - ProjectCurrentInput, - ProjectCurrentOutput, - ProjectDirectoriesInput, - ProjectDirectoriesOutput, - FormRequestListInput, - FormRequestListOutput, - FormListInput, - FormListOutput, - FormCreateInput, - FormCreateOutput, - FormGetInput, - FormGetOutput, - FormStateInput, - FormStateOutput, - FormReplyInput, - FormReplyOutput, - FormCancelInput, - FormCancelOutput, - PermissionRequestListInput, - PermissionRequestListOutput, - PermissionSavedListInput, - PermissionSavedListOutput, - PermissionSavedRemoveInput, - PermissionSavedRemoveOutput, - PermissionCreateInput, - PermissionCreateOutput, - PermissionListInput, - PermissionListOutput, - PermissionGetInput, - PermissionGetOutput, - PermissionReplyInput, - PermissionReplyOutput, - FileReadInput, - FileReadOutput, - FileListInput, - FileListOutput, - FileFindInput, - FileFindOutput, - CommandListInput, - CommandListOutput, - SkillListInput, - SkillListOutput, - EventSubscribeOutput, - PtyListInput, - PtyListOutput, - PtyCreateInput, - PtyCreateOutput, - PtyGetInput, - PtyGetOutput, - PtyUpdateInput, - PtyUpdateOutput, - PtyRemoveInput, - PtyRemoveOutput, - ShellListInput, - ShellListOutput, - ShellCreateInput, - ShellCreateOutput, - ShellGetInput, - ShellGetOutput, - ShellTimeoutInput, - ShellTimeoutOutput, - ShellOutputInput, - ShellOutputOutput, - ShellRemoveInput, - ShellRemoveOutput, - QuestionRequestListInput, - QuestionRequestListOutput, - QuestionListInput, - QuestionListOutput, - QuestionReplyInput, - QuestionReplyOutput, - QuestionRejectInput, - QuestionRejectOutput, - ReferenceListInput, - ReferenceListOutput, - ProjectCopyCreateInput, - ProjectCopyCreateOutput, - ProjectCopyRemoveInput, - ProjectCopyRemoveOutput, - ProjectCopyRefreshInput, - ProjectCopyRefreshOutput, - VcsStatusInput, - VcsStatusOutput, - VcsDiffInput, - VcsDiffOutput, - DebugLocationListOutput, - DebugLocationEvictInput, - DebugLocationEvictOutput, -} from "./types" -import { ClientError } from "./client-error" - -export interface ClientOptions { - readonly baseUrl: string - readonly fetch?: typeof globalThis.fetch - readonly headers?: RequestInit["headers"] -} - -export interface RequestOptions { - readonly signal?: AbortSignal - readonly headers?: RequestInit["headers"] -} - -interface RequestDescriptor { - readonly method: string - readonly path: string - readonly query?: Record - readonly headers?: Record - readonly body?: unknown - readonly successStatus: number - readonly declaredStatuses: ReadonlyArray - readonly empty: boolean - readonly binary?: true -} - -const maxSseEventBytes = 16 * 1024 * 1024 - -export function make(options: ClientOptions) { - const fetch = options.fetch ?? globalThis.fetch - - const prepare = (descriptor: RequestDescriptor, requestOptions?: RequestOptions) => { - const url = new URL(descriptor.path, options.baseUrl) - for (const [key, value] of Object.entries(descriptor.query ?? {})) appendQuery(url.searchParams, key, value) - const headers = new Headers(options.headers) - for (const [key, value] of Object.entries(descriptor.headers ?? {})) { - if (value !== undefined && value !== null) headers.set(key, String(value)) - } - for (const [key, value] of new Headers(requestOptions?.headers)) headers.set(key, value) - if (descriptor.body !== undefined && !headers.has("content-type")) headers.set("content-type", "application/json") - return { - url, - init: { - method: descriptor.method, - signal: requestOptions?.signal, - headers, - body: descriptor.body === undefined ? undefined : JSON.stringify(descriptor.body), - } satisfies RequestInit, - } - } - - const execute = async (descriptor: RequestDescriptor, requestOptions?: RequestOptions) => { - try { - const prepared = prepare(descriptor, requestOptions) - return await fetch(prepared.url, prepared.init) - } catch (cause) { - throw new ClientError("Transport", { cause }) - } - } - - const responseError = async (response: Response, descriptor: RequestDescriptor): Promise => { - if (descriptor.declaredStatuses.includes(response.status)) throw await json(response) - try { - await response.body?.cancel() - } catch {} - throw new ClientError("UnexpectedStatus", { cause: { status: response.status } }) - } - - const request = async (descriptor: RequestDescriptor, requestOptions?: RequestOptions): Promise => { - const response = await execute(descriptor, requestOptions) - if (response.status !== descriptor.successStatus) return responseError(response, descriptor) - if (descriptor.binary) return new Uint8Array(await response.arrayBuffer()) as A - if (descriptor.empty) { - try { - await response.body?.cancel() - } catch {} - return undefined as A - } - return (await json(response)) as A - } - - const sse = (descriptor: RequestDescriptor, requestOptions?: RequestOptions): AsyncIterable => ({ - async *[Symbol.asyncIterator]() { - const response = await execute(descriptor, requestOptions) - if (response.status !== descriptor.successStatus) await responseError(response, descriptor) - if (!isContentType(response, "text/event-stream")) { - try { - await response.body?.cancel() - } catch {} - throw new ClientError("UnsupportedContentType") - } - if (response.body === null) throw new ClientError("MalformedResponse") - const reader = response.body.getReader() - const decoder = new TextDecoder() - let buffer = "" - try { - while (true) { - let next - try { - next = await reader.read() - } catch (cause) { - throw new ClientError("Transport", { cause }) - } - buffer += decoder.decode(next.value, { stream: !next.done }) - if (buffer.length > maxSseEventBytes) throw new ClientError("SseEventTooLarge") - const trailingCarriageReturn = !next.done && buffer.endsWith("\r") - if (trailingCarriageReturn) buffer = buffer.slice(0, -1) - buffer = buffer.replaceAll("\r\n", "\n").replaceAll("\r", "\n") - if (trailingCarriageReturn) buffer += "\r" - if (next.done && buffer !== "") buffer += "\n\n" - let boundary = buffer.indexOf("\n\n") - while (boundary >= 0) { - const block = buffer.slice(0, boundary) - buffer = buffer.slice(boundary + 2) - const data = block - .split("\n") - .flatMap((line) => (line.startsWith("data:") ? [line.slice(5).trimStart()] : [])) - .join("\n") - if (data !== "") { - try { - yield JSON.parse(data) as A - } catch (cause) { - throw new ClientError("MalformedResponse", { cause }) - } - } - boundary = buffer.indexOf("\n\n") - } - if (next.done) return - } - } finally { - try { - await reader.cancel() - } catch {} - reader.releaseLock() - } - }, - }) - - return { - health: { - get: (requestOptions?: RequestOptions) => - request( - { method: "GET", path: `/api/health`, successStatus: 200, declaredStatuses: [401, 400], empty: false }, - requestOptions, - ), - }, - server: { - get: (requestOptions?: RequestOptions) => - request( - { method: "GET", path: `/api/server`, successStatus: 200, declaredStatuses: [401, 400], empty: false }, - requestOptions, - ), - }, - location: { - get: (input?: LocationGetInput, requestOptions?: RequestOptions) => - request( - { - method: "GET", - path: `/api/location`, - query: { location: input?.["location"] }, - successStatus: 200, - declaredStatuses: [401, 400], - empty: false, - }, - requestOptions, - ), - }, - agent: { - list: (input?: AgentListInput, requestOptions?: RequestOptions) => - request( - { - method: "GET", - path: `/api/agent`, - query: { location: input?.["location"] }, - successStatus: 200, - declaredStatuses: [401, 400], - empty: false, - }, - requestOptions, - ), - }, - plugin: { - list: (input?: PluginListInput, requestOptions?: RequestOptions) => - request( - { - method: "GET", - path: `/api/plugin`, - query: { location: input?.["location"] }, - successStatus: 200, - declaredStatuses: [401, 400], - empty: false, - }, - requestOptions, - ), - }, - session: { - list: (input?: SessionListInput, requestOptions?: RequestOptions) => - request( - { - method: "GET", - path: `/api/session`, - query: { - workspace: input?.["workspace"], - limit: input?.["limit"], - order: input?.["order"], - search: input?.["search"], - parentID: input?.["parentID"], - directory: input?.["directory"], - project: input?.["project"], - subpath: input?.["subpath"], - cursor: input?.["cursor"], - }, - successStatus: 200, - declaredStatuses: [400, 401], - empty: false, - }, - requestOptions, - ), - create: (input?: SessionCreateInput, requestOptions?: RequestOptions) => - request<{ readonly data: SessionCreateOutput }>( - { - method: "POST", - path: `/api/session`, - body: { - id: input?.["id"], - agent: input?.["agent"], - model: input?.["model"], - location: input?.["location"], - }, - successStatus: 200, - declaredStatuses: [401, 400], - empty: false, - }, - requestOptions, - ).then((value) => value.data), - active: (requestOptions?: RequestOptions) => - request<{ readonly data: SessionActiveOutput }>( - { - method: "GET", - path: `/api/session/active`, - successStatus: 200, - declaredStatuses: [401, 400], - empty: false, - }, - requestOptions, - ).then((value) => value.data), - get: (input: SessionGetInput, requestOptions?: RequestOptions) => - request<{ readonly data: SessionGetOutput }>( - { - method: "GET", - path: `/api/session/${encodeURIComponent(input.sessionID)}`, - successStatus: 200, - declaredStatuses: [404, 400, 401], - empty: false, - }, - requestOptions, - ).then((value) => value.data), - remove: (input: SessionRemoveInput, requestOptions?: RequestOptions) => - request( - { - method: "DELETE", - path: `/api/session/${encodeURIComponent(input.sessionID)}`, - successStatus: 204, - declaredStatuses: [404, 400, 401], - empty: true, - }, - requestOptions, - ), - fork: (input: SessionForkInput, requestOptions?: RequestOptions) => - request<{ readonly data: SessionForkOutput }>( - { - method: "POST", - path: `/api/session/${encodeURIComponent(input.sessionID)}/fork`, - body: { messageID: input["messageID"] }, - successStatus: 200, - declaredStatuses: [404, 400, 401], - empty: false, - }, - requestOptions, - ).then((value) => value.data), - switchAgent: (input: SessionSwitchAgentInput, requestOptions?: RequestOptions) => - request( - { - method: "POST", - path: `/api/session/${encodeURIComponent(input.sessionID)}/agent`, - body: { agent: input["agent"] }, - successStatus: 204, - declaredStatuses: [404, 400, 401], - empty: true, - }, - requestOptions, - ), - switchModel: (input: SessionSwitchModelInput, requestOptions?: RequestOptions) => - request( - { - method: "POST", - path: `/api/session/${encodeURIComponent(input.sessionID)}/model`, - body: { model: input["model"] }, - successStatus: 204, - declaredStatuses: [404, 400, 401], - empty: true, - }, - requestOptions, - ), - rename: (input: SessionRenameInput, requestOptions?: RequestOptions) => - request( - { - method: "POST", - path: `/api/session/${encodeURIComponent(input.sessionID)}/rename`, - body: { title: input["title"] }, - successStatus: 204, - declaredStatuses: [404, 400, 401], - empty: true, - }, - requestOptions, - ), - move: (input: SessionMoveInput, requestOptions?: RequestOptions) => - request( - { - method: "POST", - path: `/api/session/${encodeURIComponent(input.sessionID)}/move`, - body: { destination: input["destination"], moveChanges: input["moveChanges"] }, - successStatus: 204, - declaredStatuses: [404, 400, 401], - empty: true, - }, - requestOptions, - ), - prompt: (input: SessionPromptInput, requestOptions?: RequestOptions) => - request<{ readonly data: SessionPromptOutput }>( - { - method: "POST", - path: `/api/session/${encodeURIComponent(input.sessionID)}/prompt`, - body: { - id: input["id"], - text: input["text"], - files: input["files"], - agents: input["agents"], - metadata: input["metadata"], - delivery: input["delivery"], - resume: input["resume"], - }, - successStatus: 200, - declaredStatuses: [409, 400, 404, 401], - empty: false, - }, - requestOptions, - ).then((value) => value.data), - command: (input: SessionCommandInput, requestOptions?: RequestOptions) => - request<{ readonly data: SessionCommandOutput }>( - { - method: "POST", - path: `/api/session/${encodeURIComponent(input.sessionID)}/command`, - body: { - id: input["id"], - command: input["command"], - arguments: input["arguments"], - agent: input["agent"], - model: input["model"], - files: input["files"], - agents: input["agents"], - delivery: input["delivery"], - resume: input["resume"], - }, - successStatus: 200, - declaredStatuses: [409, 400, 404, 500, 401], - empty: false, - }, - requestOptions, - ).then((value) => value.data), - skill: (input: SessionSkillInput, requestOptions?: RequestOptions) => - request( - { - method: "POST", - path: `/api/session/${encodeURIComponent(input.sessionID)}/skill`, - body: { id: input["id"], skill: input["skill"], resume: input["resume"] }, - successStatus: 204, - declaredStatuses: [404, 400, 401], - empty: true, - }, - requestOptions, - ), - synthetic: (input: SessionSyntheticInput, requestOptions?: RequestOptions) => - request<{ readonly data: SessionSyntheticOutput }>( - { - method: "POST", - path: `/api/session/${encodeURIComponent(input.sessionID)}/synthetic`, - body: { - id: input["id"], - text: input["text"], - description: input["description"], - metadata: input["metadata"], - delivery: input["delivery"], - resume: input["resume"], - }, - successStatus: 200, - declaredStatuses: [409, 404, 400, 401], - empty: false, - }, - requestOptions, - ).then((value) => value.data), - shell: (input: SessionShellInput, requestOptions?: RequestOptions) => - request( - { - method: "POST", - path: `/api/session/${encodeURIComponent(input.sessionID)}/shell`, - body: { id: input["id"], command: input["command"] }, - successStatus: 204, - declaredStatuses: [404, 400, 401], - empty: true, - }, - requestOptions, - ), - compact: (input: SessionCompactInput, requestOptions?: RequestOptions) => - request<{ readonly data: SessionCompactOutput }>( - { - method: "POST", - path: `/api/session/${encodeURIComponent(input.sessionID)}/compact`, - body: { id: input["id"] }, - successStatus: 200, - declaredStatuses: [409, 404, 400, 401], - empty: false, - }, - requestOptions, - ).then((value) => value.data), - wait: (input: SessionWaitInput, requestOptions?: RequestOptions) => - request( - { - method: "POST", - path: `/api/session/${encodeURIComponent(input.sessionID)}/wait`, - successStatus: 204, - declaredStatuses: [404, 503, 400, 401], - empty: true, - }, - requestOptions, - ), - revert: { - stage: (input: SessionRevertStageInput, requestOptions?: RequestOptions) => - request<{ readonly data: SessionRevertStageOutput }>( - { - method: "POST", - path: `/api/session/${encodeURIComponent(input.sessionID)}/revert/stage`, - body: { messageID: input["messageID"], files: input["files"] }, - successStatus: 200, - declaredStatuses: [404, 409, 500, 400, 401], - empty: false, - }, - requestOptions, - ).then((value) => value.data), - clear: (input: SessionRevertClearInput, requestOptions?: RequestOptions) => - request( - { - method: "POST", - path: `/api/session/${encodeURIComponent(input.sessionID)}/revert/clear`, - successStatus: 204, - declaredStatuses: [404, 409, 500, 400, 401], - empty: true, - }, - requestOptions, - ), - commit: (input: SessionRevertCommitInput, requestOptions?: RequestOptions) => - request( - { - method: "POST", - path: `/api/session/${encodeURIComponent(input.sessionID)}/revert/commit`, - successStatus: 204, - declaredStatuses: [404, 409, 400, 401], - empty: true, - }, - requestOptions, - ), - }, - context: (input: SessionContextInput, requestOptions?: RequestOptions) => - request<{ readonly data: SessionContextOutput }>( - { - method: "GET", - path: `/api/session/${encodeURIComponent(input.sessionID)}/context`, - successStatus: 200, - declaredStatuses: [404, 500, 400, 401], - empty: false, - }, - requestOptions, - ).then((value) => value.data), - pending: { - list: (input: SessionPendingListInput, requestOptions?: RequestOptions) => - request<{ readonly data: SessionPendingListOutput }>( - { - method: "GET", - path: `/api/session/${encodeURIComponent(input.sessionID)}/pending`, - successStatus: 200, - declaredStatuses: [404, 400, 401], - empty: false, - }, - requestOptions, - ).then((value) => value.data), - }, - instructions: { - entry: { - list: (input: SessionInstructionsEntryListInput, requestOptions?: RequestOptions) => - request<{ readonly data: SessionInstructionsEntryListOutput }>( - { - method: "GET", - path: `/api/session/${encodeURIComponent(input.sessionID)}/instructions/entries`, - successStatus: 200, - declaredStatuses: [404, 400, 401], - empty: false, - }, - requestOptions, - ).then((value) => value.data), - put: (input: SessionInstructionsEntryPutInput, requestOptions?: RequestOptions) => - request( - { - method: "PUT", - path: `/api/session/${encodeURIComponent(input.sessionID)}/instructions/entries/${encodeURIComponent(input.key)}`, - body: { value: input["value"] }, - successStatus: 204, - declaredStatuses: [404, 413, 400, 401], - empty: true, - }, - requestOptions, - ), - remove: (input: SessionInstructionsEntryRemoveInput, requestOptions?: RequestOptions) => - request( - { - method: "DELETE", - path: `/api/session/${encodeURIComponent(input.sessionID)}/instructions/entries/${encodeURIComponent(input.key)}`, - successStatus: 204, - declaredStatuses: [404, 400, 401], - empty: true, - }, - requestOptions, - ), - }, - }, - log: (input: SessionLogInput, requestOptions?: RequestOptions): AsyncIterable => - sse( - { - method: "GET", - path: `/api/experimental/session/${encodeURIComponent(input.sessionID)}/log`, - query: { after: input["after"], follow: input["follow"] }, - successStatus: 200, - declaredStatuses: [404, 400, 401], - empty: false, - }, - requestOptions, - ), - interrupt: (input: SessionInterruptInput, requestOptions?: RequestOptions) => - request( - { - method: "POST", - path: `/api/session/${encodeURIComponent(input.sessionID)}/interrupt`, - successStatus: 204, - declaredStatuses: [404, 400, 401], - empty: true, - }, - requestOptions, - ), - background: (input: SessionBackgroundInput, requestOptions?: RequestOptions) => - request( - { - method: "POST", - path: `/api/session/${encodeURIComponent(input.sessionID)}/background`, - successStatus: 204, - declaredStatuses: [404, 400, 401], - empty: true, - }, - requestOptions, - ), - message: (input: SessionMessageInput, requestOptions?: RequestOptions) => - request<{ readonly data: SessionMessageOutput }>( - { - method: "GET", - path: `/api/session/${encodeURIComponent(input.sessionID)}/message/${encodeURIComponent(input.messageID)}`, - successStatus: 200, - declaredStatuses: [404, 400, 401], - empty: false, - }, - requestOptions, - ).then((value) => value.data), - }, - message: { - list: (input: MessageListInput, requestOptions?: RequestOptions) => - request( - { - method: "GET", - path: `/api/session/${encodeURIComponent(input.sessionID)}/message`, - query: { limit: input["limit"], order: input["order"], cursor: input["cursor"] }, - successStatus: 200, - declaredStatuses: [400, 404, 500, 401], - empty: false, - }, - requestOptions, - ), - }, - model: { - list: (input?: ModelListInput, requestOptions?: RequestOptions) => - request( - { - method: "GET", - path: `/api/model`, - query: { location: input?.["location"] }, - successStatus: 200, - declaredStatuses: [503, 401, 400], - empty: false, - }, - requestOptions, - ), - default: (input?: ModelDefaultInput, requestOptions?: RequestOptions) => - request( - { - method: "GET", - path: `/api/model/default`, - query: { location: input?.["location"] }, - successStatus: 200, - declaredStatuses: [503, 401, 400], - empty: false, - }, - requestOptions, - ), - }, - generate: { - text: (input: GenerateTextInput, requestOptions?: RequestOptions) => - request<{ readonly data: GenerateTextOutput }>( - { - method: "POST", - path: `/api/generate`, - query: { location: input["location"] }, - body: { prompt: input["prompt"], model: input["model"] }, - successStatus: 200, - declaredStatuses: [400, 503, 401], - empty: false, - }, - requestOptions, - ).then((value) => value.data), - }, - provider: { - list: (input?: ProviderListInput, requestOptions?: RequestOptions) => - request( - { - method: "GET", - path: `/api/provider`, - query: { location: input?.["location"] }, - successStatus: 200, - declaredStatuses: [503, 401, 400], - empty: false, - }, - requestOptions, - ), - get: (input: ProviderGetInput, requestOptions?: RequestOptions) => - request( - { - method: "GET", - path: `/api/provider/${encodeURIComponent(input.providerID)}`, - query: { location: input["location"] }, - successStatus: 200, - declaredStatuses: [404, 503, 401, 400], - empty: false, - }, - requestOptions, - ), - }, - integration: { - list: (input?: IntegrationListInput, requestOptions?: RequestOptions) => - request( - { - method: "GET", - path: `/api/integration`, - query: { location: input?.["location"] }, - successStatus: 200, - declaredStatuses: [401, 400], - empty: false, - }, - requestOptions, - ), - get: (input: IntegrationGetInput, requestOptions?: RequestOptions) => - request( - { - method: "GET", - path: `/api/integration/${encodeURIComponent(input.integrationID)}`, - query: { location: input["location"] }, - successStatus: 200, - declaredStatuses: [401, 400], - empty: false, - }, - requestOptions, - ), - connect: { - key: (input: IntegrationConnectKeyInput, requestOptions?: RequestOptions) => - request( - { - method: "POST", - path: `/api/integration/${encodeURIComponent(input.integrationID)}/connect/key`, - query: { location: input["location"] }, - body: { key: input["key"], label: input["label"] }, - successStatus: 204, - declaredStatuses: [400, 401], - empty: true, - }, - requestOptions, - ), - oauth: (input: IntegrationConnectOauthInput, requestOptions?: RequestOptions) => - request( - { - method: "POST", - path: `/api/integration/${encodeURIComponent(input.integrationID)}/connect/oauth`, - query: { location: input["location"] }, - body: { methodID: input["methodID"], inputs: input["inputs"], label: input["label"] }, - successStatus: 200, - declaredStatuses: [400, 401], - empty: false, - }, - requestOptions, - ), - }, - attempt: { - status: (input: IntegrationAttemptStatusInput, requestOptions?: RequestOptions) => - request( - { - method: "GET", - path: `/api/integration/attempt/${encodeURIComponent(input.attemptID)}`, - query: { location: input["location"] }, - successStatus: 200, - declaredStatuses: [401, 400], - empty: false, - }, - requestOptions, - ), - complete: (input: IntegrationAttemptCompleteInput, requestOptions?: RequestOptions) => - request( - { - method: "POST", - path: `/api/integration/attempt/${encodeURIComponent(input.attemptID)}/complete`, - query: { location: input["location"] }, - body: { code: input["code"] }, - successStatus: 204, - declaredStatuses: [400, 401], - empty: true, - }, - requestOptions, - ), - cancel: (input: IntegrationAttemptCancelInput, requestOptions?: RequestOptions) => - request( - { - method: "DELETE", - path: `/api/integration/attempt/${encodeURIComponent(input.attemptID)}`, - query: { location: input["location"] }, - successStatus: 204, - declaredStatuses: [401, 400], - empty: true, - }, - requestOptions, - ), - }, - }, - mcp: { - list: (input?: McpListInput, requestOptions?: RequestOptions) => - request( - { - method: "GET", - path: `/api/mcp`, - query: { location: input?.["location"] }, - successStatus: 200, - declaredStatuses: [401, 400], - empty: false, - }, - requestOptions, - ), - resource: { - catalog: (input?: McpResourceCatalogInput, requestOptions?: RequestOptions) => - request( - { - method: "GET", - path: `/api/mcp/resource`, - query: { location: input?.["location"] }, - successStatus: 200, - declaredStatuses: [401, 400], - empty: false, - }, - requestOptions, - ), - }, - }, - credential: { - update: (input: CredentialUpdateInput, requestOptions?: RequestOptions) => - request( - { - method: "PATCH", - path: `/api/credential/${encodeURIComponent(input.credentialID)}`, - query: { location: input["location"] }, - body: { label: input["label"] }, - successStatus: 204, - declaredStatuses: [401, 400], - empty: true, - }, - requestOptions, - ), - remove: (input: CredentialRemoveInput, requestOptions?: RequestOptions) => - request( - { - method: "DELETE", - path: `/api/credential/${encodeURIComponent(input.credentialID)}`, - query: { location: input["location"] }, - successStatus: 204, - declaredStatuses: [401, 400], - empty: true, - }, - requestOptions, - ), - }, - project: { - list: (requestOptions?: RequestOptions) => - request( - { method: "GET", path: `/api/project`, successStatus: 200, declaredStatuses: [401, 400], empty: false }, - requestOptions, - ), - current: (input?: ProjectCurrentInput, requestOptions?: RequestOptions) => - request( - { - method: "GET", - path: `/api/project/current`, - query: { location: input?.["location"] }, - successStatus: 200, - declaredStatuses: [401, 400], - empty: false, - }, - requestOptions, - ), - directories: (input: ProjectDirectoriesInput, requestOptions?: RequestOptions) => - request( - { - method: "GET", - path: `/api/project/${encodeURIComponent(input.projectID)}/directories`, - query: { location: input["location"] }, - successStatus: 200, - declaredStatuses: [401, 400], - empty: false, - }, - requestOptions, - ), - }, - form: { - request: { - list: (input?: FormRequestListInput, requestOptions?: RequestOptions) => - request( - { - method: "GET", - path: `/api/form/request`, - query: { location: input?.["location"] }, - successStatus: 200, - declaredStatuses: [401, 400], - empty: false, - }, - requestOptions, - ), - }, - list: (input: FormListInput, requestOptions?: RequestOptions) => - request<{ readonly data: FormListOutput }>( - { - method: "GET", - path: `/api/session/${encodeURIComponent(input.sessionID)}/form`, - successStatus: 200, - declaredStatuses: [404, 400, 401], - empty: false, - }, - requestOptions, - ).then((value) => value.data), - create: (input: FormCreateInput, requestOptions?: RequestOptions) => - request<{ readonly data: FormCreateOutput }>( - { - method: "POST", - path: `/api/session/${encodeURIComponent(input.sessionID)}/form`, - body: { id: input["id"], title: input["title"], metadata: input["metadata"], fields: input["fields"] }, - successStatus: 200, - declaredStatuses: [404, 409, 400, 401], - empty: false, - }, - requestOptions, - ).then((value) => value.data), - get: (input: FormGetInput, requestOptions?: RequestOptions) => - request<{ readonly data: FormGetOutput }>( - { - method: "GET", - path: `/api/session/${encodeURIComponent(input.sessionID)}/form/${encodeURIComponent(input.formID)}`, - successStatus: 200, - declaredStatuses: [404, 400, 401], - empty: false, - }, - requestOptions, - ).then((value) => value.data), - state: (input: FormStateInput, requestOptions?: RequestOptions) => - request<{ readonly data: FormStateOutput }>( - { - method: "GET", - path: `/api/session/${encodeURIComponent(input.sessionID)}/form/${encodeURIComponent(input.formID)}/state`, - successStatus: 200, - declaredStatuses: [404, 400, 401], - empty: false, - }, - requestOptions, - ).then((value) => value.data), - reply: (input: FormReplyInput, requestOptions?: RequestOptions) => - request( - { - method: "POST", - path: `/api/session/${encodeURIComponent(input.sessionID)}/form/${encodeURIComponent(input.formID)}/reply`, - body: { answer: input["answer"] }, - successStatus: 204, - declaredStatuses: [404, 409, 400, 401], - empty: true, - }, - requestOptions, - ), - cancel: (input: FormCancelInput, requestOptions?: RequestOptions) => - request( - { - method: "POST", - path: `/api/session/${encodeURIComponent(input.sessionID)}/form/${encodeURIComponent(input.formID)}/cancel`, - successStatus: 204, - declaredStatuses: [404, 409, 400, 401], - empty: true, - }, - requestOptions, - ), - }, - permission: { - request: { - list: (input?: PermissionRequestListInput, requestOptions?: RequestOptions) => - request( - { - method: "GET", - path: `/api/permission/request`, - query: { location: input?.["location"] }, - successStatus: 200, - declaredStatuses: [401, 400], - empty: false, - }, - requestOptions, - ), - }, - saved: { - list: (input?: PermissionSavedListInput, requestOptions?: RequestOptions) => - request<{ readonly data: PermissionSavedListOutput }>( - { - method: "GET", - path: `/api/permission/saved`, - query: { projectID: input?.["projectID"] }, - successStatus: 200, - declaredStatuses: [401, 400], - empty: false, - }, - requestOptions, - ).then((value) => value.data), - remove: (input: PermissionSavedRemoveInput, requestOptions?: RequestOptions) => - request( - { - method: "DELETE", - path: `/api/permission/saved/${encodeURIComponent(input.id)}`, - successStatus: 204, - declaredStatuses: [401, 400], - empty: true, - }, - requestOptions, - ), - }, - create: (input: PermissionCreateInput, requestOptions?: RequestOptions) => - request<{ readonly data: PermissionCreateOutput }>( - { - method: "POST", - path: `/api/session/${encodeURIComponent(input.sessionID)}/permission`, - body: { - id: input["id"], - action: input["action"], - resources: input["resources"], - save: input["save"], - metadata: input["metadata"], - source: input["source"], - agent: input["agent"], - }, - successStatus: 200, - declaredStatuses: [404, 400, 401], - empty: false, - }, - requestOptions, - ).then((value) => value.data), - list: (input: PermissionListInput, requestOptions?: RequestOptions) => - request<{ readonly data: PermissionListOutput }>( - { - method: "GET", - path: `/api/session/${encodeURIComponent(input.sessionID)}/permission`, - successStatus: 200, - declaredStatuses: [404, 400, 401], - empty: false, - }, - requestOptions, - ).then((value) => value.data), - get: (input: PermissionGetInput, requestOptions?: RequestOptions) => - request<{ readonly data: PermissionGetOutput }>( - { - method: "GET", - path: `/api/session/${encodeURIComponent(input.sessionID)}/permission/${encodeURIComponent(input.requestID)}`, - successStatus: 200, - declaredStatuses: [404, 400, 401], - empty: false, - }, - requestOptions, - ).then((value) => value.data), - reply: (input: PermissionReplyInput, requestOptions?: RequestOptions) => - request( - { - method: "POST", - path: `/api/session/${encodeURIComponent(input.sessionID)}/permission/${encodeURIComponent(input.requestID)}/reply`, - body: { reply: input["reply"], message: input["message"] }, - successStatus: 204, - declaredStatuses: [404, 400, 401], - empty: true, - }, - requestOptions, - ), - }, - file: { - read: (input: FileReadInput, requestOptions?: RequestOptions) => - request( - { - method: "GET", - path: `/api/fs/read/${encodePath(input.path)}`, - query: { location: input["location"] }, - successStatus: 200, - declaredStatuses: [401, 400], - empty: false, - binary: true, - }, - requestOptions, - ), - list: (input?: FileListInput, requestOptions?: RequestOptions) => - request( - { - method: "GET", - path: `/api/fs/list`, - query: { location: input?.["location"], path: input?.["path"] }, - successStatus: 200, - declaredStatuses: [401, 400], - empty: false, - }, - requestOptions, - ), - find: (input: FileFindInput, requestOptions?: RequestOptions) => - request( - { - method: "GET", - path: `/api/fs/find`, - query: { location: input["location"], query: input["query"], type: input["type"], limit: input["limit"] }, - successStatus: 200, - declaredStatuses: [401, 400], - empty: false, - }, - requestOptions, - ), - }, - command: { - list: (input?: CommandListInput, requestOptions?: RequestOptions) => - request( - { - method: "GET", - path: `/api/command`, - query: { location: input?.["location"] }, - successStatus: 200, - declaredStatuses: [401, 400], - empty: false, - }, - requestOptions, - ), - }, - skill: { - list: (input?: SkillListInput, requestOptions?: RequestOptions) => - request( - { - method: "GET", - path: `/api/skill`, - query: { location: input?.["location"] }, - successStatus: 200, - declaredStatuses: [401, 400], - empty: false, - }, - requestOptions, - ), - }, - event: { - subscribe: (requestOptions?: RequestOptions): AsyncIterable => - sse( - { method: "GET", path: `/api/event`, successStatus: 200, declaredStatuses: [401, 400], empty: false }, - requestOptions, - ), - }, - pty: { - list: (input?: PtyListInput, requestOptions?: RequestOptions) => - request( - { - method: "GET", - path: `/api/pty`, - query: { location: input?.["location"] }, - successStatus: 200, - declaredStatuses: [401, 400], - empty: false, - }, - requestOptions, - ), - create: (input?: PtyCreateInput, requestOptions?: RequestOptions) => - request( - { - method: "POST", - path: `/api/pty`, - query: { location: input?.["location"] }, - body: { - command: input?.["command"], - args: input?.["args"], - cwd: input?.["cwd"], - title: input?.["title"], - env: input?.["env"], - }, - successStatus: 200, - declaredStatuses: [401, 400], - empty: false, - }, - requestOptions, - ), - get: (input: PtyGetInput, requestOptions?: RequestOptions) => - request( - { - method: "GET", - path: `/api/pty/${encodeURIComponent(input.ptyID)}`, - query: { location: input["location"] }, - successStatus: 200, - declaredStatuses: [404, 401, 400], - empty: false, - }, - requestOptions, - ), - update: (input: PtyUpdateInput, requestOptions?: RequestOptions) => - request( - { - method: "PUT", - path: `/api/pty/${encodeURIComponent(input.ptyID)}`, - query: { location: input["location"] }, - body: { title: input["title"], size: input["size"] }, - successStatus: 200, - declaredStatuses: [404, 401, 400], - empty: false, - }, - requestOptions, - ), - remove: (input: PtyRemoveInput, requestOptions?: RequestOptions) => - request( - { - method: "DELETE", - path: `/api/pty/${encodeURIComponent(input.ptyID)}`, - query: { location: input["location"] }, - successStatus: 204, - declaredStatuses: [404, 401, 400], - empty: true, - }, - requestOptions, - ), - }, - shell: { - list: (input?: ShellListInput, requestOptions?: RequestOptions) => - request( - { - method: "GET", - path: `/api/shell`, - query: { location: input?.["location"] }, - successStatus: 200, - declaredStatuses: [401, 400], - empty: false, - }, - requestOptions, - ), - create: (input: ShellCreateInput, requestOptions?: RequestOptions) => - request( - { - method: "POST", - path: `/api/shell`, - query: { location: input["location"] }, - body: { - command: input["command"], - cwd: input["cwd"], - timeout: input["timeout"], - metadata: input["metadata"], - }, - successStatus: 200, - declaredStatuses: [401, 400], - empty: false, - }, - requestOptions, - ), - get: (input: ShellGetInput, requestOptions?: RequestOptions) => - request( - { - method: "GET", - path: `/api/shell/${encodeURIComponent(input.id)}`, - query: { location: input["location"] }, - successStatus: 200, - declaredStatuses: [404, 401, 400], - empty: false, - }, - requestOptions, - ), - timeout: (input: ShellTimeoutInput, requestOptions?: RequestOptions) => - request( - { - method: "PATCH", - path: `/api/shell/${encodeURIComponent(input.id)}/timeout`, - query: { location: input["location"] }, - body: { timeout: input["timeout"] }, - successStatus: 200, - declaredStatuses: [404, 401, 400], - empty: false, - }, - requestOptions, - ), - output: (input: ShellOutputInput, requestOptions?: RequestOptions) => - request( - { - method: "GET", - path: `/api/shell/${encodeURIComponent(input.id)}/output`, - query: { location: input["location"], cursor: input["cursor"], limit: input["limit"] }, - successStatus: 200, - declaredStatuses: [404, 401, 400], - empty: false, - }, - requestOptions, - ), - remove: (input: ShellRemoveInput, requestOptions?: RequestOptions) => - request( - { - method: "DELETE", - path: `/api/shell/${encodeURIComponent(input.id)}`, - query: { location: input["location"] }, - successStatus: 204, - declaredStatuses: [404, 401, 400], - empty: true, - }, - requestOptions, - ), - }, - question: { - request: { - list: (input?: QuestionRequestListInput, requestOptions?: RequestOptions) => - request( - { - method: "GET", - path: `/api/question/request`, - query: { location: input?.["location"] }, - successStatus: 200, - declaredStatuses: [401, 400], - empty: false, - }, - requestOptions, - ), - }, - list: (input: QuestionListInput, requestOptions?: RequestOptions) => - request<{ readonly data: QuestionListOutput }>( - { - method: "GET", - path: `/api/session/${encodeURIComponent(input.sessionID)}/question`, - successStatus: 200, - declaredStatuses: [404, 400, 401], - empty: false, - }, - requestOptions, - ).then((value) => value.data), - reply: (input: QuestionReplyInput, requestOptions?: RequestOptions) => - request( - { - method: "POST", - path: `/api/session/${encodeURIComponent(input.sessionID)}/question/${encodeURIComponent(input.requestID)}/reply`, - body: { answers: input["answers"] }, - successStatus: 204, - declaredStatuses: [404, 400, 401], - empty: true, - }, - requestOptions, - ), - reject: (input: QuestionRejectInput, requestOptions?: RequestOptions) => - request( - { - method: "POST", - path: `/api/session/${encodeURIComponent(input.sessionID)}/question/${encodeURIComponent(input.requestID)}/reject`, - successStatus: 204, - declaredStatuses: [404, 400, 401], - empty: true, - }, - requestOptions, - ), - }, - reference: { - list: (input?: ReferenceListInput, requestOptions?: RequestOptions) => - request( - { - method: "GET", - path: `/api/reference`, - query: { location: input?.["location"] }, - successStatus: 200, - declaredStatuses: [401, 400], - empty: false, - }, - requestOptions, - ), - }, - projectCopy: { - create: (input: ProjectCopyCreateInput, requestOptions?: RequestOptions) => - request( - { - method: "POST", - path: `/experimental/project/${encodeURIComponent(input.projectID)}/copy`, - query: { location: input["location"] }, - body: { strategy: input["strategy"], directory: input["directory"], name: input["name"] }, - successStatus: 200, - declaredStatuses: [400, 401], - empty: false, - }, - requestOptions, - ), - remove: (input: ProjectCopyRemoveInput, requestOptions?: RequestOptions) => - request( - { - method: "DELETE", - path: `/experimental/project/${encodeURIComponent(input.projectID)}/copy`, - query: { location: input["location"] }, - body: { directory: input["directory"], force: input["force"] }, - successStatus: 204, - declaredStatuses: [400, 401], - empty: true, - }, - requestOptions, - ), - refresh: (input: ProjectCopyRefreshInput, requestOptions?: RequestOptions) => - request( - { - method: "POST", - path: `/experimental/project/${encodeURIComponent(input.projectID)}/copy/refresh`, - query: { location: input["location"] }, - successStatus: 204, - declaredStatuses: [400, 401], - empty: true, - }, - requestOptions, - ), - }, - vcs: { - status: (input?: VcsStatusInput, requestOptions?: RequestOptions) => - request( - { - method: "GET", - path: `/api/vcs/status`, - query: { location: input?.["location"] }, - successStatus: 200, - declaredStatuses: [401, 400], - empty: false, - }, - requestOptions, - ), - diff: (input: VcsDiffInput, requestOptions?: RequestOptions) => - request( - { - method: "GET", - path: `/api/vcs/diff`, - query: { location: input["location"], mode: input["mode"], context: input["context"] }, - successStatus: 200, - declaredStatuses: [401, 400], - empty: false, - }, - requestOptions, - ), - }, - debug: { - location: { - list: (requestOptions?: RequestOptions) => - request( - { - method: "GET", - path: `/api/debug/location`, - successStatus: 200, - declaredStatuses: [401, 400], - empty: false, - }, - requestOptions, - ), - evict: (input?: DebugLocationEvictInput, requestOptions?: RequestOptions) => - request( - { - method: "DELETE", - path: `/api/debug/location`, - query: { location: input?.["location"] }, - successStatus: 204, - declaredStatuses: [401, 400], - empty: true, - }, - requestOptions, - ), - }, - }, - } -} - -function encodePath(value: string): string { - return value.split("/").map(encodeURIComponent).join("/") -} - -function appendQuery(params: URLSearchParams, key: string, value: unknown): void { - if (value === undefined) return - if (value === null) { - params.append(key, "null") - return - } - if (Array.isArray(value)) { - for (const item of value) appendQuery(params, key, item) - return - } - if (typeof value === "object") { - for (const [child, item] of Object.entries(value)) appendQuery(params, `${key}[${child}]`, item) - return - } - params.append(key, String(value)) -} - -async function json(response: Response): Promise { - if (!isContentType(response, "application/json") && !response.headers.get("content-type")?.includes("+json")) { - try { - await response.body?.cancel() - } catch {} - throw new ClientError("UnsupportedContentType") - } - let text: string - try { - text = await response.text() - } catch (cause) { - throw new ClientError("Transport", { cause }) - } - if (text === "") throw new ClientError("MalformedResponse") - try { - return JSON.parse(text) - } catch (cause) { - throw new ClientError("MalformedResponse", { cause }) - } -} - -function isContentType(response: Response, expected: string) { - return response.headers.get("content-type")?.split(";", 1)[0]?.trim().toLowerCase() === expected -} diff --git a/packages/client/src/promise/generated/types.ts b/packages/client/src/promise/generated/types.ts deleted file mode 100644 index 255beaa1bb..0000000000 --- a/packages/client/src/promise/generated/types.ts +++ /dev/null @@ -1,4768 +0,0 @@ -export type JsonValue = null | boolean | number | string | Array | { [key: string]: JsonValue } - -export type ModelRef = { id: string; providerID: string; variant?: string } - -export type ProviderSettings = { [x: string]: JsonValue } - -export type AgentColor = string | "primary" | "secondary" | "accent" | "success" | "warning" | "error" | "info" - -export type PermissionV2Effect = "allow" | "deny" | "ask" - -export type PluginInfo = { id: string } - -export type MoneyUSD = number - -export type TokenUsageInfo = { - input: number - output: number - reasoning: number - cache: { read: number; write: number } -} - -export type LocationRef = { directory: string; workspaceID?: string } - -export type FileDiffInfo = { - file: string - patch: string - additions: number - deletions: number - status: "added" | "deleted" | "modified" -} - -export type SessionActive = { type: "running" } - -export type PromptBase64 = string - -export type PromptFileSource = { type: "inline" } | { type: "uri"; uri: string } - -export type PromptMention = { start: number; end: number; text: string } - -export type SessionPendingSyntheticData = { text: string; description?: string; metadata?: { [x: string]: JsonValue } } - -export type SessionPendingCompaction = { - admittedSeq: number - id: string - sessionID: string - timeCreated: number - type: "compaction" -} - -export type SessionMessageAgentSelected = { - id: string - metadata?: { [x: string]: JsonValue } - time: { created: number } - type: "agent-switched" - agent: string -} - -export type SessionMessageSynthetic = { - id: string - metadata?: { [x: string]: JsonValue } - time: { created: number } - text: string - description?: string - type: "synthetic" -} - -export type SessionMessageSystem = { - id: string - metadata?: { [x: string]: JsonValue } - time: { created: number } - type: "system" - text: string -} - -export type SessionMessageSkill = { - id: string - metadata?: { [x: string]: JsonValue } - time: { created: number } - type: "skill" - skill: string - name: string - text: string -} - -export type SessionMessageShell = { - id: string - metadata?: { [x: string]: JsonValue } - time: { created: number; completed?: number } - type: "shell" - shellID: string - command: string - status: "running" | "exited" | "timeout" | "killed" - exit?: number | "Infinity" | "-Infinity" | "NaN" - output?: { output: string; cursor: number; size: number; truncated: boolean } -} - -export type SessionMessageAssistantText = { type: "text"; text: string } - -export type SessionMessageProviderState = { [x: string]: JsonValue } - -export type SessionMessageToolStateStreaming = { status: "streaming"; input: string } - -export type ToolTextContent = { type: "text"; text: string } - -export type ToolFileContent = { type: "file"; uri: string; mime: string; name?: string } - -export type SessionStructuredError = { type: string; message: string } - -export type SessionMessageCompactionRunning = { - type: "compaction" - id: string - metadata?: { [x: string]: JsonValue } - time: { created: number } - status: "running" - reason: "auto" | "manual" - summary: string - recent: string -} - -export type SessionMessageCompactionCompleted = { - type: "compaction" - id: string - metadata?: { [x: string]: JsonValue } - time: { created: number } - status: "completed" - reason: "auto" | "manual" - summary: string - recent: string -} - -export type InstructionEntryKey = string - -export type SessionPendingSyntheticData1 = { text: string; description?: string; metadata?: { [x: string]: any } } - -export type ShellInfo = { - id: string - status: "running" | "exited" | "timeout" | "killed" - command: string - cwd: string - shell: string - file: string - pid?: number - exit?: number - metadata: { [x: string]: any } - time: { started: number; completed?: number } -} - -export type SessionMessageProviderState3 = { [x: string]: any } - -export type SessionMessageProviderState4 = { [x: string]: any } - -export type SessionMessageProviderState5 = { [x: string]: any } - -export type SessionMessageProviderState6 = { [x: string]: any } - -export type SessionMessageProviderState7 = { [x: string]: any } - -export type EventLogSynced = { type: "log.synced"; aggregateID: string; seq?: number } - -export type ModelCapabilities = { tools: boolean; input: Array; output: Array } - -export type ModelVariant = { - id: string - settings?: { [x: string]: JsonValue } - headers?: { [x: string]: string } - body?: { [x: string]: JsonValue } -} - -export type MoneyUSDPerMillionTokens = number - -export type GenerateTextResponse = { data: { text: string } } - -export type ProviderV2Info = { - id: string - integrationID?: string - name: string - disabled?: boolean - package: string - settings?: { [x: string]: JsonValue } - headers?: { [x: string]: string } - body?: { [x: string]: JsonValue } -} - -export type IntegrationWhen = { key: string; op: "eq" | "neq"; value: string } - -export type IntegrationKeyMethod = { type: "key"; label?: string } - -export type IntegrationEnvMethod = { type: "env"; names: Array } - -export type ConnectionCredentialInfo = { type: "credential"; id: string; label: string } - -export type ConnectionEnvInfo = { type: "env"; name: string } - -export type IntegrationAttemptStatus = - | { - status: "pending" - time: { created: number | "Infinity" | "-Infinity" | "NaN"; expires: number | "Infinity" | "-Infinity" | "NaN" } - } - | { - status: "complete" - time: { created: number | "Infinity" | "-Infinity" | "NaN"; expires: number | "Infinity" | "-Infinity" | "NaN" } - } - | { - status: "failed" - message: string - time: { created: number | "Infinity" | "-Infinity" | "NaN"; expires: number | "Infinity" | "-Infinity" | "NaN" } - } - | { - status: "expired" - time: { created: number | "Infinity" | "-Infinity" | "NaN"; expires: number | "Infinity" | "-Infinity" | "NaN" } - } - -export type McpStatusConnected = { status: "connected" } - -export type McpStatusPending = { status: "pending" } - -export type McpStatusDisabled = { status: "disabled" } - -export type McpStatusFailed = { status: "failed"; error: string } - -export type McpStatusNeedsAuth = { status: "needs_auth" } - -export type McpStatusNeedsClientRegistration = { status: "needs_client_registration"; error: string } - -export type McpResource = { server: string; name: string; uri: string; description?: string; mimeType?: string } - -export type McpResourceTemplate = { - server: string - name: string - uriTemplate: string - description?: string - mimeType?: string -} - -export type ProjectVcs = "git" | "hg" - -export type ProjectIcon = { url?: string; override?: string; color?: string } - -export type ProjectCommands = { start?: string } - -export type ProjectTime = { created: number; updated: number; initialized?: number } - -export type ProjectCurrent = { id: string; directory: string } - -export type ProjectDirectory = { directory: string; strategy?: string } - -export type FormMetadata = { [x: string]: JsonValue } - -export type FormWhen = { - key: string - op: "eq" | "neq" - value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean -} - -export type FormOption = { value: string; label: string; description?: string } - -export type FormExternalField = { key: string; type: "external"; url: string; title?: string; description?: string } - -export type FormValue = string | number | boolean | Array - -export type PermissionV2Source = { type: "tool"; messageID: string; callID: string } - -export type PermissionSavedInfo = { id: string; projectID: string; action: string; resource: string } - -export type FileSystemEntry = { path: string; type: "file" | "directory" } - -export type SkillInfo = { - id: string - name: string - description?: string - slash?: boolean - autoinvoke?: boolean - location: string - content: string -} - -export type FileDiffLegacyInfo = { - file?: string - patch?: string - additions: number - deletions: number - status?: "added" | "deleted" | "modified" -} - -export type PermissionAction = "allow" | "deny" | "ask" - -export type JSONSchema = { [x: string]: any } - -export type ProviderAuthError = { name: "ProviderAuthError"; data: { providerID: string; message: string } } - -export type UnknownError2 = { name: "UnknownError"; data: { message: string; ref?: string | undefined } } - -export type MessageOutputLengthError = { name: "MessageOutputLengthError"; data: {} } - -export type MessageAbortedError = { name: "MessageAbortedError"; data: { message: string } } - -export type StructuredOutputError = { name: "StructuredOutputError"; data: { message: string; retries: number } } - -export type ContextOverflowError = { - name: "ContextOverflowError" - data: { message: string; responseBody?: string | undefined } -} - -export type ContentFilterError = { name: "ContentFilterError"; data: { message: string } } - -export type APIError = { - name: "APIError" - data: { - message: string - statusCode?: number | undefined - isRetryable: boolean - responseHeaders?: { [x: string]: string } | undefined - responseBody?: string | undefined - metadata?: { [x: string]: string } | undefined - } -} - -export type TextPart = { - id: string - sessionID: string - messageID: string - type: "text" - text: string - synthetic?: boolean | undefined - ignored?: boolean | undefined - time?: { start: number; end?: number | undefined } | undefined - metadata?: { [x: string]: any } | undefined -} - -export type SubtaskPart = { - id: string - sessionID: string - messageID: string - type: "subtask" - prompt: string - description: string - agent: string - model?: { providerID: string; modelID: string } | undefined - command?: string | undefined -} - -export type ReasoningPart = { - id: string - sessionID: string - messageID: string - type: "reasoning" - text: string - metadata?: { [x: string]: any } | undefined - time: { start: number; end?: number | undefined } -} - -export type FilePartSourceText = { value: string; start: number; end: number } - -export type Range = { start: { line: number; character: number }; end: { line: number; character: number } } - -export type ToolStatePending = { status: "pending"; input: { [x: string]: any }; raw: string } - -export type ToolStateRunning = { - status: "running" - input: { [x: string]: any } - title?: string | undefined - metadata?: { [x: string]: any } | undefined - time: { start: number } -} - -export type ToolStateError = { - status: "error" - input: { [x: string]: any } - error: string - metadata?: { [x: string]: any } | undefined - time: { start: number; end: number } -} - -export type StepStartPart = { - id: string - sessionID: string - messageID: string - type: "step-start" - snapshot?: string | undefined -} - -export type StepFinishPart = { - id: string - sessionID: string - messageID: string - type: "step-finish" - reason: string - snapshot?: string | undefined - cost: number - tokens: { - total?: number | undefined - input: number - output: number - reasoning: number - cache: { read: number; write: number } - } -} - -export type SnapshotPart = { id: string; sessionID: string; messageID: string; type: "snapshot"; snapshot: string } - -export type PatchPart = { - id: string - sessionID: string - messageID: string - type: "patch" - hash: string - files: Array -} - -export type AgentPart = { - id: string - sessionID: string - messageID: string - type: "agent" - name: string - source?: { value: string; start: number; end: number } | undefined -} - -export type CompactionPart = { - id: string - sessionID: string - messageID: string - type: "compaction" - auto: boolean - overflow?: boolean | undefined - tail_start_id?: string | undefined -} - -export type PermissionV2Reply = "once" | "always" | "reject" - -export type Pty = { - id: string - title: string - command: string - args: Array - cwd: string - status: "running" | "exited" - pid: number - exitCode?: number -} - -export type QuestionV2Option = { label: string; description: string } - -export type QuestionV2Tool = { messageID: string; callID: string } - -export type QuestionV2Answer = Array - -export type FormMetadata1 = { [x: string]: any } - -export type FormWhen1 = { key: string; op: "eq" | "neq"; value: string | number | boolean } - -export type SessionStatus = - | { type: "idle" } - | { - type: "retry" - attempt: number - message: string - action?: { reason: string; provider: string; title: string; message: string; label: string; link?: string } - next: number - } - | { type: "busy" } - -export type QuestionOption = { label: string; description: string } - -export type QuestionTool = { messageID: string; callID: string } - -export type QuestionAnswer = Array - -export type ShellInfo1 = { - id: string - status: "running" | "exited" | "timeout" | "killed" - command: string - cwd: string - shell: string - file: string - pid?: number - exit?: number - metadata: { [x: string]: JsonValue } - time: { started: number; completed?: number } -} - -export type ReferenceLocalSource = { type: "local"; path: string; description?: string; hidden?: boolean } - -export type ReferenceGitSource = { - type: "git" - repository: string - branch?: string - description?: string - hidden?: boolean -} - -export type ProjectCopyCopy = { directory: string } - -export type VcsFileStatus = { - file: string - additions: number - deletions: number - status: "added" | "deleted" | "modified" -} - -export type SessionMessageModelSelected = { - id: string - metadata?: { [x: string]: JsonValue } - time: { created: number } - type: "model-switched" - model: ModelRef - previous?: ModelRef -} - -export type CommandInfo = { - name: string - template: string - description?: string - agent?: string - model?: ModelRef - subtask?: boolean -} - -export type ProviderRequest = { - settings: ProviderSettings - headers: { [x: string]: string } - body: { [x: string]: JsonValue } -} - -export type PermissionV2Rule = { action: string; resource: string; effect: PermissionV2Effect } - -export type SessionAgentSelected = { - id: string - created: number - metadata?: { [x: string]: any } - type: "session.agent.selected" - durable: { aggregateID: string; seq: number; version: 1 } - location?: LocationRef - data: { sessionID: string; agent: string } -} - -export type SessionModelSelected = { - id: string - created: number - metadata?: { [x: string]: any } - type: "session.model.selected" - durable: { aggregateID: string; seq: number; version: 1 } - location?: LocationRef - data: { sessionID: string; model: ModelRef } -} - -export type SessionMoved = { - id: string - created: number - metadata?: { [x: string]: any } - type: "session.moved" - durable: { aggregateID: string; seq: number; version: 1 } - location?: LocationRef - data: { sessionID: string; location: LocationRef; subpath?: string } -} - -export type SessionRenamed = { - id: string - created: number - metadata?: { [x: string]: any } - type: "session.renamed" - durable: { aggregateID: string; seq: number; version: 1 } - location?: LocationRef - data: { sessionID: string; title: string } -} - -export type SessionDeleted = { - id: string - created: number - metadata?: { [x: string]: any } - type: "session.deleted" - durable: { aggregateID: string; seq: number; version: 2 } - location?: LocationRef - data: { sessionID: string } -} - -export type SessionForked = { - id: string - created: number - metadata?: { [x: string]: any } - type: "session.forked" - durable: { aggregateID: string; seq: number; version: 2 } - location?: LocationRef - data: { sessionID: string; parentID: string; parentSeq: number; from?: string } -} - -export type SessionInputPromoted = { - id: string - created: number - metadata?: { [x: string]: any } - type: "session.input.promoted" - durable: { aggregateID: string; seq: number; version: 1 } - location?: LocationRef - data: { sessionID: string; inputID: string } -} - -export type SessionExecutionStarted = { - id: string - created: number - metadata?: { [x: string]: any } - type: "session.execution.started" - durable: { aggregateID: string; seq: number; version: 1 } - location?: LocationRef - data: { sessionID: string } -} - -export type SessionExecutionSucceeded = { - id: string - created: number - metadata?: { [x: string]: any } - type: "session.execution.succeeded" - durable: { aggregateID: string; seq: number; version: 1 } - location?: LocationRef - data: { sessionID: string } -} - -export type SessionExecutionInterrupted = { - id: string - created: number - metadata?: { [x: string]: any } - type: "session.execution.interrupted" - durable: { aggregateID: string; seq: number; version: 1 } - location?: LocationRef - data: { sessionID: string; reason: "user" | "shutdown" | "superseded" } -} - -export type SessionInstructionsUpdated = { - id: string - created: number - metadata?: { [x: string]: any } - type: "session.instructions.updated" - durable: { aggregateID: string; seq: number; version: 2 } - location?: LocationRef - data: { sessionID: string; delta: { [x: string]: string | "removed" } } -} - -export type SessionSynthetic = { - id: string - created: number - metadata?: { [x: string]: any } - type: "session.synthetic" - durable: { aggregateID: string; seq: number; version: 1 } - location?: LocationRef - data: { sessionID: string; text: string; description?: string; metadata?: { [x: string]: any } } -} - -export type SessionSkillActivated = { - id: string - created: number - metadata?: { [x: string]: any } - type: "session.skill.activated" - durable: { aggregateID: string; seq: number; version: 1 } - location?: LocationRef - data: { sessionID: string; id: string; name: string; text: string } -} - -export type SessionStepStarted = { - id: string - created: number - metadata?: { [x: string]: any } - type: "session.step.started" - durable: { aggregateID: string; seq: number; version: 1 } - location?: LocationRef - data: { sessionID: string; assistantMessageID: string; agent: string; model: ModelRef; snapshot?: string } -} - -export type SessionStepEnded = { - id: string - created: number - metadata?: { [x: string]: any } - type: "session.step.ended" - durable: { aggregateID: string; seq: number; version: 1 } - location?: LocationRef - data: { - sessionID: string - assistantMessageID: string - finish: "stop" | "length" | "tool-calls" | "content-filter" | "error" | "unknown" - cost: MoneyUSD - tokens: TokenUsageInfo - snapshot?: string - files?: Array - } -} - -export type SessionTextStarted = { - id: string - created: number - metadata?: { [x: string]: any } - type: "session.text.started" - durable: { aggregateID: string; seq: number; version: 1 } - location?: LocationRef - data: { sessionID: string; assistantMessageID: string; ordinal: number } -} - -export type SessionTextEnded = { - id: string - created: number - metadata?: { [x: string]: any } - type: "session.text.ended" - durable: { aggregateID: string; seq: number; version: 1 } - location?: LocationRef - data: { sessionID: string; assistantMessageID: string; ordinal: number; text: string } -} - -export type SessionToolInputStarted = { - id: string - created: number - metadata?: { [x: string]: any } - type: "session.tool.input.started" - durable: { aggregateID: string; seq: number; version: 1 } - location?: LocationRef - data: { sessionID: string; assistantMessageID: string; callID: string; name: string } -} - -export type SessionToolInputEnded = { - id: string - created: number - metadata?: { [x: string]: any } - type: "session.tool.input.ended" - durable: { aggregateID: string; seq: number; version: 1 } - location?: LocationRef - data: { sessionID: string; assistantMessageID: string; callID: string; text: string } -} - -export type SessionCompactionAdmitted = { - id: string - created: number - metadata?: { [x: string]: any } - type: "session.compaction.admitted" - durable: { aggregateID: string; seq: number; version: 1 } - location?: LocationRef - data: { sessionID: string; inputID: string } -} - -export type SessionCompactionStarted = { - id: string - created: number - metadata?: { [x: string]: any } - type: "session.compaction.started" - durable: { aggregateID: string; seq: number; version: 1 } - location?: LocationRef - data: { sessionID: string; reason: "auto" | "manual"; recent: string; inputID?: string } -} - -export type SessionCompactionEnded = { - id: string - created: number - metadata?: { [x: string]: any } - type: "session.compaction.ended" - durable: { aggregateID: string; seq: number; version: 1 } - location?: LocationRef - data: { sessionID: string; reason: "auto" | "manual"; text: string; recent: string } -} - -export type SessionRevertCleared = { - id: string - created: number - metadata?: { [x: string]: any } - type: "session.revert.cleared" - durable: { aggregateID: string; seq: number; version: 1 } - location?: LocationRef - data: { sessionID: string } -} - -export type SessionRevertCommitted = { - id: string - created: number - metadata?: { [x: string]: any } - type: "session.revert.committed" - durable: { aggregateID: string; seq: number; version: 1 } - location?: LocationRef - data: { sessionID: string; to: string } -} - -export type ModelsDevRefreshed = { - id: string - created: number - metadata?: { [x: string]: any } - type: "models-dev.refreshed" - location?: LocationRef - data: {} -} - -export type IntegrationUpdated = { - id: string - created: number - metadata?: { [x: string]: any } - type: "integration.updated" - location?: LocationRef - data: {} -} - -export type IntegrationConnectionUpdated = { - id: string - created: number - metadata?: { [x: string]: any } - type: "integration.connection.updated" - location?: LocationRef - data: { integrationID: string } -} - -export type CatalogUpdated = { - id: string - created: number - metadata?: { [x: string]: any } - type: "catalog.updated" - location?: LocationRef - data: {} -} - -export type AgentUpdated = { - id: string - created: number - metadata?: { [x: string]: any } - type: "agent.updated" - location?: LocationRef - data: {} -} - -export type MessageRemoved = { - id: string - created: number - metadata?: { [x: string]: any } - type: "message.removed" - durable: { aggregateID: string; seq: number; version: 1 } - location?: LocationRef - data: { sessionID: string; messageID: string } -} - -export type MessagePartRemoved = { - id: string - created: number - metadata?: { [x: string]: any } - type: "message.part.removed" - durable: { aggregateID: string; seq: number; version: 1 } - location?: LocationRef - data: { sessionID: string; messageID: string; partID: string } -} - -export type SessionUsageUpdated = { - id: string - created: number - metadata?: { [x: string]: any } - type: "session.usage.updated" - location?: LocationRef - data: { sessionID: string; cost: MoneyUSD; tokens: TokenUsageInfo } -} - -export type SessionTextDelta = { - id: string - created: number - metadata?: { [x: string]: any } - type: "session.text.delta" - location?: LocationRef - data: { sessionID: string; assistantMessageID: string; ordinal: number; delta: string } -} - -export type SessionReasoningDelta = { - id: string - created: number - metadata?: { [x: string]: any } - type: "session.reasoning.delta" - location?: LocationRef - data: { sessionID: string; assistantMessageID: string; ordinal: number; delta: string } -} - -export type SessionToolInputDelta = { - id: string - created: number - metadata?: { [x: string]: any } - type: "session.tool.input.delta" - location?: LocationRef - data: { sessionID: string; assistantMessageID: string; callID: string; delta: string } -} - -export type SessionCompactionDelta = { - id: string - created: number - metadata?: { [x: string]: any } - type: "session.compaction.delta" - location?: LocationRef - data: { sessionID: string; text: string } -} - -export type FilesystemChanged = { - id: string - created: number - metadata?: { [x: string]: any } - type: "filesystem.changed" - location?: LocationRef - data: { file: string; event: "add" | "change" | "unlink" } -} - -export type ReferenceUpdated = { - id: string - created: number - metadata?: { [x: string]: any } - type: "reference.updated" - location?: LocationRef - data: {} -} - -export type PluginAdded = { - id: string - created: number - metadata?: { [x: string]: any } - type: "plugin.added" - location?: LocationRef - data: { id: string } -} - -export type PluginUpdated = { - id: string - created: number - metadata?: { [x: string]: any } - type: "plugin.updated" - location?: LocationRef - data: {} -} - -export type ProjectDirectoriesUpdated = { - id: string - created: number - metadata?: { [x: string]: any } - type: "project.directories.updated" - location?: LocationRef - data: { projectID: string } -} - -export type CommandUpdated = { - id: string - created: number - metadata?: { [x: string]: any } - type: "command.updated" - location?: LocationRef - data: {} -} - -export type ConfigUpdated = { - id: string - created: number - metadata?: { [x: string]: any } - type: "config.updated" - location?: LocationRef - data: {} -} - -export type SkillUpdated = { - id: string - created: number - metadata?: { [x: string]: any } - type: "skill.updated" - location?: LocationRef - data: {} -} - -export type PtyExited = { - id: string - created: number - metadata?: { [x: string]: any } - type: "pty.exited" - location?: LocationRef - data: { id: string; exitCode: number } -} - -export type PtyDeleted = { - id: string - created: number - metadata?: { [x: string]: any } - type: "pty.deleted" - location?: LocationRef - data: { id: string } -} - -export type ShellExited = { - id: string - created: number - metadata?: { [x: string]: any } - type: "shell.exited" - location?: LocationRef - data: { id: string; exit?: number; status: "running" | "exited" | "timeout" | "killed" } -} - -export type ShellDeleted = { - id: string - created: number - metadata?: { [x: string]: any } - type: "shell.deleted" - location?: LocationRef - data: { id: string } -} - -export type QuestionV2Rejected = { - id: string - created: number - metadata?: { [x: string]: any } - type: "question.v2.rejected" - location?: LocationRef - data: { sessionID: string; requestID: string } -} - -export type FormCancelled = { - id: string - created: number - metadata?: { [x: string]: any } - type: "form.cancelled" - location?: LocationRef - data: { id: string; sessionID: string } -} - -export type SessionIdle = { - id: string - created: number - metadata?: { [x: string]: any } - type: "session.idle" - location?: LocationRef - data: { sessionID: string } -} - -export type TuiPromptAppend = { - id: string - created: number - metadata?: { [x: string]: any } - type: "tui.prompt.append" - location?: LocationRef - data: { text: string } -} - -export type TuiCommandExecute = { - id: string - created: number - metadata?: { [x: string]: any } - type: "tui.command.execute" - location?: LocationRef - data: { - command: - | "session.list" - | "session.new" - | "session.share" - | "session.interrupt" - | "session.background" - | "session.compact" - | "session.page.up" - | "session.page.down" - | "session.line.up" - | "session.line.down" - | "session.half.page.up" - | "session.half.page.down" - | "session.first" - | "session.last" - | "prompt.clear" - | "prompt.submit" - | "agent.cycle" - | string - } -} - -export type TuiToastShow = { - id: string - created: number - metadata?: { [x: string]: any } - type: "tui.toast.show" - location?: LocationRef - data: { - title?: string - message: string - variant: "info" | "success" | "warning" | "error" - duration?: number | undefined - } -} - -export type TuiSessionSelect = { - id: string - created: number - metadata?: { [x: string]: any } - type: "tui.session.select" - location?: LocationRef - data: { sessionID: string } -} - -export type InstallationUpdated = { - id: string - created: number - metadata?: { [x: string]: any } - type: "installation.updated" - location?: LocationRef - data: { version: string } -} - -export type InstallationUpdateAvailable = { - id: string - created: number - metadata?: { [x: string]: any } - type: "installation.update-available" - location?: LocationRef - data: { version: string } -} - -export type VcsBranchUpdated = { - id: string - created: number - metadata?: { [x: string]: any } - type: "vcs.branch.updated" - location?: LocationRef - data: { branch?: string } -} - -export type McpStatusChanged = { - id: string - created: number - metadata?: { [x: string]: any } - type: "mcp.status.changed" - location?: LocationRef - data: { server: string } -} - -export type McpResourcesChanged = { - id: string - created: number - metadata?: { [x: string]: any } - type: "mcp.resources.changed" - location?: LocationRef - data: { server: string } -} - -export type PermissionAsked = { - id: string - created: number - metadata?: { [x: string]: any } - type: "permission.asked" - location?: LocationRef - data: { - id: string - sessionID: string - permission: string - patterns: Array - metadata: { [x: string]: any } - always: Array - tool?: { messageID: string; callID: string } | undefined - } -} - -export type PermissionReplied = { - id: string - created: number - metadata?: { [x: string]: any } - type: "permission.replied" - location?: LocationRef - data: { sessionID: string; requestID: string; reply: "once" | "always" | "reject" } -} - -export type QuestionRejected = { - id: string - created: number - metadata?: { [x: string]: any } - type: "question.rejected" - location?: LocationRef - data: { sessionID: string; requestID: string } -} - -export type V2EventServerConnected = { - id: string - metadata?: { [x: string]: any } | undefined - location?: LocationRef | undefined - type: "server.connected" - data: {} -} - -export type SessionRevert = { messageID: string; partID?: string; snapshot?: string; files?: Array } - -export type PromptFileAttachment = { - data: PromptBase64 - mime: string - source: PromptFileSource - name?: string - description?: string - mention?: PromptMention -} - -export type PromptAgentAttachment = { name: string; mention?: PromptMention } - -export type SessionPendingSynthetic = { - admittedSeq: number - id: string - sessionID: string - timeCreated: number - type: "synthetic" - data: SessionPendingSyntheticData - delivery: "steer" | "queue" -} - -export type SessionMessageAssistantReasoning = { - type: "reasoning" - text: string - state?: SessionMessageProviderState - time?: { created: number; completed?: number } -} - -export type LLMToolContent = ToolTextContent | ToolFileContent - -export type SessionMessageAssistantRetry = { attempt: number; at: number; error: SessionStructuredError } - -export type SessionMessageCompactionFailed = { - type: "compaction" - id: string - metadata?: { [x: string]: JsonValue } - time: { created: number } - status: "failed" - reason: "auto" | "manual" - error: SessionStructuredError -} - -export type SessionExecutionFailed = { - id: string - created: number - metadata?: { [x: string]: any } - type: "session.execution.failed" - durable: { aggregateID: string; seq: number; version: 1 } - location?: LocationRef - data: { sessionID: string; error: SessionStructuredError } -} - -export type SessionStepFailed = { - id: string - created: number - metadata?: { [x: string]: any } - type: "session.step.failed" - durable: { aggregateID: string; seq: number; version: 1 } - location?: LocationRef - data: { - sessionID: string - assistantMessageID: string - error: SessionStructuredError - cost?: MoneyUSD - tokens?: TokenUsageInfo - } -} - -export type SessionRetryScheduled = { - id: string - created: number - metadata?: { [x: string]: any } - type: "session.retry.scheduled" - durable: { aggregateID: string; seq: number; version: 1 } - location?: LocationRef - data: { sessionID: string; assistantMessageID: string; attempt: number; at: number; error: SessionStructuredError } -} - -export type SessionCompactionFailed = { - id: string - created: number - metadata?: { [x: string]: any } - type: "session.compaction.failed" - durable: { aggregateID: string; seq: number; version: 1 } - location?: LocationRef - data: { sessionID: string; reason: "auto" | "manual"; error: SessionStructuredError; inputID?: string } -} - -export type InstructionEntryInfo = { key: InstructionEntryKey; value: JsonValue } - -export type SessionPendingSyntheticMessage = { - type: "synthetic" - data: SessionPendingSyntheticData1 - delivery: "steer" | "queue" -} - -export type SessionShellStarted = { - id: string - created: number - metadata?: { [x: string]: any } - type: "session.shell.started" - durable: { aggregateID: string; seq: number; version: 1 } - location?: LocationRef - data: { sessionID: string; shell: ShellInfo } -} - -export type SessionShellEnded = { - id: string - created: number - metadata?: { [x: string]: any } - type: "session.shell.ended" - durable: { aggregateID: string; seq: number; version: 1 } - location?: LocationRef - data: { - sessionID: string - shell: ShellInfo - output: { output: string; cursor: number; size: number; truncated: boolean } - } -} - -export type ShellCreated = { - id: string - created: number - metadata?: { [x: string]: any } - type: "shell.created" - location?: LocationRef - data: { info: ShellInfo } -} - -export type SessionReasoningStarted = { - id: string - created: number - metadata?: { [x: string]: any } - type: "session.reasoning.started" - durable: { aggregateID: string; seq: number; version: 1 } - location?: LocationRef - data: { sessionID: string; assistantMessageID: string; ordinal: number; state?: SessionMessageProviderState3 } -} - -export type SessionReasoningEnded = { - id: string - created: number - metadata?: { [x: string]: any } - type: "session.reasoning.ended" - durable: { aggregateID: string; seq: number; version: 1 } - location?: LocationRef - data: { - sessionID: string - assistantMessageID: string - ordinal: number - text: string - state?: SessionMessageProviderState4 - } -} - -export type SessionToolCalled = { - id: string - created: number - metadata?: { [x: string]: any } - type: "session.tool.called" - durable: { aggregateID: string; seq: number; version: 1 } - location?: LocationRef - data: { - sessionID: string - assistantMessageID: string - callID: string - input: { [x: string]: any } - executed: boolean - state?: SessionMessageProviderState5 - } -} - -export type SessionToolFailed = { - id: string - created: number - metadata?: { [x: string]: any } - type: "session.tool.failed" - durable: { aggregateID: string; seq: number; version: 1 } - location?: LocationRef - data: { - sessionID: string - assistantMessageID: string - callID: string - error: SessionStructuredError - result?: any - executed: boolean - resultState?: SessionMessageProviderState7 - } -} - -export type ModelCost = { - tier?: { type: "context"; size: number } - input: MoneyUSDPerMillionTokens - output: MoneyUSDPerMillionTokens - cache: { read: MoneyUSDPerMillionTokens; write: MoneyUSDPerMillionTokens } -} - -export type IntegrationTextPrompt = { - type: "text" - key: string - message: string - placeholder?: string - when?: IntegrationWhen -} - -export type IntegrationSelectPrompt = { - type: "select" - key: string - message: string - options: Array<{ label: string; value: string; hint?: string }> - when?: IntegrationWhen -} - -export type ConnectionInfo = ConnectionCredentialInfo | ConnectionEnvInfo - -export type McpServer = { - name: string - status: - | McpStatusConnected - | McpStatusPending - | McpStatusDisabled - | McpStatusFailed - | McpStatusNeedsAuth - | McpStatusNeedsClientRegistration - integrationID?: string -} - -export type McpResourceCatalog = { resources: Array; templates: Array } - -export type Project = { - id: string - worktree: string - vcs?: ProjectVcs - name?: string - icon?: ProjectIcon - commands?: ProjectCommands - time: ProjectTime - sandboxes: Array -} - -export type ProjectDirectories = Array - -export type FormNumberField = { - key: string - title?: string - description?: string - required?: boolean - when?: Array - type: "number" - minimum?: number | "Infinity" | "-Infinity" | "NaN" - maximum?: number | "Infinity" | "-Infinity" | "NaN" - default?: number | "Infinity" | "-Infinity" | "NaN" -} - -export type FormIntegerField = { - key: string - title?: string - description?: string - required?: boolean - when?: Array - type: "integer" - minimum?: number | "Infinity" | "-Infinity" | "NaN" - maximum?: number | "Infinity" | "-Infinity" | "NaN" - default?: number | "Infinity" | "-Infinity" | "NaN" -} - -export type FormBooleanField = { - key: string - title?: string - description?: string - required?: boolean - when?: Array - type: "boolean" - default?: boolean -} - -export type FormStringField = { - key: string - title?: string - description?: string - required?: boolean - when?: Array - type: "string" - format?: "email" | "uri" | "date" | "date-time" - minLength?: number - maxLength?: number - pattern?: string - placeholder?: string - default?: string - options?: Array - custom?: boolean -} - -export type FormMultiselectField = { - key: string - title?: string - description?: string - required?: boolean - when?: Array - type: "multiselect" - options: Array - minItems?: number - maxItems?: number - custom?: boolean - default?: Array -} - -export type FormAnswer = { [x: string]: FormValue } - -export type PermissionV2Request = { - id: string - sessionID: string - action: string - resources: Array - save?: Array - metadata?: { [x: string]: JsonValue } - source?: PermissionV2Source -} - -export type PermissionV2Asked = { - id: string - created: number - metadata?: { [x: string]: any } - type: "permission.v2.asked" - location?: LocationRef - data: { - id: string - sessionID: string - action: string - resources: Array - save?: Array - metadata?: { [x: string]: any } - source?: PermissionV2Source - } -} - -export type PermissionRule = { permission: string; pattern: string; action: PermissionAction } - -export type OutputFormat = - | { type: "text" } - | { type: "json_schema"; schema: JSONSchema; retryCount?: number | undefined | undefined } - -export type AssistantMessage = { - id: string - sessionID: string - role: "assistant" - time: { created: number; completed?: number | undefined } - error?: - | ProviderAuthError - | UnknownError2 - | MessageOutputLengthError - | MessageAbortedError - | StructuredOutputError - | ContextOverflowError - | ContentFilterError - | APIError - | undefined - parentID: string - modelID: string - providerID: string - mode: string - agent: string - path: { cwd: string; root: string } - summary?: boolean | undefined - cost: number - tokens: { - total?: number | undefined - input: number - output: number - reasoning: number - cache: { read: number; write: number } - } - structured?: any | undefined - variant?: string | undefined - finish?: string | undefined -} - -export type RetryPart = { - id: string - sessionID: string - messageID: string - type: "retry" - attempt: number - error: APIError - time: { created: number } -} - -export type SessionError = { - id: string - created: number - metadata?: { [x: string]: any } - type: "session.error" - location?: LocationRef - data: { - sessionID?: string | undefined - error?: - | ProviderAuthError - | UnknownError2 - | MessageOutputLengthError - | MessageAbortedError - | StructuredOutputError - | ContextOverflowError - | ContentFilterError - | APIError - | undefined - } -} - -export type FileSource = { text: FilePartSourceText; type: "file"; path: string } - -export type ResourceSource = { text: FilePartSourceText; type: "resource"; clientName: string; uri: string } - -export type SymbolSource = { - text: FilePartSourceText - type: "symbol" - path: string - range: Range - name: string - kind: number -} - -export type PermissionV2Replied = { - id: string - created: number - metadata?: { [x: string]: any } - type: "permission.v2.replied" - location?: LocationRef - data: { sessionID: string; requestID: string; reply: PermissionV2Reply } -} - -export type PtyCreated = { - id: string - created: number - metadata?: { [x: string]: any } - type: "pty.created" - location?: LocationRef - data: { info: Pty } -} - -export type PtyUpdated = { - id: string - created: number - metadata?: { [x: string]: any } - type: "pty.updated" - location?: LocationRef - data: { info: Pty } -} - -export type QuestionV2Info = { - question: string - header: string - options: Array - multiple?: boolean - custom?: boolean -} - -export type QuestionV2Replied = { - id: string - created: number - metadata?: { [x: string]: any } - type: "question.v2.replied" - location?: LocationRef - data: { sessionID: string; requestID: string; answers: Array } -} - -export type FormStringField1 = { - key: string - title?: string - description?: string - required?: boolean - when?: Array - type: "string" - format?: "email" | "uri" | "date" | "date-time" - minLength?: number - maxLength?: number - pattern?: string - placeholder?: string - default?: string - options?: Array - custom?: boolean -} - -export type FormNumberField1 = { - key: string - title?: string - description?: string - required?: boolean - when?: Array - type: "number" - minimum?: number - maximum?: number - default?: number -} - -export type FormIntegerField1 = { - key: string - title?: string - description?: string - required?: boolean - when?: Array - type: "integer" - minimum?: number - maximum?: number - default?: number -} - -export type FormBooleanField1 = { - key: string - title?: string - description?: string - required?: boolean - when?: Array - type: "boolean" - default?: boolean -} - -export type FormMultiselectField1 = { - key: string - title?: string - description?: string - required?: boolean - when?: Array - type: "multiselect" - options: Array - minItems?: number - maxItems?: number - custom?: boolean - default?: Array -} - -export type SessionStatus2 = { - id: string - created: number - metadata?: { [x: string]: any } - type: "session.status" - location?: LocationRef - data: { sessionID: string; status: SessionStatus } -} - -export type QuestionInfo = { - question: string - header: string - options: Array - multiple?: boolean | undefined - custom?: boolean | undefined -} - -export type QuestionReplied = { - id: string - created: number - metadata?: { [x: string]: any } - type: "question.replied" - location?: LocationRef - data: { sessionID: string; requestID: string; answers: Array } -} - -export type ReferenceSource = ReferenceLocalSource | ReferenceGitSource - -export type PermissionV2Ruleset = Array - -export type SessionInfo = { - id: string - parentID?: string - fork?: { sessionID: string; messageID?: string } - projectID: string - agent?: string - model?: ModelRef - cost: MoneyUSD - tokens: TokenUsageInfo - time: { created: number; updated: number; archived?: number } - title: string - location: LocationRef - subpath?: string - revert?: SessionRevert -} - -export type SessionRevertStaged = { - id: string - created: number - metadata?: { [x: string]: any } - type: "session.revert.staged" - durable: { aggregateID: string; seq: number; version: 1 } - location?: LocationRef - data: { sessionID: string; revert: SessionRevert } -} - -export type SessionPendingUserData = { - text: string - files?: Array - agents?: Array - metadata?: { [x: string]: JsonValue } -} - -export type SessionMessageUser = { - id: string - metadata?: { [x: string]: JsonValue } - time: { created: number } - text: string - files?: Array - agents?: Array - type: "user" -} - -export type SessionPendingUserData1 = { - text: string - files?: Array - agents?: Array - metadata?: { [x: string]: any } -} - -export type SessionMessageToolStateRunning = { - status: "running" - input: { [x: string]: JsonValue } - structured: { [x: string]: JsonValue } - content: Array -} - -export type SessionMessageToolStateCompleted = { - status: "completed" - input: { [x: string]: JsonValue } - content: Array - structured: { [x: string]: JsonValue } - result?: JsonValue -} - -export type SessionMessageToolStateError = { - status: "error" - input: { [x: string]: JsonValue } - content: Array - structured: { [x: string]: JsonValue } - error: SessionStructuredError - result?: JsonValue -} - -export type SessionToolProgress = { - id: string - created: number - metadata?: { [x: string]: any } - type: "session.tool.progress" - durable: { aggregateID: string; seq: number; version: 1 } - location?: LocationRef - data: { - sessionID: string - assistantMessageID: string - callID: string - structured: { [x: string]: any } - content: Array - } -} - -export type SessionToolSuccess = { - id: string - created: number - metadata?: { [x: string]: any } - type: "session.tool.success" - durable: { aggregateID: string; seq: number; version: 1 } - location?: LocationRef - data: { - sessionID: string - assistantMessageID: string - callID: string - structured: { [x: string]: any } - content: Array - result?: any - executed: boolean - resultState?: SessionMessageProviderState6 - } -} - -export type SessionMessageCompaction = - | SessionMessageCompactionRunning - | SessionMessageCompactionCompleted - | SessionMessageCompactionFailed - -export type ModelInfo = { - id: string - modelID: string - providerID: string - family?: string - name: string - package?: string - settings?: { [x: string]: JsonValue } - headers?: { [x: string]: string } - body?: { [x: string]: JsonValue } - capabilities: ModelCapabilities - variants: Array - time: { released: number } - cost: Array - status: "alpha" | "beta" | "deprecated" | "active" - enabled: boolean - limit: { context: number; input?: number; output: number } -} - -export type IntegrationOAuthMethod = { - id: string - type: "oauth" - label: string - prompts?: Array -} - -export type FormField = - | FormStringField - | FormNumberField - | FormIntegerField - | FormBooleanField - | FormMultiselectField - | FormExternalField - -export type FormState = { status: "pending" } | { status: "answered"; answer: FormAnswer } | { status: "cancelled" } - -export type FormReplied = { - id: string - created: number - metadata?: { [x: string]: any } - type: "form.replied" - location?: LocationRef - data: { id: string; sessionID: string; answer: FormAnswer } -} - -export type PermissionRuleset = Array - -export type UserMessage = { - id: string - sessionID: string - role: "user" - time: { created: number } - format?: OutputFormat | undefined - summary?: { title?: string | undefined; body?: string | undefined; diffs: Array } | undefined - agent: string - model: { providerID: string; modelID: string; variant?: string | undefined } - system?: string | undefined - tools?: { [x: string]: boolean } | undefined -} - -export type FilePartSource = FileSource | SymbolSource | ResourceSource - -export type QuestionV2Asked = { - id: string - created: number - metadata?: { [x: string]: any } - type: "question.v2.asked" - location?: LocationRef - data: { id: string; sessionID: string; questions: Array; tool?: QuestionV2Tool } -} - -export type QuestionV2Request = { - id: string - sessionID: string - questions: Array - tool?: QuestionV2Tool -} - -export type FormField1 = - | FormStringField1 - | FormNumberField1 - | FormIntegerField1 - | FormBooleanField1 - | FormMultiselectField1 - | FormExternalField - -export type QuestionAsked = { - id: string - created: number - metadata?: { [x: string]: any } - type: "question.asked" - location?: LocationRef - data: { id: string; sessionID: string; questions: Array; tool?: QuestionTool | undefined } -} - -export type ReferenceInfo = { - name: string - path: string - description?: string - hidden?: boolean - source: ReferenceSource -} - -export type AgentInfo = { - id: string - name: string - model?: ModelRef - request: ProviderRequest - system?: string - description?: string - mode: "subagent" | "primary" | "all" - hidden: boolean - color?: AgentColor - steps?: number - permissions: PermissionV2Ruleset -} - -export type SessionsResponse = { data: Array; cursor: { previous?: string | null; next?: string | null } } - -export type SessionPendingUser = { - admittedSeq: number - id: string - sessionID: string - timeCreated: number - type: "user" - data: SessionPendingUserData - delivery: "steer" | "queue" -} - -export type SessionPendingUserMessage = { type: "user"; data: SessionPendingUserData1; delivery: "steer" | "queue" } - -export type SessionMessageAssistantTool = { - type: "tool" - id: string - name: string - executed?: boolean - providerState?: SessionMessageProviderState - providerResultState?: SessionMessageProviderState - state: - | SessionMessageToolStateStreaming - | SessionMessageToolStateRunning - | SessionMessageToolStateCompleted - | SessionMessageToolStateError - time: { created: number; ran?: number; completed?: number } -} - -export type IntegrationMethod = IntegrationOAuthMethod | IntegrationKeyMethod | IntegrationEnvMethod - -export type FormFields = [FormField, ...Array] - -export type SessionV1Info = { - id: string - slug: string - projectID: string - workspaceID?: string - directory: string - path?: string - parentID?: string - summary?: { additions: number; deletions: number; files: number; diffs?: Array } - cost?: number - tokens?: { input: number; output: number; reasoning: number; cache: { read: number; write: number } } - share?: { url: string } - title: string - agent?: string - model?: { id: string; providerID: string; variant?: string } - version: string - metadata?: { [x: string]: any } - time: { created: number; updated: number; compacting?: number; archived?: number } - permission?: PermissionRuleset - revert?: { messageID: string; partID?: string; snapshot?: string; diff?: string } -} - -export type Message = UserMessage | AssistantMessage - -export type FilePart = { - id: string - sessionID: string - messageID: string - type: "file" - mime: string - filename?: string | undefined - url: string - source?: FilePartSource | undefined -} - -export type FormFields1 = [FormField1, ...Array] - -export type SessionPendingInfo = SessionPendingUser | SessionPendingSynthetic | SessionPendingCompaction - -export type SessionPendingMessage = SessionPendingUserMessage | SessionPendingSyntheticMessage - -export type SessionMessageAssistant = { - id: string - metadata?: { [x: string]: JsonValue } - time: { created: number; completed?: number } - type: "assistant" - agent: string - model: ModelRef - content: Array - snapshot?: { start?: string; end?: string; files?: Array } - finish?: "stop" | "length" | "tool-calls" | "content-filter" | "error" | "unknown" - cost?: MoneyUSD - tokens?: TokenUsageInfo - error?: SessionStructuredError - retry?: SessionMessageAssistantRetry -} - -export type IntegrationInfo = { - id: string - name: string - methods: Array - connections: Array -} - -export type FormInfo = { id: string; sessionID: string; title: string; metadata?: FormMetadata; fields: FormFields } - -export type SessionCreated = { - id: string - created: number - metadata?: { [x: string]: any } - type: "session.created" - durable: { aggregateID: string; seq: number; version: 1 } - location?: LocationRef - data: { sessionID: string; info: SessionV1Info } -} - -export type SessionUpdated = { - id: string - created: number - metadata?: { [x: string]: any } - type: "session.updated" - durable: { aggregateID: string; seq: number; version: 1 } - location?: LocationRef - data: { sessionID: string; info: SessionV1Info } -} - -export type SessionDeleted1 = { - id: string - created: number - metadata?: { [x: string]: any } - type: "session.deleted" - durable: { aggregateID: string; seq: number; version: 1 } - location?: LocationRef - data: { sessionID: string; info: SessionV1Info } -} - -export type MessageUpdated = { - id: string - created: number - metadata?: { [x: string]: any } - type: "message.updated" - durable: { aggregateID: string; seq: number; version: 1 } - location?: LocationRef - data: { sessionID: string; info: Message } -} - -export type ToolStateCompleted = { - status: "completed" - input: { [x: string]: any } - output: string - title: string - metadata: { [x: string]: any } - time: { start: number; end: number; compacted?: number | undefined } - attachments?: Array | undefined -} - -export type FormInfo1 = { id: string; sessionID: string; title: string; metadata?: FormMetadata1; fields: FormFields1 } - -export type SessionInputAdmitted = { - id: string - created: number - metadata?: { [x: string]: any } - type: "session.input.admitted" - durable: { aggregateID: string; seq: number; version: 1 } - location?: LocationRef - data: { sessionID: string; inputID: string; input: SessionPendingMessage } -} - -export type SessionMessageInfo = - | SessionMessageAgentSelected - | SessionMessageModelSelected - | SessionMessageUser - | SessionMessageSynthetic - | SessionMessageSystem - | SessionMessageSkill - | SessionMessageShell - | SessionMessageAssistant - | SessionMessageCompaction - -export type ToolState = ToolStatePending | ToolStateRunning | ToolStateCompleted | ToolStateError - -export type FormCreated = { - id: string - created: number - metadata?: { [x: string]: any } - type: "form.created" - location?: LocationRef - data: { form: FormInfo1 } -} - -export type SessionEventDurable = - | SessionAgentSelected - | SessionModelSelected - | SessionMoved - | SessionRenamed - | SessionDeleted - | SessionForked - | SessionInputPromoted - | SessionInputAdmitted - | SessionExecutionStarted - | SessionExecutionSucceeded - | SessionExecutionFailed - | SessionExecutionInterrupted - | SessionInstructionsUpdated - | SessionSynthetic - | SessionSkillActivated - | SessionShellStarted - | SessionShellEnded - | SessionStepStarted - | SessionStepEnded - | SessionStepFailed - | SessionTextStarted - | SessionTextEnded - | SessionReasoningStarted - | SessionReasoningEnded - | SessionToolInputStarted - | SessionToolInputEnded - | SessionToolCalled - | SessionToolProgress - | SessionToolSuccess - | SessionToolFailed - | SessionRetryScheduled - | SessionCompactionAdmitted - | SessionCompactionStarted - | SessionCompactionEnded - | SessionCompactionFailed - | SessionRevertStaged - | SessionRevertCleared - | SessionRevertCommitted - -export type SessionMessagesResponse = { - data: Array - cursor: { previous?: string | null; next?: string | null } -} - -export type ToolPart = { - id: string - sessionID: string - messageID: string - type: "tool" - callID: string - tool: string - state: ToolState - metadata?: { [x: string]: any } | undefined -} - -export type SessionLogItem = SessionEventDurable | EventLogSynced - -export type Part = - | TextPart - | SubtaskPart - | ReasoningPart - | FilePart - | ToolPart - | StepStartPart - | StepFinishPart - | SnapshotPart - | PatchPart - | AgentPart - | RetryPart - | CompactionPart - -export type MessagePartUpdated = { - id: string - created: number - metadata?: { [x: string]: any } - type: "message.part.updated" - durable: { aggregateID: string; seq: number; version: 1 } - location?: LocationRef - data: { sessionID: string; part: Part; time: number } -} - -export type V2Event = - | ModelsDevRefreshed - | IntegrationUpdated - | IntegrationConnectionUpdated - | CatalogUpdated - | AgentUpdated - | SessionCreated - | SessionUpdated - | SessionDeleted1 - | MessageUpdated - | MessageRemoved - | MessagePartUpdated - | MessagePartRemoved - | SessionAgentSelected - | SessionModelSelected - | SessionMoved - | SessionRenamed - | SessionUsageUpdated - | SessionDeleted - | SessionForked - | SessionInputPromoted - | SessionInputAdmitted - | SessionExecutionStarted - | SessionExecutionSucceeded - | SessionExecutionFailed - | SessionExecutionInterrupted - | SessionInstructionsUpdated - | SessionSynthetic - | SessionSkillActivated - | SessionShellStarted - | SessionShellEnded - | SessionStepStarted - | SessionStepEnded - | SessionStepFailed - | SessionTextStarted - | SessionTextDelta - | SessionTextEnded - | SessionReasoningStarted - | SessionReasoningDelta - | SessionReasoningEnded - | SessionToolInputStarted - | SessionToolInputDelta - | SessionToolInputEnded - | SessionToolCalled - | SessionToolProgress - | SessionToolSuccess - | SessionToolFailed - | SessionRetryScheduled - | SessionCompactionAdmitted - | SessionCompactionStarted - | SessionCompactionDelta - | SessionCompactionEnded - | SessionCompactionFailed - | SessionRevertStaged - | SessionRevertCleared - | SessionRevertCommitted - | FilesystemChanged - | ReferenceUpdated - | PermissionV2Asked - | PermissionV2Replied - | PluginAdded - | PluginUpdated - | ProjectDirectoriesUpdated - | CommandUpdated - | ConfigUpdated - | SkillUpdated - | PtyCreated - | PtyUpdated - | PtyExited - | PtyDeleted - | ShellCreated - | ShellExited - | ShellDeleted - | QuestionV2Asked - | QuestionV2Replied - | QuestionV2Rejected - | FormCreated - | FormReplied - | FormCancelled - | SessionStatus2 - | SessionIdle - | TuiPromptAppend - | TuiCommandExecute - | TuiToastShow - | TuiSessionSelect - | InstallationUpdated - | InstallationUpdateAvailable - | VcsBranchUpdated - | McpStatusChanged - | McpResourcesChanged - | PermissionAsked - | PermissionReplied - | QuestionAsked - | QuestionReplied - | QuestionRejected - | SessionError - | V2EventServerConnected - -export type UnauthorizedError = { readonly _tag: "UnauthorizedError"; readonly message: string } -export const isUnauthorizedError = (value: unknown): value is UnauthorizedError => - typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "UnauthorizedError" - -export type InvalidRequestError = { - readonly _tag: "InvalidRequestError" - readonly message: string - readonly kind?: string | undefined - readonly field?: string | undefined -} -export const isInvalidRequestError = (value: unknown): value is InvalidRequestError => - typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "InvalidRequestError" - -export type InvalidCursorError = { readonly _tag: "InvalidCursorError"; readonly message: string } -export const isInvalidCursorError = (value: unknown): value is InvalidCursorError => - typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "InvalidCursorError" - -export type SessionNotFoundError = { - readonly _tag: "SessionNotFoundError" - readonly sessionID: string - readonly message: string -} -export const isSessionNotFoundError = (value: unknown): value is SessionNotFoundError => - typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "SessionNotFoundError" - -export type MessageNotFoundError = { - readonly _tag: "MessageNotFoundError" - readonly sessionID: string - readonly messageID: string - readonly message: string -} -export const isMessageNotFoundError = (value: unknown): value is MessageNotFoundError => - typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "MessageNotFoundError" - -export type ConflictError = { - readonly _tag: "ConflictError" - readonly message: string - readonly resource?: string | undefined -} -export const isConflictError = (value: unknown): value is ConflictError => - typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "ConflictError" - -export type CommandNotFoundError = { - readonly _tag: "CommandNotFoundError" - readonly command: string - readonly message: string -} -export const isCommandNotFoundError = (value: unknown): value is CommandNotFoundError => - typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "CommandNotFoundError" - -export type CommandEvaluationError = { - readonly _tag: "CommandEvaluationError" - readonly command: string - readonly message: string -} -export const isCommandEvaluationError = (value: unknown): value is CommandEvaluationError => - typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "CommandEvaluationError" - -export type SkillNotFoundError = { - readonly _tag: "SkillNotFoundError" - readonly skill: string - readonly message: string -} -export const isSkillNotFoundError = (value: unknown): value is SkillNotFoundError => - typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "SkillNotFoundError" - -export type ServiceUnavailableError = { - readonly _tag: "ServiceUnavailableError" - readonly message: string - readonly service?: string | undefined -} -export const isServiceUnavailableError = (value: unknown): value is ServiceUnavailableError => - typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "ServiceUnavailableError" - -export type SessionBusyError = { - readonly _tag: "SessionBusyError" - readonly sessionID: string - readonly message: string -} -export const isSessionBusyError = (value: unknown): value is SessionBusyError => - typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "SessionBusyError" - -export type UnknownError = { - readonly _tag: "UnknownError" - readonly message: string - readonly ref?: string | undefined -} -export const isUnknownError = (value: unknown): value is UnknownError => - typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "UnknownError" - -export type InstructionEntryValueTooLargeError = { - readonly _tag: "InstructionEntryValueTooLargeError" - readonly actualBytes: number - readonly maxBytes: number - readonly message: string -} -export const isInstructionEntryValueTooLargeError = (value: unknown): value is InstructionEntryValueTooLargeError => - typeof value === "object" && - value !== null && - "_tag" in value && - value["_tag"] === "InstructionEntryValueTooLargeError" - -export type ProviderNotFoundError = { - readonly _tag: "ProviderNotFoundError" - readonly providerID: string - readonly message: string -} -export const isProviderNotFoundError = (value: unknown): value is ProviderNotFoundError => - typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "ProviderNotFoundError" - -export type FormNotFoundError = { readonly _tag: "FormNotFoundError"; readonly id: string; readonly message: string } -export const isFormNotFoundError = (value: unknown): value is FormNotFoundError => - typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "FormNotFoundError" - -export type FormAlreadySettledError = { - readonly _tag: "FormAlreadySettledError" - readonly id: string - readonly message: string -} -export const isFormAlreadySettledError = (value: unknown): value is FormAlreadySettledError => - typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "FormAlreadySettledError" - -export type FormInvalidAnswerError = { - readonly _tag: "FormInvalidAnswerError" - readonly id: string - readonly message: string -} -export const isFormInvalidAnswerError = (value: unknown): value is FormInvalidAnswerError => - typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "FormInvalidAnswerError" - -export type PermissionNotFoundError = { - readonly _tag: "PermissionNotFoundError" - readonly requestID: string - readonly message: string -} -export const isPermissionNotFoundError = (value: unknown): value is PermissionNotFoundError => - typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "PermissionNotFoundError" - -export type PtyNotFoundError = { readonly _tag: "PtyNotFoundError"; readonly ptyID: string; readonly message: string } -export const isPtyNotFoundError = (value: unknown): value is PtyNotFoundError => - typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "PtyNotFoundError" - -export type ShellNotFoundError = { readonly _tag: "ShellNotFoundError"; readonly id: string; readonly message: string } -export const isShellNotFoundError = (value: unknown): value is ShellNotFoundError => - typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "ShellNotFoundError" - -export type QuestionNotFoundError = { - readonly _tag: "QuestionNotFoundError" - readonly requestID: string - readonly message: string -} -export const isQuestionNotFoundError = (value: unknown): value is QuestionNotFoundError => - typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "QuestionNotFoundError" - -export type ProjectCopyError = { - readonly name: "ProjectCopyError" - readonly data: { readonly message: string; readonly forceRequired?: boolean | undefined } -} -export const isProjectCopyError = (value: unknown): value is ProjectCopyError => - typeof value === "object" && value !== null && "name" in value && value["name"] === "ProjectCopyError" - -export type HealthGetOutput = { healthy: true; version: string; pid: number } - -export type ServerGetOutput = { urls: Array } - -export type LocationGetInput = { - readonly location?: { - readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined - }["location"] -} - -export type LocationGetOutput = { directory: string; workspaceID?: string; project: { id: string; directory: string } } - -export type AgentListInput = { - readonly location?: { - readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined - }["location"] -} - -export type AgentListOutput = { - location: { directory: string; workspaceID?: string; project: { id: string; directory: string } } - data: Array -} - -export type PluginListInput = { - readonly location?: { - readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined - }["location"] -} - -export type PluginListOutput = { - location: { directory: string; workspaceID?: string; project: { id: string; directory: string } } - data: Array -} - -export type SessionListInput = { - readonly workspace?: { - readonly workspace?: string | undefined - readonly limit?: number | undefined - readonly order?: "asc" | "desc" | undefined - readonly search?: string | undefined - readonly parentID?: string | null | undefined - readonly directory?: string | undefined - readonly project?: string | undefined - readonly subpath?: string | undefined - readonly cursor?: string | undefined - }["workspace"] - readonly limit?: { - readonly workspace?: string | undefined - readonly limit?: number | undefined - readonly order?: "asc" | "desc" | undefined - readonly search?: string | undefined - readonly parentID?: string | null | undefined - readonly directory?: string | undefined - readonly project?: string | undefined - readonly subpath?: string | undefined - readonly cursor?: string | undefined - }["limit"] - readonly order?: { - readonly workspace?: string | undefined - readonly limit?: number | undefined - readonly order?: "asc" | "desc" | undefined - readonly search?: string | undefined - readonly parentID?: string | null | undefined - readonly directory?: string | undefined - readonly project?: string | undefined - readonly subpath?: string | undefined - readonly cursor?: string | undefined - }["order"] - readonly search?: { - readonly workspace?: string | undefined - readonly limit?: number | undefined - readonly order?: "asc" | "desc" | undefined - readonly search?: string | undefined - readonly parentID?: string | null | undefined - readonly directory?: string | undefined - readonly project?: string | undefined - readonly subpath?: string | undefined - readonly cursor?: string | undefined - }["search"] - readonly parentID?: { - readonly workspace?: string | undefined - readonly limit?: number | undefined - readonly order?: "asc" | "desc" | undefined - readonly search?: string | undefined - readonly parentID?: string | null | undefined - readonly directory?: string | undefined - readonly project?: string | undefined - readonly subpath?: string | undefined - readonly cursor?: string | undefined - }["parentID"] - readonly directory?: { - readonly workspace?: string | undefined - readonly limit?: number | undefined - readonly order?: "asc" | "desc" | undefined - readonly search?: string | undefined - readonly parentID?: string | null | undefined - readonly directory?: string | undefined - readonly project?: string | undefined - readonly subpath?: string | undefined - readonly cursor?: string | undefined - }["directory"] - readonly project?: { - readonly workspace?: string | undefined - readonly limit?: number | undefined - readonly order?: "asc" | "desc" | undefined - readonly search?: string | undefined - readonly parentID?: string | null | undefined - readonly directory?: string | undefined - readonly project?: string | undefined - readonly subpath?: string | undefined - readonly cursor?: string | undefined - }["project"] - readonly subpath?: { - readonly workspace?: string | undefined - readonly limit?: number | undefined - readonly order?: "asc" | "desc" | undefined - readonly search?: string | undefined - readonly parentID?: string | null | undefined - readonly directory?: string | undefined - readonly project?: string | undefined - readonly subpath?: string | undefined - readonly cursor?: string | undefined - }["subpath"] - readonly cursor?: { - readonly workspace?: string | undefined - readonly limit?: number | undefined - readonly order?: "asc" | "desc" | undefined - readonly search?: string | undefined - readonly parentID?: string | null | undefined - readonly directory?: string | undefined - readonly project?: string | undefined - readonly subpath?: string | undefined - readonly cursor?: string | undefined - }["cursor"] -} - -export type SessionListOutput = SessionsResponse - -export type SessionCreateInput = { - readonly id?: { - readonly id?: string | null - readonly agent?: string | null - readonly model?: { readonly id: string; readonly providerID: string; readonly variant?: string } | null - readonly location?: { readonly directory: string; readonly workspaceID?: string } | null - }["id"] - readonly agent?: { - readonly id?: string | null - readonly agent?: string | null - readonly model?: { readonly id: string; readonly providerID: string; readonly variant?: string } | null - readonly location?: { readonly directory: string; readonly workspaceID?: string } | null - }["agent"] - readonly model?: { - readonly id?: string | null - readonly agent?: string | null - readonly model?: { readonly id: string; readonly providerID: string; readonly variant?: string } | null - readonly location?: { readonly directory: string; readonly workspaceID?: string } | null - }["model"] - readonly location?: { - readonly id?: string | null - readonly agent?: string | null - readonly model?: { readonly id: string; readonly providerID: string; readonly variant?: string } | null - readonly location?: { readonly directory: string; readonly workspaceID?: string } | null - }["location"] -} - -export type SessionCreateOutput = { data: SessionInfo }["data"] - -export type SessionActiveOutput = { data: { [x: string]: SessionActive } }["data"] - -export type SessionGetInput = { readonly sessionID: { readonly sessionID: string }["sessionID"] } - -export type SessionGetOutput = { data: SessionInfo }["data"] - -export type SessionRemoveInput = { readonly sessionID: { readonly sessionID: string }["sessionID"] } - -export type SessionRemoveOutput = void - -export type SessionForkInput = { - readonly sessionID: { readonly sessionID: string }["sessionID"] - readonly messageID?: { readonly messageID?: string | undefined }["messageID"] -} - -export type SessionForkOutput = { data: SessionInfo }["data"] - -export type SessionSwitchAgentInput = { - readonly sessionID: { readonly sessionID: string }["sessionID"] - readonly agent: { readonly agent: string }["agent"] -} - -export type SessionSwitchAgentOutput = void - -export type SessionSwitchModelInput = { - readonly sessionID: { readonly sessionID: string }["sessionID"] - readonly model: { - readonly model: { readonly id: string; readonly providerID: string; readonly variant?: string } - }["model"] -} - -export type SessionSwitchModelOutput = void - -export type SessionRenameInput = { - readonly sessionID: { readonly sessionID: string }["sessionID"] - readonly title: { readonly title: string }["title"] -} - -export type SessionRenameOutput = void - -export type SessionMoveInput = { - readonly sessionID: { readonly sessionID: string }["sessionID"] - readonly destination: { - readonly destination: { readonly directory: string } - readonly moveChanges?: boolean | undefined - }["destination"] - readonly moveChanges?: { - readonly destination: { readonly directory: string } - readonly moveChanges?: boolean | undefined - }["moveChanges"] -} - -export type SessionMoveOutput = void - -export type SessionPromptInput = { - readonly sessionID: { readonly sessionID: string }["sessionID"] - readonly id?: { - readonly id?: string | null - readonly text: string - readonly files?: ReadonlyArray<{ - readonly uri: string - readonly name?: string - readonly description?: string - readonly mention?: { readonly start: number; readonly end: number; readonly text: string } - }> - readonly agents?: ReadonlyArray<{ - readonly name: string - readonly mention?: { readonly start: number; readonly end: number; readonly text: string } - }> - readonly metadata?: { readonly [x: string]: JsonValue } - readonly delivery?: "steer" | "queue" | null - readonly resume?: boolean | null - }["id"] - readonly text: { - readonly id?: string | null - readonly text: string - readonly files?: ReadonlyArray<{ - readonly uri: string - readonly name?: string - readonly description?: string - readonly mention?: { readonly start: number; readonly end: number; readonly text: string } - }> - readonly agents?: ReadonlyArray<{ - readonly name: string - readonly mention?: { readonly start: number; readonly end: number; readonly text: string } - }> - readonly metadata?: { readonly [x: string]: JsonValue } - readonly delivery?: "steer" | "queue" | null - readonly resume?: boolean | null - }["text"] - readonly files?: { - readonly id?: string | null - readonly text: string - readonly files?: ReadonlyArray<{ - readonly uri: string - readonly name?: string - readonly description?: string - readonly mention?: { readonly start: number; readonly end: number; readonly text: string } - }> - readonly agents?: ReadonlyArray<{ - readonly name: string - readonly mention?: { readonly start: number; readonly end: number; readonly text: string } - }> - readonly metadata?: { readonly [x: string]: JsonValue } - readonly delivery?: "steer" | "queue" | null - readonly resume?: boolean | null - }["files"] - readonly agents?: { - readonly id?: string | null - readonly text: string - readonly files?: ReadonlyArray<{ - readonly uri: string - readonly name?: string - readonly description?: string - readonly mention?: { readonly start: number; readonly end: number; readonly text: string } - }> - readonly agents?: ReadonlyArray<{ - readonly name: string - readonly mention?: { readonly start: number; readonly end: number; readonly text: string } - }> - readonly metadata?: { readonly [x: string]: JsonValue } - readonly delivery?: "steer" | "queue" | null - readonly resume?: boolean | null - }["agents"] - readonly metadata?: { - readonly id?: string | null - readonly text: string - readonly files?: ReadonlyArray<{ - readonly uri: string - readonly name?: string - readonly description?: string - readonly mention?: { readonly start: number; readonly end: number; readonly text: string } - }> - readonly agents?: ReadonlyArray<{ - readonly name: string - readonly mention?: { readonly start: number; readonly end: number; readonly text: string } - }> - readonly metadata?: { readonly [x: string]: JsonValue } - readonly delivery?: "steer" | "queue" | null - readonly resume?: boolean | null - }["metadata"] - readonly delivery?: { - readonly id?: string | null - readonly text: string - readonly files?: ReadonlyArray<{ - readonly uri: string - readonly name?: string - readonly description?: string - readonly mention?: { readonly start: number; readonly end: number; readonly text: string } - }> - readonly agents?: ReadonlyArray<{ - readonly name: string - readonly mention?: { readonly start: number; readonly end: number; readonly text: string } - }> - readonly metadata?: { readonly [x: string]: JsonValue } - readonly delivery?: "steer" | "queue" | null - readonly resume?: boolean | null - }["delivery"] - readonly resume?: { - readonly id?: string | null - readonly text: string - readonly files?: ReadonlyArray<{ - readonly uri: string - readonly name?: string - readonly description?: string - readonly mention?: { readonly start: number; readonly end: number; readonly text: string } - }> - readonly agents?: ReadonlyArray<{ - readonly name: string - readonly mention?: { readonly start: number; readonly end: number; readonly text: string } - }> - readonly metadata?: { readonly [x: string]: JsonValue } - readonly delivery?: "steer" | "queue" | null - readonly resume?: boolean | null - }["resume"] -} - -export type SessionPromptOutput = { data: SessionPendingUser }["data"] - -export type SessionCommandInput = { - readonly sessionID: { readonly sessionID: string }["sessionID"] - readonly id?: { - readonly id?: string | null - readonly command: string - readonly arguments?: string | null - readonly agent?: string | null - readonly model?: { readonly id: string; readonly providerID: string; readonly variant?: string } | null - readonly files?: ReadonlyArray<{ - readonly uri: string - readonly name?: string - readonly description?: string - readonly mention?: { readonly start: number; readonly end: number; readonly text: string } - }> - readonly agents?: ReadonlyArray<{ - readonly name: string - readonly mention?: { readonly start: number; readonly end: number; readonly text: string } - }> - readonly delivery?: "steer" | "queue" | null - readonly resume?: boolean | null - }["id"] - readonly command: { - readonly id?: string | null - readonly command: string - readonly arguments?: string | null - readonly agent?: string | null - readonly model?: { readonly id: string; readonly providerID: string; readonly variant?: string } | null - readonly files?: ReadonlyArray<{ - readonly uri: string - readonly name?: string - readonly description?: string - readonly mention?: { readonly start: number; readonly end: number; readonly text: string } - }> - readonly agents?: ReadonlyArray<{ - readonly name: string - readonly mention?: { readonly start: number; readonly end: number; readonly text: string } - }> - readonly delivery?: "steer" | "queue" | null - readonly resume?: boolean | null - }["command"] - readonly arguments?: { - readonly id?: string | null - readonly command: string - readonly arguments?: string | null - readonly agent?: string | null - readonly model?: { readonly id: string; readonly providerID: string; readonly variant?: string } | null - readonly files?: ReadonlyArray<{ - readonly uri: string - readonly name?: string - readonly description?: string - readonly mention?: { readonly start: number; readonly end: number; readonly text: string } - }> - readonly agents?: ReadonlyArray<{ - readonly name: string - readonly mention?: { readonly start: number; readonly end: number; readonly text: string } - }> - readonly delivery?: "steer" | "queue" | null - readonly resume?: boolean | null - }["arguments"] - readonly agent?: { - readonly id?: string | null - readonly command: string - readonly arguments?: string | null - readonly agent?: string | null - readonly model?: { readonly id: string; readonly providerID: string; readonly variant?: string } | null - readonly files?: ReadonlyArray<{ - readonly uri: string - readonly name?: string - readonly description?: string - readonly mention?: { readonly start: number; readonly end: number; readonly text: string } - }> - readonly agents?: ReadonlyArray<{ - readonly name: string - readonly mention?: { readonly start: number; readonly end: number; readonly text: string } - }> - readonly delivery?: "steer" | "queue" | null - readonly resume?: boolean | null - }["agent"] - readonly model?: { - readonly id?: string | null - readonly command: string - readonly arguments?: string | null - readonly agent?: string | null - readonly model?: { readonly id: string; readonly providerID: string; readonly variant?: string } | null - readonly files?: ReadonlyArray<{ - readonly uri: string - readonly name?: string - readonly description?: string - readonly mention?: { readonly start: number; readonly end: number; readonly text: string } - }> - readonly agents?: ReadonlyArray<{ - readonly name: string - readonly mention?: { readonly start: number; readonly end: number; readonly text: string } - }> - readonly delivery?: "steer" | "queue" | null - readonly resume?: boolean | null - }["model"] - readonly files?: { - readonly id?: string | null - readonly command: string - readonly arguments?: string | null - readonly agent?: string | null - readonly model?: { readonly id: string; readonly providerID: string; readonly variant?: string } | null - readonly files?: ReadonlyArray<{ - readonly uri: string - readonly name?: string - readonly description?: string - readonly mention?: { readonly start: number; readonly end: number; readonly text: string } - }> - readonly agents?: ReadonlyArray<{ - readonly name: string - readonly mention?: { readonly start: number; readonly end: number; readonly text: string } - }> - readonly delivery?: "steer" | "queue" | null - readonly resume?: boolean | null - }["files"] - readonly agents?: { - readonly id?: string | null - readonly command: string - readonly arguments?: string | null - readonly agent?: string | null - readonly model?: { readonly id: string; readonly providerID: string; readonly variant?: string } | null - readonly files?: ReadonlyArray<{ - readonly uri: string - readonly name?: string - readonly description?: string - readonly mention?: { readonly start: number; readonly end: number; readonly text: string } - }> - readonly agents?: ReadonlyArray<{ - readonly name: string - readonly mention?: { readonly start: number; readonly end: number; readonly text: string } - }> - readonly delivery?: "steer" | "queue" | null - readonly resume?: boolean | null - }["agents"] - readonly delivery?: { - readonly id?: string | null - readonly command: string - readonly arguments?: string | null - readonly agent?: string | null - readonly model?: { readonly id: string; readonly providerID: string; readonly variant?: string } | null - readonly files?: ReadonlyArray<{ - readonly uri: string - readonly name?: string - readonly description?: string - readonly mention?: { readonly start: number; readonly end: number; readonly text: string } - }> - readonly agents?: ReadonlyArray<{ - readonly name: string - readonly mention?: { readonly start: number; readonly end: number; readonly text: string } - }> - readonly delivery?: "steer" | "queue" | null - readonly resume?: boolean | null - }["delivery"] - readonly resume?: { - readonly id?: string | null - readonly command: string - readonly arguments?: string | null - readonly agent?: string | null - readonly model?: { readonly id: string; readonly providerID: string; readonly variant?: string } | null - readonly files?: ReadonlyArray<{ - readonly uri: string - readonly name?: string - readonly description?: string - readonly mention?: { readonly start: number; readonly end: number; readonly text: string } - }> - readonly agents?: ReadonlyArray<{ - readonly name: string - readonly mention?: { readonly start: number; readonly end: number; readonly text: string } - }> - readonly delivery?: "steer" | "queue" | null - readonly resume?: boolean | null - }["resume"] -} - -export type SessionCommandOutput = { data: SessionPendingUser }["data"] - -export type SessionSkillInput = { - readonly sessionID: { readonly sessionID: string }["sessionID"] - readonly id?: { - readonly id?: string | undefined - readonly skill: string - readonly resume?: boolean | undefined - }["id"] - readonly skill: { - readonly id?: string | undefined - readonly skill: string - readonly resume?: boolean | undefined - }["skill"] - readonly resume?: { - readonly id?: string | undefined - readonly skill: string - readonly resume?: boolean | undefined - }["resume"] -} - -export type SessionSkillOutput = void - -export type SessionSyntheticInput = { - readonly sessionID: { readonly sessionID: string }["sessionID"] - readonly id?: { - readonly id?: string | null - readonly text: string - readonly description?: string | null - readonly metadata?: { readonly [x: string]: JsonValue } - readonly delivery?: "steer" | "queue" | null - readonly resume?: boolean | null - }["id"] - readonly text: { - readonly id?: string | null - readonly text: string - readonly description?: string | null - readonly metadata?: { readonly [x: string]: JsonValue } - readonly delivery?: "steer" | "queue" | null - readonly resume?: boolean | null - }["text"] - readonly description?: { - readonly id?: string | null - readonly text: string - readonly description?: string | null - readonly metadata?: { readonly [x: string]: JsonValue } - readonly delivery?: "steer" | "queue" | null - readonly resume?: boolean | null - }["description"] - readonly metadata?: { - readonly id?: string | null - readonly text: string - readonly description?: string | null - readonly metadata?: { readonly [x: string]: JsonValue } - readonly delivery?: "steer" | "queue" | null - readonly resume?: boolean | null - }["metadata"] - readonly delivery?: { - readonly id?: string | null - readonly text: string - readonly description?: string | null - readonly metadata?: { readonly [x: string]: JsonValue } - readonly delivery?: "steer" | "queue" | null - readonly resume?: boolean | null - }["delivery"] - readonly resume?: { - readonly id?: string | null - readonly text: string - readonly description?: string | null - readonly metadata?: { readonly [x: string]: JsonValue } - readonly delivery?: "steer" | "queue" | null - readonly resume?: boolean | null - }["resume"] -} - -export type SessionSyntheticOutput = { data: SessionPendingSynthetic }["data"] - -export type SessionShellInput = { - readonly sessionID: { readonly sessionID: string }["sessionID"] - readonly id?: { readonly id?: string | undefined; readonly command: string }["id"] - readonly command: { readonly id?: string | undefined; readonly command: string }["command"] -} - -export type SessionShellOutput = void - -export type SessionCompactInput = { - readonly sessionID: { readonly sessionID: string }["sessionID"] - readonly id?: { readonly id?: string | undefined }["id"] -} - -export type SessionCompactOutput = { data: SessionPendingCompaction }["data"] - -export type SessionWaitInput = { readonly sessionID: { readonly sessionID: string }["sessionID"] } - -export type SessionWaitOutput = void - -export type SessionRevertStageInput = { - readonly sessionID: { readonly sessionID: string }["sessionID"] - readonly messageID: { readonly messageID: string; readonly files?: boolean | undefined }["messageID"] - readonly files?: { readonly messageID: string; readonly files?: boolean | undefined }["files"] -} - -export type SessionRevertStageOutput = { data: SessionRevert }["data"] - -export type SessionRevertClearInput = { readonly sessionID: { readonly sessionID: string }["sessionID"] } - -export type SessionRevertClearOutput = void - -export type SessionRevertCommitInput = { readonly sessionID: { readonly sessionID: string }["sessionID"] } - -export type SessionRevertCommitOutput = void - -export type SessionContextInput = { readonly sessionID: { readonly sessionID: string }["sessionID"] } - -export type SessionContextOutput = { data: Array }["data"] - -export type SessionPendingListInput = { readonly sessionID: { readonly sessionID: string }["sessionID"] } - -export type SessionPendingListOutput = { data: Array }["data"] - -export type SessionInstructionsEntryListInput = { readonly sessionID: { readonly sessionID: string }["sessionID"] } - -export type SessionInstructionsEntryListOutput = { data: Array }["data"] - -export type SessionInstructionsEntryPutInput = { - readonly sessionID: { readonly sessionID: string; readonly key: string }["sessionID"] - readonly key: { readonly sessionID: string; readonly key: string }["key"] - readonly value: { readonly value: JsonValue }["value"] -} - -export type SessionInstructionsEntryPutOutput = void - -export type SessionInstructionsEntryRemoveInput = { - readonly sessionID: { readonly sessionID: string; readonly key: string }["sessionID"] - readonly key: { readonly sessionID: string; readonly key: string }["key"] -} - -export type SessionInstructionsEntryRemoveOutput = void - -export type SessionLogInput = { - readonly sessionID: { readonly sessionID: string }["sessionID"] - readonly after?: { readonly after?: number | undefined; readonly follow?: boolean | undefined }["after"] - readonly follow?: { readonly after?: number | undefined; readonly follow?: boolean | undefined }["follow"] -} - -export type SessionLogOutput = SessionLogItem - -export type SessionInterruptInput = { readonly sessionID: { readonly sessionID: string }["sessionID"] } - -export type SessionInterruptOutput = void - -export type SessionBackgroundInput = { readonly sessionID: { readonly sessionID: string }["sessionID"] } - -export type SessionBackgroundOutput = void - -export type SessionMessageInput = { - readonly sessionID: { readonly sessionID: string; readonly messageID: string }["sessionID"] - readonly messageID: { readonly sessionID: string; readonly messageID: string }["messageID"] -} - -export type SessionMessageOutput = { data: SessionMessageInfo }["data"] - -export type MessageListInput = { - readonly sessionID: { readonly sessionID: string }["sessionID"] - readonly limit?: { - readonly limit?: number | undefined - readonly order?: "asc" | "desc" | undefined - readonly cursor?: string | undefined - }["limit"] - readonly order?: { - readonly limit?: number | undefined - readonly order?: "asc" | "desc" | undefined - readonly cursor?: string | undefined - }["order"] - readonly cursor?: { - readonly limit?: number | undefined - readonly order?: "asc" | "desc" | undefined - readonly cursor?: string | undefined - }["cursor"] -} - -export type MessageListOutput = SessionMessagesResponse - -export type ModelListInput = { - readonly location?: { - readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined - }["location"] -} - -export type ModelListOutput = { - location: { directory: string; workspaceID?: string; project: { id: string; directory: string } } - data: Array -} - -export type ModelDefaultInput = { - readonly location?: { - readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined - }["location"] -} - -export type ModelDefaultOutput = { - location: { directory: string; workspaceID?: string; project: { id: string; directory: string } } - data: ModelInfo | null -} - -export type GenerateTextInput = { - readonly location?: { - readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined - }["location"] - readonly prompt: { - readonly prompt: string - readonly model?: { readonly id: string; readonly providerID: string; readonly variant?: string } | null - }["prompt"] - readonly model?: { - readonly prompt: string - readonly model?: { readonly id: string; readonly providerID: string; readonly variant?: string } | null - }["model"] -} - -export type GenerateTextOutput = GenerateTextResponse["data"] - -export type ProviderListInput = { - readonly location?: { - readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined - }["location"] -} - -export type ProviderListOutput = { - location: { directory: string; workspaceID?: string; project: { id: string; directory: string } } - data: Array -} - -export type ProviderGetInput = { - readonly providerID: { readonly providerID: string }["providerID"] - readonly location?: { - readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined - }["location"] -} - -export type ProviderGetOutput = { - location: { directory: string; workspaceID?: string; project: { id: string; directory: string } } - data: ProviderV2Info -} - -export type IntegrationListInput = { - readonly location?: { - readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined - }["location"] -} - -export type IntegrationListOutput = { - location: { directory: string; workspaceID?: string; project: { id: string; directory: string } } - data: Array -} - -export type IntegrationGetInput = { - readonly integrationID: { readonly integrationID: string }["integrationID"] - readonly location?: { - readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined - }["location"] -} - -export type IntegrationGetOutput = { - location: { directory: string; workspaceID?: string; project: { id: string; directory: string } } - data: IntegrationInfo | null -} - -export type IntegrationConnectKeyInput = { - readonly integrationID: { readonly integrationID: string }["integrationID"] - readonly location?: { - readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined - }["location"] - readonly key: { readonly key: string; readonly label?: string | undefined }["key"] - readonly label?: { readonly key: string; readonly label?: string | undefined }["label"] -} - -export type IntegrationConnectKeyOutput = void - -export type IntegrationConnectOauthInput = { - readonly integrationID: { readonly integrationID: string }["integrationID"] - readonly location?: { - readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined - }["location"] - readonly methodID: { - readonly methodID: string - readonly inputs: { readonly [x: string]: string } - readonly label?: string | undefined - }["methodID"] - readonly inputs: { - readonly methodID: string - readonly inputs: { readonly [x: string]: string } - readonly label?: string | undefined - }["inputs"] - readonly label?: { - readonly methodID: string - readonly inputs: { readonly [x: string]: string } - readonly label?: string | undefined - }["label"] -} - -export type IntegrationConnectOauthOutput = { - location: { directory: string; workspaceID?: string; project: { id: string; directory: string } } - data: { - attemptID: string - url: string - instructions: string - mode: "auto" | "code" - time: { created: number | "Infinity" | "-Infinity" | "NaN"; expires: number | "Infinity" | "-Infinity" | "NaN" } - } -} - -export type IntegrationAttemptStatusInput = { - readonly attemptID: { readonly attemptID: string }["attemptID"] - readonly location?: { - readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined - }["location"] -} - -export type IntegrationAttemptStatusOutput = { - location: { directory: string; workspaceID?: string; project: { id: string; directory: string } } - data: IntegrationAttemptStatus -} - -export type IntegrationAttemptCompleteInput = { - readonly attemptID: { readonly attemptID: string }["attemptID"] - readonly location?: { - readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined - }["location"] - readonly code?: { readonly code?: string | undefined }["code"] -} - -export type IntegrationAttemptCompleteOutput = void - -export type IntegrationAttemptCancelInput = { - readonly attemptID: { readonly attemptID: string }["attemptID"] - readonly location?: { - readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined - }["location"] -} - -export type IntegrationAttemptCancelOutput = void - -export type McpListInput = { - readonly location?: { - readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined - }["location"] -} - -export type McpListOutput = { - location: { directory: string; workspaceID?: string; project: { id: string; directory: string } } - data: Array -} - -export type McpResourceCatalogInput = { - readonly location?: { - readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined - }["location"] -} - -export type McpResourceCatalogOutput = { - location: { directory: string; workspaceID?: string; project: { id: string; directory: string } } - data: McpResourceCatalog -} - -export type CredentialUpdateInput = { - readonly credentialID: { readonly credentialID: string }["credentialID"] - readonly location?: { - readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined - }["location"] - readonly label: { readonly label: string }["label"] -} - -export type CredentialUpdateOutput = void - -export type CredentialRemoveInput = { - readonly credentialID: { readonly credentialID: string }["credentialID"] - readonly location?: { - readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined - }["location"] -} - -export type CredentialRemoveOutput = void - -export type ProjectListOutput = Array - -export type ProjectCurrentInput = { - readonly location?: { - readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined - }["location"] -} - -export type ProjectCurrentOutput = ProjectCurrent - -export type ProjectDirectoriesInput = { - readonly projectID: { readonly projectID: string }["projectID"] - readonly location?: { - readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined - }["location"] -} - -export type ProjectDirectoriesOutput = ProjectDirectories - -export type FormRequestListInput = { - readonly location?: { - readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined - }["location"] -} - -export type FormRequestListOutput = { - location: { directory: string; workspaceID?: string; project: { id: string; directory: string } } - data: Array -} - -export type FormListInput = { readonly sessionID: { readonly sessionID: string }["sessionID"] } - -export type FormListOutput = { data: Array }["data"] - -export type FormCreateInput = { - readonly sessionID: { readonly sessionID: string }["sessionID"] - readonly id?: { - readonly id?: string | null - readonly title: string - readonly metadata?: { readonly [x: string]: JsonValue } - readonly fields: readonly [ - ( - | { - readonly key: string - readonly title?: string - readonly description?: string - readonly required?: boolean - readonly when?: ReadonlyArray<{ - readonly key: string - readonly op: "eq" | "neq" - readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean - }> - readonly type: "string" - readonly format?: "email" | "uri" | "date" | "date-time" - readonly minLength?: number - readonly maxLength?: number - readonly pattern?: string - readonly placeholder?: string - readonly default?: string - readonly options?: ReadonlyArray<{ - readonly value: string - readonly label: string - readonly description?: string - }> - readonly custom?: boolean - } - | { - readonly key: string - readonly title?: string - readonly description?: string - readonly required?: boolean - readonly when?: ReadonlyArray<{ - readonly key: string - readonly op: "eq" | "neq" - readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean - }> - readonly type: "number" - readonly minimum?: number | "Infinity" | "-Infinity" | "NaN" - readonly maximum?: number | "Infinity" | "-Infinity" | "NaN" - readonly default?: number | "Infinity" | "-Infinity" | "NaN" - } - | { - readonly key: string - readonly title?: string - readonly description?: string - readonly required?: boolean - readonly when?: ReadonlyArray<{ - readonly key: string - readonly op: "eq" | "neq" - readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean - }> - readonly type: "integer" - readonly minimum?: number | "Infinity" | "-Infinity" | "NaN" - readonly maximum?: number | "Infinity" | "-Infinity" | "NaN" - readonly default?: number | "Infinity" | "-Infinity" | "NaN" - } - | { - readonly key: string - readonly title?: string - readonly description?: string - readonly required?: boolean - readonly when?: ReadonlyArray<{ - readonly key: string - readonly op: "eq" | "neq" - readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean - }> - readonly type: "boolean" - readonly default?: boolean - } - | { - readonly key: string - readonly title?: string - readonly description?: string - readonly required?: boolean - readonly when?: ReadonlyArray<{ - readonly key: string - readonly op: "eq" | "neq" - readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean - }> - readonly type: "multiselect" - readonly options: ReadonlyArray<{ - readonly value: string - readonly label: string - readonly description?: string - }> - readonly minItems?: number - readonly maxItems?: number - readonly custom?: boolean - readonly default?: ReadonlyArray - } - | { - readonly key: string - readonly type: "external" - readonly url: string - readonly title?: string - readonly description?: string - } - ), - ...Array< - | { - readonly key: string - readonly title?: string - readonly description?: string - readonly required?: boolean - readonly when?: ReadonlyArray<{ - readonly key: string - readonly op: "eq" | "neq" - readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean - }> - readonly type: "string" - readonly format?: "email" | "uri" | "date" | "date-time" - readonly minLength?: number - readonly maxLength?: number - readonly pattern?: string - readonly placeholder?: string - readonly default?: string - readonly options?: ReadonlyArray<{ - readonly value: string - readonly label: string - readonly description?: string - }> - readonly custom?: boolean - } - | { - readonly key: string - readonly title?: string - readonly description?: string - readonly required?: boolean - readonly when?: ReadonlyArray<{ - readonly key: string - readonly op: "eq" | "neq" - readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean - }> - readonly type: "number" - readonly minimum?: number | "Infinity" | "-Infinity" | "NaN" - readonly maximum?: number | "Infinity" | "-Infinity" | "NaN" - readonly default?: number | "Infinity" | "-Infinity" | "NaN" - } - | { - readonly key: string - readonly title?: string - readonly description?: string - readonly required?: boolean - readonly when?: ReadonlyArray<{ - readonly key: string - readonly op: "eq" | "neq" - readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean - }> - readonly type: "integer" - readonly minimum?: number | "Infinity" | "-Infinity" | "NaN" - readonly maximum?: number | "Infinity" | "-Infinity" | "NaN" - readonly default?: number | "Infinity" | "-Infinity" | "NaN" - } - | { - readonly key: string - readonly title?: string - readonly description?: string - readonly required?: boolean - readonly when?: ReadonlyArray<{ - readonly key: string - readonly op: "eq" | "neq" - readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean - }> - readonly type: "boolean" - readonly default?: boolean - } - | { - readonly key: string - readonly title?: string - readonly description?: string - readonly required?: boolean - readonly when?: ReadonlyArray<{ - readonly key: string - readonly op: "eq" | "neq" - readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean - }> - readonly type: "multiselect" - readonly options: ReadonlyArray<{ - readonly value: string - readonly label: string - readonly description?: string - }> - readonly minItems?: number - readonly maxItems?: number - readonly custom?: boolean - readonly default?: ReadonlyArray - } - | { - readonly key: string - readonly type: "external" - readonly url: string - readonly title?: string - readonly description?: string - } - >, - ] - }["id"] - readonly title: { - readonly id?: string | null - readonly title: string - readonly metadata?: { readonly [x: string]: JsonValue } - readonly fields: readonly [ - ( - | { - readonly key: string - readonly title?: string - readonly description?: string - readonly required?: boolean - readonly when?: ReadonlyArray<{ - readonly key: string - readonly op: "eq" | "neq" - readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean - }> - readonly type: "string" - readonly format?: "email" | "uri" | "date" | "date-time" - readonly minLength?: number - readonly maxLength?: number - readonly pattern?: string - readonly placeholder?: string - readonly default?: string - readonly options?: ReadonlyArray<{ - readonly value: string - readonly label: string - readonly description?: string - }> - readonly custom?: boolean - } - | { - readonly key: string - readonly title?: string - readonly description?: string - readonly required?: boolean - readonly when?: ReadonlyArray<{ - readonly key: string - readonly op: "eq" | "neq" - readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean - }> - readonly type: "number" - readonly minimum?: number | "Infinity" | "-Infinity" | "NaN" - readonly maximum?: number | "Infinity" | "-Infinity" | "NaN" - readonly default?: number | "Infinity" | "-Infinity" | "NaN" - } - | { - readonly key: string - readonly title?: string - readonly description?: string - readonly required?: boolean - readonly when?: ReadonlyArray<{ - readonly key: string - readonly op: "eq" | "neq" - readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean - }> - readonly type: "integer" - readonly minimum?: number | "Infinity" | "-Infinity" | "NaN" - readonly maximum?: number | "Infinity" | "-Infinity" | "NaN" - readonly default?: number | "Infinity" | "-Infinity" | "NaN" - } - | { - readonly key: string - readonly title?: string - readonly description?: string - readonly required?: boolean - readonly when?: ReadonlyArray<{ - readonly key: string - readonly op: "eq" | "neq" - readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean - }> - readonly type: "boolean" - readonly default?: boolean - } - | { - readonly key: string - readonly title?: string - readonly description?: string - readonly required?: boolean - readonly when?: ReadonlyArray<{ - readonly key: string - readonly op: "eq" | "neq" - readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean - }> - readonly type: "multiselect" - readonly options: ReadonlyArray<{ - readonly value: string - readonly label: string - readonly description?: string - }> - readonly minItems?: number - readonly maxItems?: number - readonly custom?: boolean - readonly default?: ReadonlyArray - } - | { - readonly key: string - readonly type: "external" - readonly url: string - readonly title?: string - readonly description?: string - } - ), - ...Array< - | { - readonly key: string - readonly title?: string - readonly description?: string - readonly required?: boolean - readonly when?: ReadonlyArray<{ - readonly key: string - readonly op: "eq" | "neq" - readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean - }> - readonly type: "string" - readonly format?: "email" | "uri" | "date" | "date-time" - readonly minLength?: number - readonly maxLength?: number - readonly pattern?: string - readonly placeholder?: string - readonly default?: string - readonly options?: ReadonlyArray<{ - readonly value: string - readonly label: string - readonly description?: string - }> - readonly custom?: boolean - } - | { - readonly key: string - readonly title?: string - readonly description?: string - readonly required?: boolean - readonly when?: ReadonlyArray<{ - readonly key: string - readonly op: "eq" | "neq" - readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean - }> - readonly type: "number" - readonly minimum?: number | "Infinity" | "-Infinity" | "NaN" - readonly maximum?: number | "Infinity" | "-Infinity" | "NaN" - readonly default?: number | "Infinity" | "-Infinity" | "NaN" - } - | { - readonly key: string - readonly title?: string - readonly description?: string - readonly required?: boolean - readonly when?: ReadonlyArray<{ - readonly key: string - readonly op: "eq" | "neq" - readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean - }> - readonly type: "integer" - readonly minimum?: number | "Infinity" | "-Infinity" | "NaN" - readonly maximum?: number | "Infinity" | "-Infinity" | "NaN" - readonly default?: number | "Infinity" | "-Infinity" | "NaN" - } - | { - readonly key: string - readonly title?: string - readonly description?: string - readonly required?: boolean - readonly when?: ReadonlyArray<{ - readonly key: string - readonly op: "eq" | "neq" - readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean - }> - readonly type: "boolean" - readonly default?: boolean - } - | { - readonly key: string - readonly title?: string - readonly description?: string - readonly required?: boolean - readonly when?: ReadonlyArray<{ - readonly key: string - readonly op: "eq" | "neq" - readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean - }> - readonly type: "multiselect" - readonly options: ReadonlyArray<{ - readonly value: string - readonly label: string - readonly description?: string - }> - readonly minItems?: number - readonly maxItems?: number - readonly custom?: boolean - readonly default?: ReadonlyArray - } - | { - readonly key: string - readonly type: "external" - readonly url: string - readonly title?: string - readonly description?: string - } - >, - ] - }["title"] - readonly metadata?: { - readonly id?: string | null - readonly title: string - readonly metadata?: { readonly [x: string]: JsonValue } - readonly fields: readonly [ - ( - | { - readonly key: string - readonly title?: string - readonly description?: string - readonly required?: boolean - readonly when?: ReadonlyArray<{ - readonly key: string - readonly op: "eq" | "neq" - readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean - }> - readonly type: "string" - readonly format?: "email" | "uri" | "date" | "date-time" - readonly minLength?: number - readonly maxLength?: number - readonly pattern?: string - readonly placeholder?: string - readonly default?: string - readonly options?: ReadonlyArray<{ - readonly value: string - readonly label: string - readonly description?: string - }> - readonly custom?: boolean - } - | { - readonly key: string - readonly title?: string - readonly description?: string - readonly required?: boolean - readonly when?: ReadonlyArray<{ - readonly key: string - readonly op: "eq" | "neq" - readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean - }> - readonly type: "number" - readonly minimum?: number | "Infinity" | "-Infinity" | "NaN" - readonly maximum?: number | "Infinity" | "-Infinity" | "NaN" - readonly default?: number | "Infinity" | "-Infinity" | "NaN" - } - | { - readonly key: string - readonly title?: string - readonly description?: string - readonly required?: boolean - readonly when?: ReadonlyArray<{ - readonly key: string - readonly op: "eq" | "neq" - readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean - }> - readonly type: "integer" - readonly minimum?: number | "Infinity" | "-Infinity" | "NaN" - readonly maximum?: number | "Infinity" | "-Infinity" | "NaN" - readonly default?: number | "Infinity" | "-Infinity" | "NaN" - } - | { - readonly key: string - readonly title?: string - readonly description?: string - readonly required?: boolean - readonly when?: ReadonlyArray<{ - readonly key: string - readonly op: "eq" | "neq" - readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean - }> - readonly type: "boolean" - readonly default?: boolean - } - | { - readonly key: string - readonly title?: string - readonly description?: string - readonly required?: boolean - readonly when?: ReadonlyArray<{ - readonly key: string - readonly op: "eq" | "neq" - readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean - }> - readonly type: "multiselect" - readonly options: ReadonlyArray<{ - readonly value: string - readonly label: string - readonly description?: string - }> - readonly minItems?: number - readonly maxItems?: number - readonly custom?: boolean - readonly default?: ReadonlyArray - } - | { - readonly key: string - readonly type: "external" - readonly url: string - readonly title?: string - readonly description?: string - } - ), - ...Array< - | { - readonly key: string - readonly title?: string - readonly description?: string - readonly required?: boolean - readonly when?: ReadonlyArray<{ - readonly key: string - readonly op: "eq" | "neq" - readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean - }> - readonly type: "string" - readonly format?: "email" | "uri" | "date" | "date-time" - readonly minLength?: number - readonly maxLength?: number - readonly pattern?: string - readonly placeholder?: string - readonly default?: string - readonly options?: ReadonlyArray<{ - readonly value: string - readonly label: string - readonly description?: string - }> - readonly custom?: boolean - } - | { - readonly key: string - readonly title?: string - readonly description?: string - readonly required?: boolean - readonly when?: ReadonlyArray<{ - readonly key: string - readonly op: "eq" | "neq" - readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean - }> - readonly type: "number" - readonly minimum?: number | "Infinity" | "-Infinity" | "NaN" - readonly maximum?: number | "Infinity" | "-Infinity" | "NaN" - readonly default?: number | "Infinity" | "-Infinity" | "NaN" - } - | { - readonly key: string - readonly title?: string - readonly description?: string - readonly required?: boolean - readonly when?: ReadonlyArray<{ - readonly key: string - readonly op: "eq" | "neq" - readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean - }> - readonly type: "integer" - readonly minimum?: number | "Infinity" | "-Infinity" | "NaN" - readonly maximum?: number | "Infinity" | "-Infinity" | "NaN" - readonly default?: number | "Infinity" | "-Infinity" | "NaN" - } - | { - readonly key: string - readonly title?: string - readonly description?: string - readonly required?: boolean - readonly when?: ReadonlyArray<{ - readonly key: string - readonly op: "eq" | "neq" - readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean - }> - readonly type: "boolean" - readonly default?: boolean - } - | { - readonly key: string - readonly title?: string - readonly description?: string - readonly required?: boolean - readonly when?: ReadonlyArray<{ - readonly key: string - readonly op: "eq" | "neq" - readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean - }> - readonly type: "multiselect" - readonly options: ReadonlyArray<{ - readonly value: string - readonly label: string - readonly description?: string - }> - readonly minItems?: number - readonly maxItems?: number - readonly custom?: boolean - readonly default?: ReadonlyArray - } - | { - readonly key: string - readonly type: "external" - readonly url: string - readonly title?: string - readonly description?: string - } - >, - ] - }["metadata"] - readonly fields: { - readonly id?: string | null - readonly title: string - readonly metadata?: { readonly [x: string]: JsonValue } - readonly fields: readonly [ - ( - | { - readonly key: string - readonly title?: string - readonly description?: string - readonly required?: boolean - readonly when?: ReadonlyArray<{ - readonly key: string - readonly op: "eq" | "neq" - readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean - }> - readonly type: "string" - readonly format?: "email" | "uri" | "date" | "date-time" - readonly minLength?: number - readonly maxLength?: number - readonly pattern?: string - readonly placeholder?: string - readonly default?: string - readonly options?: ReadonlyArray<{ - readonly value: string - readonly label: string - readonly description?: string - }> - readonly custom?: boolean - } - | { - readonly key: string - readonly title?: string - readonly description?: string - readonly required?: boolean - readonly when?: ReadonlyArray<{ - readonly key: string - readonly op: "eq" | "neq" - readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean - }> - readonly type: "number" - readonly minimum?: number | "Infinity" | "-Infinity" | "NaN" - readonly maximum?: number | "Infinity" | "-Infinity" | "NaN" - readonly default?: number | "Infinity" | "-Infinity" | "NaN" - } - | { - readonly key: string - readonly title?: string - readonly description?: string - readonly required?: boolean - readonly when?: ReadonlyArray<{ - readonly key: string - readonly op: "eq" | "neq" - readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean - }> - readonly type: "integer" - readonly minimum?: number | "Infinity" | "-Infinity" | "NaN" - readonly maximum?: number | "Infinity" | "-Infinity" | "NaN" - readonly default?: number | "Infinity" | "-Infinity" | "NaN" - } - | { - readonly key: string - readonly title?: string - readonly description?: string - readonly required?: boolean - readonly when?: ReadonlyArray<{ - readonly key: string - readonly op: "eq" | "neq" - readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean - }> - readonly type: "boolean" - readonly default?: boolean - } - | { - readonly key: string - readonly title?: string - readonly description?: string - readonly required?: boolean - readonly when?: ReadonlyArray<{ - readonly key: string - readonly op: "eq" | "neq" - readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean - }> - readonly type: "multiselect" - readonly options: ReadonlyArray<{ - readonly value: string - readonly label: string - readonly description?: string - }> - readonly minItems?: number - readonly maxItems?: number - readonly custom?: boolean - readonly default?: ReadonlyArray - } - | { - readonly key: string - readonly type: "external" - readonly url: string - readonly title?: string - readonly description?: string - } - ), - ...Array< - | { - readonly key: string - readonly title?: string - readonly description?: string - readonly required?: boolean - readonly when?: ReadonlyArray<{ - readonly key: string - readonly op: "eq" | "neq" - readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean - }> - readonly type: "string" - readonly format?: "email" | "uri" | "date" | "date-time" - readonly minLength?: number - readonly maxLength?: number - readonly pattern?: string - readonly placeholder?: string - readonly default?: string - readonly options?: ReadonlyArray<{ - readonly value: string - readonly label: string - readonly description?: string - }> - readonly custom?: boolean - } - | { - readonly key: string - readonly title?: string - readonly description?: string - readonly required?: boolean - readonly when?: ReadonlyArray<{ - readonly key: string - readonly op: "eq" | "neq" - readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean - }> - readonly type: "number" - readonly minimum?: number | "Infinity" | "-Infinity" | "NaN" - readonly maximum?: number | "Infinity" | "-Infinity" | "NaN" - readonly default?: number | "Infinity" | "-Infinity" | "NaN" - } - | { - readonly key: string - readonly title?: string - readonly description?: string - readonly required?: boolean - readonly when?: ReadonlyArray<{ - readonly key: string - readonly op: "eq" | "neq" - readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean - }> - readonly type: "integer" - readonly minimum?: number | "Infinity" | "-Infinity" | "NaN" - readonly maximum?: number | "Infinity" | "-Infinity" | "NaN" - readonly default?: number | "Infinity" | "-Infinity" | "NaN" - } - | { - readonly key: string - readonly title?: string - readonly description?: string - readonly required?: boolean - readonly when?: ReadonlyArray<{ - readonly key: string - readonly op: "eq" | "neq" - readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean - }> - readonly type: "boolean" - readonly default?: boolean - } - | { - readonly key: string - readonly title?: string - readonly description?: string - readonly required?: boolean - readonly when?: ReadonlyArray<{ - readonly key: string - readonly op: "eq" | "neq" - readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean - }> - readonly type: "multiselect" - readonly options: ReadonlyArray<{ - readonly value: string - readonly label: string - readonly description?: string - }> - readonly minItems?: number - readonly maxItems?: number - readonly custom?: boolean - readonly default?: ReadonlyArray - } - | { - readonly key: string - readonly type: "external" - readonly url: string - readonly title?: string - readonly description?: string - } - >, - ] - }["fields"] -} - -export type FormCreateOutput = { data: FormInfo }["data"] - -export type FormGetInput = { - readonly sessionID: { readonly sessionID: string; readonly formID: string }["sessionID"] - readonly formID: { readonly sessionID: string; readonly formID: string }["formID"] -} - -export type FormGetOutput = { data: FormInfo }["data"] - -export type FormStateInput = { - readonly sessionID: { readonly sessionID: string; readonly formID: string }["sessionID"] - readonly formID: { readonly sessionID: string; readonly formID: string }["formID"] -} - -export type FormStateOutput = { data: FormState }["data"] - -export type FormReplyInput = { - readonly sessionID: { readonly sessionID: string; readonly formID: string }["sessionID"] - readonly formID: { readonly sessionID: string; readonly formID: string }["formID"] - readonly answer: { - readonly answer: { readonly [x: string]: string | number | boolean | ReadonlyArray } - }["answer"] -} - -export type FormReplyOutput = void - -export type FormCancelInput = { - readonly sessionID: { readonly sessionID: string; readonly formID: string }["sessionID"] - readonly formID: { readonly sessionID: string; readonly formID: string }["formID"] -} - -export type FormCancelOutput = void - -export type PermissionRequestListInput = { - readonly location?: { - readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined - }["location"] -} - -export type PermissionRequestListOutput = { - location: { directory: string; workspaceID?: string; project: { id: string; directory: string } } - data: Array -} - -export type PermissionSavedListInput = { readonly projectID?: { readonly projectID?: string | undefined }["projectID"] } - -export type PermissionSavedListOutput = { data: Array }["data"] - -export type PermissionSavedRemoveInput = { readonly id: { readonly id: string }["id"] } - -export type PermissionSavedRemoveOutput = void - -export type PermissionCreateInput = { - readonly sessionID: { readonly sessionID: string }["sessionID"] - readonly id?: { - readonly id?: string | null - readonly action: string - readonly resources: ReadonlyArray - readonly save?: ReadonlyArray - readonly metadata?: { readonly [x: string]: JsonValue } - readonly source?: { readonly type: "tool"; readonly messageID: string; readonly callID: string } - readonly agent?: string | null - }["id"] - readonly action: { - readonly id?: string | null - readonly action: string - readonly resources: ReadonlyArray - readonly save?: ReadonlyArray - readonly metadata?: { readonly [x: string]: JsonValue } - readonly source?: { readonly type: "tool"; readonly messageID: string; readonly callID: string } - readonly agent?: string | null - }["action"] - readonly resources: { - readonly id?: string | null - readonly action: string - readonly resources: ReadonlyArray - readonly save?: ReadonlyArray - readonly metadata?: { readonly [x: string]: JsonValue } - readonly source?: { readonly type: "tool"; readonly messageID: string; readonly callID: string } - readonly agent?: string | null - }["resources"] - readonly save?: { - readonly id?: string | null - readonly action: string - readonly resources: ReadonlyArray - readonly save?: ReadonlyArray - readonly metadata?: { readonly [x: string]: JsonValue } - readonly source?: { readonly type: "tool"; readonly messageID: string; readonly callID: string } - readonly agent?: string | null - }["save"] - readonly metadata?: { - readonly id?: string | null - readonly action: string - readonly resources: ReadonlyArray - readonly save?: ReadonlyArray - readonly metadata?: { readonly [x: string]: JsonValue } - readonly source?: { readonly type: "tool"; readonly messageID: string; readonly callID: string } - readonly agent?: string | null - }["metadata"] - readonly source?: { - readonly id?: string | null - readonly action: string - readonly resources: ReadonlyArray - readonly save?: ReadonlyArray - readonly metadata?: { readonly [x: string]: JsonValue } - readonly source?: { readonly type: "tool"; readonly messageID: string; readonly callID: string } - readonly agent?: string | null - }["source"] - readonly agent?: { - readonly id?: string | null - readonly action: string - readonly resources: ReadonlyArray - readonly save?: ReadonlyArray - readonly metadata?: { readonly [x: string]: JsonValue } - readonly source?: { readonly type: "tool"; readonly messageID: string; readonly callID: string } - readonly agent?: string | null - }["agent"] -} - -export type PermissionCreateOutput = { data: { id: string; effect: PermissionV2Effect } }["data"] - -export type PermissionListInput = { readonly sessionID: { readonly sessionID: string }["sessionID"] } - -export type PermissionListOutput = { data: Array }["data"] - -export type PermissionGetInput = { - readonly sessionID: { readonly sessionID: string; readonly requestID: string }["sessionID"] - readonly requestID: { readonly sessionID: string; readonly requestID: string }["requestID"] -} - -export type PermissionGetOutput = { data: PermissionV2Request }["data"] - -export type PermissionReplyInput = { - readonly sessionID: { readonly sessionID: string; readonly requestID: string }["sessionID"] - readonly requestID: { readonly sessionID: string; readonly requestID: string }["requestID"] - readonly reply: { readonly reply: "once" | "always" | "reject"; readonly message?: string | undefined }["reply"] - readonly message?: { readonly reply: "once" | "always" | "reject"; readonly message?: string | undefined }["message"] -} - -export type PermissionReplyOutput = void - -export type FileReadInput = { - readonly location?: { - readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined - }["location"] - readonly path: string -} - -export type FileReadOutput = globalThis.Uint8Array - -export type FileListInput = { - readonly location?: { - readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined - readonly path?: string | undefined - }["location"] - readonly path?: { - readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined - readonly path?: string | undefined - }["path"] -} - -export type FileListOutput = { - location: { directory: string; workspaceID?: string; project: { id: string; directory: string } } - data: Array -} - -export type FileFindInput = { - readonly location?: { - readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined - readonly query: string - readonly type?: "file" | "directory" | undefined - readonly limit?: number | undefined - }["location"] - readonly query: { - readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined - readonly query: string - readonly type?: "file" | "directory" | undefined - readonly limit?: number | undefined - }["query"] - readonly type?: { - readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined - readonly query: string - readonly type?: "file" | "directory" | undefined - readonly limit?: number | undefined - }["type"] - readonly limit?: { - readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined - readonly query: string - readonly type?: "file" | "directory" | undefined - readonly limit?: number | undefined - }["limit"] -} - -export type FileFindOutput = { - location: { directory: string; workspaceID?: string; project: { id: string; directory: string } } - data: Array -} - -export type CommandListInput = { - readonly location?: { - readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined - }["location"] -} - -export type CommandListOutput = { - location: { directory: string; workspaceID?: string; project: { id: string; directory: string } } - data: Array -} - -export type SkillListInput = { - readonly location?: { - readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined - }["location"] -} - -export type SkillListOutput = { - location: { directory: string; workspaceID?: string; project: { id: string; directory: string } } - data: Array -} - -export type EventSubscribeOutput = V2Event - -export type PtyListInput = { - readonly location?: { - readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined - }["location"] -} - -export type PtyListOutput = { - location: { directory: string; workspaceID?: string; project: { id: string; directory: string } } - data: Array -} - -export type PtyCreateInput = { - readonly location?: { - readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined - }["location"] - readonly command?: { - readonly command?: string - readonly args?: ReadonlyArray - readonly cwd?: string - readonly title?: string - readonly env?: { readonly [x: string]: string } - }["command"] - readonly args?: { - readonly command?: string - readonly args?: ReadonlyArray - readonly cwd?: string - readonly title?: string - readonly env?: { readonly [x: string]: string } - }["args"] - readonly cwd?: { - readonly command?: string - readonly args?: ReadonlyArray - readonly cwd?: string - readonly title?: string - readonly env?: { readonly [x: string]: string } - }["cwd"] - readonly title?: { - readonly command?: string - readonly args?: ReadonlyArray - readonly cwd?: string - readonly title?: string - readonly env?: { readonly [x: string]: string } - }["title"] - readonly env?: { - readonly command?: string - readonly args?: ReadonlyArray - readonly cwd?: string - readonly title?: string - readonly env?: { readonly [x: string]: string } - }["env"] -} - -export type PtyCreateOutput = { - location: { directory: string; workspaceID?: string; project: { id: string; directory: string } } - data: Pty -} - -export type PtyGetInput = { - readonly ptyID: { readonly ptyID: string }["ptyID"] - readonly location?: { - readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined - }["location"] -} - -export type PtyGetOutput = { - location: { directory: string; workspaceID?: string; project: { id: string; directory: string } } - data: Pty -} - -export type PtyUpdateInput = { - readonly ptyID: { readonly ptyID: string }["ptyID"] - readonly location?: { - readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined - }["location"] - readonly title?: { - readonly title?: string - readonly size?: { readonly rows: number; readonly cols: number } - }["title"] - readonly size?: { readonly title?: string; readonly size?: { readonly rows: number; readonly cols: number } }["size"] -} - -export type PtyUpdateOutput = { - location: { directory: string; workspaceID?: string; project: { id: string; directory: string } } - data: Pty -} - -export type PtyRemoveInput = { - readonly ptyID: { readonly ptyID: string }["ptyID"] - readonly location?: { - readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined - }["location"] -} - -export type PtyRemoveOutput = void - -export type ShellListInput = { - readonly location?: { - readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined - }["location"] -} - -export type ShellListOutput = { - location: { directory: string; workspaceID?: string; project: { id: string; directory: string } } - data: Array -} - -export type ShellCreateInput = { - readonly location?: { - readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined - }["location"] - readonly command: { - readonly command: string - readonly cwd?: string - readonly timeout: number - readonly metadata?: { readonly [x: string]: JsonValue } - }["command"] - readonly cwd?: { - readonly command: string - readonly cwd?: string - readonly timeout: number - readonly metadata?: { readonly [x: string]: JsonValue } - }["cwd"] - readonly timeout: { - readonly command: string - readonly cwd?: string - readonly timeout: number - readonly metadata?: { readonly [x: string]: JsonValue } - }["timeout"] - readonly metadata?: { - readonly command: string - readonly cwd?: string - readonly timeout: number - readonly metadata?: { readonly [x: string]: JsonValue } - }["metadata"] -} - -export type ShellCreateOutput = { - location: { directory: string; workspaceID?: string; project: { id: string; directory: string } } - data: ShellInfo1 -} - -export type ShellGetInput = { - readonly id: { readonly id: string }["id"] - readonly location?: { - readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined - }["location"] -} - -export type ShellGetOutput = { - location: { directory: string; workspaceID?: string; project: { id: string; directory: string } } - data: ShellInfo1 -} - -export type ShellTimeoutInput = { - readonly id: { readonly id: string }["id"] - readonly location?: { - readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined - }["location"] - readonly timeout: { readonly timeout: number }["timeout"] -} - -export type ShellTimeoutOutput = { - location: { directory: string; workspaceID?: string; project: { id: string; directory: string } } - data: ShellInfo1 -} - -export type ShellOutputInput = { - readonly id: { readonly id: string }["id"] - readonly location?: { - readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined - readonly cursor?: number | undefined - readonly limit?: number | undefined - }["location"] - readonly cursor?: { - readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined - readonly cursor?: number | undefined - readonly limit?: number | undefined - }["cursor"] - readonly limit?: { - readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined - readonly cursor?: number | undefined - readonly limit?: number | undefined - }["limit"] -} - -export type ShellOutputOutput = { - location: { directory: string; workspaceID?: string; project: { id: string; directory: string } } - data: { output: string; cursor: number; size: number; truncated: boolean } -} - -export type ShellRemoveInput = { - readonly id: { readonly id: string }["id"] - readonly location?: { - readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined - }["location"] -} - -export type ShellRemoveOutput = void - -export type QuestionRequestListInput = { - readonly location?: { - readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined - }["location"] -} - -export type QuestionRequestListOutput = { - location: { directory: string; workspaceID?: string; project: { id: string; directory: string } } - data: Array -} - -export type QuestionListInput = { readonly sessionID: { readonly sessionID: string }["sessionID"] } - -export type QuestionListOutput = { data: Array }["data"] - -export type QuestionReplyInput = { - readonly sessionID: { readonly sessionID: string; readonly requestID: string }["sessionID"] - readonly requestID: { readonly sessionID: string; readonly requestID: string }["requestID"] - readonly answers: { readonly answers: ReadonlyArray> }["answers"] -} - -export type QuestionReplyOutput = void - -export type QuestionRejectInput = { - readonly sessionID: { readonly sessionID: string; readonly requestID: string }["sessionID"] - readonly requestID: { readonly sessionID: string; readonly requestID: string }["requestID"] -} - -export type QuestionRejectOutput = void - -export type ReferenceListInput = { - readonly location?: { - readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined - }["location"] -} - -export type ReferenceListOutput = { - location: { directory: string; workspaceID?: string; project: { id: string; directory: string } } - data: Array -} - -export type ProjectCopyCreateInput = { - readonly projectID: { readonly projectID: string }["projectID"] - readonly location?: { - readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined - }["location"] - readonly strategy: { readonly strategy: string; readonly directory: string; readonly name?: string }["strategy"] - readonly directory: { readonly strategy: string; readonly directory: string; readonly name?: string }["directory"] - readonly name?: { readonly strategy: string; readonly directory: string; readonly name?: string }["name"] -} - -export type ProjectCopyCreateOutput = ProjectCopyCopy - -export type ProjectCopyRemoveInput = { - readonly projectID: { readonly projectID: string }["projectID"] - readonly location?: { - readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined - }["location"] - readonly directory: { readonly directory: string; readonly force: boolean }["directory"] - readonly force: { readonly directory: string; readonly force: boolean }["force"] -} - -export type ProjectCopyRemoveOutput = void - -export type ProjectCopyRefreshInput = { - readonly projectID: { readonly projectID: string }["projectID"] - readonly location?: { - readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined - }["location"] -} - -export type ProjectCopyRefreshOutput = void - -export type VcsStatusInput = { - readonly location?: { - readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined - }["location"] -} - -export type VcsStatusOutput = { - location: { directory: string; workspaceID?: string; project: { id: string; directory: string } } - data: Array -} - -export type VcsDiffInput = { - readonly location?: { - readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined - readonly mode: "working" | "branch" - readonly context?: number | undefined - }["location"] - readonly mode: { - readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined - readonly mode: "working" | "branch" - readonly context?: number | undefined - }["mode"] - readonly context?: { - readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined - readonly mode: "working" | "branch" - readonly context?: number | undefined - }["context"] -} - -export type VcsDiffOutput = { - location: { directory: string; workspaceID?: string; project: { id: string; directory: string } } - data: Array -} - -export type DebugLocationListOutput = Array - -export type DebugLocationEvictInput = { - readonly location?: { - readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined - }["location"] -} - -export type DebugLocationEvictOutput = void diff --git a/packages/client/src/promise/index.ts b/packages/client/src/promise/index.ts deleted file mode 100644 index fd889c64e5..0000000000 --- a/packages/client/src/promise/index.ts +++ /dev/null @@ -1,16 +0,0 @@ -export * from "./generated/index" -export type { - AgentApi, - CatalogApi, - CommandApi, - EventApi, - IntegrationApi, - ModelApi, - PluginApi, - ProviderApi, - ReferenceApi, - SessionApi, - SkillApi, -} from "./api.js" -export type { EventSubscribeOutput as OpenCodeEvent } from "./generated/types" -export type OpenCodeClient = ReturnType diff --git a/packages/client/test/api.types.ts b/packages/client/test/api.types.ts deleted file mode 100644 index ba95bb4297..0000000000 --- a/packages/client/test/api.types.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { Effect } from "effect" -import { OpenCode as EffectOpenCode, type AppApi as EffectApi } from "../src/effect" - -type EffectClient = Effect.Success> -type PromiseClient = ReturnType - -declare const effectClient: EffectClient -declare const promiseClient: PromiseClient - -const effectApi: EffectApi = effectClient - -declare const sessionID: Parameters[0]["sessionID"] - -const effectList: Effect.Effect< - ReadonlyArray<{ readonly key: string; readonly value: unknown }>, - unknown -> = effectApi.session.instructions.entry.list({ sessionID }) -const effectPut: Effect.Effect = effectApi.session.instructions.entry.put({ - sessionID, - key: "review-notes", - value: { text: "Check the diff" }, -}) -const effectRemove: Effect.Effect = effectApi.session.instructions.entry.remove({ - sessionID, - key: "review-notes", -}) - -const promiseList: Promise> = - promiseClient.session.instructions.entry.list({ sessionID: "ses_test" }) -const promisePut: Promise = promiseClient.session.instructions.entry.put({ - sessionID: "ses_test", - key: "review-notes", - value: { text: "Check the diff" }, -}) -const promiseRemove: Promise = promiseClient.session.instructions.entry.remove({ - sessionID: "ses_test", - key: "review-notes", -}) - -void [effectList, effectPut, effectRemove, promiseList, promisePut, promiseRemove] diff --git a/packages/client/test/contract-identity.test.ts b/packages/client/test/contract-identity.test.ts index 8de2115fa7..64a2e958ce 100644 --- a/packages/client/test/contract-identity.test.ts +++ b/packages/client/test/contract-identity.test.ts @@ -1,17 +1,47 @@ import { expect, test } from "bun:test" import { Schema } from "effect" +import { AgentV2 } from "@opencode-ai/core/agent" +import { Location as CoreLocation } from "@opencode-ai/core/location" +import { ModelV2 } from "@opencode-ai/core/model" +import { SessionV2 } from "@opencode-ai/core/session" +import { SessionInput as CoreSessionInput } from "@opencode-ai/core/session/input" +import { SessionMessage as CoreSessionMessage } from "@opencode-ai/core/session/message" +import { Prompt as CorePrompt } from "@opencode-ai/core/session/prompt" import { Agent } from "@opencode-ai/schema/agent" +import { Location } from "@opencode-ai/schema/location" import { Model } from "@opencode-ai/schema/model" +import { Project } from "@opencode-ai/schema/project" +import { Provider } from "@opencode-ai/schema/provider" import { Prompt } from "@opencode-ai/schema/prompt" import { Session } from "@opencode-ai/schema/session" +import { SessionInput } from "@opencode-ai/schema/session-input" import { SessionMessage } from "@opencode-ai/schema/session-message" +import { Workspace } from "@opencode-ai/schema/workspace" +import { Api } from "@opencode-ai/server/api" +import { compile, emitPromise } from "@opencode-ai/httpapi-codegen" +import { ClientApi, endpointNames, groupNames, omitEndpoints } from "../src/contract" -const Client = await import("../src/effect") +test("Core and Server reuse the authoritative Schema and Protocol values", () => { + expect(AgentV2.ID).toBe(Agent.ID) + expect(CoreLocation.Ref).toBe(Location.Ref) + expect(ModelV2.Ref).toBe(Model.Ref) + expect(SessionV2.Info).toBe(Session.Info) + expect(CoreSessionInput.Admitted).toBe(SessionInput.Admitted) + expect(CoreSessionMessage.Message).toBe(SessionMessage.Message) + expect(CorePrompt).toBe(Prompt) + expect(Api.groups["server.session"].identifier).toBe("server.session") + expect(Object.keys(ClientApi.groups)).toEqual(Object.keys(Api.groups)) + expect(Session.ID.create()).toStartWith("ses_") + expect(Project.ID.global).toBe("global") + expect(Provider.ID.anthropic).toBe("anthropic") + expect(Workspace.ID.create()).toStartWith("wrk_") +}) -test("effect entrypoint exposes canonical Schema contracts", () => { - expect(Client.Agent).toBe(Agent) - expect(Client.Model).toBe(Model) - expect(Client.Session).toBe(Session) +test("client and Server contracts generate identically", () => { + const server = compile(Api, { groupNames, endpointNames, omitEndpoints }) + const client = compile(ClientApi, { groupNames, endpointNames, omitEndpoints }) + + expect(emitPromise(client)).toEqual(emitPromise(server)) }) test("shared DTO schemas construct and decode plain objects", () => { @@ -24,4 +54,5 @@ test("shared DTO schemas construct and decode plain objects", () => { expect(Object.getPrototypeOf(content)).toBe(Object.prototype) expect(Prompt.ast.annotations?.identifier).toBe("Prompt") expect(SessionMessage.AssistantText.ast.annotations?.identifier).toBe("Session.Message.Assistant.Text") + expect(CoreSessionMessage.AssistantText).toBe(SessionMessage.AssistantText) }) diff --git a/packages/client/test/effect.test.ts b/packages/client/test/effect.test.ts index c1254ab7d3..7bf4d26f8f 100644 --- a/packages/client/test/effect.test.ts +++ b/packages/client/test/effect.test.ts @@ -1,90 +1,27 @@ import { expect, test } from "bun:test" import { DateTime, Effect, Stream } from "effect" import { HttpClient, HttpClientResponse } from "effect/unstable/http" -import { - AbsolutePath, - Agent, - Event, - Location, - Model, - OpenCode, - Prompt, - Session, - SessionMessage, -} from "../src/effect/index" +import { AbsolutePath, Agent, Location, Model, OpenCode, Prompt, Session, SessionMessage } from "../src/effect" -const synced = { type: "log.synced" as const, aggregateID: "ses_test", seq: Event.Seq.make(1) } - -test("session.get returns the decoded Effect projection", async () => { +test("sessions.get returns the decoded Effect projection", async () => { const httpClient = HttpClient.make((request) => Effect.succeed(HttpClientResponse.fromWeb(request, Response.json(session))), ) const result = await Effect.gen(function* () { const client = yield* OpenCode.make({ baseUrl: "http://localhost:3000" }) - return yield* client.session.get({ sessionID: Session.ID.make("ses_test") }) + return yield* client.sessions.get({ sessionID: Session.ID.make("ses_test") }) }).pipe(Effect.provideService(HttpClient.HttpClient, httpClient), Effect.runPromise) expect(DateTime.toEpochMillis(result.time.created)).toBe(1_717_171_717_000) }) -test("session instructions methods use the public HTTP contract", async () => { - const requests: Array<{ method: string; url: string; body?: unknown }> = [] - const instructions = [{ key: "review-notes", value: { text: "Check the diff", priority: 1 } }] - const httpClient = HttpClient.make((request) => { - requests.push({ - method: request.method, - url: request.url, - body: request.body._tag === "Uint8Array" ? JSON.parse(new TextDecoder().decode(request.body.body)) : undefined, - }) - return Effect.succeed( - HttpClientResponse.fromWeb( - request, - request.method === "GET" ? Response.json({ data: instructions }) : new Response(null, { status: 204 }), - ), - ) - }) - const result = await Effect.gen(function* () { - const client = yield* OpenCode.make({ baseUrl: "http://localhost:3000" }) - const listed = yield* client.session.instructions.entry.list({ sessionID: Session.ID.make("ses_test") }) - yield* client.session.instructions.entry.put({ - sessionID: Session.ID.make("ses_test"), - key: "review-notes", - value: instructions[0].value, - }) - yield* client.session.instructions.entry.remove({ - sessionID: Session.ID.make("ses_test"), - key: "review-notes", - }) - return listed - }).pipe(Effect.provideService(HttpClient.HttpClient, httpClient), Effect.runPromise) - - expect(result).toEqual(instructions) - expect(requests).toEqual([ - { - method: "GET", - url: "http://localhost:3000/api/session/ses_test/instructions/entries", - body: undefined, - }, - { - method: "PUT", - url: "http://localhost:3000/api/session/ses_test/instructions/entries/review-notes", - body: { value: { text: "Check the diff", priority: 1 } }, - }, - { - method: "DELETE", - url: "http://localhost:3000/api/session/ses_test/instructions/entries/review-notes", - body: undefined, - }, - ]) -}) - -test("event.subscribe exposes and decodes the native Effect event stream", async () => { +test("events.subscribe exposes and decodes the native Effect event stream", async () => { const httpClient = HttpClient.make((request) => Effect.succeed( HttpClientResponse.fromWeb( request, new Response( - `data: ${JSON.stringify({ id: "evt_connected", created: 0, type: "server.connected", data: {} })}\n\n` + + `data: ${JSON.stringify({ id: "evt_connected", type: "server.connected", data: {} })}\n\n` + `data: ${JSON.stringify(modelSwitchedEvent)}\n\n`, { headers: { "content-type": "text/event-stream" } }, ), @@ -93,17 +30,17 @@ test("event.subscribe exposes and decodes the native Effect event stream", async ) const events = await Effect.gen(function* () { const client = yield* OpenCode.make({ baseUrl: "http://localhost:3000" }) - return yield* client.event.subscribe().pipe(Stream.runCollect) + return yield* client.events.subscribe().pipe(Stream.runCollect) }).pipe(Effect.provideService(HttpClient.HttpClient, httpClient), Effect.runPromise) - expect(Array.from(events).map((event) => event.type)).toEqual(["server.connected", "session.model.selected"]) + expect(Array.from(events).map((event) => event.type)).toEqual(["server.connected", "session.next.model.switched"]) const durable = events[1] - if (durable?.type !== "session.model.selected") throw new Error("Expected model event") - expect(DateTime.toEpochMillis(durable.created)).toBe(1_717_171_717_000) + if (durable?.type !== "session.next.model.switched") throw new Error("Expected model event") + expect(DateTime.toEpochMillis(durable.data.timestamp)).toBe(1_717_171_717_000) expect(durable.durable).toEqual({ aggregateID: "ses_test", seq: 1, version: 1 }) }) -test("event.subscribe terminates on Effect protocol decode failures", async () => { +test("events.subscribe terminates on Effect protocol decode failures", async () => { const httpClient = HttpClient.make((request) => Effect.succeed( HttpClientResponse.fromWeb( @@ -116,33 +53,42 @@ test("event.subscribe terminates on Effect protocol decode failures", async () = ) const error = await Effect.gen(function* () { const client = yield* OpenCode.make({ baseUrl: "http://localhost:3000" }) - return yield* client.event.subscribe().pipe(Stream.runCollect, Effect.flip) + return yield* client.events.subscribe().pipe(Stream.runCollect, Effect.flip) }).pipe(Effect.provideService(HttpClient.HttpClient, httpClient), Effect.runPromise) expect(error._tag).toBe("ClientError") }) test("session methods retain decoded Effect inputs and outputs", async () => { - const logQueries: Array> = [] + const historyQueries: Array> = [] + let historyPage = 0 const httpClient = HttpClient.make((request) => { const url = request.url - if (url.includes("/log")) { - logQueries.push(Object.fromEntries(request.urlParams.params)) + if (url.includes("/event")) { return Effect.succeed( HttpClientResponse.fromWeb( request, - new Response(`data: ${JSON.stringify(modelSwitchedEvent)}\n\ndata: ${JSON.stringify(synced)}\n\n`, { + new Response(`data: ${JSON.stringify(modelSwitchedEvent)}\n\n`, { headers: { "content-type": "text/event-stream" }, }), ), ) } + if (url.includes("/history")) { + historyPage++ + historyQueries.push(Object.fromEntries(request.urlParams.params)) + return Effect.succeed( + HttpClientResponse.fromWeb( + request, + Response.json( + historyPage === 1 ? { data: [modelSwitchedEvent], hasMore: true } : { data: [], hasMore: false }, + ), + ), + ) + } if (url.includes("/prompt")) { return Effect.succeed(HttpClientResponse.fromWeb(request, Response.json(admission))) } - if (url.endsWith("/compact")) { - return Effect.succeed(HttpClientResponse.fromWeb(request, Response.json(compactionAdmission))) - } if (url.includes("/context")) { return Effect.succeed(HttpClientResponse.fromWeb(request, Response.json({ data: [] }))) } @@ -166,33 +112,45 @@ test("session methods retain decoded Effect inputs and outputs", async () => { }) const result = await Effect.gen(function* () { const client = yield* OpenCode.make({ baseUrl: "http://localhost:3000" }) - const page = yield* client.session.list({ limit: 10 }) - const active = yield* client.session.active() - const created = yield* client.session.create({ + const page = yield* client.sessions.list({ limit: 10 }) + const active = yield* client.sessions.active() + const created = yield* client.sessions.create({ location: Location.Ref.make({ directory: AbsolutePath.make("/tmp/project") }), }) - yield* client.session.switchAgent({ sessionID: Session.ID.make("ses_test"), agent: Agent.ID.make("build") }) - yield* client.session.switchModel({ + yield* client.sessions.switchAgent({ sessionID: Session.ID.make("ses_test"), agent: Agent.ID.make("build") }) + yield* client.sessions.switchModel({ sessionID: Session.ID.make("ses_test"), model: Model.Ref.make({ id: "claude", providerID: "anthropic" }), }) - const admitted = yield* client.session.prompt({ + const admitted = yield* client.sessions.prompt({ sessionID: Session.ID.make("ses_test"), - text: "Hello", + prompt: Prompt.make({ text: "Hello" }), resume: false, }) - yield* client.session.compact({ sessionID: Session.ID.make("ses_test") }) - yield* client.session.wait({ sessionID: Session.ID.make("ses_test") }) - const context = yield* client.session.context({ sessionID: Session.ID.make("ses_test") }) - const log = yield* client.session - .log({ sessionID: Session.ID.make("ses_test"), after: Event.Seq.make(0) }) + yield* client.sessions.compact({ sessionID: Session.ID.make("ses_test") }) + yield* client.sessions.wait({ sessionID: Session.ID.make("ses_test") }) + const context = yield* client.sessions.context({ sessionID: Session.ID.make("ses_test") }) + const history = yield* client.sessions.history({ + sessionID: Session.ID.make("ses_test"), + after: 0, + limit: 1, + }) + const historyNext = history.hasMore + ? yield* client.sessions.history({ + sessionID: Session.ID.make("ses_test"), + after: history.data.at(-1)?.durable?.seq, + limit: 2, + }) + : undefined + const events = yield* client.sessions + .events({ sessionID: Session.ID.make("ses_test"), after: 0 }) .pipe(Stream.runCollect) - yield* client.session.interrupt({ sessionID: Session.ID.make("ses_test") }) - const message = yield* client.session.message({ + yield* client.sessions.interrupt({ sessionID: Session.ID.make("ses_test") }) + const message = yield* client.sessions.message({ sessionID: Session.ID.make("ses_test"), messageID: SessionMessage.ID.make("msg_model"), }) - return { page, active, created, admitted, context, log, message } + return { page, active, created, admitted, context, history, historyNext, events, message } }).pipe(Effect.provideService(HttpClient.HttpClient, httpClient), Effect.runPromise) expect(DateTime.toEpochMillis(result.page.data[0].time.created)).toBe(1_717_171_717_000) @@ -201,20 +159,19 @@ test("session methods retain decoded Effect inputs and outputs", async () => { expect(Object.getPrototypeOf(result.created)).toBe(Object.prototype) expect(result.created.id).toBe("ses_test") expect(Object.getPrototypeOf(result.admitted)).toBe(Object.prototype) - expect(Object.getPrototypeOf(result.admitted.data)).toBe(Object.prototype) + expect(Object.getPrototypeOf(result.admitted.prompt)).toBe(Object.prototype) expect(DateTime.toEpochMillis(result.admitted.timeCreated)).toBe(1_717_171_717_000) expect(result.context).toEqual([]) - expect(logQueries[0]).toEqual({ after: "0" }) - const logged = Array.from(result.log) - expect(logged.map((item) => item.type)).toEqual(["session.model.selected", "log.synced"]) - expect(logged[0]?.type === "session.model.selected" && DateTime.toEpochMillis(logged[0].created)).toBe( - 1_717_171_717_000, - ) - expect(logged.at(-1)).toEqual(synced) + expect(DateTime.toEpochMillis(result.history.data[0].data.timestamp)).toBe(1_717_171_717_000) + expect(result.history).toEqual(expect.objectContaining({ hasMore: true })) + expect(result.historyNext).toEqual({ data: [], hasMore: false }) + expect(historyQueries[0]).toEqual({ limit: "1", after: "0" }) + expect(historyQueries[1]).toEqual({ limit: "2", after: "1" }) + expect(DateTime.toEpochMillis(result.events[0].data.timestamp)).toBe(1_717_171_717_000) expect(result.message).toEqual(expect.objectContaining({ id: "msg_model", type: "model-switched" })) }) -test("session.log retains the typed SessionNotFoundError", async () => { +test("sessions.history retains the typed SessionNotFoundError", async () => { const httpClient = HttpClient.make((request) => Effect.succeed( HttpClientResponse.fromWeb( @@ -228,7 +185,11 @@ test("session.log retains the typed SessionNotFoundError", async () => { ) const error = await Effect.gen(function* () { const client = yield* OpenCode.make({ baseUrl: "http://localhost:3000" }) - return yield* client.session.log({ sessionID: Session.ID.make("ses_missing") }).pipe(Stream.runCollect, Effect.flip) + return yield* client.sessions + .history({ + sessionID: Session.ID.make("ses_missing"), + }) + .pipe(Effect.flip) }).pipe(Effect.provideService(HttpClient.HttpClient, httpClient), Effect.runPromise) expect(error._tag).toBe("SessionNotFoundError") @@ -259,23 +220,12 @@ const admission = { admittedSeq: 0, id: "msg_test", sessionID: "ses_test", - type: "user", - data: { text: "Hello" }, + prompt: { text: "Hello" }, delivery: "steer", timeCreated: 1_717_171_717_000, }, } -const compactionAdmission = { - data: { - type: "compaction", - admittedSeq: 1, - id: "msg_compaction", - sessionID: "ses_test", - timeCreated: 1_717_171_717_000, - }, -} - const modelSwitchedMessage = { id: "msg_model", type: "model-switched", @@ -285,11 +235,12 @@ const modelSwitchedMessage = { const modelSwitchedEvent = { id: "evt_model", - created: 1_717_171_717_000, - type: "session.model.selected", + type: "session.next.model.switched", durable: { aggregateID: "ses_test", seq: 1, version: 1 }, data: { + timestamp: 1_717_171_717_000, sessionID: "ses_test", + messageID: "msg_model", model: { id: "claude", providerID: "anthropic" }, }, } diff --git a/packages/client/test/import-boundaries.test.ts b/packages/client/test/import-boundaries.test.ts index 5b881edc32..4875a3a5dc 100644 --- a/packages/client/test/import-boundaries.test.ts +++ b/packages/client/test/import-boundaries.test.ts @@ -20,9 +20,7 @@ describe("public import boundaries", () => { expect(within(root, core)).toEqual([]) expect(within(root, server)).toEqual([]) - // The effect entry includes local service lifecycle (node spawn/fs), so it - // bundles for bun; the boundary assertions below are what matter. - const network = await bundleInputs("@opencode-ai/client/effect", "bun") + const network = await bundleInputs("@opencode-ai/client/effect", "browser") expect(within(network, effect).length).toBeGreaterThan(0) expect(within(network, schema).length).toBeGreaterThan(0) diff --git a/packages/client/test/promise.test.ts b/packages/client/test/promise.test.ts index ba822f28e5..322a39cd6b 100644 --- a/packages/client/test/promise.test.ts +++ b/packages/client/test/promise.test.ts @@ -1,172 +1,44 @@ import { expect, test } from "bun:test" -import { isSessionNotFoundError, isUnauthorizedError, OpenCode } from "../src/promise/index" +import { isSessionNotFoundError, isUnauthorizedError, OpenCode } from "../src" test("exposes every standard HTTP API group", () => { const client = OpenCode.make({ baseUrl: "http://localhost:3000" }) expect(Object.keys(client)).toEqual([ "health", - "server", "location", - "agent", - "plugin", - "session", - "message", - "model", - "generate", - "provider", - "integration", - "server.mcp", - "credential", - "project", - "form", - "permission", - "file", - "command", - "skill", - "event", - "pty", - "shell", - "question", - "reference", - "projectCopy", - "vcs", - "debug", + "agents", + "sessions", + "messages", + "models", + "providers", + "integrations", + "credentials", + "permissions", + "files", + "commands", + "skills", + "events", + "ptys", + "questions", + "references", + "projectCopies", ]) - expect(Object.keys(client.debug)).toEqual(["location"]) - expect(Object.keys(client.debug.location)).toEqual(["list", "evict"]) - expect(Object.keys(client.message)).toEqual(["list"]) - expect(Object.keys(client.integration)).toEqual(["list", "get", "connect", "attempt"]) - expect(Object.keys(client.integration.connect)).toEqual(["key", "oauth"]) - expect(Object.keys(client.integration.attempt)).toEqual(["status", "complete", "cancel"]) - expect(Object.keys(client.file)).toEqual(["read", "list", "find"]) - expect(Object.keys(client.vcs)).toEqual(["status", "diff"]) - expect(Object.keys(client.pty)).toEqual(["list", "create", "get", "update", "remove"]) - expect(Object.keys(client.shell)).toEqual(["list", "create", "get", "timeout", "output", "remove"]) - expect(Object.keys(client.project)).toEqual(["list", "current", "directories"]) -}) - -test("server.get uses the public HTTP contract", async () => { - let request: Request | undefined - const client = OpenCode.make({ - baseUrl: "http://localhost:3000", - fetch: async (input) => { - request = input instanceof Request ? input : new Request(input) - return Response.json({ urls: ["http://192.168.1.10:4096"] }) - }, - }) - - expect(await client.server.get()).toEqual({ urls: ["http://192.168.1.10:4096"] }) - expect(request?.method).toBe("GET") - expect(request?.url).toBe("http://localhost:3000/api/server") -}) - -test("MCP resource catalog uses the public HTTP contract", async () => { - let request: Request | undefined - const client = OpenCode.make({ - baseUrl: "http://localhost:3000", - fetch: async (input) => { - request = input instanceof Request ? input : new Request(input) - return Response.json({ - location: { directory: "/tmp/project", project: { id: "proj_test", directory: "/tmp/project" } }, - data: { - resources: [{ server: "docs", name: "Readme", uri: "docs://readme" }], - templates: [{ server: "docs", name: "File", uriTemplate: "docs://{path}" }], - }, - }) - }, - }) - - const result = await client["server.mcp"].resource.catalog({ location: { directory: "/tmp/project" } }) - - expect(result.data.resources[0]?.uri).toBe("docs://readme") - expect(request?.method).toBe("GET") - expect(request?.url).toBe("http://localhost:3000/api/mcp/resource?location%5Bdirectory%5D=%2Ftmp%2Fproject") -}) - -test("file.read returns binary content from the public HTTP contract", async () => { - let request: Request | undefined - const client = OpenCode.make({ - baseUrl: "http://localhost:3000", - fetch: async (input) => { - request = input instanceof Request ? input : new Request(input) - return new Response(new Uint8Array([104, 105])) - }, - }) - - const content = await client.file.read({ - path: "src/a b#c.ts", - location: { directory: "/tmp/project" }, - }) - - expect(Array.from(content)).toEqual([104, 105]) - expect(request?.url).toBe( - "http://localhost:3000/api/fs/read/src/a%20b%23c.ts?location%5Bdirectory%5D=%2Ftmp%2Fproject", - ) -}) - -test("project methods use the public HTTP contract", async () => { - const requests: string[] = [] - const client = OpenCode.make({ - baseUrl: "http://localhost:3000", - fetch: async (input) => { - const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url - requests.push(url) - if (url.includes("/directories")) return Response.json([]) - return Response.json({ id: "proj_test", directory: "/tmp/project" }) - }, - }) - - const current = await client.project.current({ location: { workspace: "wrk_test" } }) - const directories = await client.project.directories({ - projectID: current.id, - location: { directory: current.directory }, - }) - - expect(current).toEqual({ id: "proj_test", directory: "/tmp/project" }) - expect(directories).toEqual([]) - expect(requests).toEqual([ - "http://localhost:3000/api/project/current?location%5Bworkspace%5D=wrk_test", - "http://localhost:3000/api/project/proj_test/directories?location%5Bdirectory%5D=%2Ftmp%2Fproject", + expect(Object.keys(client.messages)).toEqual(["list"]) + expect(Object.keys(client.integrations)).toEqual([ + "list", + "get", + "connectKey", + "connectOauth", + "attemptStatus", + "attemptComplete", + "attemptCancel", ]) + expect(Object.keys(client.files)).toEqual(["list", "find"]) + expect(Object.keys(client.ptys)).toEqual(["list", "create", "get", "update", "remove"]) }) -test("shell list and remove use the public HTTP contract", async () => { - const requests: Array<{ method: string; url: string }> = [] - const shell = { - id: "sh_test", - status: "running", - command: "pwd", - cwd: "/tmp/project", - shell: "/bin/zsh", - file: "/tmp/opencode-shell", - metadata: { sessionID: "ses_test" }, - time: { started: 1_717_171_717_000 }, - } - const client = OpenCode.make({ - baseUrl: "http://localhost:3000", - fetch: async (input, init) => { - const request = input instanceof Request ? input : new Request(input, init) - requests.push({ method: request.method, url: request.url }) - if (request.method === "DELETE") return new Response(null, { status: 204 }) - return Response.json({ - location: { directory: "/tmp/project", project: { id: "proj_test", directory: "/tmp/project" } }, - data: [shell], - }) - }, - }) - - const result = await client.shell.list({ location: { directory: "/tmp/project" } }) - await client.shell.remove({ id: shell.id }) - - expect(result.data).toEqual([shell]) - expect(requests).toEqual([ - { method: "GET", url: "http://localhost:3000/api/shell?location%5Bdirectory%5D=%2Ftmp%2Fproject" }, - { method: "DELETE", url: "http://localhost:3000/api/shell/sh_test" }, - ]) -}) - -test("session.get returns the wire projection", async () => { +test("sessions.get returns the wire projection", async () => { const client = OpenCode.make({ baseUrl: "http://localhost:3000", fetch: async (input) => { @@ -177,152 +49,43 @@ test("session.get returns the wire projection", async () => { }, }) - const result = await client.session.get({ sessionID: "ses_test" }) + const result = await client.sessions.get({ sessionID: "ses_test" }) expect(result.time.created).toBe(1_717_171_717_000) }) -test("session instructions methods use the public HTTP contract", async () => { - const requests: Array<{ method: string; url: string; body?: unknown }> = [] - const instructions = [{ key: "review-notes", value: { text: "Check the diff", priority: 1 } }] - const client = OpenCode.make({ - baseUrl: "http://localhost:3000", - fetch: async (input, init) => { - const request = input instanceof Request ? input : new Request(input, init) - requests.push({ - method: request.method, - url: request.url, - body: request.method === "PUT" ? await request.json() : undefined, - }) - if (request.method === "GET") return Response.json({ data: instructions }) - return new Response(null, { status: 204 }) - }, - }) - - const result = await client.session.instructions.entry.list({ sessionID: "ses_test" }) - await client.session.instructions.entry.put({ - sessionID: "ses_test", - key: "review-notes", - value: instructions[0].value, - }) - await client.session.instructions.entry.remove({ sessionID: "ses_test", key: "review-notes" }) - - expect(result).toEqual(instructions) - expect(requests).toEqual([ - { - method: "GET", - url: "http://localhost:3000/api/session/ses_test/instructions/entries", - body: undefined, - }, - { - method: "PUT", - url: "http://localhost:3000/api/session/ses_test/instructions/entries/review-notes", - body: { value: { text: "Check the diff", priority: 1 } }, - }, - { - method: "DELETE", - url: "http://localhost:3000/api/session/ses_test/instructions/entries/review-notes", - body: undefined, - }, - ]) -}) - -test("session.pending.list uses the public HTTP contract", async () => { - const requests: Array<{ method: string; url: string }> = [] - const pending = [ - { - admittedSeq: 3, - id: "msg_pending", - sessionID: "ses_test", - timeCreated: 1_717_171_717_000, - type: "user", - data: { text: "Fix the failing tests" }, - delivery: "steer", - }, - ] - const client = OpenCode.make({ - baseUrl: "http://localhost:3000", - fetch: async (input, init) => { - const request = input instanceof Request ? input : new Request(input, init) - requests.push({ method: request.method, url: request.url }) - return Response.json({ data: pending }) - }, - }) - - const result = await client.session.pending.list({ sessionID: "ses_test" }) - - expect(result).toEqual(pending) - expect(requests).toEqual([{ method: "GET", url: "http://localhost:3000/api/session/ses_test/pending" }]) -}) - -test("event.subscribe exposes the Promise event stream wire projection", async () => { +test("events.subscribe exposes the Promise event stream wire projection", async () => { const client = OpenCode.make({ baseUrl: "http://localhost:3000", fetch: async () => new Response( - `: heartbeat\n\ndata: ${JSON.stringify({ id: "evt_connected", created: 0, type: "server.connected", data: {} })}\n\n` + + `: heartbeat\n\ndata: ${JSON.stringify({ id: "evt_connected", type: "server.connected", data: {} })}\n\n` + `data: ${JSON.stringify(modelSwitchedEvent)}\n\n`, { headers: { "content-type": "text/event-stream" } }, ), }) const events = [] - for await (const event of client.event.subscribe()) events.push(event) + for await (const event of client.events.subscribe()) events.push(event) - expect(events).toEqual([{ id: "evt_connected", created: 0, type: "server.connected", data: {} }, modelSwitchedEvent]) - expect(events[1]?.type === "session.model.selected" && events[1].created).toBe(1_717_171_717_000) + expect(events).toEqual([{ id: "evt_connected", type: "server.connected", data: {} }, modelSwitchedEvent]) + expect(events[1]?.type === "session.next.model.switched" && events[1].data.timestamp).toBe(1_717_171_717_000) }) -test("event.subscribe terminates on malformed Promise SSE data", async () => { +test("events.subscribe terminates on malformed Promise SSE data", async () => { const client = OpenCode.make({ baseUrl: "http://localhost:3000", fetch: async () => new Response("data: {not-json}\n\n", { headers: { "content-type": "text/event-stream" } }), }) - await expect(client.event.subscribe()[Symbol.asyncIterator]().next()).rejects.toMatchObject({ + await expect(client.events.subscribe()[Symbol.asyncIterator]().next()).rejects.toMatchObject({ name: "ClientError", reason: "MalformedResponse", }) }) -test("event.subscribe accepts a fragmented SSE event below the size limit", async () => { - const event = { id: "evt_large", type: "test.large", data: { output: "x".repeat(12 * 1024 * 1024) } } - const encoded = new TextEncoder().encode(`data: ${JSON.stringify(event)}\n\n`) - const client = OpenCode.make({ - baseUrl: "http://localhost:3000", - fetch: async () => - new Response( - new ReadableStream({ - start(controller) { - for (let offset = 0; offset < encoded.length; offset += 64 * 1024) { - controller.enqueue(encoded.slice(offset, offset + 64 * 1024)) - } - controller.close() - }, - }), - { headers: { "content-type": "text/event-stream" } }, - ), - }) - - await expect(client.event.subscribe()[Symbol.asyncIterator]().next()).resolves.toEqual({ done: false, value: event }) -}) - -test("event.subscribe rejects an SSE event above the size limit", async () => { - const client = OpenCode.make({ - baseUrl: "http://localhost:3000", - fetch: async () => - new Response(`data: ${JSON.stringify({ output: "x".repeat(16 * 1024 * 1024) })}`, { - headers: { "content-type": "text/event-stream" }, - }), - }) - - await expect(client.event.subscribe()[Symbol.asyncIterator]().next()).rejects.toMatchObject({ - name: "ClientError", - reason: "SseEventTooLarge", - }) -}) - test("session methods use the public HTTP contract", async () => { const requests: Array<{ url: string; init?: RequestInit }> = [] + let historyPage = 0 const client = OpenCode.make({ baseUrl: "http://localhost:3000", fetch: async (input, init) => { @@ -333,14 +96,13 @@ test("session methods use the public HTTP contract", async () => { headers: { "content-type": "text/event-stream" }, }) } - if (url.includes("/log")) { - return new Response(`data: ${JSON.stringify(modelSwitchedEvent)}\n\ndata: ${JSON.stringify(synced)}\n\n`, { - headers: { "content-type": "text/event-stream" }, - }) + if (url.includes("/history")) { + historyPage++ + return Response.json( + historyPage === 1 ? { data: [modelSwitchedEvent], hasMore: true } : { data: [], hasMore: false }, + ) } if (url.includes("/prompt")) return Response.json(admission) - if (url.includes("/synthetic")) return Response.json(syntheticAdmission) - if (url.endsWith("/compact")) return Response.json(compactionAdmission) if (url.includes("/context")) return Response.json({ data: [] }) if (url.includes("/message/")) return Response.json({ data: modelSwitchedMessage }) if (url.endsWith("/api/session/active")) return Response.json({ data: { ses_test: { type: "running" } } }) @@ -350,67 +112,61 @@ test("session methods use the public HTTP contract", async () => { }, }) - const page = await client.session.list({ limit: 10, order: "desc", parentID: null }) - const active = await client.session.active() - const created = await client.session.create({ location: { directory: "/tmp/project" } }) - await client.session.switchAgent({ sessionID: "ses_test", agent: "build" }) - await client.session.switchModel({ + const page = await client.sessions.list({ limit: 10, order: "desc" }) + const active = await client.sessions.active() + const created = await client.sessions.create({ location: { directory: "/tmp/project" } }) + await client.sessions.switchAgent({ sessionID: "ses_test", agent: "build" }) + await client.sessions.switchModel({ sessionID: "ses_test", model: { id: "claude", providerID: "anthropic" }, }) - const admitted = await client.session.prompt({ + const admitted = await client.sessions.prompt({ sessionID: "ses_test", - text: "Hello", + prompt: { text: "Hello" }, resume: false, }) - const synthetic = await client.session.synthetic({ - sessionID: "ses_test", - text: "Completed", - delivery: "queue", - resume: false, - }) - await client.session.compact({ sessionID: "ses_test" }) - await client.session.wait({ sessionID: "ses_test" }) - const context = await client.session.context({ sessionID: "ses_test" }) - const log = [] - for await (const item of client.session.log({ sessionID: "ses_test", after: 0 })) log.push(item) - await client.session.interrupt({ sessionID: "ses_test" }) - const message = await client.session.message({ sessionID: "ses_test", messageID: "msg_model" }) + await client.sessions.compact({ sessionID: "ses_test" }) + await client.sessions.wait({ sessionID: "ses_test" }) + const context = await client.sessions.context({ sessionID: "ses_test" }) + const history = await client.sessions.history({ sessionID: "ses_test", after: 0, limit: 1 }) + const historyAfter = history.data.at(-1)?.durable?.seq + const historyNext = history.hasMore + ? await client.sessions.history({ sessionID: "ses_test", after: historyAfter, limit: 2 }) + : undefined + const events = [] + for await (const event of client.sessions.events({ sessionID: "ses_test", after: 0 })) events.push(event) + await client.sessions.interrupt({ sessionID: "ses_test" }) + const message = await client.sessions.message({ sessionID: "ses_test", messageID: "msg_model" }) expect(page.cursor.next).toBe("next") expect(active).toEqual({ ses_test: { type: "running" } }) expect(created.id).toBe("ses_test") expect(admitted.id).toBe("msg_test") - expect(synthetic).toMatchObject({ type: "synthetic", data: { text: "Completed" }, delivery: "queue" }) expect(context).toEqual([]) - expect(log).toEqual([modelSwitchedEvent, synced]) + expect(history).toEqual({ data: [modelSwitchedEvent], hasMore: true }) + expect(historyNext).toEqual({ data: [], hasMore: false }) + expect(events).toEqual([modelSwitchedEvent]) expect(message).toEqual(modelSwitchedMessage) expect(requests.map((request) => [request.init?.method, request.url])).toEqual([ - ["GET", "http://localhost:3000/api/session?limit=10&order=desc&parentID=null"], + ["GET", "http://localhost:3000/api/session?limit=10&order=desc"], ["GET", "http://localhost:3000/api/session/active"], ["POST", "http://localhost:3000/api/session"], ["POST", "http://localhost:3000/api/session/ses_test/agent"], ["POST", "http://localhost:3000/api/session/ses_test/model"], ["POST", "http://localhost:3000/api/session/ses_test/prompt"], - ["POST", "http://localhost:3000/api/session/ses_test/synthetic"], ["POST", "http://localhost:3000/api/session/ses_test/compact"], ["POST", "http://localhost:3000/api/session/ses_test/wait"], ["GET", "http://localhost:3000/api/session/ses_test/context"], - ["GET", "http://localhost:3000/api/experimental/session/ses_test/log?after=0"], + ["GET", "http://localhost:3000/api/session/ses_test/history?limit=1&after=0"], + ["GET", "http://localhost:3000/api/session/ses_test/history?limit=2&after=1"], + ["GET", "http://localhost:3000/api/session/ses_test/event?after=0"], ["POST", "http://localhost:3000/api/session/ses_test/interrupt"], ["GET", "http://localhost:3000/api/session/ses_test/message/msg_model"], ]) const body = requests.find((request) => request.url.endsWith("/api/session/ses_test/prompt"))?.init?.body if (typeof body !== "string") throw new Error("Expected JSON request body") expect(JSON.parse(body)).toEqual({ - text: "Hello", - resume: false, - }) - const syntheticBody = requests.find((request) => request.url.endsWith("/synthetic"))?.init?.body - if (typeof syntheticBody !== "string") throw new Error("Expected JSON synthetic request body") - expect(JSON.parse(syntheticBody)).toEqual({ - text: "Completed", - delivery: "queue", + prompt: { text: "Hello" }, resume: false, }) }) @@ -423,14 +179,14 @@ test("middleware errors remain declared client errors", async () => { }) try { - await client.session.create({}) + await client.sessions.create({}) throw new Error("Expected request to fail") } catch (error) { expect(isUnauthorizedError(error)).toBe(true) } }) -test("session.log decodes SessionNotFoundError", async () => { +test("sessions.history decodes SessionNotFoundError", async () => { const client = OpenCode.make({ baseUrl: "http://localhost:3000", fetch: async () => @@ -441,7 +197,7 @@ test("session.log decodes SessionNotFoundError", async () => { }) try { - await client.session.log({ sessionID: "ses_missing" })[Symbol.asyncIterator]().next() + await client.sessions.history({ sessionID: "ses_missing" }) throw new Error("Expected request to fail") } catch (error) { expect(isSessionNotFoundError(error)).toBe(true) @@ -473,35 +229,12 @@ const admission = { admittedSeq: 0, id: "msg_test", sessionID: "ses_test", - type: "user", - data: { text: "Hello" }, + prompt: { text: "Hello" }, delivery: "steer", timeCreated: 1_717_171_717_000, }, } -const syntheticAdmission = { - data: { - admittedSeq: 1, - id: "msg_synthetic", - sessionID: "ses_test", - type: "synthetic", - data: { text: "Completed" }, - delivery: "queue", - timeCreated: 1_717_171_717_000, - }, -} - -const compactionAdmission = { - data: { - type: "compaction", - admittedSeq: 1, - id: "msg_compaction", - sessionID: "ses_test", - timeCreated: 1_717_171_717_000, - }, -} - const modelSwitchedMessage = { id: "msg_model", type: "model-switched", @@ -509,15 +242,14 @@ const modelSwitchedMessage = { model: { id: "claude", providerID: "anthropic" }, } -const synced = { type: "log.synced", aggregateID: "ses_test", seq: 1 } - const modelSwitchedEvent = { id: "evt_model", - created: 1_717_171_717_000, - type: "session.model.selected", + type: "session.next.model.switched", durable: { aggregateID: "ses_test", seq: 1, version: 1 }, data: { + timestamp: 1_717_171_717_000, sessionID: "ses_test", + messageID: "msg_model", model: { id: "claude", providerID: "anthropic" }, }, } diff --git a/packages/client/tsconfig.build.json b/packages/client/tsconfig.build.json deleted file mode 100644 index e235ae78cf..0000000000 --- a/packages/client/tsconfig.build.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "$schema": "https://json.schemastore.org/tsconfig", - "extends": "./tsconfig.json", - "compilerOptions": { - "rootDir": "src", - "outDir": "dist", - "noEmit": false, - "declaration": true - }, - "include": ["src"] -} diff --git a/packages/client/tsconfig.json b/packages/client/tsconfig.json index a7ef7a1fa3..47fc90bc55 100644 --- a/packages/client/tsconfig.json +++ b/packages/client/tsconfig.json @@ -3,8 +3,6 @@ "extends": "@tsconfig/bun/tsconfig.json", "compilerOptions": { "lib": ["ESNext", "DOM", "DOM.Iterable"], - "allowImportingTsExtensions": false, - "allowJs": false, "noUncheckedIndexedAccess": false }, "include": ["src"] diff --git a/packages/codemode/AGENTS.md b/packages/codemode/AGENTS.md index bc72c815ea..df812c6d86 100644 --- a/packages/codemode/AGENTS.md +++ b/packages/codemode/AGENTS.md @@ -4,7 +4,6 @@ - Do not add a speculative generic permission or approval policy. A host omits tools it does not expose and enforces domain authorization inside each provided tool. - Keep Code Mode unaware of host session, channel, and conversation models. The hosting application supplies trusted execution scope around it. - Tool schemas are the model-facing Interface. Keep arguments minimal and natural to the operation; never add unrelated IDs as ambient capability tokens. -- When interpreter behavior or support changes, update `interpreter-support.md` and direct tests in the same PR. Update `codemode.md` when the package design, integration status, or rationale changes. ## OpenAPI diff --git a/packages/codemode/README.md b/packages/codemode/README.md index e8447e09b2..37bde2d869 100644 --- a/packages/codemode/README.md +++ b/packages/codemode/README.md @@ -1,40 +1,37 @@ # @opencode-ai/codemode -This is our take on code mode. Programs are written in a lightweight, JavaScript-like DSL and run in the package's -own interpreter. They never execute as actual JavaScript, so there is no runtime to escape into. The interpreter -itself can reach nothing; every effect a program has goes through a tool you explicitly supplied. The tradeoff is a -bounded language rather than full JavaScript: the [interpreter support checklist](./interpreter-support.md) documents -exactly what is supported. +Effect-native confined code execution over explicit, schema-described tools. -[Cloudflare's post](https://blog.cloudflare.com/code-mode/) introduced the idea. Their implementation executes -generated code in isolate sandboxes. We took a lighter route: a pure interpreter that runs wherever your application -runs, no sandbox required. +CodeMode lets a model write a small JavaScript program that can call only the tools supplied by the host. The program can sequence calls, transform plain data, branch, loop, and run independent calls in parallel without receiving ambient filesystem, process, network, module, or application authority. -## How it differs from JavaScript +The package is currently private to this workspace. Its API is designed around one-shot and reusable execution: -The deliberate differences: +```ts +// One execution +yield * CodeMode.execute({ tools, code }) -- **No ambient authority.** No `fetch`, `process`, filesystem, timers, or host globals - only the allowlisted standard - library and the `tools` tree. -- **No dynamic code.** No `eval`, `Function`, or module loading. -- **Plain-data boundaries.** Tool arguments and program results are JSON-like data. Dates become ISO strings, RegExp, - Map, and Set serialize as `{}`, and promises, functions, and runtime references cannot cross the boundary. -- **Eager, supervised promises.** Tool calls and async functions start immediately when called. Whatever is still - running when the program returns is interrupted - race losers and fire-and-forget calls alike - so a program must - await every call whose completion matters. Rejections that settle un-awaited become `warnings` on the result instead - of crashing the run. -- **REPL-style results.** An omitted `return` yields the final top-level expression; `undefined` normalizes to `null`. +// A reusable runtime +const runtime = CodeMode.make({ tools, limits }) +yield * runtime.execute(code) +``` -Beyond these, the language is a growing subset rather than a divergent one: unsupported syntax returns an -`UnsupportedSyntax` diagnostic with a source location, and current gaps (for example thenable assimilation, classes, -generators, and full sparse-array parity) are tracked as unchecked items in the -[interpreter support checklist](./interpreter-support.md). +## Install + +Within this workspace: + +```json +{ + "dependencies": { + "@opencode-ai/codemode": "workspace:*" + } +} +``` + +Hosts interact with CodeMode through `effect` (tool `run` implementations, `Effect`-typed results), so they should depend on `effect` themselves. ## Quick Start -The package is workspace-private (`"@opencode-ai/codemode": "workspace:*"`). Hosts interact with it through `effect` -and should depend on `effect` themselves. Define tools with Effect Schema, then place them in the object tree exposed -to programs as `tools`: +Define tools with Effect Schema, then place them in the object tree exposed to programs as `tools`: ```ts import { CodeMode, Tool } from "@opencode-ai/codemode" @@ -63,53 +60,69 @@ const result = `) ``` -`result` is always a `CodeMode.Result`. Program, validation, limit, and tool failures are returned as diagnostics -rather than failing the Effect; host interruption remains interruption. +`result` is always a `CodeMode.Result`. Program, validation, limit, and tool failures are returned as diagnostics rather than failing the Effect. Host interruption remains interruption. + +Successful result values are JSON-safe data. A program that returns `undefined`, including by reaching the end without `return`, produces `null`; nested `undefined` values are normalized to `null` as well. ## API ### `Tool.make` -`input` and `output` each accept a validating Effect Schema or a render-only JSON Schema document. Effect Schema input -is decoded before `run` is invoked; an Effect Schema `output` is decoded and copied before the program sees it. JSON -Schemas only shape the model-visible signature. Without `output` the signature advertises `Promise`. -Descriptions and schemas are model-visible contract; keep authorization in `run`. +```ts +const tool = Tool.make({ + description, + input, // Effect Schema (validating) or JSON Schema (render-only) + output, // optional; same choice + run, +}) +``` -### `CodeMode.execute` and `CodeMode.make` +`input` and `output` each accept a validating Effect Schema or a render-only JSON Schema document (the natural shape for adapter-provided tools whose schemas arrive as JSON Schema, e.g. MCP definitions). Effect Schema input is decoded before `run` is invoked, and `run` returns the encoded representation of an Effect Schema `output`, which CodeMode decodes and copies before exposing it to the program. JSON Schemas only shape the model-visible signature; values pass through unvalidated (they still cross the plain-data boundary). -`CodeMode.execute({ ...options, code })` runs once and is equivalent to `CodeMode.make(options).execute(code)`. A -runtime from `make` reuses the tool set and policy: +`output` is optional. Without it the tool's signature advertises `Promise` and the host result is exposed as-is. + +The description and schemas are part of the model-visible tool contract. Keep descriptions concrete and put authorization in `run` or in the service it calls. + +Public tool types are grouped under the same namespace: `Tool.Definition`, `Tool.Options`, `Tool.SchemaType`, and `Tool.JsonSchema`. + +### `CodeMode.execute` + +Use `CodeMode.execute` for a single execution: ```ts -const runtime = CodeMode.make({ tools, limits: { timeoutMs: 30_000 } }) +const result = + yield * + CodeMode.execute({ + tools: { orders: { lookup: lookupOrder } }, + code: `return await tools.orders.lookup({ id: "order_42" })`, + limits: { maxToolCalls: 10 }, + onToolCallStart: (call) => Effect.logDebug("CodeMode tool started", call), + onToolCallEnd: (call) => Effect.logDebug("CodeMode tool settled", call), + }) +``` + +The Effect environment is inferred from the supplied tools. CodeMode does not erase service requirements introduced by tool implementations. + +### `CodeMode.make` + +Use `CodeMode.make` when the tool set and execution policy are reused: + +```ts +const runtime = CodeMode.make({ + tools: { orders: { lookup: lookupOrder } }, + limits: { timeoutMs: 30_000 }, +}) runtime.catalog() // structured tool descriptions runtime.instructions() // model-facing syntax and tool guide runtime.execute(source) // CodeMode.Result ``` -The Effect environment is inferred from the supplied tools; service requirements are not erased. Optional -`onToolCallStart` / `onToolCallEnd` hooks observe admitted calls with decoded input, outcome, and duration; both are -Effect-returning and must not fail. +`CodeMode.Input`, `CodeMode.Result`, `CodeMode.Success`, `CodeMode.Failure`, `CodeMode.Diagnostic`, and `CodeMode.DiagnosticKind` are both Effect schemas and their inferred TypeScript types. Hosts can combine `CodeMode.Input` and `CodeMode.Result` with `runtime.instructions()` and `runtime.execute()` when constructing a framework-specific agent tool. -### OpenAPI tools +All other CodeMode types use the same namespace: `CodeMode.Options`, `CodeMode.ExecuteOptions`, `CodeMode.Runtime`, `CodeMode.ExecutionLimits`, `CodeMode.DiscoveryOptions`, `CodeMode.DataValue`, `CodeMode.ToolDescription`, and the `CodeMode.ToolCall*` observation types. -`OpenAPI.fromSpec` turns an OpenAPI 3.x document into a tool subtree - one tool per operation, namespaced by dotted -`operationId`: - -```ts -const api = OpenAPI.fromSpec({ spec, auth: { resolve } }) -const runtime = CodeMode.make({ tools: { opencode: api.tools } }) -``` - -It is synchronous and returns `{ tools, skipped }`: operations with unsupported encodings, non-JSON bodies, binary -responses, or streaming land in `skipped` instead of producing broken tools. Auth is resolved host-side and never -model-visible; generated tools require `HttpClient.HttpClient` in the environment. See the option docstrings in -`src/openapi/types.ts` for full semantics. - -## Outputs - -Every execution returns a `CodeMode.Result`: +### Results ```ts type Result = Success | Failure @@ -117,7 +130,6 @@ type Result = Success | Failure interface Success { readonly ok: true readonly value: CodeMode.DataValue - readonly warnings?: ReadonlyArray readonly logs?: ReadonlyArray readonly truncated?: boolean readonly toolCalls: ReadonlyArray @@ -132,65 +144,218 @@ interface Failure { } ``` -`value` is JSON-safe data. `warnings` are non-fatal diagnostics alongside a valid value (un-awaited rejections, -timeout cleanup after the return). `logs` holds program console output, `truncated` marks any output-budget cut, and -`toolCalls` lists admitted calls in order - retained on failure for auditing. +`toolCalls` contains the names of calls admitted by the runtime in call order. It is retained on failure so hosts can audit partial execution without exposing inputs or host failures. `truncated` is present when the value or logs were cut to fit `maxOutputBytes` (see Execution Limits). -Failure `error` and success `warnings` share one diagnostic vocabulary: +### Tool-call hooks -| Kind | Meaning | -| ----------------------- | --------------------------------------------------------------------------------------------------------- | -| `ParseError` | Source is empty or cannot be parsed. | -| `UnsupportedSyntax` | Parsed JavaScript is outside the supported subset. | -| `UnknownTool` | A program referenced a tool the host did not provide. | -| `InvalidToolInput` | Tool input failed schema decoding or safe-data copying. | -| `InvalidToolOutput` | Tool output failed schema decoding or safe-data copying. | -| `InvalidDataValue` | Program data violated the plain-data contract (depth, circularity, blocked properties, non-data values). | -| `ToolCallLimitExceeded` | Calls exceeded `maxToolCalls`. | -| `TimeoutExceeded` | Execution exceeded `timeoutMs`; as a warning, background work was interrupted after the program returned. | -| `ToolFailure` | A tool refused or failed. | -| `ExecutionFailure` | The program threw or another execution error occurred. | -| `Truncated` | Warning-only marker: additional warnings were omitted by `maxOutputBytes`. | +`onToolCallStart` receives `{ index, name, input }` after input decoding and before tool execution. The input is decoded host-side data and may include values produced by schema transformations; applications should avoid logging sensitive tool arguments indiscriminately. -Unknown host failures, defects, and invalid outputs are sanitized. `toolError("safe message")` is the explicit channel -for a model-visible refusal; its optional cause never crosses the boundary. +`onToolCallEnd` receives `{ index, name, input, durationMs, outcome, message? }` when an admitted call settles. `outcome` is `"success"` or `"failure"`; `message` is the model-safe failure message and is present only on failure. Interrupted calls (for example when the execution timeout fires) do not produce an end event. Both hooks are Effect-returning and must not fail. + +### OpenAPI tools + +`OpenAPI.fromSpec` turns an OpenAPI 3.x document into a tool subtree - one tool per operation. Dotted `operationId` values form namespaces such as `v2.session.get`. Missing IDs receive a flat method/path fallback such as `getUsersById`; names are sanitized and deduplicated. The host places the subtree under a key in its `tools` tree; that key is the model-visible namespace. + +```ts +import { CodeMode, OpenAPI } from "@opencode-ai/codemode" +import { Effect } from "effect" +import { FetchHttpClient } from "effect/unstable/http" + +const api = OpenAPI.fromSpec({ + spec: await Bun.file("openapi.json").json(), // parsed document (no YAML) + auth: { + resolve: ({ name, scopes, operation }) => + name === "BearerAuth" ? Effect.succeed({ type: "bearer", token }) : Effect.succeed(undefined), + }, +}) + +const runtime = CodeMode.make({ tools: { opencode: api.tools } }) +const result = await Effect.runPromise(runtime.execute(code).pipe(Effect.provide(FetchHttpClient.layer))) +``` + +`fromSpec` is synchronous and returns `{ tools, skipped }`. The initial adapter supports query `form`/`deepObject`, path/header `simple`, JSON request bodies, JSON responses, and text responses; unsupported parameter encodings, non-JSON request bodies, binary responses, and streaming operations land in `skipped` instead of producing broken tools. Operation and path servers take precedence over document servers unless `baseUrl` explicitly overrides all of them. Tool inputs flatten path, query, header, and closed object-body fields into one model-facing object while retaining their HTTP locations internally. Cross-location name collisions receive a location prefix such as `path_id` and `query_id`; composed, nullable, dictionary, conditionally-required, and non-object JSON bodies remain under `body`. Auth is never model-visible. Responses are limited to 50 MiB, and non-2xx responses become safe tool failures carrying the status and a size-capped body summary. Deferred capabilities are tracked in `src/openapi/TODO.md`. + +Supported bearer, basic, header, and query authentication follows OpenAPI `security` semantics and is resolved host-side via `auth.resolve` - credential storage, OAuth flows, and token refresh never enter the compiler. Cookie authentication alternatives are discarded; an operation is skipped when it has no supported alternative. See the option docstrings in `src/openapi/types.ts` for the full semantics. Generated tools require `HttpClient.HttpClient` (from `effect/unstable/http`) in the Effect environment - provide `FetchHttpClient.layer` or a custom/test client layer at execution. The supplied client owns redirect policy; credentialed hosts should reject redirects or strip credentials when the origin changes. ## Discovery -The generated instructions inline a budgeted catalog (default 2,000 estimated tokens, override with -`discovery: { catalogBudget }`): every namespace is always listed with its tool count, signatures are selected -round-robin so every namespace gets representation, and the instructions state whether the list is complete or -partial. Programs also get a global `search(...)` built-in - always available, advertised when the list is partial: -synchronous, deterministic field-weighted substring matching that returns directly callable paths with full -signatures, supports namespace scoping and pagination, and treats an empty query as browsing and an exact path as -lookup. Search counts as an admitted tool call. +The agent-tool instructions use a budgeted catalog. Every tool namespace is always listed with its tool count regardless of budget, and as many complete, JSDoc-annotated tool signatures (each with a one-line description) as fit an estimated-token budget are inlined. Schema field descriptions and tags are part of each signature's measured cost. Selection is round-robin across namespaces for fairness: in each round (namespaces alphabetical), every namespace still holding un-inlined tools attempts to place its next-cheapest signature against the shared budget, and a namespace whose next signature does not fit drops out while the others keep going - so every namespace gets some representation before any namespace gets everything. The instructions state exactly how comprehensive the list is, both overall (`COMPLETE list` vs `PARTIAL - N of M shown`) and per namespace (`(3 tools)`, `(3 tools, 1 shown)`, `(3 tools, none shown)`). + +The catalog-entry budget defaults to 2,000 estimated tokens (characters / 4, the same heuristic OpenCode uses). It applies only to full tool entries shown in the catalog; fixed instructions and namespace summaries are not counted. Override it when constructing a runtime: + +```ts +const runtime = CodeMode.make({ + tools, + discovery: { catalogBudget: 6_000 }, +}) +``` + +The budget must be a non-negative safe integer. + +The runtime search tool is always registered - including when the catalog is fully inlined - so a speculative `tools.$codemode.search` call never fails as an unknown tool. It is only advertised in the instructions when the inlined list is partial: + +```ts +const matches = await tools.$codemode.search({ + query: "order status", + namespace: "orders", // optional: scope to one top-level namespace + limit: 10, + offset: 0, +}) +``` + +`search` performs deterministic, additive field-weighted matching. The query is tokenized (camelCase boundaries split; every non-alphanumeric character is a separator; empties and `*` are dropped), and each term scores every tool: exact path or path-segment match (20), path substring (8), description substring (4), and searchable-text substring (2). Each term also carries naive singular variants (trailing `s`/`es` stripped), and a field check passes when the term or any variant matches - so a plural query term (`issues`) still finds a tool whose text only says `issue`, without changing the weights. The searchable text also includes the input schema's property names and their description strings, so a query naming a parameter finds its tool, and substring matching means partial words match. Scores sum across terms; matches are sorted by score (ties broken alphabetically by path), then sliced from the zero-based `offset` (default 0) to the configured `limit` (default 10). `remaining` counts matches after the current page. `next` is `{ offset }` when another page exists and `null` on the final page; spread it into the original request to preserve its query, namespace, and limit. + +```ts +const request = { query: "order status", namespace: "orders", limit: 10 } +const page = await tools.$codemode.search(request) +const nextPage = page.next ? await tools.$codemode.search({ ...request, ...page.next }) : undefined +``` + +Each result contains the path, description, and the same generated TypeScript signature used by the inline catalog, so no second lookup is needed. Signatures use the JSDoc-annotated multiline form: each described input/output field carries its schema `description` as a `/** ... */` comment, and constraints TypeScript cannot express ride along as tags (`@deprecated`, `@default`, `@format`, `@minItems`, `@maxItems`). + +```ts +tools.github.list_issues(input: { + /** Repository owner */ + owner: string, + /** Cursor from the previous response's pageInfo */ + after?: string, + /** + * Results per page + * @default 30 + */ + perPage?: number, +}): Promise +``` + +Result paths are rendered as JavaScript expressions rooted at `tools` (`tools.orders.lookup`, or `tools.context7["resolve-library-id"]` for non-identifier segments), so each `path` is directly usable as the call site. An empty query browses the catalog alphabetically by path; combined with `namespace` (`{ query: "", namespace: "orders" }`) it lists everything in that namespace. A query that names one tool path exactly (canonical path, `tools.`-prefixed path, or rendered JavaScript expression) is treated as a lookup and returns that tool alone. + +The instructions are structured markdown, ordered so the workflow sits at the top and the catalog at the bottom: a `## Workflow` section with numbered steps (find a tool via search when the catalog is partial, or pick from the inlined list when it is complete; call the exact path as-is; return only the needed fields), a `## Rules` section holding only guidance the workflow does not already cover (only listed/search-result Code Mode tools and internal runtime tools exist inside `tools`; filter and aggregate collections in code; narrow `Promise` results at runtime; run independent calls through `Promise.all`; enumerate `tools` with `Object.keys`/`for...in`; browse a namespace and paginate search results when search is advertised), a short `## Language` section that identifies the runtime as a restricted JavaScript orchestration language and names its major unavailable capabilities, and the budgeted `## Available tools` catalog. Example call forms use explicit `.`/`` placeholders - never a real or fabricated tool name. + +A host cannot define its own `$codemode` top-level namespace. + +## Supported Programs + +CodeMode executes a deliberately bounded JavaScript subset. It supports: + +- Plain data literals, property access, assignment, and destructuring. +- `if`, conditional expressions, `switch`, `for`, `for...of` (arrays, strings, Maps, Sets), `for...in` (own keys of plain objects, index strings of arrays, and namespace/tool names of `tools` references - anything else is an error suggesting `for...of` or `Object.keys`, rather than real JS's surprising behavior of indices for strings and zero iterations for Maps/Sets), `while`, and `do...while`. +- Arrow functions and function declarations with closures, defaults, rest parameters, and destructuring. +- Optional chaining, nullish coalescing, templates, spread (arrays, strings, Maps, Sets), and `try`/`catch`. +- Common array, string, number, `Object`, `Math`, and `JSON` operations. Mutating array methods include `push`/`pop`/`shift`/`unshift`/`splice` (removes in place and returns the removed elements)/`fill`/`copyWithin`; array `keys`/`values`/`entries` return **arrays** (matching the Map/Set convention) and work with `for...of` and spread. String methods include `localeCompare` (locale/options arguments ignored), `normalize`, and the `trimLeft`/`trimRight` aliases. `Object.keys` also accepts arrays (index strings, as in JS) and tool references: `Object.keys(tools)` lists the top-level namespaces, including `$codemode`, and `Object.keys(tools.ns)` lists the names at that node (a callable tool enumerates as `[]`; an unknown path is an `UnknownTool` diagnostic). `Object.values`/`Object.entries` on a tool reference fail with a pointer at `Object.keys(tools)` and `tools.$codemode.search`. +- `Date` - `Date.now()`/`Date.parse()`/`Date.UTC()`, `new Date(...)`, the getter methods, and date arithmetic/comparison via the time value. Dates stringify as ISO (`toString` included, for determinism across host timezones). +- Regular expressions - `/literals/` and `new RegExp(...)` with `test`/`exec` (stateful `lastIndex` for `g`), plus string `match`/`matchAll`/`replace`/`replaceAll`/`split`/`search` with patterns. Match results are arrays carrying `index` and named `groups` as own properties (`input` is omitted). `replace` and `replaceAll` accept function replacers with captures, offset, input, and named groups; callbacks run sequentially, may await tool calls, and have their results coerced to strings. Invalid patterns, invalid flags, and missing-`g` calls fail with catchable errors that say what was wrong and how to fix it (escaping hints, the exact `/pattern/g` to write). Patterns run on the host engine, so pathological backtracking is bounded only by the execution timeout. +- `Map` and `Set` - construction from entries/arrays/strings, `get`/`set`/`add`/`has`/`delete`/`clear`/`size`/`forEach`, and `keys`/`values`/`entries` returning **arrays** (not iterators). +- URL helpers - `URL` resolution and mutation, linked `URLSearchParams`, `URL.canParse`/`URL.parse`, URI and URI-component encoding/decoding, and query parameter construction, lookup, mutation, sorting, callbacks, and materialization. URLSearchParams iteration methods return arrays, matching the Map/Set convention. +- First-class promises - an un-awaited `tools.ns.tool(...)` is a promise value whose call starts immediately on a supervised fiber; `await` resolves it (awaiting a non-promise value is a no-op, and `return tools.ns.tool(...)` resolves like an async-function return). `Promise.all`, `Promise.allSettled`, and `Promise.race` accept any array mixing promises and plain values (built inline, beforehand, or via spread); `Promise.resolve`/`Promise.reject` construct settled promises. `Promise.allSettled` rejection reasons are the same plain `{ name?, message }` data a `catch` binding sees, and `Promise.race` interrupts its losing in-flight calls. At most 8 tool calls run concurrently. When a program completes, still-running un-awaited calls are awaited before the execution ends; a failure from a call that was never awaited surfaces as an unhandled-rejection diagnostic. +- `throw value` and `throw new Error(message)` for explicit program failure. `Error` (and `TypeError`/`RangeError`/`SyntaxError`/`ReferenceError`/`EvalError`/`URIError`) are real constructors, callable with or without `new`; error values are plain `{ name, message }` data that additionally satisfy `instanceof Error` (a specific type matches itself and `Error`, as in JS). Every caught failure - thrown errors, interpreter runtime errors, and tool failures - is `instanceof Error` in a `catch` block; a thrown non-error value (`throw "text"`) is not, matching JS. Caught failures carry the `name` the equivalent real-JS failure would have - `JSON.parse` and invalid regex patterns produce a `SyntaxError` (satisfying `instanceof SyntaxError`), an unknown identifier a `ReferenceError`, assigning to a constant a `TypeError`, a bad `normalize` form a `RangeError`; failures with no specific analogue (including tool failures) are named `"Error"`. `instanceof` also recognizes `Date`, `RegExp`, `Map`, `Set`, `URL`, `URLSearchParams`, `Array`, `Object`, and `Promise`; any other right-hand side is a catchable error. + +Inside a program, standard-library values stay live everywhere: the internal data checkpoints (`Object.*` helpers, spread, coercion inputs) preserve the instances, so `Object.values({ d: date })[0].getTime()` and a spread copy of an object holding a Map keep working. Only at the host boundary (final result, tool arguments, `JSON.stringify`) do they serialize exactly as `JSON.stringify` would: Date and URL become strings (an invalid Date becomes `null`), while RegExp, Map, Set, and URLSearchParams become `{}`. Promise values never cross a data boundary: an un-awaited promise in a result or tool argument produces a diagnostic that says to await it, instead of serializing to `{}`. + +It does not expose `eval`, dynamic imports, modules, classes, generators, timers, host globals, prototype mutation, custom promise constructors (`new Promise`), promise chaining (`.then`/`.catch`/`.finally` - `await` with `try`/`catch` is the supported style), or arbitrary method calls. Unsupported syntax returns an `UnsupportedSyntax` diagnostic with a source location when available. + +CodeMode is an orchestration language, not a general JavaScript runtime. ## Execution Limits -| Limit | Default | Bounds | -| ---------------- | -------------------: | ---------------------------------------------------- | -| `timeoutMs` | none - no timeout | Wall-clock execution time. | -| `maxToolCalls` | none - unlimited | Tool calls admitted during the execution. | -| `maxOutputBytes` | none - no truncation | Retained result value and logs; warnings separately. | +The limits are exactly three knobs: -No limit has a default, on purpose: execution budgets are host policy. A host without its own truncation or -interruption should set `maxOutputBytes` and `timeoutMs`. Limits are safe integers; invalid configuration throws a -`RangeError` at construction. Exceeding `maxOutputBytes` never fails the execution - oversized output is truncated -with an in-band marker. The timeout interrupts in-flight tool fibers and pure busy loops alike; a value the program -already returned survives a cleanup timeout as a success with a `TimeoutExceeded` warning. CodeMode does not limit -tool-call concurrency. Data nesting at boundaries is limited to 32 levels. +| Limit | Default | Bounds | +| ---------------- | -------------------: | -------------------------------------------------------------------- | +| `timeoutMs` | none - no timeout | Wall-clock execution time. | +| `maxToolCalls` | none - unlimited | Tool calls admitted during the execution. | +| `maxOutputBytes` | none - no truncation | Model-facing output: the serialized result value plus captured logs. | -## Boundaries and Non-Goals +No limit has a default, on purpose: execution budgets are host policy, not library policy - a host that wants a bound sets one; a host that can interrupt the execution fiber (as OpenCode does on user cancel) may set no timeout, and a host with its own tool-output truncation (as OpenCode has) may leave `maxOutputBytes` unset. A host with neither should set `maxOutputBytes`, or oversized results silently flood model context. -The host owns authentication, authorization, tool selection, credentials, persistence, approval, and logging policy. -CodeMode owns interpretation, schema and plain-data boundaries, resource limits, diagnostics, and discovery. A program -can only exercise authority already present in the supplied tools - do not expose a broad tool and expect the prompt -to restrict it. +Pass only the overrides you need: -Non-goals: permission prompts and approval workflows, durable pause/resume or replay, exactly-once side effects, -application authorization policy, sandboxing arbitrary JavaScript, and compatibility with the full language or npm -ecosystem. Applications that need approval or durable consequences should model those above CodeMode and expose only -the currently authorized tools. +```ts +const runtime = CodeMode.make({ + tools, + limits: { + maxToolCalls: 20, + timeoutMs: 60_000, + }, +}) +``` + +Limits are safe integers. `timeoutMs` must be at least `1`; the others may be `0`. Invalid configuration throws a `RangeError` when `CodeMode.make` or `CodeMode.execute` is called. An explicitly `undefined` value is the same as leaving the limit unset. + +Exceeding a configured `maxOutputBytes` never fails the execution. An oversized result value is replaced by its truncated serialized text plus an explanatory marker, logs are kept from the start until the remaining budget is exhausted (with a final marker line noting the cut), and the result carries `truncated: true`. + +When configured, the timeout interrupts in-flight tool Effects, including eagerly started calls the program has not awaited (their fibers are supervised by the execution). The interpreter yields cooperatively between steps, so the timeout also interrupts pure busy loops (`while (true) {}`) - no separate work budget exists. Tool implementations remain responsible for making their external operations interruptible or independently bounded. + +Two interpreter internals are fixed constants rather than knobs: at most 8 tool calls run concurrently, and values crossing a data boundary may nest at most 32 levels deep (deeper values fail as `InvalidDataValue`, which reads better than a native stack-overflow error). Neither is part of the public contract. + +## Diagnostics + +Failures are data: + +| Kind | Meaning | +| ----------------------- | -------------------------------------------------------------------------------------------------------- | +| `ParseError` | Source is empty or cannot be parsed. | +| `UnsupportedSyntax` | Parsed JavaScript is outside the supported subset. | +| `UnknownTool` | A program referenced a tool the host did not provide. | +| `InvalidToolInput` | Tool input failed schema decoding or safe-data copying. | +| `InvalidToolOutput` | Tool output failed schema decoding or safe-data copying. | +| `InvalidDataValue` | Program data violated the plain-data contract (depth, circularity, blocked properties, non-data values). | +| `ToolCallLimitExceeded` | Calls exceeded `maxToolCalls`. | +| `TimeoutExceeded` | Execution exceeded `timeoutMs`. | +| `ToolFailure` | A tool refused or failed. | +| `ExecutionFailure` | The program threw or another execution error occurred. | + +Unknown host failures, defects, invalid outputs, and copying failures are sanitized. To return a safe operational refusal, fail with `toolError`: + +```ts +import { toolError } from "@opencode-ai/codemode" + +run: ({ id }) => (authorized(id) ? loadOrder(id) : Effect.fail(toolError("Order is unavailable"))) +``` + +Only the supplied message is model-visible. The optional cause is never returned in `CodeMode.Result`; hosts should perform any required internal logging before crossing this boundary. + +## Authority Boundary + +CodeMode confines programs to the supplied tool tree, but it does not decide what those tools may do. + +The host owns: + +- Authentication and authorization. +- Tool selection and immutable scope. +- Credentials and network clients. +- Persistence, idempotency, approval, and durable side effects. +- Logging and redaction policy. + +CodeMode owns: + +- Parsing and interpreting the supported subset without `eval`. +- Schema boundaries around tool calls. +- Plain-data copying and blocked prototype members. +- Resource limits, call accounting, and normalized diagnostics. +- Model-facing tool discovery and instructions. + +A program cannot gain authority through prose or generated code. It can only exercise authority already present in the supplied tools. Do not expose a broad tool and expect the prompt to restrict it. + +## Laws + +The public contract is guided by these equivalences: + +- `CodeMode.execute({ ...options, code })` is equivalent to `CodeMode.make(options).execute(code)`. +- A tool implementation is not invoked unless its input has decoded successfully. +- A tool result is not visible to the program unless its output has decoded and crossed the plain-data boundary successfully. +- Unknown host failures do not become model-visible diagnostics; `ToolError` is the explicit safe-message channel. +- Host interruption remains interruption rather than a `CodeMode.Failure`. + +## Non-Goals + +- Generic permission prompts or approval workflows. +- Durable pause/resume, replay, or storage adapters. +- Exactly-once external side effects. +- Application authorization or product policy. +- A filesystem or process sandbox for arbitrary JavaScript. +- Compatibility with the full JavaScript language or npm ecosystem. + +Applications that need approval or durable consequences should model those above CodeMode and expose only the currently authorized tools. ## Testing @@ -200,3 +365,5 @@ From the package directory: bun test bun run typecheck ``` + +The direct suite covers public projections, discovery, schema boundaries, diagnostic sanitization, resource limits, tool-call observation, and interruption. diff --git a/packages/codemode/codemode.md b/packages/codemode/codemode.md index f26c0446ef..f56c405746 100644 --- a/packages/codemode/codemode.md +++ b/packages/codemode/codemode.md @@ -6,8 +6,7 @@ It records current behavior, intentional boundaries, durable rationale, and mate Completed implementation history, branch names, test counts, and closed findings belong in git, not here. Remove completed work instead of preserving checked-off chronology. -Detailed package API documentation lives in [README.md](./README.md), and the checkable language/runtime matrix lives -in [interpreter-support.md](./interpreter-support.md). OpenAPI-specific follow-ups live in +Detailed package API documentation lives in [README.md](./README.md). OpenAPI-specific follow-ups live in [src/openapi/TODO.md](./src/openapi/TODO.md). ## How CodeMode Works @@ -32,7 +31,7 @@ CodeMode is an orchestration language, not a general JavaScript runtime or an ap The generic runtime lives in `packages/codemode` and is host-neutral: 1. The host builds a tree of `Tool.make(...)` definitions and calls `CodeMode.make(...)` or `CodeMode.execute(...)`. -2. CodeMode generates model instructions, a budgeted inline catalog, and the global `search(...)` built-in. +2. CodeMode generates model instructions, a budgeted inline catalog, and the internal `$codemode.search` tool. 3. TypeScript syntax is transpiled away, Acorn parses the resulting JavaScript, and an owned tree-walking interpreter executes it without `eval`. 4. Tool inputs and outputs cross schema and plain-data boundaries before they become visible on either side. @@ -46,14 +45,13 @@ advertised as `Promise`. ### Discovery and model workflow The model sees a token-budgeted catalog. Every namespace remains visible, and complete signatures are selected -round-robin across namespaces so one large namespace cannot starve the others. The global `search(...)` built-in is -always callable - synchronously, counted as an admitted tool call - and is advertised when the inline catalog is -partial. +round-robin across namespaces so one large namespace cannot starve the others. `$codemode.search` is always callable +and is advertised when the inline catalog is partial. The intended workflow is: -1. Pick an exact signature from the inline catalog, or return `search(...)` results and use a selected path in the - next execution. +1. Pick an exact signature from the inline catalog, or return `$codemode.search(...)` results and use a selected path + in the next execution. 2. Call the exact returned path without guessing or normalizing segments. 3. Narrow `Promise` results before reading fields. 4. Start independent calls together and await them with `Promise.all`. @@ -64,30 +62,13 @@ path lookup, namespace browsing, deterministic ranking, and pagination. ### Tool execution -Every sandbox promise starts eagerly on a run-once fiber owned by the whole CodeMode execution, including tool calls, -async functions, chained `.then`/`.catch`/`.finally` reactions, `new Promise(executor)` constructions, and the -`Promise.all`/`allSettled`/`race`/`any`/`resolve`/`reject` statics. Nested functions therefore cannot end the lifetime -of work they started. -Independent aggregate batches overlap, and rejection is observed at the eventual `await` or chained rejection handler. -`Promise.race` and `Promise.any` use native non-cancelling settlement semantics: the deciding member wins while losers -continue running, and an all-rejected `Promise.any` rejects with an `AggregateError`. `new Promise(...)` hands the -executor first-class resolve/reject callables that may escape and settle the promise later, exactly once. -Reaction ordering matches what V8 makes observable - handlers and await continuations are deferred and run in attach -order, and a combinator settles one reaction turn after its deciding member - without promising exact microtask-count -parity beyond that. At normal completion CodeMode interrupts everything still running - race losers, -fail-fast `Promise.all` stragglers, and fire-and-forget calls alike: the program has returned, so no future await can -exist, and work whose completion matters must be awaited by the program. Waiting for any class of leftover instead -would let it hold the execution open indefinitely. -Rejections that settled un-awaited before the return become `Success.warnings` diagnostics. A fatal program failure or -host interruption closes the execution promise scope and interrupts its active fibers instead. A timeout does the -same, except that a value the program already returned is preserved alongside a `TimeoutExceeded` warning rather than -discarded. CodeMode does not limit tool-call concurrency. +Calling a tool starts its Effect eagerly on a supervised fiber. The returned sandbox promise is run-once and can be +awaited directly or through the supported `Promise` combinators. At most eight tool calls execute concurrently. +Unfinished calls are drained before successful program completion, and an unhandled call failure becomes a diagnostic. The public execution-policy knobs are `timeoutMs`, `maxToolCalls`, and `maxOutputBytes`. The package supplies no -defaults because budgets are host policy. The interpreter also enforces a fixed internal data nesting depth. -`maxOutputBytes` bounds retained payload bytes, not the complete rendered message; -warning diagnostics have an equal separate budget so a large value cannot starve them, and fixed truncation notices and -host-added framing are intentionally outside the budgets. +defaults because budgets are host policy. The interpreter also enforces fixed internal boundaries for tool-call +concurrency and data nesting depth. ### Data, files, and failures @@ -97,13 +78,17 @@ boundary. Unknown host failures and invalid outputs are sanitized. `ToolError` is the explicit channel for a safe message that a tool wants the model to see. Diagnostic categories distinguish parsing, unsupported syntax, unknown tools, invalid -data, tool failures, limits, timeouts, execution failures, and warning truncation. +data, tool failures, limits, timeouts, and execution failures. Files and other attachment content stay outside the interpreter. A host may collect them while child tools execute and attach them to the outer result, but the program receives only the structured tool output. ### V2 OpenCode adapter +This section describes the `v2` branch integration. On `dev`, CodeMode is integrated through +`packages/opencode/src/tool/code-mode.ts`, where nested MCP calls run the `tool.execute.before` and +`tool.execute.after` plugin hooks. + CodeMode is integrated into V2 through `packages/core/src/tool/registry.ts` and `packages/core/src/tool/execute.ts`: @@ -113,8 +98,7 @@ CodeMode is integrated into V2 through `packages/core/src/tool/registry.ts` and normally. - When visible deferred tools exist, Core reserves and materializes one `execute` tool. Grouped deferred tools become CodeMode namespaces instead of flattened model-facing names. -- Nested calls execute the registered `Tool` values captured for the model request; later registrations affect later - requests. +- Each nested call checks that its captured registration is still current before dispatching it. - Authorization and side-effect ordering remain responsibilities of the leaf tool. Catalog visibility is not execution authorization. - Structured child output enters the interpreter. File parts are collected host-side and attached to the outer result. @@ -145,20 +129,48 @@ represent accurately rather than guessing semantics. ## Decisions and Rationale -| Decision | Rationale | -| ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| Keep an owned tree-walking interpreter. | The product need is bounded tool orchestration, not arbitrary JavaScript. Owning the language surface keeps authority and behavior explicit. | -| Treat schemas as the model-facing interface. | Signatures drive correct calls; Effect Schema also provides the runtime validation boundary, while JSON Schema supports adapter interoperability. | -| Keep authority host-owned. | CodeMode can only confine programs to supplied tools. The host chooses those tools, and each tool enforces its own authorization and side-effect policy. | -| Use progressive catalog disclosure plus search. | Large tool sets should not consume the prompt, but every namespace must remain discoverable and speculative search calls should remain valid. | -| Start promises eagerly and supervise them for the execution. | This preserves normal call-time parallelism and run-once settlement while allowing pending work to be interrupted when the program returns. | -| Keep files outside the sandbox value space. | Models should compose structured data without routing binary payloads through generated code or context. | -| Treat `execute` as the model-facing invocation boundary. | Nested calls are implementation details of one orchestration program. Reusing the outer context and bounding only the final result preserves complete intermediate data without inventing durable child-call identities. | -| Return expected failures as data. | Models need actionable diagnostics without exposing private host causes; host interruption and defects must still propagate correctly. | -| Leave execution-limit defaults to hosts. | Appropriate budgets depend on the surrounding product and its own cancellation, retention, and output-bounding policies. | -| Skip unsupported OpenAPI operations. | Incorrect parameter encoding, authentication, or transport behavior is worse than a precise `skipped` reason. | +| Decision | Rationale | +| -------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| Keep an owned tree-walking interpreter. | The product need is bounded tool orchestration, not arbitrary JavaScript. Owning the language surface keeps authority and behavior explicit. | +| Treat schemas as the model-facing interface. | Signatures drive correct calls; Effect Schema also provides the runtime validation boundary, while JSON Schema supports adapter interoperability. | +| Keep authority host-owned. | CodeMode can only confine programs to supplied tools. The host chooses those tools, and each tool enforces its own authorization and side-effect policy. | +| Use progressive catalog disclosure plus search. | Large tool sets should not consume the prompt, but every namespace must remain discoverable and speculative search calls should remain valid. | +| Start tool promises eagerly and supervise them. | This preserves normal call-time parallelism while giving each call run-once settlement and interruption safety. | +| Keep files outside the sandbox value space. | Models should compose structured data without routing binary payloads through generated code or context. | +| Treat `execute` as the model-facing invocation boundary. | Nested calls are implementation details of one orchestration program. Reusing the outer context and bounding only the final result preserves complete intermediate data without inventing durable child-call identities. | +| Return expected failures as data. | Models need actionable diagnostics without exposing private host causes; host interruption and defects must still propagate correctly. | +| Leave execution-limit defaults to hosts. | Appropriate budgets depend on the surrounding product and its own cancellation, retention, and output-bounding policies. | +| Skip unsupported OpenAPI operations. | Incorrect parameter encoding, authentication, or transport behavior is worse than a precise `skipped` reason. | ## Remaining Work -The [interpreter support checklist](./interpreter-support.md) owns concrete DSL, standard-library, semantic-correctness, -diagnostic, and data-boundary work. OpenAPI adapter work remains in [src/openapi/TODO.md](./src/openapi/TODO.md). +Keep only material unresolved work here. Small isolated defects should be GitHub issues; adapter-only work belongs in +the adapter TODO. Delete entries when completed. + +### DSL expansion + +The supported JavaScript subset should grow when common model-generated code improves tool orchestration. These are +current omissions to implement, not intentional product boundaries. + +- [ ] Design proper multi-stage promise pipelines. Supporting `.then`, `.catch`, and `.finally` should preserve promise + assimilation, cancellation, failure handling, and concurrent per-item pipelines rather than adding syntax-only + shims. Consider `Promise.any` in the same pass. +- [ ] Support async iteration and `for await...of`. Define behavior first for the runtime's supported promise and + collection values, then extend it to bounded host streams when a stream boundary exists. +- [ ] Support callback-bearing standard-library variants that models commonly generate: the mapper argument to + `Array.from(...)` and replacers for `JSON.stringify(...)`, including Effect-aware callbacks where needed. +- [ ] Close basic `Object` parity gaps: let `Object.values`/`Object.entries` accept arrays, make `Object.assign` validate + and mutate its target, add `Object.is`, and let `Object.fromEntries` consume every supported iterable. +- [ ] Add deterministic modern collection conveniences where they improve orchestration: `Object.groupBy`, Set + composition methods, and `Array.prototype.toSpliced`. +- [ ] Complete the deterministic `Math` surface beyond the current arithmetic, rounding, root, power, and logarithm + helpers. Decide separately whether nondeterministic `Math.random` belongs in the runtime. +- [ ] Refine diagnostics so user throws, expected tool failures, unexpected host/tool defects, and genuine interpreter + defects are distinguishable without leaking private causes. + +### Tool and result contracts + +- [ ] Design explicit tagged representations and size rules before allowing Blob, File, ArrayBuffer, typed arrays, or + host streams to cross the sandbox boundary. +- [ ] Define one consistent policy for tool path segments named `__proto__`, `constructor`, or `prototype`. They must + either be safely callable, rejected before catalog generation, or use one documented escaping rule. diff --git a/packages/codemode/interpreter-support.md b/packages/codemode/interpreter-support.md deleted file mode 100644 index aff98d9b49..0000000000 --- a/packages/codemode/interpreter-support.md +++ /dev/null @@ -1,316 +0,0 @@ -# CodeMode Interpreter Support - -This is the checkable support matrix for CodeMode's confined JavaScript interpreter. It tracks the language and -standard-library surface that programs can use today, plus concrete gaps that may be implemented later. - -- `[x]` means the feature is implemented at the scope described here. -- `[ ]` means the feature is unavailable, incomplete, or intentionally divergent as described. -- A checked item does not promise complete ECMAScript edge-case parity. Known differences are listed next to the - supported surface or under [Known semantic gaps](#known-semantic-gaps). -- [Intentional exclusions](#intentional-exclusions) are boundaries, not backlog. - -When behavior changes, update this file and the tests in the same change. The implementation and tests remain the -ultimate source of truth. - -## Source and execution model - -- [x] JavaScript parsed with the latest syntax accepted by Acorn, then restricted by the interpreter allowlist. -- [x] Erasable TypeScript syntax, including type annotations, type declarations, assertions, and non-null assertions. - TypeScript is transpiled first; the emitted JavaScript must still use the supported subset. -- [x] Top-level `await` and `return` through the program's implicit async-function scope. -- [x] Explicit `return`, final top-level expression as a REPL-style result, and `null` when no value is produced. -- [x] JSON-like host boundaries with `undefined` and non-finite numbers normalized to `null`. -- [x] Live Date, RegExp, Map, Set, URL, and URLSearchParams values inside the sandbox. -- [x] Tool calls through the host-provided `tools` tree only. -- [x] The global `search(...)` built-in: synchronous tool discovery that counts as an admitted tool call and is - shadowable by program declarations like other globals. -- [x] Cooperative timeout, an optional total tool-call limit, output bounding, and unrestricted tool-call concurrency. -- [ ] Full JavaScript or TypeScript compatibility. CodeMode is a bounded orchestration language. - -## Values and literals - -- [x] `null`, `undefined`, booleans, finite and non-finite numbers, and strings. -- [x] Array literals, including holes and spread from arrays, strings, Maps, Sets, and URLSearchParams. -- [x] Object literals with shorthand, computed string/number keys, and object spread. -- [x] Template literals with interpolation. -- [x] Regular-expression literals. -- [x] `NaN` and `Infinity` globals. -- [ ] BigInt literals and values. -- [ ] Symbols. -- [ ] Tagged template literals. -- [ ] Getters and setters in object literals. - -## Bindings and destructuring - -- [x] `const`, `let`, and accepted `var` declarations. -- [x] Object and array destructuring in declarations, parameters, assignment expressions, and `for...of` bindings. -- [x] Nested patterns, defaults, elisions, and rest elements. -- [x] Assignment to identifiers, object fields, array indexes, and writable URL fields. -- [x] Function declarations are hoisted within their interpreted scope. -- [x] Parameter defaults observe a temporal dead zone for later parameters. -- [ ] JavaScript-correct `var` function scope, hoisting, and redeclaration. Accepted `var` currently behaves like a - lexical declaration; prefer `let` or `const`. -- [ ] Complete `let`/`const` temporal-dead-zone and declaration-hoisting semantics. -- [ ] Computed object destructuring keys such as `const { [field]: value } = record`. -- [ ] Object destructuring from arrays, such as `const { length } = values`. -- [ ] Iterable array destructuring from Map, Set, string, or URLSearchParams values. -- [ ] Dynamic property deletion with `delete object[key]`. - -## Statements and control flow - -- [x] Blocks and empty statements. -- [x] `if`/`else` and conditional expressions. -- [x] `switch`, including default clauses and fallthrough. -- [x] `for`, `while`, and `do...while`. -- [x] `for...of` over arrays, strings, Maps, Sets, and URLSearchParams. -- [x] `for...in` over own keys of plain objects, arrays, and tool references. -- [x] Unlabeled `break` and `continue`. -- [x] `try`, `catch`, optional catch bindings, and `finally`. -- [x] `throw` with arbitrary values. -- [ ] Labeled statements, labeled `break`, and labeled `continue`. -- [ ] `for await...of` and async iteration. -- [ ] `with` and `debugger` statements. - -## Functions and callbacks - -- [x] Function declarations, function expressions, and arrow functions. -- [x] Synchronous and `async` functions. -- [x] Closures, recursion, default parameters, rest parameters, and destructured parameters. -- [x] Expression and block function bodies. -- [x] User callbacks for the supported Array, Map, Set, URLSearchParams, sort, and string-replacement APIs. -- [x] `Boolean`, `Number`, `String`, `parseInt`, `parseFloat`, and URI helpers as callbacks where applicable. -- [x] Async string replacement callbacks; replacements are evaluated sequentially. -- [ ] `this`, `super`, constructor functions, or function prototype methods such as `call`, `apply`, and `bind`. -- [ ] Classes and private fields. -- [ ] Generator functions and `yield`. -- [ ] Async predicates, reducers, and comparators with automatic awaiting. Async mapping can be joined explicitly with - `Promise.all`, but a promise is not a meaningful predicate or sort result. -- [ ] General built-in callable references as callbacks, such as `values.map(Math.abs)` or - `records.map(JSON.stringify)`. - -## Expressions and operators - -- [x] Property access with dot or computed bracket syntax. -- [x] Optional property access and optional calls. -- [x] Function/tool calls and spread arguments. -- [x] Sequence expressions (the comma operator). -- [x] `await` for sandbox promises; a plain value passes through unchanged, though every `await` still defers its - continuation one reaction turn. -- [x] `new` for Error types, Date, RegExp, Map, Set, URL, URLSearchParams, and Promise. -- [x] Arithmetic operators: `+`, `-`, `*`, `/`, `%`, and `**`. -- [x] Equality and ordering: `==`, `!=`, `===`, `!==`, `<`, `<=`, `>`, and `>=`. -- [x] Bitwise operators: `&`, `|`, `^`, `~`, `<<`, `>>`, and `>>>`. -- [x] Logical operators: `&&`, `||`, `??`, and `!`, with short-circuiting. -- [x] Unary `+`, unary `-`, `typeof`, `instanceof`, and own-property-only `in`. -- [x] Prefix and postfix `++` and `--`. -- [x] Plain, arithmetic, bitwise, and logical assignment operators. -- [ ] Unary `void` and `delete`. -- [ ] Arbitrary constructors. - -## Promises and tools - -- [x] Tool calls start eagerly and return supervised, run-once sandbox promises. -- [x] Direct `await`, repeated awaits, and implicit resolution when a promise is returned from a function/program. -- [x] `Promise.resolve` and `Promise.reject`. -- [x] `Promise.all`, `Promise.allSettled`, `Promise.race`, and `Promise.any` over supported collections containing - promises and plain values. -- [x] `Promise.all` preserves result order and rejects on the first observed failure without cancelling siblings. -- [x] `Promise.allSettled` returns plain fulfilled/rejected outcome records. -- [x] `Promise.race` settles from the first result without cancelling losers at settlement time. -- [x] Real promise values from `Promise.all`, `Promise.allSettled`, and `Promise.race`; separately constructed - combinator batches overlap as in normal JavaScript. -- [x] Promise chaining with `.then`, `.catch`, and `.finally`: handlers run deferred in attach order, returned - promises are adopted, handler throws reject the derived promise, `.finally` preserves the original settlement - unless its cleanup fails, and direct self-resolution rejects with a `TypeError`. -- [x] Every `await` (including of plain values and already-settled promises) defers its continuation one reaction - turn, so concurrent async functions interleave at await points as in JavaScript. -- [x] Combinators settle one reaction turn after their deciding member (V8-observable ordering): reactions already - attached to members run first, and an aggregate cannot beat a plain value settling in the same turn into a - `Promise.race`. Exact microtask-count parity beyond this observable ordering is not a documented guarantee. -- [x] All still-pending work (race losers, fail-fast `Promise.all` stragglers, and un-awaited calls alike) is - interrupted when the program returns; rejections that settled un-awaited become `Success.warnings` - diagnostics. A combinator abandoned inside its final settlement turn counts as pending and is interrupted - without a warning. -- [x] `try`/`catch` can handle awaited tool and promise failures. -- [x] `Promise.any`: first fulfillment wins; all-rejected rejects with an `AggregateError` whose `errors` array holds - the catch-normalized reasons in input order, and empty input rejects with an empty `AggregateError`. -- [x] `new Promise((resolve, reject) => ...)`: the executor runs synchronously and receives first-class resolve/reject - callables that settle the promise exactly once (they may escape the executor and settle later); an executor - throw rejects unless the promise already settled, resolving with a promise adopts it, and resolving with the - promise itself rejects with a `TypeError`. Resolver callables work as `.then`/`.catch` handlers and collection - callbacks but remain opaque references that cannot cross the data boundary. -- [ ] Thenable assimilation (objects with a `then` method are plain data, not promises). -- [ ] Async iterables, host streams, and stream consumption. - -## Objects and properties - -- [x] Own-field reads and writes on plain data objects. -- [x] Computed property names and object spread. -- [x] `Object.keys`, `Object.values`, `Object.entries`, `Object.hasOwn`, `Object.assign`, and `Object.fromEntries`. -- [x] `Object.keys` over arrays and tool references. -- [x] Object identity is preserved by in-sandbox Object helpers. -- [x] Blocked access to `__proto__`, `constructor`, and `prototype`. -- [ ] `Object.is`; runtime and tool-reference identity semantics need to be defined first. -- [ ] `Object.groupBy`. -- [ ] Object creation, descriptors, freezing/sealing, prototype APIs, and reflection APIs. -- [ ] A final policy for legal data/tool keys named `__proto__`, `constructor`, or `prototype`. - -## Arrays - -- [x] Static methods: `Array.isArray`, `Array.of`, and `Array.from`. -- [x] Iteration/transformation: `map`, `filter`, `flatMap`, and `forEach`. -- [x] Searching/tests: `find`, `findIndex`, `findLast`, `findLastIndex`, `some`, `every`, `includes`, `indexOf`, and - `lastIndexOf`. -- [x] Aggregation: `reduce` and `reduceRight`. -- [x] Ordering: `sort`, `toSorted`, `reverse`, and `toReversed`. -- [x] Access/copying: `at`, `slice`, `concat`, `flat`, `with`, and `join`. -- [x] Mutation: `push`, `pop`, `shift`, `unshift`, `splice`, `fill`, and `copyWithin`. -- [x] Materialized iteration helpers: `keys`, `values`, and `entries` return arrays rather than iterators. -- [x] `length`, numeric indexing, index assignment, spread, and `for...of`. -- [ ] The mapper and `thisArg` forms of `Array.from`. -- [ ] `Array.prototype.toSpliced`. -- [ ] Canonical index handling: a key such as `"01"` must not alias index `1`. -- [ ] Complete sparse-array parity. Promise combinators do consume holes as `undefined` members, as in JS. -- [ ] Correct `findLast` return behavior when its predicate mutates the examined element. - -## Strings - -- [x] Case/normalization: `toLowerCase`, `toUpperCase`, `normalize`. -- [x] Trimming: `trim`, `trimStart`, and `trimEnd`. -- [x] Searching/tests: `includes`, `startsWith`, `endsWith`, `indexOf`, `lastIndexOf`, and `search`. -- [x] Slicing/access: `slice`, `substring`, `at`, `charAt`, `charCodeAt`, and `codePointAt`. -- [x] Construction/transformation: `split`, `concat`, `repeat`, `padStart`, `padEnd`, `replace`, and `replaceAll`. -- [x] Regular-expression integration: `match`, materialized `matchAll`, `replace`, `replaceAll`, `split`, and `search`. -- [x] `localeCompare`; locale and options arguments are currently ignored. -- [x] `toString`, `length`, numeric indexing, spread, and `for...of` by Unicode code point. -- [x] Static `String.fromCharCode` and `String.fromCodePoint`. -- [ ] Locale/options-aware `localeCompare` and locale formatting APIs. -- [ ] Exact native coercion across every string method; CodeMode often requires explicit strings/numbers. -- [ ] Native no-argument parity for `match()` and `search()`. - -## Numbers and Math - -- [x] Coercion functions: `Number`, `parseInt`, and `parseFloat`. -- [x] Number predicates/parsers: `Number.isInteger`, `Number.isFinite`, `Number.isNaN`, `Number.isSafeInteger`, - `Number.parseInt`, and `Number.parseFloat`. -- [x] Number formatting: `toFixed`, `toPrecision`, `toExponential`, `toString`, and `valueOf`. -- [x] Number constants: `MAX_SAFE_INTEGER`, `MIN_SAFE_INTEGER`, `MAX_VALUE`, `MIN_VALUE`, `EPSILON`, `NaN`, - `POSITIVE_INFINITY`, and `NEGATIVE_INFINITY`. -- [x] Math constants: `PI`, `E`, `LN2`, `LN10`, `LOG2E`, `LOG10E`, `SQRT2`, and `SQRT1_2`. -- [x] Math methods: `random`, `max`, `min`, `abs`, `acos`, `acosh`, `asin`, `asinh`, `atan`, `atan2`, `atanh`, - `floor`, `ceil`, `round`, `trunc`, `sign`, `sqrt`, `cbrt`, `pow`, `hypot`, `cos`, `cosh`, `sin`, `sinh`, - `tan`, `tanh`, `log`, `log2`, `log10`, `log1p`, `exp`, `expm1`, `f16round`, `fround`, `clz32`, and `imul`. -- [ ] Native zero-argument behavior for `Number()` and `String()`; they currently do not produce `0` and `""`. -- [ ] Safe interpreter coercion for `++` and `--` rather than host `Number(...)` coercion. -- [ ] Reliable feature detection for unknown static members. -- [ ] `Math.sumPrecise`. -- [ ] Global coercing `isFinite` and `isNaN`. - -## JSON and console - -- [x] `JSON.parse` and `JSON.stringify`. -- [x] Numeric/string indentation for `JSON.stringify`. -- [x] Captured `console.log`, `console.info`, `console.debug`, `console.warn`, and `console.error`. -- [x] Captured `console.dir` and `console.table`. -- [ ] `JSON.parse` reviver callbacks. -- [ ] `JSON.stringify` function/array replacers. -- [ ] Other console methods, timers, counters, groups, and host console access. - -## Date - -- [x] `Date.now`, `Date.parse`, and `Date.UTC`. -- [x] `new Date()` from the current time, epoch milliseconds, a date string, another Date, or local components. -- [x] `getTime`, `valueOf`, `toISOString`, `toJSON`, and deterministic ISO `toString`. -- [x] Local getters: `getFullYear`, `getMonth`, `getDate`, `getDay`, `getHours`, `getMinutes`, `getSeconds`, and - `getMilliseconds`. -- [x] UTC getters: `getUTCFullYear`, `getUTCMonth`, `getUTCDate`, `getUTCDay`, `getUTCHours`, `getUTCMinutes`, - `getUTCSeconds`, and `getUTCMilliseconds`. -- [x] `getTimezoneOffset`, arithmetic, relational comparison, and `instanceof Date`. -- [x] Date values serialize to ISO strings; invalid dates serialize to `null`. -- [ ] Date setters. -- [ ] `toUTCString`, locale methods, and other Date formatting methods. -- [ ] Exact native constructor coercion, local-time, and loose-equality semantics. -- [ ] Native `RangeError` branding for invalid `toISOString()` calls. -- [ ] Temporal and Intl date/time APIs. - -## Regular expressions - -- [x] Literal and `new RegExp(pattern, flags)` construction. -- [x] `test`, `exec`, and `toString`. -- [x] Readable `source`, `flags`, `lastIndex`, `global`, `ignoreCase`, `multiline`, `sticky`, `unicode`, and `dotAll`. -- [x] Captures, named groups, match indexes, and stateful global matching. -- [x] Integration with supported String methods, including async function replacers. -- [ ] Writable `lastIndex`. -- [ ] Exposed metadata for the `d` and `v` flags. -- [ ] `RegExp.escape`. -- [ ] Protection from pathological host-regex backtracking beyond the cooperative execution timeout. - -## Map and Set - -- [x] `new Map()` from entry arrays or another Map. -- [x] Map `get`, `set`, `has`, `delete`, `clear`, `size`, and `forEach`. -- [x] `new Set()` from arrays, strings, or another Set. -- [x] Set `add`, `has`, `delete`, `clear`, `size`, and `forEach`. -- [x] Materialized `keys`, `values`, and `entries` arrays for Map and Set. -- [x] Spread, `for...of`, `Array.from`, and `Object.fromEntries` integration. -- [x] Map and Set values serialize to `{}` at host/JSON boundaries. -- [ ] Set composition methods such as `union`, `intersection`, `difference`, and relation predicates. -- [ ] WeakMap and WeakSet. -- [ ] Native iterator objects and custom iterators. - -## URL and URI helpers - -- [x] `encodeURI`, `encodeURIComponent`, `decodeURI`, and `decodeURIComponent`. -- [x] `new URL(input, base)`, `URL.canParse`, and `URL.parse`. -- [x] URL `toString`, `toJSON`, and linked `searchParams`. -- [x] Readable URL fields: `href`, `origin`, `protocol`, `username`, `password`, `host`, `hostname`, `port`, - `pathname`, `search`, and `hash`. -- [x] Writable URL fields except `origin`. -- [x] `new URLSearchParams()` from query strings, data objects, pairs, Maps, and URLSearchParams. -- [x] URLSearchParams `append`, `delete`, `get`, `getAll`, `has`, `set`, `sort`, `forEach`, `keys`, `values`, - `entries`, `toString`, and `size`. -- [x] URL values serialize to their href; URLSearchParams serialize to `{}`. - -## Errors and diagnostics - -- [x] `Error`, `TypeError`, `RangeError`, `SyntaxError`, `ReferenceError`, `EvalError`, and `URIError`, callable with - or without `new`. -- [x] `AggregateError` with the `(errors, message?)` signature and an own `errors` array, constructed directly or by - an all-rejected `Promise.any`. -- [x] Error `name`/`message`, error inheritance through `instanceof`, and plain-data serialization. -- [x] `instanceof` for Date, RegExp, Map, Set, URL, URLSearchParams, Array, Object, Promise, and Error types. -- [x] Catchable interpreter failures and awaited tool failures. -- [x] Source locations on unsupported-syntax diagnostics when available. -- [x] Sanitized model-visible diagnostics and explicit safe `ToolError` messages. -- [ ] Distinct public categories for user throws, tool refusal, tool internal failure, invalid returned data, compile - failures, and genuine interpreter defects. -- [ ] Preservation of detailed recoverable failure categories inside `catch` and `Promise.allSettled`. - -## Known semantic gaps - -These are actionable implementation items. Check them off only when behavior and direct tests land. - -- [x] Return real promises from `Promise.all`, `Promise.allSettled`, and `Promise.race`. -- [ ] Guarantee every advertised tool path is executable, including dotted and blocked path segments. -- [ ] Define safe outbound handling for non-finite numbers and `undefined` so invalid values cannot silently become - `null` in render-only or OpenAPI tool calls. -- [ ] Make regular-expression execution genuinely timeout-safe, or narrow the timeout guarantee explicitly. -- [ ] Complete lexical declaration and destructuring semantics listed above. -- [ ] Make callback acceptance and async callback behavior consistent across built-ins. -- [ ] Reject every unsupported callback argument explicitly rather than silently ignoring it. -- [ ] Resolve the built-in correctness gaps listed in the Array, String, Number, Date, and RegExp sections. -- [ ] Make tool search tokenization Unicode-aware. -- [ ] Design explicit tagged representations and size limits before adding binary values or streams. - -## Intentional exclusions - -These constraints preserve CodeMode's confinement and host-neutral scope. They are not TODO items. - -- Ambient filesystem, process, environment, credential, network, or application access. -- `fetch`, timers, crypto, or other host globals unless a future host explicitly supplies a bounded capability. -- Static imports, dynamic imports, modules, npm packages, and module loading. -- `eval`, `Function(...)`, arbitrary host execution, and prototype mutation. -- Generic permission prompts, authorization policy, persistence, replay, or exactly-once side effects. -- Arbitrary method dispatch outside the documented allowlists. -- Automatic parsing of text tool results as JSON. -- Full browser, Node.js, Bun, or ECMAScript runtime compatibility. diff --git a/packages/codemode/package.json b/packages/codemode/package.json index 263daa6a53..1641518483 100644 --- a/packages/codemode/package.json +++ b/packages/codemode/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/codemode", - "version": "1.17.18", + "version": "1.18.11", "description": "Effect-native confined code execution over schema-described tools", "private": true, "type": "module", diff --git a/packages/codemode/src/codemode.ts b/packages/codemode/src/codemode.ts index c53c7b40ab..14782d7c8a 100644 --- a/packages/codemode/src/codemode.ts +++ b/packages/codemode/src/codemode.ts @@ -1,5 +1,5 @@ import { Effect, Schema } from "effect" -import { executeWithLimits } from "./interpreter/execute.js" +import { executeWithLimits } from "./interpreter/runtime.js" import { type HostTools, type Services, type ToolDescription, ToolRuntime } from "./tool-runtime.js" import type { Definition } from "./tool.js" @@ -8,17 +8,11 @@ export type { ToolCall, ToolCallEnded, ToolCallHooks, ToolCallStarted, ToolDescr /** Resource budgets enforced independently during each CodeMode program execution. */ export type ExecutionLimits = { - /** - * Wall-clock milliseconds before interruption. Result delivery waits for tool cleanup. - * No default: absent means no timeout. - */ + /** Maximum wall-clock execution time in milliseconds. No default: absent means no timeout. */ readonly timeoutMs?: number /** Maximum number of tool calls admitted by the runtime. No default: absent means unlimited. */ readonly maxToolCalls?: number - /** - * Maximum UTF-8 bytes retained from the result and logs. Warnings have a separate equal budget; - * truncation notices and host formatting are additional. - */ + /** Maximum UTF-8 bytes of model-facing output. No default: absent means no truncation. */ readonly maxOutputBytes?: number } @@ -76,9 +70,8 @@ export const DiagnosticKind = Schema.Literals([ "TimeoutExceeded", "ToolFailure", "ExecutionFailure", - "Truncated", ]) -/** Stable categories produced by program, schema, tool, limit, and truncation diagnostics. */ +/** Stable categories produced by program, schema, tool, and limit failures. */ export type DiagnosticKind = typeof DiagnosticKind.Type export const Diagnostic = Schema.Struct({ @@ -94,7 +87,6 @@ const ToolCallSchema = Schema.Struct({ name: Schema.String }) export const Success = Schema.Struct({ ok: Schema.Literal(true), value: Schema.Json, - warnings: Schema.optionalKey(Schema.Array(Diagnostic)), logs: Schema.optionalKey(Schema.Array(Schema.String)), truncated: Schema.optionalKey(Schema.Boolean), toolCalls: Schema.Array(ToolCallSchema), @@ -124,7 +116,11 @@ export type Runtime = { readonly execute: (code: string) => Effect.Effect } -const validateLimit = (name: keyof ExecutionLimits, value: number | undefined, minimum: number): number | undefined => { +const validateLimit = ( + name: keyof ExecutionLimits, + value: Value, + minimum: number, +): Value => { if (value !== undefined && (!Number.isSafeInteger(value) || value < minimum)) { throw new RangeError(`${name} must be a safe integer greater than or equal to ${minimum}.`) } @@ -142,6 +138,7 @@ export const execute = >( options: ExecuteOptions, ): Effect.Effect> => { const tools = (options.tools ?? {}) as HostTools> + ToolRuntime.assertValidTools(tools) return executeWithLimits(options, resolveExecutionLimits(options.limits), ToolRuntime.searchIndex(tools)) } @@ -150,6 +147,7 @@ export const make = = {}>( options: Options = {} as Options, ): Runtime> => { const tools = (options.tools ?? {}) as HostTools> + ToolRuntime.assertValidTools(tools) const limits = resolveExecutionLimits(options.limits) const prepared = ToolRuntime.prepare(tools, options.discovery?.catalogBudget) diff --git a/packages/codemode/src/interpreter/errors.ts b/packages/codemode/src/interpreter/errors.ts deleted file mode 100644 index 58ddb8febd..0000000000 --- a/packages/codemode/src/interpreter/errors.ts +++ /dev/null @@ -1,93 +0,0 @@ -import type { Diagnostic } from "../codemode.js" -import { ToolError } from "../tool-error.js" -import { copyOut, ToolRuntimeError, type SafeObject } from "../tool-runtime.js" -import { type AstNode, formatLocation, InterpreterRuntimeError, ProgramThrow, sourceLocation } from "./model.js" -import { containsRuntimeReference } from "./references.js" -import { spreadItems } from "../stdlib/collections.js" -import { coerceToString, createAggregateErrorValue, createErrorValue, errorConstructors } from "../stdlib/value.js" - -export const normalizeError = (error: unknown): Diagnostic => { - if (error instanceof InterpreterRuntimeError) { - return { - kind: error.kind, - message: `${error.message}${formatLocation(error.node)}`, - ...(error.node?.loc ? { location: sourceLocation(error.node) } : {}), - ...(error.suggestions ? { suggestions: error.suggestions } : {}), - } - } - - if (error instanceof ToolRuntimeError) { - return { - kind: error.kind, - message: error.message, - ...(error.suggestions.length > 0 ? { suggestions: error.suggestions } : {}), - } - } - - if (error instanceof ToolError) { - return { kind: "ToolFailure", message: error.message } - } - - if (error instanceof ProgramThrow) { - const value = error.value - let message: string - if (containsRuntimeReference(value)) { - // Never expose runtime reference internals through thrown values. - message = "a non-data value" - } else if (typeof value === "string") { - message = value - } else if ( - value !== null && - typeof value === "object" && - typeof (value as { message?: unknown }).message === "string" - ) { - message = (value as { message: string }).message - } else { - try { - message = JSON.stringify(copyOut(value)) ?? String(value) - } catch { - message = String(value) - } - } - return { kind: "ExecutionFailure", message: `Uncaught: ${message}` } - } - - if (error instanceof RangeError && /call stack|recursion/i.test(error.message)) { - return { - kind: "ExecutionFailure", - message: "Execution exceeded the maximum nesting depth.", - } - } - - if (error instanceof Error) { - return { - kind: error.name === "SyntaxError" ? "ParseError" : "ExecutionFailure", - message: error.message, - } - } - - return { - kind: "ExecutionFailure", - message: String(error), - } -} - -export const caughtErrorValue = (thrown: unknown): unknown => { - if (thrown instanceof ProgramThrow) return thrown.value - if (thrown instanceof InterpreterRuntimeError) return createErrorValue(thrown.errorName, thrown.message) - const name = thrown instanceof Error && errorConstructors.has(thrown.name) ? thrown.name : "Error" - return createErrorValue(name, normalizeError(thrown).message) -} - -export const constructErrorValue = (name: string, args: Array, node: AstNode): SafeObject => { - if (name !== "AggregateError") return createErrorValue(name, args[0] === undefined ? "" : coerceToString(args[0])) - const errors = spreadItems(args[0]) - if (errors === undefined) { - throw new InterpreterRuntimeError( - "new AggregateError(...) expects an array of errors (e.g. new AggregateError(errors, message?)).", - node, - ).as("TypeError") - } - // Error values must not alias caller-owned arrays. - return createAggregateErrorValue([...errors], args[1] === undefined ? "" : coerceToString(args[1])) -} diff --git a/packages/codemode/src/interpreter/execute.ts b/packages/codemode/src/interpreter/execute.ts deleted file mode 100644 index 1e81a0bfca..0000000000 --- a/packages/codemode/src/interpreter/execute.ts +++ /dev/null @@ -1,224 +0,0 @@ -import { parse } from "acorn" -import { Cause, Effect, Scope } from "effect" -import { DiagnosticCategory, ModuleKind, ScriptTarget, flattenDiagnosticMessageText, transpileModule } from "typescript" -import type { DataValue, Diagnostic, ExecuteOptions, ResolvedExecutionLimits, Result } from "../codemode.js" -import { copyIn, copyOut, ToolRuntime, type HostTools, type Services } from "../tool-runtime.js" -import { normalizeError } from "./errors.js" -import { InterpreterRuntimeError, isRecord, type ProgramNode } from "./model.js" -import { PromiseRuntime } from "./promises.js" -import { Interpreter } from "./runtime.js" - -export const executeWithLimits = >( - options: ExecuteOptions, - limits: ResolvedExecutionLimits, - searchIndex: ToolRuntime.DiscoveryPlan["searchIndex"], -): Effect.Effect> => { - if (options.code.trim().length === 0) { - return Effect.succeed({ - ok: false, - error: { kind: "ParseError", message: "Code cannot be empty." }, - toolCalls: [], - }) - } - - // Allocate execution state inside suspension so reused Effects never share it. - return Effect.suspend(() => { - const tools = ToolRuntime.make( - (options.tools ?? {}) as HostTools>, - limits.maxToolCalls, - searchIndex, - { - onToolCallStart: options.onToolCallStart, - onToolCallEnd: options.onToolCallEnd, - }, - ) - const logs: Array = [] - const logged = () => (logs.length > 0 ? { logs: [...logs] } : {}) - // Set only after copy-out so timeouts cannot report invalid values as completed. - let returned: { value: DataValue; promises: PromiseRuntime> } | undefined - - const base = Effect.acquireUseRelease( - Scope.make("parallel"), - (scope) => - Effect.gen(function* () { - const program = parseProgram(options.code) - const promises = new PromiseRuntime>(scope) - const interpreter = new Interpreter>(tools.invoke, tools.search, tools.keys, promises, logs) - const value = yield* interpreter.run(program) - const result = copyOut(copyIn(value, "Execution result"), true) as DataValue - returned = { value: result, promises } - const warnings = yield* promises.interrupt() - return { - ok: true, - value: result, - ...(warnings.length > 0 ? { warnings } : {}), - ...logged(), - toolCalls: tools.calls, - } satisfies Result - }), - (scope, exit) => Scope.close(scope, exit), - ) - const timeoutMs = limits.timeoutMs - const operation = - timeoutMs === undefined - ? base - : base.pipe( - Effect.timeoutOrElse({ - duration: timeoutMs, - orElse: () => - Effect.sync(() => { - if (returned === undefined) { - return { - ok: false, - error: { kind: "TimeoutExceeded", message: `Execution timed out after ${timeoutMs}ms.` }, - ...logged(), - toolCalls: tools.calls, - } satisfies Result - } - // Keep the timeout warning first so truncation preserves it. - return { - ok: true, - value: returned.value, - warnings: [ - { - kind: "TimeoutExceeded", - message: `The program returned, but background work was still running at the ${timeoutMs}ms timeout and was interrupted. Await all started promises.`, - }, - ...returned.promises.diagnostics(), - ], - ...logged(), - toolCalls: tools.calls, - } satisfies Result - }), - }), - ) - - return operation.pipe( - Effect.catchCause((cause) => - Cause.hasInterruptsOnly(cause) - ? Effect.interrupt - : Effect.succeed({ - ok: false, - error: normalizeError(Cause.squash(cause)), - ...logged(), - toolCalls: tools.calls, - } satisfies Result), - ), - Effect.map((result) => - limits.maxOutputBytes === undefined ? result : boundOutput(result, limits.maxOutputBytes), - ), - ) - }) -} - -const parseProgram = (code: string): ProgramNode => { - const transpiled = transpileModule(`async function __codemode__() {\n${code}\n}`, { - reportDiagnostics: true, - compilerOptions: { - target: ScriptTarget.ESNext, - module: ModuleKind.ESNext, - }, - }) - const diagnostic = transpiled.diagnostics?.find((item) => item.category === DiagnosticCategory.Error) - - if (diagnostic) { - throw new InterpreterRuntimeError( - `Failed to parse TypeScript: ${flattenDiagnosticMessageText(diagnostic.messageText, "\n")}`, - undefined, - "ParseError", - ) - } - - const bodyStart = transpiled.outputText.indexOf("{") + 1 - const bodyEnd = transpiled.outputText.lastIndexOf("}") - const executableCode = transpiled.outputText.slice(bodyStart, bodyEnd) - const parsed = parse(executableCode, { - ecmaVersion: "latest", - sourceType: "script", - allowReturnOutsideFunction: true, - allowAwaitOutsideFunction: true, - locations: true, - }) as unknown - - if (!isRecord(parsed) || parsed.type !== "Program" || !Array.isArray(parsed.body)) { - throw new InterpreterRuntimeError("Failed to parse script as a Program node.") - } - - return parsed as ProgramNode -} - -const utf8ByteLength = (value: string): number => new TextEncoder().encode(value).byteLength - -// Drop a replacement character produced by truncating inside a UTF-8 sequence. -const utf8Truncate = (value: string, maxBytes: number): string => { - const bytes = new TextEncoder().encode(value) - if (bytes.byteLength <= maxBytes) return value - const text = new TextDecoder("utf-8").decode(bytes.slice(0, Math.max(0, maxBytes))) - return text.endsWith("\uFFFD") ? text.slice(0, -1) : text -} - -// Warnings have a separate budget so result data cannot starve diagnostics. -const boundOutput = (result: Result, maxOutputBytes: number): Result => { - let truncated = false - - let value: DataValue = null - let valueBytes = 0 - if (result.ok) { - const serialized = JSON.stringify(result.value) ?? "null" - const bytes = utf8ByteLength(serialized) - if (bytes > maxOutputBytes) { - truncated = true - value = `${utf8Truncate(serialized, maxOutputBytes)} [result truncated: ${bytes} bytes exceeds the ${maxOutputBytes}-byte output limit; return a smaller value]` - valueBytes = maxOutputBytes - } else { - value = result.value - valueBytes = bytes - } - } - - const warnings = result.ok ? (result.warnings ?? []) : [] - const keptWarnings: Array = [] - let warningBytes = 0 - for (const warning of warnings) { - const bytes = utf8ByteLength(JSON.stringify(warning)) + 1 - if (warningBytes + bytes > maxOutputBytes) break - warningBytes += bytes - keptWarnings.push(warning) - } - if (keptWarnings.length < warnings.length) { - truncated = true - keptWarnings.push({ - kind: "Truncated", - message: `${warnings.length - keptWarnings.length} additional warnings omitted by the output limit.`, - }) - } - - const logs = result.logs ?? [] - const kept: Array = [] - const logBudget = Math.max(0, maxOutputBytes - valueBytes) - let logBytes = 0 - for (const line of logs) { - const lineBytes = utf8ByteLength(line) + 1 - if (logBytes + lineBytes > logBudget) break - logBytes += lineBytes - kept.push(line) - } - if (kept.length < logs.length) { - truncated = true - kept.push(`[logs truncated: showing ${kept.length} of ${logs.length} lines]`) - } - - if (!truncated) return result - const warningsPart = keptWarnings.length > 0 ? { warnings: keptWarnings } : {} - const logsPart = kept.length > 0 ? { logs: kept } : {} - return result.ok - ? { - ok: true, - value, - ...warningsPart, - ...logsPart, - truncated: true, - toolCalls: result.toolCalls, - } - : { ok: false, error: result.error, ...logsPart, truncated: true, toolCalls: result.toolCalls } -} diff --git a/packages/codemode/src/interpreter/methods.ts b/packages/codemode/src/interpreter/methods.ts deleted file mode 100644 index 365a077837..0000000000 --- a/packages/codemode/src/interpreter/methods.ts +++ /dev/null @@ -1,819 +0,0 @@ -import { Effect } from "effect" -import { - type AstNode, - CodeModeFunction, - CoercionFunction, - GlobalMethodReference, - IntrinsicReference, - InterpreterRuntimeError, - PromiseCapabilityFunction, - supportedSyntaxMessage, - UriFunction, -} from "./model.js" -import { rejectCircularInsertion } from "./references.js" -import { isBlockedMember, type SafeObject } from "../tool-runtime.js" -import { - SandboxDate, - SandboxMap, - SandboxPromise, - SandboxRegExp, - SandboxSet, - SandboxURL, - SandboxURLSearchParams, -} from "../values.js" -import { invokeDateMethod, invokeDateStatic } from "../stdlib/date.js" -import { invokeJsonMethod } from "../stdlib/json.js" -import { invokeMathMethod } from "../stdlib/math.js" -import { invokeNumberMethod, invokeNumberStatic } from "../stdlib/number.js" -import { invokeObjectMethod } from "../stdlib/object.js" -import { invokeRegExpMethod, matchToValue, toHostRegex } from "../stdlib/regexp.js" -import { invokeStringStatic } from "../stdlib/string.js" -import { invokeUriFunction, invokeURLMethod, invokeURLStatic, uriArgument } from "../stdlib/url.js" -import { boundedData, coerceToNumber, coerceToString, invokeCoercion } from "../stdlib/value.js" - -export type CallbackRunner = { - readonly invokeFunction: (fn: CodeModeFunction, args: Array) => Effect.Effect - readonly settlePromise: (promise: SandboxPromise) => Effect.Effect -} - -export const invokeIntrinsic = ( - runner: CallbackRunner, - ref: IntrinsicReference, - args: Array, - node: AstNode, -): Effect.Effect => { - if (typeof ref.receiver === "string") { - if ( - (ref.name === "replace" || ref.name === "replaceAll") && - (args[1] instanceof CodeModeFunction || args[1] instanceof CoercionFunction || args[1] instanceof UriFunction) - ) { - return invokeStringReplacer(runner, ref.receiver, ref.name, args, node) - } - return Effect.succeed(invokeStringMethod(ref.receiver, ref.name, args, node)) - } - if (typeof ref.receiver === "number") { - return Effect.succeed(invokeNumberMethod(ref.receiver, ref.name, args, node)) - } - if (Array.isArray(ref.receiver)) { - return invokeArrayMethod(runner, ref.receiver, ref.name, args, node) - } - if (ref.receiver instanceof SandboxDate) { - return Effect.succeed(invokeDateMethod(ref.receiver, ref.name, node)) - } - if (ref.receiver instanceof SandboxRegExp) { - return Effect.succeed(invokeRegExpMethod(ref.receiver, ref.name, args, node)) - } - if (ref.receiver instanceof SandboxMap) { - return invokeMapMethod(runner, ref.receiver, ref.name, args, node) - } - if (ref.receiver instanceof SandboxSet) { - return invokeSetMethod(runner, ref.receiver, ref.name, args, node) - } - if (ref.receiver instanceof SandboxURL) { - return Effect.succeed(invokeURLMethod(ref.receiver, ref.name, node)) - } - if (ref.receiver instanceof SandboxURLSearchParams) { - return invokeURLSearchParamsMethod(runner, ref.receiver, ref.name, args, node) - } - throw new InterpreterRuntimeError(`Method '${ref.name}' is not available in CodeMode.`, node) -} - -export const invokeGlobalMethod = (ref: GlobalMethodReference, args: Array, node: AstNode): unknown => { - if (ref.namespace === "console") - throw new InterpreterRuntimeError(`console.${ref.name} is not available in CodeMode.`, node) - if (ref.namespace === "Object") return invokeObjectMethod(ref.name, args, node) - if (ref.namespace === "Math") return invokeMathMethod(ref.name, args, node) - if (ref.namespace === "Array") return invokeArrayStatic(ref.name, args, node) - if (ref.namespace === "Number") return invokeNumberStatic(ref.name, args, node) - if (ref.namespace === "String") return invokeStringStatic(ref.name, args, node) - if (ref.namespace === "URL") return invokeURLStatic(ref.name, args, node) - if (ref.namespace === "Date") return invokeDateStatic(ref.name, args, node) - if ( - ref.namespace === "RegExp" || - ref.namespace === "Map" || - ref.namespace === "Set" || - ref.namespace === "URLSearchParams" - ) { - throw new InterpreterRuntimeError(`${ref.namespace}.${ref.name} is not available in CodeMode.`, node) - } - return invokeJsonMethod(ref.name, args, node) -} - -const invokeStringMethod = (value: string, name: string, args: Array, node: AstNode): unknown => { - const str = (index: number): string => { - const arg = args[index] - if (typeof arg !== "string") - throw new InterpreterRuntimeError(`String.${name} expects argument ${index + 1} to be a string.`, node) - return arg - } - const num = (index: number): number => { - const arg = args[index] - if (typeof arg !== "number") - throw new InterpreterRuntimeError(`String.${name} expects argument ${index + 1} to be a number.`, node) - return arg - } - const optNum = (index: number): number | undefined => (args[index] === undefined ? undefined : num(index)) - const optStr = (index: number): string | undefined => (args[index] === undefined ? undefined : str(index)) - - let result: unknown - switch (name) { - case "toLowerCase": - result = value.toLowerCase() - break - case "toUpperCase": - result = value.toUpperCase() - break - case "trim": - result = value.trim() - break - case "trimStart": - result = value.trimStart() - break - case "trimEnd": - result = value.trimEnd() - break - // Locale/options are deliberately unsupported; comparison uses the host default locale. - case "localeCompare": - result = value.localeCompare(str(0)) - break - case "normalize": { - const form = optStr(0) - try { - result = value.normalize(form) - } catch { - throw new InterpreterRuntimeError( - `String.normalize expects the form "NFC", "NFD", "NFKC", or "NFKD" (got ${JSON.stringify(form)}).`, - node, - ).as("RangeError") - } - break - } - case "split": { - if (args.length === 0) { - result = [value] - break - } - if (args[0] instanceof SandboxRegExp) { - result = value.split(args[0].regex, optNum(1)) - break - } - const requestedLimit = optNum(1) - result = value.split(str(0), requestedLimit === undefined ? undefined : requestedLimit >>> 0) - break - } - case "slice": - result = value.slice(optNum(0), optNum(1)) - break - case "includes": - result = value.includes(str(0), optNum(1)) - break - case "startsWith": - result = value.startsWith(str(0), optNum(1)) - break - case "endsWith": - result = value.endsWith(str(0), optNum(1)) - break - case "indexOf": - result = value.indexOf(str(0), optNum(1)) - break - case "lastIndexOf": - result = value.lastIndexOf(str(0), optNum(1)) - break - case "replace": - case "replaceAll": { - if (args[0] instanceof SandboxRegExp) { - const pattern = args[0].regex - const replacement = str(1) - if (name === "replaceAll" && !pattern.global) { - throw new InterpreterRuntimeError( - `String.replaceAll requires a regular expression with the global (g) flag: write /${pattern.source}/${pattern.flags}g, or use String.replace to replace only the first match.`, - node, - ) - } - result = name === "replace" ? value.replace(pattern, replacement) : value.replaceAll(pattern, replacement) - break - } - if (name === "replace") { - result = value.replace(str(0), str(1)) - break - } - result = value.replaceAll(str(0), str(1)) - break - } - case "match": { - const pattern = toHostRegex(args[0], name, node) - const matched = value.match(pattern) - if (matched === null) return null - // Preserve the own `index` and `groups` properties on non-global matches. - if (pattern.global) return boundedData(matched, "String.match result") - return matchToValue(matched) - } - case "matchAll": { - const pattern = toHostRegex(args[0], name, node, "g") - if (!pattern.global) { - throw new InterpreterRuntimeError( - `String.matchAll requires a regular expression with the global (g) flag: write /${pattern.source}/${pattern.flags}g, or use String.match for a single match.`, - node, - ) - } - return Array.from(value.matchAll(pattern), matchToValue) - } - case "search": { - result = value.search(toHostRegex(args[0], name, node)) - break - } - case "repeat": { - const count = num(0) - if (!Number.isFinite(count) || count < 0) - throw new InterpreterRuntimeError("String.repeat expects a finite non-negative count.", node) - result = value.repeat(count) - break - } - case "padStart": - result = value.padStart(num(0), optStr(1)) - break - case "padEnd": - result = value.padEnd(num(0), optStr(1)) - break - case "charAt": - result = value.charAt(optNum(0) ?? 0) - break - case "at": - result = value.at(optNum(0) ?? 0) - break - case "substring": - result = value.substring(optNum(0) ?? 0, optNum(1)) - break - case "charCodeAt": - result = value.charCodeAt(optNum(0) ?? 0) - break - case "codePointAt": - result = value.codePointAt(optNum(0) ?? 0) - break - case "toString": - result = value - break - case "concat": { - result = value.concat(...args.map((_, index) => str(index))) - break - } - default: - throw new InterpreterRuntimeError(`String method '${name}' is not available in CodeMode.`, node) - } - return boundedData(result, `String.${name} result`) -} - -const invokeArrayStatic = (name: string, args: Array, node: AstNode): unknown => { - switch (name) { - case "isArray": - return Array.isArray(args[0]) - case "of": - return [...args] - case "from": { - if (args.length > 1) { - throw new InterpreterRuntimeError( - "Array.from(...) does not support a map function in CodeMode; call .map() on the result instead.", - node, - "UnsupportedSyntax", - [supportedSyntaxMessage], - ) - } - if (args[0] instanceof SandboxMap) return Array.from(args[0].map.entries(), ([key, item]) => [key, item]) - if (args[0] instanceof SandboxSet) return Array.from(args[0].set.values()) - if (args[0] instanceof SandboxURLSearchParams) { - return Array.from(args[0].params.entries(), ([key, value]) => [key, value]) - } - const source = args[0] - if (source instanceof SandboxPromise) { - throw new InterpreterRuntimeError( - "Array.from received an un-awaited Promise; await it before creating the array.", - node, - "InvalidDataValue", - ) - } - if (typeof source === "string") return Array.from(source) - if (Array.isArray(source)) return [...source] - if ( - source !== null && - typeof source === "object" && - (Object.getPrototypeOf(source) === Object.prototype || Object.getPrototypeOf(source) === null) && - typeof (source as { length?: unknown }).length === "number" - ) { - return Array.from(source as ArrayLike) - } - throw new InterpreterRuntimeError( - "Array.from expects an array, string, Map, Set, or array-like value.", - node, - "InvalidDataValue", - ) - } - default: - throw new InterpreterRuntimeError(`Array.${name} is not available in CodeMode.`, node) - } -} - -const invokeStringReplacer = ( - runner: CallbackRunner, - value: string, - name: "replace" | "replaceAll", - args: Array, - node: AstNode, -): Effect.Effect => { - const apply = applyCollectionCallback(runner, args[1], `String.${name}`, node) - const matches: Array<{ readonly match: string; readonly offset: number; readonly args: Array }> = [] - const collect = (...callbackArgs: Array): string => { - const match = callbackArgs[0] - const groups = callbackArgs[callbackArgs.length - 1] - const hasGroups = groups !== null && typeof groups === "object" - const offset = callbackArgs[callbackArgs.length - (hasGroups ? 3 : 2)] - if (typeof match !== "string" || typeof offset !== "number") { - throw new InterpreterRuntimeError(`String.${name} produced an invalid replacement match.`, node) - } - if (hasGroups) { - const safeGroups: SafeObject = Object.create(null) as SafeObject - for (const [key, group] of Object.entries(groups)) { - if (!isBlockedMember(key)) safeGroups[key] = group - } - callbackArgs[callbackArgs.length - 1] = safeGroups - } - matches.push({ match, offset, args: callbackArgs }) - return match - } - - const pattern = args[0] - if (pattern instanceof SandboxRegExp) { - if (name === "replaceAll" && !pattern.regex.global) { - throw new InterpreterRuntimeError( - `String.replaceAll requires a regular expression with the global (g) flag: write /${pattern.regex.source}/${pattern.regex.flags}g, or use String.replace to replace only the first match.`, - node, - ) - } - if (name === "replace") value.replace(pattern.regex, collect) - else value.replaceAll(pattern.regex, collect) - } else { - if (typeof pattern !== "string") { - throw new InterpreterRuntimeError(`String.${name} expects argument 1 to be a string.`, node) - } - if (name === "replace") value.replace(pattern, collect) - else value.replaceAll(pattern, collect) - } - - return Effect.gen(function* () { - const output: Array = [] - let end = 0 - for (const match of matches) { - const replacement = yield* apply(match.args) - const resolved = - args[1] instanceof CodeModeFunction && args[1].async && replacement instanceof SandboxPromise - ? yield* runner.settlePromise(replacement) - : replacement - output.push( - value.slice(end, match.offset), - coerceToString(boundedData(resolved, `String.${name} replacer result`)), - ) - end = match.offset + match.match.length - } - output.push(value.slice(end)) - return boundedData(output.join(""), `String.${name} result`) - }) -} - -export const applyCollectionCallback = ( - runner: CallbackRunner, - callback: unknown, - name: string, - node: AstNode, -): ((args: Array) => Effect.Effect) => { - if ( - !(callback instanceof CodeModeFunction) && - !(callback instanceof CoercionFunction) && - !(callback instanceof UriFunction) && - !(callback instanceof PromiseCapabilityFunction) - ) { - throw new InterpreterRuntimeError(`${name} expects a function callback.`, node) - } - return (callbackArgs) => - callback instanceof CoercionFunction - ? Effect.succeed(invokeCoercion(callback, callbackArgs, node)) - : callback instanceof UriFunction - ? Effect.succeed(invokeUriFunction(callback, callbackArgs, node)) - : callback instanceof PromiseCapabilityFunction - ? Effect.sync(() => callback.settle(callbackArgs[0])) - : runner.invokeFunction(callback, callbackArgs) -} - -const invokeMapMethod = ( - runner: CallbackRunner, - target: SandboxMap, - name: string, - args: Array, - node: AstNode, -): Effect.Effect => { - switch (name) { - case "get": - return Effect.succeed(target.map.get(args[0])) - case "has": - return Effect.succeed(target.map.has(args[0])) - case "set": - return Effect.sync(() => { - target.map.set(args[0], args[1]) - return target - }) - case "delete": - return Effect.sync(() => target.map.delete(args[0])) - case "clear": - return Effect.sync(() => { - target.map.clear() - return undefined - }) - case "keys": - return Effect.sync(() => Array.from(target.map.keys())) - case "values": - return Effect.sync(() => Array.from(target.map.values())) - case "entries": - return Effect.sync(() => Array.from(target.map.entries(), ([key, item]): Array => [key, item])) - case "forEach": { - const apply = applyCollectionCallback(runner, args[0], "Map.forEach", node) - return Effect.gen(function* () { - for (const [key, item] of Array.from(target.map.entries())) yield* apply([item, key, target]) - return undefined - }) - } - default: - throw new InterpreterRuntimeError(`Map method '${name}' is not available in CodeMode.`, node) - } -} - -const invokeSetMethod = ( - runner: CallbackRunner, - target: SandboxSet, - name: string, - args: Array, - node: AstNode, -): Effect.Effect => { - switch (name) { - case "has": - return Effect.succeed(target.set.has(args[0])) - case "add": - return Effect.sync(() => { - target.set.add(args[0]) - return target - }) - case "delete": - return Effect.sync(() => target.set.delete(args[0])) - case "clear": - return Effect.sync(() => { - target.set.clear() - return undefined - }) - case "keys": - case "values": - return Effect.sync(() => Array.from(target.set.values())) - case "entries": - return Effect.sync(() => Array.from(target.set.values(), (item): Array => [item, item])) - case "forEach": { - const apply = applyCollectionCallback(runner, args[0], "Set.forEach", node) - return Effect.gen(function* () { - for (const item of Array.from(target.set.values())) yield* apply([item, item, target]) - return undefined - }) - } - default: - throw new InterpreterRuntimeError(`Set method '${name}' is not available in CodeMode.`, node) - } -} - -const invokeURLSearchParamsMethod = ( - runner: CallbackRunner, - target: SandboxURLSearchParams, - name: string, - args: Array, - node: AstNode, -): Effect.Effect => { - const arg = (index: number): string => uriArgument(args[index], `URLSearchParams.${name} argument ${index + 1}`) - const requireArgs = (count: number): void => { - if (args.length < count) { - throw new InterpreterRuntimeError( - `URLSearchParams.${name} requires ${count} argument${count === 1 ? "" : "s"}.`, - node, - ).as("TypeError") - } - } - switch (name) { - case "append": { - requireArgs(2) - return Effect.sync(() => { - target.params.append(arg(0), arg(1)) - return undefined - }) - } - case "delete": { - requireArgs(1) - return Effect.sync(() => { - if (args[1] !== undefined) target.params.delete(arg(0), arg(1)) - else target.params.delete(arg(0)) - return undefined - }) - } - case "get": - requireArgs(1) - return Effect.sync(() => target.params.get(arg(0))) - case "getAll": - requireArgs(1) - return Effect.sync(() => target.params.getAll(arg(0))) - case "has": - requireArgs(1) - return Effect.sync(() => (args[1] !== undefined ? target.params.has(arg(0), arg(1)) : target.params.has(arg(0)))) - case "set": { - requireArgs(2) - return Effect.sync(() => { - target.params.set(arg(0), arg(1)) - return undefined - }) - } - case "sort": - return Effect.sync(() => { - target.params.sort() - return undefined - }) - case "keys": - return Effect.sync(() => Array.from(target.params.keys())) - case "values": - return Effect.sync(() => Array.from(target.params.values())) - case "entries": - return Effect.sync(() => Array.from(target.params.entries(), ([key, value]): Array => [key, value])) - case "toString": - return Effect.sync(() => target.params.toString()) - case "forEach": { - requireArgs(1) - const apply = applyCollectionCallback(runner, args[0], "URLSearchParams.forEach", node) - return Effect.gen(function* () { - for (const [key, value] of Array.from(target.params.entries())) yield* apply([value, key, target]) - return undefined - }) - } - default: - throw new InterpreterRuntimeError(`URLSearchParams method '${name}' is not available in CodeMode.`, node) - } -} - -const invokeArrayMethod = ( - runner: CallbackRunner, - target: Array, - name: string, - args: Array, - node: AstNode, -): Effect.Effect => { - const optNumber = (value: unknown, label: string): number | undefined => { - if (value === undefined) return undefined - if (typeof value !== "number") - throw new InterpreterRuntimeError(`Array.${name} expects ${label} to be a number.`, node) - return value - } - switch (name) { - case "join": { - if (args.length > 1 || (args.length === 1 && typeof args[0] !== "string")) { - throw new InterpreterRuntimeError("Array.join expects zero arguments or one string separator.", node) - } - const input = boundedData(target, "Array.join input") as Array - return Effect.succeed( - input.map((item) => coerceToString(item ?? "")).join(args.length === 0 ? "," : (args[0] as string)), - ) - } - case "includes": - if (args.length === 0 || args.length > 2) - throw new InterpreterRuntimeError("Array.includes expects a value and optional start index.", node) - return Effect.succeed(target.includes(args[0], optNumber(args[1], "start index"))) - case "indexOf": - return Effect.succeed(target.indexOf(args[0], optNumber(args[1], "start index"))) - case "lastIndexOf": - return Effect.succeed( - args[1] === undefined - ? target.lastIndexOf(args[0]) - : target.lastIndexOf(args[0], optNumber(args[1], "start index")), - ) - case "at": - return Effect.succeed(target.at(optNumber(args[0], "index") ?? 0)) - case "slice": - return Effect.succeed(target.slice(optNumber(args[0], "start"), optNumber(args[1], "end"))) - case "concat": - return Effect.succeed(target.concat(...args)) - case "flat": - return Effect.succeed(target.flat(optNumber(args[0], "depth") ?? 1)) - case "reverse": - return Effect.succeed(target.reverse()) - case "sort": - return Effect.map(sortArray(runner, target, args[0], node), (sorted) => { - target.splice(0, target.length, ...sorted) - return target - }) - case "toSorted": - return sortArray(runner, target, args[0], node) - case "toReversed": - return Effect.succeed([...target].reverse()) - case "with": { - const index = optNumber(args[0], "index") ?? 0 - const resolved = index < 0 ? target.length + index : index - if (resolved < 0 || resolved >= target.length) { - throw new InterpreterRuntimeError("Array.with index is out of range.", node) - } - const copied = [...target] - copied[resolved] = args[1] - return Effect.succeed(copied) - } - case "push": { - // Validate all insertions before mutating to avoid partial cyclic updates. - for (const item of args) rejectCircularInsertion(target, item, "Array.push result", node) - target.push(...args) - return Effect.succeed(target.length) - } - case "unshift": { - for (const item of args) rejectCircularInsertion(target, item, "Array.unshift result", node) - target.unshift(...args) - return Effect.succeed(target.length) - } - case "pop": - return Effect.succeed(target.pop()) - case "shift": - return Effect.succeed(target.shift()) - case "splice": { - if (args.length === 0) return Effect.succeed(target.splice(0, 0)) - const start = optNumber(args[0], "start") ?? 0 - if (args.length === 1) return Effect.succeed(target.splice(start)) - const deleteCount = optNumber(args[1], "delete count") ?? 0 - const inserted = args.slice(2) - for (const item of inserted) rejectCircularInsertion(target, item, "Array.splice result", node) - return Effect.succeed(target.splice(start, deleteCount, ...inserted)) - } - case "fill": { - rejectCircularInsertion(target, args[0], "Array.fill result", node) - return Effect.succeed(target.fill(args[0], optNumber(args[1], "start"), optNumber(args[2], "end"))) - } - case "copyWithin": - return Effect.succeed( - target.copyWithin( - optNumber(args[0], "target index") ?? 0, - optNumber(args[1], "start") ?? 0, - optNumber(args[2], "end"), - ), - ) - case "keys": - return Effect.succeed(Array.from(target.keys())) - case "values": - return Effect.succeed([...target]) - case "entries": - return Effect.succeed(Array.from(target.entries(), ([index, item]): Array => [index, item])) - } - - const apply = applyCollectionCallback(runner, args[0], `Array.${name}`, node) - return Effect.gen(function* () { - // Fix iteration length while reading existing elements live. - const length = target.length - switch (name) { - case "map": { - const values: Array = [] - values.length = length - for (let index = 0; index < length; index += 1) { - if (!(index in target)) continue - values[index] = yield* apply([target[index], index, target]) - } - return values - } - case "flatMap": { - const values: Array = [] - for (let index = 0; index < length; index += 1) { - if (!(index in target)) continue - const mapped = yield* apply([target[index], index, target]) - if (Array.isArray(mapped)) values.push(...mapped) - else values.push(mapped) - } - return values - } - case "filter": { - const values: Array = [] - for (let index = 0; index < length; index += 1) { - if (!(index in target)) continue - const item = target[index] - if (yield* apply([item, index, target])) values.push(item) - } - return values - } - case "find": - for (let index = 0; index < length; index += 1) { - const item = target[index] - if (yield* apply([item, index, target])) return item - } - return undefined - case "findIndex": - for (let index = 0; index < length; index += 1) { - if (yield* apply([target[index], index, target])) return index - } - return -1 - case "some": - for (let index = 0; index < length; index += 1) { - if (!(index in target)) continue - if (yield* apply([target[index], index, target])) return true - } - return false - case "every": - for (let index = 0; index < length; index += 1) { - if (!(index in target)) continue - if (!(yield* apply([target[index], index, target]))) return false - } - return true - case "forEach": - for (let index = 0; index < length; index += 1) { - if (index in target) yield* apply([target[index], index, target]) - } - return undefined - case "reduce": { - let accumulator: unknown - let start: number - if (args.length >= 2) { - accumulator = args[1] - start = 0 - } else { - if (length === 0) - throw new InterpreterRuntimeError("Array.reduce of an empty array with no initial value.", node) - accumulator = target[0] - start = 1 - } - for (let index = start; index < length; index += 1) { - if (!(index in target)) continue - accumulator = yield* apply([accumulator, target[index], index, target]) - } - return accumulator - } - case "reduceRight": { - let accumulator: unknown - let start: number - if (args.length >= 2) { - accumulator = args[1] - start = length - 1 - } else { - if (length === 0) - throw new InterpreterRuntimeError("Array.reduceRight of an empty array with no initial value.", node) - accumulator = target[length - 1] - start = length - 2 - } - for (let index = start; index >= 0; index -= 1) { - if (!(index in target)) continue - accumulator = yield* apply([accumulator, target[index], index, target]) - } - return accumulator - } - case "findLast": - for (let index = length - 1; index >= 0; index -= 1) { - if (yield* apply([target[index], index, target])) return target[index] - } - return undefined - case "findLastIndex": - for (let index = length - 1; index >= 0; index -= 1) { - if (yield* apply([target[index], index, target])) return index - } - return -1 - } - throw new InterpreterRuntimeError(`Array method '${name}' is not available in CodeMode.`, node) - }) -} - -const sortArray = ( - runner: CallbackRunner, - target: Array, - comparator: unknown, - node: AstNode, -): Effect.Effect, unknown, R> => { - if (comparator !== undefined && !(comparator instanceof CodeModeFunction)) { - throw new InterpreterRuntimeError("Array.sort expects an arrow function comparator.", node) - } - if (!(comparator instanceof CodeModeFunction)) { - return Effect.sync(() => - [...target].sort((a, b) => { - const left = coerceToString(a) - const right = coerceToString(b) - return left < right ? -1 : left > right ? 1 : 0 - }), - ) - } - const mergeSort = (items: Array): Effect.Effect, unknown, R> => { - if (items.length <= 1) return Effect.succeed(items) - const midpoint = Math.floor(items.length / 2) - return Effect.gen(function* () { - const left = yield* mergeSort(items.slice(0, midpoint)) - const right = yield* mergeSort(items.slice(midpoint)) - const merged: Array = [] - let leftIndex = 0 - let rightIndex = 0 - while (leftIndex < left.length && rightIndex < right.length) { - // Treat a NaN comparator result as equal to preserve stable ordering. - const order = coerceToNumber(yield* runner.invokeFunction(comparator, [left[leftIndex], right[rightIndex]])) - if (Number.isNaN(order) || order <= 0) merged.push(left[leftIndex++]) - else merged.push(right[rightIndex++]) - } - return [...merged, ...left.slice(leftIndex), ...right.slice(rightIndex)] - }) - } - const defined = target.filter((item) => item !== undefined) - const undefinedCount = target.length - defined.length - return Effect.map(mergeSort(defined), (items) => [...items, ...Array(undefinedCount).fill(undefined)]) -} diff --git a/packages/codemode/src/interpreter/model.ts b/packages/codemode/src/interpreter/model.ts index b6e6ad64f8..d953514510 100644 --- a/packages/codemode/src/interpreter/model.ts +++ b/packages/codemode/src/interpreter/model.ts @@ -1,5 +1,5 @@ import type { SafeObject } from "../tool-runtime.js" -import type { SandboxPromise, SandboxURL } from "../values.js" +import type { SandboxURL } from "../values.js" export type SourcePosition = { line: number @@ -30,6 +30,7 @@ export type Binding = { export type StatementResult = | { kind: "none" } + | { kind: "value"; value: unknown } | { kind: "return"; value: unknown } | { kind: "break" } | { kind: "continue" } @@ -44,7 +45,6 @@ export class CodeModeFunction { readonly parameters: ReadonlyArray, readonly body: AstNode, readonly capturedScopes: ReadonlyArray>, - readonly async: boolean, ) {} } @@ -61,25 +61,12 @@ export class ComputedValue { export class PromiseNamespace {} -export type PromiseMethodName = "all" | "allSettled" | "race" | "any" | "resolve" | "reject" +export type PromiseMethodName = "all" | "allSettled" | "race" | "resolve" | "reject" export class PromiseMethodReference { constructor(readonly name: PromiseMethodName) {} } -export type PromiseInstanceMethodName = "then" | "catch" | "finally" - -export class PromiseInstanceMethodReference { - constructor( - readonly promise: SandboxPromise, - readonly name: PromiseInstanceMethodName, - ) {} -} - -export class PromiseCapabilityFunction { - constructor(readonly settle: (value: unknown) => void) {} -} - export type GlobalNamespaceName = | "Object" | "Math" @@ -112,8 +99,6 @@ export class UriFunction { constructor(readonly name: "encodeURI" | "encodeURIComponent" | "decodeURI" | "decodeURIComponent") {} } -export class SearchFunction {} - export class ProgramThrow { constructor(readonly value: unknown) {} } @@ -137,11 +122,11 @@ export type DiagnosticKind = export const OptionalShortCircuit: unique symbol = Symbol("codemode.optional-short-circuit") export const supportedSyntaxMessage = - "Supported orchestration syntax: tools.* calls (they return promises - resolve them with await), data literals, destructuring, optional chaining, template literals, conditionals, switch, loops (incl. for...of and for...in over object/array/tools keys), arrow functions, spread, try/catch, array methods (map/filter/find/findIndex/some/every/reduce/flatMap/forEach/sort/slice/concat/indexOf/lastIndexOf/at/flat/reverse/includes/join), string methods (incl. match/matchAll/replace/split with regular expressions), Date/RegExp/Map/Set/URL/URLSearchParams, URI encoding helpers, Object/Math/JSON helpers, captured console.log/warn/error/dir/table, Promise.all/allSettled/race/any/resolve/reject over arrays mixing promises and plain values for parallel tool calls, promise chaining with .then/.catch/.finally, and new Promise((resolve, reject) => ...) construction." + "Supported orchestration syntax: tools.* calls (they return promises - resolve them with await), data literals, destructuring, optional chaining, template literals, conditionals, switch, loops (incl. for...of and for...in over object/array/tools keys), arrow functions, spread, try/catch, array methods (map/filter/find/findIndex/some/every/reduce/flatMap/forEach/sort/slice/concat/indexOf/lastIndexOf/at/flat/reverse/includes/join), string methods (incl. match/matchAll/replace/split with regular expressions), Date/RegExp/Map/Set/URL/URLSearchParams, URI encoding helpers, Object/Math/JSON helpers, captured console.log/warn/error/dir/table, and Promise.all/allSettled/race/resolve/reject over arrays mixing promises and plain values for parallel tool calls (promise chaining with .then/.catch is not supported - use await with try/catch)." export class InterpreterRuntimeError extends Error { readonly node?: AstNode - errorName = "Error" + errorName: string = "Error" constructor( message: string, diff --git a/packages/codemode/src/interpreter/promises.ts b/packages/codemode/src/interpreter/promises.ts deleted file mode 100644 index 95f9b7741f..0000000000 --- a/packages/codemode/src/interpreter/promises.ts +++ /dev/null @@ -1,336 +0,0 @@ -import { Cause, Deferred, Effect, Exit, Fiber, Scope } from "effect" -import type { Diagnostic } from "../codemode.js" -import type { SafeObject } from "../tool-runtime.js" -import { - type AstNode, - CodeModeFunction, - CoercionFunction, - InterpreterRuntimeError, - ProgramThrow, - PromiseCapabilityFunction, - PromiseInstanceMethodReference, - PromiseMethodReference, - UriFunction, -} from "./model.js" -import { caughtErrorValue, normalizeError } from "./errors.js" -import { applyCollectionCallback, type CallbackRunner } from "./methods.js" -import { typeofValue } from "./references.js" -import { spreadItems } from "../stdlib/collections.js" -import { createAggregateErrorValue } from "../stdlib/value.js" -import { SandboxPromise } from "../values.js" - -// Observation only controls rejection reporting; program completion interrupts all promise work. -export class PromiseRuntime { - private readonly active = new Set() - private readonly ids = new WeakMap() - private readonly observed = new WeakSet() - private readonly failures = new Map() - private nextID = 0 - - constructor(private readonly scope: Scope.Scope) {} - - create(effect: Effect.Effect): Effect.Effect { - return Effect.suspend(() => { - // Allocate before forking so reruns get distinct IDs and diagnostics retain creation order. - const id = this.nextID++ - return Effect.map(Effect.forkIn(effect, this.scope, { startImmediately: true }), (fiber) => { - const promise = new SandboxPromise(fiber) - this.active.add(promise) - this.ids.set(promise, id) - fiber.addObserver((exit) => { - this.active.delete(promise) - if (Exit.isSuccess(exit) || Cause.hasInterruptsOnly(exit.cause) || this.observed.has(promise)) { - this.ids.delete(promise) - return - } - const failure = normalizeError(Cause.squash(exit.cause)) - this.failures.set(id, { - ...failure, - message: `Unhandled rejection from an un-awaited promise: ${failure.message}`, - }) - }) - return promise - }) - }) - } - - // Observation must be recorded when responsibility transfers, before the consumer fiber runs. - markObserved(promise: SandboxPromise): void { - this.observed.add(promise) - const id = this.ids.get(promise) - this.ids.delete(promise) - if (id !== undefined) this.failures.delete(id) - } - - await(promise: SandboxPromise): Effect.Effect> { - return Fiber.await(promise.fiber) - } - - diagnostics(): Array { - return [...this.failures].sort(([left], [right]) => left - right).map(([, failure]) => failure) - } - - // Re-check because a straggler can create promises before its interruption lands. - interrupt(): Effect.Effect> { - const self = this - return Effect.gen(function* () { - while (self.active.size > 0) { - yield* Fiber.interruptAll([...self.active].map((promise) => promise.fiber)) - } - return self.diagnostics() - }) - } -} - -export const selfResolutionError = (node?: AstNode): InterpreterRuntimeError => - new InterpreterRuntimeError("Chaining cycle detected: a promise cannot resolve with itself.", node).as("TypeError") - -export const invokePromiseMethod = ( - runner: CallbackRunner, - promises: PromiseRuntime, - ref: PromiseMethodReference, - args: Array, - node: AstNode, -): Effect.Effect => { - if (ref.name === "resolve") { - const value = args[0] - return value instanceof SandboxPromise ? Effect.succeed(value) : promises.create(Effect.succeed(value)) - } - if (ref.name === "reject") { - return promises.create(Effect.fail(new ProgramThrow(args[0]))) - } - - const spread = spreadItems(args[0]) - if (spread === undefined) { - return promises.create( - Effect.fail( - new InterpreterRuntimeError( - `Promise.${ref.name} expects an array of promises or plain values (e.g. Promise.${ref.name}(items.map((item) => tools.ns.tool(item)))).`, - node, - ).as("TypeError"), - ), - ) - } - const items = Array.from(spread) - - for (const item of items) { - if (item instanceof SandboxPromise) promises.markObserved(item) - } - - switch (ref.name) { - case "all": { - const observations = items.map((item) => - item instanceof SandboxPromise ? Effect.flatten(promises.await(item)) : Effect.succeed(item), - ) - return promises.create(settleAfterTurn(Effect.all(observations, { concurrency: "unbounded" }))) - } - case "allSettled": { - const observations = items.map((item) => - item instanceof SandboxPromise ? promises.await(item) : Effect.succeed(Exit.succeed(item)), - ) - return promises.create( - settleAfterTurn( - Effect.gen(function* () { - const outcomes: Array = [] - for (const observation of observations) { - const exit = yield* observation - if (Exit.isSuccess(exit)) { - outcomes.push( - Object.assign(Object.create(null) as SafeObject, { status: "fulfilled", value: exit.value }), - ) - continue - } - if (Cause.hasInterruptsOnly(exit.cause)) { - // Teardown interruption is not a program-level rejection. - return yield* Effect.failCause(exit.cause) - } - outcomes.push( - Object.assign(Object.create(null) as SafeObject, { - status: "rejected", - reason: caughtErrorValue(Cause.squash(exit.cause)), - }), - ) - } - return outcomes - }), - ), - ) - } - case "race": { - if (items.length === 0) { - return promises.create( - Effect.fail( - new InterpreterRuntimeError( - "Promise.race([]) would never settle; provide at least one promise or value.", - node, - ), - ), - ) - } - const observations = items.map((item) => - item instanceof SandboxPromise ? promises.await(item) : Effect.succeed(Exit.succeed(item)), - ) - return promises.create(settleAfterTurn(Effect.flatten(Effect.raceAll(observations)))) - } - case "any": { - const flipped = items.map((item) => - item instanceof SandboxPromise - ? Effect.flatMap(promises.await(item), (exit) => { - if (Exit.isSuccess(exit)) return Effect.fail(new PromiseAnyFulfilled(exit.value)) - if (Cause.hasInterruptsOnly(exit.cause)) return Effect.failCause(exit.cause) - return Effect.succeed(caughtErrorValue(Cause.squash(exit.cause))) - }) - : Effect.fail(new PromiseAnyFulfilled(item)), - ) - const body = Effect.all(flipped, { concurrency: "unbounded" }).pipe( - Effect.flatMap((reasons) => - Effect.fail(new ProgramThrow(createAggregateErrorValue(reasons, "All promises were rejected"))), - ), - Effect.catch((error) => - error instanceof PromiseAnyFulfilled ? Effect.succeed(error.value) : Effect.fail(error), - ), - ) - return promises.create(settleAfterTurn(body)) - } - } -} - -export const invokePromiseInstanceMethod = ( - runner: CallbackRunner, - promises: PromiseRuntime, - ref: PromiseInstanceMethodReference, - args: Array, - node: AstNode, -): Effect.Effect => { - const method = `Promise.prototype.${ref.name}` - promises.markObserved(ref.promise) - if (ref.name === "finally") { - return chainFinally(runner, promises, ref.promise, reactionHandler(args[0], method, node), method, node) - } - const onFulfilled = ref.name === "then" ? reactionHandler(args[0], method, node) : undefined - const onRejected = reactionHandler(ref.name === "then" ? args[1] : args[0], method, node) - return chainReaction(runner, promises, ref.promise, onFulfilled, onRejected, method, node) -} - -export const constructPromise = ( - runner: CallbackRunner, - promises: PromiseRuntime, - executor: unknown, - node: AstNode, -): Effect.Effect => { - if (!(executor instanceof CodeModeFunction)) { - throw new InterpreterRuntimeError( - "new Promise(...) expects an executor function (e.g. new Promise((resolve, reject) => { ... })).", - node, - ).as("TypeError") - } - return Effect.gen(function* () { - const deferred = Deferred.makeUnsafe() - const box: { own?: SandboxPromise } = {} - const promise = yield* promises.create( - Effect.flatMap(Deferred.await(deferred), (value) => { - if (!(value instanceof SandboxPromise)) return Effect.succeed(value) - if (value === box.own) return Effect.fail(selfResolutionError(node)) - return runner.settlePromise(value) - }), - ) - box.own = promise - const resolve = new PromiseCapabilityFunction((value) => { - Deferred.doneUnsafe(deferred, Exit.succeed(value)) - }) - const reject = new PromiseCapabilityFunction((value) => { - Deferred.doneUnsafe(deferred, Exit.fail(new ProgramThrow(value))) - }) - const executed = yield* Effect.exit(runner.invokeFunction(executor, [resolve, reject])) - if (!Exit.isSuccess(executed)) { - if (Cause.hasInterruptsOnly(executed.cause)) return yield* Effect.failCause(executed.cause) - Deferred.doneUnsafe(deferred, Exit.fail(Cause.squash(executed.cause))) - } - return promise - }) -} - -// Settle one reaction turn after the deciding member, after its existing reactions. -const settleAfterTurn = (body: Effect.Effect): Effect.Effect => - Effect.flatMap(Effect.exit(body), (exit) => Effect.andThen(Effect.yieldNow, exit)) - -class PromiseAnyFulfilled { - constructor(readonly value: unknown) {} -} - -type ReactionHandler = CodeModeFunction | CoercionFunction | UriFunction | PromiseCapabilityFunction - -const reactionHandler = (value: unknown, method: string, node: AstNode): ReactionHandler | undefined => { - if ( - value instanceof CodeModeFunction || - value instanceof CoercionFunction || - value instanceof UriFunction || - value instanceof PromiseCapabilityFunction - ) { - return value - } - if (typeofValue(value) === "function") { - throw new InterpreterRuntimeError( - `${method} handlers must be plain functions; wrap other callables in an arrow function, e.g. (value) => tools.ns.tool(value).`, - node, - ) - } - return undefined -} - -// Teardown bypasses handlers; settled reactions yield once so handlers never run inline. -const reactionExit = ( - promises: PromiseRuntime, - source: SandboxPromise, -): Effect.Effect, unknown, R> => - Effect.gen(function* () { - const exit = yield* promises.await(source) - if (!Exit.isSuccess(exit) && Cause.hasInterruptsOnly(exit.cause)) return yield* Effect.failCause(exit.cause) - yield* Effect.yieldNow - return exit - }) - -const chainReaction = ( - runner: CallbackRunner, - promises: PromiseRuntime, - source: SandboxPromise, - onFulfilled: ReactionHandler | undefined, - onRejected: ReactionHandler | undefined, - method: string, - node: AstNode, -): Effect.Effect => { - const box: { derived?: SandboxPromise } = {} - const body = Effect.gen(function* () { - const exit = yield* reactionExit(promises, source) - const handler = Exit.isSuccess(exit) ? onFulfilled : onRejected - if (handler === undefined) return yield* exit - const input = Exit.isSuccess(exit) ? exit.value : caughtErrorValue(Cause.squash(exit.cause)) - const result = yield* applyCollectionCallback(runner, handler, method, node)([input]) - if (result === box.derived) return yield* Effect.fail(selfResolutionError(node)) - if (result instanceof SandboxPromise) return yield* runner.settlePromise(result) - return result - }) - return Effect.map(promises.create(body), (derived) => { - box.derived = derived - return derived - }) -} - -const chainFinally = ( - runner: CallbackRunner, - promises: PromiseRuntime, - source: SandboxPromise, - cleanup: ReactionHandler | undefined, - method: string, - node: AstNode, -): Effect.Effect => - promises.create( - Effect.gen(function* () { - const exit = yield* reactionExit(promises, source) - if (cleanup !== undefined) { - const result = yield* applyCollectionCallback(runner, cleanup, method, node)([]) - if (result instanceof SandboxPromise) yield* runner.settlePromise(result) - } - return yield* exit - }), - ) diff --git a/packages/codemode/src/interpreter/references.ts b/packages/codemode/src/interpreter/references.ts deleted file mode 100644 index afbbdbf9e9..0000000000 --- a/packages/codemode/src/interpreter/references.ts +++ /dev/null @@ -1,99 +0,0 @@ -import { - type AstNode, - CodeModeFunction, - CoercionFunction, - ErrorConstructorReference, - GlobalMethodReference, - GlobalNamespace, - InterpreterRuntimeError, - IntrinsicReference, - PromiseCapabilityFunction, - PromiseInstanceMethodReference, - PromiseMethodReference, - PromiseNamespace, - SearchFunction, - UriFunction, -} from "./model.js" -import { ToolReference } from "../tool-runtime.js" -import { isSandboxValue, SandboxPromise } from "../values.js" - -export const isRuntimeReference = (value: unknown): boolean => - value instanceof CodeModeFunction || - value instanceof ToolReference || - value instanceof IntrinsicReference || - value instanceof GlobalNamespace || - value instanceof GlobalMethodReference || - value instanceof PromiseNamespace || - value instanceof PromiseMethodReference || - value instanceof PromiseInstanceMethodReference || - value instanceof SandboxPromise || - value instanceof CoercionFunction || - value instanceof UriFunction || - value instanceof SearchFunction || - value instanceof PromiseCapabilityFunction || - value instanceof ErrorConstructorReference || - isSandboxValue(value) - -export const containsRuntimeReference = (value: unknown, seen = new Set()): boolean => { - if (isRuntimeReference(value)) return true - if (value === null || typeof value !== "object") return false - if (seen.has(value)) return false - seen.add(value) - const contains = Array.isArray(value) - ? value.some((item) => containsRuntimeReference(item, seen)) - : Object.values(value).some((item) => containsRuntimeReference(item, seen)) - seen.delete(value) - return contains -} - -// Sandbox values are data here, not opaque interpreter references. -export const containsOpaqueReference = (value: unknown, seen = new Set()): boolean => { - if (isSandboxValue(value)) return false - if (isRuntimeReference(value)) return true - if (value === null || typeof value !== "object") return false - if (seen.has(value)) return false - seen.add(value) - const contains = Array.isArray(value) - ? value.some((item) => containsOpaqueReference(item, seen)) - : Object.values(value).some((item) => containsOpaqueReference(item, seen)) - seen.delete(value) - return contains -} - -// Reject cycles before mutation so later boundary walks remain safe. -export const rejectCircularInsertion = ( - container: object, - value: unknown, - label: string, - node: AstNode, - seen = new Set(), -): void => { - if (value === container) - throw new InterpreterRuntimeError(`${label} contains a circular value.`, node, "InvalidDataValue") - if (value === null || typeof value !== "object" || isRuntimeReference(value) || seen.has(value)) return - seen.add(value) - const items = Array.isArray(value) ? value : Object.values(value) - for (const item of items) rejectCircularInsertion(container, item, label, node, seen) - seen.delete(value) -} - -export const typeofValue = (value: unknown): string => { - if ( - value instanceof CodeModeFunction || - value instanceof CoercionFunction || - value instanceof IntrinsicReference || - value instanceof GlobalMethodReference || - value instanceof PromiseMethodReference || - value instanceof PromiseInstanceMethodReference || - value instanceof PromiseNamespace || - value instanceof PromiseCapabilityFunction || - value instanceof ErrorConstructorReference - ) - return "function" - if (value instanceof UriFunction || value instanceof SearchFunction) return "function" - if (value instanceof ToolReference) return value.path.length > 0 ? "function" : "object" - if (value instanceof GlobalNamespace) { - return value.name === "Math" || value.name === "JSON" || value.name === "console" ? "object" : "function" - } - return typeof value -} diff --git a/packages/codemode/src/interpreter/runtime.ts b/packages/codemode/src/interpreter/runtime.ts index 3b9e18cc9d..093f577765 100644 --- a/packages/codemode/src/interpreter/runtime.ts +++ b/packages/codemode/src/interpreter/runtime.ts @@ -1,5 +1,26 @@ -import { Cause, Effect } from "effect" -import { isBlockedMember, ToolReference, ToolRuntimeError, type SafeObject } from "../tool-runtime.js" +import { parse } from "acorn" +import { Cause, Effect, Exit, Fiber, Semaphore } from "effect" +import { DiagnosticCategory, ModuleKind, ScriptTarget, flattenDiagnosticMessageText, transpileModule } from "typescript" +import { + copyIn, + copyOut, + isBlockedMember, + ToolReference, + ToolRuntime, + ToolRuntimeError, + type HostTools, + type SafeObject, + type Services, +} from "../tool-runtime.js" +import { ToolError } from "../tool-error.js" +import type { + DataValue, + Diagnostic, + DiagnosticKind, + ExecuteOptions, + ResolvedExecutionLimits, + Result, +} from "../codemode.js" import { type AstNode, asNode, @@ -10,6 +31,8 @@ import { ErrorConstructorReference, GlobalMethodReference, GlobalNamespace, + type GlobalNamespaceName, + formatLocation, getArray, getBoolean, getNode, @@ -20,45 +43,50 @@ import { isRecord, type MemberReference, OptionalShortCircuit, - PromiseCapabilityFunction, - PromiseInstanceMethodReference, PromiseMethodReference, type PromiseMethodName, PromiseNamespace, ProgramThrow, type ProgramNode, - SearchFunction, type StatementResult, + sourceLocation, supportedSyntaxMessage, unsupportedSyntax, UriFunction, } from "./model.js" -import { caughtErrorValue, constructErrorValue } from "./errors.js" -import { type CallbackRunner, invokeGlobalMethod, invokeIntrinsic } from "./methods.js" -import { - constructPromise, - invokePromiseInstanceMethod, - invokePromiseMethod, - PromiseRuntime, - selfResolutionError, -} from "./promises.js" -import { containsOpaqueReference, isRuntimeReference, rejectCircularInsertion, typeofValue } from "./references.js" -import { ScopeStack } from "./scope.js" import { arrayMethods, mapMethods, setMethods, spreadItems } from "../stdlib/collections.js" -import { consoleMethods, formatConsoleMessage } from "../stdlib/console.js" -import { dateMethods } from "../stdlib/date.js" -import { mathConstants } from "../stdlib/math.js" -import { numberConstants, numberMethods, numberStatics } from "../stdlib/number.js" -import { objectMethodsPreservingIdentity } from "../stdlib/object.js" -import { promiseStatics } from "../stdlib/promise.js" -import { escapeRegexHint, regexpMethods, regexpProperties, regexFailureReason } from "../stdlib/regexp.js" -import { stringMethods, stringStatics } from "../stdlib/string.js" +import { consoleMethods, MAX_CONSOLE_DEPTH } from "../stdlib/console.js" +import { dateMethods, dateStatics, invokeDateMethod, invokeDateStatic } from "../stdlib/date.js" +import { invokeJsonMethod } from "../stdlib/json.js" +import { invokeMathMethod, mathConstants } from "../stdlib/math.js" +import { + invokeNumberMethod, + invokeNumberStatic, + numberConstants, + numberMethods, + numberStatics, +} from "../stdlib/number.js" +import { invokeObjectMethod } from "../stdlib/object.js" +import { promiseStatics, TOOL_CALL_CONCURRENCY } from "../stdlib/promise.js" +import { + escapeRegexHint, + invokeRegExpMethod, + matchToValue, + regexpMethods, + regexpProperties, + regexFailureReason, + toHostRegex, +} from "../stdlib/regexp.js" +import { invokeStringStatic, stringMethods, stringStatics } from "../stdlib/string.js" import { urlMethods, urlProperties, urlSearchParamsMethods, + urlStatics, urlWritableProperties, invokeUriFunction, + invokeURLMethod, + invokeURLStatic, uriArgument, urlArgument, } from "../stdlib/url.js" @@ -67,6 +95,7 @@ import { coerceToNumber, coerceToString, compoundOperators, + createErrorValue, errorBrandName, errorConstructors, invokeCoercion, @@ -83,6 +112,189 @@ import { SandboxURLSearchParams, } from "../values.js" +const parseProgram = (code: string): ProgramNode => { + const transpiled = transpileModule(`async function __codemode__() {\n${code}\n}`, { + reportDiagnostics: true, + compilerOptions: { + target: ScriptTarget.ESNext, + module: ModuleKind.ESNext, + }, + }) + const diagnostic = transpiled.diagnostics?.find((item) => item.category === DiagnosticCategory.Error) + + if (diagnostic) { + throw new InterpreterRuntimeError( + `Failed to parse TypeScript: ${flattenDiagnosticMessageText(diagnostic.messageText, "\n")}`, + undefined, + "ParseError", + ) + } + + const bodyStart = transpiled.outputText.indexOf("{") + 1 + const bodyEnd = transpiled.outputText.lastIndexOf("}") + const executableCode = transpiled.outputText.slice(bodyStart, bodyEnd) + const parsed = parse(executableCode, { + ecmaVersion: "latest", + sourceType: "script", + allowReturnOutsideFunction: true, + allowAwaitOutsideFunction: true, + locations: true, + }) as unknown + + if (!isRecord(parsed) || parsed.type !== "Program" || !Array.isArray(parsed.body)) { + throw new InterpreterRuntimeError("Failed to parse script as a Program node.") + } + + return parsed as ProgramNode +} + +const publicErrorMessage = (message: string): string => + message.replace(/\/(?:Users|home|private|tmp|var\/folders)\/[^\s"'`]+/g, "") + +const normalizeError = (error: unknown): Diagnostic => { + if (error instanceof InterpreterRuntimeError) { + return { + kind: error.kind, + message: `${error.message}${formatLocation(error.node)}`, + ...(error.node?.loc ? { location: sourceLocation(error.node) } : {}), + ...(error.suggestions ? { suggestions: error.suggestions } : {}), + } + } + + if (error instanceof ToolRuntimeError) { + return { + kind: error.kind, + message: error.message, + ...(error.suggestions.length > 0 ? { suggestions: error.suggestions } : {}), + } + } + + if (error instanceof ToolError) { + return { kind: "ToolFailure", message: publicErrorMessage(error.message) } + } + + if (error instanceof ProgramThrow) { + const value = error.value + let message: string + if (containsRuntimeReference(value)) { + // A thrown tool/function reference must not leak its internal structure. + message = "a non-data value" + } else if (typeof value === "string") { + message = value + } else if ( + value !== null && + typeof value === "object" && + typeof (value as { message?: unknown }).message === "string" + ) { + message = (value as { message: string }).message + } else { + try { + message = JSON.stringify(copyOut(value)) ?? String(value) + } catch { + message = String(value) + } + } + return { kind: "ExecutionFailure", message: `Uncaught: ${message}` } + } + + if (error instanceof RangeError && /call stack|recursion/i.test(error.message)) { + return { + kind: "ExecutionFailure", + message: "Execution exceeded the maximum nesting depth.", + } + } + + if (error instanceof Error) { + return { + kind: error.name === "SyntaxError" ? "ParseError" : "ExecutionFailure", + message: publicErrorMessage(error.message), + } + } + + // A non-Error thrown by a host tool (raw string / number / Symbol) still routes through + // path redaction so filesystem paths can never leak through the catch-all branch. + return { + kind: "ExecutionFailure", + message: publicErrorMessage(String(error)), + } +} + +// Shared by catch bindings, Promise.allSettled rejection reasons, and Promise.race losers. +const caughtErrorValue = (thrown: unknown): unknown => { + if (thrown instanceof ProgramThrow) return thrown.value + if (thrown instanceof InterpreterRuntimeError) return createErrorValue(thrown.errorName, thrown.message) + const name = thrown instanceof Error && errorConstructors.has(thrown.name) ? thrown.name : "Error" + return createErrorValue(name, normalizeError(thrown).message) +} + +const isRuntimeReference = (value: unknown): boolean => + value instanceof CodeModeFunction || + value instanceof ToolReference || + value instanceof IntrinsicReference || + value instanceof GlobalNamespace || + value instanceof GlobalMethodReference || + value instanceof PromiseNamespace || + value instanceof PromiseMethodReference || + value instanceof SandboxPromise || + value instanceof CoercionFunction || + value instanceof UriFunction || + value instanceof ErrorConstructorReference || + isSandboxValue(value) + +const containsRuntimeReference = (value: unknown, seen = new Set()): boolean => { + if (isRuntimeReference(value)) return true + if (value === null || typeof value !== "object") return false + if (seen.has(value)) return false + seen.add(value) + const contains = Array.isArray(value) + ? value.some((item) => containsRuntimeReference(item, seen)) + : Object.values(value).some((item) => containsRuntimeReference(item, seen)) + seen.delete(value) + return contains +} + +// Like containsRuntimeReference, but sandbox standard-library values count as data: +// operators and switch treat them as ordinary object operands (identity equality, ToPrimitive +// coercion) rather than rejecting them as opaque interpreter machinery. +const containsOpaqueReference = (value: unknown, seen = new Set()): boolean => { + if (isSandboxValue(value)) return false + if (isRuntimeReference(value)) return true + if (value === null || typeof value !== "object") return false + if (seen.has(value)) return false + seen.add(value) + const contains = Array.isArray(value) + ? value.some((item) => containsOpaqueReference(item, seen)) + : Object.values(value).some((item) => containsOpaqueReference(item, seen)) + seen.delete(value) + return contains +} + +// `typeof` never throws in JS; map every interpreter value to its JS-visible category. +// A SandboxPromise falls through to the final `typeof value` and reports "object", exactly +// like a real JS promise. +const typeofValue = (value: unknown): string => { + if ( + value instanceof CodeModeFunction || + value instanceof CoercionFunction || + value instanceof IntrinsicReference || + value instanceof GlobalMethodReference || + value instanceof PromiseMethodReference || + value instanceof PromiseNamespace || + value instanceof ErrorConstructorReference + ) + return "function" + if (value instanceof UriFunction) return "function" + if (value instanceof ToolReference) return value.path.length > 0 ? "function" : "object" + if (value instanceof GlobalNamespace) { + return value.name === "Math" || value.name === "JSON" || value.name === "console" ? "object" : "function" + } + return typeof value +} + +// `x instanceof C` against the constructors CodeMode knows. Like `typeof`, it observes any +// left-hand value (opaque references included) without coercing it. Error checks use the +// error brand: `instanceof Error` accepts every branded error; a specific error type matches +// its own brand only (as in JS, where TypeError instances are also Error instances). const instanceofValue = (lhs: unknown, rhs: unknown, node: AstNode): boolean => { if (rhs instanceof ErrorConstructorReference) { const brand = errorBrandName(lhs) @@ -109,6 +321,8 @@ const instanceofValue = (lhs: unknown, rhs: unknown, node: AstNode): boolean => } } if (rhs instanceof PromiseNamespace) return lhs instanceof SandboxPromise + // Number/String/Boolean wrap primitives in JS; no boxed values exist in CodeMode, so + // `x instanceof Number` is always false - exactly what it is for primitives in JS. if (rhs instanceof CoercionFunction && (rhs.name === "Number" || rhs.name === "String" || rhs.name === "Boolean")) { return false } @@ -118,6 +332,247 @@ const instanceofValue = (lhs: unknown, rhs: unknown, node: AstNode): boolean => ) } +const invokeStringMethod = (value: string, name: string, args: Array, node: AstNode): unknown => { + const str = (index: number): string => { + const arg = args[index] + if (typeof arg !== "string") + throw new InterpreterRuntimeError(`String.${name} expects argument ${index + 1} to be a string.`, node) + return arg + } + const num = (index: number): number => { + const arg = args[index] + if (typeof arg !== "number") + throw new InterpreterRuntimeError(`String.${name} expects argument ${index + 1} to be a number.`, node) + return arg + } + const optNum = (index: number): number | undefined => (args[index] === undefined ? undefined : num(index)) + const optStr = (index: number): string | undefined => (args[index] === undefined ? undefined : str(index)) + + let result: unknown + switch (name) { + case "toLowerCase": + result = value.toLowerCase() + break + case "toUpperCase": + result = value.toUpperCase() + break + case "trim": + result = value.trim() + break + // trimLeft/trimRight are the legacy aliases of trimStart/trimEnd, kept because models write them. + case "trimStart": + case "trimLeft": + result = value.trimStart() + break + case "trimEnd": + case "trimRight": + result = value.trimEnd() + break + // Locale/options arguments are ignored: comparison runs with the host default locale, and + // the common use is a sort comparator where any consistent order works. + case "localeCompare": + result = value.localeCompare(str(0)) + break + case "normalize": { + const form = optStr(0) + try { + result = value.normalize(form) + } catch { + throw new InterpreterRuntimeError( + `String.normalize expects the form "NFC", "NFD", "NFKC", or "NFKD" (got ${JSON.stringify(form)}).`, + node, + ).as("RangeError") + } + break + } + case "split": { + if (args.length === 0) { + result = [value] + break + } + if (args[0] instanceof SandboxRegExp) { + result = value.split((args[0] as SandboxRegExp).regex, optNum(1)) + break + } + const requestedLimit = optNum(1) + result = value.split(str(0), requestedLimit === undefined ? undefined : requestedLimit >>> 0) + break + } + case "slice": + result = value.slice(optNum(0), optNum(1)) + break + case "includes": + result = value.includes(str(0), optNum(1)) + break + case "startsWith": + result = value.startsWith(str(0), optNum(1)) + break + case "endsWith": + result = value.endsWith(str(0), optNum(1)) + break + case "indexOf": + result = value.indexOf(str(0), optNum(1)) + break + case "lastIndexOf": + result = value.lastIndexOf(str(0), optNum(1)) + break + case "replace": + case "replaceAll": { + if (args[0] instanceof SandboxRegExp) { + const pattern = (args[0] as SandboxRegExp).regex + const replacement = str(1) + if (name === "replaceAll" && !pattern.global) { + throw new InterpreterRuntimeError( + `String.replaceAll requires a regular expression with the global (g) flag: write /${pattern.source}/${pattern.flags}g, or use String.replace to replace only the first match.`, + node, + ) + } + result = name === "replace" ? value.replace(pattern, replacement) : value.replaceAll(pattern, replacement) + break + } + if (name === "replace") { + result = value.replace(str(0), str(1)) + break + } + result = value.replaceAll(str(0), str(1)) + break + } + case "match": { + const pattern = toHostRegex(args[0], name, node) + const matched = value.match(pattern) + if (matched === null) return null + // A global match is a plain array of matched strings; a non-global match carries + // index/groups own properties, so bypass the copying data checkpoint to keep them. + if (pattern.global) return boundedData(matched, "String.match result") + return matchToValue(matched) + } + case "matchAll": { + const pattern = toHostRegex(args[0], name, node, "g") + if (!pattern.global) { + throw new InterpreterRuntimeError( + `String.matchAll requires a regular expression with the global (g) flag: write /${pattern.source}/${pattern.flags}g, or use String.match for a single match.`, + node, + ) + } + // Materialized as an array (not an iterator); each entry is a match array with + // index/groups own properties. Match count is bounded by the subject length. + return Array.from(value.matchAll(pattern), matchToValue) + } + case "search": { + result = value.search(toHostRegex(args[0], name, node)) + break + } + case "repeat": { + const count = num(0) + if (!Number.isFinite(count) || count < 0) + throw new InterpreterRuntimeError("String.repeat expects a finite non-negative count.", node) + result = value.repeat(count) + break + } + case "padStart": + result = value.padStart(num(0), optStr(1)) + break + case "padEnd": + result = value.padEnd(num(0), optStr(1)) + break + case "charAt": + result = value.charAt(optNum(0) ?? 0) + break + case "at": + result = value.at(optNum(0) ?? 0) + break + case "substring": + result = value.substring(optNum(0) ?? 0, optNum(1)) + break + case "substr": + result = value.substr(optNum(0) ?? 0, optNum(1)) + break + // JS charCodeAt returns NaN out of range; NaN flows as an ordinary in-sandbox value + // (normalized to null only at the data boundary - see copyOut), so return it as-is. + case "charCodeAt": + result = value.charCodeAt(optNum(0) ?? 0) + break + case "codePointAt": + result = value.codePointAt(optNum(0) ?? 0) + break + case "toString": + result = value + break + case "concat": { + result = value.concat(...args.map((_, index) => str(index))) + break + } + default: + throw new InterpreterRuntimeError(`String method '${name}' is not available in CodeMode.`, node) + } + return boundedData(result, `String.${name} result`) +} + +const invokeArrayStatic = (name: string, args: Array, node: AstNode): unknown => { + switch (name) { + case "isArray": + return Array.isArray(args[0]) + case "of": + return [...args] + case "from": { + if (args.length > 1) { + throw new InterpreterRuntimeError( + "Array.from(...) does not support a map function in CodeMode; call .map() on the result instead.", + node, + "UnsupportedSyntax", + [supportedSyntaxMessage], + ) + } + // Map/Set materialize directly (the data checkpoint would serialize them to {}). + if (args[0] instanceof SandboxMap) + return Array.from((args[0] as SandboxMap).map.entries(), ([key, item]) => [key, item]) + if (args[0] instanceof SandboxSet) return Array.from((args[0] as SandboxSet).set.values()) + if (args[0] instanceof SandboxURLSearchParams) { + return Array.from(args[0].params.entries(), ([key, value]) => [key, value]) + } + const source = boundedData(args[0], "Array.from input") + if (typeof source === "string") return Array.from(source) + if (Array.isArray(source)) return [...source] + if ( + source !== null && + typeof source === "object" && + typeof (source as { length?: unknown }).length === "number" + ) { + return Array.from(source as ArrayLike) + } + throw new InterpreterRuntimeError("Array.from expects an array, string, Map, Set, or array-like value.", node) + } + default: + throw new InterpreterRuntimeError(`Array.${name} is not available in CodeMode.`, node) + } +} + +const invokeGlobalMethod = (ref: GlobalMethodReference, args: Array, node: AstNode): unknown => { + if (ref.namespace === "console") + throw new InterpreterRuntimeError(`console.${ref.name} is not available in CodeMode.`, node) + if (ref.namespace === "Object") return invokeObjectMethod(ref.name, args, node) + if (ref.namespace === "Math") return invokeMathMethod(ref.name, args, node) + if (ref.namespace === "Array") return invokeArrayStatic(ref.name, args, node) + if (ref.namespace === "Number") return invokeNumberStatic(ref.name, args, node) + if (ref.namespace === "String") return invokeStringStatic(ref.name, args, node) + if (ref.namespace === "URL") return invokeURLStatic(ref.name, args, node) + if (ref.namespace === "Date") { + if (!dateStatics.has(ref.name)) + throw new InterpreterRuntimeError(`Date.${ref.name} is not available in CodeMode.`, node) + return invokeDateStatic(ref.name, args, node) + } + if ( + ref.namespace === "RegExp" || + ref.namespace === "Map" || + ref.namespace === "Set" || + ref.namespace === "URLSearchParams" + ) { + throw new InterpreterRuntimeError(`${ref.namespace}.${ref.name} is not available in CodeMode.`, node) + } + return invokeJsonMethod(ref.name, args, node) +} + +// Every identifier a parameter pattern binds, used to seed TDZ slots before defaults run. const collectPatternNames = (pattern: AstNode, out: Array = []): Array => { switch (pattern.type) { case "Identifier": @@ -144,34 +599,34 @@ const collectPatternNames = (pattern: AstNode, out: Array = []): Array { - private scopes: ScopeStack +class Interpreter { + private scopes: Array> private readonly invokeTool: (path: ReadonlyArray, args: Array) => Effect.Effect - private readonly invokeSearch: (args: Array) => Effect.Effect + // Enumerable namespace/tool names at a node of the host tool tree, threaded from + // ToolRuntime.make like invokeTool: the interpreter never holds the tree itself. private readonly toolKeys: (path: ReadonlyArray) => ReadonlyArray private readonly logs: Array - private readonly promises: PromiseRuntime - private readonly runner: CallbackRunner = { - invokeFunction: (fn, args) => this.invokeFunction(fn, args), - settlePromise: (promise) => this.settlePromise(promise), - } + private lastValue: unknown + // Caps how many eagerly forked tool calls run at once (the parallel-call concurrency cap). + private readonly callPermits: Semaphore.Semaphore + // Fiber-backed promises whose settlement no program construct has observed yet. Successful + // program completion drains these (like a runtime waiting on in-flight work at exit) and + // surfaces a never-awaited failure as an unhandled-rejection diagnostic. + private readonly pendingSettlements = new Set() constructor( invokeTool: (path: ReadonlyArray, args: Array) => Effect.Effect, - invokeSearch: (args: Array) => Effect.Effect, toolKeys: (path: ReadonlyArray) => ReadonlyArray, - promises: PromiseRuntime, logs: Array = [], ) { const globalScope = new Map() - this.scopes = new ScopeStack([globalScope]) + this.scopes = [globalScope] this.invokeTool = invokeTool - this.invokeSearch = invokeSearch this.toolKeys = toolKeys this.logs = logs - this.promises = promises + this.lastValue = undefined + this.callPermits = Semaphore.makeUnsafe(TOOL_CALL_CONCURRENCY) globalScope.set("tools", { mutable: false, value: new ToolReference([]) }) - globalScope.set("search", { mutable: false, value: new SearchFunction() }) globalScope.set("Promise", { mutable: false, value: new PromiseNamespace() }) globalScope.set("undefined", { mutable: false, value: undefined }) globalScope.set("Object", { mutable: false, value: new GlobalNamespace("Object") }) @@ -194,68 +649,136 @@ export class Interpreter { globalScope.set("encodeURIComponent", { mutable: false, value: new UriFunction("encodeURIComponent") }) globalScope.set("decodeURI", { mutable: false, value: new UriFunction("decodeURI") }) globalScope.set("decodeURIComponent", { mutable: false, value: new UriFunction("decodeURIComponent") }) + // Error constructors are real values, so `x instanceof Error` works and `Error("msg")` + // (with or without `new`) constructs a branded { name, message } error object. for (const name of errorConstructors) { globalScope.set(name, { mutable: false, value: new ErrorConstructorReference(name) }) } + // NaN/Infinity flow as ordinary in-sandbox values (normalized to null only at the data + // boundary - see copyOut), so their global bindings must exist too, e.g. `reduce(max, -Infinity)`. globalScope.set("NaN", { mutable: false, value: NaN }) globalScope.set("Infinity", { mutable: false, value: Infinity }) } run(program: ProgramNode): Effect.Effect { const self = this - // Keep top-level declarations separate so they can shadow builtins. - this.scopes.push() + // Run the program body in its own module scope on top of the builtin global scope, so + // top-level declarations (`let undefined = 5`, `const Object = ...`) shadow builtins like + // JS module scope, instead of colliding with the seeded globals. + this.pushScope() return Effect.gen(function* () { self.hoistFunctions(program.body) let value: unknown = undefined - for (const [index, statement] of program.body.entries()) { - if (index === program.body.length - 1 && statement.type === "ExpressionStatement") { - value = yield* self.evaluateExpression(getNode(statement, "expression")) - break - } + let returned = false + for (const statement of program.body) { const result = yield* self.evaluateStatement(statement) if (result.kind === "return") { value = result.value + returned = true break } if (result.kind === "break" || result.kind === "continue") { throw new InterpreterRuntimeError(`Unexpected '${result.kind}' outside of a loop.`, statement) } - } - // The implicit async body adopts returned promises before copy-out. + if (result.kind === "value") { + self.lastValue = result.value + } + } + if (!returned) value = self.lastValue + + // The program body runs inside an implicit async function, so a returned promise + // resolves before crossing the data boundary - `return tools.ns.tool(...)` works + // without an explicit await, exactly as in JS. if (value instanceof SandboxPromise) value = yield* self.settlePromise(value) + yield* self.drainPendingSettlements() return value - }).pipe(Effect.ensuring(Effect.sync(() => self.scopes.pop()))) + }).pipe(Effect.ensuring(Effect.sync(() => self.popScope()))) } - // Fork at the call site so admission and hooks occur when the call is made. + // Awaits every fiber-backed promise the program abandoned (fire-and-forget tool calls), so + // their work completes before the execution ends - mirroring a JS runtime waiting on + // in-flight I/O at exit. A failure nobody could have handled becomes an unhandled-rejection + // diagnostic (interrupted calls, e.g. Promise.race losers, are ignored). + private drainPendingSettlements(): Effect.Effect { + const self = this + return Effect.gen(function* () { + for (const promise of [...self.pendingSettlements]) { + const exit = yield* self.observePromise(promise) + if (Exit.isSuccess(exit) || Cause.hasInterruptsOnly(exit.cause)) continue + const failure = normalizeError(Cause.squash(exit.cause)) + throw new InterpreterRuntimeError( + `Unhandled rejection from an un-awaited tool call: ${failure.message}`, + undefined, + failure.kind, + ["Await tool calls - `const result = await tools.ns.tool(...)` - so failures can be caught and handled."], + ) + } + }) + } + + // Eagerly starts a tool call on a supervised child fiber (so the execution timeout and + // scope teardown interrupt it) gated by the concurrency semaphore, and wraps the fiber in a + // first-class promise value. `startImmediately` makes the runtime admit the call - charging + // the tool-call budget and firing onToolCallStart - at the call site, before any await. private createToolCallPromise( path: ReadonlyArray, args: Array, ): Effect.Effect { - return this.createPromise(Effect.suspend(() => this.invokeTool(path, args))) + const self = this + return Effect.map( + Effect.forkChild(this.callPermits.withPermit(Effect.suspend(() => self.invokeTool(path, args))), { + startImmediately: true, + }), + (fiber) => { + const promise = new SandboxPromise(fiber) + self.pendingSettlements.add(promise) + return promise + }, + ) } - private createPromise(effect: Effect.Effect): Effect.Effect { - return this.promises.create(effect) + // The promise's settlement as an Exit, marking it observed for unhandled-rejection tracking. + // Fiber settlement is idempotent, so observing the same promise repeatedly (await twice, + // Promise.all([p, p])) never re-runs the underlying call. + private observePromise(promise: SandboxPromise): Effect.Effect> { + this.pendingSettlements.delete(promise) + return promise.fiber !== undefined ? Fiber.await(promise.fiber) : Effect.exit(promise.immediate ?? Effect.void) } - // Fiber exits make settlement idempotent; yielding prevents inline continuation. - private settlePromise(promise: SandboxPromise): Effect.Effect { - const promises = this.promises - return Effect.suspend(() => { - promises.markObserved(promise) - return Effect.flatMap(promises.await(promise), (exit) => Effect.andThen(Effect.yieldNow, exit)) - }) + // `await promise`: succeed with the fulfilled value or re-raise the failure so try/catch + // observes it exactly like a synchronous throw at the await site. + private settlePromise(promise: SandboxPromise, node?: AstNode): Effect.Effect { + const self = this + return Effect.flatMap(this.observePromise(promise), (exit) => self.unwrapPromiseExit(promise, exit, node)) + } + + private unwrapPromiseExit( + promise: SandboxPromise | undefined, + exit: Exit.Exit, + node?: AstNode, + ): Effect.Effect { + if (Exit.isSuccess(exit)) return Effect.succeed(exit.value) + // A call Promise.race interrupted after losing settles as a catchable program failure; + // any other interruption is execution teardown (timeout/host) and must keep propagating + // as interruption rather than becoming program-visible data. + if (promise?.interrupted === true && Cause.hasInterruptsOnly(exit.cause)) { + return Effect.fail( + new InterpreterRuntimeError( + "This tool call was interrupted because another value settled a Promise.race first.", + node, + ), + ) + } + return Effect.failCause(exit.cause) } private evaluateStatement(node: AstNode): Effect.Effect { switch (node.type) { case "ExpressionStatement": - return Effect.as(this.evaluateExpression(getNode(node, "expression")), { kind: "none" }) + return Effect.map(this.evaluateExpression(getNode(node, "expression")), (value) => ({ kind: "value", value })) case "VariableDeclaration": return Effect.map(this.evaluateVariableDeclaration(node), () => ({ kind: "none" })) case "ReturnStatement": { @@ -291,14 +814,14 @@ export class Interpreter { case "EmptyStatement": return Effect.succeed({ kind: "none" }) case "FunctionDeclaration": - return Effect.succeed({ kind: "none" }) + return Effect.succeed({ kind: "none" }) // bound ahead of time by hoistFunctions default: throw unsupportedSyntax(node.type, node) } } private evaluateBlock(node: AstNode): Effect.Effect { - this.scopes.push() + this.pushScope() const self = this return Effect.gen(function* () { const body = getArray(node, "body") @@ -308,13 +831,18 @@ export class Interpreter { const statement = asNode(statementValue, "body") const result = yield* self.evaluateStatement(statement) + if (result.kind === "value") { + self.lastValue = result.value + continue + } + if (result.kind !== "none") { return result } } return { kind: "none" } satisfies StatementResult - }).pipe(Effect.ensuring(Effect.sync(() => self.scopes.pop()))) + }).pipe(Effect.ensuring(Effect.sync(() => self.popScope()))) } private createFunction(node: AstNode): CodeModeFunction { @@ -329,16 +857,17 @@ export class Interpreter { return new CodeModeFunction( getArray(node, "params").map((parameter, index) => asNode(parameter, `params[${index}]`)), getNode(node, "body"), - this.scopes.capture(), - node.async === true, + this.scopes.slice(), ) } + // Function declarations are hoisted: bound in their scope before the body runs, so a + // program can call a helper defined further down (matching JavaScript). private hoistFunctions(statements: Array): void { for (const statementValue of statements) { if (!isRecord(statementValue) || statementValue.type !== "FunctionDeclaration") continue const node = statementValue as AstNode - this.scopes.declare(getString(getNode(node, "id"), "name"), this.createFunction(node), true, node) + this.declare(getString(getNode(node, "id"), "name"), this.createFunction(node), true, node) } } @@ -358,7 +887,7 @@ export class Interpreter { private evaluateSwitchStatement(node: AstNode): Effect.Effect { const self = this - this.scopes.push() + this.pushScope() return Effect.gen(function* () { const discriminant = yield* self.evaluateExpression(getNode(node, "discriminant")) if (containsOpaqueReference(discriminant)) { @@ -397,10 +926,11 @@ export class Interpreter { const result = yield* self.evaluateStatement(asNode(statementValue, "consequent")) if (result.kind === "break") return { kind: "none" } satisfies StatementResult if (result.kind === "return" || result.kind === "continue") return result + if (result.kind === "value") self.lastValue = result.value } } return { kind: "none" } satisfies StatementResult - }).pipe(Effect.ensuring(Effect.sync(() => self.scopes.pop()))) + }).pipe(Effect.ensuring(Effect.sync(() => self.popScope()))) } private evaluateWhileStatement(node: AstNode): Effect.Effect { @@ -423,6 +953,10 @@ export class Interpreter { if (result.kind === "return") { return result } + + if (result.kind === "value") { + self.lastValue = result.value + } } return { kind: "none" } satisfies StatementResult @@ -449,6 +983,10 @@ export class Interpreter { if (result.kind === "return") { return result } + + if (result.kind === "value") { + self.lastValue = result.value + } } while (yield* self.evaluateExpression(testNode)) return { kind: "none" } satisfies StatementResult @@ -456,7 +994,7 @@ export class Interpreter { } private evaluateForStatement(node: AstNode): Effect.Effect { - this.scopes.push() + this.pushScope() const self = this return Effect.gen(function* () { const initNode = getOptionalNode(node, "init") @@ -474,21 +1012,24 @@ export class Interpreter { const perIterationBindings = initNode?.type === "VariableDeclaration" && getString(initNode, "kind") !== "var" - ? Array.from(self.scopes.current().keys()) + ? Array.from(self.currentScope().keys()) : [] while (testNode ? yield* self.evaluateExpression(testNode) : true) { - const iterationScope = - perIterationBindings.length > 0 - ? new Map( - perIterationBindings.map((name): [string, Binding] => [name, { ...self.scopes.current().get(name)! }]), - ) - : undefined - if (iterationScope) self.scopes.push(iterationScope) + let iterationScope: Map | undefined + if (perIterationBindings.length > 0) { + iterationScope = new Map( + perIterationBindings.map((name) => { + const binding = self.currentScope().get(name)! + return [name, { ...binding }] + }), + ) + self.scopes.push(iterationScope) + } const result = yield* self.evaluateStatement(bodyNode).pipe( Effect.ensuring( Effect.sync(() => { - if (iterationScope) self.scopes.pop() + if (iterationScope) self.popScope() }), ), ) @@ -501,8 +1042,12 @@ export class Interpreter { return { kind: "none" } satisfies StatementResult } + if (result.kind === "value") { + self.lastValue = result.value + } + if (iterationScope) { - const loopScope = self.scopes.current() + const loopScope = self.currentScope() for (const name of perIterationBindings) { loopScope.set(name, { ...iterationScope.get(name)! }) } @@ -518,7 +1063,7 @@ export class Interpreter { } return { kind: "none" } satisfies StatementResult - }).pipe(Effect.ensuring(Effect.sync(() => self.scopes.pop()))) + }).pipe(Effect.ensuring(Effect.sync(() => self.popScope()))) } private evaluateForOfStatement(node: AstNode): Effect.Effect { @@ -532,13 +1077,15 @@ export class Interpreter { const right = yield* self.evaluateExpression(getNode(node, "right")) const body = getNode(node, "body") - const iterable = spreadItems(right) + // Arrays iterate in place; strings iterate code points; Maps iterate [key, value] + // pairs and Sets iterate values over a snapshot (mutation during iteration is safe). + const iterable = Array.isArray(right) ? right : spreadItems(right) if (iterable === undefined) { throw new InterpreterRuntimeError("for...of requires an array, string, Map, or Set value in CodeMode.", node) } let declaration: { readonly pattern: AstNode; readonly mutable: boolean } | undefined - let assignment: AstNode | undefined + let assignmentName: string | undefined if (left.type === "VariableDeclaration") { const declarations = getArray(left, "declarations") @@ -548,29 +1095,24 @@ export class Interpreter { const declarator = asNode(declarations[0], "declarations[0]") declaration = { pattern: getNode(declarator, "id"), mutable: getString(left, "kind") !== "const" } - } else if ( - left.type === "Identifier" || - left.type === "MemberExpression" || - left.type === "ArrayPattern" || - left.type === "ObjectPattern" - ) { - assignment = left + } else if (left.type === "Identifier") { + assignmentName = getString(left, "name") } else { throw new InterpreterRuntimeError("Unsupported for...of binding.", left) } for (const value of iterable) { if (declaration) { - self.scopes.push() + self.pushScope() yield* self.declarePattern(declaration.pattern, value, declaration.mutable, left) - } else if (assignment) { - yield* self.assignPattern(assignment, value, left) + } else if (assignmentName) { + self.setIdentifierValue(assignmentName, value, left) } const result = yield* self.evaluateStatement(body).pipe( Effect.ensuring( Effect.sync(() => { - if (declaration) self.scopes.pop() + if (declaration) self.popScope() }), ), ) @@ -583,6 +1125,10 @@ export class Interpreter { return { kind: "none" } } + if (result.kind === "value") { + self.lastValue = result.value + } + if (result.kind === "continue") { continue } @@ -592,6 +1138,11 @@ export class Interpreter { }) } + // Own enumerable string keys of a value, shared by `for...in` and `Object.keys` over tool + // references: plain data objects enumerate their own keys, arrays their index strings (plus + // any own non-index properties, e.g. match results' index/groups - exactly Object.keys in + // JS), and a tool reference the namespace/tool names at its path in the host tool tree. + // Returns undefined for everything else so callers can raise a contextual error. private enumerableKeys(value: unknown): Array | undefined { if (value instanceof ToolReference) { return [...this.toolKeys(value.path)] @@ -612,6 +1163,12 @@ export class Interpreter { const right = yield* self.evaluateExpression(getNode(node, "right")) const body = getNode(node, "body") + // Keys are snapshotted up front (mutation during iteration is safe): plain objects + // enumerate their own keys, arrays their index strings, and tool references the + // namespace/tool names at that node - the same enumeration Object.keys performs. + // Anything else (strings, Maps, Sets, numbers, null, ...) is a deliberate error rather + // than real JS's surprising behavior (indices for strings, zero iterations for + // Maps/Sets/null): the hint points at the constructs that do what the program means. const keys = self.enumerableKeys(right) if (keys === undefined) { throw new InterpreterRuntimeError( @@ -639,16 +1196,16 @@ export class Interpreter { for (const key of keys) { if (declaration) { - self.scopes.push() + self.pushScope() yield* self.declarePattern(declaration.pattern, key, declaration.mutable, left) } else if (assignmentName) { - self.scopes.set(assignmentName, key, left) + self.setIdentifierValue(assignmentName, key, left) } const result = yield* self.evaluateStatement(body).pipe( Effect.ensuring( Effect.sync(() => { - if (declaration) self.scopes.pop() + if (declaration) self.popScope() }), ), ) @@ -661,6 +1218,10 @@ export class Interpreter { return { kind: "none" } } + if (result.kind === "value") { + self.lastValue = result.value + } + if (result.kind === "continue") { continue } @@ -707,13 +1268,15 @@ export class Interpreter { return Effect.failCause(cause) } + // The program sees a plain { message } error (or the thrown value itself) - see + // caughtErrorValue, shared with Promise.allSettled rejection reasons. const caught = caughtErrorValue(Cause.squash(cause)) const parameter = getOptionalNode(handler, "param") - self.scopes.push() + self.pushScope() return Effect.gen(function* () { if (parameter) yield* self.declarePattern(parameter, caught, true, handler) return yield* self.evaluateStatement(getNode(handler, "body")) - }).pipe(Effect.ensuring(Effect.sync(() => self.scopes.pop()))) + }).pipe(Effect.ensuring(Effect.sync(() => self.popScope()))) }, onSuccess: Effect.succeed, }) @@ -765,10 +1328,11 @@ export class Interpreter { const self = this return Effect.gen(function* () { if (pattern.type === "Identifier") { - self.scopes.declare(getString(pattern, "name"), value, mutable, node) + self.declare(getString(pattern, "name"), value, mutable, node) return } + // Default values: `x = expr` / `{ a = 1 }` - the default is evaluated only when the value is undefined. if (pattern.type === "AssignmentPattern") { const resolved = value === undefined ? yield* self.evaluateExpression(getNode(pattern, "right")) : value yield* self.declarePattern(getNode(pattern, "left"), resolved, mutable, node) @@ -788,6 +1352,7 @@ export class Interpreter { for (const propertyValue of getArray(pattern, "properties")) { const property = asNode(propertyValue, "properties") + // Object rest: `{ a, ...others }` - gather the not-yet-consumed own keys. if (property.type === "RestElement") { const rest: SafeObject = Object.create(null) as SafeObject for (const [key, item] of Object.entries(value as SafeObject)) { @@ -824,6 +1389,7 @@ export class Interpreter { for (const [index, item] of getArray(pattern, "elements").entries()) { if (item === null) continue const element = asNode(item, `elements[${index}]`) + // Array rest: `[head, ...tail]` - binds the remaining elements (must be last). if (element.type === "RestElement") { yield* self.declarePattern(getNode(element, "argument"), value.slice(index), mutable, element) break @@ -837,87 +1403,11 @@ export class Interpreter { }) } - private assignPattern(pattern: AstNode, value: unknown, node: AstNode): Effect.Effect { - const self = this - return Effect.gen(function* () { - if (pattern.type === "Identifier") { - self.scopes.set(getString(pattern, "name"), value, pattern) - return - } - - if (pattern.type === "MemberExpression") { - yield* self.writeMember(pattern, value) - return - } - - if (pattern.type === "AssignmentPattern") { - const resolved = value === undefined ? yield* self.evaluateExpression(getNode(pattern, "right")) : value - yield* self.assignPattern(getNode(pattern, "left"), resolved, node) - return - } - - if (pattern.type === "ObjectPattern") { - if (value === null || typeof value !== "object" || Array.isArray(value) || isRuntimeReference(value)) { - throw new InterpreterRuntimeError( - "Object destructuring requires a data object value.", - pattern, - "InvalidDataValue", - ) - } - - const source = value as SafeObject - const consumed = new Set() - for (const propertyValue of getArray(pattern, "properties")) { - const property = asNode(propertyValue, "properties") - if (property.type === "RestElement") { - const rest: SafeObject = Object.create(null) as SafeObject - for (const [key, item] of Object.entries(source)) { - if (!consumed.has(key) && !isBlockedMember(key)) rest[key] = item - } - yield* self.assignPattern(getNode(property, "argument"), rest, property) - continue - } - if ( - property.type !== "Property" || - getBoolean(property, "computed") || - getString(property, "kind") !== "init" - ) { - throw new InterpreterRuntimeError("Only named object destructuring properties are supported.", property) - } - const keyNode = getNode(property, "key") - const key = keyNode.type === "Identifier" ? getString(keyNode, "name") : String(keyNode.value) - if (isBlockedMember(key)) { - throw new InterpreterRuntimeError(`Property '${key}' is not available in CodeMode.`, keyNode) - } - consumed.add(key) - yield* self.assignPattern(getNode(property, "value"), source[key], property) - } - return - } - - if (pattern.type === "ArrayPattern") { - if (!Array.isArray(value)) { - throw new InterpreterRuntimeError("Array destructuring requires an array value.", pattern) - } - for (const [index, item] of getArray(pattern, "elements").entries()) { - if (item === null) continue - const element = asNode(item, `elements[${index}]`) - if (element.type === "RestElement") { - yield* self.assignPattern(getNode(element, "argument"), value.slice(index), element) - break - } - yield* self.assignPattern(element, value[index], pattern) - } - return - } - - throw new InterpreterRuntimeError(`Unsupported assignment pattern '${pattern.type}'.`, node) - }) - } - private evaluateExpression(node: AstNode): Effect.Effect { switch (node.type) { case "Literal": { + // A regex literal parses as a Literal node carrying { pattern, flags }; construct the + // sandbox regex from those (the host `value` instance is never exposed). const regex = node.regex if (isRecord(regex) && typeof regex.pattern === "string") { return Effect.sync(() => @@ -927,7 +1417,7 @@ export class Interpreter { return Effect.sync(() => boundedData(node.value, "Literal")) } case "Identifier": - return Effect.sync(() => this.scopes.get(getString(node, "name"), node)) + return Effect.sync(() => this.getIdentifierValue(getString(node, "name"), node)) case "BinaryExpression": return this.evaluateBinaryExpression(node) case "LogicalExpression": @@ -936,16 +1426,6 @@ export class Interpreter { return this.evaluateUnaryExpression(node) case "AssignmentExpression": return this.evaluateAssignmentExpression(node) - case "SequenceExpression": { - const self = this - return Effect.gen(function* () { - let result: unknown - for (const expression of getArray(node, "expressions")) { - result = yield* self.evaluateExpression(asNode(expression, "expressions")) - } - return result - }) - } case "CallExpression": return this.evaluateCallExpression(node) case "ArrowFunctionExpression": @@ -968,10 +1448,11 @@ export class Interpreter { case "UpdateExpression": return this.evaluateUpdateExpression(node) case "AwaitExpression": { - // Await always suspends, including for plain values. + // `await` resolves a promise value; awaiting anything else is a passthrough no-op, + // matching real JS semantics for non-thenables. const self = this return Effect.flatMap(this.evaluateExpression(getNode(node, "argument")), (value) => - value instanceof SandboxPromise ? self.settlePromise(value) : Effect.as(Effect.yieldNow, value), + value instanceof SandboxPromise ? self.settlePromise(value, node) : Effect.succeed(value), ) } case "NewExpression": @@ -990,12 +1471,19 @@ export class Interpreter { const argNodes = getArray(node, "arguments") const self = this if (name === "Promise") { - return Effect.flatMap(this.evaluateCallArguments(argNodes), (args) => - constructPromise(self.runner, self.promises, args[0], node), + throw new InterpreterRuntimeError( + "new Promise(...) is not supported in CodeMode; tool calls already return promises - call the tool and await the result.", + node, + "UnsupportedSyntax", + [supportedSyntaxMessage], ) } if (errorConstructors.has(name)) { - return Effect.map(this.evaluateCallArguments(argNodes), (args) => constructErrorValue(name, args, node)) + return Effect.gen(function* () { + const arg = + argNodes.length > 0 ? yield* self.evaluateExpression(asNode(argNodes[0], "arguments[0]")) : undefined + return createErrorValue(name, arg === undefined ? "" : coerceToString(arg)) + }) } if (valueConstructors.has(name)) { return Effect.gen(function* () { @@ -1028,6 +1516,7 @@ export class Interpreter { if (typeof arg === "string") return new SandboxDate(Date.parse(arg)) return new SandboxDate(Number.NaN) } + // new Date(year, month, day?, hours?, ...) - local-time component form. const parts = args.map((arg) => coerceToNumber(arg)) return new SandboxDate(new Date(...(parts as [number, number])).getTime()) } @@ -1047,6 +1536,9 @@ export class Interpreter { try { return new SandboxRegExp(pattern, flags) } catch (error) { + // Say which part was rejected and how to fix it, instead of passing the engine + // message through bare. A flags failure names the flags; a pattern failure gets the + // escaping hint (the usual cause is an unescaped metacharacter in a built-up string). const reason = regexFailureReason(error) throw new InterpreterRuntimeError( /flag/i.test(reason) @@ -1164,17 +1656,29 @@ export class Interpreter { return Effect.gen(function* () { const lhs = yield* self.evaluateExpression(getNode(node, "left")) const rhs = yield* self.evaluateExpression(getNode(node, "right")) + // Like `typeof`, `instanceof` observes any value without coercing it (a promise or + // function operand is a legitimate question, not an error), so it is handled before + // the data-only operand check. if (operator === "instanceof") return instanceofValue(lhs, rhs, node) return boundedData(self.applyBinaryOperator(operator, lhs, rhs, node), "Binary expression result") }) } + /** + * Applies a binary operator to two already-evaluated operands with CodeMode's coercion + * semantics. Shared by binary expressions and compound assignment (`x op= y` must behave + * exactly like `x = x op y`, coercion included). + */ private applyBinaryOperator(operator: string, lhs: unknown, rhs: unknown, node: AstNode): unknown { if (containsOpaqueReference(lhs) || containsOpaqueReference(rhs)) { throw new InterpreterRuntimeError("Binary operators require data values in CodeMode.", node, "InvalidDataValue") } - // Null-prototype data needs explicit primitive coercion; identity and `in` retain raw objects. - // Dates use string coercion for `+` and epoch time elsewhere. + // Data objects/arrays are null-prototype, so JS's ToPrimitive throws an opaque host + // "No default value" TypeError when an operator coerces them. Coerce to their JS string + // form first (as String(x) / template literals do) so operators behave like JavaScript. + // A Date follows its ToPrimitive hints: string for `+` (concatenation), its time value + // for arithmetic and ordering - so `end - start` and `a < b` work as in JS. + // Identity (=== / !==) and the right operand of `in` keep their raw object value. const coerceOperand = (operand: unknown): unknown => { if (operand instanceof SandboxDate) return operator === "+" ? coerceToString(operand) : operand.time return operand !== null && typeof operand === "object" ? coerceToString(operand) : operand @@ -1195,6 +1699,7 @@ export class Interpreter { return (l as number) % (r as number) case "**": return (l as number) ** (r as number) + // Two objects compare by identity in JS (no ToPrimitive); only object-vs-primitive coerces. case "==": return bothObjects ? lhs === rhs : l == r case "===": @@ -1227,7 +1732,7 @@ export class Interpreter { if (rhs === null || typeof rhs !== "object") { throw new InterpreterRuntimeError("The 'in' operator requires a data object on the right-hand side.", node) } - // Never expose properties inherited from host prototypes. + // Own properties only, so arrays don't leak the host Array.prototype (map/constructor/...). return Object.hasOwn(rhs as object, coerceOperand(lhs) as PropertyKey) default: throw new InterpreterRuntimeError(`Unsupported binary operator '${operator}'.`, node) @@ -1250,16 +1755,23 @@ export class Interpreter { private evaluateUnaryExpression(node: AstNode): Effect.Effect { const operator = getString(node, "operator") const argument = getNode(node, "argument") - // Undeclared names short-circuit, but declared TDZ bindings must still throw. - if (operator === "typeof" && argument.type === "Identifier" && !this.scopes.resolve(getString(argument, "name"))) { + // `typeof undeclaredIdentifier` is `"undefined"` in JS (never a ReferenceError), so + // feature-detection guards like `typeof x !== "undefined"` don't crash. Short-circuit before + // evaluating the argument; a declared-but-TDZ binding still falls through to the normal throw. + if (operator === "typeof" && argument.type === "Identifier" && !this.resolveBinding(getString(argument, "name"))) { return Effect.succeed("undefined") } return Effect.map(this.evaluateExpression(argument), (value) => { + // `typeof` and `!` never throw in JS - they observe any value (functions and runtime + // references included) without coercing it, so feature detection and negation work. if (operator === "typeof") return typeofValue(value) if (operator === "!") return !value if (containsOpaqueReference(value)) { throw new InterpreterRuntimeError("Unary operators require data values in CodeMode.", node, "InvalidDataValue") } + // Numeric/bitwise unary operators ToPrimitive their operand; a Date yields its time value + // (`+date` is the epoch-ms idiom), other null-prototype data objects/arrays coerce to + // their JS string form first (see evaluateBinaryExpression). const operand = value instanceof SandboxDate ? value.time @@ -1292,36 +1804,25 @@ export class Interpreter { if (operator === "??=" || operator === "||=" || operator === "&&=") { return yield* self.evaluateLogicalAssignment(node, left, operator) } - if (operator === "=" && (left.type === "ObjectPattern" || left.type === "ArrayPattern")) { - const rightValue = yield* self.evaluateExpression(getNode(node, "right")) - yield* self.assignPattern(left, rightValue, node) - return rightValue - } + const rightValue = yield* self.evaluateExpression(getNode(node, "right")) if (left.type === "Identifier") { const name = getString(left, "name") - if (operator !== "=") { - const current = self.scopes.get(name, left) - const rightValue = yield* self.evaluateExpression(getNode(node, "right")) + if (operator === "=") return self.setIdentifierValue(name, rightValue, left) + const next = boundedData( + self.applyCompoundAssignment(operator, self.getIdentifierValue(name, left), rightValue, node), + "Assignment result", + ) + return self.setIdentifierValue(name, next, left) + } + if (left.type === "MemberExpression") { + if (operator === "=") return yield* self.writeMember(left, rightValue) + return yield* self.modifyMember(left, (current) => { const next = boundedData( self.applyCompoundAssignment(operator, current, rightValue, node), "Assignment result", ) - return self.scopes.set(name, next, left) - } - const rightValue = yield* self.evaluateExpression(getNode(node, "right")) - return self.scopes.set(name, rightValue, left) - } - if (left.type === "MemberExpression") { - return yield* self.modifyMember(left, (current) => - Effect.map(self.evaluateExpression(getNode(node, "right")), (rightValue) => { - if (operator === "=") return { write: true, next: rightValue, result: rightValue } - const next = boundedData( - self.applyCompoundAssignment(operator, current, rightValue, node), - "Assignment result", - ) - return { write: true, next, result: next } - }), - ) + return Effect.succeed({ write: true, next, result: next }) + }) } throw new InterpreterRuntimeError("Assignment target must be an Identifier or MemberExpression.", left) }) @@ -1338,13 +1839,14 @@ export class Interpreter { if (left.type === "Identifier") { const name = getString(left, "name") return Effect.gen(function* () { - const current = self.scopes.get(name, left) + const current = self.getIdentifierValue(name, left) if (!shouldAssign(current)) return current const rightValue = yield* self.evaluateExpression(getNode(node, "right")) - return self.scopes.set(name, rightValue, left) + return self.setIdentifierValue(name, rightValue, left) }) } if (left.type === "MemberExpression") { + // Resolve the member exactly once; evaluate the RHS only if we actually assign. return self.modifyMember(left, (current) => shouldAssign(current) ? Effect.map(self.evaluateExpression(getNode(node, "right")), (rightValue) => ({ @@ -1372,9 +1874,9 @@ export class Interpreter { if (argument.type === "Identifier") { return Effect.sync(() => { const name = getString(argument, "name") - const current = Number(this.scopes.get(name, argument)) + const current = Number(this.getIdentifierValue(name, argument)) const next = current + increment - this.scopes.set(name, next, argument) + this.setIdentifierValue(name, next, argument) return prefix ? next : current }) } @@ -1404,30 +1906,22 @@ export class Interpreter { if (callable instanceof ToolReference) { if (callable.path.length === 0) throw new InterpreterRuntimeError("The tools root is not callable.", callee) + // An un-awaited tool call is a first-class promise value; the call itself starts now. return yield* self.createToolCallPromise(callable.path, args) } if (callable instanceof PromiseMethodReference) { - return yield* invokePromiseMethod(self.runner, self.promises, callable, args, node) - } - if (callable instanceof PromiseInstanceMethodReference) { - return yield* invokePromiseInstanceMethod(self.runner, self.promises, callable, args, node) + return yield* self.invokePromiseMethod(callable, args, node) } if (callable instanceof CodeModeFunction) { return yield* self.invokeFunction(callable, args) } if (callable instanceof IntrinsicReference) { - return yield* invokeIntrinsic(self.runner, callable, args, node) + return yield* self.invokeIntrinsic(callable, args, node) } if (callable instanceof GlobalMethodReference) { if (callable.namespace === "console") return self.invokeConsole(callable.name, args, node) if (callable.namespace === "Object" && args[0] instanceof ToolReference) { - return self.invokeObjectMethodOnTools(callable.name, args[0], node) - } - if (callable.namespace === "Object" && objectMethodsPreservingIdentity.has(callable.name)) { - return invokeGlobalMethod(callable, args, node) - } - if (callable.namespace === "Array" && (callable.name === "from" || callable.name === "of")) { - return invokeGlobalMethod(callable, args, node) + return self.invokeObjectMethodOnTools(callable.name, args[0] as ToolReference, node) } return boundedData(invokeGlobalMethod(callable, args, node), `${callable.namespace}.${callable.name} result`) } @@ -1437,26 +1931,24 @@ export class Interpreter { if (callable instanceof UriFunction) { return invokeUriFunction(callable, args, node) } - if (callable instanceof SearchFunction) { - return yield* self.invokeSearch(args) - } + // `Error("msg")` without `new` constructs an error exactly like `new Error("msg")`, as in JS. if (callable instanceof ErrorConstructorReference) { - return constructErrorValue(callable.name, args, node) - } - if (callable instanceof PromiseCapabilityFunction) { - callable.settle(args[0]) - return undefined + return createErrorValue(callable.name, args[0] === undefined ? "" : coerceToString(args[0])) } throw new InterpreterRuntimeError("Only tools are callable in CodeMode.", callee) }) } + // Object.* over a tool reference: `Object.keys(tools)` / `Object.keys(tools.ns)` enumerate + // namespace/tool names from the host tool tree - the discovery idiom a model reaches for + // first. Every other Object helper cannot produce data from a tool reference, so it fails + // with a pointer at the working idioms instead of the generic plain-objects-only message. private invokeObjectMethodOnTools(name: string, ref: ToolReference, node: AstNode): unknown { if (name === "keys") { return boundedData(this.enumerableKeys(ref)!, "Object.keys result") } throw new InterpreterRuntimeError( - `Object.${name}(...) cannot read tool references: they are not plain data. Use Object.keys(tools) for names, or search({ query }) for signatures.`, + `Object.${name}(...) cannot read tool references: they are not plain data. Use Object.keys(tools) for names, or tools.$codemode.search({ query }) for signatures.`, node, "InvalidDataValue", ) @@ -1465,10 +1957,126 @@ export class Interpreter { private invokeConsole(name: string, args: Array, node: AstNode): undefined { if (!consoleMethods.has(name)) throw new InterpreterRuntimeError(`console.${name} is not available in CodeMode.`, node) - this.logs.push(formatConsoleMessage(name, args)) + this.logs.push(publicErrorMessage(this.formatConsoleMessage(name, args, node))) return undefined } + private formatConsoleMessage(name: string, args: Array, node: AstNode): string { + if (name === "dir") return args.length === 0 ? "undefined" : this.formatConsoleArgument(args[0]) + if (name === "table") return this.formatConsoleTable(args[0], args[1], node) + const prefix = name === "warn" ? "[warn] " : name === "error" ? "[error] " : name === "debug" ? "[debug] " : "" + return `${prefix}${args.map((arg) => this.formatConsoleArgument(arg)).join(" ")}` + } + + // Console arguments format deeply and totally: values render as a debugger would show them + // rather than as boundary JSON - numbers keep NaN/Infinity (JSON would say null), sandbox + // values keep their friendly forms at ANY depth (ISO date, /regex/flags, Map(n) [...], + // Set(n) [...]), opaque runtime references become "[CodeMode reference]" markers in place, + // and plain objects/arrays render JSON-style. Formatting never fails the program: cycles + // render "[Circular]" and extreme depth degrades to "...". + private formatConsoleArgument(value: unknown): string { + if (value === undefined) return "undefined" + // A top-level string prints bare; nested strings are JSON-quoted (see formatConsoleValue). + if (typeof value === "string") return value + return this.formatConsoleValue(value, new Set(), 0) + } + + private formatConsoleValue(value: unknown, seen: Set, depth: number): string { + // Nested undefined renders as null, matching what JSON boundary output would show. + if (value === null || value === undefined) return "null" + if (typeof value === "string") return JSON.stringify(value) + // String(value) keeps NaN/Infinity/-Infinity readable; finite numbers match their JSON form. + if (typeof value === "number" || typeof value === "boolean") return String(value) + if (typeof value !== "object") return String(value) + if (value instanceof SandboxPromise) return "[Promise (await it to get its value)]" + if (value instanceof SandboxDate) return coerceToString(value) + if (value instanceof SandboxRegExp) return coerceToString(value) + if (value instanceof SandboxURL) return coerceToString(value) + if (value instanceof SandboxURLSearchParams) return coerceToString(value) + if (depth > MAX_CONSOLE_DEPTH) return "..." + if (seen.has(value)) return "[Circular]" + if (value instanceof SandboxMap) { + seen.add(value) + try { + const entries = Array.from(value.map.entries(), ([key, item]): Array => [key, item]) + return `Map(${value.map.size}) ${this.formatConsoleValue(entries, seen, depth + 1)}` + } finally { + seen.delete(value) + } + } + if (value instanceof SandboxSet) { + seen.add(value) + try { + return `Set(${value.set.size}) ${this.formatConsoleValue(Array.from(value.set.values()), seen, depth + 1)}` + } finally { + seen.delete(value) + } + } + if (isRuntimeReference(value)) return "[CodeMode reference]" + seen.add(value) + try { + if (Array.isArray(value)) { + return `[${value.map((item) => this.formatConsoleValue(item, seen, depth + 1)).join(",")}]` + } + return `{${Object.entries(value) + .map(([key, item]) => `${JSON.stringify(key)}:${this.formatConsoleValue(item, seen, depth + 1)}`) + .join(",")}}` + } finally { + seen.delete(value) + } + } + + private formatConsoleTable(value: unknown, columnsArgument: unknown, node: AstNode): string { + if (value === undefined) return "undefined" + // Sandbox values are legitimate table data (cells render their friendly forms); only + // truly opaque references (functions, tools, promises) collapse to the marker. + if (containsOpaqueReference(value)) return "[CodeMode reference]" + const data = boundedData(value, "console.table argument") + const columns = this.consoleTableColumns(columnsArgument, node) + const rows = this.consoleTableRows(data, columns) + const keys = columns ?? Array.from(new Set(rows.flatMap((row) => Object.keys(row.values)))) + const header = ["(index)", ...keys].join("\t") + return [ + header, + ...rows.map((row) => [row.index, ...keys.map((key) => this.formatConsoleTableCell(row.values[key]))].join("\t")), + ].join("\n") + } + + private consoleTableColumns(value: unknown, node: AstNode): ReadonlyArray | undefined { + if (value === undefined) return undefined + if (containsRuntimeReference(value)) return undefined + const columns = copyOut(copyIn(value, "console.table columns"), true) + return Array.isArray(columns) ? columns.map((column) => String(column)) : undefined + } + + private consoleTableRows( + data: unknown, + columns: ReadonlyArray | undefined, + ): Array<{ readonly index: string; readonly values: Record }> { + if (Array.isArray(data)) { + return data.map((item, index) => ({ index: String(index), values: this.consoleTableValues(item, columns) })) + } + if (data !== null && typeof data === "object" && !isSandboxValue(data)) { + return Object.entries(data).map(([index, item]) => ({ index, values: this.consoleTableValues(item, columns) })) + } + return [{ index: "0", values: { Value: data } }] + } + + private consoleTableValues(value: unknown, columns: ReadonlyArray | undefined): Record { + if (value !== null && typeof value === "object" && !Array.isArray(value) && !isSandboxValue(value)) { + const source = value as Record + if (columns !== undefined) return Object.fromEntries(columns.map((column) => [column, source[column]])) + return Object.fromEntries(Object.entries(source)) + } + return { Value: value } + } + + private formatConsoleTableCell(value: unknown): string { + if (value === undefined) return "" + if (typeof value === "string") return value + return this.formatConsoleValue(value, new Set(), 0) + } + private evaluateCallArguments(argNodes: Array): Effect.Effect, unknown, R> { const self = this return Effect.gen(function* () { @@ -1492,48 +2100,703 @@ export class Interpreter { }) } + // Promise.* over ordinary runtime values. Combinators accept ANY array (or spreadable + // collection) mixing promise values and plain data - built inline, beforehand, via spread, + // whatever - because tool calls already run eagerly on their own fibers; the combinators + // only observe settlements. Joining is therefore sequential (no extra fibers) without + // costing parallelism, and the concurrency cap stays where the work is: the fork semaphore. + private invokePromiseMethod( + ref: PromiseMethodReference, + args: Array, + node: AstNode, + ): Effect.Effect { + const self = this + if (ref.name === "resolve") { + // Promise.resolve of a promise is that promise (JS flattens); anything else is a + // promise already fulfilled with the value. + const value = args[0] + return Effect.succeed( + value instanceof SandboxPromise ? value : new SandboxPromise(undefined, Effect.succeed(value)), + ) + } + if (ref.name === "reject") { + return Effect.sync(() => new SandboxPromise(undefined, Effect.fail(new ProgramThrow(args[0])))) + } + + const items = Array.isArray(args[0]) ? args[0] : spreadItems(args[0]) + if (items === undefined) { + throw new InterpreterRuntimeError( + `Promise.${ref.name} expects an array of promises or plain values (e.g. Promise.${ref.name}(items.map((item) => tools.ns.tool(item)))).`, + node, + ) + } + + switch (ref.name) { + case "all": { + // Mark every promise element observed up-front (Promise.all handles all of its + // members' failures, as in JS), then join in index order; the first failure rejects + // the whole call while unrelated in-flight members keep running. + const settles = items.map((item) => + item instanceof SandboxPromise ? this.settlePromise(item, node) : Effect.succeed(item), + ) + return Effect.gen(function* () { + const values: Array = [] + for (const settle of settles) values.push(yield* settle) + return values + }) + } + case "allSettled": { + const observations = items.map((item) => + item instanceof SandboxPromise + ? Effect.map(this.observePromise(item), (exit) => ({ promise: item as SandboxPromise | undefined, exit })) + : Effect.succeed({ promise: undefined as SandboxPromise | undefined, exit: Exit.succeed(item as unknown) }), + ) + return Effect.gen(function* () { + const outcomes: Array = [] + for (const observation of observations) { + const { exit, promise } = yield* observation + if (Exit.isSuccess(exit)) { + outcomes.push( + Object.assign(Object.create(null) as SafeObject, { status: "fulfilled", value: exit.value }), + ) + continue + } + const raceInterrupted = promise?.interrupted === true && Cause.hasInterruptsOnly(exit.cause) + if (Cause.hasInterruptsOnly(exit.cause) && !raceInterrupted) { + // Execution teardown (timeout/host interruption), not a program-level rejection. + return yield* Effect.failCause(exit.cause) + } + const thrown = raceInterrupted + ? new InterpreterRuntimeError( + "This tool call was interrupted because another value settled a Promise.race first.", + node, + ) + : Cause.squash(exit.cause) + outcomes.push( + Object.assign(Object.create(null) as SafeObject, { + status: "rejected", + reason: caughtErrorValue(thrown), + }), + ) + } + return outcomes + }) + } + case "race": { + if (items.length === 0) { + throw new InterpreterRuntimeError( + "Promise.race([]) would never settle; provide at least one promise or value.", + node, + ) + } + const observations = items.map((item, index) => + item instanceof SandboxPromise + ? Effect.map(this.observePromise(item), (exit) => ({ index, exit })) + : Effect.succeed({ index, exit: Exit.succeed(item as unknown) }), + ) + return Effect.gen(function* () { + // First settlement (fulfilled OR rejected) wins; the observations never fail, so + // racing them yields exactly that. Losing in-flight calls are then interrupted. + const winner = yield* Effect.raceAll(observations) + for (const [index, item] of items.entries()) { + if (index === winner.index || !(item instanceof SandboxPromise) || item.fiber === undefined) continue + item.interrupted = true + yield* Fiber.interrupt(item.fiber) + } + const winningItem = items[winner.index] + return yield* self.unwrapPromiseExit( + winningItem instanceof SandboxPromise ? winningItem : undefined, + winner.exit, + node, + ) + }) + } + } + } + private invokeFunction(fn: CodeModeFunction, args: Array): Effect.Effect { - const invocation = new Interpreter(this.invokeTool, this.invokeSearch, this.toolKeys, this.promises, this.logs) - invocation.scopes = new ScopeStack([...fn.capturedScopes, new Map()]) - const run = Effect.gen(function* () { - // Seed all parameters first so defaults cannot fall through to same-named outer bindings. - const paramScope = invocation.scopes.current() - for (const parameter of fn.parameters) { - for (const name of collectPatternNames(parameter)) { - paramScope.set(name, { mutable: true, value: undefined, initialized: false }) + const self = this + return Effect.suspend(() => { + const savedScopes = self.scopes + self.scopes = [...fn.capturedScopes, new Map()] + const run = Effect.gen(function* () { + // Seed every parameter name into the scope as a TDZ slot first, so a default that + // references another parameter resolves to that (uninitialized) param rather than + // silently falling through to an outer binding of the same name - matching JS. + const paramScope = self.currentScope() + for (const parameter of fn.parameters) { + for (const name of collectPatternNames(parameter)) { + paramScope.set(name, { mutable: true, value: undefined, initialized: false }) + } } - } - for (const [index, parameter] of fn.parameters.entries()) { - if (parameter.type === "RestElement") { - yield* invocation.declarePattern(getNode(parameter, "argument"), args.slice(index), true, parameter) - break + for (const [index, parameter] of fn.parameters.entries()) { + if (parameter.type === "RestElement") { + yield* self.declarePattern(getNode(parameter, "argument"), args.slice(index), true, parameter) + break + } + yield* self.declarePattern(parameter, args[index], true, parameter) } - yield* invocation.declarePattern(parameter, args[index], true, parameter) - } - if (fn.body.type === "BlockStatement") { - const result = yield* invocation.evaluateStatement(fn.body) - return result.kind === "return" ? result.value : undefined - } + if (fn.body.type === "BlockStatement") { + const result = yield* self.evaluateStatement(fn.body) + return result.kind === "return" || result.kind === "value" ? result.value : undefined + } - return yield* invocation.evaluateExpression(fn.body) + return yield* self.evaluateExpression(fn.body) + }) + return run.pipe( + Effect.ensuring( + Effect.sync(() => { + self.scopes = savedScopes + }), + ), + ) }) - if (!fn.async) return run - // The initial yield assigns `box.own` before the body can self-resolve. - const box: { own?: SandboxPromise } = {} - return Effect.map( - this.createPromise( - Effect.flatMap(run, (value) => { - if (!(value instanceof SandboxPromise)) return Effect.succeed(value) - if (value === box.own) return Effect.fail(selfResolutionError()) - return invocation.settlePromise(value) + } + + private invokeIntrinsic( + ref: IntrinsicReference, + args: Array, + node: AstNode, + ): Effect.Effect { + if (typeof ref.receiver === "string") { + if ( + (ref.name === "replace" || ref.name === "replaceAll") && + (args[1] instanceof CodeModeFunction || args[1] instanceof CoercionFunction || args[1] instanceof UriFunction) + ) { + return this.invokeStringReplacer(ref.receiver, ref.name, args, node) + } + return Effect.succeed(invokeStringMethod(ref.receiver, ref.name, args, node)) + } + if (typeof ref.receiver === "number") { + return Effect.succeed(invokeNumberMethod(ref.receiver, ref.name, args, node)) + } + if (Array.isArray(ref.receiver)) { + return this.invokeArrayMethod(ref.receiver, ref.name, args, node) + } + if (ref.receiver instanceof SandboxDate) { + return Effect.succeed(invokeDateMethod(ref.receiver, ref.name, node)) + } + if (ref.receiver instanceof SandboxRegExp) { + return Effect.succeed(invokeRegExpMethod(ref.receiver, ref.name, args, node)) + } + if (ref.receiver instanceof SandboxMap) { + return this.invokeMapMethod(ref.receiver, ref.name, args, node) + } + if (ref.receiver instanceof SandboxSet) { + return this.invokeSetMethod(ref.receiver, ref.name, args, node) + } + if (ref.receiver instanceof SandboxURL) { + return Effect.succeed(invokeURLMethod(ref.receiver, ref.name, node)) + } + if (ref.receiver instanceof SandboxURLSearchParams) { + return this.invokeURLSearchParamsMethod(ref.receiver, ref.name, args, node) + } + throw new InterpreterRuntimeError(`Method '${ref.name}' is not available in CodeMode.`, node) + } + + private invokeStringReplacer( + value: string, + name: "replace" | "replaceAll", + args: Array, + node: AstNode, + ): Effect.Effect { + const apply = this.applyCollectionCallback(args[1], `String.${name}`, node) + const matches: Array<{ readonly match: string; readonly offset: number; readonly args: Array }> = [] + const collect = (...callbackArgs: Array): string => { + const match = callbackArgs[0] + const groups = callbackArgs[callbackArgs.length - 1] + const hasGroups = groups !== null && typeof groups === "object" + const offset = callbackArgs[callbackArgs.length - (hasGroups ? 3 : 2)] + if (typeof match !== "string" || typeof offset !== "number") { + throw new InterpreterRuntimeError(`String.${name} produced an invalid replacement match.`, node) + } + if (hasGroups) { + const safeGroups: SafeObject = Object.create(null) as SafeObject + for (const [key, group] of Object.entries(groups)) { + if (!isBlockedMember(key)) safeGroups[key] = group + } + callbackArgs[callbackArgs.length - 1] = safeGroups + } + matches.push({ match, offset, args: callbackArgs }) + return match + } + + const pattern = args[0] + if (pattern instanceof SandboxRegExp) { + if (name === "replaceAll" && !pattern.regex.global) { + throw new InterpreterRuntimeError( + `String.replaceAll requires a regular expression with the global (g) flag: write /${pattern.regex.source}/${pattern.regex.flags}g, or use String.replace to replace only the first match.`, + node, + ) + } + if (name === "replace") value.replace(pattern.regex, collect) + else value.replaceAll(pattern.regex, collect) + } else { + if (typeof pattern !== "string") { + throw new InterpreterRuntimeError(`String.${name} expects argument 1 to be a string.`, node) + } + if (name === "replace") value.replace(pattern, collect) + else value.replaceAll(pattern, collect) + } + + return Effect.gen(function* () { + const output: Array = [] + let end = 0 + for (const match of matches) { + output.push( + value.slice(end, match.offset), + coerceToString(boundedData(yield* apply(match.args), `String.${name} replacer result`)), + ) + end = match.offset + match.match.length + } + output.push(value.slice(end)) + return boundedData(output.join(""), `String.${name} result`) + }) + } + + // Runs a collection callback accepting a user function or supported builtin callable, + // mirroring the array-method callback contract. + private applyCollectionCallback( + callback: unknown, + name: string, + node: AstNode, + ): (args: Array) => Effect.Effect { + if ( + !(callback instanceof CodeModeFunction) && + !(callback instanceof CoercionFunction) && + !(callback instanceof UriFunction) + ) { + throw new InterpreterRuntimeError(`${name} expects a function callback.`, node) + } + return (callbackArgs) => + callback instanceof CoercionFunction + ? Effect.succeed(invokeCoercion(callback, callbackArgs, node)) + : callback instanceof UriFunction + ? Effect.succeed(invokeUriFunction(callback, callbackArgs, node)) + : this.invokeFunction(callback, callbackArgs) + } + + private invokeMapMethod( + target: SandboxMap, + name: string, + args: Array, + node: AstNode, + ): Effect.Effect { + switch (name) { + case "get": + return Effect.succeed(target.map.get(args[0])) + case "has": + return Effect.succeed(target.map.has(args[0])) + case "set": + return Effect.sync(() => { + target.map.set(args[0], args[1]) + return target + }) + case "delete": + return Effect.sync(() => target.map.delete(args[0])) + case "clear": + return Effect.sync(() => { + target.map.clear() + return undefined + }) + case "keys": + return Effect.sync(() => Array.from(target.map.keys())) + case "values": + return Effect.sync(() => Array.from(target.map.values())) + case "entries": + return Effect.sync(() => Array.from(target.map.entries(), ([key, item]): Array => [key, item])) + case "forEach": { + const apply = this.applyCollectionCallback(args[0], "Map.forEach", node) + return Effect.gen(function* () { + // Snapshot iteration, matching the array-method callback contract. + for (const [key, item] of Array.from(target.map.entries())) yield* apply([item, key, target]) + return undefined + }) + } + default: + throw new InterpreterRuntimeError(`Map method '${name}' is not available in CodeMode.`, node) + } + } + + private invokeSetMethod( + target: SandboxSet, + name: string, + args: Array, + node: AstNode, + ): Effect.Effect { + switch (name) { + case "has": + return Effect.succeed(target.set.has(args[0])) + case "add": + return Effect.sync(() => { + target.set.add(args[0]) + return target + }) + case "delete": + return Effect.sync(() => target.set.delete(args[0])) + case "clear": + return Effect.sync(() => { + target.set.clear() + return undefined + }) + case "keys": + case "values": + return Effect.sync(() => Array.from(target.set.values())) + case "entries": + return Effect.sync(() => Array.from(target.set.values(), (item): Array => [item, item])) + case "forEach": { + const apply = this.applyCollectionCallback(args[0], "Set.forEach", node) + return Effect.gen(function* () { + for (const item of Array.from(target.set.values())) yield* apply([item, item, target]) + return undefined + }) + } + default: + throw new InterpreterRuntimeError(`Set method '${name}' is not available in CodeMode.`, node) + } + } + + private invokeURLSearchParamsMethod( + target: SandboxURLSearchParams, + name: string, + args: Array, + node: AstNode, + ): Effect.Effect { + const arg = (index: number): string => uriArgument(args[index], `URLSearchParams.${name} argument ${index + 1}`) + const requireArgs = (count: number): void => { + if (args.length < count) { + throw new InterpreterRuntimeError( + `URLSearchParams.${name} requires ${count} argument${count === 1 ? "" : "s"}.`, + node, + ).as("TypeError") + } + } + switch (name) { + case "append": { + requireArgs(2) + return Effect.sync(() => { + target.params.append(arg(0), arg(1)) + return undefined + }) + } + case "delete": { + requireArgs(1) + return Effect.sync(() => { + if (args[1] !== undefined) target.params.delete(arg(0), arg(1)) + else target.params.delete(arg(0)) + return undefined + }) + } + case "get": + requireArgs(1) + return Effect.sync(() => target.params.get(arg(0))) + case "getAll": + requireArgs(1) + return Effect.sync(() => target.params.getAll(arg(0))) + case "has": + requireArgs(1) + return Effect.sync(() => + args[1] !== undefined ? target.params.has(arg(0), arg(1)) : target.params.has(arg(0)), + ) + case "set": { + requireArgs(2) + return Effect.sync(() => { + target.params.set(arg(0), arg(1)) + return undefined + }) + } + case "sort": + return Effect.sync(() => { + target.params.sort() + return undefined + }) + case "keys": + return Effect.sync(() => Array.from(target.params.keys())) + case "values": + return Effect.sync(() => Array.from(target.params.values())) + case "entries": + return Effect.sync(() => Array.from(target.params.entries(), ([key, value]): Array => [key, value])) + case "toString": + return Effect.sync(() => target.params.toString()) + case "forEach": { + requireArgs(1) + const apply = this.applyCollectionCallback(args[0], "URLSearchParams.forEach", node) + return Effect.gen(function* () { + for (const [key, value] of Array.from(target.params.entries())) yield* apply([value, key, target]) + return undefined + }) + } + default: + throw new InterpreterRuntimeError(`URLSearchParams method '${name}' is not available in CodeMode.`, node) + } + } + + private invokeArrayMethod( + target: Array, + name: string, + args: Array, + node: AstNode, + ): Effect.Effect { + const optNumber = (value: unknown, label: string): number | undefined => { + if (value === undefined) return undefined + if (typeof value !== "number") + throw new InterpreterRuntimeError(`Array.${name} expects ${label} to be a number.`, node) + return value + } + switch (name) { + case "join": { + if (args.length > 1 || (args.length === 1 && typeof args[0] !== "string")) { + throw new InterpreterRuntimeError("Array.join expects zero arguments or one string separator.", node) + } + const input = boundedData(target, "Array.join input") as Array + return Effect.succeed( + input.map((item) => coerceToString(item ?? "")).join(args.length === 0 ? "," : (args[0] as string)), + ) + } + case "includes": + if (args.length === 0 || args.length > 2) + throw new InterpreterRuntimeError("Array.includes expects a value and optional start index.", node) + return Effect.succeed(target.includes(args[0], optNumber(args[1], "start index"))) + case "indexOf": + return Effect.succeed(target.indexOf(args[0], optNumber(args[1], "start index"))) + case "lastIndexOf": + return Effect.succeed( + args[1] === undefined + ? target.lastIndexOf(args[0]) + : target.lastIndexOf(args[0], optNumber(args[1], "start index")), + ) + case "at": + return Effect.succeed(target.at(optNumber(args[0], "index") ?? 0)) + case "slice": + return Effect.succeed(target.slice(optNumber(args[0], "start"), optNumber(args[1], "end"))) + case "concat": + return Effect.succeed(target.concat(...args)) + case "flat": + return Effect.succeed(target.flat(optNumber(args[0], "depth") ?? 1)) + case "reverse": + return Effect.succeed([...target].reverse()) + case "sort": + case "toSorted": + return this.sortArray(target, args[0], node) + case "toReversed": + return Effect.succeed([...target].reverse()) + case "with": { + const index = optNumber(args[0], "index") ?? 0 + const resolved = index < 0 ? target.length + index : index + if (resolved < 0 || resolved >= target.length) { + throw new InterpreterRuntimeError("Array.with index is out of range.", node) + } + const copied = [...target] + copied[resolved] = args[1] + return Effect.succeed(copied) + } + case "push": { + // Validate before mutating (so no rollback is needed): inserting a container into + // itself would create a cycle no later walk could survive. + for (const item of args) this.rejectCircularInsertion(target, item, "Array.push result", node) + target.push(...args) + return Effect.succeed(target.length) + } + case "unshift": { + for (const item of args) this.rejectCircularInsertion(target, item, "Array.unshift result", node) + target.unshift(...args) + return Effect.succeed(target.length) + } + case "pop": + return Effect.succeed(target.pop()) + case "shift": + return Effect.succeed(target.shift()) + case "splice": { + // Mutates in place and returns the removed elements, exactly like JS: one argument + // removes to the end, an undefined delete count removes nothing. + if (args.length === 0) return Effect.succeed(target.splice(0, 0)) + const start = optNumber(args[0], "start") ?? 0 + if (args.length === 1) return Effect.succeed(target.splice(start)) + const deleteCount = optNumber(args[1], "delete count") ?? 0 + const inserted = args.slice(2) + for (const item of inserted) this.rejectCircularInsertion(target, item, "Array.splice result", node) + return Effect.succeed(target.splice(start, deleteCount, ...inserted)) + } + case "fill": { + this.rejectCircularInsertion(target, args[0], "Array.fill result", node) + return Effect.succeed(target.fill(args[0], optNumber(args[1], "start"), optNumber(args[2], "end"))) + } + case "copyWithin": + return Effect.succeed( + target.copyWithin( + optNumber(args[0], "target index") ?? 0, + optNumber(args[1], "start") ?? 0, + optNumber(args[2], "end"), + ), + ) + // keys/values/entries return arrays (not iterators), matching the Map/Set convention; + // they work with for...of and spread either way. + case "keys": + return Effect.succeed(Array.from(target.keys())) + case "values": + return Effect.succeed([...target]) + case "entries": + return Effect.succeed(Array.from(target.entries(), ([index, item]): Array => [index, item])) + } + + const callback = args[0] + if ( + !(callback instanceof CodeModeFunction) && + !(callback instanceof CoercionFunction) && + !(callback instanceof UriFunction) + ) { + throw new InterpreterRuntimeError(`Array.${name} expects a function callback.`, node) + } + const self = this + // Accept a user function or supported builtin callable, so idioms such as + // `filter(Boolean)`, `map(String)`, and `map(encodeURIComponent)` work as in JS. Builtins + // are synchronous; only CodeModeFunctions can await tool calls. + const apply = (callbackArgs: Array): Effect.Effect => + callback instanceof CoercionFunction + ? Effect.succeed(invokeCoercion(callback, callbackArgs, node)) + : callback instanceof UriFunction + ? Effect.succeed(invokeUriFunction(callback, callbackArgs, node)) + : self.invokeFunction(callback, callbackArgs) + return Effect.gen(function* () { + // Iterate a snapshot taken at call time so a callback that mutates the array can't + // self-extend the loop - matching JS, where elements appended during iteration are not visited. + const items = target.slice() + switch (name) { + case "map": { + const values: Array = [] + for (const [index, item] of items.entries()) values.push(yield* apply([item, index, items])) + return values + } + case "flatMap": { + const values: Array = [] + for (const [index, item] of items.entries()) { + const mapped = yield* apply([item, index, items]) + if (Array.isArray(mapped)) values.push(...mapped) + else values.push(mapped) + } + return values + } + case "filter": { + const values: Array = [] + for (const [index, item] of items.entries()) { + if (yield* apply([item, index, items])) values.push(item) + } + return values + } + case "find": + for (const [index, item] of items.entries()) { + if (yield* apply([item, index, items])) return item + } + return undefined + case "findIndex": + for (const [index, item] of items.entries()) { + if (yield* apply([item, index, items])) return index + } + return -1 + case "some": + for (const [index, item] of items.entries()) { + if (yield* apply([item, index, items])) return true + } + return false + case "every": + for (const [index, item] of items.entries()) { + if (!(yield* apply([item, index, items]))) return false + } + return true + case "forEach": + for (const [index, item] of items.entries()) yield* apply([item, index, items]) + return undefined + case "reduce": { + let accumulator: unknown + let start: number + if (args.length >= 2) { + accumulator = args[1] + start = 0 + } else { + if (items.length === 0) + throw new InterpreterRuntimeError("Array.reduce of an empty array with no initial value.", node) + accumulator = items[0] + start = 1 + } + for (let index = start; index < items.length; index += 1) { + accumulator = yield* apply([accumulator, items[index], index, items]) + } + return accumulator + } + case "reduceRight": { + let accumulator: unknown + let start: number + if (args.length >= 2) { + accumulator = args[1] + start = items.length - 1 + } else { + if (items.length === 0) + throw new InterpreterRuntimeError("Array.reduceRight of an empty array with no initial value.", node) + accumulator = items[items.length - 1] + start = items.length - 2 + } + for (let index = start; index >= 0; index -= 1) { + accumulator = yield* apply([accumulator, items[index], index, items]) + } + return accumulator + } + case "findLast": + for (let index = items.length - 1; index >= 0; index -= 1) { + if (yield* apply([items[index], index, items])) return items[index] + } + return undefined + case "findLastIndex": + for (let index = items.length - 1; index >= 0; index -= 1) { + if (yield* apply([items[index], index, items])) return index + } + return -1 + } + throw new InterpreterRuntimeError(`Array method '${name}' is not available in CodeMode.`, node) + }) + } + + private sortArray( + target: Array, + comparator: unknown, + node: AstNode, + ): Effect.Effect, unknown, R> { + if (comparator !== undefined && !(comparator instanceof CodeModeFunction)) { + throw new InterpreterRuntimeError("Array.sort expects an arrow function comparator.", node) + } + if (!(comparator instanceof CodeModeFunction)) { + return Effect.sync(() => + [...target].sort((a, b) => { + const left = coerceToString(a) + const right = coerceToString(b) + return left < right ? -1 : left > right ? 1 : 0 }), - ), - (promise) => { - box.own = promise - return promise - }, - ) + ) + } + const self = this + const mergeSort = (items: Array): Effect.Effect, unknown, R> => { + if (items.length <= 1) return Effect.succeed(items) + const midpoint = Math.floor(items.length / 2) + return Effect.gen(function* () { + const left = yield* mergeSort(items.slice(0, midpoint)) + const right = yield* mergeSort(items.slice(midpoint)) + const merged: Array = [] + let leftIndex = 0 + let rightIndex = 0 + while (leftIndex < left.length && rightIndex < right.length) { + // Coerce the comparator's result like JS ToNumber (data objects -> NaN, never a host + // crash) and treat NaN as 0 - the spec's "no consistent order" -> keep the left element. + const order = coerceToNumber(yield* self.invokeFunction(comparator, [left[leftIndex], right[rightIndex]])) + if (Number.isNaN(order) || order <= 0) merged.push(left[leftIndex++]) + else merged.push(right[rightIndex++]) + } + return [...merged, ...left.slice(leftIndex), ...right.slice(rightIndex)] + }) + } + // Per spec, undefined elements sort to the end and the comparator is never called on them. + const defined = target.filter((item) => item !== undefined) + const undefinedCount = target.length - defined.length + return Effect.map(mergeSort(defined), (items) => [...items, ...Array(undefinedCount).fill(undefined)]) } private evaluateObjectExpression(node: AstNode): Effect.Effect, unknown, R> { @@ -1546,6 +2809,9 @@ export class Interpreter { if (property.type === "SpreadElement") { const spread = yield* self.evaluateExpression(getNode(property, "argument")) + // JS treats `{ ...null }` / `{ ...undefined }` as a no-op, so the common + // `{ ...maybeOpts, override }` merge works when the operand is absent. Sandbox values + // have no own enumerable properties in JS, so they are no-ops too. if (spread === null || spread === undefined || isSandboxValue(spread)) continue if (typeof spread !== "object" || Array.isArray(spread) || isRuntimeReference(spread)) { throw new InterpreterRuntimeError( @@ -1645,6 +2911,8 @@ export class Interpreter { if (index < expressions.length) { const raw = yield* self.evaluateExpression(asNode(expressions[index], "expressions")) + // The preserving checkpoint keeps sandbox values intact, so coerceToString renders + // them directly (ISO date, /regex/ literal form) instead of a JSON-serialized husk. output += coerceToString(boundedData(raw, "Template interpolation")) } } @@ -1660,6 +2928,10 @@ export class Interpreter { } private applyCompoundAssignment(operator: string, current: unknown, incoming: unknown, node: AstNode): unknown { + // `x op= y` is `x = x op y`: dispatch through the shared binary operator implementation + // so compound assignment inherits the same coercion semantics (Dates, data objects, ...). + // Only the arithmetic/bitwise operators are compoundable; logical assignments (&&=/||=/??=) + // short-circuit and are handled by evaluateLogicalAssignment before reaching here. if (!compoundOperators.has(operator)) { throw new InterpreterRuntimeError(`Unsupported assignment operator '${operator}'.`, node) } @@ -1672,7 +2944,6 @@ export class Interpreter { | MemberReference | ToolReference | PromiseMethodReference - | PromiseInstanceMethodReference | IntrinsicReference | GlobalMethodReference | ComputedValue @@ -1709,7 +2980,7 @@ export class Interpreter { return new PromiseMethodReference(key as PromiseMethodName) } throw new InterpreterRuntimeError( - `Promise.${String(key)} is not available in CodeMode. Available: Promise.all, Promise.allSettled, Promise.race, Promise.any, Promise.resolve, and Promise.reject; consume promises with await.`, + `Promise.${String(key)} is not available in CodeMode. Available: Promise.all, Promise.allSettled, Promise.race, Promise.resolve, and Promise.reject; consume promises with await.`, propertyNode, ) } @@ -1732,14 +3003,20 @@ export class Interpreter { if (typeof key === "number") return new ComputedValue(objectValue[key]) if (typeof key === "string" && /^\d+$/.test(key)) return new ComputedValue(objectValue[Number(key)]) if (typeof key === "string" && stringMethods.has(key)) return new IntrinsicReference(objectValue, key) + // Unknown property on a string reads as `undefined`, matching JS (`"x".foo === undefined`), + // instead of throwing - so defensive access like `result?.login ?? result` on a JSON-string + // tool result doesn't crash. (Optional chaining only guards null/undefined receivers, so a + // real string still reaches here.) Only the method allowlist above yields callables. return new ComputedValue(undefined) } if (typeof objectValue === "number") { if (typeof key === "string" && numberMethods.has(key)) return new IntrinsicReference(objectValue, key) + // Unknown property on a number reads as `undefined`, matching JS, rather than throwing. return new ComputedValue(undefined) } + // Number / String expose a small allowlist of statics; everything else stays opaque. if (objectValue instanceof CoercionFunction && typeof key === "string" && !isBlockedMember(key)) { if (objectValue.name === "Number" && numberConstants.has(key)) { return new ComputedValue((Number as unknown as Record)[key]) @@ -1748,6 +3025,8 @@ export class Interpreter { if (objectValue.name === "String" && stringStatics.has(key)) return new GlobalMethodReference("String", key) } + // Sandbox value types expose their method/property allowlists; any other key reads as + // `undefined`, consistent with unknown-property reads on strings/numbers/arrays. if (objectValue instanceof SandboxDate) { if (typeof key === "string" && dateMethods.has(key)) return new IntrinsicReference(objectValue, key) return new ComputedValue(undefined) @@ -1785,13 +3064,20 @@ export class Interpreter { return new ComputedValue(undefined) } - // Reject unknown promise properties so a missing await cannot hide. + // Any property access on a promise is a confused program (`p.then(...)`, `p.value`); + // reading `undefined` here would hide the missing await, so both paths get an explicit, + // await-hinting error instead of the forgiving unknown-property fallthrough. if (objectValue instanceof SandboxPromise) { if (key === "then" || key === "catch" || key === "finally") { - return new PromiseInstanceMethodReference(objectValue, key) + throw new InterpreterRuntimeError( + `Promise.prototype.${String(key)} is not supported in CodeMode; use await instead (with try/catch to handle failures) - e.g. \`const result = await tools.ns.tool(...)\`.`, + propertyNode, + "UnsupportedSyntax", + [supportedSyntaxMessage], + ) } throw new InterpreterRuntimeError( - "This value is an un-awaited Promise; await it first - e.g. `const result = await tools.ns.tool(...)`.", + "This value is an un-awaited Promise and has no readable properties; await it first - e.g. `const result = await tools.ns.tool(...)`.", objectNode, "InvalidDataValue", ) @@ -1820,9 +3106,13 @@ export class Interpreter { typeof key !== "number" && !/^\d+$/.test(key) ) { + // Own non-index properties read through (match results carry index/groups); like JS, + // they are readable in place and dropped by JSON at data boundaries. if (typeof key === "string" && Object.hasOwn(objectValue, key)) { return new ComputedValue((objectValue as Record & Array)[key]) } + // Unknown property on an array reads as `undefined`, matching JS (`[1,2].foo === undefined`), + // instead of throwing - so defensive access under optional chaining behaves as expected. return new ComputedValue(undefined) } return { target: objectValue, key } @@ -1840,7 +3130,6 @@ export class Interpreter { reference === undefined || reference instanceof ToolReference || reference instanceof PromiseMethodReference || - reference instanceof PromiseInstanceMethodReference || reference instanceof IntrinsicReference || reference instanceof GlobalMethodReference ) @@ -1862,7 +3151,9 @@ export class Interpreter { return this.modifyMember(node, () => Effect.succeed({ write: true, next: value, result: value })) } - // Resolve side-effecting object and key expressions exactly once. + // Resolves the member reference EXACTLY ONCE (so a side-effecting object/key expression + // runs once), then lets `compute` decide whether to write - enabling compound assignment, + // updates, plain writes, and short-circuiting logical assignment to share one safe path. private modifyMember( node: AstNode, compute: (current: unknown) => Effect.Effect<{ write: boolean; next: unknown; result: unknown }, unknown, R>, @@ -1876,7 +3167,6 @@ export class Interpreter { reference === undefined || reference instanceof ToolReference || reference instanceof PromiseMethodReference || - reference instanceof PromiseInstanceMethodReference || reference instanceof IntrinsicReference || reference instanceof GlobalMethodReference ) { @@ -1900,6 +3190,24 @@ export class Interpreter { }) } + // Rejects inserting a value that (transitively) contains the container it is being inserted + // into - the mutation that would create a circular structure no later walk could survive. + private rejectCircularInsertion( + container: object, + value: unknown, + label: string, + node: AstNode, + seen = new Set(), + ): void { + if (value === container) + throw new InterpreterRuntimeError(`${label} contains a circular value.`, node, "InvalidDataValue") + if (value === null || typeof value !== "object" || isRuntimeReference(value) || seen.has(value)) return + seen.add(value) + const items = Array.isArray(value) ? value : Object.values(value) + for (const item of items) this.rejectCircularInsertion(container, item, label, node, seen) + seen.delete(value) + } + private assignToReference(reference: MemberReference, key: number | string, next: unknown, node: AstNode): void { if (Array.isArray(reference.target)) { const target = reference.target @@ -1911,7 +3219,7 @@ export class Interpreter { "InvalidDataValue", ) } - rejectCircularInsertion(target, next, "Array assignment result", node) + this.rejectCircularInsertion(target, next, "Array assignment result", node) target[index] = next return } @@ -1931,7 +3239,7 @@ export class Interpreter { } const target = reference.target as SafeObject const objectKey = key as string - rejectCircularInsertion(target, next, "Object assignment result", node) + this.rejectCircularInsertion(target, next, "Object assignment result", node) target[objectKey] = next } @@ -1942,4 +3250,216 @@ export class Interpreter { throw new InterpreterRuntimeError("Property key must be a string or number.", node) } + + private declare(name: string, value: unknown, mutable: boolean, node: AstNode): void { + const scope = this.currentScope() + + // A pre-seeded parameter slot (initialized === false) is being bound for the first time; + // anything else already present is a genuine duplicate declaration. + const existing = scope.get(name) + if (existing && existing.initialized !== false) { + throw new InterpreterRuntimeError(`Identifier '${name}' has already been declared.`, node) + } + + scope.set(name, { mutable, value, initialized: true }) + } + + private getIdentifierValue(name: string, node: AstNode): unknown { + const binding = this.resolveBinding(name) + + if (!binding) { + throw new InterpreterRuntimeError(`Unknown identifier '${name}'.`, node).as("ReferenceError") + } + + // A parameter default that forward-references a later (not-yet-bound) parameter - JS TDZ. + if (binding.initialized === false) { + throw new InterpreterRuntimeError(`Cannot access '${name}' before initialization.`, node).as("ReferenceError") + } + + return binding.value + } + + private setIdentifierValue(name: string, value: unknown, node: AstNode): unknown { + const binding = this.resolveBinding(name) + + if (!binding) { + throw new InterpreterRuntimeError(`Unknown identifier '${name}'.`, node).as("ReferenceError") + } + + if (!binding.mutable) { + throw new InterpreterRuntimeError(`Cannot assign to constant '${name}'.`, node).as("TypeError") + } + + binding.value = value + return value + } + + private resolveBinding(name: string): Binding | undefined { + for (let index = this.scopes.length - 1; index >= 0; index -= 1) { + const scope = this.scopes[index] + const binding = scope?.get(name) + + if (binding) { + return binding + } + } + + return undefined + } + + private currentScope(): Map { + const scope = this.scopes[this.scopes.length - 1] + + if (!scope) { + throw new InterpreterRuntimeError("Interpreter scope stack is empty.") + } + + return scope + } + + private pushScope(): void { + this.scopes.push(new Map()) + } + + private popScope(): void { + this.scopes.pop() + } +} + +/** + * Executes one Effect-native CodeMode program without constructing a reusable runtime. + * + * @example + * ```ts + * const result = yield* CodeMode.execute({ + * tools: { lookup }, + * code: `return await tools.lookup({ id: "order_42" })`, + * }) + * ``` + */ +export const executeWithLimits = >( + options: ExecuteOptions, + limits: ResolvedExecutionLimits, + searchIndex: ToolRuntime.DiscoveryPlan["searchIndex"], +): Effect.Effect> => { + const hooks = { + ...(options.onToolCallStart === undefined ? {} : { onToolCallStart: options.onToolCallStart }), + ...(options.onToolCallEnd === undefined ? {} : { onToolCallEnd: options.onToolCallEnd }), + } + const tools = ToolRuntime.make( + (options.tools ?? {}) as HostTools>, + limits.maxToolCalls, + searchIndex, + hooks, + ) + const logs: Array = [] + const logged = () => (logs.length > 0 ? { logs: [...logs] } : {}) + + if (options.code.trim().length === 0) { + return Effect.succeed({ + ok: false, + error: { kind: "ParseError", message: "Code cannot be empty." }, + toolCalls: tools.calls, + }) + } + + const operation = Effect.gen(function* () { + const program = parseProgram(options.code) + const interpreter = new Interpreter>(tools.invoke, tools.keys, logs) + const value = yield* interpreter.run(program) + const result = copyOut(copyIn(value, "Execution result"), true) as DataValue + return { + ok: true, + value: result, + ...logged(), + toolCalls: tools.calls, + } satisfies Result + }).pipe((program) => { + const timeoutMs = limits.timeoutMs + if (timeoutMs === undefined) return program + return program.pipe( + Effect.timeoutOrElse({ + duration: timeoutMs, + orElse: () => + Effect.succeed({ + ok: false, + error: { kind: "TimeoutExceeded", message: `Execution timed out after ${timeoutMs}ms.` }, + ...logged(), + toolCalls: tools.calls, + } satisfies Result), + }), + ) + }) + + return operation.pipe( + Effect.catchCause((cause) => + Cause.hasInterruptsOnly(cause) + ? Effect.interrupt + : Effect.succeed({ + ok: false, + error: normalizeError(Cause.squash(cause)), + ...logged(), + toolCalls: tools.calls, + } satisfies Result), + ), + Effect.map((result) => (limits.maxOutputBytes === undefined ? result : boundOutput(result, limits.maxOutputBytes))), + ) +} + +const utf8ByteLength = (value: string): number => new TextEncoder().encode(value).byteLength + +// Truncates to a UTF-8 byte budget without splitting a code point (a split multi-byte +// sequence decodes to a replacement character, which is dropped). +const utf8Truncate = (value: string, maxBytes: number): string => { + const bytes = new TextEncoder().encode(value) + if (bytes.byteLength <= maxBytes) return value + const text = new TextDecoder("utf-8").decode(bytes.slice(0, Math.max(0, maxBytes))) + return text.endsWith("\uFFFD") ? text.slice(0, -1) : text +} + +/** + * Bounds the model-facing output (serialized result value plus logs) to `maxOutputBytes`. + * Oversized values are replaced by their truncated serialized text with an explanatory marker, + * and logs are kept from the start until the remaining budget is exhausted. Truncation never + * fails the execution; `truncated: true` marks affected results. Only runs when the host set + * `maxOutputBytes` - with the limit absent, output passes through unbounded. + */ +const boundOutput = (result: Result, maxOutputBytes: number): Result => { + let truncated = false + + let value: DataValue = null + let valueBytes = 0 + if (result.ok) { + const serialized = JSON.stringify(result.value) ?? "null" + const bytes = utf8ByteLength(serialized) + if (bytes > maxOutputBytes) { + truncated = true + value = `${utf8Truncate(serialized, maxOutputBytes)} [result truncated: ${bytes} bytes exceeds the ${maxOutputBytes}-byte output limit; return a smaller value]` + valueBytes = maxOutputBytes + } else { + value = result.value + valueBytes = bytes + } + } + + const logs = result.logs ?? [] + const kept: Array = [] + const logBudget = Math.max(0, maxOutputBytes - valueBytes) + let logBytes = 0 + for (const line of logs) { + const lineBytes = utf8ByteLength(line) + 1 + if (logBytes + lineBytes > logBudget) break + logBytes += lineBytes + kept.push(line) + } + if (kept.length < logs.length) { + truncated = true + kept.push(`[logs truncated: showing ${kept.length} of ${logs.length} lines]`) + } + + if (!truncated) return result + const logsPart = kept.length > 0 ? { logs: kept } : {} + return result.ok + ? { ok: true, value, ...logsPart, truncated: true, toolCalls: result.toolCalls } + : { ok: false, error: result.error, ...logsPart, truncated: true, toolCalls: result.toolCalls } } diff --git a/packages/codemode/src/interpreter/scope.ts b/packages/codemode/src/interpreter/scope.ts deleted file mode 100644 index f5ee137cbc..0000000000 --- a/packages/codemode/src/interpreter/scope.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { type AstNode, type Binding, InterpreterRuntimeError } from "./model.js" - -export class ScopeStack { - private readonly scopes: Array> - - constructor(scopes: Array>) { - this.scopes = scopes - } - - declare(name: string, value: unknown, mutable: boolean, node: AstNode): void { - const scope = this.current() - - const existing = scope.get(name) - if (existing && existing.initialized !== false) { - throw new InterpreterRuntimeError(`Identifier '${name}' has already been declared.`, node) - } - - scope.set(name, { mutable, value, initialized: true }) - } - - get(name: string, node: AstNode): unknown { - const binding = this.resolve(name) - - if (!binding) { - throw new InterpreterRuntimeError(`Unknown identifier '${name}'.`, node).as("ReferenceError") - } - - if (binding.initialized === false) { - throw new InterpreterRuntimeError(`Cannot access '${name}' before initialization.`, node).as("ReferenceError") - } - - return binding.value - } - - set(name: string, value: unknown, node: AstNode): unknown { - const binding = this.resolve(name) - - if (!binding) { - throw new InterpreterRuntimeError(`Unknown identifier '${name}'.`, node).as("ReferenceError") - } - - if (!binding.mutable) { - throw new InterpreterRuntimeError(`Cannot assign to constant '${name}'.`, node).as("TypeError") - } - - binding.value = value - return value - } - - resolve(name: string): Binding | undefined { - for (let index = this.scopes.length - 1; index >= 0; index -= 1) { - const scope = this.scopes[index] - const binding = scope?.get(name) - - if (binding) { - return binding - } - } - - return undefined - } - - current(): Map { - const scope = this.scopes[this.scopes.length - 1] - - if (!scope) { - throw new InterpreterRuntimeError("Interpreter scope stack is empty.") - } - - return scope - } - - push(scope: Map = new Map()): void { - this.scopes.push(scope) - } - - pop(): void { - this.scopes.pop() - } - - capture(): Array> { - return this.scopes.slice() - } -} diff --git a/packages/codemode/src/openapi/index.ts b/packages/codemode/src/openapi/index.ts index e1f2c64166..7f1770ef36 100644 --- a/packages/codemode/src/openapi/index.ts +++ b/packages/codemode/src/openapi/index.ts @@ -31,8 +31,10 @@ export type { } from "./types.js" /** - * Builds one CodeMode tool per representable OpenAPI 3.x operation. Auth remains host-side, - * tools require `HttpClient.HttpClient`, and unrepresentable operations land in `skipped`. + * Builds a CodeMode tool subtree from an OpenAPI 3.x document, one tool per + * operation. Auth is resolved host-side via `auth.resolve` and never + * model-visible. Tools require `HttpClient.HttpClient`; unrepresentable + * operations land in `skipped`. */ export const fromSpec = (options: Options): Result => { const document = options.spec diff --git a/packages/codemode/src/openapi/runtime.ts b/packages/codemode/src/openapi/runtime.ts index 2b2dcd1e51..2515621792 100644 --- a/packages/codemode/src/openapi/runtime.ts +++ b/packages/codemode/src/openapi/runtime.ts @@ -56,7 +56,7 @@ const buildRequest = ( input: Readonly>, ): Effect.Effect => Effect.gen(function* () { - // Validate model input before auth resolution can refresh credentials. + // Validate every model-controlled value before auth resolution, which may refresh tokens. const url = buildUrl(plan, input) if (url instanceof ToolError) return yield* Effect.fail(url) const missing = plan.fields.find( @@ -77,6 +77,7 @@ const buildRequest = ( request = serialized } + // Host headers first, then declared header parameters. request = HttpClientRequest.setHeaders(request, plan.headers) for (const field of plan.fields) { if (field.location !== "header") continue @@ -168,7 +169,7 @@ const applyCredentials = ( continue } if (credential.type === "basic") { - // Basic auth credentials are UTF-8; btoa rejects non-Latin-1 input. + // Buffer instead of btoa: btoa throws on non-Latin-1 credentials. const duplicate = add( "header", "authorization", @@ -182,6 +183,7 @@ const applyCredentials = ( if (duplicate !== undefined) return duplicate continue } + // apiKey: the carrier comes from the scheme declaration. if (definition.type !== "apiKey") { return toolError( `Security scheme '${name}' is not an apiKey scheme; resolve a bearer, basic, or header credential for it.`, @@ -210,7 +212,8 @@ const buildUrl = (plan: Plan, input: Readonly>): string ), ) if (fieldValue instanceof ToolError) return fieldValue - // URL normalization collapses encoded `.` and `..`, which could retarget the request. + // '.'/'..' survive encoding and URL normalization collapses them, letting a + // model-supplied value retarget the request to a different endpoint. if (fieldValue === "" || fieldValue === "." || fieldValue === "..") { return toolError(`Invalid path parameter '${field.inputName}'.`) } diff --git a/packages/codemode/src/openapi/spec.ts b/packages/codemode/src/openapi/spec.ts index b74b3e69f2..bd4dc5aed6 100644 --- a/packages/codemode/src/openapi/spec.ts +++ b/packages/codemode/src/openapi/spec.ts @@ -23,7 +23,8 @@ const asArray = (value: unknown): ReadonlyArray => (Array.isArray(value export const nonEmptyString = (value: unknown): string | undefined => typeof value === "string" && value !== "" ? value : undefined -// Spec- and model-controlled keys must not resolve inherited properties. +// Guards record lookups keyed by spec- or model-controlled names against +// prototype-inherited values (e.g. a parameter named `toString`). export const own = (record: Readonly>, key: string): T | undefined => Object.hasOwn(record, key) ? record[key] : undefined @@ -105,7 +106,7 @@ const operationParameters = ( pathItem: Record, operation: Record, ): Parsed> => { - // OpenAPI operation parameters override path parameters with the same location and name. + // Operation-level parameters override path-level ones sharing (location, name). const declared = new Map< string, { readonly name: string; readonly location: string; readonly parameter: Record } diff --git a/packages/codemode/src/openapi/types.ts b/packages/codemode/src/openapi/types.ts index 6f3eb86283..cab772e701 100644 --- a/packages/codemode/src/openapi/types.ts +++ b/packages/codemode/src/openapi/types.ts @@ -22,8 +22,9 @@ export type SecurityScheme = | { readonly type: "openIdConnect" } /** - * Credential material returned by a host auth resolver. `apiKey` uses the scheme's carrier; - * `header` supports nonstandard schemes. + * Credential material returned by a host auth resolver. The carrier for `apiKey` + * comes from the scheme definition, not the credential. `header` is the escape + * hatch for nonstandard schemes. */ export type Credential = | { readonly type: "bearer"; readonly token: string } @@ -32,7 +33,9 @@ export type Credential = | { readonly type: "header"; readonly name: string; readonly value: string } /** - * Resolves credentials at call time. `undefined` tries the next OR alternative; failure aborts. + * Resolves credential material for one named security scheme at call time. + * `undefined` means unavailable, try the next OR alternative; a failure aborts + * the call rather than falling through. */ export type AuthResolver = (context: { readonly name: string @@ -71,7 +74,9 @@ export type Parsed = { readonly ok: true; readonly value: T } | { readonly ok export type InputLocation = "path" | "query" | "header" | "body" export type InputField = { + /** Model-visible field name after cross-location collision handling. */ readonly inputName: string + /** Original parameter or body-property name used on the wire. */ readonly name: string readonly location: InputLocation readonly required: boolean @@ -87,6 +92,7 @@ export type OperationInput = { readonly body: Body | undefined } +/** One OR alternative: scheme name -> required scopes. Empty object = unauthenticated is acceptable. */ export type SecurityRequirement = Readonly>> export type Plan = { diff --git a/packages/codemode/src/stdlib/console.ts b/packages/codemode/src/stdlib/console.ts index 4663f62f1a..798563128e 100644 --- a/packages/codemode/src/stdlib/console.ts +++ b/packages/codemode/src/stdlib/console.ts @@ -1,122 +1,4 @@ -import { containsOpaqueReference, containsRuntimeReference, isRuntimeReference } from "../interpreter/references.js" -import { copyIn, copyOut } from "../tool-runtime.js" -import { - isSandboxValue, - SandboxDate, - SandboxMap, - SandboxPromise, - SandboxRegExp, - SandboxSet, - SandboxURL, - SandboxURLSearchParams, -} from "../values.js" -import { boundedData, coerceToString } from "./value.js" - export const consoleMethods = new Set(["log", "info", "debug", "warn", "error", "dir", "table"]) -const MAX_CONSOLE_DEPTH = 32 - -export const formatConsoleMessage = (name: string, args: Array): string => { - if (name === "dir") return args.length === 0 ? "undefined" : formatConsoleArgument(args[0]) - if (name === "table") return formatConsoleTable(args[0], args[1]) - const prefix = name === "warn" ? "[warn] " : name === "error" ? "[error] " : name === "debug" ? "[debug] " : "" - return `${prefix}${args.map((arg) => formatConsoleArgument(arg)).join(" ")}` -} - -const formatConsoleArgument = (value: unknown): string => { - if (value === undefined) return "undefined" - if (typeof value === "string") return value - return formatConsoleValue(value, new Set(), 0) -} - -const formatConsoleValue = (value: unknown, seen: Set, depth: number): string => { - if (value === null || value === undefined) return "null" - if (typeof value === "string") return JSON.stringify(value) - if (typeof value === "number" || typeof value === "boolean") return String(value) - if (typeof value !== "object") return String(value) - if (value instanceof SandboxPromise) return "[Promise (await it to get its value)]" - if (value instanceof SandboxDate) return coerceToString(value) - if (value instanceof SandboxRegExp) return coerceToString(value) - if (value instanceof SandboxURL) return coerceToString(value) - if (value instanceof SandboxURLSearchParams) return coerceToString(value) - if (depth > MAX_CONSOLE_DEPTH) return "..." - if (seen.has(value)) return "[Circular]" - if (value instanceof SandboxMap) { - seen.add(value) - try { - const entries = Array.from(value.map.entries(), ([key, item]): Array => [key, item]) - return `Map(${value.map.size}) ${formatConsoleValue(entries, seen, depth + 1)}` - } finally { - seen.delete(value) - } - } - if (value instanceof SandboxSet) { - seen.add(value) - try { - return `Set(${value.set.size}) ${formatConsoleValue(Array.from(value.set.values()), seen, depth + 1)}` - } finally { - seen.delete(value) - } - } - if (isRuntimeReference(value)) return "[CodeMode reference]" - seen.add(value) - try { - if (Array.isArray(value)) { - return `[${value.map((item) => formatConsoleValue(item, seen, depth + 1)).join(",")}]` - } - return `{${Object.entries(value) - .map(([key, item]) => `${JSON.stringify(key)}:${formatConsoleValue(item, seen, depth + 1)}`) - .join(",")}}` - } finally { - seen.delete(value) - } -} - -const formatConsoleTable = (value: unknown, columnsArgument: unknown): string => { - if (value === undefined) return "undefined" - if (containsOpaqueReference(value)) return "[CodeMode reference]" - const data = boundedData(value, "console.table argument") - const columns = consoleTableColumns(columnsArgument) - const rows = consoleTableRows(data, columns) - const keys = columns ?? Array.from(new Set(rows.flatMap((row) => Object.keys(row.values)))) - const header = ["(index)", ...keys].join("\t") - return [ - header, - ...rows.map((row) => [row.index, ...keys.map((key) => formatConsoleTableCell(row.values[key]))].join("\t")), - ].join("\n") -} - -const consoleTableColumns = (value: unknown): ReadonlyArray | undefined => { - if (value === undefined) return undefined - if (containsRuntimeReference(value)) return undefined - const columns = copyOut(copyIn(value, "console.table columns"), true) - return Array.isArray(columns) ? columns.map((column) => String(column)) : undefined -} - -const consoleTableRows = ( - data: unknown, - columns: ReadonlyArray | undefined, -): Array<{ readonly index: string; readonly values: Record }> => { - if (Array.isArray(data)) { - return data.map((item, index) => ({ index: String(index), values: consoleTableValues(item, columns) })) - } - if (data !== null && typeof data === "object" && !isSandboxValue(data)) { - return Object.entries(data).map(([index, item]) => ({ index, values: consoleTableValues(item, columns) })) - } - return [{ index: "0", values: { Value: data } }] -} - -const consoleTableValues = (value: unknown, columns: ReadonlyArray | undefined): Record => { - if (value !== null && typeof value === "object" && !Array.isArray(value) && !isSandboxValue(value)) { - const source = value as Record - if (columns !== undefined) return Object.fromEntries(columns.map((column) => [column, source[column]])) - return Object.fromEntries(Object.entries(source)) - } - return { Value: value } -} - -const formatConsoleTableCell = (value: unknown): string => { - if (value === undefined) return "" - if (typeof value === "string") return value - return formatConsoleValue(value, new Set(), 0) -} +/** Console formatting recursion ceiling; deeper values render as "...". */ +export const MAX_CONSOLE_DEPTH = 32 diff --git a/packages/codemode/src/stdlib/date.ts b/packages/codemode/src/stdlib/date.ts index 11566e2d7c..c492f58f94 100644 --- a/packages/codemode/src/stdlib/date.ts +++ b/packages/codemode/src/stdlib/date.ts @@ -23,6 +23,8 @@ export const dateMethods = new Set([ "getTimezoneOffset", ]) +export const dateStatics = new Set(["now", "parse", "UTC"]) + export const invokeDateStatic = (name: string, args: Array, node: AstNode): number => { switch (name) { case "now": diff --git a/packages/codemode/src/stdlib/json.ts b/packages/codemode/src/stdlib/json.ts index a7cc13629e..8a479d2c8c 100644 --- a/packages/codemode/src/stdlib/json.ts +++ b/packages/codemode/src/stdlib/json.ts @@ -6,7 +6,10 @@ import { } from "../interpreter/model.js" import { copyIn, copyOut } from "../tool-runtime.js" +export const jsonStatics = new Set(["stringify", "parse"]) + export const invokeJsonMethod = (name: string, args: Array, node: AstNode): unknown => { + if (!jsonStatics.has(name)) throw new InterpreterRuntimeError(`JSON.${name} is not available in CodeMode.`, node) switch (name) { case "stringify": { const replacer = args[1] diff --git a/packages/codemode/src/stdlib/math.ts b/packages/codemode/src/stdlib/math.ts index 7720f69775..cc8dd0670e 100644 --- a/packages/codemode/src/stdlib/math.ts +++ b/packages/codemode/src/stdlib/math.ts @@ -1,17 +1,9 @@ export const mathConstants = new Set(["PI", "E", "LN2", "LN10", "LOG2E", "LOG10E", "SQRT2", "SQRT1_2"]) export const mathMethods = new Set([ - "random", "max", "min", "abs", - "acos", - "acosh", - "asin", - "asinh", - "atan", - "atan2", - "atanh", "floor", "ceil", "round", @@ -21,27 +13,14 @@ export const mathMethods = new Set([ "cbrt", "pow", "hypot", - "cos", - "cosh", - "sin", - "sinh", - "tan", - "tanh", "log", "log2", "log10", - "log1p", "exp", - "expm1", - "f16round", - "fround", - "clz32", - "imul", ]) export const invokeMathMethod = (name: string, args: Array, node: AstNode): number => { if (!mathMethods.has(name)) throw new InterpreterRuntimeError(`Math.${name} is not available in CodeMode.`, node) - if (name === "random") return Math.random() const nums = args.map((arg) => { if (typeof arg !== "number") throw new InterpreterRuntimeError(`Math.${name} expects number arguments.`, node) return arg @@ -54,20 +33,6 @@ export const invokeMathMethod = (name: string, args: Array, node: AstNo return Math.min(...nums) case "abs": return Math.abs(a) - case "acos": - return Math.acos(a) - case "acosh": - return Math.acosh(a) - case "asin": - return Math.asin(a) - case "asinh": - return Math.asinh(a) - case "atan": - return Math.atan(a) - case "atan2": - return Math.atan2(a, b) - case "atanh": - return Math.atanh(a) case "floor": return Math.floor(a) case "ceil": @@ -86,38 +51,14 @@ export const invokeMathMethod = (name: string, args: Array, node: AstNo return Math.pow(a, b) case "hypot": return Math.hypot(...nums) - case "cos": - return Math.cos(a) - case "cosh": - return Math.cosh(a) - case "sin": - return Math.sin(a) - case "sinh": - return Math.sinh(a) - case "tan": - return Math.tan(a) - case "tanh": - return Math.tanh(a) case "log": return Math.log(a) case "log2": return Math.log2(a) case "log10": return Math.log10(a) - case "log1p": - return Math.log1p(a) case "exp": return Math.exp(a) - case "expm1": - return Math.expm1(a) - case "f16round": - return Math.f16round(a) - case "fround": - return Math.fround(a) - case "clz32": - return Math.clz32(a) - case "imul": - return Math.imul(a, b) } throw new InterpreterRuntimeError(`Math.${name} is not available in CodeMode.`, node) } diff --git a/packages/codemode/src/stdlib/number.ts b/packages/codemode/src/stdlib/number.ts index 02dfa953b4..79710e1ad2 100644 --- a/packages/codemode/src/stdlib/number.ts +++ b/packages/codemode/src/stdlib/number.ts @@ -1,15 +1,6 @@ -export const numberMethods = new Set(["toFixed", "toPrecision", "toExponential", "toString", "valueOf"]) +export const numberMethods = new Set(["toFixed", "toPrecision", "toExponential", "toString"]) -export const numberConstants = new Set([ - "MAX_SAFE_INTEGER", - "MIN_SAFE_INTEGER", - "MAX_VALUE", - "MIN_VALUE", - "EPSILON", - "NaN", - "POSITIVE_INFINITY", - "NEGATIVE_INFINITY", -]) +export const numberConstants = new Set(["MAX_SAFE_INTEGER", "MIN_SAFE_INTEGER", "MAX_VALUE", "MIN_VALUE", "EPSILON"]) export const numberStatics = new Set(["isInteger", "isFinite", "isNaN", "isSafeInteger", "parseInt", "parseFloat"]) @@ -41,9 +32,6 @@ export const invokeNumberMethod = (value: number, name: string, args: Array, node: AstNode): unknown => { + if (!objectStatics.has(name)) throw new InterpreterRuntimeError(`Object.${name} is not available in CodeMode.`, node) const requireObject = (): Record => { - const input = args[0] - if (Array.isArray(input)) return input as unknown as Record - if (isSandboxValue(input)) return {} - if (input instanceof SandboxPromise) { - throw new InterpreterRuntimeError( - `Object.${name} received an un-awaited Promise; await it before inspecting the result.`, - node, - "InvalidDataValue", - ) + const value = boundedData(args[0], `Object.${name} input`) + if (isSandboxValue(value)) return {} + if (value === null || typeof value !== "object" || Array.isArray(value)) { + throw new InterpreterRuntimeError(`Object.${name} expects a data object.`, node) } - if (input === null || typeof input !== "object") { - throw new InterpreterRuntimeError(`Object.${name} expects a data object or array.`, node, "InvalidDataValue") - } - const prototype = Object.getPrototypeOf(input) - if (prototype !== null && prototype !== Object.prototype) { - throw new InterpreterRuntimeError(`Object.${name} expects a data object or array.`, node, "InvalidDataValue") - } - return input as Record + return value as Record } const guardedSet = (out: Record, key: string, item: unknown): void => { if (isBlockedMember(key)) throw new InterpreterRuntimeError(`Property '${key}' is not available in CodeMode.`, node) out[key] = item } - const addEntry = (out: Record, key: unknown, item: unknown): void => { - boundedData(key, "Object.fromEntries key") - boundedData(item, "Object.fromEntries value") - guardedSet(out, coerceToString(key), item) - } switch (name) { - case "keys": - return Object.keys(requireObject()) + case "keys": { + const value = boundedData(args[0], "Object.keys input") + if (isSandboxValue(value)) return [] + if (Array.isArray(value)) return Object.keys(value) + if (value === null || typeof value !== "object") { + throw new InterpreterRuntimeError("Object.keys expects a data object or array.", node) + } + return Object.keys(value) + } case "values": return Object.values(requireObject()) case "entries": @@ -45,24 +36,22 @@ export const invokeObjectMethod = (name: string, args: Array, node: Ast case "hasOwn": return Object.hasOwn(requireObject(), String(args[1])) case "assign": { - const target = args[0] - if (target === null || typeof target !== "object" || Array.isArray(target) || isSandboxValue(target)) { - throw new InterpreterRuntimeError("Object.assign expects a data object target.", node) - } - const out = target as Record - for (const source of args.slice(1)) { - if (source === null || source === undefined || isSandboxValue(source)) continue - if (typeof source !== "object" || Array.isArray(source)) { + const out: Record = Object.create(null) + for (const source of args) { + if (source === null || source === undefined) continue + const value = boundedData(source, "Object.assign input") + if (isSandboxValue(value)) continue + if (value === null || typeof value !== "object" || Array.isArray(value)) { throw new InterpreterRuntimeError("Object.assign expects data objects.", node) } - for (const [key, item] of Object.entries(source)) guardedSet(out, key, item) + for (const [key, item] of Object.entries(value)) guardedSet(out, key, item) } return out } case "fromEntries": { if (args[0] instanceof SandboxMap) { const out: Record = Object.create(null) - for (const [key, item] of args[0].map.entries()) addEntry(out, key, item) + for (const [key, item] of args[0].map.entries()) guardedSet(out, coerceToString(key), item) return out } if (args[0] instanceof SandboxURLSearchParams) { @@ -70,18 +59,16 @@ export const invokeObjectMethod = (name: string, args: Array, node: Ast for (const [key, value] of args[0].params.entries()) guardedSet(out, key, value) return out } - const pairs = args[0] instanceof SandboxSet ? Array.from(args[0].set.values()) : args[0] + const pairs = boundedData(args[0], "Object.fromEntries input") if (!Array.isArray(pairs)) { - boundedData(args[0], "Object.fromEntries input") throw new InterpreterRuntimeError("Object.fromEntries expects an array of [key, value] pairs.", node) } const out: Record = Object.create(null) for (const pair of pairs) { - const validated = boundedData(pair, "Object.fromEntries entry") - if (validated === null || typeof validated !== "object" || isSandboxValue(validated)) - throw new InterpreterRuntimeError("Object.fromEntries expects [key, value] entry objects.", node) - const entry = pair as Record - addEntry(out, entry[0], entry[1]) + if (!Array.isArray(pair)) { + throw new InterpreterRuntimeError("Object.fromEntries expects [key, value] pairs.", node) + } + guardedSet(out, String(pair[0]), pair[1]) } return out } diff --git a/packages/codemode/src/stdlib/promise.ts b/packages/codemode/src/stdlib/promise.ts index 73d02d6c0c..d0b442ccf7 100644 --- a/packages/codemode/src/stdlib/promise.ts +++ b/packages/codemode/src/stdlib/promise.ts @@ -1,3 +1,6 @@ import type { PromiseMethodName } from "../interpreter/model.js" -export const promiseStatics = new Set(["all", "allSettled", "race", "any", "resolve", "reject"]) +export const promiseStatics = new Set(["all", "allSettled", "race", "resolve", "reject"]) + +/** Maximum number of eagerly forked tool calls that may run concurrently. */ +export const TOOL_CALL_CONCURRENCY = 8 diff --git a/packages/codemode/src/stdlib/string.ts b/packages/codemode/src/stdlib/string.ts index ffc33797dd..3ac4372e36 100644 --- a/packages/codemode/src/stdlib/string.ts +++ b/packages/codemode/src/stdlib/string.ts @@ -4,9 +4,12 @@ export const stringMethods = new Set([ "trim", "trimStart", "trimEnd", + "trimLeft", + "trimRight", "split", "slice", "substring", + "substr", "includes", "startsWith", "endsWith", diff --git a/packages/codemode/src/stdlib/value.ts b/packages/codemode/src/stdlib/value.ts index 7cdd8cc7dc..ab40dc07dd 100644 --- a/packages/codemode/src/stdlib/value.ts +++ b/packages/codemode/src/stdlib/value.ts @@ -6,7 +6,6 @@ export const errorConstructors = new Set([ "ReferenceError", "EvalError", "URIError", - "AggregateError", ]) export const valueConstructors = new Set(["Date", "RegExp", "Map", "Set", "URL", "URLSearchParams"]) @@ -21,9 +20,6 @@ export const createErrorValue = (name: string, message: string): SafeObject => { return value } -export const createAggregateErrorValue = (errors: Array, message: string): SafeObject => - Object.assign(createErrorValue("AggregateError", message), { errors }) - export const errorBrandName = (value: unknown): string | undefined => value !== null && typeof value === "object" ? ((value as Record)[ErrorBrand] as string | undefined) @@ -64,7 +60,7 @@ export const invokeCoercion = (ref: CoercionFunction, args: Array, node if (ref.name === "parseInt") return parseInt(coerceToString(raw)) return parseFloat(coerceToString(raw)) } - const value = boundedData(raw, `${ref.name} input`) + const value = boundedData(args[0], `${ref.name} input`) if (ref.name === "Number") return coerceToNumber(value) if (ref.name === "Boolean") return Boolean(value) if (ref.name === "parseInt") { diff --git a/packages/codemode/src/tool-runtime.ts b/packages/codemode/src/tool-runtime.ts index 0bb8c00aec..f4ccc61d4c 100644 --- a/packages/codemode/src/tool-runtime.ts +++ b/packages/codemode/src/tool-runtime.ts @@ -44,30 +44,36 @@ type ServicesOf> = Depth["length"] e : ServicesOf : never +/** Minimal audit record retained for each admitted tool call. */ export type ToolCall = { readonly name: string } +/** Decoded tool call observed immediately before tool execution. */ export type ToolCallStarted = { readonly index: number readonly name: string readonly input: unknown } +/** Completed tool call observed immediately after tool execution settles. */ export type ToolCallEnded = { readonly index: number readonly name: string readonly input: unknown readonly durationMs: number readonly outcome: "success" | "failure" + /** Model-safe failure message; present only when `outcome` is `"failure"`. */ readonly message?: string } +/** Non-throwing observation hooks fired around each admitted tool call. */ export type ToolCallHooks = { readonly onToolCallStart?: ((call: ToolCallStarted) => Effect.Effect) | undefined readonly onToolCallEnd?: ((call: ToolCallEnded) => Effect.Effect) | undefined } +/** Model-visible description of one schema-backed tool. */ export type ToolDescription = { readonly path: string readonly description: string @@ -76,6 +82,7 @@ export type ToolDescription = { export type SafeObject = Record +const reservedNamespace = "$codemode" const defaultCatalogBudget = 2_000 const defaultSearchLimit = 10 const PositiveInt = Schema.Int.check(Schema.isGreaterThan(0)) @@ -107,6 +114,11 @@ export class ToolReference { constructor(readonly path: ReadonlyArray) {} } +/** + * Maximum nesting depth for values crossing a data boundary. Fixed (not a configurable + * limit) purely because it produces a clearer diagnostic than a native stack-overflow + * RangeError would. + */ const MAX_VALUE_DEPTH = 32 export class ToolRuntimeError extends Error { @@ -141,7 +153,21 @@ const blockedMemberNames = new Set(["__proto__", "constructor", "prototype"]) export const isBlockedMember = (name: string): boolean => blockedMemberNames.has(name) -// Checkpoint mode preserves sandbox values; boundary mode JSON-normalizes them. +/** + * Validates and copies a value against the plain-data contract (depth, circularity, plain + * objects only, blocked properties, data-only leaves). + * + * Two modes share the walk: + * - **Boundary** (`preserveSandboxValues` false, the default): the host<->sandbox boundary - + * final results, tool-call arguments, `JSON.stringify`. Sandbox value types serialize + * exactly as JSON.stringify would: Date/URL -> strings, the remaining value types -> {}. + * - **Intra-sandbox checkpoint** (`preserveSandboxValues` true; see `boundedData` in + * codemode.ts): standard-library value instances pass through untouched (treated as leaves, + * contents not walked), so values flowing through `Object.*` helpers, coercion inputs, and + * other in-sandbox checkpoints stay fully usable (`.getTime()`, `.has()`, ...). + * + * Both modes reject un-awaited promises with an await-hinting diagnostic. + */ export const copyIn = (value: unknown, label: string, preserveSandboxValues = false): unknown => copyBounded(value, label, 0, new Set(), preserveSandboxValues) @@ -160,6 +186,10 @@ const copyBounded = ( value === undefined || typeof value === "string" || typeof value === "boolean" || + // NaN/Infinity are allowed to exist as in-sandbox intermediates (matching real JS and a real + // engine) so defensive guards like `Number.isNaN(x)` / `parseInt(x) || 0` can run. They are + // normalized to `null` when the value leaves the sandbox - see copyOut - exactly as + // JSON.stringify already does at any tool boundary. typeof value === "number" ) { return value @@ -169,6 +199,8 @@ const copyBounded = ( throw new ToolRuntimeError("InvalidDataValue", `${label} must contain data only.`) } + // An un-awaited promise never crosses a data checkpoint as `{}`; the diagnostic tells the + // model exactly how to fix the program instead. if (value instanceof SandboxPromise) { throw new ToolRuntimeError( "InvalidDataValue", @@ -177,6 +209,9 @@ const copyBounded = ( } if (preserveSandboxValues) { + // Intra-sandbox checkpoints keep sandbox value instances alive as leaves; their contents + // are never walked here (Map/Set members are validated where mutation happens, and the + // real boundary still serializes them below). if ( value instanceof SandboxDate || value instanceof SandboxRegExp || @@ -187,6 +222,8 @@ const copyBounded = ( ) { return value } + // Host instances cannot normally reach an intra-sandbox checkpoint (tool results cross + // the boundary first), but wrap them defensively rather than degrading to JSON forms. if (value instanceof Date) return new SandboxDate(value.getTime()) if (value instanceof RegExp) return new SandboxRegExp(value.source, value.flags) if (value instanceof Map) { @@ -205,6 +242,9 @@ const copyBounded = ( if (value instanceof URLSearchParams) return new SandboxURLSearchParams(new URLSearchParams(value)) } + // Sandbox value types (and their host counterparts, which a host tool may legitimately + // return) serialize exactly as JSON.stringify would at the data boundary: Date/URL use + // toJSON(), while RegExp/Map/Set/URLSearchParams have no JSON form beyond {}. if (value instanceof SandboxDate) { return Number.isFinite(value.time) ? new Date(value.time).toISOString() : null } @@ -234,16 +274,6 @@ const copyBounded = ( if (Array.isArray(value)) { const copied = value.map((item) => copyBounded(item, label, depth + 1, seen, preserveSandboxValues)) - if (preserveSandboxValues) { - // Checkpoint copies retain array metadata that boundary copies omit. - for (const [key, item] of Object.entries(value)) { - if (Object.hasOwn(copied, key)) continue - if (isBlockedMember(key)) { - throw new ToolRuntimeError("InvalidDataValue", `${label} contains blocked property '${key}'.`) - } - Reflect.set(copied, key, copyBounded(item, label, depth + 1, seen, true)) - } - } seen.delete(value) return copied } @@ -266,6 +296,9 @@ const copyBounded = ( export const copyOut = (value: unknown, undefinedAsNull = false): unknown => { if (value === undefined && undefinedAsNull) return null + // Normalize non-finite numbers to null as the value crosses out of the sandbox (final return + // and tool-call arguments both funnel through here), matching JSON semantics - NaN/Infinity + // have no JSON representation, so JSON.stringify would produce null anyway. if (typeof value === "number" && !Number.isFinite(value)) { return null } @@ -283,12 +316,15 @@ export const copyOut = (value: unknown, undefinedAsNull = false): unknown => { const definitions = ( tools: HostTools, path: ReadonlyArray = [], -): Array<{ path: string; definition: Definition }> => - Object.entries(tools).flatMap(([name, value]) => { +): Array<{ path: string; definition: Definition }> => { + const entries: Array<{ path: string; definition: Definition }> = [] + for (const [name, value] of Object.entries(tools)) { const next = [...path, name] - if (isDefinition(value)) return [{ path: next.join("."), definition: value }] - return typeof value === "function" ? [] : definitions(value, next) - }) + if (isDefinition(value)) entries.push({ path: next.join("."), definition: value }) + else if (typeof value !== "function") entries.push(...definitions(value, next)) + } + return entries +} const describeDefinition = (path: string, definition: Definition): ToolDescription => ({ path, @@ -303,6 +339,9 @@ const visibleDefinitions = (tools: HostTools) => description: describeDefinition(path, definition), })) +export const catalog = (tools: HostTools): ReadonlyArray => + visibleDefinitions(tools).map(({ description }) => description) + export type DiscoveryPlan = { readonly catalog: ReadonlyArray readonly instructions: string @@ -311,10 +350,18 @@ export type DiscoveryPlan = { export type SearchEntry = { readonly description: ToolDescription + /** Top-level namespace (first path segment), matched by the search `namespace` option. */ readonly namespace: string + /** Lowercased path + description + input property names/descriptions, for substring matching. */ readonly searchText: string } +/** + * Split a query into lowercased search terms. camelCase boundaries are split + * (`resolveLibrary` -> `resolve library`) and every non-alphanumeric character is a + * separator, so `resolve-library-id`, `resolveLibraryId`, and `resolve library id` all + * tokenize alike. Empties and the `*` wildcard are dropped. + */ const tokenize = (query: string): Array => query .replace(/([a-z0-9])([A-Z])/g, "$1 $2") @@ -322,6 +369,13 @@ const tokenize = (query: string): Array => .split(/[^a-z0-9]+/) .filter((term) => term.length > 0 && term !== "*") +/** + * A term plus its naive singular variants (trailing "s"/"es" stripped), so a plural + * query term ("issues") still matches indexed text that only carries the singular + * ("issue"). Matching is one-directional substring containment, so the variants are + * needed only on the query side; scoring weights are unchanged - each field check + * passes when ANY form matches. + */ const termForms = (term: string): Array => { const forms = [term] if (term.endsWith("es") && term.length > 3) forms.push(term.slice(0, -2)) @@ -343,6 +397,8 @@ const makeSearchTool = (searchIndex: ReadonlyArray): Definition => request.namespace === undefined ? searchIndex : searchIndex.filter((entry) => entry.namespace === request.namespace) + // A query that names one tool path exactly (canonical path or rendered JavaScript + // expression) is a lookup, not a search: return that tool alone. const trimmed = query.trim() const pathQuery = trimmed.startsWith("tools.") ? trimmed.slice("tools.".length) : trimmed const exact = @@ -352,6 +408,9 @@ const makeSearchTool = (searchIndex: ReadonlyArray): Definition => (entry) => entry.description.path === pathQuery || toolExpression(entry.description.path) === trimmed, ) const terms = tokenize(query).map(termForms) + // Additive field-weighted scoring, summed across terms: exact path or path segment + // (20) > path substring (8) > description substring (4) > any searchable text, + // including input parameter names and descriptions (2). const ranked = exact !== undefined ? [exact] @@ -389,12 +448,10 @@ const makeSearchTool = (searchIndex: ReadonlyArray): Definition => }), }) -const searchSignature = (() => { - const definition = makeSearchTool([]) - return `search(input: ${inputTypeScript(definition, true)}): ${outputTypeScript(definition, true)}` -})() +const searchDescription = describeDefinition(`${reservedNamespace}.search`, makeSearchTool([])) const catalogLine = (tool: ToolDescription) => { + // Keep the tool description concise; the full schema documentation remains in the signature. const line = tool.description.split("\n", 1)[0]!.trim() const description = line.length > 120 ? line.slice(0, 119) + "..." : line return description === "" ? ` - ${tool.signature}` : ` - ${tool.signature} // ${description}` @@ -414,10 +471,27 @@ const toSearchEntry = (path: string, definition: Definition, description: .toLowerCase(), }) +/** The runtime search index over every described tool. Search is always registered. */ export const searchIndex = (tools: HostTools): ReadonlyArray => visibleDefinitions(tools).map(({ path, definition, description }) => toSearchEntry(path, definition, description)) -// Budget signatures round-robin so every namespace remains visible. +export const assertValidTools = (tools: HostTools): void => { + if (Object.hasOwn(tools, reservedNamespace)) { + throw new Error(`Tool namespace '${reservedNamespace}' is reserved for CodeMode discovery tools.`) + } +} + +/** + * Budgeted catalog: every namespace is always listed with its tool count; full call + * signatures are inlined against the `catalogBudget` (estimated tokens, + * chars/4) round-robin across namespaces - in each round (namespaces alphabetical), every + * namespace still holding un-inlined tools attempts to place its next-cheapest line, and + * a namespace whose next line does not fit is done while the others keep going - so every + * namespace gets some representation before any namespace gets everything. The section + * states exactly how comprehensive it is - overall (COMPLETE vs PARTIAL) and per + * namespace. Namespace stub lines are never budgeted: every namespace appears with its + * tool count even at budget 0. + */ export const prepare = (tools: HostTools, catalogBudget = defaultCatalogBudget): DiscoveryPlan => { if (!Number.isSafeInteger(catalogBudget) || catalogBudget < 0) { throw new RangeError("discovery.catalogBudget must be a non-negative safe integer") @@ -434,6 +508,12 @@ export const prepare = (tools: HostTools, catalogBudget = defaultCatalogBu } const ordered = [...namespaces].sort(([left], [right]) => left.localeCompare(right)) + // Select which signatures fit the budget before emitting, so the list can state + // exactly how comprehensive it is. Round-robin fairness: in each round (namespaces + // alphabetical), every namespace still holding un-inlined tools tries to place its + // next-cheapest line against the shared budget; a namespace whose next line does not + // fit is done - the others keep going - so every namespace gets some representation + // before any namespace gets everything. const selections = ordered.map(([namespace, group]) => ({ namespace, picked: new Set(), @@ -465,17 +545,23 @@ export const prepare = (tools: HostTools, catalogBudget = defaultCatalogBu const empty = described.length === 0 + // Section order is deliberate: workflow first (the top is the least likely part of a long + // description to be truncated or skimmed away), then rules, then syntax, with the budgeted + // catalog at the bottom. Example call forms use placeholders - never a real or fabricated + // tool name - and show both dot and bracket notation so non-identifier names are not normalized. const intro = [ empty ? "This is a restricted JavaScript language for calling tools, not a general-purpose runtime." : complete - ? "This is a restricted JavaScript language for calling tools, not a general-purpose runtime. Inside the confined interpreter, `tools` contains the Code Mode tools listed below; surrounding agent tools are not available." - : "This is a restricted JavaScript language for calling tools, not a general-purpose runtime. Inside the confined interpreter, `tools` contains the Code Mode tools listed or searchable below; surrounding agent tools are not available.", + ? "This is a restricted JavaScript language for calling tools, not a general-purpose runtime. Inside the confined interpreter, `tools` contains the Code Mode tools listed below and internal runtime tools; surrounding agent tools are not available." + : "This is a restricted JavaScript language for calling tools, not a general-purpose runtime. Inside the confined interpreter, `tools` contains the Code Mode tools listed or searchable below and internal runtime tools; surrounding agent tools are not available.", ...(empty ? [] : ["Do not infer or normalize tool names; use only exact signatures shown below or returned by search."]), ] + // The search step exists only when search is advertised (PARTIAL catalog); a COMPLETE + // catalog already shows every signature, so step 1 picks from the list instead. const workflow = empty ? [] : [ @@ -489,7 +575,7 @@ export const prepare = (tools: HostTools, catalogBudget = defaultCatalogBu "3. Return only the fields you need from structured results; narrow unknown results before reading fields, and avoid returning large raw payloads.", ] : [ - '1. If needed, discover tools with the built-in search function: `return search({ query: "" })`.', + '1. If needed, discover tools: `return await tools.$codemode.search({ query: "" })`.', "2. In the next execution, copy a returned path exactly, call it, and return only the needed fields.", ]), ] @@ -501,17 +587,16 @@ export const prepare = (tools: HostTools, catalogBudget = defaultCatalogBu "## Rules", "", complete - ? "- Only Code Mode tools listed here are available; surrounding agent tools are not implicitly exposed." - : "- Only Code Mode tools listed here or returned by the built-in `search` function are available; surrounding agent tools are not implicitly exposed.", + ? "- Only Code Mode tools listed here and internal runtime tools are available; surrounding agent tools are not implicitly exposed." + : "- Only Code Mode tools listed here or returned by `tools.$codemode.search` and internal runtime tools are available; surrounding agent tools are not implicitly exposed.", "- Filter, aggregate, and transform collections in code - never return them raw or call a tool per item across messages.", "- A result typed `Promise` may be structured data or text. Before reading fields, check that it is a non-null object and not an array; otherwise handle the returned text or primitive directly.", '- Run independent calls in parallel: `await Promise.all(items.map((item) => tools..(item)))`, or use `tools.["tool-name"](item)` when the listed signature uses bracket notation.', - "- Execution ends when the program returns; pending promises are interrupted, so await every call whose completion matters.", "- `Object.keys(tools)` lists namespaces; `Object.keys(tools.)` lists its tools; `for...in` works on both.", ...(complete ? [] : [ - '- Browse one namespace: `search({ query: "", namespace: "" })`.', + '- Browse one namespace: `await tools.$codemode.search({ query: "", namespace: "" })`.', "- If search returns `next`, repeat the same search with `offset: next.offset`.", ]), ] @@ -521,8 +606,7 @@ export const prepare = (tools: HostTools, catalogBudget = defaultCatalogBu "## Language", "", "Use common JavaScript data operations, functions, control flow, selected standard-library methods, and awaited tool calls. Built-ins include Date, RegExp, Map, Set, URL, URLSearchParams, and URI encoding helpers.", - "Modules/imports, classes, generators, timers, fetch, eval, prototype access, and unlisted methods are unavailable. Use Code Mode tools for external operations. Use await with try/catch.", - "Prefer explicit `return`; otherwise only the final top-level expression becomes the result.", + "Modules/imports, classes, generators, timers, fetch, eval, prototype access, unlisted methods, and promise chaining are unavailable. Use Code Mode tools for external operations. Use await with try/catch.", "Dates and URLs serialize to strings at data boundaries; Map/Set/RegExp/URLSearchParams serialize to `{}`.", ] @@ -533,12 +617,14 @@ export const prepare = (tools: HostTools, catalogBudget = defaultCatalogBu toolSection.push( complete ? "## Available tools (COMPLETE list - every tool is shown below with its full call signature)" - : `## Available tools (PARTIAL - ${totalShown} of ${described.length} shown; find the rest with search(...))`, + : `## Available tools (PARTIAL - ${totalShown} of ${described.length} shown; find the rest with tools.$codemode.search)`, "", ) for (const [namespace, group] of ordered) { const picked = shown.get(namespace)! const count = `${group.length} tool${group.length === 1 ? "" : "s"}` + // Annotate only when a namespace is not fully shown, so a comprehensive + // namespace reads cleanly and a truncated one is unambiguous. const label = picked.size === group.length ? count @@ -549,7 +635,7 @@ export const prepare = (tools: HostTools, catalogBudget = defaultCatalogBu for (const tool of group) if (picked.has(tool)) toolSection.push(catalogLine(tool)) } if (!complete) { - toolSection.push("", "Search returns complete callable signatures:", `- ${searchSignature}`) + toolSection.push("", "Search returns complete callable signatures:", `- ${searchDescription.signature}`) } } @@ -561,6 +647,13 @@ export const prepare = (tools: HostTools, catalogBudget = defaultCatalogBu } } +/** + * The enumerable names at one node of the callable tool tree - namespace names at the root, + * tool/namespace names below - powering `Object.keys(tools)` and `for...in` over tool + * references. A callable tool is a leaf and enumerates as `[]` (like `Object.keys` of a + * function in JS). An unknown path is an `UnknownTool` error pointing at the working + * discovery idioms, mirroring how calling an unknown tool fails. + */ const namespaceKeys = (tools: HostTools, path: ReadonlyArray): ReadonlyArray => { let value: HostTool | Definition | HostTools = tools for (const segment of path) { @@ -571,7 +664,7 @@ const namespaceKeys = (tools: HostTools, path: ReadonlyArray): Rea !Object.hasOwn(value, segment) ) { throw new ToolRuntimeError("UnknownTool", `Unknown tool namespace '${path.join(".")}'.`, [ - "Object.keys(tools) lists the available namespaces; search({ query }) finds described tools.", + "Object.keys(tools) lists the available namespaces; tools.$codemode.search({ query }) finds described tools.", ]) } value = value[segment] as HostTool | Definition | HostTools @@ -591,7 +684,7 @@ const resolve = (tools: HostTools, path: ReadonlyArray): HostTool< !Object.hasOwn(value, segment) ) { throw new ToolRuntimeError("UnknownTool", `Unknown tool '${path.join(".")}'.`, [ - "Use search({ query }) to find available described tools.", + "Use tools.$codemode.search({ query }) to find available described tools.", ]) } value = value[segment] as HostTool | Definition | HostTools @@ -608,20 +701,25 @@ export type ToolRuntime = { readonly root: ToolReference readonly calls: Array readonly invoke: (path: ReadonlyArray, args: Array) => Effect.Effect - readonly search: (args: Array) => Effect.Effect + /** Enumerable namespace/tool names at one node of the callable tool tree; see `namespaceKeys`. */ readonly keys: (path: ReadonlyArray) => ReadonlyArray } export const make = ( tools: HostTools, + /** Undefined means unlimited tool calls. */ maxToolCalls: number | undefined, searchIndex: ReadonlyArray, hooks?: ToolCallHooks, ): ToolRuntime => { const calls: Array = [] - const searchTool = makeSearchTool(searchIndex) + const callableTools = { + ...tools, + [reservedNamespace]: { search: makeSearchTool(searchIndex) }, + } - // End hooks observe settled success or failure; interruption emits neither outcome. + // Wraps the settling portion of a tool call so onToolCallEnd observes success and failure + // symmetrically. Interruption (e.g. the execution timeout) fires neither outcome. const observeEnd = (effect: Effect.Effect, call: ToolCallStarted): Effect.Effect => { const onEnd = hooks?.onToolCallEnd if (onEnd === undefined) return effect @@ -654,59 +752,52 @@ export const make = ( calls.push(call) } - const recordAndObserve = (name: string, input: unknown) => - Effect.sync(() => { - recordCall({ name }) - return calls.length - 1 - }).pipe(Effect.tap((index) => hooks?.onToolCallStart?.({ index, name, input }) ?? Effect.void)) - - const invokeDefinition = (name: string, tool: Definition, externalArgs: Array) => - Effect.gen(function* () { - if (externalArgs.length !== 1) - throw new ToolRuntimeError("InvalidToolInput", `Tool '${name}' expects exactly one input object.`) - const input = yield* Effect.try({ - try: () => decodeToolInput(tool, externalArgs[0]), - catch: (cause) => - new ToolRuntimeError("InvalidToolInput", `Invalid input for tool '${name}': ${String(cause)}`), - }) - const index = yield* recordAndObserve(name, input) - return yield* observeEnd( - Effect.gen(function* () { - const raw = yield* runHost(Effect.suspend(() => tool.run(input))) - const result = yield* Effect.try({ - try: () => decodeToolOutput(tool, raw), - catch: () => new ToolRuntimeError("InvalidToolOutput", `Invalid output from tool '${name}'.`), - }) - return yield* decodeOutput(result, name) - }), - { index, name, input }, - ) - }) - return { root: new ToolReference([]), calls, - keys: (path) => namespaceKeys(tools, path), - search: (args) => - Effect.suspend(() => - invokeDefinition( - "search", - searchTool, - args.map((arg) => copyOut(copyIn(arg, "Arguments for tool 'search'"))), - ), - ), + keys: (path) => namespaceKeys(callableTools, path), invoke: (path, args) => Effect.gen(function* () { const name = path.join(".") const externalArgs = args.map((arg) => copyOut(copyIn(arg, `Arguments for tool '${name}'`))) - const tool = resolve(tools, path) - if (isDefinition(tool)) return yield* invokeDefinition(name, tool, externalArgs) - const index = yield* recordAndObserve(name, externalArgs) + const call = { name } + const recordAndObserve = (input: unknown) => + Effect.sync(() => { + recordCall(call) + return calls.length - 1 + }).pipe(Effect.tap((index) => hooks?.onToolCallStart?.({ index, name, input }) ?? Effect.void)) + const tool = resolve(callableTools, path) + let describedInput: unknown + if (isDefinition(tool)) { + if (externalArgs.length !== 1) + throw new ToolRuntimeError("InvalidToolInput", `Tool '${name}' expects exactly one input object.`) + describedInput = yield* Effect.try({ + try: () => decodeToolInput(tool, externalArgs[0]), + catch: (cause) => + new ToolRuntimeError("InvalidToolInput", `Invalid input for tool '${name}': ${String(cause)}`), + }) + } + const input = isDefinition(tool) ? describedInput : externalArgs + const index = yield* recordAndObserve(input) + const currentCall = { index, name, input } + if (isDefinition(tool)) { + return yield* observeEnd( + Effect.gen(function* () { + const raw = yield* runHost(Effect.suspend(() => tool.run(describedInput))) + const result = yield* Effect.try({ + try: () => decodeToolOutput(tool, raw), + catch: () => new ToolRuntimeError("InvalidToolOutput", `Invalid output from tool '${name}'.`), + }) + return yield* decodeOutput(result, name) + }), + currentCall, + ) + } return yield* observeEnd( Effect.gen(function* () { return yield* decodeOutput(yield* runHost(Effect.suspend(() => tool(...externalArgs))), name) }), - { index, name, input: externalArgs }, + currentCall, ) }), } diff --git a/packages/codemode/src/tool-schema.ts b/packages/codemode/src/tool-schema.ts index d8b48dc548..16213fa8ee 100644 --- a/packages/codemode/src/tool-schema.ts +++ b/packages/codemode/src/tool-schema.ts @@ -5,8 +5,13 @@ const isEffectSchema = (schema: SchemaType): schema is Schema.Decoder & const renderLiteral = (value: unknown): string => JSON.stringify(value) ?? "unknown" +/** + * Bare TypeScript identifier - usable unquoted as an object key (and, in the tool runtime, + * with dot access as a tool-path segment). Anything else must be quoted/bracketed. + */ export const identifierSegment = /^[A-Za-z_$][A-Za-z0-9_$]*$/ +/** Renders a property name as a valid TS object key: bare when an identifier, quoted otherwise. */ const renderKey = (name: string): string => (identifierSegment.test(name) ? name : JSON.stringify(name)) const effectNumberSentinel = (schema: JsonSchema) => @@ -18,14 +23,20 @@ const effectNumberSentinel = (schema: JsonSchema) => const intersection = (members: ReadonlyArray): string => { const concrete = members.filter((member) => member !== "unknown") if (concrete.length === 0) return "unknown" - if (concrete.length === 1) return concrete[0] + if (concrete.length === 1) return concrete[0] ?? "unknown" return concrete.map((member) => (member.includes(" | ") ? `(${member})` : member)).join(" & ") } +/** + * Recursion ceiling for schema rendering. Object, array, and union recursion all increment + * depth, so this bounds every recursion path - pathological or structurally cyclic schemas + * degrade to `unknown` instead of overflowing the stack (rendering must never throw). + */ const MAX_RENDER_DEPTH = 8 type RenderContext = { readonly definitions: Readonly> + /** Indented, JSDoc-annotated multiline rendering (search results); compact single line otherwise. */ readonly pretty: boolean } @@ -53,6 +64,10 @@ const hasUnresolvedRef = ( ].some((item) => hasUnresolvedRef(item, definitions, seen, nextVisited)) } +/** + * Schema constraints a TypeScript type cannot express natively but a model benefits from, + * surfaced as JSDoc tags (`@deprecated`, `@default`, `@format`, `@minItems`, `@maxItems`). + */ const docTags = (schema: JsonSchema): Array => { const tags: Array = [] if (schema.deprecated === true) tags.push("@deprecated") @@ -60,7 +75,9 @@ const docTags = (schema: JsonSchema): Array => { try { const rendered = JSON.stringify(schema.default) if (rendered !== undefined) tags.push(`@default ${rendered}`) - } catch {} + } catch { + // unserializable default: skip rather than emit a broken tag + } } if (typeof schema.format === "string") tags.push(`@format ${schema.format}`) if (typeof schema.minItems === "number") tags.push(`@minItems ${schema.minItems}`) @@ -68,7 +85,13 @@ const docTags = (schema: JsonSchema): Array => { return tags } -// Neutralize `*\/` so model-provided schema text cannot terminate generated documentation. +/** + * Format a schema `description` plus `tags` as a JSDoc comment at the given indent, + * preserving multi-line text (a single line stays `/** ... *\/`; multiple lines become a + * `*`-prefixed block). `*\/` is neutralized so nothing can close the comment early, and + * blank leading/trailing lines are trimmed. Returns "" (else a trailing newline) so + * callers can prepend it directly to the field line. + */ const jsdoc = (description: string | undefined, tags: ReadonlyArray, pad: string): string => { const lines = [...(description === undefined ? [] : description.split("\n")), ...tags].map((line) => line.replaceAll("*/", "* /").replace(/\s+$/, ""), @@ -105,11 +128,17 @@ const renderSchema = ( if (schema.enum) return schema.enum.map(renderLiteral).join(" | ") const alternatives = schema.anyOf ?? schema.oneOf if (alternatives) { + // Effect's number schema emits `anyOf: [{ type: "number" }, { const: "NaN" }, + // { const: "Infinity" }, { const: "-Infinity" }]`. Collapse only that artifact; + // real JSON Schema unions such as `string | number` or `number | null` must keep + // every branch. if ( alternatives.some((item) => item.type === "number") && alternatives.every((item) => item.type === "number" || effectNumberSentinel(item)) ) return "number" + // An empty Schema.Struct({}) emits `anyOf: [{ type: "object" }, { type: "array" }]` + // (no properties/items); render the bare shape as {} instead of `{} | Array`. if ( alternatives.length === 2 && alternatives[0]?.type === "object" && @@ -154,6 +183,7 @@ const renderSchema = ( return fields.length === 0 ? "{}" : `{ ${fields.join("; ")} }` } + // Pretty: an indented block, each described field preceded by its JSDoc comment. if (properties.length === 0 && indexType === undefined) return "{}" const pad = " ".repeat(depth + 1) const lines = properties.map( @@ -178,6 +208,7 @@ export const toTypeScript = (schema: Schema.Top, decoded = false, pretty = false } } +/** Renders a raw JSON Schema document as a TypeScript type string. */ export const jsonSchemaToTypeScript = (schema: JsonSchema, pretty = false): string => { try { return renderSchema(schema, { definitions: { ...(schema.definitions ?? {}), ...(schema.$defs ?? {}) }, pretty }) @@ -186,12 +217,20 @@ export const jsonSchemaToTypeScript = (schema: JsonSchema, pretty = false): stri } } +/** One input property of a tool, extracted best-effort from its input schema. */ export type InputProperty = { readonly name: string readonly description: string | undefined readonly required: boolean } +/** + * The property names, descriptions, and required flags of a tool's input schema - the raw + * material for search text. Best-effort: Effect Schemas go through their + * JSON Schema document (the same emission signature rendering uses); JSON Schemas are read + * directly, resolving a trivial top-level `$ref` into `$defs`/`definitions` when present. + * Anything unresolvable yields `[]` (search falls back to path + description). + */ export const inputProperties = (definition: Definition): Array => { try { const document = isEffectSchema(definition.input) @@ -223,11 +262,20 @@ export const inputProperties = (definition: Definition): Array(definition: Definition, pretty = false): string => isEffectSchema(definition.input) ? toTypeScript(definition.input, false, pretty) : jsonSchemaToTypeScript(definition.input, pretty) +/** + * The model-visible TypeScript type of a tool's result; tools without an output schema + * return `unknown`. `pretty` renders the JSDoc-annotated multiline form, as for inputs. + */ export const outputTypeScript = (definition: Definition, pretty = false): string => definition.output === undefined ? "unknown" @@ -235,9 +283,18 @@ export const outputTypeScript = (definition: Definition, pretty = false): ? toTypeScript(definition.output, true, pretty) : jsonSchemaToTypeScript(definition.output, pretty) +/** + * Decodes tool input before `run` is invoked. Effect Schemas validate (throwing on failure); + * JSON-Schema-described inputs pass through unvalidated (render-only). + */ export const decodeInput = (definition: Definition, value: unknown): unknown => isEffectSchema(definition.input) ? Schema.decodeUnknownSync(definition.input)(value) : value +/** + * Decodes a tool result before it is exposed to the program. Effect Schemas validate and + * transform (throwing on failure); JSON Schema outputs and tools without an output schema pass + * the host value through unchanged. + */ export const decodeOutput = (definition: Definition, value: unknown): unknown => definition.output !== undefined && isEffectSchema(definition.output) ? Schema.decodeUnknownSync(definition.output)(value) diff --git a/packages/codemode/src/tool.ts b/packages/codemode/src/tool.ts index 0535cc9caa..6c6863f99d 100644 --- a/packages/codemode/src/tool.ts +++ b/packages/codemode/src/tool.ts @@ -1,8 +1,11 @@ import { Effect, Schema } from "effect" /** - * JSON Schema subset for model-visible signatures. CodeMode does not validate values against - * these schemas. + * JSON Schema subset accepted for render-only tool schemas. + * + * A JSON-Schema-described side of a tool is used to generate the model-visible TypeScript + * signature only - CodeMode performs no validation against it. This is the natural shape for + * adapter-provided tools (e.g. MCP definitions) whose schemas arrive as JSON Schema documents. */ export type JsonSchema = { readonly type?: string | ReadonlyArray @@ -38,8 +41,10 @@ export type Definition = { readonly run: (input: unknown) => Effect.Effect } +/** The value `run` receives: the decoded type for Effect Schemas, `unknown` for JSON Schemas. */ type InputType = S extends Schema.Decoder ? S["Type"] : unknown +/** The value `run` returns: the encoded type for Effect Schemas, `unknown` otherwise. */ type ResultType = S extends Schema.Decoder ? S["Encoded"] : unknown /** Options for defining one CodeMode tool. */ @@ -56,9 +61,29 @@ export const isDefinition = (value: unknown): value is Definition /** * Defines one schema-described tool available to a CodeMode program through `tools.*`. * - * Effect Schemas validate values; JSON Schemas only shape the model-visible signature. - * Without `output`, results are exposed as `unknown`. Hosts remain responsible for authorization - * and durable side effects. + * `input` and `output` each accept a validating Effect Schema or a render-only JSON Schema + * document. Effect Schema input is decoded before `run` is invoked, and `run` returns the + * encoded representation of an Effect Schema `output`, which CodeMode decodes before returning + * it to the program. JSON Schemas only shape the model-visible signature; values pass through + * unvalidated. `output` is optional - without it the signature advertises `unknown` and the + * host result is exposed as-is. The host tool remains responsible for authorization and + * durable side-effect handling. + * + * @example + * ```ts + * const lookup = Tool.make({ + * description: "Look up an order", + * input: Schema.Struct({ id: Schema.String }), + * output: Schema.Struct({ status: Schema.String }), + * run: ({ id }) => Effect.succeed({ status: "open" }), + * }) + * + * const fromJsonSchema = Tool.make({ + * description: "Call an adapter-described tool", + * input: { type: "object", properties: { id: { type: "string" } }, required: ["id"] }, + * run: (input) => callHost(input), + * }) + * ``` */ export const make = ( options: Options, diff --git a/packages/codemode/src/values.ts b/packages/codemode/src/values.ts index 3ba2421d5d..4ca305d815 100644 --- a/packages/codemode/src/values.ts +++ b/packages/codemode/src/values.ts @@ -1,7 +1,11 @@ -import type { Fiber } from "effect" +import type { Effect, Fiber } from "effect" export class SandboxPromise { - constructor(readonly fiber: Fiber.Fiber) {} + interrupted = false + constructor( + readonly fiber: Fiber.Fiber | undefined, + readonly immediate?: Effect.Effect, + ) {} } export class SandboxDate { diff --git a/packages/codemode/test/LICENSE.test262 b/packages/codemode/test/LICENSE.test262 deleted file mode 100644 index fb7434013b..0000000000 --- a/packages/codemode/test/LICENSE.test262 +++ /dev/null @@ -1,28 +0,0 @@ -Test262: ECMAScript Test Suite ("Software") is protected by copyright and is being -made available under the "BSD License", included below. This Software may be subject to third party rights (rights -from parties other than Ecma International), including patent rights, and no licenses under such third party rights -are granted under this license even if the third party concerned is a member of Ecma International. SEE THE ECMA -CODE OF CONDUCT IN PATENT MATTERS AVAILABLE AT https://www.ecma-international.org/ipr FOR -INFORMATION REGARDING THE LICENSING OF PATENT CLAIMS THAT ARE REQUIRED TO IMPLEMENT ECMA INTERNATIONAL STANDARDS*. - -Copyright (C) 2012 Ecma International -All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, are permitted provided that the -following conditions are met: -1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following - disclaimer. -2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the - following disclaimer in the documentation and/or other materials provided with the distribution. -3. Neither the name of the authors nor Ecma International may be used to endorse or promote products derived from - this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE ECMA INTERNATIONAL "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT -SHALL ECMA INTERNATIONAL BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING -NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH -DAMAGE. - -* Ecma International Standards hereafter means Ecma International Standards as well as Ecma Technical Reports diff --git a/packages/codemode/test/array-callbacks-test262.test.ts b/packages/codemode/test/array-callbacks-test262.test.ts deleted file mode 100644 index 3de3a94a4c..0000000000 --- a/packages/codemode/test/array-callbacks-test262.test.ts +++ /dev/null @@ -1,325 +0,0 @@ -/* - * Portions adapted from Test262 at revision 250f204f23a9249ff204be2baec29600faae7b75: - * - test/built-ins/Array/prototype/map/15.4.4.19-8-1.js - * - test/built-ins/Array/prototype/map/15.4.4.19-8-2.js - * - test/built-ins/Array/prototype/map/15.4.4.19-8-b-1.js - * - test/built-ins/Array/prototype/filter/15.4.4.20-9-1.js - * - test/built-ins/Array/prototype/filter/15.4.4.20-9-2.js - * - test/built-ins/Array/prototype/filter/15.4.4.20-9-b-1.js - * - test/built-ins/Array/prototype/find/predicate-call-parameters.js - * - test/built-ins/Array/prototype/find/predicate-not-called-on-empty-array.js - * - test/built-ins/Array/prototype/find/return-found-value-predicate-result-is-true.js - * - test/built-ins/Array/prototype/find/return-undefined-if-predicate-returns-false-value.js - * - test/built-ins/Array/prototype/findIndex/predicate-call-parameters.js - * - test/built-ins/Array/prototype/findIndex/return-index-predicate-result-is-true.js - * - test/built-ins/Array/prototype/findIndex/return-negative-one-if-predicate-returns-false-value.js - * - test/built-ins/Array/prototype/findLast/predicate-call-parameters.js - * - test/built-ins/Array/prototype/findLast/return-found-value-predicate-result-is-true.js - * - test/built-ins/Array/prototype/findLast/return-undefined-if-predicate-returns-false-value.js - * - test/built-ins/Array/prototype/findLastIndex/predicate-call-parameters.js - * - test/built-ins/Array/prototype/findLastIndex/return-index-predicate-result-is-true.js - * - test/built-ins/Array/prototype/findLastIndex/return-negative-one-if-predicate-returns-false-value.js - * - test/built-ins/Array/prototype/some/15.4.4.17-7-1.js - * - test/built-ins/Array/prototype/some/15.4.4.17-8-1.js - * - test/built-ins/Array/prototype/every/15.4.4.16-7-1.js - * - test/built-ins/Array/prototype/every/15.4.4.16-8-1.js - * - test/built-ins/Array/prototype/forEach/15.4.4.18-7-1.js - * - test/built-ins/Array/prototype/forEach/15.4.4.18-7-2.js - * - test/built-ins/Array/prototype/reduce/15.4.4.21-9-5.js - * - test/built-ins/Array/prototype/reduce/15.4.4.21-9-1.js - * - test/built-ins/Array/prototype/reduce/15.4.4.21-10-1.js - * - test/built-ins/Array/prototype/reduceRight/15.4.4.22-9-5.js - * - test/built-ins/Array/prototype/reduceRight/15.4.4.22-9-1.js - * - test/built-ins/Array/prototype/reduceRight/15.4.4.22-10-1.js - * - test/built-ins/Array/prototype/flatMap/depth-always-one.js - * - test/built-ins/Array/prototype/flatMap/mapperfunction-throws.js - * - test/built-ins/Array/prototype/sort/S15.4.4.11_A1.1_T1.js - * - test/built-ins/Array/prototype/sort/S15.4.4.11_A2.1_T1.js - * - test/built-ins/Array/prototype/sort/stability-5-elements.js - * - test/built-ins/Array/prototype/toSorted/comparefn-controls-sort.js - * - test/built-ins/Array/prototype/toSorted/comparefn-default.js - * - test/built-ins/Array/prototype/toSorted/immutable.js - * - test/built-ins/Array/prototype/toSorted/zero-or-one-element.js - * - * Copyright (C) 2015 the V8 project authors. All rights reserved. - * Copyright (C) 2018 Mathias Bynens. All rights reserved. - * Copyright (C) 2018 Shilpi Jain and Michael Ficarra. All rights reserved. - * Copyright (C) 2021 Igalia, S.L. All rights reserved. - * Copyright (C) 2021 Microsoft. All rights reserved. - * Copyright (C) 2025 Google. All rights reserved. - * Copyright (C) 2026 Garham Lee. All rights reserved. - * Copyright (c) 2012 Ecma International. All rights reserved. - * Copyright 2009 the Sputnik authors. All rights reserved. - * Test262 portions are governed by the BSD license in LICENSE.test262. - */ -import { describe, expect, test } from "bun:test" -import { Effect } from "effect" -import { CodeMode } from "../src/index.js" - -const value = async (code: string) => { - const result = await Effect.runPromise(CodeMode.execute({ code, tools: {} })) - if (!result.ok) throw new Error(`expected success, got ${result.error.kind}: ${result.error.message}`) - return result.value -} - -const cases = [ - { - path: "test/built-ins/Array/prototype/map/15.4.4.19-8-1.js", - code: `const input = [1, 2]; input[3] = 4; input[4] = 5; const result = input.map((value) => { input[2] = 3; input[5] = 6; return 1 }); return [result.length, result[5] === undefined]`, - expected: [5, true], - }, - { - path: "test/built-ins/Array/prototype/map/15.4.4.19-8-2.js", - code: `const input = [1, 2, 3, 4, 5]; const result = input.map((value) => { input[4] = -1; return value > 0 ? 1 : 0 }); return [result.length, result[4]]`, - expected: [5, 0], - }, - { - path: "test/built-ins/Array/prototype/map/15.4.4.19-8-b-1.js", - code: `const input = []; input[10] = 0; input.pop(); input[1] = undefined; let calls = 0; const result = input.map(() => { calls += 1; return 1 }); return [result.length, calls, 0 in result, 1 in result]`, - expected: [10, 1, false, true], - }, - { - path: "test/built-ins/Array/prototype/filter/15.4.4.20-9-1.js", - code: `const input = [1, 2]; input[3] = 4; input[4] = 5; const result = input.filter(() => { input[2] = 3; input[5] = 6; return true }); return result`, - expected: [1, 2, 3, 4, 5], - }, - { - path: "test/built-ins/Array/prototype/filter/15.4.4.20-9-2.js", - code: `const input = [1, 2, 3, 4, 5]; return input.filter((value) => { input[2] = -1; input[4] = -1; return value > 0 })`, - expected: [1, 2, 4], - }, - { - path: "test/built-ins/Array/prototype/filter/15.4.4.20-9-b-1.js", - code: `const input = []; input[9] = 0; input.pop(); input[1] = undefined; let calls = 0; const result = input.filter(() => { calls += 1; return false }); return [result, calls]`, - expected: [[], 1], - }, - { - path: "test/built-ins/Array/prototype/find/predicate-call-parameters.js", - code: `const input = [10, 20]; const seen = []; input.find((value, index, receiver) => { seen.push([value, index, receiver === input]); return false }); return seen`, - expected: [ - [10, 0, true], - [20, 1, true], - ], - }, - { - path: "test/built-ins/Array/prototype/find/return-found-value-predicate-result-is-true.js", - code: `return [1, 2, 3].find((value) => value > 1)`, - expected: 2, - }, - { - path: "test/built-ins/Array/prototype/find/return-undefined-if-predicate-returns-false-value.js", - code: `return [1, 2, 3].find((value) => value > 4) === undefined`, - expected: true, - }, - { - path: "test/built-ins/Array/prototype/find/predicate-not-called-on-empty-array.js", - code: `let calls = 0; const result = [].find(() => { calls += 1; return true }); return [result === undefined, calls]`, - expected: [true, 0], - }, - { - path: "test/built-ins/Array/prototype/findIndex/predicate-call-parameters.js", - code: `const input = [10, 20]; const seen = []; input.findIndex((value, index, receiver) => { seen.push([value, index, receiver === input]); return false }); return seen`, - expected: [ - [10, 0, true], - [20, 1, true], - ], - }, - { - path: "test/built-ins/Array/prototype/findIndex/return-index-predicate-result-is-true.js", - code: `return [1, 2, 3].findIndex((value) => value > 1)`, - expected: 1, - }, - { - path: "test/built-ins/Array/prototype/findIndex/return-negative-one-if-predicate-returns-false-value.js", - code: `return [1, 2, 3].findIndex((value) => value > 4)`, - expected: -1, - }, - { - path: "test/built-ins/Array/prototype/findLast/predicate-call-parameters.js", - code: `const input = [10, 20]; const seen = []; input.findLast((value, index, receiver) => { seen.push([value, index, receiver === input]); return false }); return seen`, - expected: [ - [20, 1, true], - [10, 0, true], - ], - }, - { - path: "test/built-ins/Array/prototype/findLast/return-found-value-predicate-result-is-true.js", - code: `return [1, 2, 3].findLast((value) => value < 3)`, - expected: 2, - }, - { - path: "test/built-ins/Array/prototype/findLast/return-undefined-if-predicate-returns-false-value.js", - code: `return [1, 2, 3].findLast((value) => value > 4) === undefined`, - expected: true, - }, - { - path: "test/built-ins/Array/prototype/findLastIndex/predicate-call-parameters.js", - code: `const input = [10, 20]; const seen = []; input.findLastIndex((value, index, receiver) => { seen.push([value, index, receiver === input]); return false }); return seen`, - expected: [ - [20, 1, true], - [10, 0, true], - ], - }, - { - path: "test/built-ins/Array/prototype/findLastIndex/return-index-predicate-result-is-true.js", - code: `return [1, 2, 3].findLastIndex((value) => value < 3)`, - expected: 1, - }, - { - path: "test/built-ins/Array/prototype/findLastIndex/return-negative-one-if-predicate-returns-false-value.js", - code: `return [1, 2, 3].findLastIndex((value) => value > 4)`, - expected: -1, - }, - { - path: "test/built-ins/Array/prototype/some/15.4.4.17-7-1.js", - code: `const input = [1, 2]; input[3] = 4; input[4] = 5; const seen = []; const result = input.some((value) => { input[2] = 3; seen.push(value); return false }); return [result, seen.includes(3)]`, - expected: [false, true], - }, - { - path: "test/built-ins/Array/prototype/some/15.4.4.17-8-1.js", - code: `return [].some(() => true)`, - expected: false, - }, - { - path: "test/built-ins/Array/prototype/every/15.4.4.16-7-1.js", - code: `const input = [1, 2]; input[3] = 4; input[4] = 5; const seen = []; const result = input.every((value) => { input[2] = 3; seen.push(value); return true }); return [result, seen.includes(3)]`, - expected: [true, true], - }, - { - path: "test/built-ins/Array/prototype/every/15.4.4.16-8-1.js", - code: `return [].every(() => false)`, - expected: true, - }, - { - path: "test/built-ins/Array/prototype/forEach/15.4.4.18-7-1.js", - code: `const input = [1, 2]; input[3] = 4; input[4] = 5; let calls = 0; input.forEach(() => { calls += 1; input[2] = 3; input[5] = 6 }); return calls`, - expected: 5, - }, - { - path: "test/built-ins/Array/prototype/forEach/15.4.4.18-7-2.js", - code: `const input = [1, 2, 3]; const seen = []; input.forEach((value, index) => { seen.push(value); if (index === 0) input.pop() }); return seen`, - expected: [1, 2], - }, - { - path: "test/built-ins/Array/prototype/reduce/15.4.4.21-9-1.js", - code: `const input = [1, 2]; input[3] = 4; input[4] = "5"; return input.reduce((accumulator, value) => { input[2] = 3; input[5] = 6; return accumulator + value })`, - expected: "105", - }, - { - path: "test/built-ins/Array/prototype/reduce/15.4.4.21-9-5.js", - code: `let calls = 0; const result = [1].reduce(() => { calls += 1; return 2 }); return [result, calls]`, - expected: [1, 0], - }, - { - path: "test/built-ins/Array/prototype/reduce/15.4.4.21-10-1.js", - code: `const input = [1, 2, 3, 4, 5]; input.reduce(() => 1); return input`, - expected: [1, 2, 3, 4, 5], - }, - { - path: "test/built-ins/Array/prototype/reduceRight/15.4.4.22-9-1.js", - code: `const input = ["1", 2]; input[3] = 4; input[4] = "5"; return input.reduceRight((accumulator, value) => { input[2] = 3; input[5] = 6; return accumulator + value })`, - expected: "54321", - }, - { - path: "test/built-ins/Array/prototype/reduceRight/15.4.4.22-9-5.js", - code: `let calls = 0; const result = [1].reduceRight(() => { calls += 1; return 2 }); return [result, calls]`, - expected: [1, 0], - }, - { - path: "test/built-ins/Array/prototype/reduceRight/15.4.4.22-10-1.js", - code: `const input = [1, 2, 3, 4, 5]; input.reduceRight(() => 1); return input`, - expected: [1, 2, 3, 4, 5], - }, - { - path: "test/built-ins/Array/prototype/flatMap/depth-always-one.js", - code: `return [1, 2, 3].flatMap((value) => [[value * 2]])`, - expected: [[2], [4], [6]], - }, - { - path: "test/built-ins/Array/prototype/flatMap/mapperfunction-throws.js", - code: `try { [1, 2].flatMap(() => { throw "stop" }) } catch (error) { return error === "stop" } return false`, - expected: true, - }, - { - path: "test/built-ins/Array/prototype/sort/S15.4.4.11_A1.1_T1.js", - code: `const input = []; input[2] = 0; input.pop(); input.sort(); return [input.length, input[0] === undefined, input[1] === undefined]`, - expected: [2, true, true], - }, - { - path: "test/built-ins/Array/prototype/sort/S15.4.4.11_A2.1_T1.js", - code: `return ["z", "y", "x", "w", "v", "u", "t", "s", "r", "q", "p", "o", "n", "M", "L", "K", "J", "I", "H", "G", "F", "E", "D", "C", "B", "A"].sort()`, - expected: [ - "A", - "B", - "C", - "D", - "E", - "F", - "G", - "H", - "I", - "J", - "K", - "L", - "M", - "n", - "o", - "p", - "q", - "r", - "s", - "t", - "u", - "v", - "w", - "x", - "y", - "z", - ], - }, - { - path: "test/built-ins/Array/prototype/sort/stability-5-elements.js", - code: `const input = [{ n: "A", r: 2 }, { n: "B", r: 3 }, { n: "C", r: 2 }, { n: "D", r: 3 }, { n: "E", r: 3 }]; return input.sort((left, right) => right.r - left.r).map((item) => item.n).join("")`, - expected: "BDEAC", - }, - { - path: "test/built-ins/Array/prototype/toSorted/comparefn-controls-sort.js", - code: `const mixed = [333, 33, 3, 222, 22, 2, 111, 11, 1]; return [[1, 2, 3, 4].toSorted((a, b) => a - b), [4, 3, 2, 1].toSorted((a, b) => a - b), mixed.toSorted((a, b) => a - b), [1, 2, 3, 4].toSorted((a, b) => b - a), [4, 3, 2, 1].toSorted((a, b) => b - a), mixed.toSorted((a, b) => b - a)]`, - expected: [ - [1, 2, 3, 4], - [1, 2, 3, 4], - [1, 2, 3, 11, 22, 33, 111, 222, 333], - [4, 3, 2, 1], - [4, 3, 2, 1], - [333, 222, 111, 33, 22, 11, 3, 2, 1], - ], - }, - { - path: "test/built-ins/Array/prototype/toSorted/comparefn-default.js", - code: `return [[1, 2, 3, 4].toSorted(), [4, 3, 2, 1].toSorted(), ["a", 2, 1, "z"].toSorted(), [333, 33, 3, 222, 22, 2, 111, 11, 1].toSorted()]`, - expected: [ - [1, 2, 3, 4], - [1, 2, 3, 4], - [1, 2, "a", "z"], - [1, 11, 111, 2, 22, 222, 3, 33, 333], - ], - }, - { - path: "test/built-ins/Array/prototype/toSorted/immutable.js", - code: `const input = [2, 0, 1]; const result = input.toSorted(); return [input, result !== input]`, - expected: [[2, 0, 1], true], - }, - { - path: "test/built-ins/Array/prototype/toSorted/zero-or-one-element.js", - code: `const zero = []; const one = [1]; const zeroResult = zero.toSorted(); const oneResult = one.toSorted(); return [zeroResult, oneResult, zeroResult !== zero, oneResult !== one]`, - expected: [[], [1], true, true], - }, -] as const - -describe("Test262 Array callback adaptations", () => { - for (const item of cases) { - test(item.path, async () => { - expect(await value(item.code)).toEqual(item.expected) - }) - } -}) diff --git a/packages/codemode/test/array-core-test262.test.ts b/packages/codemode/test/array-core-test262.test.ts deleted file mode 100644 index a04e1c2566..0000000000 --- a/packages/codemode/test/array-core-test262.test.ts +++ /dev/null @@ -1,323 +0,0 @@ -/* - * Portions adapted from Test262 at revision 250f204f23a9249ff204be2baec29600faae7b75: - * - test/built-ins/Array/prototype/includes/samevaluezero.js - * - test/built-ins/Array/prototype/includes/using-fromindex.js - * - test/built-ins/Array/prototype/join/S15.4.4.5_A3.1_T1.js - * - test/built-ins/Array/prototype/join/S15.4.4.5_A3.2_T1.js - * - test/built-ins/Array/prototype/slice/S15.4.4.10_A1.2_T2.js - * - test/built-ins/Array/prototype/concat/S15.4.4.4_A1_T1.js - * - test/built-ins/Array/prototype/concat/S15.4.4.4_A1_T2.js - * - test/built-ins/Array/prototype/concat/S15.4.4.4_A1_T3.js - * - test/built-ins/Array/prototype/indexOf/fromindex-zero-conversion.js - * - test/built-ins/Array/prototype/indexOf/length-zero-returns-minus-one.js - * - test/built-ins/Array/prototype/lastIndexOf/fromindex-zero-conversion.js - * - test/built-ins/Array/prototype/lastIndexOf/length-zero-returns-minus-one.js - * - test/built-ins/Array/prototype/at/returns-item.js - * - test/built-ins/Array/prototype/at/returns-item-relative-index.js - * - test/built-ins/Array/prototype/at/returns-undefined-for-out-of-range-index.js - * - test/built-ins/Array/prototype/flat/null-undefined-elements.js - * - test/built-ins/Array/prototype/flat/positive-infinity.js - * - test/built-ins/Array/prototype/reverse/S15.4.4.8_A1_T1.js - * - test/built-ins/Array/prototype/toReversed/immutable.js - * - test/built-ins/Array/prototype/toReversed/zero-or-one-element.js - * - test/built-ins/Array/prototype/with/immutable.js - * - test/built-ins/Array/prototype/with/index-negative.js - * - test/built-ins/Array/prototype/push/S15.4.4.7_A1_T1.js - * - test/built-ins/Array/prototype/pop/S15.4.4.6_A1.1_T1.js - * - test/built-ins/Array/prototype/shift/S15.4.4.9_A1.1_T1.js - * - test/built-ins/Array/prototype/unshift/S15.4.4.13_A1_T1.js - * - test/built-ins/Array/prototype/splice/S15.4.4.12_A1.1_T1.js - * - test/built-ins/Array/prototype/splice/S15.4.4.12_A1.2_T1.js - * - test/built-ins/Array/prototype/splice/called_with_one_argument.js - * - test/built-ins/Array/prototype/fill/fill-values.js - * - test/built-ins/Array/prototype/fill/fill-values-custom-start-and-end.js - * - test/built-ins/Array/prototype/fill/return-this.js - * - test/built-ins/Array/prototype/copyWithin/non-negative-target-start-and-end.js - * - test/built-ins/Array/prototype/copyWithin/return-this.js - * - test/built-ins/Array/prototype/keys/iteration.js - * - test/built-ins/Array/prototype/values/iteration.js - * - test/built-ins/Array/prototype/entries/iteration.js - * - test/built-ins/Array/isArray/15.4.3.2-0-3.js - * - test/built-ins/Array/isArray/15.4.3.2-0-4.js - * - test/built-ins/Array/from/from-array.js - * - test/built-ins/Array/from/from-string.js - * - test/built-ins/Array/from/array-like-has-length-but-no-indexes-with-values.js - * - test/built-ins/Array/of/creates-a-new-array-from-arguments.js - * - * Copyright (C) 2015 André Bargull. All rights reserved. - * Copyright (C) 2015 the V8 project authors. All rights reserved. - * Copyright (C) 2016 the V8 project authors. All rights reserved. - * Copyright (C) 2018 Shilpi Jain and Michael Ficarra. All rights reserved. - * Copyright (C) 2020 Alexey Shvayka. All rights reserved. - * Copyright (C) 2020 Rick Waldron. All rights reserved. - * Copyright (C) 2021 Igalia, S.L. All rights reserved. - * Copyright (c) 2012 Ecma International. All rights reserved. - * Copyright (c) 2014 Hank Yates. All rights reserved. - * Copyright (c) 2015 the V8 project authors. All rights reserved. - * Copyright (c) 2021 Rick Waldron. All rights reserved. - * Copyright 2009 the Sputnik authors. All rights reserved. - * Copyright 2015 Microsoft Corporation. All rights reserved. - * Copyright 2016 The V8 project authors. All rights reserved. - * Test262 portions are governed by the BSD license in LICENSE.test262. - */ -import { describe, expect, test } from "bun:test" -import { Effect } from "effect" -import { CodeMode } from "../src/index.js" - -const value = async (code: string) => { - const result = await Effect.runPromise(CodeMode.execute({ code, tools: {} })) - if (!result.ok) throw new Error(`expected success, got ${result.error.kind}: ${result.error.message}`) - return result.value -} - -const cases = [ - { - path: "test/built-ins/Array/prototype/includes/samevaluezero.js", - code: `const input = [42, 0, 1, NaN]; return [input.includes(42), input.includes("42"), input.includes([42]), input.includes(true), input.includes(NaN), input.includes(0), input.includes(-0), input.includes(null), input.includes("")]`, - expected: [true, false, false, false, true, true, true, false, false], - }, - { - path: "test/built-ins/Array/prototype/includes/using-fromindex.js", - code: `const input = ["a", "b", "c"]; return [input.includes("a", 0), input.includes("a", 1), input.includes("a", -4), input.includes("a", -3), input.includes("a", -2), input.includes("b", 0), input.includes("b", 1), input.includes("b", 2), input.includes("b", -3), input.includes("b", -2), input.includes("b", -1), input.includes("c", 0), input.includes("c", 2), input.includes("c", 3), input.includes("c", -3), input.includes("c", -1)]`, - expected: [true, false, true, true, false, true, true, false, true, true, false, true, true, false, true, true], - }, - { - path: "test/built-ins/Array/prototype/join/S15.4.4.5_A3.1_T1.js", - code: `return [[0, 1, 2, 3].join("&"), [0, 1, 2, 3].join("")]`, - expected: ["0&1&2&3", "0123"], - }, - { - path: "test/built-ins/Array/prototype/join/S15.4.4.5_A3.2_T1.js", - code: `return [ - ["", "", ""].join(""), - ["&", "&", "&"].join("&"), - [true, true, true].join(), - [null, null, null].join(), - [undefined, undefined, undefined].join(), - [Infinity, Infinity, Infinity].join(), - [NaN, NaN, NaN].join(), - ]`, - expected: ["", "&&&&&", "true,true,true", ",,", ",,", "Infinity,Infinity,Infinity", "NaN,NaN,NaN"], - }, - { - path: "test/built-ins/Array/prototype/slice/S15.4.4.10_A1.2_T2.js", - code: `return [0, 1, 2, 3, 4].slice(-1, 5)`, - expected: [4], - }, - { - path: "test/built-ins/Array/prototype/concat/S15.4.4.4_A1_T1.js", - code: `return [].concat([0, 1], [2, 3, 4])`, - expected: [0, 1, 2, 3, 4], - }, - { - path: "test/built-ins/Array/prototype/concat/S15.4.4.4_A1_T2.js", - code: `const object = { value: 1 }; const result = [0].concat(object, [1, 2], -1, true, "NaN"); return [result, result[1] === object]`, - expected: [[0, { value: 1 }, 1, 2, -1, true, "NaN"], true], - }, - { - path: "test/built-ins/Array/prototype/concat/S15.4.4.4_A1_T3.js", - code: `const input = [0, 1]; const result = input.concat(); return [result, result !== input]`, - expected: [[0, 1], true], - }, - { - path: "test/built-ins/Array/prototype/indexOf/fromindex-zero-conversion.js", - code: `const result = [true].indexOf(true, -0); return [result, 1 / result === Infinity]`, - expected: [0, true], - }, - { - path: "test/built-ins/Array/prototype/indexOf/length-zero-returns-minus-one.js", - code: `return [].indexOf(1)`, - expected: -1, - }, - { - path: "test/built-ins/Array/prototype/lastIndexOf/fromindex-zero-conversion.js", - code: `const result = [true].lastIndexOf(true, -0); return [result, 1 / result === Infinity]`, - expected: [0, true], - }, - { - path: "test/built-ins/Array/prototype/lastIndexOf/length-zero-returns-minus-one.js", - code: `return [].lastIndexOf(1)`, - expected: -1, - }, - { - path: "test/built-ins/Array/prototype/at/returns-item.js", - code: `const input = [1, 2, 3, 4, undefined, 5]; return [input.at(0), input.at(1), input.at(2), input.at(3), input.at(4) === undefined, input.at(5)]`, - expected: [1, 2, 3, 4, true, 5], - }, - { - path: "test/built-ins/Array/prototype/at/returns-item-relative-index.js", - code: `const input = [1, 2, 3, 4, undefined, 5]; return [input.at(0), input.at(-1), input.at(-2) === undefined, input.at(-3), input.at(-4)]`, - expected: [1, 5, true, 4, 3], - }, - { - path: "test/built-ins/Array/prototype/at/returns-undefined-for-out-of-range-index.js", - code: `const input = []; return [input.at(-2) === undefined, input.at(0) === undefined, input.at(1) === undefined]`, - expected: [true, true, true], - }, - { - path: "test/built-ins/Array/prototype/flat/null-undefined-elements.js", - code: `const result = [1, [null, [undefined]]].flat(2); return [result.length, result[0], result[1] === null, result[2] === undefined]`, - expected: [3, 1, true, true], - }, - { - path: "test/built-ins/Array/prototype/flat/positive-infinity.js", - code: `return [1, [2, [3, [4]]]].flat(Infinity)`, - expected: [1, 2, 3, 4], - }, - { - path: "test/built-ins/Array/prototype/reverse/S15.4.4.8_A1_T1.js", - code: `const empty = []; const one = [1]; const input = [1, 2]; const emptyResult = empty.reverse(); const oneResult = one.reverse(); const result = input.reverse(); return [emptyResult === empty, oneResult === one, result === input, input]`, - expected: [true, true, true, [2, 1]], - }, - { - path: "test/built-ins/Array/prototype/toReversed/immutable.js", - code: `const input = [0, 1, 2]; const result = input.toReversed(); return [input, result !== input]`, - expected: [[0, 1, 2], true], - }, - { - path: "test/built-ins/Array/prototype/toReversed/zero-or-one-element.js", - code: `const zero = []; const one = [1]; const zeroResult = zero.toReversed(); const oneResult = one.toReversed(); return [zeroResult, oneResult, zeroResult !== zero, oneResult !== one]`, - expected: [[], [1], true, true], - }, - { - path: "test/built-ins/Array/prototype/with/immutable.js", - code: `const input = [0, 1, 2]; const result = input.with(1, 3); return [input, result !== input, input.with(1, 1) !== input]`, - expected: [[0, 1, 2], true, true], - }, - { - path: "test/built-ins/Array/prototype/with/index-negative.js", - code: `const input = [0, 1, 2]; return [input.with(-1, 4), input.with(-3, 4)]`, - expected: [ - [0, 1, 4], - [4, 1, 2], - ], - }, - { - path: "test/built-ins/Array/prototype/push/S15.4.4.7_A1_T1.js", - code: `const input = []; return [input.push(1), input.push(), input.push(-1), input]`, - expected: [1, 1, 2, [1, -1]], - }, - { - path: "test/built-ins/Array/prototype/pop/S15.4.4.6_A1.1_T1.js", - code: `const input = []; return [input.pop() === undefined, input.length]`, - expected: [true, 0], - }, - { - path: "test/built-ins/Array/prototype/shift/S15.4.4.9_A1.1_T1.js", - code: `const input = []; return [input.shift() === undefined, input.length]`, - expected: [true, 0], - }, - { - path: "test/built-ins/Array/prototype/unshift/S15.4.4.13_A1_T1.js", - code: `const input = []; return [input.unshift(1), input[0], input.unshift(), input.unshift(-1), input]`, - expected: [1, 1, 1, 2, [-1, 1]], - }, - { - path: "test/built-ins/Array/prototype/splice/S15.4.4.12_A1.1_T1.js", - code: `const input = [0, 1, 2, 3]; const removed = input.splice(0, 3); return [input, removed]`, - expected: [[3], [0, 1, 2]], - }, - { - path: "test/built-ins/Array/prototype/splice/S15.4.4.12_A1.2_T1.js", - code: `const input = [0, 1]; const removed = input.splice(-2, -1); return [input, removed]`, - expected: [[0, 1], []], - }, - { - path: "test/built-ins/Array/prototype/splice/called_with_one_argument.js", - code: `const input = ["first", "second", "third"]; const removed = input.splice(1); return [input, removed]`, - expected: [["first"], ["second", "third"]], - }, - { - path: "test/built-ins/Array/prototype/fill/fill-values-custom-start-and-end.js", - code: `const input = [0, 0, 0, 0, 0]; input.fill(8, -3, 4); const sparse = []; sparse[4] = 0; sparse.fill(8, 1, 3); return [[0, 0, 0].fill(8, 1, 2), input, [0, 0, 0, 0, 0].fill(8, -2, -1), [0, 0, 0, 0, 0].fill(8, -1, -3), [0 in sparse, sparse[1], sparse[2], 3 in sparse, sparse[4]]]`, - expected: [ - [0, 8, 0], - [0, 0, 8, 8, 0], - [0, 0, 0, 8, 0], - [0, 0, 0, 0, 0], - [false, 8, 8, false, 0], - ], - }, - { - path: "test/built-ins/Array/prototype/fill/return-this.js", - code: `const input = []; return input.fill(1) === input`, - expected: true, - }, - { - path: "test/built-ins/Array/prototype/fill/fill-values.js", - code: `const omitted = [0, 0].fill(); return [[].fill(8), omitted.map((value) => value === undefined), [0, 0, 0].fill(8)]`, - expected: [[], [true, true], [8, 8, 8]], - }, - { - path: "test/built-ins/Array/prototype/copyWithin/non-negative-target-start-and-end.js", - code: `return [[0, 1, 2, 3].copyWithin(0, 0, 0), [0, 1, 2, 3].copyWithin(0, 0, 2), [0, 1, 2, 3].copyWithin(0, 1, 2), [0, 1, 2, 3].copyWithin(1, 0, 2), [0, 1, 2, 3, 4, 5].copyWithin(1, 3, 5)]`, - expected: [ - [0, 1, 2, 3], - [0, 1, 2, 3], - [1, 1, 2, 3], - [0, 0, 1, 3], - [0, 3, 4, 3, 4, 5], - ], - }, - { - path: "test/built-ins/Array/prototype/copyWithin/return-this.js", - code: `const input = [0, 1, 2, 3]; const result = input.copyWithin(1, 0, 2); return [input, result === input]`, - expected: [[0, 0, 1, 3], true], - }, - { - path: "test/built-ins/Array/prototype/keys/iteration.js", - code: `return ["a", "b", "c"].keys()`, - expected: [0, 1, 2], - }, - { - path: "test/built-ins/Array/prototype/values/iteration.js", - code: `return ["a", "b", "c"].values()`, - expected: ["a", "b", "c"], - }, - { - path: "test/built-ins/Array/prototype/entries/iteration.js", - code: `return ["a", "b"].entries()`, - expected: [ - [0, "a"], - [1, "b"], - ], - }, - { - path: "test/built-ins/Array/isArray/15.4.3.2-0-3.js", - code: `return [Array.isArray([]), Array.isArray([1]), Array.isArray(Array.of(1))]`, - expected: [true, true, true], - }, - { - path: "test/built-ins/Array/isArray/15.4.3.2-0-4.js", - code: `return [Array.isArray(42), Array.isArray({}), Array.isArray(null), Array.isArray("array")]`, - expected: [false, false, false, false], - }, - { - path: "test/built-ins/Array/from/from-array.js", - code: `const input = [0, "foo", undefined, Infinity]; const result = Array.from(input); return [result.length, result[0], result[1], result[2] === undefined, result[3] === Infinity, result !== input, result instanceof Array]`, - expected: [4, 0, "foo", true, true, true, true], - }, - { - path: "test/built-ins/Array/from/from-string.js", - code: `return Array.from("Test")`, - expected: ["T", "e", "s", "t"], - }, - { - path: "test/built-ins/Array/from/array-like-has-length-but-no-indexes-with-values.js", - code: `const result = Array.from({ length: 5 }); const mapped = result.map(() => 1); return [result.length, result.map((value) => value === undefined), mapped.length, mapped]`, - expected: [5, [true, true, true, true, true], 5, [1, 1, 1, 1, 1]], - }, - { - path: "test/built-ins/Array/of/creates-a-new-array-from-arguments.js", - code: `const mixed = Array.of(undefined, false, null, undefined); return [Array.of("Mike", "Rick", "Leo"), mixed.length, mixed[0] === undefined, mixed[1], mixed[2], mixed[3] === undefined, Array.of()]`, - expected: [["Mike", "Rick", "Leo"], 4, true, false, null, true, []], - }, -] as const - -describe("Test262 Array core adaptations", () => { - for (const item of cases) { - test(item.path, async () => { - expect(await value(item.code)).toEqual(item.expected) - }) - } -}) diff --git a/packages/codemode/test/codemode.test.ts b/packages/codemode/test/codemode.test.ts index a9218f0e7e..221b5e07df 100644 --- a/packages/codemode/test/codemode.test.ts +++ b/packages/codemode/test/codemode.test.ts @@ -26,22 +26,6 @@ describe("CodeMode host failure boundary", () => { }) }) - test("does not rewrite explicit safe tool failures", async () => { - const result = await run( - Tool.make({ - description: "Fail safely", - input: Schema.Struct({}), - output: Schema.String, - run: () => Effect.fail(toolError("File not found: /tmp/report.json")), - }), - ) - - expect(result.ok ? undefined : result.error).toStrictEqual({ - kind: "ToolFailure", - message: "File not found: /tmp/report.json", - }) - }) - test("sanitizes unknown host failures and defects", async () => { for (const failure of [ Effect.fail(new UnsafeHostError({ reason: "Authorization: Bearer typed-secret" })), @@ -522,26 +506,6 @@ describe("CodeMode public contract", () => { expect(Schema.decodeUnknownSync(CodeMode.Result)(JSON.parse(JSON.stringify(reusable)))).toStrictEqual(reusable) }) - test("a reused execution Effect starts from a clean slate", async () => { - const echo = Tool.make({ - description: "echo", - input: Schema.Struct({}), - output: Schema.Number, - run: () => Effect.succeed(1), - }) - const effect = CodeMode.execute({ - tools: { host: { echo } }, - code: `console.log("hi"); return await tools.host.echo({})`, - limits: { maxToolCalls: 1 }, - }) - const first = await Effect.runPromise(effect) - const second = await Effect.runPromise(effect) - // Per-execution state (tool-call budget and audit list, logs, timeout bookkeeping) must - // bind at run time, so the second run neither exhausts the budget nor leaks run 1's logs. - expect(first).toStrictEqual(second) - expect(second).toStrictEqual({ ok: true, value: 1, logs: ["hi"], toolCalls: [{ name: "host.echo" }] }) - }) - test("inlines a COMPLETE small catalog and keeps search registered but unadvertised", async () => { const runtime = CodeMode.make({ tools }) expect(runtime.catalog()).toStrictEqual([ @@ -557,11 +521,11 @@ describe("CodeMode public contract", () => { " - tools.orders.lookup(input: {\n id: string,\n}): Promise<{\n id: string,\n status: string,\n}> // Look up an order by ID", ) // A fully inlined catalog does not advertise search in the instructions... - expect(runtime.instructions()).not.toContain("search(") + expect(runtime.instructions()).not.toMatch(/\$codemode/) - // ...but the search built-in stays available, so a speculative call still works with the + // ...but the search tool stays registered, so a speculative call still works with the // same signature as the inline catalog. - const result = await Effect.runPromise(runtime.execute(`return search({ query: "order" })`)) + const result = await Effect.runPromise(runtime.execute(`return await tools.$codemode.search({ query: "order" })`)) expect(result.ok).toBe(true) if (result.ok) { expect(result.value).toStrictEqual({ @@ -599,7 +563,9 @@ describe("CodeMode public contract", () => { 'tools.context7["resolve-library-id"](input: {\n libraryName: string,\n}): Promise', ) - const search = await Effect.runPromise(runtime.execute(`return search({ query: "resolve library id" })`)) + const search = await Effect.runPromise( + runtime.execute(`return await tools.$codemode.search({ query: "resolve library id" })`), + ) expect(search.ok).toBe(true) if (search.ok) { expect(search.value).toStrictEqual({ @@ -622,7 +588,7 @@ describe("CodeMode public contract", () => { if (call.ok) expect(call.value).toBe("/resolved/TypeScript") const exact = await Effect.runPromise( - runtime.execute(`return search({ query: 'tools.context7["resolve-library-id"]' })`), + runtime.execute(`return await tools.$codemode.search({ query: 'tools.context7["resolve-library-id"]' })`), ) expect(exact.ok).toBe(true) if (exact.ok) expect(exact.value).toMatchObject({ remaining: 0, next: null }) @@ -646,7 +612,7 @@ describe("CodeMode public contract", () => { expect(instructions).toContain("Do not infer or normalize tool names") expect(instructions).toContain("bracket notation and quotes are part of the path") expect(instructions).toContain("surrounding agent tools are not available") - expect(instructions).toContain("Only Code Mode tools listed here are available") + expect(instructions).toContain("Only Code Mode tools listed here and internal runtime tools") // Placeholders use generic namespace/tool/field names only - no fabricated real tools // and no real catalog tools cherry-picked into example lines. expect(instructions).toContain("`const result = await tools..(input)`") @@ -665,11 +631,15 @@ describe("CodeMode public contract", () => { // PARTIAL: the workflow starts with search (with query-style guidance that is clearly // a query string, never a tool name) and the browse-namespace rule appears. expect(partial).toContain( - '1. If needed, discover tools with the built-in search function: `return search({ query: "" })`.', + '1. If needed, discover tools: `return await tools.$codemode.search({ query: "" })`.', ) expect(partial).toContain("In the next execution, copy a returned path exactly") - expect(partial).toContain("Only Code Mode tools listed here or returned by the built-in `search` function") - expect(partial).toContain('- Browse one namespace: `search({ query: "", namespace: "" })`.') + expect(partial).toContain( + "Only Code Mode tools listed here or returned by `tools.$codemode.search` and internal runtime tools", + ) + expect(partial).toContain( + '- Browse one namespace: `await tools.$codemode.search({ query: "", namespace: "" })`.', + ) expect(partial).toContain("repeat the same search with `offset: next.offset`") expect(partial).toContain(" limit?: number,\n offset?: number,") expect(partial).not.toContain("total_count") @@ -682,17 +652,12 @@ describe("CodeMode public contract", () => { expect(instructions).toContain("not a general-purpose runtime") expect(instructions).not.toContain("Standard modern JavaScript works") expect(instructions).not.toContain("TypeScript type annotations") - for (const missing of ["Modules/imports", "classes", "generators", "fetch"]) { + for (const missing of ["Modules/imports", "classes", "generators", "fetch", "promise chaining"]) { expect(instructions).toContain(missing) } - expect(instructions).not.toContain("new Promise(...) are unavailable") - expect(instructions).not.toContain("promise chaining") expect(instructions).toContain("URL, URLSearchParams, and URI encoding helpers") expect(instructions).not.toContain("host globals") expect(instructions).toContain("Use Code Mode tools for external operations") - expect(instructions).toContain( - "Prefer explicit `return`; otherwise only the final top-level expression becomes the result.", - ) expect(instructions).toContain( "Dates and URLs serialize to strings at data boundaries; Map/Set/RegExp/URLSearchParams serialize to `{}`.", ) @@ -706,7 +671,7 @@ describe("CodeMode public contract", () => { expect(instructions).toContain("## Available tools") expect(instructions).not.toContain("## Workflow") expect(instructions).not.toContain("## Rules") - expect(instructions).not.toContain("search(") + expect(instructions).not.toMatch(/\$codemode/) }) test("uses one ranked search returning complete definitions for large catalogs", async () => { @@ -726,15 +691,17 @@ describe("CodeMode public contract", () => { tools: { thread: { uploadFile: upload, generateImage: generate }, orders: { lookup } }, discovery: { catalogBudget: 0 }, }) - expect(runtime.instructions()).toContain("Available tools (PARTIAL - 0 of 3 shown; find the rest with search(...))") + expect(runtime.instructions()).toContain( + "Available tools (PARTIAL - 0 of 3 shown; find the rest with tools.$codemode.search)", + ) expect(runtime.instructions()).toContain("- thread (2 tools, none shown)") expect(runtime.instructions()).toContain("- orders (1 tool, none shown)") - expect(runtime.instructions()).toContain("Search returns complete callable signatures:\n- search(input: {") + expect(runtime.instructions()).toMatch(/\$codemode\.search/) expect(runtime.instructions()).not.toMatch(/tools\.thread\.uploadFile\(input/) const result = await Effect.runPromise( runtime.execute(` - return search({ + return await tools.$codemode.search({ query: "send message attachment upload file to current Discord thread", limit: 2 }) @@ -758,14 +725,14 @@ describe("CodeMode public contract", () => { remaining: 0, next: null, }) - expect(result.toolCalls).toStrictEqual([{ name: "search" }]) + expect(result.toolCalls).toStrictEqual([{ name: "$codemode.search" }]) const variants = await Effect.runPromise( runtime.execute(` - return [ - search({ query: "file" }), - search({ query: "image" }) - ] + return await Promise.all([ + tools.$codemode.search({ query: "file" }), + tools.$codemode.search({ query: "image" }) + ]) `), ) expect(variants.ok).toBe(true) @@ -777,35 +744,12 @@ describe("CodeMode public contract", () => { "tools.thread.generateImage", ) } - }) - test("search is a counted tool call: it burns maxToolCalls and fires the hooks", async () => { - const started: Array = [] - const ended: Array = [] - const limited = CodeMode.make({ - tools, - limits: { maxToolCalls: 1 }, - onToolCallStart: (call) => Effect.sync(() => void started.push(call.name)), - onToolCallEnd: (call) => Effect.sync(() => void ended.push(`${call.name}:${call.outcome}`)), - }) - const result = await Effect.runPromise(limited.execute(`search({}); return search({})`)) - expect(result.ok).toBe(false) - if (!result.ok) expect(result.error.kind).toBe("ToolCallLimitExceeded") - expect(started).toEqual(["search"]) - expect(ended).toEqual(["search:success"]) - }) - - test("search is an opaque, shadowable global like other built-ins", async () => { - const runtime = CodeMode.make({ tools }) - expect(await Effect.runPromise(runtime.execute(`return typeof search`))).toMatchObject({ value: "function" }) - // A program-level declaration shadows the global, as JS module scope does. - const shadowed = await Effect.runPromise(runtime.execute(`const search = () => "local"; return search()`)) - expect(shadowed.ok).toBe(true) - if (shadowed.ok) expect(shadowed.value).toBe("local") - // The reference itself cannot cross the data boundary. - const escaped = await Effect.runPromise(runtime.execute(`return { search }`)) - expect(escaped.ok).toBe(false) - if (!escaped.ok) expect(escaped.error.kind).toBe("InvalidDataValue") + const removed = await Effect.runPromise( + runtime.execute(`return await tools.$codemode.describe({ path: "thread.uploadFile" })`), + ) + expect(removed.ok).toBe(false) + if (!removed.ok) expect(removed.error.kind).toBe("UnknownTool") }) test("search defaults to 10 results and resolves exact tool paths", async () => { @@ -822,7 +766,7 @@ describe("CodeMode public contract", () => { }, }) - const browse = await Effect.runPromise(runtime.execute(`return search({})`)) + const browse = await Effect.runPromise(runtime.execute(`return await tools.$codemode.search({})`)) expect(browse.ok).toBe(true) if (browse.ok) { const value = browse.value as { @@ -836,7 +780,9 @@ describe("CodeMode public contract", () => { } for (const query of ["many.tool13", "tools.many.tool13"]) { - const exact = await Effect.runPromise(runtime.execute(`return search({ query: ${JSON.stringify(query)} })`)) + const exact = await Effect.runPromise( + runtime.execute(`return await tools.$codemode.search({ query: ${JSON.stringify(query)} })`), + ) expect(exact.ok).toBe(true) if (exact.ok) { expect(exact.value).toStrictEqual({ @@ -870,7 +816,9 @@ describe("CodeMode public contract", () => { }) // Empty query + namespace browses just that namespace, alphabetical by path. - const browse = await Effect.runPromise(runtime.execute(`return search({ query: "", namespace: "github" })`)) + const browse = await Effect.runPromise( + runtime.execute(`return await tools.$codemode.search({ query: "", namespace: "github" })`), + ) expect(browse.ok).toBe(true) if (browse.ok) { const value = browse.value as { items: Array<{ path: string }>; remaining: number } @@ -882,7 +830,9 @@ describe("CodeMode public contract", () => { } // A query + namespace ranks within that namespace only. - const scoped = await Effect.runPromise(runtime.execute(`return search({ query: "issues", namespace: "linear" })`)) + const scoped = await Effect.runPromise( + runtime.execute(`return await tools.$codemode.search({ query: "issues", namespace: "linear" })`), + ) expect(scoped.ok).toBe(true) if (scoped.ok) { const value = scoped.value as { items: Array<{ path: string }>; remaining: number } @@ -890,7 +840,9 @@ describe("CodeMode public contract", () => { expect(value.items[0]?.path).toBe("tools.linear.list_issues") } - const invalid = await Effect.runPromise(runtime.execute(`return search({ query: "issues", namespace: 7 })`)) + const invalid = await Effect.runPromise( + runtime.execute(`return await tools.$codemode.search({ query: "issues", namespace: 7 })`), + ) expect(invalid.ok).toBe(false) if (!invalid.ok) expect(invalid.error.kind).toBe("InvalidToolInput") }) @@ -915,7 +867,9 @@ describe("CodeMode public contract", () => { // "attachment" appears in neither path nor description - only in the input schema's // property names, which the searchable text includes. - const byParameter = await Effect.runPromise(runtime.execute(`return search({ query: "attachment" })`)) + const byParameter = await Effect.runPromise( + runtime.execute(`return await tools.$codemode.search({ query: "attachment" })`), + ) expect(byParameter.ok).toBe(true) if (byParameter.ok) { const value = byParameter.value as { items: Array<{ path: string }>; remaining: number } @@ -924,7 +878,9 @@ describe("CodeMode public contract", () => { } // Substring matching: a partial word ("docum") still hits the description. - const bySubstring = await Effect.runPromise(runtime.execute(`return search({ query: "docum" })`)) + const bySubstring = await Effect.runPromise( + runtime.execute(`return await tools.$codemode.search({ query: "docum" })`), + ) expect(bySubstring.ok).toBe(true) if (bySubstring.ok) { const value = bySubstring.value as { items: Array<{ path: string }>; remaining: number } @@ -951,7 +907,9 @@ describe("CodeMode public contract", () => { }) // "issues" still finds the singular-only tool (term OR singular(term) per field)... - const plural = await Effect.runPromise(runtime.execute(`return search({ query: "issues", namespace: "tracker" })`)) + const plural = await Effect.runPromise( + runtime.execute(`return await tools.$codemode.search({ query: "issues", namespace: "tracker" })`), + ) expect(plural.ok).toBe(true) if (plural.ok) { const value = plural.value as { items: Array<{ path: string }>; remaining: number } @@ -960,7 +918,7 @@ describe("CodeMode public contract", () => { } // ...while a true "issues" path match still outranks the singular-only description match. - const ranked = await Effect.runPromise(runtime.execute(`return search({ query: "issues" })`)) + const ranked = await Effect.runPromise(runtime.execute(`return await tools.$codemode.search({ query: "issues" })`)) expect(ranked.ok).toBe(true) if (ranked.ok) { const value = ranked.value as { items: Array<{ path: string }>; remaining: number } @@ -987,7 +945,7 @@ describe("CodeMode public contract", () => { alpha: { beta: simple("Middle"), aardvark: simple("First") }, }, }) - const browse = await Effect.runPromise(runtime.execute(`return search({})`)) + const browse = await Effect.runPromise(runtime.execute(`return await tools.$codemode.search({})`)) expect(browse.ok).toBe(true) if (browse.ok) { const value = browse.value as { items: Array<{ path: string }>; remaining: number; next: unknown } @@ -1000,7 +958,9 @@ describe("CodeMode public contract", () => { expect(value.next).toBeNull() } - const middle = await Effect.runPromise(runtime.execute(`return search({ limit: 1, offset: 1 })`)) + const middle = await Effect.runPromise( + runtime.execute(`return await tools.$codemode.search({ limit: 1, offset: 1 })`), + ) expect(middle.ok).toBe(true) if (middle.ok) { expect(middle.value).toMatchObject({ @@ -1010,7 +970,9 @@ describe("CodeMode public contract", () => { }) } - const exhausted = await Effect.runPromise(runtime.execute(`return search({ limit: 1, offset: 3 })`)) + const exhausted = await Effect.runPromise( + runtime.execute(`return await tools.$codemode.search({ limit: 1, offset: 3 })`), + ) expect(exhausted.ok).toBe(true) if (exhausted.ok) expect(exhausted.value).toStrictEqual({ items: [], remaining: 0, next: null }) }) @@ -1041,14 +1003,16 @@ describe("CodeMode public contract", () => { }) const instructions = runtime.instructions() - expect(instructions).toContain("Available tools (PARTIAL - 2 of 3 shown; find the rest with search(...))") + expect(instructions).toContain( + "Available tools (PARTIAL - 2 of 3 shown; find the rest with tools.$codemode.search)", + ) expect(instructions).toContain("- alpha (2 tools, 1 shown)") expect(instructions).toContain(" - tools.alpha.cheap(input: {\n q: string,\n}): Promise // Cheap") expect(instructions).not.toContain("tools.alpha.expensive(") // Fully shown namespaces read cleanly (no "shown" annotation). expect(instructions).toContain("- beta (1 tool)") expect(instructions).toContain(" - tools.beta.cheap(input: {\n q: string,\n}): Promise // Cheap") - expect(instructions).toContain("Search returns complete callable signatures:\n- search(input: {") + expect(instructions).toMatch(/\$codemode\.search/) }) test("charges inline JSDoc against the catalog token budget", () => { @@ -1069,7 +1033,9 @@ describe("CodeMode public contract", () => { }) expect(runtime.catalog()[0]?.signature).toContain("/** A detailed identifier description.") - expect(runtime.instructions()).toContain("Available tools (PARTIAL - 0 of 1 shown; find the rest with search(...))") + expect(runtime.instructions()).toContain( + "Available tools (PARTIAL - 0 of 1 shown; find the rest with tools.$codemode.search)", + ) expect(runtime.instructions()).not.toContain("tools.records.lookup(input:") }) @@ -1115,24 +1081,6 @@ describe("CodeMode public contract", () => { expect(Schema.decodeUnknownSync(CodeMode.Result)(JSON.parse(JSON.stringify(result)))).toStrictEqual(result) }) - test("returns the final top-level expression when return is omitted", async () => { - const result = await Effect.runPromise(CodeMode.execute({ code: `1; 2` })) - - expect(result).toStrictEqual({ ok: true, value: 2, toolCalls: [] }) - }) - - test("does not implicitly return expressions nested in control flow", async () => { - const result = await Effect.runPromise(CodeMode.execute({ code: `if (true) { 2 }` })) - - expect(result).toStrictEqual({ ok: true, value: null, toolCalls: [] }) - }) - - test("returns null when the final top-level statement is not an expression", async () => { - const result = await Effect.runPromise(CodeMode.execute({ code: `1; const value = 2` })) - - expect(result).toStrictEqual({ ok: true, value: null, toolCalls: [] }) - }) - test("rejects invalid configuration and discovery limits", async () => { expect(() => CodeMode.execute({ code: "return 1", limits: { timeoutMs: 0 } })).toThrow(RangeError) expect(() => CodeMode.execute({ code: "return 1", limits: { timeoutMs: Number.POSITIVE_INFINITY } })).toThrow( @@ -1147,7 +1095,7 @@ describe("CodeMode public contract", () => { CodeMode.make({ tools, discovery: { catalogBudget: 0 }, - }).execute(`return search({ query: "order", limit: 0.5 })`), + }).execute(`return await tools.$codemode.search({ query: "order", limit: 0.5 })`), ) expect(result.ok).toBe(false) if (result.ok) return @@ -1155,7 +1103,9 @@ describe("CodeMode public contract", () => { for (const offset of [-1, 0.5, Number.MAX_SAFE_INTEGER + 1, "1"]) { const invalidOffset = await Effect.runPromise( - CodeMode.make({ tools }).execute(`return search({ query: "order", offset: ${JSON.stringify(offset)} })`), + CodeMode.make({ tools }).execute( + `return await tools.$codemode.search({ query: "order", offset: ${JSON.stringify(offset)} })`, + ), ) expect(invalidOffset.ok).toBe(false) if (!invalidOffset.ok) expect(invalidOffset.error.kind).toBe("InvalidToolInput") @@ -1206,4 +1156,8 @@ describe("CodeMode public contract", () => { } expect(elapsedMs).toBeLessThan(3_000) }) + + test("reserves the discovery namespace", () => { + expect(() => CodeMode.make({ tools: { $codemode: { lookup } } })).toThrow(/reserved for CodeMode discovery tools/) + }) }) diff --git a/packages/codemode/test/enumeration.test.ts b/packages/codemode/test/enumeration.test.ts index ca71226a57..0de3dc3ea0 100644 --- a/packages/codemode/test/enumeration.test.ts +++ b/packages/codemode/test/enumeration.test.ts @@ -41,7 +41,7 @@ describe("Object.keys over tool references", () => { const namespaces = Object.keys(tools) return { namespaces, count: namespaces.length } `), - ).toEqual({ namespaces: ["github", "memory", "playwright"], count: 3 }) + ).toEqual({ namespaces: ["github", "memory", "playwright", "$codemode"], count: 4 }) }) test("enumerates tool names at a nested namespace", async () => { @@ -52,8 +52,8 @@ describe("Object.keys over tool references", () => { expect(await value(`return Object.keys(tools.github.list_issues)`)).toEqual([]) }) - test("search is a global built-in function", async () => { - expect(await value(`return typeof search`)).toBe("function") + test("the internal discovery namespace enumerates its callable surface", async () => { + expect(await value(`return Object.keys(tools.$codemode)`)).toEqual(["search"]) }) test("an unknown namespace is an UnknownTool error pointing at the discovery idioms", async () => { @@ -68,7 +68,7 @@ describe("Object.keys over tool references", () => { const failure = await error(`return Object.${method}(tools)`) expect(failure.kind).toBe("InvalidDataValue") expect(failure.message).toContain( - `Object.${method}(...) cannot read tool references: they are not plain data. Use Object.keys(tools) for names, or search({ query }) for signatures.`, + `Object.${method}(...) cannot read tool references: they are not plain data. Use Object.keys(tools) for names, or tools.$codemode.search({ query }) for signatures.`, ) } const nested = await error(`return Object.entries(tools.github)`) @@ -146,7 +146,7 @@ describe("for...in", () => { } return names `), - ).toEqual(["github.list_issues", "github.get_issue", "memory.search", "playwright.navigate"]) + ).toEqual(["github.list_issues", "github.get_issue", "memory.search", "playwright.navigate", "$codemode.search"]) }) test("unsupported values fail with a hint at for...of and Object.keys", async () => { diff --git a/packages/codemode/test/fixtures/openapi-happy-path.json b/packages/codemode/test/fixtures/openapi-happy-path.json index dc3cbe5f8f..8052dd7391 100644 --- a/packages/codemode/test/fixtures/openapi-happy-path.json +++ b/packages/codemode/test/fixtures/openapi-happy-path.json @@ -93,16 +93,10 @@ }, "role": { "type": "string", - "enum": [ - "admin", - "member" - ] + "enum": ["admin", "member"] } }, - "required": [ - "name", - "email" - ], + "required": ["name", "email"], "additionalProperties": false } } @@ -143,9 +137,7 @@ "type": "integer" } }, - "required": [ - "query" - ], + "required": ["query"], "additionalProperties": false } }, @@ -216,17 +208,10 @@ }, "role": { "type": "string", - "enum": [ - "admin", - "member" - ] + "enum": ["admin", "member"] } }, - "required": [ - "id", - "name", - "email" - ], + "required": ["id", "name", "email"], "additionalProperties": false } }, diff --git a/packages/codemode/test/fixtures/opencode-v2-openapi.json b/packages/codemode/test/fixtures/opencode-v2-openapi.json index 543a418173..c78194e669 100644 --- a/packages/codemode/test/fixtures/opencode-v2-openapi.json +++ b/packages/codemode/test/fixtures/opencode-v2-openapi.json @@ -8,9 +8,7 @@ "paths": { "/api/health": { "get": { - "tags": [ - "health" - ], + "tags": ["server.health"], "operationId": "v2.health.get", "parameters": [], "security": [], @@ -24,27 +22,10 @@ "properties": { "healthy": { "type": "boolean", - "enum": [ - true - ] - }, - "version": { - "type": "string" - }, - "pid": { - "type": "integer", - "allOf": [ - { - "exclusiveMinimum": 0 - } - ] + "enum": [true] } }, - "required": [ - "healthy", - "version", - "pid" - ], + "required": ["healthy"], "additionalProperties": false } } @@ -75,67 +56,9 @@ "summary": "Check server health" } }, - "/api/server": { - "get": { - "tags": [ - "server" - ], - "operationId": "v2.server.get", - "parameters": [], - "security": [], - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "urls": { - "type": "array", - "items": { - "type": "string" - } - } - }, - "required": [ - "urls" - ], - "additionalProperties": false - } - } - } - }, - "400": { - "description": "InvalidRequestError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequestError" - } - } - } - }, - "401": { - "description": "UnauthorizedError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnauthorizedError" - } - } - } - } - }, - "description": "Return the URLs that can be used to connect to this server.", - "summary": "Get server information" - } - }, "/api/location": { "get": { - "tags": [ - "location" - ], + "tags": ["server.location"], "operationId": "v2.location.get", "parameters": [ { @@ -218,9 +141,7 @@ }, "/api/agent": { "get": { - "tags": [ - "agent" - ], + "tags": ["server.agent"], "operationId": "v2.agent.list", "parameters": [ { @@ -279,14 +200,11 @@ "data": { "type": "array", "items": { - "$ref": "#/components/schemas/Agent.Info" + "$ref": "#/components/schemas/AgentV2.Info" } } }, - "required": [ - "location", - "data" - ], + "required": ["location", "data"], "additionalProperties": false } } @@ -319,9 +237,7 @@ }, "/api/plugin": { "get": { - "tags": [ - "plugin" - ], + "tags": ["plugins"], "operationId": "v2.plugin.list", "parameters": [ { @@ -384,10 +300,7 @@ } } }, - "required": [ - "location", - "data" - ], + "required": ["location", "data"], "additionalProperties": false } } @@ -420,9 +333,7 @@ }, "/api/session": { "get": { - "tags": [ - "session" - ], + "tags": ["sessions"], "operationId": "v2.session.list", "parameters": [ { @@ -468,10 +379,7 @@ "anyOf": [ { "type": "string", - "enum": [ - "asc", - "desc" - ] + "enum": ["asc", "desc"] }, { "type": "null" @@ -513,9 +421,7 @@ }, { "type": "string", - "enum": [ - "null" - ] + "enum": ["null"] } ], "description": "Filter by parent session. Use null to return only root sessions." @@ -636,9 +542,7 @@ "summary": "List sessions" }, "post": { - "tags": [ - "session" - ], + "tags": ["sessions"], "operationId": "v2.session.create", "parameters": [], "security": [], @@ -651,12 +555,10 @@ "type": "object", "properties": { "data": { - "$ref": "#/components/schemas/Session.Info" + "$ref": "#/components/schemas/SessionV2.Info" } }, - "required": [ - "data" - ], + "required": ["data"], "additionalProperties": false } } @@ -747,9 +649,7 @@ }, "/api/session/active": { "get": { - "tags": [ - "session" - ], + "tags": ["sessions"], "operationId": "v2.session.active", "parameters": [], "security": [], @@ -768,11 +668,12 @@ "$ref": "#/components/schemas/SessionActive" } } + }, + "watermarks": { + "$ref": "#/components/schemas/SessionWatermarks" } }, - "required": [ - "data" - ], + "required": ["data", "watermarks"], "additionalProperties": false } } @@ -799,15 +700,13 @@ } } }, - "description": "Retrieve foreground Session drains currently owned by this OpenCode process. Sessions absent from the result are inactive.", + "description": "Retrieve foreground Session drains currently owned by this OpenCode process. Sessions absent from the result are inactive. Watermarks are the durable log positions read alongside the activity snapshot; activity itself is process state, so the pairing is advisory rather than transactional.", "summary": "List active sessions" } }, "/api/session/{sessionID}": { "get": { - "tags": [ - "session" - ], + "tags": ["sessions"], "operationId": "v2.session.get", "parameters": [ { @@ -834,12 +733,10 @@ "type": "object", "properties": { "data": { - "$ref": "#/components/schemas/Session.Info" + "$ref": "#/components/schemas/SessionV2.Info" } }, - "required": [ - "data" - ], + "required": ["data"], "additionalProperties": false } } @@ -885,79 +782,11 @@ }, "description": "Retrieve a session by ID.", "summary": "Get session" - }, - "delete": { - "tags": [ - "session" - ], - "operationId": "v2.session.remove", - "parameters": [ - { - "name": "sessionID", - "in": "path", - "schema": { - "type": "string", - "allOf": [ - { - "pattern": "^ses" - } - ] - }, - "required": true - } - ], - "security": [], - "responses": { - "204": { - "description": "" - }, - "400": { - "description": "InvalidRequestError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequestError" - } - } - } - }, - "401": { - "description": "UnauthorizedError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnauthorizedError" - } - } - } - }, - "404": { - "description": "SessionNotFoundError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/SessionNotFoundError" - }, - { - "$ref": "#/components/schemas/SessionNotFoundError" - } - ] - } - } - } - } - }, - "description": "Delete a session and its child sessions.", - "summary": "Delete session" } }, "/api/session/{sessionID}/fork": { "post": { - "tags": [ - "session" - ], + "tags": ["sessions"], "operationId": "v2.session.fork", "parameters": [ { @@ -984,12 +813,10 @@ "type": "object", "properties": { "data": { - "$ref": "#/components/schemas/Session.Info" + "$ref": "#/components/schemas/SessionV2.Info" } }, - "required": [ - "data" - ], + "required": ["data"], "additionalProperties": false } } @@ -1070,9 +897,7 @@ }, "/api/session/{sessionID}/agent": { "post": { - "tags": [ - "session" - ], + "tags": ["sessions"], "operationId": "v2.session.switchAgent", "parameters": [ { @@ -1144,9 +969,7 @@ "type": "string" } }, - "required": [ - "agent" - ], + "required": ["agent"], "additionalProperties": false } } @@ -1157,9 +980,7 @@ }, "/api/session/{sessionID}/model": { "post": { - "tags": [ - "session" - ], + "tags": ["sessions"], "operationId": "v2.session.switchModel", "parameters": [ { @@ -1231,9 +1052,7 @@ "$ref": "#/components/schemas/Model.Ref" } }, - "required": [ - "model" - ], + "required": ["model"], "additionalProperties": false } } @@ -1244,9 +1063,7 @@ }, "/api/session/{sessionID}/rename": { "post": { - "tags": [ - "session" - ], + "tags": ["sessions"], "operationId": "v2.session.rename", "parameters": [ { @@ -1318,122 +1135,7 @@ "type": "string" } }, - "required": [ - "title" - ], - "additionalProperties": false - } - } - }, - "required": true - } - } - }, - "/api/session/{sessionID}/move": { - "post": { - "tags": [ - "session" - ], - "operationId": "v2.session.move", - "parameters": [ - { - "name": "sessionID", - "in": "path", - "schema": { - "type": "string", - "allOf": [ - { - "pattern": "^ses" - } - ] - }, - "required": true - } - ], - "security": [], - "responses": { - "204": { - "description": "" - }, - "400": { - "description": "InvalidRequestError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/InvalidRequestError1" - }, - { - "$ref": "#/components/schemas/InvalidRequestError" - } - ] - } - } - } - }, - "401": { - "description": "UnauthorizedError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnauthorizedError" - } - } - } - }, - "404": { - "description": "SessionNotFoundError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/SessionNotFoundError" - }, - { - "$ref": "#/components/schemas/SessionNotFoundError" - } - ] - } - } - } - } - }, - "description": "Move a session to another project directory, optionally transferring local changes.", - "summary": "Move session", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "destination": { - "type": "object", - "properties": { - "directory": { - "type": "string" - } - }, - "required": [ - "directory" - ], - "additionalProperties": false - }, - "moveChanges": { - "anyOf": [ - { - "type": "boolean" - }, - { - "type": "null" - } - ] - } - }, - "required": [ - "destination" - ], + "required": ["title"], "additionalProperties": false } } @@ -1444,9 +1146,7 @@ }, "/api/session/{sessionID}/prompt": { "post": { - "tags": [ - "session" - ], + "tags": ["sessions"], "operationId": "v2.session.prompt", "parameters": [ { @@ -1476,9 +1176,7 @@ "$ref": "#/components/schemas/SessionInput.Admitted" } }, - "required": [ - "data" - ], + "required": ["data"], "additionalProperties": false } } @@ -1489,14 +1187,7 @@ "content": { "application/json": { "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/InvalidRequestError1" - }, - { - "$ref": "#/components/schemas/InvalidRequestError" - } - ] + "$ref": "#/components/schemas/InvalidRequestError" } } } @@ -1569,10 +1260,7 @@ "anyOf": [ { "type": "string", - "enum": [ - "steer", - "queue" - ] + "enum": ["steer", "queue"] }, { "type": "null" @@ -1590,9 +1278,7 @@ ] } }, - "required": [ - "prompt" - ], + "required": ["prompt"], "additionalProperties": false } } @@ -1603,9 +1289,7 @@ }, "/api/session/{sessionID}/command": { "post": { - "tags": [ - "session" - ], + "tags": ["sessions"], "operationId": "v2.session.command", "parameters": [ { @@ -1635,9 +1319,7 @@ "$ref": "#/components/schemas/SessionInput.Admitted" } }, - "required": [ - "data" - ], + "required": ["data"], "additionalProperties": false } } @@ -1648,14 +1330,7 @@ "content": { "application/json": { "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/InvalidRequestError1" - }, - { - "$ref": "#/components/schemas/InvalidRequestError" - } - ] + "$ref": "#/components/schemas/InvalidRequestError" } } } @@ -1783,10 +1458,7 @@ "anyOf": [ { "type": "string", - "enum": [ - "steer", - "queue" - ] + "enum": ["steer", "queue"] }, { "type": "null" @@ -1804,9 +1476,7 @@ ] } }, - "required": [ - "command" - ], + "required": ["command"], "additionalProperties": false } } @@ -1817,9 +1487,7 @@ }, "/api/session/{sessionID}/skill": { "post": { - "tags": [ - "session" - ], + "tags": ["sessions"], "operationId": "v2.session.skill", "parameters": [ { @@ -1919,9 +1587,7 @@ ] } }, - "required": [ - "skill" - ], + "required": ["skill"], "additionalProperties": false } } @@ -1932,9 +1598,7 @@ }, "/api/session/{sessionID}/synthetic": { "post": { - "tags": [ - "session" - ], + "tags": ["sessions"], "operationId": "v2.session.synthetic", "parameters": [ { @@ -2017,21 +1681,9 @@ }, "metadata": { "type": "object" - }, - "resume": { - "anyOf": [ - { - "type": "boolean" - }, - { - "type": "null" - } - ] } }, - "required": [ - "text" - ], + "required": ["text"], "additionalProperties": false } } @@ -2040,12 +1692,10 @@ } } }, - "/api/session/{sessionID}/shell": { + "/api/session/{sessionID}/compact": { "post": { - "tags": [ - "session" - ], - "operationId": "v2.session.shell", + "tags": ["sessions"], + "operationId": "v2.session.compact", "parameters": [ { "name": "sessionID", @@ -2086,124 +1736,6 @@ } } }, - "404": { - "description": "SessionNotFoundError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/SessionNotFoundError" - }, - { - "$ref": "#/components/schemas/SessionNotFoundError" - } - ] - } - } - } - } - }, - "description": "Execute one shell command in the session's working directory. Emits a shell.started event before execution and a shell.ended event with the merged output after.", - "summary": "Run shell command", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "id": { - "anyOf": [ - { - "type": "string", - "allOf": [ - { - "pattern": "^evt_" - } - ] - }, - { - "type": "null" - } - ] - }, - "command": { - "type": "string" - } - }, - "required": [ - "command" - ], - "additionalProperties": false - } - } - }, - "required": true - } - } - }, - "/api/session/{sessionID}/compact": { - "post": { - "tags": [ - "session" - ], - "operationId": "v2.session.compact", - "parameters": [ - { - "name": "sessionID", - "in": "path", - "schema": { - "type": "string", - "allOf": [ - { - "pattern": "^ses" - } - ] - }, - "required": true - } - ], - "security": [], - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "data": { - "$ref": "#/components/schemas/SessionInput.Compaction" - } - }, - "required": [ - "data" - ], - "additionalProperties": false - } - } - } - }, - "400": { - "description": "InvalidRequestError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequestError" - } - } - } - }, - "401": { - "description": "UnauthorizedError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnauthorizedError" - } - } - } - }, "404": { "description": "SessionNotFoundError", "content": { @@ -2222,53 +1754,43 @@ } }, "409": { - "description": "ConflictError", + "description": "SessionBusyError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ConflictError" + "$ref": "#/components/schemas/SessionBusyError" + } + } + } + }, + "500": { + "description": "UnknownError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnknownError" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableError" } } } } }, - "description": "Queue a durable session compaction request.", - "summary": "Compact session", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "id": { - "anyOf": [ - { - "type": "string", - "allOf": [ - { - "pattern": "^msg_" - } - ] - }, - { - "type": "null" - } - ] - } - }, - "additionalProperties": false - } - } - }, - "required": true - } + "description": "Compact a session conversation.", + "summary": "Compact session" } }, "/api/session/{sessionID}/wait": { "post": { - "tags": [ - "session" - ], + "tags": ["sessions"], "operationId": "v2.session.wait", "parameters": [ { @@ -2344,9 +1866,7 @@ }, "/api/session/{sessionID}/revert/stage": { "post": { - "tags": [ - "session" - ], + "tags": ["sessions"], "operationId": "v2.session.revert.stage", "parameters": [ { @@ -2373,12 +1893,10 @@ "type": "object", "properties": { "data": { - "$ref": "#/components/schemas/Session.Revert" + "$ref": "#/components/schemas/Revert.State" } }, - "required": [ - "data" - ], + "required": ["data"], "additionalProperties": false } } @@ -2472,9 +1990,7 @@ ] } }, - "required": [ - "messageID" - ], + "required": ["messageID"], "additionalProperties": false } } @@ -2485,9 +2001,7 @@ }, "/api/session/{sessionID}/revert/clear": { "post": { - "tags": [ - "session" - ], + "tags": ["sessions"], "operationId": "v2.session.revert.clear", "parameters": [ { @@ -2572,9 +2086,7 @@ }, "/api/session/{sessionID}/revert/commit": { "post": { - "tags": [ - "session" - ], + "tags": ["sessions"], "operationId": "v2.session.revert.commit", "parameters": [ { @@ -2649,9 +2161,7 @@ }, "/api/session/{sessionID}/context": { "get": { - "tags": [ - "session" - ], + "tags": ["sessions"], "operationId": "v2.session.context", "parameters": [ { @@ -2680,13 +2190,11 @@ "data": { "type": "array", "items": { - "$ref": "#/components/schemas/Session.Message.Info" + "$ref": "#/components/schemas/Session.Message" } } }, - "required": [ - "data" - ], + "required": ["data"], "additionalProperties": false } } @@ -2744,12 +2252,10 @@ "summary": "Get session context" } }, - "/api/session/{sessionID}/instructions/entries": { + "/api/session/{sessionID}/context-entry": { "get": { - "tags": [ - "session" - ], - "operationId": "v2.session.instructions.entry.list", + "tags": ["sessions"], + "operationId": "v2.session.context.entry.list", "parameters": [ { "name": "sessionID", @@ -2777,13 +2283,11 @@ "data": { "type": "array", "items": { - "$ref": "#/components/schemas/InstructionEntry.Info" + "$ref": "#/components/schemas/SessionContextEntry.Info" } } }, - "required": [ - "data" - ], + "required": ["data"], "additionalProperties": false } } @@ -2827,16 +2331,14 @@ } } }, - "description": "List API-managed instruction entries attached to the session.", - "summary": "List instruction entries" + "description": "List API-managed context entries attached to the session's system context.", + "summary": "List context entries" } }, - "/api/session/{sessionID}/instructions/entries/{key}": { + "/api/session/{sessionID}/context-entry/{key}": { "put": { - "tags": [ - "session" - ], - "operationId": "v2.session.instructions.entry.put", + "tags": ["sessions"], + "operationId": "v2.session.context.entry.put", "parameters": [ { "name": "sessionID", @@ -2855,7 +2357,7 @@ "name": "key", "in": "path", "schema": { - "$ref": "#/components/schemas/InstructionEntry.Key" + "$ref": "#/components/schemas/SessionContextEntry.Key" }, "required": true } @@ -2903,8 +2405,8 @@ } } }, - "description": "Attach or replace one durable instruction entry. Changes announce as updates at the next step boundary.", - "summary": "Put instruction entry", + "description": "Attach or replace one durable context entry. The value is rendered into the session's system context; changes announce as updates at the next turn boundary.", + "summary": "Put context entry", "requestBody": { "content": { "application/json": { @@ -2913,9 +2415,7 @@ "properties": { "value": {} }, - "required": [ - "value" - ], + "required": ["value"], "additionalProperties": false } } @@ -2924,10 +2424,8 @@ } }, "delete": { - "tags": [ - "session" - ], - "operationId": "v2.session.instructions.entry.remove", + "tags": ["sessions"], + "operationId": "v2.session.context.entry.remove", "parameters": [ { "name": "sessionID", @@ -2946,7 +2444,7 @@ "name": "key", "in": "path", "schema": { - "$ref": "#/components/schemas/InstructionEntry.Key" + "$ref": "#/components/schemas/SessionContextEntry.Key" }, "required": true } @@ -2994,15 +2492,13 @@ } } }, - "description": "Remove one instruction entry; the removal is announced to the model at the next step boundary.", - "summary": "Remove instruction entry" + "description": "Remove one context entry; the removal is announced to the model at the next turn boundary.", + "summary": "Remove context entry" } }, - "/api/experimental/session/{sessionID}/log": { + "/api/session/{sessionID}/log": { "get": { - "tags": [ - "session" - ], + "tags": ["sessions"], "operationId": "v2.session.log", "parameters": [ { @@ -3040,10 +2536,7 @@ "anyOf": [ { "type": "string", - "enum": [ - "true", - "false" - ] + "enum": ["true", "false"] }, { "type": "null" @@ -3079,11 +2572,7 @@ "$ref": "#/components/schemas/SessionLogItemStream" } }, - "required": [ - "id", - "event", - "data" - ], + "required": ["id", "event", "data"], "additionalProperties": false }, "x-effect-stream": { @@ -3097,18 +2586,13 @@ "properties": { "_tag": { "type": "string", - "enum": [ - "Fail" - ] + "enum": ["Fail"] }, "error": { "not": {} } }, - "required": [ - "_tag", - "error" - ], + "required": ["_tag", "error"], "additionalProperties": false }, { @@ -3116,16 +2600,11 @@ "properties": { "_tag": { "type": "string", - "enum": [ - "Die" - ] + "enum": ["Die"] }, "defect": {} }, - "required": [ - "_tag", - "defect" - ], + "required": ["_tag", "defect"], "additionalProperties": false }, { @@ -3133,9 +2612,7 @@ "properties": { "_tag": { "type": "string", - "enum": [ - "Interrupt" - ] + "enum": ["Interrupt"] }, "fiberId": { "anyOf": [ @@ -3148,10 +2625,7 @@ ] } }, - "required": [ - "_tag", - "fiberId" - ], + "required": ["_tag", "fiberId"], "additionalProperties": false } ] @@ -3203,15 +2677,13 @@ } } }, - "description": "Experimental durable session event log. Reads events after an exclusive aggregate sequence and continues with live events when follow=true.", + "description": "Durable, ordered, gap-free read of public session events after an exclusive aggregate sequence. Emits a synced marker once replay reaches the captured watermark, then completes; with follow=true it continues with live events instead. The only event API that promises reliability: attach after a snapshot watermark to compose fetch and stream without a race window.", "summary": "Read the session log" } }, "/api/session/{sessionID}/interrupt": { "post": { - "tags": [ - "session" - ], + "tags": ["sessions"], "operationId": "v2.session.interrupt", "parameters": [ { @@ -3277,9 +2749,7 @@ }, "/api/session/{sessionID}/background": { "post": { - "tags": [ - "session" - ], + "tags": ["sessions"], "operationId": "v2.session.background", "parameters": [ { @@ -3345,9 +2815,7 @@ }, "/api/session/{sessionID}/message/{messageID}": { "get": { - "tags": [ - "session" - ], + "tags": ["sessions"], "operationId": "v2.session.message", "parameters": [ { @@ -3387,12 +2855,10 @@ "type": "object", "properties": { "data": { - "$ref": "#/components/schemas/Session.Message.Info" + "$ref": "#/components/schemas/Session.Message" } }, - "required": [ - "data" - ], + "required": ["data"], "additionalProperties": false } } @@ -3445,10 +2911,8 @@ }, "/api/session/{sessionID}/message": { "get": { - "tags": [ - "session" - ], - "operationId": "v2.message.list", + "tags": ["messages"], + "operationId": "v2.session.messages", "parameters": [ { "name": "sessionID", @@ -3486,10 +2950,7 @@ "anyOf": [ { "type": "string", - "enum": [ - "asc", - "desc" - ] + "enum": ["asc", "desc"] }, { "type": "null" @@ -3589,9 +3050,7 @@ }, "/api/model": { "get": { - "tags": [ - "model" - ], + "tags": ["models"], "operationId": "v2.model.list", "parameters": [ { @@ -3650,14 +3109,11 @@ "data": { "type": "array", "items": { - "$ref": "#/components/schemas/Model.Info" + "$ref": "#/components/schemas/ModelV2.Info" } } }, - "required": [ - "location", - "data" - ], + "required": ["location", "data"], "additionalProperties": false } } @@ -3700,9 +3156,7 @@ }, "/api/model/default": { "get": { - "tags": [ - "model" - ], + "tags": ["models"], "operationId": "v2.model.default", "parameters": [ { @@ -3761,7 +3215,7 @@ "data": { "anyOf": [ { - "$ref": "#/components/schemas/Model.Info" + "$ref": "#/components/schemas/ModelV2.Info" }, { "type": "null" @@ -3769,10 +3223,7 @@ ] } }, - "required": [ - "location", - "data" - ], + "required": ["location", "data"], "additionalProperties": false } } @@ -3815,9 +3266,7 @@ }, "/api/generate": { "post": { - "tags": [ - "generate" - ], + "tags": ["generate"], "operationId": "v2.generate.text", "parameters": [ { @@ -3933,9 +3382,7 @@ ] } }, - "required": [ - "prompt" - ], + "required": ["prompt"], "additionalProperties": false } } @@ -3946,9 +3393,7 @@ }, "/api/provider": { "get": { - "tags": [ - "provider" - ], + "tags": ["providers"], "operationId": "v2.provider.list", "parameters": [ { @@ -4011,10 +3456,7 @@ } } }, - "required": [ - "location", - "data" - ], + "required": ["location", "data"], "additionalProperties": false } } @@ -4057,9 +3499,7 @@ }, "/api/provider/{providerID}": { "get": { - "tags": [ - "provider" - ], + "tags": ["providers"], "operationId": "v2.provider.get", "parameters": [ { @@ -4127,10 +3567,7 @@ "$ref": "#/components/schemas/ProviderV2.Info" } }, - "required": [ - "location", - "data" - ], + "required": ["location", "data"], "additionalProperties": false } } @@ -4183,9 +3620,7 @@ }, "/api/integration": { "get": { - "tags": [ - "integration" - ], + "tags": ["integrations"], "operationId": "v2.integration.list", "parameters": [ { @@ -4248,10 +3683,7 @@ } } }, - "required": [ - "location", - "data" - ], + "required": ["location", "data"], "additionalProperties": false } } @@ -4284,9 +3716,7 @@ }, "/api/integration/{integrationID}": { "get": { - "tags": [ - "integration" - ], + "tags": ["integrations"], "operationId": "v2.integration.get", "parameters": [ { @@ -4361,10 +3791,7 @@ ] } }, - "required": [ - "location", - "data" - ], + "required": ["location", "data"], "additionalProperties": false } } @@ -4397,9 +3824,7 @@ }, "/api/integration/{integrationID}/connect/key": { "post": { - "tags": [ - "integration" - ], + "tags": ["integrations"], "operationId": "v2.integration.connect.key", "parameters": [ { @@ -4506,9 +3931,7 @@ ] } }, - "required": [ - "key" - ], + "required": ["key"], "additionalProperties": false } } @@ -4519,9 +3942,7 @@ }, "/api/integration/{integrationID}/connect/oauth": { "post": { - "tags": [ - "integration" - ], + "tags": ["integrations"], "operationId": "v2.integration.connect.oauth", "parameters": [ { @@ -4589,10 +4010,7 @@ "$ref": "#/components/schemas/Integration.Attempt" } }, - "required": [ - "location", - "data" - ], + "required": ["location", "data"], "additionalProperties": false } } @@ -4654,10 +4072,7 @@ ] } }, - "required": [ - "methodID", - "inputs" - ], + "required": ["methodID", "inputs"], "additionalProperties": false } } @@ -4668,9 +4083,7 @@ }, "/api/integration/attempt/{attemptID}": { "get": { - "tags": [ - "integration" - ], + "tags": ["integrations"], "operationId": "v2.integration.attempt.status", "parameters": [ { @@ -4738,10 +4151,7 @@ "$ref": "#/components/schemas/Integration.AttemptStatus" } }, - "required": [ - "location", - "data" - ], + "required": ["location", "data"], "additionalProperties": false } } @@ -4772,9 +4182,7 @@ "summary": "Get OAuth attempt status" }, "delete": { - "tags": [ - "integration" - ], + "tags": ["integrations"], "operationId": "v2.integration.attempt.cancel", "parameters": [ { @@ -4858,9 +4266,7 @@ }, "/api/integration/attempt/{attemptID}/complete": { "post": { - "tags": [ - "integration" - ], + "tags": ["integrations"], "operationId": "v2.integration.attempt.complete", "parameters": [ { @@ -4974,9 +4380,7 @@ }, "/api/mcp": { "get": { - "tags": [ - "mcp" - ], + "tags": ["mcp"], "operationId": "v2.mcp.list", "parameters": [ { @@ -5039,10 +4443,7 @@ } } }, - "required": [ - "location", - "data" - ], + "required": ["location", "data"], "additionalProperties": false } } @@ -5073,109 +4474,9 @@ "summary": "List MCP servers" } }, - "/api/mcp/resource": { - "get": { - "tags": [ - "mcp" - ], - "operationId": "v2.mcp.resource.catalog", - "parameters": [ - { - "name": "location", - "in": "query", - "schema": { - "anyOf": [ - { - "type": "object", - "properties": { - "directory": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ] - }, - "workspace": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ] - } - }, - "additionalProperties": false - }, - { - "type": "null" - } - ] - }, - "required": false, - "style": "deepObject", - "explode": true - } - ], - "security": [], - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "location": { - "$ref": "#/components/schemas/Location.Info" - }, - "data": { - "$ref": "#/components/schemas/Mcp.ResourceCatalog" - } - }, - "required": [ - "location", - "data" - ], - "additionalProperties": false - } - } - } - }, - "400": { - "description": "InvalidRequestError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequestError" - } - } - } - }, - "401": { - "description": "UnauthorizedError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnauthorizedError" - } - } - } - } - }, - "description": "Retrieve resources and resource templates from connected MCP servers.", - "summary": "List MCP resources" - } - }, "/api/credential/{credentialID}": { "patch": { - "tags": [ - "credential" - ], + "tags": ["server.credential"], "operationId": "v2.credential.update", "parameters": [ { @@ -5265,9 +4566,7 @@ "type": "string" } }, - "required": [ - "label" - ], + "required": ["label"], "additionalProperties": false } } @@ -5276,9 +4575,7 @@ } }, "delete": { - "tags": [ - "credential" - ], + "tags": ["server.credential"], "operationId": "v2.credential.remove", "parameters": [ { @@ -5360,58 +4657,9 @@ "summary": "Remove credential" } }, - "/api/project": { - "get": { - "tags": [ - "project" - ], - "operationId": "v2.project.list", - "parameters": [], - "security": [], - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Project" - } - } - } - } - }, - "400": { - "description": "InvalidRequestError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequestError" - } - } - } - }, - "401": { - "description": "UnauthorizedError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnauthorizedError" - } - } - } - } - }, - "description": "List known projects.", - "summary": "List projects" - } - }, "/api/project/current": { "get": { - "tags": [ - "project" - ], + "tags": ["projects"], "operationId": "v2.project.current", "parameters": [ { @@ -5494,9 +4742,7 @@ }, "/api/project/{projectID}/directories": { "get": { - "tags": [ - "project" - ], + "tags": ["projects"], "operationId": "v2.project.directories", "parameters": [ { @@ -5587,9 +4833,7 @@ }, "/api/form/request": { "get": { - "tags": [ - "form" - ], + "tags": ["forms"], "operationId": "v2.form.request.list", "parameters": [ { @@ -5659,10 +4903,7 @@ } } }, - "required": [ - "location", - "data" - ], + "required": ["location", "data"], "additionalProperties": false } } @@ -5695,9 +4936,7 @@ }, "/api/session/{sessionID}/form": { "get": { - "tags": [ - "form" - ], + "tags": ["forms"], "operationId": "v2.session.form.list", "parameters": [ { @@ -5732,9 +4971,7 @@ } } }, - "required": [ - "data" - ], + "required": ["data"], "additionalProperties": false } } @@ -5782,9 +5019,7 @@ "summary": "List session forms" }, "post": { - "tags": [ - "form" - ], + "tags": ["forms"], "operationId": "v2.session.form.create", "parameters": [ { @@ -5816,9 +5051,7 @@ ] } }, - "required": [ - "data" - ], + "required": ["data"], "additionalProperties": false } } @@ -5895,9 +5128,7 @@ }, "/api/session/{sessionID}/form/{formID}": { "get": { - "tags": [ - "form" - ], + "tags": ["forms"], "operationId": "v2.session.form.get", "parameters": [ { @@ -5942,9 +5173,7 @@ ] } }, - "required": [ - "data" - ], + "required": ["data"], "additionalProperties": false } } @@ -5997,9 +5226,7 @@ }, "/api/session/{sessionID}/form/{formID}/state": { "get": { - "tags": [ - "form" - ], + "tags": ["forms"], "operationId": "v2.session.form.state", "parameters": [ { @@ -6037,9 +5264,7 @@ "$ref": "#/components/schemas/Form.State" } }, - "required": [ - "data" - ], + "required": ["data"], "additionalProperties": false } } @@ -6092,9 +5317,7 @@ }, "/api/session/{sessionID}/form/{formID}/reply": { "post": { - "tags": [ - "form" - ], + "tags": ["forms"], "operationId": "v2.session.form.reply", "parameters": [ { @@ -6198,9 +5421,7 @@ }, "/api/session/{sessionID}/form/{formID}/cancel": { "post": { - "tags": [ - "form" - ], + "tags": ["forms"], "operationId": "v2.session.form.cancel", "parameters": [ { @@ -6287,9 +5508,7 @@ }, "/api/permission/request": { "get": { - "tags": [ - "permission" - ], + "tags": ["permissions"], "operationId": "v2.permission.request.list", "parameters": [ { @@ -6352,10 +5571,7 @@ } } }, - "required": [ - "location", - "data" - ], + "required": ["location", "data"], "additionalProperties": false } } @@ -6388,9 +5604,7 @@ }, "/api/permission/saved": { "get": { - "tags": [ - "permission" - ], + "tags": ["permissions"], "operationId": "v2.permission.saved.list", "parameters": [ { @@ -6425,9 +5639,7 @@ } } }, - "required": [ - "data" - ], + "required": ["data"], "additionalProperties": false } } @@ -6460,9 +5672,7 @@ }, "/api/permission/saved/{id}": { "delete": { - "tags": [ - "permission" - ], + "tags": ["permissions"], "operationId": "v2.permission.saved.remove", "parameters": [ { @@ -6506,9 +5716,7 @@ }, "/api/session/{sessionID}/permission": { "post": { - "tags": [ - "permission" - ], + "tags": ["permissions"], "operationId": "v2.session.permission.create", "parameters": [ { @@ -6549,16 +5757,11 @@ "$ref": "#/components/schemas/PermissionV2.Effect" } }, - "required": [ - "id", - "effect" - ], + "required": ["id", "effect"], "additionalProperties": false } }, - "required": [ - "data" - ], + "required": ["data"], "additionalProperties": false } } @@ -6657,10 +5860,7 @@ ] } }, - "required": [ - "action", - "resources" - ], + "required": ["action", "resources"], "additionalProperties": false } } @@ -6669,9 +5869,7 @@ } }, "get": { - "tags": [ - "permission" - ], + "tags": ["permissions"], "operationId": "v2.session.permission.list", "parameters": [ { @@ -6704,9 +5902,7 @@ } } }, - "required": [ - "data" - ], + "required": ["data"], "additionalProperties": false } } @@ -6756,9 +5952,7 @@ }, "/api/session/{sessionID}/permission/{requestID}": { "get": { - "tags": [ - "permission" - ], + "tags": ["permissions"], "operationId": "v2.session.permission.get", "parameters": [ { @@ -6801,9 +5995,7 @@ "$ref": "#/components/schemas/PermissionV2.Request" } }, - "required": [ - "data" - ], + "required": ["data"], "additionalProperties": false } } @@ -6856,9 +6048,7 @@ }, "/api/session/{sessionID}/permission/{requestID}/reply": { "post": { - "tags": [ - "permission" - ], + "tags": ["permissions"], "operationId": "v2.session.permission.reply", "parameters": [ { @@ -6956,9 +6146,7 @@ ] } }, - "required": [ - "reply" - ], + "required": ["reply"], "additionalProperties": false } } @@ -6969,9 +6157,7 @@ }, "/api/fs/read/*": { "get": { - "tags": [ - "filesystem" - ], + "tags": ["filesystem"], "operationId": "v2.fs.read", "parameters": [ { @@ -7055,9 +6241,7 @@ }, "/api/fs/list": { "get": { - "tags": [ - "filesystem" - ], + "tags": ["filesystem"], "operationId": "v2.fs.list", "parameters": [ { @@ -7135,10 +6319,7 @@ } } }, - "required": [ - "location", - "data" - ], + "required": ["location", "data"], "additionalProperties": false } } @@ -7171,9 +6352,7 @@ }, "/api/fs/find": { "get": { - "tags": [ - "filesystem" - ], + "tags": ["filesystem"], "operationId": "v2.fs.find", "parameters": [ { @@ -7229,10 +6408,7 @@ "in": "query", "schema": { "type": "string", - "enum": [ - "file", - "directory" - ] + "enum": ["file", "directory"] }, "required": false }, @@ -7271,10 +6447,7 @@ } } }, - "required": [ - "location", - "data" - ], + "required": ["location", "data"], "additionalProperties": false } } @@ -7307,9 +6480,7 @@ }, "/api/command": { "get": { - "tags": [ - "command" - ], + "tags": ["commands"], "operationId": "v2.command.list", "parameters": [ { @@ -7368,14 +6539,11 @@ "data": { "type": "array", "items": { - "$ref": "#/components/schemas/Command.Info" + "$ref": "#/components/schemas/CommandV2.Info" } } }, - "required": [ - "location", - "data" - ], + "required": ["location", "data"], "additionalProperties": false } } @@ -7408,9 +6576,7 @@ }, "/api/skill": { "get": { - "tags": [ - "skill" - ], + "tags": ["skills"], "operationId": "v2.skill.list", "parameters": [ { @@ -7469,14 +6635,11 @@ "data": { "type": "array", "items": { - "$ref": "#/components/schemas/Skill.Info" + "$ref": "#/components/schemas/SkillV2.Info" } } }, - "required": [ - "location", - "data" - ], + "required": ["location", "data"], "additionalProperties": false } } @@ -7509,9 +6672,7 @@ }, "/api/event": { "get": { - "tags": [ - "event" - ], + "tags": ["events"], "operationId": "v2.event.subscribe", "parameters": [], "security": [], @@ -7540,11 +6701,7 @@ "$ref": "#/components/schemas/V2EventStream" } }, - "required": [ - "id", - "event", - "data" - ], + "required": ["id", "event", "data"], "additionalProperties": false }, "x-effect-stream": { @@ -7558,18 +6715,13 @@ "properties": { "_tag": { "type": "string", - "enum": [ - "Fail" - ] + "enum": ["Fail"] }, "error": { "not": {} } }, - "required": [ - "_tag", - "error" - ], + "required": ["_tag", "error"], "additionalProperties": false }, { @@ -7577,16 +6729,11 @@ "properties": { "_tag": { "type": "string", - "enum": [ - "Die" - ] + "enum": ["Die"] }, "defect": {} }, - "required": [ - "_tag", - "defect" - ], + "required": ["_tag", "defect"], "additionalProperties": false }, { @@ -7594,9 +6741,7 @@ "properties": { "_tag": { "type": "string", - "enum": [ - "Interrupt" - ] + "enum": ["Interrupt"] }, "fiberId": { "anyOf": [ @@ -7609,10 +6754,7 @@ ] } }, - "required": [ - "_tag", - "fiberId" - ], + "required": ["_tag", "fiberId"], "additionalProperties": false } ] @@ -7647,15 +6789,136 @@ } } }, - "description": "Subscribe to native event payloads for the server. Volatile by contract: a slow consumer overflows and fails the stream, and events during disconnection are missed.", + "description": "Subscribe to native event payloads for the server. Volatile by contract: a slow consumer overflows and fails the stream, and events during disconnection are missed. Consumers that need reliability should combine the changes feed with durable session log reads.", "summary": "Subscribe to events" } }, + "/api/event/changes": { + "get": { + "tags": ["events"], + "operationId": "v2.event.changes", + "parameters": [], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "text/event-stream": { + "schema": { + "type": "object", + "properties": { + "id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "event": { + "type": "string" + }, + "data": { + "$ref": "#/components/schemas/EventLog.ChangeStream" + } + }, + "required": ["id", "event", "data"], + "additionalProperties": false + }, + "x-effect-stream": { + "encoding": "sse", + "causeSchema": { + "type": "array", + "items": { + "anyOf": [ + { + "type": "object", + "properties": { + "_tag": { + "type": "string", + "enum": ["Fail"] + }, + "error": { + "not": {} + } + }, + "required": ["_tag", "error"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "_tag": { + "type": "string", + "enum": ["Die"] + }, + "defect": {} + }, + "required": ["_tag", "defect"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "_tag": { + "type": "string", + "enum": ["Interrupt"] + }, + "fiberId": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + } + }, + "required": ["_tag", "fiberId"], + "additionalProperties": false + } + ] + } + }, + "errorSchema": { + "not": {} + }, + "failureEvent": "effect/httpapi/stream/failure" + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + } + }, + "description": "Payload-free hint channel: after an event commits, a subscriber eventually receives a hint for that aggregate with seq at or beyond the event, or a sweep-required marker. Hints coalesce to the latest seq per aggregate under backpressure and the stream never fails from overflow. No consumer may derive correctness from receiving a hint; correctness always comes from durable log reads plus the consumer's own checkpoint. A sweep-required marker is emitted first on every (re)subscribe and whenever hint retention is exceeded: treat every aggregate as potentially dirty and recover via bounded sweep plus log reads.", + "summary": "Subscribe to change hints" + } + }, "/api/pty": { "get": { - "tags": [ - "pty" - ], + "tags": ["pty"], "operationId": "v2.pty.list", "parameters": [ { @@ -7718,10 +6981,7 @@ } } }, - "required": [ - "location", - "data" - ], + "required": ["location", "data"], "additionalProperties": false } } @@ -7752,9 +7012,7 @@ "summary": "List PTY sessions" }, "post": { - "tags": [ - "pty" - ], + "tags": ["pty"], "operationId": "v2.pty.create", "parameters": [ { @@ -7814,10 +7072,7 @@ "$ref": "#/components/schemas/Pty" } }, - "required": [ - "location", - "data" - ], + "required": ["location", "data"], "additionalProperties": false } } @@ -7884,9 +7139,7 @@ }, "/api/pty/{ptyID}": { "get": { - "tags": [ - "pty" - ], + "tags": ["pty"], "operationId": "v2.pty.get", "parameters": [ { @@ -7959,10 +7212,7 @@ "$ref": "#/components/schemas/Pty" } }, - "required": [ - "location", - "data" - ], + "required": ["location", "data"], "additionalProperties": false } } @@ -8003,9 +7253,7 @@ "summary": "Get PTY session" }, "put": { - "tags": [ - "pty" - ], + "tags": ["pty"], "operationId": "v2.pty.update", "parameters": [ { @@ -8078,10 +7326,7 @@ "$ref": "#/components/schemas/Pty" } }, - "required": [ - "location", - "data" - ], + "required": ["location", "data"], "additionalProperties": false } } @@ -8149,10 +7394,7 @@ ] } }, - "required": [ - "rows", - "cols" - ], + "required": ["rows", "cols"], "additionalProperties": false } }, @@ -8164,9 +7406,7 @@ } }, "delete": { - "tags": [ - "pty" - ], + "tags": ["pty"], "operationId": "v2.pty.remove", "parameters": [ { @@ -8265,10 +7505,8 @@ }, "/api/pty/{ptyID}/connect-token": { "post": { - "tags": [ - "pty" - ], - "operationId": "v2.pty.connect.token", + "tags": ["pty"], + "operationId": "v2.pty.connectToken", "parameters": [ { "name": "ptyID", @@ -8340,10 +7578,7 @@ "$ref": "#/components/schemas/PtyTicket.ConnectToken" } }, - "required": [ - "location", - "data" - ], + "required": ["location", "data"], "additionalProperties": false } } @@ -8396,10 +7631,9 @@ }, "/api/pty/{ptyID}/connect": { "get": { - "tags": [ - "pty" - ], + "tags": ["pty"], "operationId": "v2.pty.connect", + "x-websocket": true, "parameters": [ { "name": "ptyID", @@ -8497,15 +7731,12 @@ } }, "description": "Establish a WebSocket connection streaming PTY output and accepting terminal input.", - "summary": "Connect to PTY session", - "x-websocket": true + "summary": "Connect to PTY session" } }, "/api/shell": { "get": { - "tags": [ - "shell" - ], + "tags": ["shell"], "operationId": "v2.shell.list", "parameters": [ { @@ -8568,10 +7799,7 @@ } } }, - "required": [ - "location", - "data" - ], + "required": ["location", "data"], "additionalProperties": false } } @@ -8602,9 +7830,7 @@ "summary": "List running shell commands" }, "post": { - "tags": [ - "shell" - ], + "tags": ["shell"], "operationId": "v2.shell.create", "parameters": [ { @@ -8664,10 +7890,7 @@ "$ref": "#/components/schemas/Shell1" } }, - "required": [ - "location", - "data" - ], + "required": ["location", "data"], "additionalProperties": false } } @@ -8720,10 +7943,7 @@ "type": "object" } }, - "required": [ - "command", - "timeout" - ], + "required": ["command"], "additionalProperties": false } } @@ -8734,9 +7954,7 @@ }, "/api/shell/{id}": { "get": { - "tags": [ - "shell" - ], + "tags": ["shell"], "operationId": "v2.shell.get", "parameters": [ { @@ -8809,10 +8027,7 @@ "$ref": "#/components/schemas/Shell1" } }, - "required": [ - "location", - "data" - ], + "required": ["location", "data"], "additionalProperties": false } } @@ -8853,9 +8068,7 @@ "summary": "Get shell command" }, "delete": { - "tags": [ - "shell" - ], + "tags": ["shell"], "operationId": "v2.shell.remove", "parameters": [ { @@ -8952,156 +8165,9 @@ "summary": "Remove shell command" } }, - "/api/shell/{id}/timeout": { - "patch": { - "tags": [ - "shell" - ], - "operationId": "v2.shell.timeout", - "parameters": [ - { - "name": "id", - "in": "path", - "schema": { - "type": "string", - "allOf": [ - { - "pattern": "^sh_" - } - ] - }, - "required": true - }, - { - "name": "location", - "in": "query", - "schema": { - "anyOf": [ - { - "type": "object", - "properties": { - "directory": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ] - }, - "workspace": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ] - } - }, - "additionalProperties": false - }, - { - "type": "null" - } - ] - }, - "required": false, - "style": "deepObject", - "explode": true - } - ], - "security": [], - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "location": { - "$ref": "#/components/schemas/Location.Info" - }, - "data": { - "$ref": "#/components/schemas/Shell1" - } - }, - "required": [ - "location", - "data" - ], - "additionalProperties": false - } - } - } - }, - "400": { - "description": "InvalidRequestError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequestError" - } - } - } - }, - "401": { - "description": "UnauthorizedError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnauthorizedError" - } - } - } - }, - "404": { - "description": "ShellNotFoundError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ShellNotFoundError" - } - } - } - } - }, - "description": "Replace a running shell command's timeout from now, or clear it with zero.", - "summary": "Update shell timeout", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "timeout": { - "type": "integer", - "allOf": [ - { - "minimum": 0 - } - ] - } - }, - "required": [ - "timeout" - ], - "additionalProperties": false - } - } - }, - "required": true - } - } - }, "/api/shell/{id}/output": { "get": { - "tags": [ - "shell" - ], + "tags": ["shell"], "operationId": "v2.shell.output", "parameters": [ { @@ -9222,19 +8288,11 @@ "type": "boolean" } }, - "required": [ - "output", - "cursor", - "size", - "truncated" - ], + "required": ["output", "cursor", "size", "truncated"], "additionalProperties": false } }, - "required": [ - "location", - "data" - ], + "required": ["location", "data"], "additionalProperties": false } } @@ -9277,9 +8335,7 @@ }, "/api/question/request": { "get": { - "tags": [ - "question" - ], + "tags": ["session questions"], "operationId": "v2.question.request.list", "parameters": [ { @@ -9342,10 +8398,7 @@ } } }, - "required": [ - "location", - "data" - ], + "required": ["location", "data"], "additionalProperties": false } } @@ -9378,9 +8431,7 @@ }, "/api/session/{sessionID}/question": { "get": { - "tags": [ - "question" - ], + "tags": ["session questions"], "operationId": "v2.session.question.list", "parameters": [ { @@ -9413,9 +8464,7 @@ } } }, - "required": [ - "data" - ], + "required": ["data"], "additionalProperties": false } } @@ -9465,9 +8514,7 @@ }, "/api/session/{sessionID}/question/{requestID}/reply": { "post": { - "tags": [ - "question" - ], + "tags": ["session questions"], "operationId": "v2.session.question.reply", "parameters": [ { @@ -9559,9 +8606,7 @@ }, "/api/session/{sessionID}/question/{requestID}/reject": { "post": { - "tags": [ - "question" - ], + "tags": ["session questions"], "operationId": "v2.session.question.reject", "parameters": [ { @@ -9643,9 +8688,7 @@ }, "/api/reference": { "get": { - "tags": [ - "reference" - ], + "tags": ["reference"], "operationId": "v2.reference.list", "parameters": [ { @@ -9708,10 +8751,7 @@ } } }, - "required": [ - "location", - "data" - ], + "required": ["location", "data"], "additionalProperties": false } } @@ -9744,9 +8784,7 @@ }, "/experimental/project/{projectID}/copy": { "post": { - "tags": [ - "projectCopy" - ], + "tags": ["projectCopy"], "operationId": "v2.projectCopy.create", "parameters": [ { @@ -9854,10 +8892,7 @@ "type": "string" } }, - "required": [ - "strategy", - "directory" - ], + "required": ["strategy", "directory"], "additionalProperties": false } } @@ -9866,9 +8901,7 @@ } }, "delete": { - "tags": [ - "projectCopy" - ], + "tags": ["projectCopy"], "operationId": "v2.projectCopy.remove", "parameters": [ { @@ -9966,10 +8999,7 @@ "type": "boolean" } }, - "required": [ - "directory", - "force" - ], + "required": ["directory", "force"], "additionalProperties": false } } @@ -9980,9 +9010,7 @@ }, "/experimental/project/{projectID}/copy/refresh": { "post": { - "tags": [ - "projectCopy" - ], + "tags": ["projectCopy"], "operationId": "v2.projectCopy.refresh", "parameters": [ { @@ -10071,9 +9099,7 @@ }, "/api/vcs/status": { "get": { - "tags": [ - "vcs" - ], + "tags": ["vcs"], "operationId": "v2.vcs.status", "parameters": [ { @@ -10136,10 +9162,7 @@ } } }, - "required": [ - "location", - "data" - ], + "required": ["location", "data"], "additionalProperties": false } } @@ -10172,9 +9195,7 @@ }, "/api/vcs/diff": { "get": { - "tags": [ - "vcs" - ], + "tags": ["vcs"], "operationId": "v2.vcs.diff", "parameters": [ { @@ -10256,14 +9277,11 @@ "data": { "type": "array", "items": { - "$ref": "#/components/schemas/FileDiff.Info" + "$ref": "#/components/schemas/SnapshotFileDiff" } } }, - "required": [ - "location", - "data" - ], + "required": ["location", "data"], "additionalProperties": false } } @@ -10293,129 +9311,6 @@ "description": "Diff the working copy against HEAD (mode git) or the default-branch merge base (mode branch) for the requested location.", "summary": "VCS diff" } - }, - "/api/debug/location": { - "get": { - "tags": [ - "debug" - ], - "operationId": "v2.debug.location.list", - "parameters": [], - "security": [], - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Location.Ref" - } - } - } - } - }, - "400": { - "description": "InvalidRequestError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequestError" - } - } - } - }, - "401": { - "description": "UnauthorizedError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnauthorizedError" - } - } - } - } - }, - "description": "List locations currently loaded by the server.", - "summary": "List loaded locations" - }, - "delete": { - "tags": [ - "debug" - ], - "operationId": "v2.debug.location.evict", - "parameters": [ - { - "name": "location", - "in": "query", - "schema": { - "anyOf": [ - { - "type": "object", - "properties": { - "directory": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ] - }, - "workspace": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ] - } - }, - "additionalProperties": false - }, - { - "type": "null" - } - ] - }, - "required": false, - "style": "deepObject", - "explode": true - } - ], - "security": [], - "responses": { - "204": { - "description": "" - }, - "400": { - "description": "InvalidRequestError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequestError" - } - } - } - }, - "401": { - "description": "UnauthorizedError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnauthorizedError" - } - } - } - } - }, - "description": "Dispose the requested location's cached services so its next use boots them fresh.", - "summary": "Evict a loaded location" - } } }, "components": { @@ -10425,18 +9320,13 @@ "properties": { "_tag": { "type": "string", - "enum": [ - "UnauthorizedError" - ] + "enum": ["UnauthorizedError"] }, "message": { "type": "string" } }, - "required": [ - "_tag", - "message" - ], + "required": ["_tag", "message"], "additionalProperties": false }, "InvalidRequestError": { @@ -10444,9 +9334,7 @@ "properties": { "_tag": { "type": "string", - "enum": [ - "InvalidRequestError" - ] + "enum": ["InvalidRequestError"] }, "message": { "type": "string" @@ -10472,10 +9360,7 @@ ] } }, - "required": [ - "_tag", - "message" - ], + "required": ["_tag", "message"], "additionalProperties": false }, "Location.Info": { @@ -10502,17 +9387,11 @@ "type": "string" } }, - "required": [ - "id", - "directory" - ], + "required": ["id", "directory"], "additionalProperties": false } }, - "required": [ - "directory", - "project" - ], + "required": ["directory", "project"], "additionalProperties": false }, "Model.Ref": { @@ -10528,10 +9407,7 @@ "type": "string" } }, - "required": [ - "id", - "providerID" - ], + "required": ["id", "providerID"], "additionalProperties": false }, "Provider.Settings": { @@ -10553,11 +9429,7 @@ "type": "object" } }, - "required": [ - "settings", - "headers", - "body" - ], + "required": ["settings", "headers", "body"], "additionalProperties": false }, "Agent.Color": { @@ -10572,25 +9444,13 @@ }, { "type": "string", - "enum": [ - "primary", - "secondary", - "accent", - "success", - "warning", - "error", - "info" - ] + "enum": ["primary", "secondary", "accent", "success", "warning", "error", "info"] } ] }, "PermissionV2.Effect": { "type": "string", - "enum": [ - "allow", - "deny", - "ask" - ] + "enum": ["allow", "deny", "ask"] }, "PermissionV2.Rule": { "type": "object", @@ -10605,11 +9465,7 @@ "$ref": "#/components/schemas/PermissionV2.Effect" } }, - "required": [ - "action", - "resource", - "effect" - ], + "required": ["action", "resource", "effect"], "additionalProperties": false }, "PermissionV2.Ruleset": { @@ -10618,15 +9474,12 @@ "$ref": "#/components/schemas/PermissionV2.Rule" } }, - "Agent.Info": { + "AgentV2.Info": { "type": "object", "properties": { "id": { "type": "string" }, - "name": { - "type": "string" - }, "model": { "$ref": "#/components/schemas/Model.Ref" }, @@ -10641,11 +9494,7 @@ }, "mode": { "type": "string", - "enum": [ - "subagent", - "primary", - "all" - ] + "enum": ["subagent", "primary", "all"] }, "hidden": { "type": "boolean" @@ -10665,14 +9514,7 @@ "$ref": "#/components/schemas/PermissionV2.Ruleset" } }, - "required": [ - "id", - "name", - "request", - "mode", - "hidden", - "permissions" - ], + "required": ["id", "request", "mode", "hidden", "permissions"], "additionalProperties": false }, "Plugin.Info": { @@ -10682,49 +9524,7 @@ "type": "string" } }, - "required": [ - "id" - ], - "additionalProperties": false - }, - "Money.USD": { - "type": "number" - }, - "TokenUsage.Info": { - "type": "object", - "properties": { - "input": { - "type": "number" - }, - "output": { - "type": "number" - }, - "reasoning": { - "type": "number" - }, - "cache": { - "type": "object", - "properties": { - "read": { - "type": "number" - }, - "write": { - "type": "number" - } - }, - "required": [ - "read", - "write" - ], - "additionalProperties": false - } - }, - "required": [ - "input", - "output", - "reasoning", - "cache" - ], + "required": ["id"], "additionalProperties": false }, "Location.Ref": { @@ -10742,19 +9542,18 @@ ] } }, - "required": [ - "directory" - ], + "required": ["directory"], "additionalProperties": false }, - "FileDiff.Info": { + "File.Diff": { "type": "object", "properties": { - "file": { + "path": { "type": "string" }, - "patch": { - "type": "string" + "status": { + "type": "string", + "enum": ["added", "modified", "deleted"] }, "additions": { "type": "integer", @@ -10772,25 +9571,14 @@ } ] }, - "status": { - "type": "string", - "enum": [ - "added", - "deleted", - "modified" - ] + "patch": { + "type": "string" } }, - "required": [ - "file", - "patch", - "additions", - "deletions", - "status" - ], + "required": ["path", "status", "additions", "deletions", "patch"], "additionalProperties": false }, - "Session.Revert": { + "Revert.State": { "type": "object", "properties": { "messageID": { @@ -10807,19 +9595,20 @@ "snapshot": { "type": "string" }, + "diff": { + "type": "string" + }, "files": { "type": "array", "items": { - "$ref": "#/components/schemas/FileDiff.Info" + "$ref": "#/components/schemas/File.Diff" } } }, - "required": [ - "messageID" - ], + "required": ["messageID"], "additionalProperties": false }, - "Session.Info": { + "SessionV2.Info": { "type": "object", "properties": { "id": { @@ -10838,31 +9627,6 @@ } ] }, - "fork": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "allOf": [ - { - "pattern": "^ses" - } - ] - }, - "messageID": { - "type": "string", - "allOf": [ - { - "pattern": "^msg_" - } - ] - } - }, - "required": [ - "sessionID" - ], - "additionalProperties": false - }, "projectID": { "type": "string" }, @@ -10873,10 +9637,36 @@ "$ref": "#/components/schemas/Model.Ref" }, "cost": { - "$ref": "#/components/schemas/Money.USD" + "type": "number" }, "tokens": { - "$ref": "#/components/schemas/TokenUsage.Info" + "type": "object", + "properties": { + "input": { + "type": "number" + }, + "output": { + "type": "number" + }, + "reasoning": { + "type": "number" + }, + "cache": { + "type": "object", + "properties": { + "read": { + "type": "number" + }, + "write": { + "type": "number" + } + }, + "required": ["read", "write"], + "additionalProperties": false + } + }, + "required": ["input", "output", "reasoning", "cache"], + "additionalProperties": false }, "time": { "type": "object", @@ -10891,10 +9681,7 @@ "type": "number" } }, - "required": [ - "created", - "updated" - ], + "required": ["created", "updated"], "additionalProperties": false }, "title": { @@ -10907,29 +9694,38 @@ "type": "string" }, "revert": { - "$ref": "#/components/schemas/Session.Revert" + "$ref": "#/components/schemas/Revert.State" } }, - "required": [ - "id", - "projectID", - "cost", - "tokens", - "time", - "title", - "location" - ], + "required": ["id", "projectID", "cost", "tokens", "time", "title", "location"], "additionalProperties": false }, + "SessionWatermarks": { + "type": "object", + "patternProperties": { + "^ses": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + } + }, + "description": "Durable log seq each session's snapshot was computed at. Attach a live log read after the watermark to compose fetch and stream gap-free; apply a snapshot only where its watermark is at or beyond already-applied events. Sessions without durable events are absent." + }, "SessionsResponse": { "type": "object", "properties": { "data": { "type": "array", "items": { - "$ref": "#/components/schemas/Session.Info" + "$ref": "#/components/schemas/SessionV2.Info" } }, + "watermarks": { + "$ref": "#/components/schemas/SessionWatermarks" + }, "cursor": { "type": "object", "properties": { @@ -10957,10 +9753,7 @@ "additionalProperties": false } }, - "required": [ - "data", - "cursor" - ], + "required": ["data", "watermarks", "cursor"], "additionalProperties": false }, "InvalidCursorError": { @@ -10968,18 +9761,13 @@ "properties": { "_tag": { "type": "string", - "enum": [ - "InvalidCursorError" - ] + "enum": ["InvalidCursorError"] }, "message": { "type": "string" } }, - "required": [ - "_tag", - "message" - ], + "required": ["_tag", "message"], "additionalProperties": false }, "InvalidRequestError1": { @@ -10987,9 +9775,7 @@ "properties": { "_tag": { "type": "string", - "enum": [ - "InvalidRequestError" - ] + "enum": ["InvalidRequestError"] }, "message": { "type": "string" @@ -11015,10 +9801,7 @@ ] } }, - "required": [ - "_tag", - "message" - ], + "required": ["_tag", "message"], "additionalProperties": false }, "SessionActive": { @@ -11026,14 +9809,10 @@ "properties": { "type": { "type": "string", - "enum": [ - "running" - ] + "enum": ["running"] } }, - "required": [ - "type" - ], + "required": ["type"], "additionalProperties": false }, "SessionNotFoundError": { @@ -11041,9 +9820,7 @@ "properties": { "_tag": { "type": "string", - "enum": [ - "SessionNotFoundError" - ] + "enum": ["SessionNotFoundError"] }, "sessionID": { "type": "string" @@ -11052,11 +9829,7 @@ "type": "string" } }, - "required": [ - "_tag", - "sessionID", - "message" - ], + "required": ["_tag", "sessionID", "message"], "additionalProperties": false }, "MessageNotFoundError": { @@ -11064,9 +9837,7 @@ "properties": { "_tag": { "type": "string", - "enum": [ - "MessageNotFoundError" - ] + "enum": ["MessageNotFoundError"] }, "sessionID": { "type": "string" @@ -11078,15 +9849,10 @@ "type": "string" } }, - "required": [ - "_tag", - "sessionID", - "messageID", - "message" - ], + "required": ["_tag", "sessionID", "messageID", "message"], "additionalProperties": false }, - "Prompt.Mention": { + "Prompt.Source": { "type": "object", "properties": { "start": { @@ -11099,11 +9865,7 @@ "type": "string" } }, - "required": [ - "start", - "end", - "text" - ], + "required": ["start", "end", "text"], "additionalProperties": false }, "PromptInput.FileAttachment": { @@ -11118,13 +9880,11 @@ "description": { "type": "string" }, - "mention": { - "$ref": "#/components/schemas/Prompt.Mention" + "source": { + "$ref": "#/components/schemas/Prompt.Source" } }, - "required": [ - "uri" - ], + "required": ["uri"], "additionalProperties": false }, "Prompt.AgentAttachment": { @@ -11133,13 +9893,11 @@ "name": { "type": "string" }, - "mention": { - "$ref": "#/components/schemas/Prompt.Mention" + "source": { + "$ref": "#/components/schemas/Prompt.Source" } }, - "required": [ - "name" - ], + "required": ["name"], "additionalProperties": false }, "PromptInput": { @@ -11161,84 +9919,29 @@ } } }, - "required": [ - "text" - ], + "required": ["text"], "additionalProperties": false }, - "Prompt.Base64": { - "type": "string", - "allOf": [ - { - "pattern": "^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$" - } - ] - }, - "Prompt.FileSource": { - "anyOf": [ - { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": [ - "inline" - ] - } - }, - "required": [ - "type" - ], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": [ - "uri" - ] - }, - "uri": { - "type": "string" - } - }, - "required": [ - "type", - "uri" - ], - "additionalProperties": false - } - ] - }, "Prompt.FileAttachment": { "type": "object", "properties": { - "data": { - "$ref": "#/components/schemas/Prompt.Base64" + "uri": { + "type": "string" }, "mime": { "type": "string" }, - "source": { - "$ref": "#/components/schemas/Prompt.FileSource" - }, "name": { "type": "string" }, "description": { "type": "string" }, - "mention": { - "$ref": "#/components/schemas/Prompt.Mention" + "source": { + "$ref": "#/components/schemas/Prompt.Source" } }, - "required": [ - "data", - "mime", - "source" - ], + "required": ["uri", "mime"], "additionalProperties": false }, "Prompt": { @@ -11260,9 +9963,7 @@ } } }, - "required": [ - "text" - ], + "required": ["text"], "additionalProperties": false }, "SessionInput.Admitted": { @@ -11297,10 +9998,7 @@ }, "delivery": { "type": "string", - "enum": [ - "steer", - "queue" - ] + "enum": ["steer", "queue"] }, "timeCreated": { "type": "number" @@ -11314,14 +10012,7 @@ ] } }, - "required": [ - "admittedSeq", - "id", - "sessionID", - "prompt", - "delivery", - "timeCreated" - ], + "required": ["admittedSeq", "id", "sessionID", "prompt", "delivery", "timeCreated"], "additionalProperties": false }, "ConflictError": { @@ -11329,9 +10020,7 @@ "properties": { "_tag": { "type": "string", - "enum": [ - "ConflictError" - ] + "enum": ["ConflictError"] }, "message": { "type": "string" @@ -11347,10 +10036,7 @@ ] } }, - "required": [ - "_tag", - "message" - ], + "required": ["_tag", "message"], "additionalProperties": false }, "CommandNotFoundError": { @@ -11358,9 +10044,7 @@ "properties": { "_tag": { "type": "string", - "enum": [ - "CommandNotFoundError" - ] + "enum": ["CommandNotFoundError"] }, "command": { "type": "string" @@ -11369,11 +10053,7 @@ "type": "string" } }, - "required": [ - "_tag", - "command", - "message" - ], + "required": ["_tag", "command", "message"], "additionalProperties": false }, "CommandEvaluationError": { @@ -11381,9 +10061,7 @@ "properties": { "_tag": { "type": "string", - "enum": [ - "CommandEvaluationError" - ] + "enum": ["CommandEvaluationError"] }, "command": { "type": "string" @@ -11392,11 +10070,7 @@ "type": "string" } }, - "required": [ - "_tag", - "command", - "message" - ], + "required": ["_tag", "command", "message"], "additionalProperties": false }, "SkillNotFoundError": { @@ -11404,9 +10078,7 @@ "properties": { "_tag": { "type": "string", - "enum": [ - "SkillNotFoundError" - ] + "enum": ["SkillNotFoundError"] }, "skill": { "type": "string" @@ -11415,65 +10087,24 @@ "type": "string" } }, - "required": [ - "_tag", - "skill", - "message" - ], + "required": ["_tag", "skill", "message"], "additionalProperties": false }, - "SessionInput.Compaction": { + "SessionBusyError": { "type": "object", "properties": { - "type": { + "_tag": { "type": "string", - "enum": [ - "compaction" - ] - }, - "admittedSeq": { - "type": "integer", - "allOf": [ - { - "minimum": 0 - } - ] - }, - "id": { - "type": "string", - "allOf": [ - { - "pattern": "^msg_" - } - ] + "enum": ["SessionBusyError"] }, "sessionID": { - "type": "string", - "allOf": [ - { - "pattern": "^ses" - } - ] + "type": "string" }, - "timeCreated": { - "type": "number" - }, - "handledSeq": { - "type": "integer", - "allOf": [ - { - "minimum": 0 - } - ] + "message": { + "type": "string" } }, - "required": [ - "type", - "admittedSeq", - "id", - "sessionID", - "timeCreated" - ], + "required": ["_tag", "sessionID", "message"], "additionalProperties": false }, "ServiceUnavailableError": { @@ -11481,9 +10112,7 @@ "properties": { "_tag": { "type": "string", - "enum": [ - "ServiceUnavailableError" - ] + "enum": ["ServiceUnavailableError"] }, "message": { "type": "string" @@ -11499,33 +10128,7 @@ ] } }, - "required": [ - "_tag", - "message" - ], - "additionalProperties": false - }, - "SessionBusyError": { - "type": "object", - "properties": { - "_tag": { - "type": "string", - "enum": [ - "SessionBusyError" - ] - }, - "sessionID": { - "type": "string" - }, - "message": { - "type": "string" - } - }, - "required": [ - "_tag", - "sessionID", - "message" - ], + "required": ["_tag", "message"], "additionalProperties": false }, "UnknownError": { @@ -11533,9 +10136,7 @@ "properties": { "_tag": { "type": "string", - "enum": [ - "UnknownError" - ] + "enum": ["UnknownError"] }, "message": { "type": "string" @@ -11551,13 +10152,10 @@ ] } }, - "required": [ - "_tag", - "message" - ], + "required": ["_tag", "message"], "additionalProperties": false }, - "Session.Message.AgentSelected": { + "Session.Message.AgentSwitched": { "type": "object", "properties": { "id": { @@ -11578,30 +10176,21 @@ "type": "number" } }, - "required": [ - "created" - ], + "required": ["created"], "additionalProperties": false }, "type": { "type": "string", - "enum": [ - "agent-switched" - ] + "enum": ["agent-switched"] }, "agent": { "type": "string" } }, - "required": [ - "id", - "time", - "type", - "agent" - ], + "required": ["id", "time", "type", "agent"], "additionalProperties": false }, - "Session.Message.ModelSelected": { + "Session.Message.ModelSwitched": { "type": "object", "properties": { "id": { @@ -11622,30 +10211,18 @@ "type": "number" } }, - "required": [ - "created" - ], + "required": ["created"], "additionalProperties": false }, "type": { "type": "string", - "enum": [ - "model-switched" - ] + "enum": ["model-switched"] }, "model": { "$ref": "#/components/schemas/Model.Ref" - }, - "previous": { - "$ref": "#/components/schemas/Model.Ref" } }, - "required": [ - "id", - "time", - "type", - "model" - ], + "required": ["id", "time", "type", "model"], "additionalProperties": false }, "Session.Message.User": { @@ -11669,9 +10246,7 @@ "type": "number" } }, - "required": [ - "created" - ], + "required": ["created"], "additionalProperties": false }, "text": { @@ -11691,17 +10266,10 @@ }, "type": { "type": "string", - "enum": [ - "user" - ] + "enum": ["user"] } }, - "required": [ - "id", - "time", - "text", - "type" - ], + "required": ["id", "time", "text", "type"], "additionalProperties": false }, "Session.Message.Synthetic": { @@ -11725,11 +10293,17 @@ "type": "number" } }, - "required": [ - "created" - ], + "required": ["created"], "additionalProperties": false }, + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, "text": { "type": "string" }, @@ -11738,17 +10312,10 @@ }, "type": { "type": "string", - "enum": [ - "synthetic" - ] + "enum": ["synthetic"] } }, - "required": [ - "id", - "time", - "text", - "type" - ], + "required": ["id", "time", "sessionID", "text", "type"], "additionalProperties": false }, "Session.Message.System": { @@ -11772,27 +10339,18 @@ "type": "number" } }, - "required": [ - "created" - ], + "required": ["created"], "additionalProperties": false }, "type": { "type": "string", - "enum": [ - "system" - ] + "enum": ["system"] }, "text": { "type": "string" } }, - "required": [ - "id", - "time", - "type", - "text" - ], + "required": ["id", "time", "type", "text"], "additionalProperties": false }, "Session.Message.Skill": { @@ -11816,19 +10374,12 @@ "type": "number" } }, - "required": [ - "created" - ], + "required": ["created"], "additionalProperties": false }, "type": { "type": "string", - "enum": [ - "skill" - ] - }, - "skill": { - "type": "string" + "enum": ["skill"] }, "name": { "type": "string" @@ -11837,14 +10388,7 @@ "type": "string" } }, - "required": [ - "id", - "time", - "type", - "skill", - "name", - "text" - ], + "required": ["id", "time", "type", "name", "text"], "additionalProperties": false }, "Session.Message.Shell": { @@ -11871,117 +10415,24 @@ "type": "number" } }, - "required": [ - "created" - ], + "required": ["created"], "additionalProperties": false }, "type": { "type": "string", - "enum": [ - "shell" - ] + "enum": ["shell"] }, - "shellID": { - "type": "string", - "allOf": [ - { - "pattern": "^sh_" - } - ] + "callID": { + "type": "string" }, "command": { "type": "string" }, - "status": { - "type": "string", - "enum": [ - "running", - "exited", - "timeout", - "killed" - ] - }, - "exit": { - "anyOf": [ - { - "anyOf": [ - { - "type": "number" - }, - { - "type": "string", - "enum": [ - "NaN" - ] - }, - { - "type": "string", - "enum": [ - "Infinity" - ] - }, - { - "type": "string", - "enum": [ - "-Infinity" - ] - } - ] - }, - { - "type": "string", - "enum": [ - "Infinity", - "-Infinity", - "NaN" - ] - } - ] - }, "output": { - "type": "object", - "properties": { - "output": { - "type": "string" - }, - "cursor": { - "type": "integer", - "allOf": [ - { - "minimum": 0 - } - ] - }, - "size": { - "type": "integer", - "allOf": [ - { - "minimum": 0 - } - ] - }, - "truncated": { - "type": "boolean" - } - }, - "required": [ - "output", - "cursor", - "size", - "truncated" - ], - "additionalProperties": false + "type": "string" } }, - "required": [ - "id", - "time", - "type", - "shellID", - "command", - "status" - ], + "required": ["id", "time", "type", "callID", "command", "output"], "additionalProperties": false }, "Session.Message.Assistant.Text": { @@ -11989,37 +10440,39 @@ "properties": { "type": { "type": "string", - "enum": [ - "text" - ] + "enum": ["text"] + }, + "id": { + "type": "string" }, "text": { "type": "string" } }, - "required": [ - "type", - "text" - ], + "required": ["type", "id", "text"], "additionalProperties": false }, - "Session.Message.ProviderState": { - "type": "object" + "LLM.ProviderMetadata": { + "type": "object", + "additionalProperties": { + "type": "object" + } }, "Session.Message.Assistant.Reasoning": { "type": "object", "properties": { "type": { "type": "string", - "enum": [ - "reasoning" - ] + "enum": ["reasoning"] + }, + "id": { + "type": "string" }, "text": { "type": "string" }, - "state": { - "$ref": "#/components/schemas/Session.Message.ProviderState" + "providerMetadata": { + "$ref": "#/components/schemas/LLM.ProviderMetadata" }, "time": { "type": "object", @@ -12031,35 +10484,25 @@ "type": "number" } }, - "required": [ - "created" - ], + "required": ["created"], "additionalProperties": false } }, - "required": [ - "type", - "text" - ], + "required": ["type", "id", "text"], "additionalProperties": false }, - "Session.Message.ToolState.Streaming": { + "Session.Message.ToolState.Pending": { "type": "object", "properties": { "status": { "type": "string", - "enum": [ - "streaming" - ] + "enum": ["pending"] }, "input": { "type": "string" } }, - "required": [ - "status", - "input" - ], + "required": ["status", "input"], "additionalProperties": false }, "Tool.TextContent": { @@ -12067,18 +10510,13 @@ "properties": { "type": { "type": "string", - "enum": [ - "text" - ] + "enum": ["text"] }, "text": { "type": "string" } }, - "required": [ - "type", - "text" - ], + "required": ["type", "text"], "additionalProperties": false }, "Tool.FileContent": { @@ -12086,9 +10524,7 @@ "properties": { "type": { "type": "string", - "enum": [ - "file" - ] + "enum": ["file"] }, "uri": { "type": "string" @@ -12100,11 +10536,7 @@ "type": "string" } }, - "required": [ - "type", - "uri", - "mime" - ], + "required": ["type", "uri", "mime"], "additionalProperties": false }, "LLM.ToolContent": { @@ -12122,9 +10554,7 @@ "properties": { "status": { "type": "string", - "enum": [ - "running" - ] + "enum": ["running"] }, "input": { "type": "object" @@ -12139,12 +10569,7 @@ } } }, - "required": [ - "status", - "input", - "structured", - "content" - ], + "required": ["status", "input", "structured", "content"], "additionalProperties": false }, "Session.Message.ToolState.Completed": { @@ -12152,46 +10577,49 @@ "properties": { "status": { "type": "string", - "enum": [ - "completed" - ] + "enum": ["completed"] }, "input": { "type": "object" }, + "attachments": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Prompt.FileAttachment" + } + }, "content": { "type": "array", "items": { "$ref": "#/components/schemas/LLM.ToolContent" } }, + "outputPaths": { + "type": "array", + "items": { + "type": "string" + } + }, "structured": { "type": "object" }, "result": {} }, - "required": [ - "status", - "input", - "content", - "structured" - ], + "required": ["status", "input", "content", "structured"], "additionalProperties": false }, - "Session.StructuredError": { + "Session.Error.Unknown": { "type": "object", "properties": { "type": { - "type": "string" + "type": "string", + "enum": ["unknown"] }, "message": { "type": "string" } }, - "required": [ - "type", - "message" - ], + "required": ["type", "message"], "additionalProperties": false }, "Session.Message.ToolState.Error": { @@ -12199,9 +10627,7 @@ "properties": { "status": { "type": "string", - "enum": [ - "error" - ] + "enum": ["error"] }, "input": { "type": "object" @@ -12216,17 +10642,11 @@ "type": "object" }, "error": { - "$ref": "#/components/schemas/Session.StructuredError" + "$ref": "#/components/schemas/Session.Error.Unknown" }, "result": {} }, - "required": [ - "status", - "input", - "content", - "structured", - "error" - ], + "required": ["status", "input", "content", "structured", "error"], "additionalProperties": false }, "Session.Message.Assistant.Tool": { @@ -12234,9 +10654,7 @@ "properties": { "type": { "type": "string", - "enum": [ - "tool" - ] + "enum": ["tool"] }, "id": { "type": "string" @@ -12244,19 +10662,26 @@ "name": { "type": "string" }, - "executed": { - "type": "boolean" - }, - "providerState": { - "$ref": "#/components/schemas/Session.Message.ProviderState" - }, - "providerResultState": { - "$ref": "#/components/schemas/Session.Message.ProviderState" + "provider": { + "type": "object", + "properties": { + "executed": { + "type": "boolean" + }, + "metadata": { + "$ref": "#/components/schemas/LLM.ProviderMetadata" + }, + "resultMetadata": { + "$ref": "#/components/schemas/LLM.ProviderMetadata" + } + }, + "required": ["executed"], + "additionalProperties": false }, "state": { "anyOf": [ { - "$ref": "#/components/schemas/Session.Message.ToolState.Streaming" + "$ref": "#/components/schemas/Session.Message.ToolState.Pending" }, { "$ref": "#/components/schemas/Session.Message.ToolState.Running" @@ -12280,46 +10705,16 @@ }, "completed": { "type": "number" + }, + "pruned": { + "type": "number" } }, - "required": [ - "created" - ], + "required": ["created"], "additionalProperties": false } }, - "required": [ - "type", - "id", - "name", - "state", - "time" - ], - "additionalProperties": false - }, - "Session.Message.Assistant.Retry": { - "type": "object", - "properties": { - "attempt": { - "type": "integer", - "allOf": [ - { - "exclusiveMinimum": 0 - } - ] - }, - "at": { - "type": "number" - }, - "error": { - "$ref": "#/components/schemas/Session.StructuredError" - } - }, - "required": [ - "attempt", - "at", - "error" - ], + "required": ["type", "id", "name", "state", "time"], "additionalProperties": false }, "Session.Message.Assistant": { @@ -12346,16 +10741,12 @@ "type": "number" } }, - "required": [ - "created" - ], + "required": ["created"], "additionalProperties": false }, "type": { "type": "string", - "enum": [ - "assistant" - ] + "enum": ["assistant"] }, "agent": { "type": "string" @@ -12398,244 +10789,96 @@ "additionalProperties": false }, "finish": { - "type": "string", - "enum": [ - "stop", - "length", - "tool-calls", - "content-filter", - "error", - "unknown" - ] + "type": "string" }, "cost": { - "$ref": "#/components/schemas/Money.USD" + "type": "number" }, "tokens": { - "$ref": "#/components/schemas/TokenUsage.Info" + "type": "object", + "properties": { + "input": { + "type": "number" + }, + "output": { + "type": "number" + }, + "reasoning": { + "type": "number" + }, + "cache": { + "type": "object", + "properties": { + "read": { + "type": "number" + }, + "write": { + "type": "number" + } + }, + "required": ["read", "write"], + "additionalProperties": false + } + }, + "required": ["input", "output", "reasoning", "cache"], + "additionalProperties": false }, "error": { - "$ref": "#/components/schemas/Session.StructuredError" - }, - "retry": { - "$ref": "#/components/schemas/Session.Message.Assistant.Retry" + "$ref": "#/components/schemas/Session.Error.Unknown" } }, - "required": [ - "id", - "time", - "type", - "agent", - "model", - "content" - ], - "additionalProperties": false - }, - "Session.Message.Compaction.Running": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": [ - "compaction" - ] - }, - "id": { - "type": "string", - "allOf": [ - { - "pattern": "^msg_" - } - ] - }, - "metadata": { - "type": "object" - }, - "time": { - "type": "object", - "properties": { - "created": { - "type": "number" - } - }, - "required": [ - "created" - ], - "additionalProperties": false - }, - "status": { - "type": "string", - "enum": [ - "running" - ] - }, - "reason": { - "type": "string", - "enum": [ - "auto", - "manual" - ] - }, - "summary": { - "type": "string" - }, - "recent": { - "type": "string" - } - }, - "required": [ - "type", - "id", - "time", - "status", - "reason", - "summary", - "recent" - ], - "additionalProperties": false - }, - "Session.Message.Compaction.Completed": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": [ - "compaction" - ] - }, - "id": { - "type": "string", - "allOf": [ - { - "pattern": "^msg_" - } - ] - }, - "metadata": { - "type": "object" - }, - "time": { - "type": "object", - "properties": { - "created": { - "type": "number" - } - }, - "required": [ - "created" - ], - "additionalProperties": false - }, - "status": { - "type": "string", - "enum": [ - "completed" - ] - }, - "reason": { - "type": "string", - "enum": [ - "auto", - "manual" - ] - }, - "summary": { - "type": "string" - }, - "recent": { - "type": "string" - } - }, - "required": [ - "type", - "id", - "time", - "status", - "reason", - "summary", - "recent" - ], - "additionalProperties": false - }, - "Session.Message.Compaction.Failed": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": [ - "compaction" - ] - }, - "id": { - "type": "string", - "allOf": [ - { - "pattern": "^msg_" - } - ] - }, - "metadata": { - "type": "object" - }, - "time": { - "type": "object", - "properties": { - "created": { - "type": "number" - } - }, - "required": [ - "created" - ], - "additionalProperties": false - }, - "status": { - "type": "string", - "enum": [ - "failed" - ] - }, - "reason": { - "type": "string", - "enum": [ - "auto", - "manual" - ] - }, - "error": { - "$ref": "#/components/schemas/Session.StructuredError" - } - }, - "required": [ - "type", - "id", - "time", - "status", - "reason", - "error" - ], + "required": ["id", "time", "type", "agent", "model", "content"], "additionalProperties": false }, "Session.Message.Compaction": { - "anyOf": [ - { - "$ref": "#/components/schemas/Session.Message.Compaction.Running" + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["compaction"] }, - { - "$ref": "#/components/schemas/Session.Message.Compaction.Completed" + "reason": { + "type": "string", + "enum": ["auto", "manual"] }, - { - "$ref": "#/components/schemas/Session.Message.Compaction.Failed" + "summary": { + "type": "string" + }, + "recent": { + "type": "string" + }, + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, + "metadata": { + "type": "object" + }, + "time": { + "type": "object", + "properties": { + "created": { + "type": "number" + } + }, + "required": ["created"], + "additionalProperties": false } - ] + }, + "required": ["type", "reason", "summary", "recent", "id", "time"], + "additionalProperties": false }, - "Session.Message.Info": { + "Session.Message": { "anyOf": [ { - "$ref": "#/components/schemas/Session.Message.AgentSelected" + "$ref": "#/components/schemas/Session.Message.AgentSwitched" }, { - "$ref": "#/components/schemas/Session.Message.ModelSelected" + "$ref": "#/components/schemas/Session.Message.ModelSwitched" }, { "$ref": "#/components/schemas/Session.Message.User" @@ -12660,30 +10903,27 @@ } ] }, - "InstructionEntry.Key": { + "SessionContextEntry.Key": { "type": "string", "allOf": [ { "pattern": "^[a-z0-9][a-z0-9._-]*$", - "description": "Instruction entry key (lowercase alphanumerics plus . _ -)" + "description": "Context entry key (lowercase alphanumerics plus . _ -)" } ] }, - "InstructionEntry.Info": { + "SessionContextEntry.Info": { "type": "object", "properties": { "key": { - "$ref": "#/components/schemas/InstructionEntry.Key" + "$ref": "#/components/schemas/SessionContextEntry.Key" }, "value": {} }, - "required": [ - "key", - "value" - ], + "required": ["key", "value"], "additionalProperties": false }, - "session.agent.selected": { + "session.next.agent.switched": { "type": "object", "properties": { "id": { @@ -12694,17 +10934,12 @@ } ] }, - "created": { - "type": "number" - }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": [ - "session.agent.selected" - ] + "enum": ["session.next.agent.switched"] }, "durable": { "type": "object", @@ -12721,17 +10956,15 @@ ] }, "version": { - "type": "number", - "enum": [ - 1 + "type": "integer", + "allOf": [ + { + "minimum": 1 + } ] } }, - "required": [ - "aggregateID", - "seq", - "version" - ], + "required": ["aggregateID", "seq", "version"], "additionalProperties": false }, "location": { @@ -12740,6 +10973,9 @@ "data": { "type": "object", "properties": { + "timestamp": { + "type": "number" + }, "sessionID": { "type": "string", "allOf": [ @@ -12748,27 +10984,26 @@ } ] }, + "messageID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, "agent": { "type": "string" } }, - "required": [ - "sessionID", - "agent" - ], + "required": ["timestamp", "sessionID", "messageID", "agent"], "additionalProperties": false } }, - "required": [ - "id", - "created", - "type", - "durable", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, - "session.model.selected": { + "session.next.model.switched": { "type": "object", "properties": { "id": { @@ -12779,17 +11014,12 @@ } ] }, - "created": { - "type": "number" - }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": [ - "session.model.selected" - ] + "enum": ["session.next.model.switched"] }, "durable": { "type": "object", @@ -12806,17 +11036,15 @@ ] }, "version": { - "type": "number", - "enum": [ - 1 + "type": "integer", + "allOf": [ + { + "minimum": 1 + } ] } }, - "required": [ - "aggregateID", - "seq", - "version" - ], + "required": ["aggregateID", "seq", "version"], "additionalProperties": false }, "location": { @@ -12825,6 +11053,9 @@ "data": { "type": "object", "properties": { + "timestamp": { + "type": "number" + }, "sessionID": { "type": "string", "allOf": [ @@ -12833,27 +11064,26 @@ } ] }, + "messageID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, "model": { "$ref": "#/components/schemas/Model.Ref" } }, - "required": [ - "sessionID", - "model" - ], + "required": ["timestamp", "sessionID", "messageID", "model"], "additionalProperties": false } }, - "required": [ - "id", - "created", - "type", - "durable", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, - "session.moved": { + "session.next.moved": { "type": "object", "properties": { "id": { @@ -12864,17 +11094,12 @@ } ] }, - "created": { - "type": "number" - }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": [ - "session.moved" - ] + "enum": ["session.next.moved"] }, "durable": { "type": "object", @@ -12891,17 +11116,15 @@ ] }, "version": { - "type": "number", - "enum": [ - 1 + "type": "integer", + "allOf": [ + { + "minimum": 1 + } ] } }, - "required": [ - "aggregateID", - "seq", - "version" - ], + "required": ["aggregateID", "seq", "version"], "additionalProperties": false }, "location": { @@ -12910,6 +11133,9 @@ "data": { "type": "object", "properties": { + "timestamp": { + "type": "number" + }, "sessionID": { "type": "string", "allOf": [ @@ -12921,27 +11147,18 @@ "location": { "$ref": "#/components/schemas/Location.Ref" }, - "subpath": { + "subdirectory": { "type": "string" } }, - "required": [ - "sessionID", - "location" - ], + "required": ["timestamp", "sessionID", "location"], "additionalProperties": false } }, - "required": [ - "id", - "created", - "type", - "durable", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, - "session.renamed": { + "session.next.renamed": { "type": "object", "properties": { "id": { @@ -12952,17 +11169,12 @@ } ] }, - "created": { - "type": "number" - }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": [ - "session.renamed" - ] + "enum": ["session.next.renamed"] }, "durable": { "type": "object", @@ -12979,17 +11191,15 @@ ] }, "version": { - "type": "number", - "enum": [ - 1 + "type": "integer", + "allOf": [ + { + "minimum": 1 + } ] } }, - "required": [ - "aggregateID", - "seq", - "version" - ], + "required": ["aggregateID", "seq", "version"], "additionalProperties": false }, "location": { @@ -12998,6 +11208,9 @@ "data": { "type": "object", "properties": { + "timestamp": { + "type": "number" + }, "sessionID": { "type": "string", "allOf": [ @@ -13010,23 +11223,14 @@ "type": "string" } }, - "required": [ - "sessionID", - "title" - ], + "required": ["timestamp", "sessionID", "title"], "additionalProperties": false } }, - "required": [ - "id", - "created", - "type", - "durable", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, - "session.deleted": { + "session.next.forked": { "type": "object", "properties": { "id": { @@ -13037,17 +11241,12 @@ } ] }, - "created": { - "type": "number" - }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": [ - "session.deleted" - ] + "enum": ["session.next.forked"] }, "durable": { "type": "object", @@ -13064,98 +11263,15 @@ ] }, "version": { - "type": "number", - "enum": [ - 2 - ] - } - }, - "required": [ - "aggregateID", - "seq", - "version" - ], - "additionalProperties": false - }, - "location": { - "$ref": "#/components/schemas/Location.Ref" - }, - "data": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "allOf": [ - { - "pattern": "^ses" - } - ] - } - }, - "required": [ - "sessionID" - ], - "additionalProperties": false - } - }, - "required": [ - "id", - "created", - "type", - "durable", - "data" - ], - "additionalProperties": false - }, - "session.forked": { - "type": "object", - "properties": { - "id": { - "type": "string", - "allOf": [ - { - "pattern": "^evt_" - } - ] - }, - "created": { - "type": "number" - }, - "metadata": { - "type": "object" - }, - "type": { - "type": "string", - "enum": [ - "session.forked" - ] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { "type": "integer", "allOf": [ { - "minimum": 0 + "minimum": 1 } ] - }, - "version": { - "type": "number", - "enum": [ - 1 - ] } }, - "required": [ - "aggregateID", - "seq", - "version" - ], + "required": ["aggregateID", "seq", "version"], "additionalProperties": false }, "location": { @@ -13164,6 +11280,9 @@ "data": { "type": "object", "properties": { + "timestamp": { + "type": "number" + }, "sessionID": { "type": "string", "allOf": [ @@ -13180,7 +11299,7 @@ } ] }, - "from": { + "messageID": { "type": "string", "allOf": [ { @@ -13189,23 +11308,14 @@ ] } }, - "required": [ - "sessionID", - "parentID" - ], + "required": ["timestamp", "sessionID", "parentID"], "additionalProperties": false } }, - "required": [ - "id", - "created", - "type", - "durable", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, - "session.prompt.promoted": { + "session.next.prompted": { "type": "object", "properties": { "id": { @@ -13216,17 +11326,12 @@ } ] }, - "created": { - "type": "number" - }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": [ - "session.prompt.promoted" - ] + "enum": ["session.next.prompted"] }, "durable": { "type": "object", @@ -13243,107 +11348,15 @@ ] }, "version": { - "type": "number", - "enum": [ - 1 - ] - } - }, - "required": [ - "aggregateID", - "seq", - "version" - ], - "additionalProperties": false - }, - "location": { - "$ref": "#/components/schemas/Location.Ref" - }, - "data": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "allOf": [ - { - "pattern": "^ses" - } - ] - }, - "inputID": { - "type": "string", - "allOf": [ - { - "pattern": "^msg_" - } - ] - } - }, - "required": [ - "sessionID", - "inputID" - ], - "additionalProperties": false - } - }, - "required": [ - "id", - "created", - "type", - "durable", - "data" - ], - "additionalProperties": false - }, - "session.prompt.admitted": { - "type": "object", - "properties": { - "id": { - "type": "string", - "allOf": [ - { - "pattern": "^evt_" - } - ] - }, - "created": { - "type": "number" - }, - "metadata": { - "type": "object" - }, - "type": { - "type": "string", - "enum": [ - "session.prompt.admitted" - ] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { "type": "integer", "allOf": [ { - "minimum": 0 + "minimum": 1 } ] - }, - "version": { - "type": "number", - "enum": [ - 1 - ] } }, - "required": [ - "aggregateID", - "seq", - "version" - ], + "required": ["aggregateID", "seq", "version"], "additionalProperties": false }, "location": { @@ -13352,6 +11365,9 @@ "data": { "type": "object", "properties": { + "timestamp": { + "type": "number" + }, "sessionID": { "type": "string", "allOf": [ @@ -13360,7 +11376,7 @@ } ] }, - "inputID": { + "messageID": { "type": "string", "allOf": [ { @@ -13373,31 +11389,17 @@ }, "delivery": { "type": "string", - "enum": [ - "steer", - "queue" - ] + "enum": ["steer", "queue"] } }, - "required": [ - "sessionID", - "inputID", - "prompt", - "delivery" - ], + "required": ["timestamp", "sessionID", "messageID", "prompt", "delivery"], "additionalProperties": false } }, - "required": [ - "id", - "created", - "type", - "durable", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, - "session.execution.started": { + "session.next.prompt.admitted": { "type": "object", "properties": { "id": { @@ -13408,17 +11410,12 @@ } ] }, - "created": { - "type": "number" - }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": [ - "session.execution.started" - ] + "enum": ["session.next.prompt.admitted"] }, "durable": { "type": "object", @@ -13435,17 +11432,15 @@ ] }, "version": { - "type": "number", - "enum": [ - 1 + "type": "integer", + "allOf": [ + { + "minimum": 1 + } ] } }, - "required": [ - "aggregateID", - "seq", - "version" - ], + "required": ["aggregateID", "seq", "version"], "additionalProperties": false }, "location": { @@ -13454,6 +11449,9 @@ "data": { "type": "object", "properties": { + "timestamp": { + "type": "number" + }, "sessionID": { "type": "string", "allOf": [ @@ -13461,24 +11459,31 @@ "pattern": "^ses" } ] + }, + "messageID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, + "prompt": { + "$ref": "#/components/schemas/Prompt" + }, + "delivery": { + "type": "string", + "enum": ["steer", "queue"] } }, - "required": [ - "sessionID" - ], + "required": ["timestamp", "sessionID", "messageID", "prompt", "delivery"], "additionalProperties": false } }, - "required": [ - "id", - "created", - "type", - "durable", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, - "session.execution.succeeded": { + "session.next.context.updated": { "type": "object", "properties": { "id": { @@ -13489,17 +11494,12 @@ } ] }, - "created": { - "type": "number" - }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": [ - "session.execution.succeeded" - ] + "enum": ["session.next.context.updated"] }, "durable": { "type": "object", @@ -13516,98 +11516,15 @@ ] }, "version": { - "type": "number", - "enum": [ - 1 - ] - } - }, - "required": [ - "aggregateID", - "seq", - "version" - ], - "additionalProperties": false - }, - "location": { - "$ref": "#/components/schemas/Location.Ref" - }, - "data": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "allOf": [ - { - "pattern": "^ses" - } - ] - } - }, - "required": [ - "sessionID" - ], - "additionalProperties": false - } - }, - "required": [ - "id", - "created", - "type", - "durable", - "data" - ], - "additionalProperties": false - }, - "session.execution.failed": { - "type": "object", - "properties": { - "id": { - "type": "string", - "allOf": [ - { - "pattern": "^evt_" - } - ] - }, - "created": { - "type": "number" - }, - "metadata": { - "type": "object" - }, - "type": { - "type": "string", - "enum": [ - "session.execution.failed" - ] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { "type": "integer", "allOf": [ { - "minimum": 0 + "minimum": 1 } ] - }, - "version": { - "type": "number", - "enum": [ - 1 - ] } }, - "required": [ - "aggregateID", - "seq", - "version" - ], + "required": ["aggregateID", "seq", "version"], "additionalProperties": false }, "location": { @@ -13616,6 +11533,9 @@ "data": { "type": "object", "properties": { + "timestamp": { + "type": "number" + }, "sessionID": { "type": "string", "allOf": [ @@ -13624,178 +11544,11 @@ } ] }, - "error": { - "$ref": "#/components/schemas/Session.StructuredError" - } - }, - "required": [ - "sessionID", - "error" - ], - "additionalProperties": false - } - }, - "required": [ - "id", - "created", - "type", - "durable", - "data" - ], - "additionalProperties": false - }, - "session.execution.interrupted": { - "type": "object", - "properties": { - "id": { - "type": "string", - "allOf": [ - { - "pattern": "^evt_" - } - ] - }, - "created": { - "type": "number" - }, - "metadata": { - "type": "object" - }, - "type": { - "type": "string", - "enum": [ - "session.execution.interrupted" - ] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer", - "allOf": [ - { - "minimum": 0 - } - ] - }, - "version": { - "type": "number", - "enum": [ - 1 - ] - } - }, - "required": [ - "aggregateID", - "seq", - "version" - ], - "additionalProperties": false - }, - "location": { - "$ref": "#/components/schemas/Location.Ref" - }, - "data": { - "type": "object", - "properties": { - "sessionID": { + "messageID": { "type": "string", "allOf": [ { - "pattern": "^ses" - } - ] - }, - "reason": { - "type": "string", - "enum": [ - "user", - "shutdown", - "superseded" - ] - } - }, - "required": [ - "sessionID", - "reason" - ], - "additionalProperties": false - } - }, - "required": [ - "id", - "created", - "type", - "durable", - "data" - ], - "additionalProperties": false - }, - "session.instructions.updated": { - "type": "object", - "properties": { - "id": { - "type": "string", - "allOf": [ - { - "pattern": "^evt_" - } - ] - }, - "created": { - "type": "number" - }, - "metadata": { - "type": "object" - }, - "type": { - "type": "string", - "enum": [ - "session.instructions.updated" - ] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer", - "allOf": [ - { - "minimum": 0 - } - ] - }, - "version": { - "type": "number", - "enum": [ - 1 - ] - } - }, - "required": [ - "aggregateID", - "seq", - "version" - ], - "additionalProperties": false - }, - "location": { - "$ref": "#/components/schemas/Location.Ref" - }, - "data": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "allOf": [ - { - "pattern": "^ses" + "pattern": "^msg_" } ] }, @@ -13803,23 +11556,14 @@ "type": "string" } }, - "required": [ - "sessionID", - "text" - ], + "required": ["timestamp", "sessionID", "messageID", "text"], "additionalProperties": false } }, - "required": [ - "id", - "created", - "type", - "durable", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, - "session.synthetic": { + "session.next.synthetic": { "type": "object", "properties": { "id": { @@ -13830,17 +11574,12 @@ } ] }, - "created": { - "type": "number" - }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": [ - "session.synthetic" - ] + "enum": ["session.next.synthetic"] }, "durable": { "type": "object", @@ -13857,17 +11596,15 @@ ] }, "version": { - "type": "number", - "enum": [ - 1 + "type": "integer", + "allOf": [ + { + "minimum": 1 + } ] } }, - "required": [ - "aggregateID", - "seq", - "version" - ], + "required": ["aggregateID", "seq", "version"], "additionalProperties": false }, "location": { @@ -13876,6 +11613,9 @@ "data": { "type": "object", "properties": { + "timestamp": { + "type": "number" + }, "sessionID": { "type": "string", "allOf": [ @@ -13884,6 +11624,14 @@ } ] }, + "messageID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, "text": { "type": "string" }, @@ -13894,23 +11642,14 @@ "type": "object" } }, - "required": [ - "sessionID", - "text" - ], + "required": ["timestamp", "sessionID", "messageID", "text"], "additionalProperties": false } }, - "required": [ - "id", - "created", - "type", - "durable", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, - "session.skill.activated": { + "session.next.skill.activated": { "type": "object", "properties": { "id": { @@ -13921,17 +11660,12 @@ } ] }, - "created": { - "type": "number" - }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": [ - "session.skill.activated" - ] + "enum": ["session.next.skill.activated"] }, "durable": { "type": "object", @@ -13948,17 +11682,15 @@ ] }, "version": { - "type": "number", - "enum": [ - 1 + "type": "integer", + "allOf": [ + { + "minimum": 1 + } ] } }, - "required": [ - "aggregateID", - "seq", - "version" - ], + "required": ["aggregateID", "seq", "version"], "additionalProperties": false }, "location": { @@ -13967,6 +11699,9 @@ "data": { "type": "object", "properties": { + "timestamp": { + "type": "number" + }, "sessionID": { "type": "string", "allOf": [ @@ -13975,8 +11710,13 @@ } ] }, - "id": { - "type": "string" + "messageID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] }, "name": { "type": "string" @@ -13985,165 +11725,97 @@ "type": "string" } }, - "required": [ - "sessionID", - "id", - "name", - "text" - ], + "required": ["timestamp", "sessionID", "messageID", "name", "text"], "additionalProperties": false } }, - "required": [ - "id", - "created", - "type", - "durable", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, - "Shell": { + "session.next.shell.started": { "type": "object", "properties": { "id": { "type": "string", "allOf": [ { - "pattern": "^sh_" + "pattern": "^evt_" } ] }, - "status": { + "metadata": { + "type": "object" + }, + "type": { "type": "string", - "enum": [ - "running", - "exited", - "timeout", - "killed" - ] + "enum": ["session.next.shell.started"] }, - "command": { - "type": "string" - }, - "cwd": { - "type": "string" - }, - "shell": { - "type": "string" - }, - "file": { - "type": "string" - }, - "pid": { - "type": "integer", - "allOf": [ - { - "minimum": 0 + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "integer", + "allOf": [ + { + "minimum": 1 + } + ] } - ] + }, + "required": ["aggregateID", "seq", "version"], + "additionalProperties": false }, - "exit": { - "anyOf": [ - { + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "timestamp": { "type": "number" }, - { + "sessionID": { "type": "string", - "enum": [ - "NaN" - ] - }, - { - "type": "string", - "enum": [ - "Infinity" - ] - }, - { - "type": "string", - "enum": [ - "-Infinity" - ] - } - ] - }, - "metadata": { - "type": "object" - }, - "time": { - "type": "object", - "properties": { - "started": { - "anyOf": [ + "allOf": [ { - "type": "number" - }, - { - "type": "string", - "enum": [ - "NaN" - ] - }, - { - "type": "string", - "enum": [ - "Infinity" - ] - }, - { - "type": "string", - "enum": [ - "-Infinity" - ] + "pattern": "^ses" } ] }, - "completed": { - "anyOf": [ + "messageID": { + "type": "string", + "allOf": [ { - "type": "number" - }, - { - "type": "string", - "enum": [ - "NaN" - ] - }, - { - "type": "string", - "enum": [ - "Infinity" - ] - }, - { - "type": "string", - "enum": [ - "-Infinity" - ] + "pattern": "^msg_" } ] + }, + "callID": { + "type": "string" + }, + "command": { + "type": "string" } }, - "required": [ - "started" - ], + "required": ["timestamp", "sessionID", "messageID", "callID", "command"], "additionalProperties": false } }, - "required": [ - "id", - "status", - "command", - "cwd", - "shell", - "file", - "metadata", - "time" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, - "session.shell.started": { + "session.next.shell.ended": { "type": "object", "properties": { "id": { @@ -14154,17 +11826,12 @@ } ] }, - "created": { - "type": "number" - }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": [ - "session.shell.started" - ] + "enum": ["session.next.shell.ended"] }, "durable": { "type": "object", @@ -14181,102 +11848,15 @@ ] }, "version": { - "type": "number", - "enum": [ - 1 - ] - } - }, - "required": [ - "aggregateID", - "seq", - "version" - ], - "additionalProperties": false - }, - "location": { - "$ref": "#/components/schemas/Location.Ref" - }, - "data": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "allOf": [ - { - "pattern": "^ses" - } - ] - }, - "shell": { - "$ref": "#/components/schemas/Shell" - } - }, - "required": [ - "sessionID", - "shell" - ], - "additionalProperties": false - } - }, - "required": [ - "id", - "created", - "type", - "durable", - "data" - ], - "additionalProperties": false - }, - "session.shell.ended": { - "type": "object", - "properties": { - "id": { - "type": "string", - "allOf": [ - { - "pattern": "^evt_" - } - ] - }, - "created": { - "type": "number" - }, - "metadata": { - "type": "object" - }, - "type": { - "type": "string", - "enum": [ - "session.shell.ended" - ] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { "type": "integer", "allOf": [ { - "minimum": 0 + "minimum": 1 } ] - }, - "version": { - "type": "number", - "enum": [ - 1 - ] } }, - "required": [ - "aggregateID", - "seq", - "version" - ], + "required": ["aggregateID", "seq", "version"], "additionalProperties": false }, "location": { @@ -14285,6 +11865,9 @@ "data": { "type": "object", "properties": { + "timestamp": { + "type": "number" + }, "sessionID": { "type": "string", "allOf": [ @@ -14293,62 +11876,21 @@ } ] }, - "shell": { - "$ref": "#/components/schemas/Shell" + "callID": { + "type": "string" }, "output": { - "type": "object", - "properties": { - "output": { - "type": "string" - }, - "cursor": { - "type": "integer", - "allOf": [ - { - "minimum": 0 - } - ] - }, - "size": { - "type": "integer", - "allOf": [ - { - "minimum": 0 - } - ] - }, - "truncated": { - "type": "boolean" - } - }, - "required": [ - "output", - "cursor", - "size", - "truncated" - ], - "additionalProperties": false + "type": "string" } }, - "required": [ - "sessionID", - "shell", - "output" - ], + "required": ["timestamp", "sessionID", "callID", "output"], "additionalProperties": false } }, - "required": [ - "id", - "created", - "type", - "durable", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, - "session.step.started": { + "session.next.step.started": { "type": "object", "properties": { "id": { @@ -14359,17 +11901,12 @@ } ] }, - "created": { - "type": "number" - }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": [ - "session.step.started" - ] + "enum": ["session.next.step.started"] }, "durable": { "type": "object", @@ -14386,17 +11923,15 @@ ] }, "version": { - "type": "number", - "enum": [ - 1 + "type": "integer", + "allOf": [ + { + "minimum": 1 + } ] } }, - "required": [ - "aggregateID", - "seq", - "version" - ], + "required": ["aggregateID", "seq", "version"], "additionalProperties": false }, "location": { @@ -14405,6 +11940,9 @@ "data": { "type": "object", "properties": { + "timestamp": { + "type": "number" + }, "sessionID": { "type": "string", "allOf": [ @@ -14431,25 +11969,14 @@ "type": "string" } }, - "required": [ - "sessionID", - "assistantMessageID", - "agent", - "model" - ], + "required": ["timestamp", "sessionID", "assistantMessageID", "agent", "model"], "additionalProperties": false } }, - "required": [ - "id", - "created", - "type", - "durable", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, - "session.step.ended": { + "session.next.step.ended": { "type": "object", "properties": { "id": { @@ -14460,17 +11987,12 @@ } ] }, - "created": { - "type": "number" - }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": [ - "session.step.ended" - ] + "enum": ["session.next.step.ended"] }, "durable": { "type": "object", @@ -14487,17 +12009,15 @@ ] }, "version": { - "type": "number", - "enum": [ - 1 + "type": "integer", + "allOf": [ + { + "minimum": 1 + } ] } }, - "required": [ - "aggregateID", - "seq", - "version" - ], + "required": ["aggregateID", "seq", "version"], "additionalProperties": false }, "location": { @@ -14506,6 +12026,9 @@ "data": { "type": "object", "properties": { + "timestamp": { + "type": "number" + }, "sessionID": { "type": "string", "allOf": [ @@ -14523,21 +12046,39 @@ ] }, "finish": { - "type": "string", - "enum": [ - "stop", - "length", - "tool-calls", - "content-filter", - "error", - "unknown" - ] + "type": "string" }, "cost": { - "$ref": "#/components/schemas/Money.USD" + "type": "number" }, "tokens": { - "$ref": "#/components/schemas/TokenUsage.Info" + "type": "object", + "properties": { + "input": { + "type": "number" + }, + "output": { + "type": "number" + }, + "reasoning": { + "type": "number" + }, + "cache": { + "type": "object", + "properties": { + "read": { + "type": "number" + }, + "write": { + "type": "number" + } + }, + "required": ["read", "write"], + "additionalProperties": false + } + }, + "required": ["input", "output", "reasoning", "cache"], + "additionalProperties": false }, "snapshot": { "type": "string" @@ -14549,26 +12090,14 @@ } } }, - "required": [ - "sessionID", - "assistantMessageID", - "finish", - "cost", - "tokens" - ], + "required": ["timestamp", "sessionID", "assistantMessageID", "finish", "cost", "tokens"], "additionalProperties": false } }, - "required": [ - "id", - "created", - "type", - "durable", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, - "session.step.failed": { + "session.next.step.failed": { "type": "object", "properties": { "id": { @@ -14579,17 +12108,12 @@ } ] }, - "created": { - "type": "number" - }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": [ - "session.step.failed" - ] + "enum": ["session.next.step.failed"] }, "durable": { "type": "object", @@ -14606,17 +12130,15 @@ ] }, "version": { - "type": "number", - "enum": [ - 1 + "type": "integer", + "allOf": [ + { + "minimum": 1 + } ] } }, - "required": [ - "aggregateID", - "seq", - "version" - ], + "required": ["aggregateID", "seq", "version"], "additionalProperties": false }, "location": { @@ -14625,6 +12147,9 @@ "data": { "type": "object", "properties": { + "timestamp": { + "type": "number" + }, "sessionID": { "type": "string", "allOf": [ @@ -14642,33 +12167,17 @@ ] }, "error": { - "$ref": "#/components/schemas/Session.StructuredError" - }, - "cost": { - "$ref": "#/components/schemas/Money.USD" - }, - "tokens": { - "$ref": "#/components/schemas/TokenUsage.Info" + "$ref": "#/components/schemas/Session.Error.Unknown" } }, - "required": [ - "sessionID", - "assistantMessageID", - "error" - ], + "required": ["timestamp", "sessionID", "assistantMessageID", "error"], "additionalProperties": false } }, - "required": [ - "id", - "created", - "type", - "durable", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, - "session.text.started": { + "session.next.text.started": { "type": "object", "properties": { "id": { @@ -14679,17 +12188,12 @@ } ] }, - "created": { - "type": "number" - }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": [ - "session.text.started" - ] + "enum": ["session.next.text.started"] }, "durable": { "type": "object", @@ -14706,17 +12210,15 @@ ] }, "version": { - "type": "number", - "enum": [ - 1 + "type": "integer", + "allOf": [ + { + "minimum": 1 + } ] } }, - "required": [ - "aggregateID", - "seq", - "version" - ], + "required": ["aggregateID", "seq", "version"], "additionalProperties": false }, "location": { @@ -14725,6 +12227,9 @@ "data": { "type": "object", "properties": { + "timestamp": { + "type": "number" + }, "sessionID": { "type": "string", "allOf": [ @@ -14741,33 +12246,18 @@ } ] }, - "ordinal": { - "type": "integer", - "allOf": [ - { - "minimum": 0 - } - ] + "textID": { + "type": "string" } }, - "required": [ - "sessionID", - "assistantMessageID", - "ordinal" - ], + "required": ["timestamp", "sessionID", "assistantMessageID", "textID"], "additionalProperties": false } }, - "required": [ - "id", - "created", - "type", - "durable", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, - "session.text.ended": { + "session.next.text.ended": { "type": "object", "properties": { "id": { @@ -14778,17 +12268,12 @@ } ] }, - "created": { - "type": "number" - }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": [ - "session.text.ended" - ] + "enum": ["session.next.text.ended"] }, "durable": { "type": "object", @@ -14805,17 +12290,15 @@ ] }, "version": { - "type": "number", - "enum": [ - 1 + "type": "integer", + "allOf": [ + { + "minimum": 1 + } ] } }, - "required": [ - "aggregateID", - "seq", - "version" - ], + "required": ["aggregateID", "seq", "version"], "additionalProperties": false }, "location": { @@ -14824,6 +12307,9 @@ "data": { "type": "object", "properties": { + "timestamp": { + "type": "number" + }, "sessionID": { "type": "string", "allOf": [ @@ -14840,40 +12326,21 @@ } ] }, - "ordinal": { - "type": "integer", - "allOf": [ - { - "minimum": 0 - } - ] + "textID": { + "type": "string" }, "text": { "type": "string" } }, - "required": [ - "sessionID", - "assistantMessageID", - "ordinal", - "text" - ], + "required": ["timestamp", "sessionID", "assistantMessageID", "textID", "text"], "additionalProperties": false } }, - "required": [ - "id", - "created", - "type", - "durable", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, - "Session.Message.ProviderState3": { - "type": "object" - }, - "session.reasoning.started": { + "session.next.tool.input.started": { "type": "object", "properties": { "id": { @@ -14884,17 +12351,12 @@ } ] }, - "created": { - "type": "number" - }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": [ - "session.reasoning.started" - ] + "enum": ["session.next.tool.input.started"] }, "durable": { "type": "object", @@ -14911,17 +12373,15 @@ ] }, "version": { - "type": "number", - "enum": [ - 1 + "type": "integer", + "allOf": [ + { + "minimum": 1 + } ] } }, - "required": [ - "aggregateID", - "seq", - "version" - ], + "required": ["aggregateID", "seq", "version"], "additionalProperties": false }, "location": { @@ -14930,217 +12390,9 @@ "data": { "type": "object", "properties": { - "sessionID": { - "type": "string", - "allOf": [ - { - "pattern": "^ses" - } - ] + "timestamp": { + "type": "number" }, - "assistantMessageID": { - "type": "string", - "allOf": [ - { - "pattern": "^msg_" - } - ] - }, - "ordinal": { - "type": "integer", - "allOf": [ - { - "minimum": 0 - } - ] - }, - "state": { - "$ref": "#/components/schemas/Session.Message.ProviderState3" - } - }, - "required": [ - "sessionID", - "assistantMessageID", - "ordinal" - ], - "additionalProperties": false - } - }, - "required": [ - "id", - "created", - "type", - "durable", - "data" - ], - "additionalProperties": false - }, - "Session.Message.ProviderState4": { - "type": "object" - }, - "session.reasoning.ended": { - "type": "object", - "properties": { - "id": { - "type": "string", - "allOf": [ - { - "pattern": "^evt_" - } - ] - }, - "created": { - "type": "number" - }, - "metadata": { - "type": "object" - }, - "type": { - "type": "string", - "enum": [ - "session.reasoning.ended" - ] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer", - "allOf": [ - { - "minimum": 0 - } - ] - }, - "version": { - "type": "number", - "enum": [ - 1 - ] - } - }, - "required": [ - "aggregateID", - "seq", - "version" - ], - "additionalProperties": false - }, - "location": { - "$ref": "#/components/schemas/Location.Ref" - }, - "data": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "allOf": [ - { - "pattern": "^ses" - } - ] - }, - "assistantMessageID": { - "type": "string", - "allOf": [ - { - "pattern": "^msg_" - } - ] - }, - "ordinal": { - "type": "integer", - "allOf": [ - { - "minimum": 0 - } - ] - }, - "text": { - "type": "string" - }, - "state": { - "$ref": "#/components/schemas/Session.Message.ProviderState4" - } - }, - "required": [ - "sessionID", - "assistantMessageID", - "ordinal", - "text" - ], - "additionalProperties": false - } - }, - "required": [ - "id", - "created", - "type", - "durable", - "data" - ], - "additionalProperties": false - }, - "session.tool.input.started": { - "type": "object", - "properties": { - "id": { - "type": "string", - "allOf": [ - { - "pattern": "^evt_" - } - ] - }, - "created": { - "type": "number" - }, - "metadata": { - "type": "object" - }, - "type": { - "type": "string", - "enum": [ - "session.tool.input.started" - ] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer", - "allOf": [ - { - "minimum": 0 - } - ] - }, - "version": { - "type": "number", - "enum": [ - 1 - ] - } - }, - "required": [ - "aggregateID", - "seq", - "version" - ], - "additionalProperties": false - }, - "location": { - "$ref": "#/components/schemas/Location.Ref" - }, - "data": { - "type": "object", - "properties": { "sessionID": { "type": "string", "allOf": [ @@ -15164,25 +12416,14 @@ "type": "string" } }, - "required": [ - "sessionID", - "assistantMessageID", - "callID", - "name" - ], + "required": ["timestamp", "sessionID", "assistantMessageID", "callID", "name"], "additionalProperties": false } }, - "required": [ - "id", - "created", - "type", - "durable", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, - "session.tool.input.ended": { + "session.next.tool.input.ended": { "type": "object", "properties": { "id": { @@ -15193,17 +12434,12 @@ } ] }, - "created": { - "type": "number" - }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": [ - "session.tool.input.ended" - ] + "enum": ["session.next.tool.input.ended"] }, "durable": { "type": "object", @@ -15220,17 +12456,15 @@ ] }, "version": { - "type": "number", - "enum": [ - 1 + "type": "integer", + "allOf": [ + { + "minimum": 1 + } ] } }, - "required": [ - "aggregateID", - "seq", - "version" - ], + "required": ["aggregateID", "seq", "version"], "additionalProperties": false }, "location": { @@ -15239,6 +12473,9 @@ "data": { "type": "object", "properties": { + "timestamp": { + "type": "number" + }, "sessionID": { "type": "string", "allOf": [ @@ -15262,28 +12499,20 @@ "type": "string" } }, - "required": [ - "sessionID", - "assistantMessageID", - "callID", - "text" - ], + "required": ["timestamp", "sessionID", "assistantMessageID", "callID", "text"], "additionalProperties": false } }, - "required": [ - "id", - "created", - "type", - "durable", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, - "Session.Message.ProviderState5": { - "type": "object" + "LLM.ProviderMetadata3": { + "type": "object", + "additionalProperties": { + "type": "object" + } }, - "session.tool.called": { + "session.next.tool.called": { "type": "object", "properties": { "id": { @@ -15294,17 +12523,12 @@ } ] }, - "created": { - "type": "number" - }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": [ - "session.tool.called" - ] + "enum": ["session.next.tool.called"] }, "durable": { "type": "object", @@ -15321,17 +12545,15 @@ ] }, "version": { - "type": "number", - "enum": [ - 1 + "type": "integer", + "allOf": [ + { + "minimum": 1 + } ] } }, - "required": [ - "aggregateID", - "seq", - "version" - ], + "required": ["aggregateID", "seq", "version"], "additionalProperties": false }, "location": { @@ -15340,6 +12562,9 @@ "data": { "type": "object", "properties": { + "timestamp": { + "type": "number" + }, "sessionID": { "type": "string", "allOf": [ @@ -15359,36 +12584,34 @@ "callID": { "type": "string" }, + "tool": { + "type": "string" + }, "input": { "type": "object" }, - "executed": { - "type": "boolean" - }, - "state": { - "$ref": "#/components/schemas/Session.Message.ProviderState5" + "provider": { + "type": "object", + "properties": { + "executed": { + "type": "boolean" + }, + "metadata": { + "$ref": "#/components/schemas/LLM.ProviderMetadata3" + } + }, + "required": ["executed"], + "additionalProperties": false } }, - "required": [ - "sessionID", - "assistantMessageID", - "callID", - "input", - "executed" - ], + "required": ["timestamp", "sessionID", "assistantMessageID", "callID", "tool", "input", "provider"], "additionalProperties": false } }, - "required": [ - "id", - "created", - "type", - "durable", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, - "session.tool.progress": { + "session.next.tool.progress": { "type": "object", "properties": { "id": { @@ -15399,17 +12622,12 @@ } ] }, - "created": { - "type": "number" - }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": [ - "session.tool.progress" - ] + "enum": ["session.next.tool.progress"] }, "durable": { "type": "object", @@ -15426,17 +12644,15 @@ ] }, "version": { - "type": "number", - "enum": [ - 1 + "type": "integer", + "allOf": [ + { + "minimum": 1 + } ] } }, - "required": [ - "aggregateID", - "seq", - "version" - ], + "required": ["aggregateID", "seq", "version"], "additionalProperties": false }, "location": { @@ -15445,6 +12661,9 @@ "data": { "type": "object", "properties": { + "timestamp": { + "type": "number" + }, "sessionID": { "type": "string", "allOf": [ @@ -15474,29 +12693,20 @@ } } }, - "required": [ - "sessionID", - "assistantMessageID", - "callID", - "structured", - "content" - ], + "required": ["timestamp", "sessionID", "assistantMessageID", "callID", "structured", "content"], "additionalProperties": false } }, - "required": [ - "id", - "created", - "type", - "durable", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, - "Session.Message.ProviderState6": { - "type": "object" + "LLM.ProviderMetadata4": { + "type": "object", + "additionalProperties": { + "type": "object" + } }, - "session.tool.success": { + "session.next.tool.success": { "type": "object", "properties": { "id": { @@ -15507,17 +12717,12 @@ } ] }, - "created": { - "type": "number" - }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": [ - "session.tool.success" - ] + "enum": ["session.next.tool.success"] }, "durable": { "type": "object", @@ -15534,17 +12739,15 @@ ] }, "version": { - "type": "number", - "enum": [ - 1 + "type": "integer", + "allOf": [ + { + "minimum": 1 + } ] } }, - "required": [ - "aggregateID", - "seq", - "version" - ], + "required": ["aggregateID", "seq", "version"], "additionalProperties": false }, "location": { @@ -15553,6 +12756,9 @@ "data": { "type": "object", "properties": { + "timestamp": { + "type": "number" + }, "sessionID": { "type": "string", "allOf": [ @@ -15581,38 +12787,41 @@ "$ref": "#/components/schemas/LLM.ToolContent" } }, + "outputPaths": { + "type": "array", + "items": { + "type": "string" + } + }, "result": {}, - "executed": { - "type": "boolean" - }, - "resultState": { - "$ref": "#/components/schemas/Session.Message.ProviderState6" + "provider": { + "type": "object", + "properties": { + "executed": { + "type": "boolean" + }, + "metadata": { + "$ref": "#/components/schemas/LLM.ProviderMetadata4" + } + }, + "required": ["executed"], + "additionalProperties": false } }, - "required": [ - "sessionID", - "assistantMessageID", - "callID", - "structured", - "content", - "executed" - ], + "required": ["timestamp", "sessionID", "assistantMessageID", "callID", "structured", "content", "provider"], "additionalProperties": false } }, - "required": [ - "id", - "created", - "type", - "durable", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, - "Session.Message.ProviderState7": { - "type": "object" + "LLM.ProviderMetadata5": { + "type": "object", + "additionalProperties": { + "type": "object" + } }, - "session.tool.failed": { + "session.next.tool.failed": { "type": "object", "properties": { "id": { @@ -15623,17 +12832,12 @@ } ] }, - "created": { - "type": "number" - }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": [ - "session.tool.failed" - ] + "enum": ["session.next.tool.failed"] }, "durable": { "type": "object", @@ -15650,17 +12854,15 @@ ] }, "version": { - "type": "number", - "enum": [ - 1 + "type": "integer", + "allOf": [ + { + "minimum": 1 + } ] } }, - "required": [ - "aggregateID", - "seq", - "version" - ], + "required": ["aggregateID", "seq", "version"], "additionalProperties": false }, "location": { @@ -15669,6 +12871,9 @@ "data": { "type": "object", "properties": { + "timestamp": { + "type": "number" + }, "sessionID": { "type": "string", "allOf": [ @@ -15689,36 +12894,37 @@ "type": "string" }, "error": { - "$ref": "#/components/schemas/Session.StructuredError" + "$ref": "#/components/schemas/Session.Error.Unknown" }, "result": {}, - "executed": { - "type": "boolean" - }, - "resultState": { - "$ref": "#/components/schemas/Session.Message.ProviderState7" + "provider": { + "type": "object", + "properties": { + "executed": { + "type": "boolean" + }, + "metadata": { + "$ref": "#/components/schemas/LLM.ProviderMetadata5" + } + }, + "required": ["executed"], + "additionalProperties": false } }, - "required": [ - "sessionID", - "assistantMessageID", - "callID", - "error", - "executed" - ], + "required": ["timestamp", "sessionID", "assistantMessageID", "callID", "error", "provider"], "additionalProperties": false } }, - "required": [ - "id", - "created", - "type", - "durable", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, - "session.retry.scheduled": { + "LLM.ProviderMetadata6": { + "type": "object", + "additionalProperties": { + "type": "object" + } + }, + "session.next.reasoning.started": { "type": "object", "properties": { "id": { @@ -15729,17 +12935,12 @@ } ] }, - "created": { - "type": "number" - }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": [ - "session.retry.scheduled" - ] + "enum": ["session.next.reasoning.started"] }, "durable": { "type": "object", @@ -15756,17 +12957,15 @@ ] }, "version": { - "type": "number", - "enum": [ - 1 + "type": "integer", + "allOf": [ + { + "minimum": 1 + } ] } }, - "required": [ - "aggregateID", - "seq", - "version" - ], + "required": ["aggregateID", "seq", "version"], "additionalProperties": false }, "location": { @@ -15775,6 +12974,9 @@ "data": { "type": "object", "properties": { + "timestamp": { + "type": "number" + }, "sessionID": { "type": "string", "allOf": [ @@ -15791,46 +12993,219 @@ } ] }, + "reasoningID": { + "type": "string" + }, + "providerMetadata": { + "$ref": "#/components/schemas/LLM.ProviderMetadata6" + } + }, + "required": ["timestamp", "sessionID", "assistantMessageID", "reasoningID"], + "additionalProperties": false + } + }, + "required": ["id", "type", "data"], + "additionalProperties": false + }, + "LLM.ProviderMetadata7": { + "type": "object", + "additionalProperties": { + "type": "object" + } + }, + "session.next.reasoning.ended": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": ["session.next.reasoning.ended"] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "integer", + "allOf": [ + { + "minimum": 1 + } + ] + } + }, + "required": ["aggregateID", "seq", "version"], + "additionalProperties": false + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "timestamp": { + "type": "number" + }, + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "assistantMessageID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, + "reasoningID": { + "type": "string" + }, + "text": { + "type": "string" + }, + "providerMetadata": { + "$ref": "#/components/schemas/LLM.ProviderMetadata7" + } + }, + "required": ["timestamp", "sessionID", "assistantMessageID", "reasoningID", "text"], + "additionalProperties": false + } + }, + "required": ["id", "type", "data"], + "additionalProperties": false + }, + "session.next.retry_error": { + "type": "object", + "properties": { + "message": { + "type": "string" + }, + "statusCode": { + "type": "number" + }, + "isRetryable": { + "type": "boolean" + }, + "responseHeaders": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "responseBody": { + "type": "string" + }, + "metadata": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "required": ["message", "isRetryable"], + "additionalProperties": false + }, + "session.next.retried": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": ["session.next.retried"] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "integer", + "allOf": [ + { + "minimum": 1 + } + ] + } + }, + "required": ["aggregateID", "seq", "version"], + "additionalProperties": false + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "timestamp": { + "type": "number" + }, + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, "attempt": { - "type": "integer", - "allOf": [ - { - "exclusiveMinimum": 0 - } - ] - }, - "at": { - "type": "integer", - "allOf": [ - { - "minimum": 0 - } - ] + "type": "number" }, "error": { - "$ref": "#/components/schemas/Session.StructuredError" + "$ref": "#/components/schemas/session.next.retry_error" } }, - "required": [ - "sessionID", - "assistantMessageID", - "attempt", - "at", - "error" - ], + "required": ["timestamp", "sessionID", "attempt", "error"], "additionalProperties": false } }, - "required": [ - "id", - "created", - "type", - "durable", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, - "session.compaction.admitted": { + "session.next.compaction.started": { "type": "object", "properties": { "id": { @@ -15841,17 +13216,12 @@ } ] }, - "created": { - "type": "number" - }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": [ - "session.compaction.admitted" - ] + "enum": ["session.next.compaction.started"] }, "durable": { "type": "object", @@ -15868,17 +13238,15 @@ ] }, "version": { - "type": "number", - "enum": [ - 1 + "type": "integer", + "allOf": [ + { + "minimum": 1 + } ] } }, - "required": [ - "aggregateID", - "seq", - "version" - ], + "required": ["aggregateID", "seq", "version"], "additionalProperties": false }, "location": { @@ -15887,6 +13255,9 @@ "data": { "type": "object", "properties": { + "timestamp": { + "type": "number" + }, "sessionID": { "type": "string", "allOf": [ @@ -15895,32 +13266,27 @@ } ] }, - "inputID": { + "messageID": { "type": "string", "allOf": [ { "pattern": "^msg_" } ] + }, + "reason": { + "type": "string", + "enum": ["auto", "manual"] } }, - "required": [ - "sessionID", - "inputID" - ], + "required": ["timestamp", "sessionID", "messageID", "reason"], "additionalProperties": false } }, - "required": [ - "id", - "created", - "type", - "durable", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, - "session.compaction.started": { + "session.next.compaction.ended": { "type": "object", "properties": { "id": { @@ -15931,17 +13297,12 @@ } ] }, - "created": { - "type": "number" - }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": [ - "session.compaction.started" - ] + "enum": ["session.next.compaction.ended"] }, "durable": { "type": "object", @@ -15958,17 +13319,15 @@ ] }, "version": { - "type": "number", - "enum": [ - 1 + "type": "integer", + "allOf": [ + { + "minimum": 1 + } ] } }, - "required": [ - "aggregateID", - "seq", - "version" - ], + "required": ["aggregateID", "seq", "version"], "additionalProperties": false }, "location": { @@ -15977,6 +13336,9 @@ "data": { "type": "object", "properties": { + "timestamp": { + "type": "number" + }, "sessionID": { "type": "string", "allOf": [ @@ -15985,113 +13347,17 @@ } ] }, - "reason": { - "type": "string", - "enum": [ - "auto", - "manual" - ] - }, - "recent": { - "type": "string" - }, - "inputID": { + "messageID": { "type": "string", "allOf": [ { "pattern": "^msg_" } ] - } - }, - "required": [ - "sessionID", - "reason", - "recent" - ], - "additionalProperties": false - } - }, - "required": [ - "id", - "created", - "type", - "durable", - "data" - ], - "additionalProperties": false - }, - "session.compaction.ended": { - "type": "object", - "properties": { - "id": { - "type": "string", - "allOf": [ - { - "pattern": "^evt_" - } - ] - }, - "created": { - "type": "number" - }, - "metadata": { - "type": "object" - }, - "type": { - "type": "string", - "enum": [ - "session.compaction.ended" - ] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { - "type": "integer", - "allOf": [ - { - "minimum": 0 - } - ] - }, - "version": { - "type": "number", - "enum": [ - 1 - ] - } - }, - "required": [ - "aggregateID", - "seq", - "version" - ], - "additionalProperties": false - }, - "location": { - "$ref": "#/components/schemas/Location.Ref" - }, - "data": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "allOf": [ - { - "pattern": "^ses" - } - ] }, "reason": { "type": "string", - "enum": [ - "auto", - "manual" - ] + "enum": ["auto", "manual"] }, "text": { "type": "string" @@ -16100,25 +13366,14 @@ "type": "string" } }, - "required": [ - "sessionID", - "reason", - "text", - "recent" - ], + "required": ["timestamp", "sessionID", "messageID", "reason", "text", "recent"], "additionalProperties": false } }, - "required": [ - "id", - "created", - "type", - "durable", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, - "session.compaction.failed": { + "session.next.revert.staged": { "type": "object", "properties": { "id": { @@ -16129,17 +13384,12 @@ } ] }, - "created": { - "type": "number" - }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": [ - "session.compaction.failed" - ] + "enum": ["session.next.revert.staged"] }, "durable": { "type": "object", @@ -16156,118 +13406,15 @@ ] }, "version": { - "type": "number", - "enum": [ - 1 - ] - } - }, - "required": [ - "aggregateID", - "seq", - "version" - ], - "additionalProperties": false - }, - "location": { - "$ref": "#/components/schemas/Location.Ref" - }, - "data": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "allOf": [ - { - "pattern": "^ses" - } - ] - }, - "reason": { - "type": "string", - "enum": [ - "auto", - "manual" - ] - }, - "error": { - "$ref": "#/components/schemas/Session.StructuredError" - }, - "inputID": { - "type": "string", - "allOf": [ - { - "pattern": "^msg_" - } - ] - } - }, - "required": [ - "sessionID", - "reason", - "error" - ], - "additionalProperties": false - } - }, - "required": [ - "id", - "created", - "type", - "durable", - "data" - ], - "additionalProperties": false - }, - "session.revert.staged": { - "type": "object", - "properties": { - "id": { - "type": "string", - "allOf": [ - { - "pattern": "^evt_" - } - ] - }, - "created": { - "type": "number" - }, - "metadata": { - "type": "object" - }, - "type": { - "type": "string", - "enum": [ - "session.revert.staged" - ] - }, - "durable": { - "type": "object", - "properties": { - "aggregateID": { - "type": "string" - }, - "seq": { "type": "integer", "allOf": [ { - "minimum": 0 + "minimum": 1 } ] - }, - "version": { - "type": "number", - "enum": [ - 1 - ] } }, - "required": [ - "aggregateID", - "seq", - "version" - ], + "required": ["aggregateID", "seq", "version"], "additionalProperties": false }, "location": { @@ -16276,6 +13423,9 @@ "data": { "type": "object", "properties": { + "timestamp": { + "type": "number" + }, "sessionID": { "type": "string", "allOf": [ @@ -16285,26 +13435,17 @@ ] }, "revert": { - "$ref": "#/components/schemas/Session.Revert" + "$ref": "#/components/schemas/Revert.State" } }, - "required": [ - "sessionID", - "revert" - ], + "required": ["timestamp", "sessionID", "revert"], "additionalProperties": false } }, - "required": [ - "id", - "created", - "type", - "durable", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, - "session.revert.cleared": { + "session.next.revert.cleared": { "type": "object", "properties": { "id": { @@ -16315,17 +13456,12 @@ } ] }, - "created": { - "type": "number" - }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": [ - "session.revert.cleared" - ] + "enum": ["session.next.revert.cleared"] }, "durable": { "type": "object", @@ -16342,17 +13478,15 @@ ] }, "version": { - "type": "number", - "enum": [ - 1 + "type": "integer", + "allOf": [ + { + "minimum": 1 + } ] } }, - "required": [ - "aggregateID", - "seq", - "version" - ], + "required": ["aggregateID", "seq", "version"], "additionalProperties": false }, "location": { @@ -16361,6 +13495,9 @@ "data": { "type": "object", "properties": { + "timestamp": { + "type": "number" + }, "sessionID": { "type": "string", "allOf": [ @@ -16370,22 +13507,14 @@ ] } }, - "required": [ - "sessionID" - ], + "required": ["timestamp", "sessionID"], "additionalProperties": false } }, - "required": [ - "id", - "created", - "type", - "durable", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, - "session.revert.committed": { + "session.next.revert.committed": { "type": "object", "properties": { "id": { @@ -16396,17 +13525,12 @@ } ] }, - "created": { - "type": "number" - }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": [ - "session.revert.committed" - ] + "enum": ["session.next.revert.committed"] }, "durable": { "type": "object", @@ -16423,17 +13547,15 @@ ] }, "version": { - "type": "number", - "enum": [ - 1 + "type": "integer", + "allOf": [ + { + "minimum": 1 + } ] } }, - "required": [ - "aggregateID", - "seq", - "version" - ], + "required": ["aggregateID", "seq", "version"], "additionalProperties": false }, "location": { @@ -16442,6 +13564,9 @@ "data": { "type": "object", "properties": { + "timestamp": { + "type": "number" + }, "sessionID": { "type": "string", "allOf": [ @@ -16450,7 +13575,7 @@ } ] }, - "to": { + "messageID": { "type": "string", "allOf": [ { @@ -16459,137 +13584,107 @@ ] } }, - "required": [ - "sessionID", - "to" - ], + "required": ["timestamp", "sessionID", "messageID"], "additionalProperties": false } }, - "required": [ - "id", - "created", - "type", - "durable", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, - "Session.Event.Durable": { + "SessionDurableEvent": { "oneOf": [ { - "$ref": "#/components/schemas/session.agent.selected" + "$ref": "#/components/schemas/session.next.agent.switched" }, { - "$ref": "#/components/schemas/session.model.selected" + "$ref": "#/components/schemas/session.next.model.switched" }, { - "$ref": "#/components/schemas/session.moved" + "$ref": "#/components/schemas/session.next.moved" }, { - "$ref": "#/components/schemas/session.renamed" + "$ref": "#/components/schemas/session.next.renamed" }, { - "$ref": "#/components/schemas/session.deleted" + "$ref": "#/components/schemas/session.next.forked" }, { - "$ref": "#/components/schemas/session.forked" + "$ref": "#/components/schemas/session.next.prompted" }, { - "$ref": "#/components/schemas/session.prompt.promoted" + "$ref": "#/components/schemas/session.next.prompt.admitted" }, { - "$ref": "#/components/schemas/session.prompt.admitted" + "$ref": "#/components/schemas/session.next.context.updated" }, { - "$ref": "#/components/schemas/session.execution.started" + "$ref": "#/components/schemas/session.next.synthetic" }, { - "$ref": "#/components/schemas/session.execution.succeeded" + "$ref": "#/components/schemas/session.next.skill.activated" }, { - "$ref": "#/components/schemas/session.execution.failed" + "$ref": "#/components/schemas/session.next.shell.started" }, { - "$ref": "#/components/schemas/session.execution.interrupted" + "$ref": "#/components/schemas/session.next.shell.ended" }, { - "$ref": "#/components/schemas/session.instructions.updated" + "$ref": "#/components/schemas/session.next.step.started" }, { - "$ref": "#/components/schemas/session.synthetic" + "$ref": "#/components/schemas/session.next.step.ended" }, { - "$ref": "#/components/schemas/session.skill.activated" + "$ref": "#/components/schemas/session.next.step.failed" }, { - "$ref": "#/components/schemas/session.shell.started" + "$ref": "#/components/schemas/session.next.text.started" }, { - "$ref": "#/components/schemas/session.shell.ended" + "$ref": "#/components/schemas/session.next.text.ended" }, { - "$ref": "#/components/schemas/session.step.started" + "$ref": "#/components/schemas/session.next.tool.input.started" }, { - "$ref": "#/components/schemas/session.step.ended" + "$ref": "#/components/schemas/session.next.tool.input.ended" }, { - "$ref": "#/components/schemas/session.step.failed" + "$ref": "#/components/schemas/session.next.tool.called" }, { - "$ref": "#/components/schemas/session.text.started" + "$ref": "#/components/schemas/session.next.tool.progress" }, { - "$ref": "#/components/schemas/session.text.ended" + "$ref": "#/components/schemas/session.next.tool.success" }, { - "$ref": "#/components/schemas/session.reasoning.started" + "$ref": "#/components/schemas/session.next.tool.failed" }, { - "$ref": "#/components/schemas/session.reasoning.ended" + "$ref": "#/components/schemas/session.next.reasoning.started" }, { - "$ref": "#/components/schemas/session.tool.input.started" + "$ref": "#/components/schemas/session.next.reasoning.ended" }, { - "$ref": "#/components/schemas/session.tool.input.ended" + "$ref": "#/components/schemas/session.next.retried" }, { - "$ref": "#/components/schemas/session.tool.called" + "$ref": "#/components/schemas/session.next.compaction.started" }, { - "$ref": "#/components/schemas/session.tool.progress" + "$ref": "#/components/schemas/session.next.compaction.ended" }, { - "$ref": "#/components/schemas/session.tool.success" + "$ref": "#/components/schemas/session.next.revert.staged" }, { - "$ref": "#/components/schemas/session.tool.failed" + "$ref": "#/components/schemas/session.next.revert.cleared" }, { - "$ref": "#/components/schemas/session.retry.scheduled" - }, - { - "$ref": "#/components/schemas/session.compaction.admitted" - }, - { - "$ref": "#/components/schemas/session.compaction.started" - }, - { - "$ref": "#/components/schemas/session.compaction.ended" - }, - { - "$ref": "#/components/schemas/session.compaction.failed" - }, - { - "$ref": "#/components/schemas/session.revert.staged" - }, - { - "$ref": "#/components/schemas/session.revert.cleared" - }, - { - "$ref": "#/components/schemas/session.revert.committed" + "$ref": "#/components/schemas/session.next.revert.committed" } ] }, @@ -16598,9 +13693,7 @@ "properties": { "type": { "type": "string", - "enum": [ - "log.synced" - ] + "enum": ["log.synced"] }, "aggregateID": { "type": "string" @@ -16614,17 +13707,14 @@ ] } }, - "required": [ - "type", - "aggregateID" - ], + "required": ["type", "aggregateID"], "additionalProperties": false, "description": "Marker emitted once when a log read reaches its captured watermark. The reader holds every event committed at or below seq." }, "SessionLogItem": { "anyOf": [ { - "$ref": "#/components/schemas/Session.Event.Durable" + "$ref": "#/components/schemas/SessionDurableEvent" }, { "$ref": "#/components/schemas/EventLog.Synced" @@ -16644,9 +13734,17 @@ "data": { "type": "array", "items": { - "$ref": "#/components/schemas/Session.Message.Info" + "$ref": "#/components/schemas/Session.Message" } }, + "watermark": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, "cursor": { "type": "object", "properties": { @@ -16674,12 +13772,56 @@ "additionalProperties": false } }, - "required": [ - "data", - "cursor" - ], + "required": ["data", "cursor"], "additionalProperties": false }, + "Model.Api": { + "anyOf": [ + { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "type": { + "type": "string", + "enum": ["aisdk"] + }, + "package": { + "type": "string" + }, + "url": { + "type": "string" + }, + "settings": { + "type": "object" + } + }, + "required": ["id", "type", "package"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "type": { + "type": "string", + "enum": ["native"] + }, + "url": { + "type": "string" + }, + "settings": { + "type": "object" + } + }, + "required": ["id", "type", "settings"], + "additionalProperties": false + } + ] + }, "Model.Capabilities": { "type": "object", "properties": { @@ -16699,40 +13841,9 @@ } } }, - "required": [ - "tools", - "input", - "output" - ], + "required": ["tools", "input", "output"], "additionalProperties": false }, - "Model.Variant": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "settings": { - "type": "object" - }, - "headers": { - "type": "object", - "additionalProperties": { - "type": "string" - } - }, - "body": { - "type": "object" - } - }, - "required": [ - "id" - ], - "additionalProperties": false - }, - "Money.USDPerMillionTokens": { - "type": "number" - }, "Model.Cost": { "type": "object", "properties": { @@ -16741,59 +13852,44 @@ "properties": { "type": { "type": "string", - "enum": [ - "context" - ] + "enum": ["context"] }, "size": { "type": "integer" } }, - "required": [ - "type", - "size" - ], + "required": ["type", "size"], "additionalProperties": false }, "input": { - "$ref": "#/components/schemas/Money.USDPerMillionTokens" + "type": "number" }, "output": { - "$ref": "#/components/schemas/Money.USDPerMillionTokens" + "type": "number" }, "cache": { "type": "object", "properties": { "read": { - "$ref": "#/components/schemas/Money.USDPerMillionTokens" + "type": "number" }, "write": { - "$ref": "#/components/schemas/Money.USDPerMillionTokens" + "type": "number" } }, - "required": [ - "read", - "write" - ], + "required": ["read", "write"], "additionalProperties": false } }, - "required": [ - "input", - "output", - "cache" - ], + "required": ["input", "output", "cache"], "additionalProperties": false }, - "Model.Info": { + "ModelV2.Info": { "type": "object", "properties": { "id": { "type": "string" }, - "modelID": { - "type": "string" - }, "providerID": { "type": "string" }, @@ -16803,28 +13899,57 @@ "name": { "type": "string" }, - "package": { - "type": "string" - }, - "settings": { - "type": "object" - }, - "headers": { - "type": "object", - "additionalProperties": { - "type": "string" - } - }, - "body": { - "type": "object" + "api": { + "$ref": "#/components/schemas/Model.Api" }, "capabilities": { "$ref": "#/components/schemas/Model.Capabilities" }, + "request": { + "type": "object", + "properties": { + "settings": { + "$ref": "#/components/schemas/Provider.Settings" + }, + "headers": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "body": { + "type": "object" + }, + "variant": { + "type": "string" + } + }, + "required": ["settings", "headers", "body"], + "additionalProperties": false + }, "variants": { "type": "array", "items": { - "$ref": "#/components/schemas/Model.Variant" + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "settings": { + "$ref": "#/components/schemas/Provider.Settings" + }, + "headers": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "body": { + "type": "object" + } + }, + "required": ["id", "settings", "headers", "body"], + "additionalProperties": false } }, "time": { @@ -16834,9 +13959,7 @@ "type": "number" } }, - "required": [ - "released" - ], + "required": ["released"], "additionalProperties": false }, "cost": { @@ -16847,12 +13970,7 @@ }, "status": { "type": "string", - "enum": [ - "alpha", - "beta", - "deprecated", - "active" - ] + "enum": ["alpha", "beta", "deprecated", "active"] }, "enabled": { "type": "boolean" @@ -16870,19 +13988,17 @@ "type": "integer" } }, - "required": [ - "context", - "output" - ], + "required": ["context", "output"], "additionalProperties": false } }, "required": [ "id", - "modelID", "providerID", "name", + "api", "capabilities", + "request", "variants", "time", "cost", @@ -16902,17 +14018,60 @@ "type": "string" } }, - "required": [ - "text" - ], + "required": ["text"], "additionalProperties": false } }, - "required": [ - "data" - ], + "required": ["data"], "additionalProperties": false }, + "Provider.AISDK": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["aisdk"] + }, + "package": { + "type": "string" + }, + "url": { + "type": "string" + }, + "settings": { + "type": "object" + } + }, + "required": ["type", "package"], + "additionalProperties": false + }, + "Provider.Native": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["native"] + }, + "url": { + "type": "string" + }, + "settings": { + "type": "object" + } + }, + "required": ["type", "settings"], + "additionalProperties": false + }, + "Provider.Api": { + "anyOf": [ + { + "$ref": "#/components/schemas/Provider.AISDK" + }, + { + "$ref": "#/components/schemas/Provider.Native" + } + ] + }, "ProviderV2.Info": { "type": "object", "properties": { @@ -16928,27 +14087,14 @@ "disabled": { "type": "boolean" }, - "package": { - "type": "string" + "api": { + "$ref": "#/components/schemas/Provider.Api" }, - "settings": { - "type": "object" - }, - "headers": { - "type": "object", - "additionalProperties": { - "type": "string" - } - }, - "body": { - "type": "object" + "request": { + "$ref": "#/components/schemas/Provider.Request" } }, - "required": [ - "id", - "name", - "package" - ], + "required": ["id", "name", "api", "request"], "additionalProperties": false }, "ProviderNotFoundError": { @@ -16956,9 +14102,7 @@ "properties": { "_tag": { "type": "string", - "enum": [ - "ProviderNotFoundError" - ] + "enum": ["ProviderNotFoundError"] }, "providerID": { "type": "string" @@ -16967,11 +14111,7 @@ "type": "string" } }, - "required": [ - "_tag", - "providerID", - "message" - ], + "required": ["_tag", "providerID", "message"], "additionalProperties": false }, "Integration.When": { @@ -16982,20 +14122,13 @@ }, "op": { "type": "string", - "enum": [ - "eq", - "neq" - ] + "enum": ["eq", "neq"] }, "value": { "type": "string" } }, - "required": [ - "key", - "op", - "value" - ], + "required": ["key", "op", "value"], "additionalProperties": false }, "Integration.TextPrompt": { @@ -17003,9 +14136,7 @@ "properties": { "type": { "type": "string", - "enum": [ - "text" - ] + "enum": ["text"] }, "key": { "type": "string" @@ -17020,11 +14151,7 @@ "$ref": "#/components/schemas/Integration.When" } }, - "required": [ - "type", - "key", - "message" - ], + "required": ["type", "key", "message"], "additionalProperties": false }, "Integration.SelectPrompt": { @@ -17032,9 +14159,7 @@ "properties": { "type": { "type": "string", - "enum": [ - "select" - ] + "enum": ["select"] }, "key": { "type": "string" @@ -17057,10 +14182,7 @@ "type": "string" } }, - "required": [ - "label", - "value" - ], + "required": ["label", "value"], "additionalProperties": false } }, @@ -17068,12 +14190,7 @@ "$ref": "#/components/schemas/Integration.When" } }, - "required": [ - "type", - "key", - "message", - "options" - ], + "required": ["type", "key", "message", "options"], "additionalProperties": false }, "Integration.OAuthMethod": { @@ -17084,9 +14201,7 @@ }, "type": { "type": "string", - "enum": [ - "oauth" - ] + "enum": ["oauth"] }, "label": { "type": "string" @@ -17105,11 +14220,7 @@ } } }, - "required": [ - "id", - "type", - "label" - ], + "required": ["id", "type", "label"], "additionalProperties": false }, "Integration.KeyMethod": { @@ -17117,17 +14228,13 @@ "properties": { "type": { "type": "string", - "enum": [ - "key" - ] + "enum": ["key"] }, "label": { "type": "string" } }, - "required": [ - "type" - ], + "required": ["type"], "additionalProperties": false }, "Integration.EnvMethod": { @@ -17135,9 +14242,7 @@ "properties": { "type": { "type": "string", - "enum": [ - "env" - ] + "enum": ["env"] }, "names": { "type": "array", @@ -17146,10 +14251,7 @@ } } }, - "required": [ - "type", - "names" - ], + "required": ["type", "names"], "additionalProperties": false }, "Integration.Method": { @@ -17170,9 +14272,7 @@ "properties": { "type": { "type": "string", - "enum": [ - "credential" - ] + "enum": ["credential"] }, "id": { "type": "string" @@ -17181,11 +14281,7 @@ "type": "string" } }, - "required": [ - "type", - "id", - "label" - ], + "required": ["type", "id", "label"], "additionalProperties": false }, "Connection.EnvInfo": { @@ -17193,18 +14289,13 @@ "properties": { "type": { "type": "string", - "enum": [ - "env" - ] + "enum": ["env"] }, "name": { "type": "string" } }, - "required": [ - "type", - "name" - ], + "required": ["type", "name"], "additionalProperties": false }, "Connection.Info": { @@ -17239,12 +14330,7 @@ } } }, - "required": [ - "id", - "name", - "methods", - "connections" - ], + "required": ["id", "name", "methods", "connections"], "additionalProperties": false }, "Integration.Attempt": { @@ -17261,10 +14347,7 @@ }, "mode": { "type": "string", - "enum": [ - "auto", - "code" - ] + "enum": ["auto", "code"] }, "time": { "type": "object", @@ -17278,31 +14361,21 @@ }, { "type": "string", - "enum": [ - "NaN" - ] + "enum": ["NaN"] }, { "type": "string", - "enum": [ - "Infinity" - ] + "enum": ["Infinity"] }, { "type": "string", - "enum": [ - "-Infinity" - ] + "enum": ["-Infinity"] } ] }, { "type": "string", - "enum": [ - "Infinity", - "-Infinity", - "NaN" - ] + "enum": ["Infinity", "-Infinity", "NaN"] } ] }, @@ -17315,49 +14388,30 @@ }, { "type": "string", - "enum": [ - "NaN" - ] + "enum": ["NaN"] }, { "type": "string", - "enum": [ - "Infinity" - ] + "enum": ["Infinity"] }, { "type": "string", - "enum": [ - "-Infinity" - ] + "enum": ["-Infinity"] } ] }, { "type": "string", - "enum": [ - "Infinity", - "-Infinity", - "NaN" - ] + "enum": ["Infinity", "-Infinity", "NaN"] } ] } }, - "required": [ - "created", - "expires" - ], + "required": ["created", "expires"], "additionalProperties": false } }, - "required": [ - "attemptID", - "url", - "instructions", - "mode", - "time" - ], + "required": ["attemptID", "url", "instructions", "mode", "time"], "additionalProperties": false }, "Integration.AttemptStatus": { @@ -17367,9 +14421,7 @@ "properties": { "status": { "type": "string", - "enum": [ - "pending" - ] + "enum": ["pending"] }, "time": { "type": "object", @@ -17383,31 +14435,21 @@ }, { "type": "string", - "enum": [ - "NaN" - ] + "enum": ["NaN"] }, { "type": "string", - "enum": [ - "Infinity" - ] + "enum": ["Infinity"] }, { "type": "string", - "enum": [ - "-Infinity" - ] + "enum": ["-Infinity"] } ] }, { "type": "string", - "enum": [ - "Infinity", - "-Infinity", - "NaN" - ] + "enum": ["Infinity", "-Infinity", "NaN"] } ] }, @@ -17420,46 +14462,30 @@ }, { "type": "string", - "enum": [ - "NaN" - ] + "enum": ["NaN"] }, { "type": "string", - "enum": [ - "Infinity" - ] + "enum": ["Infinity"] }, { "type": "string", - "enum": [ - "-Infinity" - ] + "enum": ["-Infinity"] } ] }, { "type": "string", - "enum": [ - "Infinity", - "-Infinity", - "NaN" - ] + "enum": ["Infinity", "-Infinity", "NaN"] } ] } }, - "required": [ - "created", - "expires" - ], + "required": ["created", "expires"], "additionalProperties": false } }, - "required": [ - "status", - "time" - ], + "required": ["status", "time"], "additionalProperties": false }, { @@ -17467,9 +14493,7 @@ "properties": { "status": { "type": "string", - "enum": [ - "complete" - ] + "enum": ["complete"] }, "time": { "type": "object", @@ -17483,31 +14507,21 @@ }, { "type": "string", - "enum": [ - "NaN" - ] + "enum": ["NaN"] }, { "type": "string", - "enum": [ - "Infinity" - ] + "enum": ["Infinity"] }, { "type": "string", - "enum": [ - "-Infinity" - ] + "enum": ["-Infinity"] } ] }, { "type": "string", - "enum": [ - "Infinity", - "-Infinity", - "NaN" - ] + "enum": ["Infinity", "-Infinity", "NaN"] } ] }, @@ -17520,46 +14534,30 @@ }, { "type": "string", - "enum": [ - "NaN" - ] + "enum": ["NaN"] }, { "type": "string", - "enum": [ - "Infinity" - ] + "enum": ["Infinity"] }, { "type": "string", - "enum": [ - "-Infinity" - ] + "enum": ["-Infinity"] } ] }, { "type": "string", - "enum": [ - "Infinity", - "-Infinity", - "NaN" - ] + "enum": ["Infinity", "-Infinity", "NaN"] } ] } }, - "required": [ - "created", - "expires" - ], + "required": ["created", "expires"], "additionalProperties": false } }, - "required": [ - "status", - "time" - ], + "required": ["status", "time"], "additionalProperties": false }, { @@ -17567,9 +14565,7 @@ "properties": { "status": { "type": "string", - "enum": [ - "failed" - ] + "enum": ["failed"] }, "message": { "type": "string" @@ -17586,31 +14582,21 @@ }, { "type": "string", - "enum": [ - "NaN" - ] + "enum": ["NaN"] }, { "type": "string", - "enum": [ - "Infinity" - ] + "enum": ["Infinity"] }, { "type": "string", - "enum": [ - "-Infinity" - ] + "enum": ["-Infinity"] } ] }, { "type": "string", - "enum": [ - "Infinity", - "-Infinity", - "NaN" - ] + "enum": ["Infinity", "-Infinity", "NaN"] } ] }, @@ -17623,47 +14609,30 @@ }, { "type": "string", - "enum": [ - "NaN" - ] + "enum": ["NaN"] }, { "type": "string", - "enum": [ - "Infinity" - ] + "enum": ["Infinity"] }, { "type": "string", - "enum": [ - "-Infinity" - ] + "enum": ["-Infinity"] } ] }, { "type": "string", - "enum": [ - "Infinity", - "-Infinity", - "NaN" - ] + "enum": ["Infinity", "-Infinity", "NaN"] } ] } }, - "required": [ - "created", - "expires" - ], + "required": ["created", "expires"], "additionalProperties": false } }, - "required": [ - "status", - "message", - "time" - ], + "required": ["status", "message", "time"], "additionalProperties": false }, { @@ -17671,9 +14640,7 @@ "properties": { "status": { "type": "string", - "enum": [ - "expired" - ] + "enum": ["expired"] }, "time": { "type": "object", @@ -17687,31 +14654,21 @@ }, { "type": "string", - "enum": [ - "NaN" - ] + "enum": ["NaN"] }, { "type": "string", - "enum": [ - "Infinity" - ] + "enum": ["Infinity"] }, { "type": "string", - "enum": [ - "-Infinity" - ] + "enum": ["-Infinity"] } ] }, { "type": "string", - "enum": [ - "Infinity", - "-Infinity", - "NaN" - ] + "enum": ["Infinity", "-Infinity", "NaN"] } ] }, @@ -17724,46 +14681,30 @@ }, { "type": "string", - "enum": [ - "NaN" - ] + "enum": ["NaN"] }, { "type": "string", - "enum": [ - "Infinity" - ] + "enum": ["Infinity"] }, { "type": "string", - "enum": [ - "-Infinity" - ] + "enum": ["-Infinity"] } ] }, { "type": "string", - "enum": [ - "Infinity", - "-Infinity", - "NaN" - ] + "enum": ["Infinity", "-Infinity", "NaN"] } ] } }, - "required": [ - "created", - "expires" - ], + "required": ["created", "expires"], "additionalProperties": false } }, - "required": [ - "status", - "time" - ], + "required": ["status", "time"], "additionalProperties": false } ] @@ -17773,29 +14714,21 @@ "properties": { "status": { "type": "string", - "enum": [ - "connected" - ] + "enum": ["connected"] } }, - "required": [ - "status" - ], + "required": ["status"], "additionalProperties": false }, - "Mcp.Status.Pending": { + "Mcp.Status.Disconnected": { "type": "object", "properties": { "status": { "type": "string", - "enum": [ - "pending" - ] + "enum": ["disconnected"] } }, - "required": [ - "status" - ], + "required": ["status"], "additionalProperties": false }, "Mcp.Status.Disabled": { @@ -17803,14 +14736,10 @@ "properties": { "status": { "type": "string", - "enum": [ - "disabled" - ] + "enum": ["disabled"] } }, - "required": [ - "status" - ], + "required": ["status"], "additionalProperties": false }, "Mcp.Status.Failed": { @@ -17818,18 +14747,13 @@ "properties": { "status": { "type": "string", - "enum": [ - "failed" - ] + "enum": ["failed"] }, "error": { "type": "string" } }, - "required": [ - "status", - "error" - ], + "required": ["status", "error"], "additionalProperties": false }, "Mcp.Status.NeedsAuth": { @@ -17837,14 +14761,10 @@ "properties": { "status": { "type": "string", - "enum": [ - "needs_auth" - ] + "enum": ["needs_auth"] } }, - "required": [ - "status" - ], + "required": ["status"], "additionalProperties": false }, "Mcp.Status.NeedsClientRegistration": { @@ -17852,18 +14772,13 @@ "properties": { "status": { "type": "string", - "enum": [ - "needs_client_registration" - ] + "enum": ["needs_client_registration"] }, "error": { "type": "string" } }, - "required": [ - "status", - "error" - ], + "required": ["status", "error"], "additionalProperties": false }, "Mcp.Server": { @@ -17878,7 +14793,7 @@ "$ref": "#/components/schemas/Mcp.Status.Connected" }, { - "$ref": "#/components/schemas/Mcp.Status.Pending" + "$ref": "#/components/schemas/Mcp.Status.Disconnected" }, { "$ref": "#/components/schemas/Mcp.Status.Disabled" @@ -17898,189 +14813,7 @@ "type": "string" } }, - "required": [ - "name", - "status" - ], - "additionalProperties": false - }, - "Mcp.Resource": { - "type": "object", - "properties": { - "server": { - "type": "string" - }, - "name": { - "type": "string" - }, - "uri": { - "type": "string" - }, - "description": { - "type": "string" - }, - "mimeType": { - "type": "string" - } - }, - "required": [ - "server", - "name", - "uri" - ], - "additionalProperties": false - }, - "Mcp.ResourceTemplate": { - "type": "object", - "properties": { - "server": { - "type": "string" - }, - "name": { - "type": "string" - }, - "uriTemplate": { - "type": "string" - }, - "description": { - "type": "string" - }, - "mimeType": { - "type": "string" - } - }, - "required": [ - "server", - "name", - "uriTemplate" - ], - "additionalProperties": false - }, - "Mcp.ResourceCatalog": { - "type": "object", - "properties": { - "resources": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Mcp.Resource" - } - }, - "templates": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Mcp.ResourceTemplate" - } - } - }, - "required": [ - "resources", - "templates" - ], - "additionalProperties": false - }, - "Project.Vcs": { - "type": "string", - "enum": [ - "git", - "hg" - ] - }, - "Project.Icon": { - "type": "object", - "properties": { - "url": { - "type": "string" - }, - "override": { - "type": "string" - }, - "color": { - "type": "string" - } - }, - "additionalProperties": false - }, - "Project.Commands": { - "type": "object", - "properties": { - "start": { - "type": "string", - "description": "Startup script to run when creating a new workspace (worktree)" - } - }, - "additionalProperties": false - }, - "Project.Time": { - "type": "object", - "properties": { - "created": { - "type": "integer", - "allOf": [ - { - "minimum": 0 - } - ] - }, - "updated": { - "type": "integer", - "allOf": [ - { - "minimum": 0 - } - ] - }, - "initialized": { - "type": "integer", - "allOf": [ - { - "minimum": 0 - } - ] - } - }, - "required": [ - "created", - "updated" - ], - "additionalProperties": false - }, - "Project": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "worktree": { - "type": "string" - }, - "vcs": { - "$ref": "#/components/schemas/Project.Vcs" - }, - "name": { - "type": "string" - }, - "icon": { - "$ref": "#/components/schemas/Project.Icon" - }, - "commands": { - "$ref": "#/components/schemas/Project.Commands" - }, - "time": { - "$ref": "#/components/schemas/Project.Time" - }, - "sandboxes": { - "type": "array", - "items": { - "type": "string" - } - } - }, - "required": [ - "id", - "worktree", - "time", - "sandboxes" - ], + "required": ["name", "status"], "additionalProperties": false }, "Project.Current": { @@ -18093,10 +14826,7 @@ "type": "string" } }, - "required": [ - "id", - "directory" - ], + "required": ["id", "directory"], "additionalProperties": false }, "Project.Directory": { @@ -18109,9 +14839,7 @@ "type": "string" } }, - "required": [ - "directory" - ], + "required": ["directory"], "additionalProperties": false }, "Project.Directories": { @@ -18131,10 +14859,7 @@ }, "op": { "type": "string", - "enum": [ - "eq", - "neq" - ] + "enum": ["eq", "neq"] }, "value": { "anyOf": [ @@ -18150,31 +14875,21 @@ }, { "type": "string", - "enum": [ - "NaN" - ] + "enum": ["NaN"] }, { "type": "string", - "enum": [ - "Infinity" - ] + "enum": ["Infinity"] }, { "type": "string", - "enum": [ - "-Infinity" - ] + "enum": ["-Infinity"] } ] }, { "type": "string", - "enum": [ - "Infinity", - "-Infinity", - "NaN" - ] + "enum": ["Infinity", "-Infinity", "NaN"] } ] }, @@ -18184,11 +14899,7 @@ ] } }, - "required": [ - "key", - "op", - "value" - ], + "required": ["key", "op", "value"], "additionalProperties": false }, "Form.Option": { @@ -18204,10 +14915,7 @@ "type": "string" } }, - "required": [ - "value", - "label" - ], + "required": ["value", "label"], "additionalProperties": false }, "Form.StringField": { @@ -18233,18 +14941,11 @@ }, "type": { "type": "string", - "enum": [ - "string" - ] + "enum": ["string"] }, "format": { "type": "string", - "enum": [ - "email", - "uri", - "date", - "date-time" - ] + "enum": ["email", "uri", "date", "date-time"] }, "minLength": { "type": "integer", @@ -18281,10 +14982,7 @@ "type": "boolean" } }, - "required": [ - "key", - "type" - ], + "required": ["key", "type"], "additionalProperties": false }, "Form.NumberField": { @@ -18310,9 +15008,7 @@ }, "type": { "type": "string", - "enum": [ - "number" - ] + "enum": ["number"] }, "minimum": { "anyOf": [ @@ -18323,31 +15019,21 @@ }, { "type": "string", - "enum": [ - "NaN" - ] + "enum": ["NaN"] }, { "type": "string", - "enum": [ - "Infinity" - ] + "enum": ["Infinity"] }, { "type": "string", - "enum": [ - "-Infinity" - ] + "enum": ["-Infinity"] } ] }, { "type": "string", - "enum": [ - "Infinity", - "-Infinity", - "NaN" - ] + "enum": ["Infinity", "-Infinity", "NaN"] } ] }, @@ -18360,31 +15046,21 @@ }, { "type": "string", - "enum": [ - "NaN" - ] + "enum": ["NaN"] }, { "type": "string", - "enum": [ - "Infinity" - ] + "enum": ["Infinity"] }, { "type": "string", - "enum": [ - "-Infinity" - ] + "enum": ["-Infinity"] } ] }, { "type": "string", - "enum": [ - "Infinity", - "-Infinity", - "NaN" - ] + "enum": ["Infinity", "-Infinity", "NaN"] } ] }, @@ -18397,39 +15073,26 @@ }, { "type": "string", - "enum": [ - "NaN" - ] + "enum": ["NaN"] }, { "type": "string", - "enum": [ - "Infinity" - ] + "enum": ["Infinity"] }, { "type": "string", - "enum": [ - "-Infinity" - ] + "enum": ["-Infinity"] } ] }, { "type": "string", - "enum": [ - "Infinity", - "-Infinity", - "NaN" - ] + "enum": ["Infinity", "-Infinity", "NaN"] } ] } }, - "required": [ - "key", - "type" - ], + "required": ["key", "type"], "additionalProperties": false }, "Form.IntegerField": { @@ -18455,9 +15118,7 @@ }, "type": { "type": "string", - "enum": [ - "integer" - ] + "enum": ["integer"] }, "minimum": { "anyOf": [ @@ -18468,31 +15129,21 @@ }, { "type": "string", - "enum": [ - "NaN" - ] + "enum": ["NaN"] }, { "type": "string", - "enum": [ - "Infinity" - ] + "enum": ["Infinity"] }, { "type": "string", - "enum": [ - "-Infinity" - ] + "enum": ["-Infinity"] } ] }, { "type": "string", - "enum": [ - "Infinity", - "-Infinity", - "NaN" - ] + "enum": ["Infinity", "-Infinity", "NaN"] } ] }, @@ -18505,31 +15156,21 @@ }, { "type": "string", - "enum": [ - "NaN" - ] + "enum": ["NaN"] }, { "type": "string", - "enum": [ - "Infinity" - ] + "enum": ["Infinity"] }, { "type": "string", - "enum": [ - "-Infinity" - ] + "enum": ["-Infinity"] } ] }, { "type": "string", - "enum": [ - "Infinity", - "-Infinity", - "NaN" - ] + "enum": ["Infinity", "-Infinity", "NaN"] } ] }, @@ -18542,39 +15183,26 @@ }, { "type": "string", - "enum": [ - "NaN" - ] + "enum": ["NaN"] }, { "type": "string", - "enum": [ - "Infinity" - ] + "enum": ["Infinity"] }, { "type": "string", - "enum": [ - "-Infinity" - ] + "enum": ["-Infinity"] } ] }, { "type": "string", - "enum": [ - "Infinity", - "-Infinity", - "NaN" - ] + "enum": ["Infinity", "-Infinity", "NaN"] } ] } }, - "required": [ - "key", - "type" - ], + "required": ["key", "type"], "additionalProperties": false }, "Form.BooleanField": { @@ -18600,18 +15228,13 @@ }, "type": { "type": "string", - "enum": [ - "boolean" - ] + "enum": ["boolean"] }, "default": { "type": "boolean" } }, - "required": [ - "key", - "type" - ], + "required": ["key", "type"], "additionalProperties": false }, "Form.MultiselectField": { @@ -18637,9 +15260,7 @@ }, "type": { "type": "string", - "enum": [ - "multiselect" - ] + "enum": ["multiselect"] }, "options": { "type": "array", @@ -18673,11 +15294,7 @@ } } }, - "required": [ - "key", - "type", - "options" - ], + "required": ["key", "type", "options"], "additionalProperties": false }, "Form.FormInfo": { @@ -18702,9 +15319,7 @@ }, "mode": { "type": "string", - "enum": [ - "form" - ] + "enum": ["form"] }, "fields": { "type": "array", @@ -18729,13 +15344,7 @@ } } }, - "required": [ - "id", - "sessionID", - "title", - "mode", - "fields" - ], + "required": ["id", "sessionID", "mode", "fields"], "additionalProperties": false }, "Form.UrlInfo": { @@ -18760,21 +15369,13 @@ }, "mode": { "type": "string", - "enum": [ - "url" - ] + "enum": ["url"] }, "url": { "type": "string" } }, - "required": [ - "id", - "sessionID", - "title", - "mode", - "url" - ], + "required": ["id", "sessionID", "mode", "url"], "additionalProperties": false }, "Form.CreatePayload": { @@ -18803,10 +15404,7 @@ }, "mode": { "type": "string", - "enum": [ - "form", - "url" - ] + "enum": ["form", "url"] }, "fields": { "anyOf": [ @@ -18848,10 +15446,7 @@ ] } }, - "required": [ - "title", - "mode" - ], + "required": ["mode"], "additionalProperties": false }, "FormNotFoundError": { @@ -18859,9 +15454,7 @@ "properties": { "_tag": { "type": "string", - "enum": [ - "FormNotFoundError" - ] + "enum": ["FormNotFoundError"] }, "id": { "type": "string" @@ -18870,11 +15463,7 @@ "type": "string" } }, - "required": [ - "_tag", - "id", - "message" - ], + "required": ["_tag", "id", "message"], "additionalProperties": false }, "Form.Value": { @@ -18891,31 +15480,21 @@ }, { "type": "string", - "enum": [ - "NaN" - ] + "enum": ["NaN"] }, { "type": "string", - "enum": [ - "Infinity" - ] + "enum": ["Infinity"] }, { "type": "string", - "enum": [ - "-Infinity" - ] + "enum": ["-Infinity"] } ] }, { "type": "string", - "enum": [ - "Infinity", - "-Infinity", - "NaN" - ] + "enum": ["Infinity", "-Infinity", "NaN"] } ] }, @@ -18943,14 +15522,10 @@ "properties": { "status": { "type": "string", - "enum": [ - "pending" - ] + "enum": ["pending"] } }, - "required": [ - "status" - ], + "required": ["status"], "additionalProperties": false }, { @@ -18958,18 +15533,13 @@ "properties": { "status": { "type": "string", - "enum": [ - "answered" - ] + "enum": ["answered"] }, "answer": { "$ref": "#/components/schemas/Form.Answer" } }, - "required": [ - "status", - "answer" - ], + "required": ["status", "answer"], "additionalProperties": false }, { @@ -18977,14 +15547,10 @@ "properties": { "status": { "type": "string", - "enum": [ - "cancelled" - ] + "enum": ["cancelled"] } }, - "required": [ - "status" - ], + "required": ["status"], "additionalProperties": false } ] @@ -18996,9 +15562,7 @@ "$ref": "#/components/schemas/Form.Answer" } }, - "required": [ - "answer" - ], + "required": ["answer"], "additionalProperties": false }, "FormAlreadySettledError": { @@ -19006,9 +15570,7 @@ "properties": { "_tag": { "type": "string", - "enum": [ - "FormAlreadySettledError" - ] + "enum": ["FormAlreadySettledError"] }, "id": { "type": "string" @@ -19017,11 +15579,7 @@ "type": "string" } }, - "required": [ - "_tag", - "id", - "message" - ], + "required": ["_tag", "id", "message"], "additionalProperties": false }, "FormInvalidAnswerError": { @@ -19029,9 +15587,7 @@ "properties": { "_tag": { "type": "string", - "enum": [ - "FormInvalidAnswerError" - ] + "enum": ["FormInvalidAnswerError"] }, "id": { "type": "string" @@ -19040,11 +15596,7 @@ "type": "string" } }, - "required": [ - "_tag", - "id", - "message" - ], + "required": ["_tag", "id", "message"], "additionalProperties": false }, "PermissionV2.Source": { @@ -19054,9 +15606,7 @@ "properties": { "type": { "type": "string", - "enum": [ - "tool" - ] + "enum": ["tool"] }, "messageID": { "type": "string" @@ -19065,11 +15615,7 @@ "type": "string" } }, - "required": [ - "type", - "messageID", - "callID" - ], + "required": ["type", "messageID", "callID"], "additionalProperties": false } ] @@ -19115,12 +15661,7 @@ "$ref": "#/components/schemas/PermissionV2.Source" } }, - "required": [ - "id", - "sessionID", - "action", - "resources" - ], + "required": ["id", "sessionID", "action", "resources"], "additionalProperties": false }, "PermissionSaved.Info": { @@ -19139,12 +15680,7 @@ "type": "string" } }, - "required": [ - "id", - "projectID", - "action", - "resource" - ], + "required": ["id", "projectID", "action", "resource"], "additionalProperties": false }, "PermissionNotFoundError": { @@ -19152,9 +15688,7 @@ "properties": { "_tag": { "type": "string", - "enum": [ - "PermissionNotFoundError" - ] + "enum": ["PermissionNotFoundError"] }, "requestID": { "type": "string" @@ -19163,20 +15697,12 @@ "type": "string" } }, - "required": [ - "_tag", - "requestID", - "message" - ], + "required": ["_tag", "requestID", "message"], "additionalProperties": false }, "PermissionV2.Reply": { "type": "string", - "enum": [ - "once", - "always", - "reject" - ] + "enum": ["once", "always", "reject"] }, "FileSystem.Entry": { "type": "object", @@ -19186,19 +15712,13 @@ }, "type": { "type": "string", - "enum": [ - "file", - "directory" - ] + "enum": ["file", "directory"] } }, - "required": [ - "path", - "type" - ], + "required": ["path", "type"], "additionalProperties": false }, - "Command.Info": { + "CommandV2.Info": { "type": "object", "properties": { "name": { @@ -19220,18 +15740,12 @@ "type": "boolean" } }, - "required": [ - "name", - "template" - ], + "required": ["name", "template"], "additionalProperties": false }, - "Skill.Info": { + "SkillV2.Info": { "type": "object", "properties": { - "id": { - "type": "string" - }, "name": { "type": "string" }, @@ -19251,12 +15765,7 @@ "type": "string" } }, - "required": [ - "id", - "name", - "location", - "content" - ], + "required": ["name", "location", "content"], "additionalProperties": false }, "models-dev.refreshed": { @@ -19270,17 +15779,38 @@ } ] }, - "created": { - "type": "number" - }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": [ - "models-dev.refreshed" - ] + "enum": ["models-dev.refreshed"] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "integer", + "allOf": [ + { + "minimum": 1 + } + ] + } + }, + "required": ["aggregateID", "seq", "version"], + "additionalProperties": false }, "location": { "$ref": "#/components/schemas/Location.Ref" @@ -19296,12 +15826,7 @@ ] } }, - "required": [ - "id", - "created", - "type", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, "integration.updated": { @@ -19315,17 +15840,38 @@ } ] }, - "created": { - "type": "number" - }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": [ - "integration.updated" - ] + "enum": ["integration.updated"] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "integer", + "allOf": [ + { + "minimum": 1 + } + ] + } + }, + "required": ["aggregateID", "seq", "version"], + "additionalProperties": false }, "location": { "$ref": "#/components/schemas/Location.Ref" @@ -19341,12 +15887,7 @@ ] } }, - "required": [ - "id", - "created", - "type", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, "integration.connection.updated": { @@ -19360,17 +15901,38 @@ } ] }, - "created": { - "type": "number" - }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": [ - "integration.connection.updated" - ] + "enum": ["integration.connection.updated"] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "integer", + "allOf": [ + { + "minimum": 1 + } + ] + } + }, + "required": ["aggregateID", "seq", "version"], + "additionalProperties": false }, "location": { "$ref": "#/components/schemas/Location.Ref" @@ -19382,18 +15944,11 @@ "type": "string" } }, - "required": [ - "integrationID" - ], + "required": ["integrationID"], "additionalProperties": false } }, - "required": [ - "id", - "created", - "type", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, "catalog.updated": { @@ -19407,17 +15962,38 @@ } ] }, - "created": { - "type": "number" - }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": [ - "catalog.updated" - ] + "enum": ["catalog.updated"] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "integer", + "allOf": [ + { + "minimum": 1 + } + ] + } + }, + "required": ["aggregateID", "seq", "version"], + "additionalProperties": false }, "location": { "$ref": "#/components/schemas/Location.Ref" @@ -19433,12 +16009,7 @@ ] } }, - "required": [ - "id", - "created", - "type", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, "agent.updated": { @@ -19452,17 +16023,38 @@ } ] }, - "created": { - "type": "number" - }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": [ - "agent.updated" - ] + "enum": ["agent.updated"] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "integer", + "allOf": [ + { + "minimum": 1 + } + ] + } + }, + "required": ["aggregateID", "seq", "version"], + "additionalProperties": false }, "location": { "$ref": "#/components/schemas/Location.Ref" @@ -19478,15 +16070,10 @@ ] } }, - "required": [ - "id", - "created", - "type", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, - "FileDiff.LegacyInfo": { + "SnapshotFileDiff": { "type": "object", "properties": { "file": { @@ -19503,26 +16090,15 @@ }, "status": { "type": "string", - "enum": [ - "added", - "deleted", - "modified" - ] + "enum": ["added", "deleted", "modified"] } }, - "required": [ - "additions", - "deletions" - ], + "required": ["additions", "deletions"], "additionalProperties": false }, "PermissionAction": { "type": "string", - "enum": [ - "allow", - "deny", - "ask" - ] + "enum": ["allow", "deny", "ask"] }, "PermissionRule": { "type": "object", @@ -19537,11 +16113,7 @@ "$ref": "#/components/schemas/PermissionAction" } }, - "required": [ - "permission", - "pattern", - "action" - ], + "required": ["permission", "pattern", "action"], "additionalProperties": false }, "PermissionRuleset": { @@ -19550,7 +16122,7 @@ "$ref": "#/components/schemas/PermissionRule" } }, - "SessionV1.Info": { + "Session": { "type": "object", "properties": { "id": { @@ -19604,15 +16176,11 @@ "diffs": { "type": "array", "items": { - "$ref": "#/components/schemas/FileDiff.LegacyInfo" + "$ref": "#/components/schemas/SnapshotFileDiff" } } }, - "required": [ - "additions", - "deletions", - "files" - ], + "required": ["additions", "deletions", "files"], "additionalProperties": false }, "cost": { @@ -19640,19 +16208,11 @@ "type": "number" } }, - "required": [ - "read", - "write" - ], + "required": ["read", "write"], "additionalProperties": false } }, - "required": [ - "input", - "output", - "reasoning", - "cache" - ], + "required": ["input", "output", "reasoning", "cache"], "additionalProperties": false }, "share": { @@ -19662,9 +16222,7 @@ "type": "string" } }, - "required": [ - "url" - ], + "required": ["url"], "additionalProperties": false }, "title": { @@ -19686,10 +16244,7 @@ "type": "string" } }, - "required": [ - "id", - "providerID" - ], + "required": ["id", "providerID"], "additionalProperties": false }, "version": { @@ -19729,10 +16284,7 @@ "type": "number" } }, - "required": [ - "created", - "updated" - ], + "required": ["created", "updated"], "additionalProperties": false }, "permission": { @@ -19764,21 +16316,11 @@ "type": "string" } }, - "required": [ - "messageID" - ], + "required": ["messageID"], "additionalProperties": false } }, - "required": [ - "id", - "slug", - "projectID", - "directory", - "title", - "version", - "time" - ], + "required": ["id", "slug", "projectID", "directory", "title", "version", "time"], "additionalProperties": false }, "session.created": { @@ -19792,17 +16334,12 @@ } ] }, - "created": { - "type": "number" - }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": [ - "session.created" - ] + "enum": ["session.created"] }, "durable": { "type": "object", @@ -19819,17 +16356,15 @@ ] }, "version": { - "type": "number", - "enum": [ - 1 + "type": "integer", + "allOf": [ + { + "minimum": 1 + } ] } }, - "required": [ - "aggregateID", - "seq", - "version" - ], + "required": ["aggregateID", "seq", "version"], "additionalProperties": false }, "location": { @@ -19847,23 +16382,14 @@ ] }, "info": { - "$ref": "#/components/schemas/SessionV1.Info" + "$ref": "#/components/schemas/Session" } }, - "required": [ - "sessionID", - "info" - ], + "required": ["sessionID", "info"], "additionalProperties": false } }, - "required": [ - "id", - "created", - "type", - "durable", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, "session.updated": { @@ -19877,17 +16403,12 @@ } ] }, - "created": { - "type": "number" - }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": [ - "session.updated" - ] + "enum": ["session.updated"] }, "durable": { "type": "object", @@ -19904,17 +16425,15 @@ ] }, "version": { - "type": "number", - "enum": [ - 1 + "type": "integer", + "allOf": [ + { + "minimum": 1 + } ] } }, - "required": [ - "aggregateID", - "seq", - "version" - ], + "required": ["aggregateID", "seq", "version"], "additionalProperties": false }, "location": { @@ -19932,26 +16451,17 @@ ] }, "info": { - "$ref": "#/components/schemas/SessionV1.Info" + "$ref": "#/components/schemas/Session" } }, - "required": [ - "sessionID", - "info" - ], + "required": ["sessionID", "info"], "additionalProperties": false } }, - "required": [ - "id", - "created", - "type", - "durable", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, - "session.deleted1": { + "session.deleted": { "type": "object", "properties": { "id": { @@ -19962,17 +16472,12 @@ } ] }, - "created": { - "type": "number" - }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": [ - "session.deleted" - ] + "enum": ["session.deleted"] }, "durable": { "type": "object", @@ -19989,17 +16494,15 @@ ] }, "version": { - "type": "number", - "enum": [ - 1 + "type": "integer", + "allOf": [ + { + "minimum": 1 + } ] } }, - "required": [ - "aggregateID", - "seq", - "version" - ], + "required": ["aggregateID", "seq", "version"], "additionalProperties": false }, "location": { @@ -20017,23 +16520,14 @@ ] }, "info": { - "$ref": "#/components/schemas/SessionV1.Info" + "$ref": "#/components/schemas/Session" } }, - "required": [ - "sessionID", - "info" - ], + "required": ["sessionID", "info"], "additionalProperties": false } }, - "required": [ - "id", - "created", - "type", - "durable", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, "JSONSchema": { @@ -20046,14 +16540,10 @@ "properties": { "type": { "type": "string", - "enum": [ - "text" - ] + "enum": ["text"] } }, - "required": [ - "type" - ], + "required": ["type"], "additionalProperties": false }, { @@ -20061,9 +16551,7 @@ "properties": { "type": { "type": "string", - "enum": [ - "json_schema" - ] + "enum": ["json_schema"] }, "schema": { "$ref": "#/components/schemas/JSONSchema" @@ -20091,10 +16579,7 @@ ] } }, - "required": [ - "type", - "schema" - ], + "required": ["type", "schema"], "additionalProperties": false } ] @@ -20120,9 +16605,7 @@ }, "role": { "type": "string", - "enum": [ - "user" - ] + "enum": ["user"] }, "time": { "type": "object", @@ -20136,9 +16619,7 @@ ] } }, - "required": [ - "created" - ], + "required": ["created"], "additionalProperties": false }, "format": { @@ -20179,13 +16660,11 @@ "diffs": { "type": "array", "items": { - "$ref": "#/components/schemas/FileDiff.LegacyInfo" + "$ref": "#/components/schemas/SnapshotFileDiff" } } }, - "required": [ - "diffs" - ], + "required": ["diffs"], "additionalProperties": false }, { @@ -20216,10 +16695,7 @@ ] } }, - "required": [ - "providerID", - "modelID" - ], + "required": ["providerID", "modelID"], "additionalProperties": false }, "system": { @@ -20246,14 +16722,7 @@ ] } }, - "required": [ - "id", - "sessionID", - "role", - "time", - "agent", - "model" - ], + "required": ["id", "sessionID", "role", "time", "agent", "model"], "additionalProperties": false }, "ProviderAuthError": { @@ -20261,9 +16730,7 @@ "properties": { "name": { "type": "string", - "enum": [ - "ProviderAuthError" - ] + "enum": ["ProviderAuthError"] }, "data": { "type": "object", @@ -20275,17 +16742,11 @@ "type": "string" } }, - "required": [ - "providerID", - "message" - ], + "required": ["providerID", "message"], "additionalProperties": false } }, - "required": [ - "name", - "data" - ], + "required": ["name", "data"], "additionalProperties": false }, "UnknownError1": { @@ -20293,9 +16754,7 @@ "properties": { "name": { "type": "string", - "enum": [ - "UnknownError" - ] + "enum": ["UnknownError"] }, "data": { "type": "object", @@ -20314,16 +16773,11 @@ ] } }, - "required": [ - "message" - ], + "required": ["message"], "additionalProperties": false } }, - "required": [ - "name", - "data" - ], + "required": ["name", "data"], "additionalProperties": false }, "MessageOutputLengthError": { @@ -20331,9 +16785,7 @@ "properties": { "name": { "type": "string", - "enum": [ - "MessageOutputLengthError" - ] + "enum": ["MessageOutputLengthError"] }, "data": { "anyOf": [ @@ -20346,10 +16798,7 @@ ] } }, - "required": [ - "name", - "data" - ], + "required": ["name", "data"], "additionalProperties": false }, "MessageAbortedError": { @@ -20357,9 +16806,7 @@ "properties": { "name": { "type": "string", - "enum": [ - "MessageAbortedError" - ] + "enum": ["MessageAbortedError"] }, "data": { "type": "object", @@ -20368,16 +16815,11 @@ "type": "string" } }, - "required": [ - "message" - ], + "required": ["message"], "additionalProperties": false } }, - "required": [ - "name", - "data" - ], + "required": ["name", "data"], "additionalProperties": false }, "StructuredOutputError": { @@ -20385,9 +16827,7 @@ "properties": { "name": { "type": "string", - "enum": [ - "StructuredOutputError" - ] + "enum": ["StructuredOutputError"] }, "data": { "type": "object", @@ -20404,17 +16844,11 @@ ] } }, - "required": [ - "message", - "retries" - ], + "required": ["message", "retries"], "additionalProperties": false } }, - "required": [ - "name", - "data" - ], + "required": ["name", "data"], "additionalProperties": false }, "ContextOverflowError": { @@ -20422,9 +16856,7 @@ "properties": { "name": { "type": "string", - "enum": [ - "ContextOverflowError" - ] + "enum": ["ContextOverflowError"] }, "data": { "type": "object", @@ -20443,16 +16875,11 @@ ] } }, - "required": [ - "message" - ], + "required": ["message"], "additionalProperties": false } }, - "required": [ - "name", - "data" - ], + "required": ["name", "data"], "additionalProperties": false }, "ContentFilterError": { @@ -20460,9 +16887,7 @@ "properties": { "name": { "type": "string", - "enum": [ - "ContentFilterError" - ] + "enum": ["ContentFilterError"] }, "data": { "type": "object", @@ -20471,16 +16896,11 @@ "type": "string" } }, - "required": [ - "message" - ], + "required": ["message"], "additionalProperties": false } }, - "required": [ - "name", - "data" - ], + "required": ["name", "data"], "additionalProperties": false }, "APIError": { @@ -20488,9 +16908,7 @@ "properties": { "name": { "type": "string", - "enum": [ - "APIError" - ] + "enum": ["APIError"] }, "data": { "type": "object", @@ -20553,17 +16971,11 @@ ] } }, - "required": [ - "message", - "isRetryable" - ], + "required": ["message", "isRetryable"], "additionalProperties": false } }, - "required": [ - "name", - "data" - ], + "required": ["name", "data"], "additionalProperties": false }, "AssistantMessage": { @@ -20587,9 +16999,7 @@ }, "role": { "type": "string", - "enum": [ - "assistant" - ] + "enum": ["assistant"] }, "time": { "type": "object", @@ -20618,9 +17028,7 @@ ] } }, - "required": [ - "created" - ], + "required": ["created"], "additionalProperties": false }, "error": { @@ -20688,10 +17096,7 @@ "type": "string" } }, - "required": [ - "cwd", - "root" - ], + "required": ["cwd", "root"], "additionalProperties": false }, "summary": { @@ -20739,19 +17144,11 @@ "type": "number" } }, - "required": [ - "read", - "write" - ], + "required": ["read", "write"], "additionalProperties": false } }, - "required": [ - "input", - "output", - "reasoning", - "cache" - ], + "required": ["input", "output", "reasoning", "cache"], "additionalProperties": false }, "structured": { @@ -20820,17 +17217,12 @@ } ] }, - "created": { - "type": "number" - }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": [ - "message.updated" - ] + "enum": ["message.updated"] }, "durable": { "type": "object", @@ -20847,17 +17239,15 @@ ] }, "version": { - "type": "number", - "enum": [ - 1 + "type": "integer", + "allOf": [ + { + "minimum": 1 + } ] } }, - "required": [ - "aggregateID", - "seq", - "version" - ], + "required": ["aggregateID", "seq", "version"], "additionalProperties": false }, "location": { @@ -20878,20 +17268,11 @@ "$ref": "#/components/schemas/Message" } }, - "required": [ - "sessionID", - "info" - ], + "required": ["sessionID", "info"], "additionalProperties": false } }, - "required": [ - "id", - "created", - "type", - "durable", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, "message.removed": { @@ -20905,17 +17286,12 @@ } ] }, - "created": { - "type": "number" - }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": [ - "message.removed" - ] + "enum": ["message.removed"] }, "durable": { "type": "object", @@ -20932,17 +17308,15 @@ ] }, "version": { - "type": "number", - "enum": [ - 1 + "type": "integer", + "allOf": [ + { + "minimum": 1 + } ] } }, - "required": [ - "aggregateID", - "seq", - "version" - ], + "required": ["aggregateID", "seq", "version"], "additionalProperties": false }, "location": { @@ -20968,20 +17342,11 @@ ] } }, - "required": [ - "sessionID", - "messageID" - ], + "required": ["sessionID", "messageID"], "additionalProperties": false } }, - "required": [ - "id", - "created", - "type", - "durable", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, "TextPart": { @@ -21013,9 +17378,7 @@ }, "type": { "type": "string", - "enum": [ - "text" - ] + "enum": ["text"] }, "text": { "type": "string" @@ -21069,9 +17432,7 @@ ] } }, - "required": [ - "start" - ], + "required": ["start"], "additionalProperties": false }, { @@ -21090,13 +17451,7 @@ ] } }, - "required": [ - "id", - "sessionID", - "messageID", - "type", - "text" - ], + "required": ["id", "sessionID", "messageID", "type", "text"], "additionalProperties": false }, "SubtaskPart": { @@ -21128,9 +17483,7 @@ }, "type": { "type": "string", - "enum": [ - "subtask" - ] + "enum": ["subtask"] }, "prompt": { "type": "string" @@ -21153,10 +17506,7 @@ "type": "string" } }, - "required": [ - "providerID", - "modelID" - ], + "required": ["providerID", "modelID"], "additionalProperties": false }, { @@ -21175,15 +17525,7 @@ ] } }, - "required": [ - "id", - "sessionID", - "messageID", - "type", - "prompt", - "description", - "agent" - ], + "required": ["id", "sessionID", "messageID", "type", "prompt", "description", "agent"], "additionalProperties": false }, "ReasoningPart": { @@ -21215,9 +17557,7 @@ }, "type": { "type": "string", - "enum": [ - "reasoning" - ] + "enum": ["reasoning"] }, "text": { "type": "string" @@ -21259,20 +17599,11 @@ ] } }, - "required": [ - "start" - ], + "required": ["start"], "additionalProperties": false } }, - "required": [ - "id", - "sessionID", - "messageID", - "type", - "text", - "time" - ], + "required": ["id", "sessionID", "messageID", "type", "text", "time"], "additionalProperties": false }, "FilePartSourceText": { @@ -21288,11 +17619,7 @@ "type": "number" } }, - "required": [ - "value", - "start", - "end" - ], + "required": ["value", "start", "end"], "additionalProperties": false }, "FileSource": { @@ -21303,19 +17630,13 @@ }, "type": { "type": "string", - "enum": [ - "file" - ] + "enum": ["file"] }, "path": { "type": "string" } }, - "required": [ - "text", - "type", - "path" - ], + "required": ["text", "type", "path"], "additionalProperties": false }, "Range": { @@ -21341,10 +17662,7 @@ ] } }, - "required": [ - "line", - "character" - ], + "required": ["line", "character"], "additionalProperties": false }, "end": { @@ -21367,17 +17685,11 @@ ] } }, - "required": [ - "line", - "character" - ], + "required": ["line", "character"], "additionalProperties": false } }, - "required": [ - "start", - "end" - ], + "required": ["start", "end"], "additionalProperties": false }, "SymbolSource": { @@ -21388,9 +17700,7 @@ }, "type": { "type": "string", - "enum": [ - "symbol" - ] + "enum": ["symbol"] }, "path": { "type": "string" @@ -21410,14 +17720,7 @@ ] } }, - "required": [ - "text", - "type", - "path", - "range", - "name", - "kind" - ], + "required": ["text", "type", "path", "range", "name", "kind"], "additionalProperties": false }, "ResourceSource": { @@ -21428,9 +17731,7 @@ }, "type": { "type": "string", - "enum": [ - "resource" - ] + "enum": ["resource"] }, "clientName": { "type": "string" @@ -21439,12 +17740,7 @@ "type": "string" } }, - "required": [ - "text", - "type", - "clientName", - "uri" - ], + "required": ["text", "type", "clientName", "uri"], "additionalProperties": false }, "FilePartSource": { @@ -21489,9 +17785,7 @@ }, "type": { "type": "string", - "enum": [ - "file" - ] + "enum": ["file"] }, "mime": { "type": "string" @@ -21520,14 +17814,7 @@ ] } }, - "required": [ - "id", - "sessionID", - "messageID", - "type", - "mime", - "url" - ], + "required": ["id", "sessionID", "messageID", "type", "mime", "url"], "additionalProperties": false }, "ToolStatePending": { @@ -21535,9 +17822,7 @@ "properties": { "status": { "type": "string", - "enum": [ - "pending" - ] + "enum": ["pending"] }, "input": { "type": "object" @@ -21546,11 +17831,7 @@ "type": "string" } }, - "required": [ - "status", - "input", - "raw" - ], + "required": ["status", "input", "raw"], "additionalProperties": false }, "ToolStateRunning": { @@ -21558,9 +17839,7 @@ "properties": { "status": { "type": "string", - "enum": [ - "running" - ] + "enum": ["running"] }, "input": { "type": "object" @@ -21597,17 +17876,11 @@ ] } }, - "required": [ - "start" - ], + "required": ["start"], "additionalProperties": false } }, - "required": [ - "status", - "input", - "time" - ], + "required": ["status", "input", "time"], "additionalProperties": false }, "ToolStateCompleted": { @@ -21615,9 +17888,7 @@ "properties": { "status": { "type": "string", - "enum": [ - "completed" - ] + "enum": ["completed"] }, "input": { "type": "object" @@ -21666,10 +17937,7 @@ ] } }, - "required": [ - "start", - "end" - ], + "required": ["start", "end"], "additionalProperties": false }, "attachments": { @@ -21686,14 +17954,7 @@ ] } }, - "required": [ - "status", - "input", - "output", - "title", - "metadata", - "time" - ], + "required": ["status", "input", "output", "title", "metadata", "time"], "additionalProperties": false }, "ToolStateError": { @@ -21701,9 +17962,7 @@ "properties": { "status": { "type": "string", - "enum": [ - "error" - ] + "enum": ["error"] }, "input": { "type": "object" @@ -21741,19 +18000,11 @@ ] } }, - "required": [ - "start", - "end" - ], + "required": ["start", "end"], "additionalProperties": false } }, - "required": [ - "status", - "input", - "error", - "time" - ], + "required": ["status", "input", "error", "time"], "additionalProperties": false }, "ToolState": { @@ -21801,9 +18052,7 @@ }, "type": { "type": "string", - "enum": [ - "tool" - ] + "enum": ["tool"] }, "callID": { "type": "string" @@ -21825,15 +18074,7 @@ ] } }, - "required": [ - "id", - "sessionID", - "messageID", - "type", - "callID", - "tool", - "state" - ], + "required": ["id", "sessionID", "messageID", "type", "callID", "tool", "state"], "additionalProperties": false }, "StepStartPart": { @@ -21865,9 +18106,7 @@ }, "type": { "type": "string", - "enum": [ - "step-start" - ] + "enum": ["step-start"] }, "snapshot": { "anyOf": [ @@ -21880,12 +18119,7 @@ ] } }, - "required": [ - "id", - "sessionID", - "messageID", - "type" - ], + "required": ["id", "sessionID", "messageID", "type"], "additionalProperties": false }, "StepFinishPart": { @@ -21917,9 +18151,7 @@ }, "type": { "type": "string", - "enum": [ - "step-finish" - ] + "enum": ["step-finish"] }, "reason": { "type": "string" @@ -21969,31 +18201,15 @@ "type": "number" } }, - "required": [ - "read", - "write" - ], + "required": ["read", "write"], "additionalProperties": false } }, - "required": [ - "input", - "output", - "reasoning", - "cache" - ], + "required": ["input", "output", "reasoning", "cache"], "additionalProperties": false } }, - "required": [ - "id", - "sessionID", - "messageID", - "type", - "reason", - "cost", - "tokens" - ], + "required": ["id", "sessionID", "messageID", "type", "reason", "cost", "tokens"], "additionalProperties": false }, "SnapshotPart": { @@ -22025,21 +18241,13 @@ }, "type": { "type": "string", - "enum": [ - "snapshot" - ] + "enum": ["snapshot"] }, "snapshot": { "type": "string" } }, - "required": [ - "id", - "sessionID", - "messageID", - "type", - "snapshot" - ], + "required": ["id", "sessionID", "messageID", "type", "snapshot"], "additionalProperties": false }, "PatchPart": { @@ -22071,9 +18279,7 @@ }, "type": { "type": "string", - "enum": [ - "patch" - ] + "enum": ["patch"] }, "hash": { "type": "string" @@ -22085,14 +18291,7 @@ } } }, - "required": [ - "id", - "sessionID", - "messageID", - "type", - "hash", - "files" - ], + "required": ["id", "sessionID", "messageID", "type", "hash", "files"], "additionalProperties": false }, "AgentPart": { @@ -22124,9 +18323,7 @@ }, "type": { "type": "string", - "enum": [ - "agent" - ] + "enum": ["agent"] }, "name": { "type": "string" @@ -22156,11 +18353,7 @@ ] } }, - "required": [ - "value", - "start", - "end" - ], + "required": ["value", "start", "end"], "additionalProperties": false }, { @@ -22169,13 +18362,7 @@ ] } }, - "required": [ - "id", - "sessionID", - "messageID", - "type", - "name" - ], + "required": ["id", "sessionID", "messageID", "type", "name"], "additionalProperties": false }, "RetryPart": { @@ -22207,9 +18394,7 @@ }, "type": { "type": "string", - "enum": [ - "retry" - ] + "enum": ["retry"] }, "attempt": { "type": "integer", @@ -22234,21 +18419,11 @@ ] } }, - "required": [ - "created" - ], + "required": ["created"], "additionalProperties": false } }, - "required": [ - "id", - "sessionID", - "messageID", - "type", - "attempt", - "error", - "time" - ], + "required": ["id", "sessionID", "messageID", "type", "attempt", "error", "time"], "additionalProperties": false }, "CompactionPart": { @@ -22280,9 +18455,7 @@ }, "type": { "type": "string", - "enum": [ - "compaction" - ] + "enum": ["compaction"] }, "auto": { "type": "boolean" @@ -22313,13 +18486,7 @@ ] } }, - "required": [ - "id", - "sessionID", - "messageID", - "type", - "auto" - ], + "required": ["id", "sessionID", "messageID", "type", "auto"], "additionalProperties": false }, "Part": { @@ -22373,17 +18540,12 @@ } ] }, - "created": { - "type": "number" - }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": [ - "message.part.updated" - ] + "enum": ["message.part.updated"] }, "durable": { "type": "object", @@ -22400,17 +18562,15 @@ ] }, "version": { - "type": "number", - "enum": [ - 1 + "type": "integer", + "allOf": [ + { + "minimum": 1 + } ] } }, - "required": [ - "aggregateID", - "seq", - "version" - ], + "required": ["aggregateID", "seq", "version"], "additionalProperties": false }, "location": { @@ -22434,21 +18594,11 @@ "type": "number" } }, - "required": [ - "sessionID", - "part", - "time" - ], + "required": ["sessionID", "part", "time"], "additionalProperties": false } }, - "required": [ - "id", - "created", - "type", - "durable", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, "message.part.removed": { @@ -22462,17 +18612,12 @@ } ] }, - "created": { - "type": "number" - }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": [ - "message.part.removed" - ] + "enum": ["message.part.removed"] }, "durable": { "type": "object", @@ -22489,17 +18634,15 @@ ] }, "version": { - "type": "number", - "enum": [ - 1 + "type": "integer", + "allOf": [ + { + "minimum": 1 + } ] } }, - "required": [ - "aggregateID", - "seq", - "version" - ], + "required": ["aggregateID", "seq", "version"], "additionalProperties": false }, "location": { @@ -22533,24 +18676,14 @@ ] } }, - "required": [ - "sessionID", - "messageID", - "partID" - ], + "required": ["sessionID", "messageID", "partID"], "additionalProperties": false } }, - "required": [ - "id", - "created", - "type", - "durable", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, - "session.usage.updated": { + "session.next.execution.settled": { "type": "object", "properties": { "id": { @@ -22561,17 +18694,38 @@ } ] }, - "created": { - "type": "number" - }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": [ - "session.usage.updated" - ] + "enum": ["session.next.execution.settled"] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "integer", + "allOf": [ + { + "minimum": 1 + } + ] + } + }, + "required": ["aggregateID", "seq", "version"], + "additionalProperties": false }, "location": { "$ref": "#/components/schemas/Location.Ref" @@ -22579,6 +18733,9 @@ "data": { "type": "object", "properties": { + "timestamp": { + "type": "number" + }, "sessionID": { "type": "string", "allOf": [ @@ -22587,30 +18744,22 @@ } ] }, - "cost": { - "$ref": "#/components/schemas/Money.USD" + "outcome": { + "type": "string", + "enum": ["success", "failure", "interrupted"] }, - "tokens": { - "$ref": "#/components/schemas/TokenUsage.Info" + "error": { + "$ref": "#/components/schemas/Session.Error.Unknown" } }, - "required": [ - "sessionID", - "cost", - "tokens" - ], + "required": ["timestamp", "sessionID", "outcome"], "additionalProperties": false } }, - "required": [ - "id", - "created", - "type", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, - "session.text.delta": { + "session.next.text.delta": { "type": "object", "properties": { "id": { @@ -22621,17 +18770,38 @@ } ] }, - "created": { - "type": "number" - }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": [ - "session.text.delta" - ] + "enum": ["session.next.text.delta"] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "integer", + "allOf": [ + { + "minimum": 1 + } + ] + } + }, + "required": ["aggregateID", "seq", "version"], + "additionalProperties": false }, "location": { "$ref": "#/components/schemas/Location.Ref" @@ -22639,6 +18809,9 @@ "data": { "type": "object", "properties": { + "timestamp": { + "type": "number" + }, "sessionID": { "type": "string", "allOf": [ @@ -22655,36 +18828,21 @@ } ] }, - "ordinal": { - "type": "integer", - "allOf": [ - { - "minimum": 0 - } - ] + "textID": { + "type": "string" }, "delta": { "type": "string" } }, - "required": [ - "sessionID", - "assistantMessageID", - "ordinal", - "delta" - ], + "required": ["timestamp", "sessionID", "assistantMessageID", "textID", "delta"], "additionalProperties": false } }, - "required": [ - "id", - "created", - "type", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, - "session.reasoning.delta": { + "session.next.reasoning.delta": { "type": "object", "properties": { "id": { @@ -22695,17 +18853,38 @@ } ] }, - "created": { - "type": "number" - }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": [ - "session.reasoning.delta" - ] + "enum": ["session.next.reasoning.delta"] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "integer", + "allOf": [ + { + "minimum": 1 + } + ] + } + }, + "required": ["aggregateID", "seq", "version"], + "additionalProperties": false }, "location": { "$ref": "#/components/schemas/Location.Ref" @@ -22713,6 +18892,9 @@ "data": { "type": "object", "properties": { + "timestamp": { + "type": "number" + }, "sessionID": { "type": "string", "allOf": [ @@ -22729,36 +18911,21 @@ } ] }, - "ordinal": { - "type": "integer", - "allOf": [ - { - "minimum": 0 - } - ] + "reasoningID": { + "type": "string" }, "delta": { "type": "string" } }, - "required": [ - "sessionID", - "assistantMessageID", - "ordinal", - "delta" - ], + "required": ["timestamp", "sessionID", "assistantMessageID", "reasoningID", "delta"], "additionalProperties": false } }, - "required": [ - "id", - "created", - "type", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, - "session.tool.input.delta": { + "session.next.tool.input.delta": { "type": "object", "properties": { "id": { @@ -22769,17 +18936,38 @@ } ] }, - "created": { - "type": "number" - }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": [ - "session.tool.input.delta" - ] + "enum": ["session.next.tool.input.delta"] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "integer", + "allOf": [ + { + "minimum": 1 + } + ] + } + }, + "required": ["aggregateID", "seq", "version"], + "additionalProperties": false }, "location": { "$ref": "#/components/schemas/Location.Ref" @@ -22787,6 +18975,9 @@ "data": { "type": "object", "properties": { + "timestamp": { + "type": "number" + }, "sessionID": { "type": "string", "allOf": [ @@ -22810,24 +19001,14 @@ "type": "string" } }, - "required": [ - "sessionID", - "assistantMessageID", - "callID", - "delta" - ], + "required": ["timestamp", "sessionID", "assistantMessageID", "callID", "delta"], "additionalProperties": false } }, - "required": [ - "id", - "created", - "type", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, - "session.compaction.delta": { + "session.next.compaction.delta": { "type": "object", "properties": { "id": { @@ -22838,17 +19019,38 @@ } ] }, - "created": { - "type": "number" - }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": [ - "session.compaction.delta" - ] + "enum": ["session.next.compaction.delta"] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "integer", + "allOf": [ + { + "minimum": 1 + } + ] + } + }, + "required": ["aggregateID", "seq", "version"], + "additionalProperties": false }, "location": { "$ref": "#/components/schemas/Location.Ref" @@ -22856,6 +19058,9 @@ "data": { "type": "object", "properties": { + "timestamp": { + "type": "number" + }, "sessionID": { "type": "string", "allOf": [ @@ -22864,26 +19069,26 @@ } ] }, + "messageID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, "text": { "type": "string" } }, - "required": [ - "sessionID", - "text" - ], + "required": ["timestamp", "sessionID", "messageID", "text"], "additionalProperties": false } }, - "required": [ - "id", - "created", - "type", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, - "filesystem.changed": { + "file.edited": { "type": "object", "properties": { "id": { @@ -22894,17 +19099,38 @@ } ] }, - "created": { - "type": "number" - }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": [ - "filesystem.changed" - ] + "enum": ["file.edited"] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "integer", + "allOf": [ + { + "minimum": 1 + } + ] + } + }, + "required": ["aggregateID", "seq", "version"], + "additionalProperties": false }, "location": { "$ref": "#/components/schemas/Location.Ref" @@ -22914,29 +19140,13 @@ "properties": { "file": { "type": "string" - }, - "event": { - "type": "string", - "enum": [ - "add", - "change", - "unlink" - ] } }, - "required": [ - "file", - "event" - ], + "required": ["file"], "additionalProperties": false } }, - "required": [ - "id", - "created", - "type", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, "reference.updated": { @@ -22950,17 +19160,38 @@ } ] }, - "created": { - "type": "number" - }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": [ - "reference.updated" - ] + "enum": ["reference.updated"] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "integer", + "allOf": [ + { + "minimum": 1 + } + ] + } + }, + "required": ["aggregateID", "seq", "version"], + "additionalProperties": false }, "location": { "$ref": "#/components/schemas/Location.Ref" @@ -22976,12 +19207,7 @@ ] } }, - "required": [ - "id", - "created", - "type", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, "permission.v2.asked": { @@ -22995,17 +19221,38 @@ } ] }, - "created": { - "type": "number" - }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": [ - "permission.v2.asked" - ] + "enum": ["permission.v2.asked"] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "integer", + "allOf": [ + { + "minimum": 1 + } + ] + } + }, + "required": ["aggregateID", "seq", "version"], + "additionalProperties": false }, "location": { "$ref": "#/components/schemas/Location.Ref" @@ -23051,21 +19298,11 @@ "$ref": "#/components/schemas/PermissionV2.Source" } }, - "required": [ - "id", - "sessionID", - "action", - "resources" - ], + "required": ["id", "sessionID", "action", "resources"], "additionalProperties": false } }, - "required": [ - "id", - "created", - "type", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, "permission.v2.replied": { @@ -23079,17 +19316,38 @@ } ] }, - "created": { - "type": "number" - }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": [ - "permission.v2.replied" - ] + "enum": ["permission.v2.replied"] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "integer", + "allOf": [ + { + "minimum": 1 + } + ] + } + }, + "required": ["aggregateID", "seq", "version"], + "additionalProperties": false }, "location": { "$ref": "#/components/schemas/Location.Ref" @@ -23117,20 +19375,11 @@ "$ref": "#/components/schemas/PermissionV2.Reply" } }, - "required": [ - "sessionID", - "requestID", - "reply" - ], + "required": ["sessionID", "requestID", "reply"], "additionalProperties": false } }, - "required": [ - "id", - "created", - "type", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, "plugin.added": { @@ -23144,17 +19393,38 @@ } ] }, - "created": { - "type": "number" - }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": [ - "plugin.added" - ] + "enum": ["plugin.added"] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "integer", + "allOf": [ + { + "minimum": 1 + } + ] + } + }, + "required": ["aggregateID", "seq", "version"], + "additionalProperties": false }, "location": { "$ref": "#/components/schemas/Location.Ref" @@ -23166,63 +19436,11 @@ "type": "string" } }, - "required": [ - "id" - ], + "required": ["id"], "additionalProperties": false } }, - "required": [ - "id", - "created", - "type", - "data" - ], - "additionalProperties": false - }, - "plugin.updated": { - "type": "object", - "properties": { - "id": { - "type": "string", - "allOf": [ - { - "pattern": "^evt_" - } - ] - }, - "created": { - "type": "number" - }, - "metadata": { - "type": "object" - }, - "type": { - "type": "string", - "enum": [ - "plugin.updated" - ] - }, - "location": { - "$ref": "#/components/schemas/Location.Ref" - }, - "data": { - "anyOf": [ - { - "type": "object" - }, - { - "type": "array" - } - ] - } - }, - "required": [ - "id", - "created", - "type", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, "project.directories.updated": { @@ -23236,17 +19454,38 @@ } ] }, - "created": { - "type": "number" - }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": [ - "project.directories.updated" - ] + "enum": ["project.directories.updated"] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "integer", + "allOf": [ + { + "minimum": 1 + } + ] + } + }, + "required": ["aggregateID", "seq", "version"], + "additionalProperties": false }, "location": { "$ref": "#/components/schemas/Location.Ref" @@ -23258,18 +19497,11 @@ "type": "string" } }, - "required": [ - "projectID" - ], + "required": ["projectID"], "additionalProperties": false } }, - "required": [ - "id", - "created", - "type", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, "command.updated": { @@ -23283,17 +19515,38 @@ } ] }, - "created": { - "type": "number" - }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": [ - "command.updated" - ] + "enum": ["command.updated"] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "integer", + "allOf": [ + { + "minimum": 1 + } + ] + } + }, + "required": ["aggregateID", "seq", "version"], + "additionalProperties": false }, "location": { "$ref": "#/components/schemas/Location.Ref" @@ -23309,57 +19562,7 @@ ] } }, - "required": [ - "id", - "created", - "type", - "data" - ], - "additionalProperties": false - }, - "config.updated": { - "type": "object", - "properties": { - "id": { - "type": "string", - "allOf": [ - { - "pattern": "^evt_" - } - ] - }, - "created": { - "type": "number" - }, - "metadata": { - "type": "object" - }, - "type": { - "type": "string", - "enum": [ - "config.updated" - ] - }, - "location": { - "$ref": "#/components/schemas/Location.Ref" - }, - "data": { - "anyOf": [ - { - "type": "object" - }, - { - "type": "array" - } - ] - } - }, - "required": [ - "id", - "created", - "type", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, "skill.updated": { @@ -23373,17 +19576,38 @@ } ] }, - "created": { - "type": "number" - }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": [ - "skill.updated" - ] + "enum": ["skill.updated"] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "integer", + "allOf": [ + { + "minimum": 1 + } + ] + } + }, + "required": ["aggregateID", "seq", "version"], + "additionalProperties": false }, "location": { "$ref": "#/components/schemas/Location.Ref" @@ -23399,12 +19623,72 @@ ] } }, - "required": [ - "id", - "created", - "type", - "data" - ], + "required": ["id", "type", "data"], + "additionalProperties": false + }, + "file.watcher.updated": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": ["file.watcher.updated"] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "integer", + "allOf": [ + { + "minimum": 1 + } + ] + } + }, + "required": ["aggregateID", "seq", "version"], + "additionalProperties": false + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "file": { + "type": "string" + }, + "event": { + "type": "string", + "enum": ["add", "change", "unlink"] + } + }, + "required": ["file", "event"], + "additionalProperties": false + } + }, + "required": ["id", "type", "data"], "additionalProperties": false }, "Pty": { @@ -23435,10 +19719,7 @@ }, "status": { "type": "string", - "enum": [ - "running", - "exited" - ] + "enum": ["running", "exited"] }, "pid": { "type": "integer", @@ -23457,15 +19738,7 @@ ] } }, - "required": [ - "id", - "title", - "command", - "args", - "cwd", - "status", - "pid" - ], + "required": ["id", "title", "command", "args", "cwd", "status", "pid"], "additionalProperties": false }, "pty.created": { @@ -23479,17 +19752,38 @@ } ] }, - "created": { - "type": "number" - }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": [ - "pty.created" - ] + "enum": ["pty.created"] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "integer", + "allOf": [ + { + "minimum": 1 + } + ] + } + }, + "required": ["aggregateID", "seq", "version"], + "additionalProperties": false }, "location": { "$ref": "#/components/schemas/Location.Ref" @@ -23501,18 +19795,11 @@ "$ref": "#/components/schemas/Pty" } }, - "required": [ - "info" - ], + "required": ["info"], "additionalProperties": false } }, - "required": [ - "id", - "created", - "type", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, "pty.updated": { @@ -23526,17 +19813,38 @@ } ] }, - "created": { - "type": "number" - }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": [ - "pty.updated" - ] + "enum": ["pty.updated"] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "integer", + "allOf": [ + { + "minimum": 1 + } + ] + } + }, + "required": ["aggregateID", "seq", "version"], + "additionalProperties": false }, "location": { "$ref": "#/components/schemas/Location.Ref" @@ -23548,18 +19856,11 @@ "$ref": "#/components/schemas/Pty" } }, - "required": [ - "info" - ], + "required": ["info"], "additionalProperties": false } }, - "required": [ - "id", - "created", - "type", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, "pty.exited": { @@ -23573,17 +19874,38 @@ } ] }, - "created": { - "type": "number" - }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": [ - "pty.exited" - ] + "enum": ["pty.exited"] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "integer", + "allOf": [ + { + "minimum": 1 + } + ] + } + }, + "required": ["aggregateID", "seq", "version"], + "additionalProperties": false }, "location": { "$ref": "#/components/schemas/Location.Ref" @@ -23608,19 +19930,11 @@ ] } }, - "required": [ - "id", - "exitCode" - ], + "required": ["id", "exitCode"], "additionalProperties": false } }, - "required": [ - "id", - "created", - "type", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, "pty.deleted": { @@ -23634,17 +19948,38 @@ } ] }, - "created": { - "type": "number" - }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": [ - "pty.deleted" - ] + "enum": ["pty.deleted"] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "integer", + "allOf": [ + { + "minimum": 1 + } + ] + } + }, + "required": ["aggregateID", "seq", "version"], + "additionalProperties": false }, "location": { "$ref": "#/components/schemas/Location.Ref" @@ -23661,18 +19996,117 @@ ] } }, - "required": [ - "id" - ], + "required": ["id"], "additionalProperties": false } }, - "required": [ - "id", - "created", - "type", - "data" - ], + "required": ["id", "type", "data"], + "additionalProperties": false + }, + "Shell": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^sh_" + } + ] + }, + "status": { + "type": "string", + "enum": ["running", "exited", "timeout", "killed"] + }, + "command": { + "type": "string" + }, + "cwd": { + "type": "string" + }, + "shell": { + "type": "string" + }, + "file": { + "type": "string" + }, + "pid": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "exit": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": ["NaN"] + }, + { + "type": "string", + "enum": ["Infinity"] + }, + { + "type": "string", + "enum": ["-Infinity"] + } + ] + }, + "metadata": { + "type": "object" + }, + "time": { + "type": "object", + "properties": { + "started": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": ["NaN"] + }, + { + "type": "string", + "enum": ["Infinity"] + }, + { + "type": "string", + "enum": ["-Infinity"] + } + ] + }, + "completed": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": ["NaN"] + }, + { + "type": "string", + "enum": ["Infinity"] + }, + { + "type": "string", + "enum": ["-Infinity"] + } + ] + } + }, + "required": ["started"], + "additionalProperties": false + } + }, + "required": ["id", "status", "command", "cwd", "shell", "file", "metadata", "time"], "additionalProperties": false }, "shell.created": { @@ -23686,17 +20120,38 @@ } ] }, - "created": { - "type": "number" - }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": [ - "shell.created" - ] + "enum": ["shell.created"] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "integer", + "allOf": [ + { + "minimum": 1 + } + ] + } + }, + "required": ["aggregateID", "seq", "version"], + "additionalProperties": false }, "location": { "$ref": "#/components/schemas/Location.Ref" @@ -23708,18 +20163,11 @@ "$ref": "#/components/schemas/Shell" } }, - "required": [ - "info" - ], + "required": ["info"], "additionalProperties": false } }, - "required": [ - "id", - "created", - "type", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, "shell.exited": { @@ -23733,17 +20181,38 @@ } ] }, - "created": { - "type": "number" - }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": [ - "shell.exited" - ] + "enum": ["shell.exited"] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "integer", + "allOf": [ + { + "minimum": 1 + } + ] + } + }, + "required": ["aggregateID", "seq", "version"], + "additionalProperties": false }, "location": { "$ref": "#/components/schemas/Location.Ref" @@ -23766,47 +20235,28 @@ }, { "type": "string", - "enum": [ - "NaN" - ] + "enum": ["NaN"] }, { "type": "string", - "enum": [ - "Infinity" - ] + "enum": ["Infinity"] }, { "type": "string", - "enum": [ - "-Infinity" - ] + "enum": ["-Infinity"] } ] }, "status": { "type": "string", - "enum": [ - "running", - "exited", - "timeout", - "killed" - ] + "enum": ["running", "exited", "timeout", "killed"] } }, - "required": [ - "id", - "status" - ], + "required": ["id", "status"], "additionalProperties": false } }, - "required": [ - "id", - "created", - "type", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, "shell.deleted": { @@ -23820,17 +20270,38 @@ } ] }, - "created": { - "type": "number" - }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": [ - "shell.deleted" - ] + "enum": ["shell.deleted"] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "integer", + "allOf": [ + { + "minimum": 1 + } + ] + } + }, + "required": ["aggregateID", "seq", "version"], + "additionalProperties": false }, "location": { "$ref": "#/components/schemas/Location.Ref" @@ -23847,18 +20318,11 @@ ] } }, - "required": [ - "id" - ], + "required": ["id"], "additionalProperties": false } }, - "required": [ - "id", - "created", - "type", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, "QuestionV2.Option": { @@ -23873,10 +20337,7 @@ "description": "Explanation of choice" } }, - "required": [ - "label", - "description" - ], + "required": ["label", "description"], "additionalProperties": false }, "QuestionV2.Info": { @@ -23904,11 +20365,7 @@ "type": "boolean" } }, - "required": [ - "question", - "header", - "options" - ], + "required": ["question", "header", "options"], "additionalProperties": false }, "QuestionV2.Tool": { @@ -23921,10 +20378,7 @@ "type": "string" } }, - "required": [ - "messageID", - "callID" - ], + "required": ["messageID", "callID"], "additionalProperties": false }, "question.v2.asked": { @@ -23938,17 +20392,38 @@ } ] }, - "created": { - "type": "number" - }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": [ - "question.v2.asked" - ] + "enum": ["question.v2.asked"] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "integer", + "allOf": [ + { + "minimum": 1 + } + ] + } + }, + "required": ["aggregateID", "seq", "version"], + "additionalProperties": false }, "location": { "$ref": "#/components/schemas/Location.Ref" @@ -23983,20 +20458,11 @@ "$ref": "#/components/schemas/QuestionV2.Tool" } }, - "required": [ - "id", - "sessionID", - "questions" - ], + "required": ["id", "sessionID", "questions"], "additionalProperties": false } }, - "required": [ - "id", - "created", - "type", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, "QuestionV2.Answer": { @@ -24016,17 +20482,38 @@ } ] }, - "created": { - "type": "number" - }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": [ - "question.v2.replied" - ] + "enum": ["question.v2.replied"] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "integer", + "allOf": [ + { + "minimum": 1 + } + ] + } + }, + "required": ["aggregateID", "seq", "version"], + "additionalProperties": false }, "location": { "$ref": "#/components/schemas/Location.Ref" @@ -24057,20 +20544,11 @@ } } }, - "required": [ - "sessionID", - "requestID", - "answers" - ], + "required": ["sessionID", "requestID", "answers"], "additionalProperties": false } }, - "required": [ - "id", - "created", - "type", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, "question.v2.rejected": { @@ -24084,17 +20562,38 @@ } ] }, - "created": { - "type": "number" - }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": [ - "question.v2.rejected" - ] + "enum": ["question.v2.rejected"] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "integer", + "allOf": [ + { + "minimum": 1 + } + ] + } + }, + "required": ["aggregateID", "seq", "version"], + "additionalProperties": false }, "location": { "$ref": "#/components/schemas/Location.Ref" @@ -24119,19 +20618,11 @@ ] } }, - "required": [ - "sessionID", - "requestID" - ], + "required": ["sessionID", "requestID"], "additionalProperties": false } }, - "required": [ - "id", - "created", - "type", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, "Form.Metadata1": { @@ -24145,10 +20636,7 @@ }, "op": { "type": "string", - "enum": [ - "eq", - "neq" - ] + "enum": ["eq", "neq"] }, "value": { "anyOf": [ @@ -24162,21 +20650,15 @@ }, { "type": "string", - "enum": [ - "NaN" - ] + "enum": ["NaN"] }, { "type": "string", - "enum": [ - "Infinity" - ] + "enum": ["Infinity"] }, { "type": "string", - "enum": [ - "-Infinity" - ] + "enum": ["-Infinity"] } ] }, @@ -24186,11 +20668,7 @@ ] } }, - "required": [ - "key", - "op", - "value" - ], + "required": ["key", "op", "value"], "additionalProperties": false }, "Form.StringField1": { @@ -24216,18 +20694,11 @@ }, "type": { "type": "string", - "enum": [ - "string" - ] + "enum": ["string"] }, "format": { "type": "string", - "enum": [ - "email", - "uri", - "date", - "date-time" - ] + "enum": ["email", "uri", "date", "date-time"] }, "minLength": { "type": "integer", @@ -24264,10 +20735,7 @@ "type": "boolean" } }, - "required": [ - "key", - "type" - ], + "required": ["key", "type"], "additionalProperties": false }, "Form.NumberField1": { @@ -24293,9 +20761,7 @@ }, "type": { "type": "string", - "enum": [ - "number" - ] + "enum": ["number"] }, "minimum": { "anyOf": [ @@ -24304,21 +20770,15 @@ }, { "type": "string", - "enum": [ - "NaN" - ] + "enum": ["NaN"] }, { "type": "string", - "enum": [ - "Infinity" - ] + "enum": ["Infinity"] }, { "type": "string", - "enum": [ - "-Infinity" - ] + "enum": ["-Infinity"] } ] }, @@ -24329,21 +20789,15 @@ }, { "type": "string", - "enum": [ - "NaN" - ] + "enum": ["NaN"] }, { "type": "string", - "enum": [ - "Infinity" - ] + "enum": ["Infinity"] }, { "type": "string", - "enum": [ - "-Infinity" - ] + "enum": ["-Infinity"] } ] }, @@ -24354,29 +20808,20 @@ }, { "type": "string", - "enum": [ - "NaN" - ] + "enum": ["NaN"] }, { "type": "string", - "enum": [ - "Infinity" - ] + "enum": ["Infinity"] }, { "type": "string", - "enum": [ - "-Infinity" - ] + "enum": ["-Infinity"] } ] } }, - "required": [ - "key", - "type" - ], + "required": ["key", "type"], "additionalProperties": false }, "Form.IntegerField1": { @@ -24402,9 +20847,7 @@ }, "type": { "type": "string", - "enum": [ - "integer" - ] + "enum": ["integer"] }, "minimum": { "anyOf": [ @@ -24413,21 +20856,15 @@ }, { "type": "string", - "enum": [ - "NaN" - ] + "enum": ["NaN"] }, { "type": "string", - "enum": [ - "Infinity" - ] + "enum": ["Infinity"] }, { "type": "string", - "enum": [ - "-Infinity" - ] + "enum": ["-Infinity"] } ] }, @@ -24438,21 +20875,15 @@ }, { "type": "string", - "enum": [ - "NaN" - ] + "enum": ["NaN"] }, { "type": "string", - "enum": [ - "Infinity" - ] + "enum": ["Infinity"] }, { "type": "string", - "enum": [ - "-Infinity" - ] + "enum": ["-Infinity"] } ] }, @@ -24463,29 +20894,20 @@ }, { "type": "string", - "enum": [ - "NaN" - ] + "enum": ["NaN"] }, { "type": "string", - "enum": [ - "Infinity" - ] + "enum": ["Infinity"] }, { "type": "string", - "enum": [ - "-Infinity" - ] + "enum": ["-Infinity"] } ] } }, - "required": [ - "key", - "type" - ], + "required": ["key", "type"], "additionalProperties": false }, "Form.BooleanField1": { @@ -24511,18 +20933,13 @@ }, "type": { "type": "string", - "enum": [ - "boolean" - ] + "enum": ["boolean"] }, "default": { "type": "boolean" } }, - "required": [ - "key", - "type" - ], + "required": ["key", "type"], "additionalProperties": false }, "Form.MultiselectField1": { @@ -24548,9 +20965,7 @@ }, "type": { "type": "string", - "enum": [ - "multiselect" - ] + "enum": ["multiselect"] }, "options": { "type": "array", @@ -24584,11 +20999,7 @@ } } }, - "required": [ - "key", - "type", - "options" - ], + "required": ["key", "type", "options"], "additionalProperties": false }, "Form.FormInfo1": { @@ -24613,9 +21024,7 @@ }, "mode": { "type": "string", - "enum": [ - "form" - ] + "enum": ["form"] }, "fields": { "type": "array", @@ -24640,13 +21049,7 @@ } } }, - "required": [ - "id", - "sessionID", - "title", - "mode", - "fields" - ], + "required": ["id", "sessionID", "mode", "fields"], "additionalProperties": false }, "Form.UrlInfo1": { @@ -24671,21 +21074,13 @@ }, "mode": { "type": "string", - "enum": [ - "url" - ] + "enum": ["url"] }, "url": { "type": "string" } }, - "required": [ - "id", - "sessionID", - "title", - "mode", - "url" - ], + "required": ["id", "sessionID", "mode", "url"], "additionalProperties": false }, "form.created": { @@ -24699,17 +21094,38 @@ } ] }, - "created": { - "type": "number" - }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": [ - "form.created" - ] + "enum": ["form.created"] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "integer", + "allOf": [ + { + "minimum": 1 + } + ] + } + }, + "required": ["aggregateID", "seq", "version"], + "additionalProperties": false }, "location": { "$ref": "#/components/schemas/Location.Ref" @@ -24728,18 +21144,11 @@ ] } }, - "required": [ - "form" - ], + "required": ["form"], "additionalProperties": false } }, - "required": [ - "id", - "created", - "type", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, "Form.Value1": { @@ -24754,21 +21163,15 @@ }, { "type": "string", - "enum": [ - "NaN" - ] + "enum": ["NaN"] }, { "type": "string", - "enum": [ - "Infinity" - ] + "enum": ["Infinity"] }, { "type": "string", - "enum": [ - "-Infinity" - ] + "enum": ["-Infinity"] } ] }, @@ -24800,17 +21203,38 @@ } ] }, - "created": { - "type": "number" - }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": [ - "form.replied" - ] + "enum": ["form.replied"] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "integer", + "allOf": [ + { + "minimum": 1 + } + ] + } + }, + "required": ["aggregateID", "seq", "version"], + "additionalProperties": false }, "location": { "$ref": "#/components/schemas/Location.Ref" @@ -24833,20 +21257,11 @@ "$ref": "#/components/schemas/Form.Answer1" } }, - "required": [ - "id", - "sessionID", - "answer" - ], + "required": ["id", "sessionID", "answer"], "additionalProperties": false } }, - "required": [ - "id", - "created", - "type", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, "form.cancelled": { @@ -24860,17 +21275,38 @@ } ] }, - "created": { - "type": "number" - }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": [ - "form.cancelled" - ] + "enum": ["form.cancelled"] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "integer", + "allOf": [ + { + "minimum": 1 + } + ] + } + }, + "required": ["aggregateID", "seq", "version"], + "additionalProperties": false }, "location": { "$ref": "#/components/schemas/Location.Ref" @@ -24890,19 +21326,102 @@ "type": "string" } }, - "required": [ - "id", - "sessionID" - ], + "required": ["id", "sessionID"], "additionalProperties": false } }, - "required": [ - "id", - "created", - "type", - "data" - ], + "required": ["id", "type", "data"], + "additionalProperties": false + }, + "Todo": { + "type": "object", + "properties": { + "content": { + "type": "string", + "description": "Brief description of the task" + }, + "status": { + "type": "string", + "description": "Current status of the task: pending, in_progress, completed, cancelled" + }, + "priority": { + "type": "string", + "description": "Priority level of the task: high, medium, low" + } + }, + "required": ["content", "status", "priority"], + "additionalProperties": false + }, + "todo.updated": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": ["todo.updated"] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "integer", + "allOf": [ + { + "minimum": 1 + } + ] + } + }, + "required": ["aggregateID", "seq", "version"], + "additionalProperties": false + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "todos": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Todo" + } + } + }, + "required": ["sessionID", "todos"], + "additionalProperties": false + } + }, + "required": ["id", "type", "data"], "additionalProperties": false }, "SessionStatus": { @@ -24912,14 +21431,10 @@ "properties": { "type": { "type": "string", - "enum": [ - "idle" - ] + "enum": ["idle"] } }, - "required": [ - "type" - ], + "required": ["type"], "additionalProperties": false }, { @@ -24927,9 +21442,7 @@ "properties": { "type": { "type": "string", - "enum": [ - "retry" - ] + "enum": ["retry"] }, "attempt": { "type": "integer", @@ -24964,13 +21477,7 @@ "type": "string" } }, - "required": [ - "reason", - "provider", - "title", - "message", - "label" - ], + "required": ["reason", "provider", "title", "message", "label"], "additionalProperties": false }, "next": { @@ -24982,12 +21489,7 @@ ] } }, - "required": [ - "type", - "attempt", - "message", - "next" - ], + "required": ["type", "attempt", "message", "next"], "additionalProperties": false }, { @@ -24995,14 +21497,10 @@ "properties": { "type": { "type": "string", - "enum": [ - "busy" - ] + "enum": ["busy"] } }, - "required": [ - "type" - ], + "required": ["type"], "additionalProperties": false } ] @@ -25018,17 +21516,38 @@ } ] }, - "created": { - "type": "number" - }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": [ - "session.status" - ] + "enum": ["session.status"] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "integer", + "allOf": [ + { + "minimum": 1 + } + ] + } + }, + "required": ["aggregateID", "seq", "version"], + "additionalProperties": false }, "location": { "$ref": "#/components/schemas/Location.Ref" @@ -25048,19 +21567,11 @@ "$ref": "#/components/schemas/SessionStatus" } }, - "required": [ - "sessionID", - "status" - ], + "required": ["sessionID", "status"], "additionalProperties": false } }, - "required": [ - "id", - "created", - "type", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, "session.idle": { @@ -25074,17 +21585,38 @@ } ] }, - "created": { - "type": "number" - }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": [ - "session.idle" - ] + "enum": ["session.idle"] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "integer", + "allOf": [ + { + "minimum": 1 + } + ] + } + }, + "required": ["aggregateID", "seq", "version"], + "additionalProperties": false }, "location": { "$ref": "#/components/schemas/Location.Ref" @@ -25101,18 +21633,11 @@ ] } }, - "required": [ - "sessionID" - ], + "required": ["sessionID"], "additionalProperties": false } }, - "required": [ - "id", - "created", - "type", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, "tui.prompt.append": { @@ -25126,17 +21651,38 @@ } ] }, - "created": { - "type": "number" - }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": [ - "tui.prompt.append" - ] + "enum": ["tui.prompt.append"] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "integer", + "allOf": [ + { + "minimum": 1 + } + ] + } + }, + "required": ["aggregateID", "seq", "version"], + "additionalProperties": false }, "location": { "$ref": "#/components/schemas/Location.Ref" @@ -25148,18 +21694,11 @@ "type": "string" } }, - "required": [ - "text" - ], + "required": ["text"], "additionalProperties": false } }, - "required": [ - "id", - "created", - "type", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, "tui.command.execute": { @@ -25173,17 +21712,38 @@ } ] }, - "created": { - "type": "number" - }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": [ - "tui.command.execute" - ] + "enum": ["tui.command.execute"] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "integer", + "allOf": [ + { + "minimum": 1 + } + ] + } + }, + "required": ["aggregateID", "seq", "version"], + "additionalProperties": false }, "location": { "$ref": "#/components/schemas/Location.Ref" @@ -25221,18 +21781,11 @@ ] } }, - "required": [ - "command" - ], + "required": ["command"], "additionalProperties": false } }, - "required": [ - "id", - "created", - "type", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, "tui.toast.show": { @@ -25246,17 +21799,38 @@ } ] }, - "created": { - "type": "number" - }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": [ - "tui.toast.show" - ] + "enum": ["tui.toast.show"] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "integer", + "allOf": [ + { + "minimum": 1 + } + ] + } + }, + "required": ["aggregateID", "seq", "version"], + "additionalProperties": false }, "location": { "$ref": "#/components/schemas/Location.Ref" @@ -25272,12 +21846,7 @@ }, "variant": { "type": "string", - "enum": [ - "info", - "success", - "warning", - "error" - ] + "enum": ["info", "success", "warning", "error"] }, "duration": { "anyOf": [ @@ -25295,19 +21864,11 @@ ] } }, - "required": [ - "message", - "variant" - ], + "required": ["message", "variant"], "additionalProperties": false } }, - "required": [ - "id", - "created", - "type", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, "tui.session.select": { @@ -25321,17 +21882,38 @@ } ] }, - "created": { - "type": "number" - }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": [ - "tui.session.select" - ] + "enum": ["tui.session.select"] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "integer", + "allOf": [ + { + "minimum": 1 + } + ] + } + }, + "required": ["aggregateID", "seq", "version"], + "additionalProperties": false }, "location": { "$ref": "#/components/schemas/Location.Ref" @@ -25349,18 +21931,11 @@ ] } }, - "required": [ - "sessionID" - ], + "required": ["sessionID"], "additionalProperties": false } }, - "required": [ - "id", - "created", - "type", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, "installation.updated": { @@ -25374,17 +21949,38 @@ } ] }, - "created": { - "type": "number" - }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": [ - "installation.updated" - ] + "enum": ["installation.updated"] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "integer", + "allOf": [ + { + "minimum": 1 + } + ] + } + }, + "required": ["aggregateID", "seq", "version"], + "additionalProperties": false }, "location": { "$ref": "#/components/schemas/Location.Ref" @@ -25396,18 +21992,11 @@ "type": "string" } }, - "required": [ - "version" - ], + "required": ["version"], "additionalProperties": false } }, - "required": [ - "id", - "created", - "type", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, "installation.update-available": { @@ -25421,17 +22010,38 @@ } ] }, - "created": { - "type": "number" - }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": [ - "installation.update-available" - ] + "enum": ["installation.update-available"] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "integer", + "allOf": [ + { + "minimum": 1 + } + ] + } + }, + "required": ["aggregateID", "seq", "version"], + "additionalProperties": false }, "location": { "$ref": "#/components/schemas/Location.Ref" @@ -25443,18 +22053,11 @@ "type": "string" } }, - "required": [ - "version" - ], + "required": ["version"], "additionalProperties": false } }, - "required": [ - "id", - "created", - "type", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, "vcs.branch.updated": { @@ -25468,17 +22071,38 @@ } ] }, - "created": { - "type": "number" - }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": [ - "vcs.branch.updated" - ] + "enum": ["vcs.branch.updated"] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "integer", + "allOf": [ + { + "minimum": 1 + } + ] + } + }, + "required": ["aggregateID", "seq", "version"], + "additionalProperties": false }, "location": { "$ref": "#/components/schemas/Location.Ref" @@ -25493,12 +22117,7 @@ "additionalProperties": false } }, - "required": [ - "id", - "created", - "type", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, "mcp.status.changed": { @@ -25512,17 +22131,38 @@ } ] }, - "created": { - "type": "number" - }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": [ - "mcp.status.changed" - ] + "enum": ["mcp.status.changed"] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "integer", + "allOf": [ + { + "minimum": 1 + } + ] + } + }, + "required": ["aggregateID", "seq", "version"], + "additionalProperties": false }, "location": { "$ref": "#/components/schemas/Location.Ref" @@ -25534,65 +22174,11 @@ "type": "string" } }, - "required": [ - "server" - ], + "required": ["server"], "additionalProperties": false } }, - "required": [ - "id", - "created", - "type", - "data" - ], - "additionalProperties": false - }, - "mcp.resources.changed": { - "type": "object", - "properties": { - "id": { - "type": "string", - "allOf": [ - { - "pattern": "^evt_" - } - ] - }, - "created": { - "type": "number" - }, - "metadata": { - "type": "object" - }, - "type": { - "type": "string", - "enum": [ - "mcp.resources.changed" - ] - }, - "location": { - "$ref": "#/components/schemas/Location.Ref" - }, - "data": { - "type": "object", - "properties": { - "server": { - "type": "string" - } - }, - "required": [ - "server" - ], - "additionalProperties": false - } - }, - "required": [ - "id", - "created", - "type", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, "permission.asked": { @@ -25606,17 +22192,38 @@ } ] }, - "created": { - "type": "number" - }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": [ - "permission.asked" - ] + "enum": ["permission.asked"] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "integer", + "allOf": [ + { + "minimum": 1 + } + ] + } + }, + "required": ["aggregateID", "seq", "version"], + "additionalProperties": false }, "location": { "$ref": "#/components/schemas/Location.Ref" @@ -25670,10 +22277,7 @@ "type": "string" } }, - "required": [ - "messageID", - "callID" - ], + "required": ["messageID", "callID"], "additionalProperties": false }, { @@ -25682,23 +22286,11 @@ ] } }, - "required": [ - "id", - "sessionID", - "permission", - "patterns", - "metadata", - "always" - ], + "required": ["id", "sessionID", "permission", "patterns", "metadata", "always"], "additionalProperties": false } }, - "required": [ - "id", - "created", - "type", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, "permission.replied": { @@ -25712,17 +22304,38 @@ } ] }, - "created": { - "type": "number" - }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": [ - "permission.replied" - ] + "enum": ["permission.replied"] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "integer", + "allOf": [ + { + "minimum": 1 + } + ] + } + }, + "required": ["aggregateID", "seq", "version"], + "additionalProperties": false }, "location": { "$ref": "#/components/schemas/Location.Ref" @@ -25748,27 +22361,14 @@ }, "reply": { "type": "string", - "enum": [ - "once", - "always", - "reject" - ] + "enum": ["once", "always", "reject"] } }, - "required": [ - "sessionID", - "requestID", - "reply" - ], + "required": ["sessionID", "requestID", "reply"], "additionalProperties": false } }, - "required": [ - "id", - "created", - "type", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, "QuestionOption": { @@ -25783,10 +22383,7 @@ "description": "Explanation of choice" } }, - "required": [ - "label", - "description" - ], + "required": ["label", "description"], "additionalProperties": false }, "QuestionInfo": { @@ -25830,11 +22427,7 @@ "description": "Allow typing a custom answer (default: true)" } }, - "required": [ - "question", - "header", - "options" - ], + "required": ["question", "header", "options"], "additionalProperties": false }, "QuestionTool": { @@ -25852,10 +22445,7 @@ "type": "string" } }, - "required": [ - "messageID", - "callID" - ], + "required": ["messageID", "callID"], "additionalProperties": false }, "question.asked": { @@ -25869,17 +22459,38 @@ } ] }, - "created": { - "type": "number" - }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": [ - "question.asked" - ] + "enum": ["question.asked"] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "integer", + "allOf": [ + { + "minimum": 1 + } + ] + } + }, + "required": ["aggregateID", "seq", "version"], + "additionalProperties": false }, "location": { "$ref": "#/components/schemas/Location.Ref" @@ -25921,20 +22532,11 @@ ] } }, - "required": [ - "id", - "sessionID", - "questions" - ], + "required": ["id", "sessionID", "questions"], "additionalProperties": false } }, - "required": [ - "id", - "created", - "type", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, "QuestionAnswer": { @@ -25954,17 +22556,38 @@ } ] }, - "created": { - "type": "number" - }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": [ - "question.replied" - ] + "enum": ["question.replied"] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "integer", + "allOf": [ + { + "minimum": 1 + } + ] + } + }, + "required": ["aggregateID", "seq", "version"], + "additionalProperties": false }, "location": { "$ref": "#/components/schemas/Location.Ref" @@ -25995,20 +22618,11 @@ } } }, - "required": [ - "sessionID", - "requestID", - "answers" - ], + "required": ["sessionID", "requestID", "answers"], "additionalProperties": false } }, - "required": [ - "id", - "created", - "type", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, "question.rejected": { @@ -26022,17 +22636,38 @@ } ] }, - "created": { - "type": "number" - }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": [ - "question.rejected" - ] + "enum": ["question.rejected"] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "integer", + "allOf": [ + { + "minimum": 1 + } + ] + } + }, + "required": ["aggregateID", "seq", "version"], + "additionalProperties": false }, "location": { "$ref": "#/components/schemas/Location.Ref" @@ -26057,19 +22692,11 @@ ] } }, - "required": [ - "sessionID", - "requestID" - ], + "required": ["sessionID", "requestID"], "additionalProperties": false } }, - "required": [ - "id", - "created", - "type", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, "session.error": { @@ -26083,17 +22710,38 @@ } ] }, - "created": { - "type": "number" - }, "metadata": { "type": "object" }, "type": { "type": "string", - "enum": [ - "session.error" - ] + "enum": ["session.error"] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "integer", + "allOf": [ + { + "minimum": 1 + } + ] + } + }, + "required": ["aggregateID", "seq", "version"], + "additionalProperties": false }, "location": { "$ref": "#/components/schemas/Location.Ref" @@ -26155,12 +22803,7 @@ "additionalProperties": false } }, - "required": [ - "id", - "created", - "type", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, "V2Event.server.connected": { @@ -26184,6 +22827,39 @@ } ] }, + "durable": { + "anyOf": [ + { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "integer", + "allOf": [ + { + "minimum": 1 + } + ] + } + }, + "required": ["aggregateID", "seq", "version"], + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, "location": { "anyOf": [ { @@ -26196,9 +22872,7 @@ }, "type": { "type": "string", - "enum": [ - "server.connected" - ] + "enum": ["server.connected"] }, "data": { "anyOf": [ @@ -26211,11 +22885,7 @@ ] } }, - "required": [ - "id", - "type", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, "V2Event": { @@ -26242,7 +22912,7 @@ "$ref": "#/components/schemas/session.updated" }, { - "$ref": "#/components/schemas/session.deleted1" + "$ref": "#/components/schemas/session.deleted" }, { "$ref": "#/components/schemas/message.updated" @@ -26257,136 +22927,115 @@ "$ref": "#/components/schemas/message.part.removed" }, { - "$ref": "#/components/schemas/session.agent.selected" + "$ref": "#/components/schemas/session.next.agent.switched" }, { - "$ref": "#/components/schemas/session.model.selected" + "$ref": "#/components/schemas/session.next.model.switched" }, { - "$ref": "#/components/schemas/session.moved" + "$ref": "#/components/schemas/session.next.moved" }, { - "$ref": "#/components/schemas/session.renamed" + "$ref": "#/components/schemas/session.next.renamed" }, { - "$ref": "#/components/schemas/session.usage.updated" + "$ref": "#/components/schemas/session.next.forked" }, { - "$ref": "#/components/schemas/session.deleted" + "$ref": "#/components/schemas/session.next.prompted" }, { - "$ref": "#/components/schemas/session.forked" + "$ref": "#/components/schemas/session.next.prompt.admitted" }, { - "$ref": "#/components/schemas/session.prompt.promoted" + "$ref": "#/components/schemas/session.next.execution.settled" }, { - "$ref": "#/components/schemas/session.prompt.admitted" + "$ref": "#/components/schemas/session.next.context.updated" }, { - "$ref": "#/components/schemas/session.execution.started" + "$ref": "#/components/schemas/session.next.synthetic" }, { - "$ref": "#/components/schemas/session.execution.succeeded" + "$ref": "#/components/schemas/session.next.skill.activated" }, { - "$ref": "#/components/schemas/session.execution.failed" + "$ref": "#/components/schemas/session.next.shell.started" }, { - "$ref": "#/components/schemas/session.execution.interrupted" + "$ref": "#/components/schemas/session.next.shell.ended" }, { - "$ref": "#/components/schemas/session.instructions.updated" + "$ref": "#/components/schemas/session.next.step.started" }, { - "$ref": "#/components/schemas/session.synthetic" + "$ref": "#/components/schemas/session.next.step.ended" }, { - "$ref": "#/components/schemas/session.skill.activated" + "$ref": "#/components/schemas/session.next.step.failed" }, { - "$ref": "#/components/schemas/session.shell.started" + "$ref": "#/components/schemas/session.next.text.started" }, { - "$ref": "#/components/schemas/session.shell.ended" + "$ref": "#/components/schemas/session.next.text.delta" }, { - "$ref": "#/components/schemas/session.step.started" + "$ref": "#/components/schemas/session.next.text.ended" }, { - "$ref": "#/components/schemas/session.step.ended" + "$ref": "#/components/schemas/session.next.reasoning.started" }, { - "$ref": "#/components/schemas/session.step.failed" + "$ref": "#/components/schemas/session.next.reasoning.delta" }, { - "$ref": "#/components/schemas/session.text.started" + "$ref": "#/components/schemas/session.next.reasoning.ended" }, { - "$ref": "#/components/schemas/session.text.delta" + "$ref": "#/components/schemas/session.next.tool.input.started" }, { - "$ref": "#/components/schemas/session.text.ended" + "$ref": "#/components/schemas/session.next.tool.input.delta" }, { - "$ref": "#/components/schemas/session.reasoning.started" + "$ref": "#/components/schemas/session.next.tool.input.ended" }, { - "$ref": "#/components/schemas/session.reasoning.delta" + "$ref": "#/components/schemas/session.next.tool.called" }, { - "$ref": "#/components/schemas/session.reasoning.ended" + "$ref": "#/components/schemas/session.next.tool.progress" }, { - "$ref": "#/components/schemas/session.tool.input.started" + "$ref": "#/components/schemas/session.next.tool.success" }, { - "$ref": "#/components/schemas/session.tool.input.delta" + "$ref": "#/components/schemas/session.next.tool.failed" }, { - "$ref": "#/components/schemas/session.tool.input.ended" + "$ref": "#/components/schemas/session.next.retried" }, { - "$ref": "#/components/schemas/session.tool.called" + "$ref": "#/components/schemas/session.next.compaction.started" }, { - "$ref": "#/components/schemas/session.tool.progress" + "$ref": "#/components/schemas/session.next.compaction.delta" }, { - "$ref": "#/components/schemas/session.tool.success" + "$ref": "#/components/schemas/session.next.compaction.ended" }, { - "$ref": "#/components/schemas/session.tool.failed" + "$ref": "#/components/schemas/session.next.revert.staged" }, { - "$ref": "#/components/schemas/session.retry.scheduled" + "$ref": "#/components/schemas/session.next.revert.cleared" }, { - "$ref": "#/components/schemas/session.compaction.admitted" + "$ref": "#/components/schemas/session.next.revert.committed" }, { - "$ref": "#/components/schemas/session.compaction.started" - }, - { - "$ref": "#/components/schemas/session.compaction.delta" - }, - { - "$ref": "#/components/schemas/session.compaction.ended" - }, - { - "$ref": "#/components/schemas/session.compaction.failed" - }, - { - "$ref": "#/components/schemas/session.revert.staged" - }, - { - "$ref": "#/components/schemas/session.revert.cleared" - }, - { - "$ref": "#/components/schemas/session.revert.committed" - }, - { - "$ref": "#/components/schemas/filesystem.changed" + "$ref": "#/components/schemas/file.edited" }, { "$ref": "#/components/schemas/reference.updated" @@ -26400,9 +23049,6 @@ { "$ref": "#/components/schemas/plugin.added" }, - { - "$ref": "#/components/schemas/plugin.updated" - }, { "$ref": "#/components/schemas/project.directories.updated" }, @@ -26410,10 +23056,10 @@ "$ref": "#/components/schemas/command.updated" }, { - "$ref": "#/components/schemas/config.updated" + "$ref": "#/components/schemas/skill.updated" }, { - "$ref": "#/components/schemas/skill.updated" + "$ref": "#/components/schemas/file.watcher.updated" }, { "$ref": "#/components/schemas/pty.created" @@ -26454,6 +23100,9 @@ { "$ref": "#/components/schemas/form.cancelled" }, + { + "$ref": "#/components/schemas/todo.updated" + }, { "$ref": "#/components/schemas/session.status" }, @@ -26484,9 +23133,6 @@ { "$ref": "#/components/schemas/mcp.status.changed" }, - { - "$ref": "#/components/schemas/mcp.resources.changed" - }, { "$ref": "#/components/schemas/permission.asked" }, @@ -26517,14 +23163,64 @@ }, "contentMediaType": "application/json" }, + "EventLog.Hint": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["log.hint"] + }, + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + } + }, + "required": ["type", "aggregateID", "seq"], + "additionalProperties": false, + "description": "Payload-free change hint: the aggregate's durable log advanced to at least seq. Hints coalesce under backpressure (latest per aggregate) and are never a delivery guarantee." + }, + "EventLog.SweepRequired": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["log.sweep_required"] + } + }, + "required": ["type"], + "additionalProperties": false, + "description": "Hints may have been lost; treat every aggregate as potentially dirty and recover via bounded sweep plus durable log reads. Emitted first on every (re)subscribe." + }, + "EventLog.Change": { + "anyOf": [ + { + "$ref": "#/components/schemas/EventLog.Hint" + }, + { + "$ref": "#/components/schemas/EventLog.SweepRequired" + } + ] + }, + "EventLog.ChangeStream": { + "type": "string", + "contentSchema": { + "$ref": "#/components/schemas/EventLog.Change" + }, + "contentMediaType": "application/json" + }, "PtyNotFoundError": { "type": "object", "properties": { "_tag": { "type": "string", - "enum": [ - "PtyNotFoundError" - ] + "enum": ["PtyNotFoundError"] }, "ptyID": { "type": "string" @@ -26533,11 +23229,7 @@ "type": "string" } }, - "required": [ - "_tag", - "ptyID", - "message" - ], + "required": ["_tag", "ptyID", "message"], "additionalProperties": false }, "PtyTicket.ConnectToken": { @@ -26555,10 +23247,7 @@ ] } }, - "required": [ - "ticket", - "expires_in" - ], + "required": ["ticket", "expires_in"], "additionalProperties": false }, "ForbiddenError": { @@ -26566,18 +23255,13 @@ "properties": { "_tag": { "type": "string", - "enum": [ - "ForbiddenError" - ] + "enum": ["ForbiddenError"] }, "message": { "type": "string" } }, - "required": [ - "_tag", - "message" - ], + "required": ["_tag", "message"], "additionalProperties": false }, "Shell1": { @@ -26593,12 +23277,7 @@ }, "status": { "type": "string", - "enum": [ - "running", - "exited", - "timeout", - "killed" - ] + "enum": ["running", "exited", "timeout", "killed"] }, "command": { "type": "string" @@ -26629,31 +23308,21 @@ }, { "type": "string", - "enum": [ - "NaN" - ] + "enum": ["NaN"] }, { "type": "string", - "enum": [ - "Infinity" - ] + "enum": ["Infinity"] }, { "type": "string", - "enum": [ - "-Infinity" - ] + "enum": ["-Infinity"] } ] }, { "type": "string", - "enum": [ - "Infinity", - "-Infinity", - "NaN" - ] + "enum": ["Infinity", "-Infinity", "NaN"] } ] }, @@ -26672,31 +23341,21 @@ }, { "type": "string", - "enum": [ - "NaN" - ] + "enum": ["NaN"] }, { "type": "string", - "enum": [ - "Infinity" - ] + "enum": ["Infinity"] }, { "type": "string", - "enum": [ - "-Infinity" - ] + "enum": ["-Infinity"] } ] }, { "type": "string", - "enum": [ - "Infinity", - "-Infinity", - "NaN" - ] + "enum": ["Infinity", "-Infinity", "NaN"] } ] }, @@ -26709,51 +23368,30 @@ }, { "type": "string", - "enum": [ - "NaN" - ] + "enum": ["NaN"] }, { "type": "string", - "enum": [ - "Infinity" - ] + "enum": ["Infinity"] }, { "type": "string", - "enum": [ - "-Infinity" - ] + "enum": ["-Infinity"] } ] }, { "type": "string", - "enum": [ - "Infinity", - "-Infinity", - "NaN" - ] + "enum": ["Infinity", "-Infinity", "NaN"] } ] } }, - "required": [ - "started" - ], + "required": ["started"], "additionalProperties": false } }, - "required": [ - "id", - "status", - "command", - "cwd", - "shell", - "file", - "metadata", - "time" - ], + "required": ["id", "status", "command", "cwd", "shell", "file", "metadata", "time"], "additionalProperties": false }, "ShellNotFoundError": { @@ -26761,9 +23399,7 @@ "properties": { "_tag": { "type": "string", - "enum": [ - "ShellNotFoundError" - ] + "enum": ["ShellNotFoundError"] }, "id": { "type": "string" @@ -26772,11 +23408,7 @@ "type": "string" } }, - "required": [ - "_tag", - "id", - "message" - ], + "required": ["_tag", "id", "message"], "additionalProperties": false }, "QuestionV2.Request": { @@ -26809,11 +23441,7 @@ "$ref": "#/components/schemas/QuestionV2.Tool" } }, - "required": [ - "id", - "sessionID", - "questions" - ], + "required": ["id", "sessionID", "questions"], "additionalProperties": false }, "QuestionV2.Reply": { @@ -26827,9 +23455,7 @@ "description": "User answers in order of questions (each answer is an array of selected labels)" } }, - "required": [ - "answers" - ], + "required": ["answers"], "additionalProperties": false }, "QuestionNotFoundError": { @@ -26837,9 +23463,7 @@ "properties": { "_tag": { "type": "string", - "enum": [ - "QuestionNotFoundError" - ] + "enum": ["QuestionNotFoundError"] }, "requestID": { "type": "string" @@ -26848,11 +23472,7 @@ "type": "string" } }, - "required": [ - "_tag", - "requestID", - "message" - ], + "required": ["_tag", "requestID", "message"], "additionalProperties": false }, "Reference.LocalSource": { @@ -26860,9 +23480,7 @@ "properties": { "type": { "type": "string", - "enum": [ - "local" - ] + "enum": ["local"] }, "path": { "type": "string" @@ -26874,10 +23492,7 @@ "type": "boolean" } }, - "required": [ - "type", - "path" - ], + "required": ["type", "path"], "additionalProperties": false }, "Reference.GitSource": { @@ -26885,9 +23500,7 @@ "properties": { "type": { "type": "string", - "enum": [ - "git" - ] + "enum": ["git"] }, "repository": { "type": "string" @@ -26902,10 +23515,7 @@ "type": "boolean" } }, - "required": [ - "type", - "repository" - ], + "required": ["type", "repository"], "additionalProperties": false }, "Reference.Source": { @@ -26937,11 +23547,7 @@ "$ref": "#/components/schemas/Reference.Source" } }, - "required": [ - "name", - "path", - "source" - ], + "required": ["name", "path", "source"], "additionalProperties": false }, "ProjectCopy.Copy": { @@ -26951,9 +23557,7 @@ "type": "string" } }, - "required": [ - "directory" - ], + "required": ["directory"], "additionalProperties": false }, "ProjectCopyError": { @@ -26961,9 +23565,7 @@ "properties": { "name": { "type": "string", - "enum": [ - "ProjectCopyError" - ] + "enum": ["ProjectCopyError"] }, "data": { "type": "object", @@ -26982,16 +23584,11 @@ ] } }, - "required": [ - "message" - ], + "required": ["message"], "additionalProperties": false } }, - "required": [ - "name", - "data" - ], + "required": ["name", "data"], "additionalProperties": false }, "Vcs.FileStatus": { @@ -27018,27 +23615,15 @@ }, "status": { "type": "string", - "enum": [ - "added", - "deleted", - "modified" - ] + "enum": ["added", "deleted", "modified"] } }, - "required": [ - "file", - "additions", - "deletions", - "status" - ], + "required": ["file", "additions", "deletions", "status"], "additionalProperties": false }, "Vcs.Mode": { "type": "string", - "enum": [ - "working", - "branch" - ] + "enum": ["working", "branch"] } }, "securitySchemes": {} @@ -27046,31 +23631,28 @@ "security": [], "tags": [ { - "name": "health" + "name": "server.health" }, { - "name": "server" + "name": "server.location" }, { - "name": "location" + "name": "server.agent" }, { - "name": "agent" - }, - { - "name": "plugin", + "name": "plugins", "description": "Experimental plugin routes." }, { - "name": "session", + "name": "sessions", "description": "Experimental session routes." }, { - "name": "session", + "name": "messages", "description": "Experimental message routes." }, { - "name": "model", + "name": "models", "description": "Experimental model routes." }, { @@ -27078,30 +23660,30 @@ "description": "Experimental one-shot generation routes." }, { - "name": "provider", + "name": "providers", "description": "Experimental provider routes." }, { - "name": "integration", + "name": "integrations", "description": "Integration discovery and authentication routes." }, { "name": "mcp", - "description": "MCP server and resource routes." + "description": "MCP server status routes." }, { - "name": "credential" + "name": "server.credential" }, { - "name": "project", + "name": "projects", "description": "Location-scoped project routes." }, { - "name": "form", + "name": "forms", "description": "Session form routes." }, { - "name": "permission", + "name": "permissions", "description": "Experimental permission routes." }, { @@ -27109,15 +23691,15 @@ "description": "Experimental location-scoped filesystem routes." }, { - "name": "command", + "name": "commands", "description": "Experimental command routes." }, { - "name": "skill", + "name": "skills", "description": "Experimental skill routes." }, { - "name": "event", + "name": "events", "description": "Experimental event stream routes." }, { @@ -27129,7 +23711,7 @@ "description": "Experimental location-scoped shell command routes." }, { - "name": "question", + "name": "session questions", "description": "Experimental session question routes." }, { @@ -27143,9 +23725,6 @@ { "name": "vcs", "description": "Location-scoped version control routes." - }, - { - "name": "debug" } ] } diff --git a/packages/codemode/test/openapi.test.ts b/packages/codemode/test/openapi.test.ts index 2d256393a2..12a094e033 100644 --- a/packages/codemode/test/openapi.test.ts +++ b/packages/codemode/test/openapi.test.ts @@ -177,13 +177,13 @@ describe("OpenAPI.fromSpec", () => { const spec = await opencodeSpec() const result = OpenAPI.fromSpec({ spec, baseUrl }) - expect(result.skipped).toHaveLength(4) + expect(result.skipped).toHaveLength(5) expect(result.skipped).toContainEqual({ method: "GET", path: "/api/pty/{ptyID}/connect", reason: "WebSocket operations are not supported", }) - expect(result.skipped.filter((item) => item.reason === "SSE operations are not supported")).toHaveLength(2) + expect(result.skipped.filter((item) => item.reason === "SSE operations are not supported")).toHaveLength(3) expect(result.skipped).toContainEqual({ method: "GET", path: "/api/fs/read/*", @@ -205,16 +205,17 @@ describe("OpenAPI.fromSpec", () => { if (!Tool.isDefinition(switchAgent)) throw new Error("v2.session.switchAgent was not generated") expect(inputTypeScript(switchAgent)).toBe("{ sessionID: string; agent: string }") - const instructionPut = toolAt(result.tools, "v2.session.instructions.entry.put") - expect(Tool.isDefinition(instructionPut)).toBe(true) - if (!Tool.isDefinition(instructionPut)) throw new Error("v2.session.instructions.entry.put was not generated") - expect(inputTypeScript(instructionPut)).toBe("{ sessionID: string; key: string; value: unknown }") - expect(toolAt(result.tools, "v2_session_instructions_entry_put_2")).toBeUndefined() - expect(Tool.isDefinition(toolAt(result.tools, "v2.pty.connect"))).toBe(false) + const contextEntryPut = toolAt(result.tools, "v2.session.contextEntry.put") + expect(Tool.isDefinition(contextEntryPut)).toBe(true) + if (!Tool.isDefinition(contextEntryPut)) throw new Error("v2.session.contextEntry.put was not generated") + expect(inputTypeScript(contextEntryPut)).toBe("{ sessionID: string; key: string; value: unknown }") + expect(toolAt(result.tools, "v2_session_context_entry_put_2")).toBeUndefined() + expect(toolAt(result.tools, "v2.pty.connect")).toBeUndefined() expect(toolAt(result.tools, "v2.session.log")).toBeUndefined() expect(toolAt(result.tools, "v2.event.subscribe")).toBeUndefined() + expect(toolAt(result.tools, "v2.event.changes")).toBeUndefined() expect(toolAt(result.tools, "v2.fs.read")).toBeUndefined() - expect(toolAt(result.tools, "v2.pty.connect.token")).not.toBeUndefined() + expect(toolAt(result.tools, "v2.pty.connectToken")).not.toBeUndefined() }) test("preserves operation path sanitization and collision handling", () => { @@ -377,7 +378,7 @@ describe("OpenAPI.fromSpec", () => { runtime .execute( ` - return search({ query: "global health", namespace: "opencode", limit: 1 }) + return await tools.$codemode.search({ query: "global health", namespace: "opencode", limit: 1 }) `, ) .pipe(Effect.provide(layer)), diff --git a/packages/codemode/test/parity.test.ts b/packages/codemode/test/parity.test.ts index 33c6e22361..dfa8583183 100644 --- a/packages/codemode/test/parity.test.ts +++ b/packages/codemode/test/parity.test.ts @@ -42,6 +42,11 @@ describe("H2: string property access reads as undefined (not a throw)", () => { test("unknown property on a number is undefined", async () => { expect(await value(`return (5).foo ?? "n"`)).toBe("n") }) + + test("supported string methods still work", async () => { + expect(await value(`return "AB".toLowerCase()`)).toBe("ab") + expect(await value(`return "hello".length`)).toBe(5) + }) }) describe("H3: array property access reads as undefined (not a throw)", () => { @@ -58,7 +63,8 @@ describe("H3: array property access reads as undefined (not a throw)", () => { expect(await value(`return [1,2,3].toSpliced === undefined`)).toBe(true) }) - test("array indexing still works", async () => { + test("supported array methods and indexing still work", async () => { + expect(await value(`return [1,2,3].map(x => x + 1)`)).toEqual([2, 3, 4]) expect(await value(`return [1,2,3][9] === undefined`)).toBe(true) expect(await value(`return [1,2,3][9]`)).toBeNull() }) @@ -196,6 +202,9 @@ describe("Error values and instanceof", () => { "TypeError", true, ]) + expect(await value(`try { "a".normalize("NOPE") } catch (e) { return [e.name, e instanceof RangeError] }`)).toEqual( + ["RangeError", true], + ) expect(await value(`try { "a".match("(") } catch (e) { return [e.name, e instanceof SyntaxError] }`)).toEqual([ "SyntaxError", true, @@ -254,18 +263,15 @@ describe("Error values and instanceof", () => { }) }) -describe("CodeMode-specific array behavior", () => { - test("sort with a comparator mutates and returns the receiver", async () => { - expect( - await value(` - const input = [3, 1, 2] - const result = input.sort((a, b) => a - b) - return { input, same: input === result } - `), - ).toEqual({ input: [1, 2, 3], same: true }) +describe("array methods: splice, fill, copyWithin, keys/values/entries", () => { + test("splice removes in place and returns the removed elements", async () => { + expect(await value(`const a = [1,2,3,4]; const removed = a.splice(1, 2); return { removed, a }`)).toEqual({ + removed: [2, 3], + a: [1, 4], + }) }) - test("splice can replace and insert elements", async () => { + test("splice inserts new elements at the cut", async () => { expect(await value(`const a = ["a","d"]; a.splice(1, 0, "b", "c"); return a`)).toEqual(["a", "b", "c", "d"]) expect(await value(`const a = [1,2,3]; const removed = a.splice(1, 1, "x"); return { removed, a }`)).toEqual({ removed: [2], @@ -273,12 +279,32 @@ describe("CodeMode-specific array behavior", () => { }) }) + test("splice with one argument removes to the end; negative start counts back", async () => { + expect(await value(`const a = [1,2,3]; const removed = a.splice(1); return { removed, a }`)).toEqual({ + removed: [2, 3], + a: [1], + }) + expect(await value(`const a = [1,2,3]; const removed = a.splice(-1); return { removed, a }`)).toEqual({ + removed: [3], + a: [1, 2], + }) + }) + test("splice rejects inserting a container into itself", async () => { const err = await error(`const a = [1]; a.splice(0, 0, [a]); return a`) expect(err.kind).toBe("InvalidDataValue") expect(err.message).toContain("circular") }) + test("fill overwrites a range and returns the mutated array", async () => { + expect(await value(`const a = [1,2,3,4]; return a.fill(0, 1, 3)`)).toEqual([1, 0, 0, 4]) + expect(await value(`return [1,2,3].fill("z")`)).toEqual(["z", "z", "z"]) + }) + + test("copyWithin copies a range in place", async () => { + expect(await value(`return [1,2,3,4,5].copyWithin(0, 3)`)).toEqual([4, 5, 3, 4, 5]) + }) + test("keys/values/entries return arrays usable with for...of and spread", async () => { expect(await value(`return [...["x","y","z"].keys()]`)).toEqual([0, 1, 2]) expect(await value(`return ["x","y"].values()`)).toEqual(["x", "y"]) @@ -293,21 +319,25 @@ describe("CodeMode-specific array behavior", () => { }) }) -describe("CodeMode-specific string behavior", () => { +describe("string methods: localeCompare, normalize, trim aliases", () => { test("localeCompare orders strings for sorting", async () => { expect(await value(`return ["b","a","c"].sort((x, y) => x.localeCompare(y))`)).toEqual(["a", "b", "c"]) + expect(await value(`return "a".localeCompare("a")`)).toBe(0) + }) + + test("normalize applies unicode normalization forms", async () => { + expect(await value(`return "\\u0065\\u0301".normalize("NFC").length`)).toBe(1) + expect(await value(`return "\\u00e9".normalize("NFD").length`)).toBe(2) + expect(await value(`return "x".normalize() === "x"`)).toBe(true) }) test("an invalid normalize form is a clear catchable error", async () => { expect(await value(`try { "x".normalize("nope"); return "no" } catch (e) { return e.message }`)).toContain('"NFC"') }) - test("does not expose obsolete string aliases", async () => { - expect(await value(`return [typeof "x".trimLeft, typeof "x".trimRight, typeof "x".substr]`)).toEqual([ - "undefined", - "undefined", - "undefined", - ]) + test("trimLeft/trimRight alias trimStart/trimEnd", async () => { + expect(await value(`return " x ".trimLeft()`)).toBe("x ") + expect(await value(`return " x ".trimRight()`)).toBe(" x") }) }) @@ -383,91 +413,13 @@ describe("H5: builtin coercion functions work as array callbacks", () => { expect(await value(`return [1, 2, 3].map(String)`)).toEqual(["1", "2", "3"]) }) + test("arrow callbacks still work (no regression)", async () => { + expect(await value(`return [1, 2, 3, 4].filter(x => x % 2 === 0)`)).toEqual([2, 4]) + expect(await value(`return [1, 2, 3].reduce((a, b) => a + b, 0)`)).toBe(6) + }) + test("a non-callable callback is still rejected", async () => { const err = await error(`return [1,2,3].map(42)`) expect(err.message).toContain("callback") }) }) - -describe("for...of assignment destructuring", () => { - test("assigns entry pairs into predeclared variables", async () => { - expect( - await value(` - let key - let item - const out = [] - for ([key, item] of Object.entries({ a: 1, b: 2 })) out.push(key + item) - return { key, item, out } - `), - ).toEqual({ key: "b", item: 2, out: ["a1", "b2"] }) - }) - - test("assigns object patterns and defaults", async () => { - expect( - await value(` - let id - let label - const labels = [] - for ({ id, label = "unknown" } of [{ id: 1 }, { id: 2, label: "two" }]) labels.push(label) - return { id, label, labels } - `), - ).toEqual({ id: 2, label: "two", labels: ["unknown", "two"] }) - }) -}) - -describe("sequence expressions", () => { - test("evaluate left to right and return the final value", async () => { - expect(await value(`let x = 0; const result = (x += 1, x *= 3, x + 2); return { x, result }`)).toEqual({ - x: 3, - result: 5, - }) - }) - - test("support comma-separated for-loop updates", async () => { - expect( - await value(` - const pairs = [] - for (let left = 0, right = 3; left < right; left++, right--) pairs.push([left, right]) - return pairs - `), - ).toEqual([ - [0, 3], - [1, 2], - ]) - }) -}) - -describe("destructuring assignment", () => { - test("assigns object and array patterns to existing bindings", async () => { - expect( - await value(` - let a = 0 - let b = 0 - ;({ a } = { a: 2 }) - ;[a, b] = [3, 4] - return [a, b] - `), - ).toEqual([3, 4]) - }) - - test("supports defaults, nesting, rest, and member targets", async () => { - expect( - await value(` - let first = 0 - let fallback = 0 - let rest = {} - const target = {} - ;[first, fallback = 2, ...target.tail] = [1] - ;({ nested: { value: target.value }, kept: target.kept = 3, ...rest } = { - nested: { value: 4 }, - extra: 5, - }) - return { first, fallback, target, rest } - `), - ).toEqual({ first: 1, fallback: 2, target: { tail: [], value: 4, kept: 3 }, rest: { extra: 5 } }) - }) - - test("returns the assigned value", async () => { - expect(await value(`let a = 0; const result = ([a] = [7]); return [a, result]`)).toEqual([7, [7]]) - }) -}) diff --git a/packages/codemode/test/promise-test262.test.ts b/packages/codemode/test/promise-test262.test.ts deleted file mode 100644 index e416ec942c..0000000000 --- a/packages/codemode/test/promise-test262.test.ts +++ /dev/null @@ -1,1423 +0,0 @@ -/* - * Portions adapted from Test262 at revision 250f204f23a9249ff204be2baec29600faae7b75. - * Every test names its upstream source; test.failing cases are executable conformance - * targets for intended Promise behavior that CodeMode does not implement yet. - * - * Copyright 2014 Cubane Canada, Inc. All rights reserved. - * Copyright 2015 Microsoft Corporation. All rights reserved. - * Copyright 2016 Microsoft, Inc. All rights reserved. - * Copyright 2017 Caitlin Potter. All rights reserved. - * Copyright (C) 2016-2020 the V8 project authors. All rights reserved. - * Copyright (C) 2018-2020 Rick Waldron. All rights reserved. - * Copyright (C) 2019 Leo Balter. All rights reserved. - * Test262 portions are governed by the BSD license in LICENSE.test262. - */ -import { describe, expect, test } from "bun:test" -import { Effect } from "effect" -import { CodeMode } from "../src/index.js" - -const execute = (code: string) => Effect.runPromise(CodeMode.execute({ code, tools: {}, limits: { timeoutMs: 1_000 } })) - -const value = async (code: string) => { - const result = await execute(code) - if (!result.ok) throw new Error(`expected success, got ${result.error.kind}: ${result.error.message}`) - return result.value -} - -describe("Test262 Promise statics", () => { - test("statics are callable and return promises", async () => { - // Sources: - // test/built-ins/Promise/all/S25.4.4.1_A1.1_T1.js - // test/built-ins/Promise/allSettled/is-function.js - // test/built-ins/Promise/allSettled/returns-promise.js - // test/built-ins/Promise/race/S25.4.4.3_A1.1_T1.js - // test/built-ins/Promise/resolve/S25.4.4.5_A1.1_T1.js - // test/built-ins/Promise/reject/S25.4.4.4_A1.1_T1.js - expect( - await value(` - const values = [ - Promise.all([]), - Promise.allSettled([]), - Promise.race([undefined]), - Promise.resolve(), - Promise.reject(), - ] - const callable = [ - typeof Promise.all, - typeof Promise.allSettled, - typeof Promise.race, - typeof Promise.resolve, - typeof Promise.reject, - ] - try { await values[4] } catch {} - return [callable, values.map((item) => item instanceof Promise)] - `), - ).toEqual([ - ["function", "function", "function", "function", "function"], - [true, true, true, true, true], - ]) - }) - - test("Promise.all returns fresh arrays for empty and settled inputs", async () => { - // Sources: - // test/built-ins/Promise/all/S25.4.4.1_A2.1_T1.js - // test/built-ins/Promise/all/S25.4.4.1_A2.3_T1.js - // test/built-ins/Promise/all/S25.4.4.1_A2.3_T2.js - // test/built-ins/Promise/all/S25.4.4.1_A2.3_T3.js - // test/built-ins/Promise/all/S25.4.4.1_A7.1_T1.js - expect( - await value(` - const input = [] - const emptyPromise = Promise.all(input) - const empty = await emptyPromise - const onePromise = Promise.all([Promise.resolve(3)]) - const one = await onePromise - return [ - emptyPromise instanceof Promise, - empty instanceof Array, - empty.length, - empty !== input, - onePromise instanceof Promise, - one instanceof Array, - one.length, - one[0], - ] - `), - ).toEqual([true, true, 0, true, true, true, 1, 3]) - }) - - test("Promise.all adopts values and preserves input order and identity", async () => { - // Sources: - // test/built-ins/Promise/all/resolve-non-thenable.js - // test/built-ins/Promise/all/S25.4.4.1_A8.2_T1.js - // test/built-ins/Promise/all/S25.4.4.1_A8.2_T2.js - const result = await value(` - const first = { id: 1 } - const second = { id: 2 } - const values = await Promise.all([Promise.resolve(3), first, Promise.resolve(second)]) - const observe = async (promise) => { - try { await promise; return "fulfilled" } catch (reason) { return reason } - } - return [ - values.length, - values[0], - values[1] === first, - values[2] === second, - await observe(Promise.all([Promise.reject(1), Promise.resolve(2)])), - await observe(Promise.all([Promise.resolve(1), Promise.reject(2)])), - ] - `) - expect(result).toEqual([3, 3, true, true, 1, 2]) - }) - - test("Promise.allSettled returns fresh arrays and ordered outcome records", async () => { - // Sources: - // test/built-ins/Promise/allSettled/resolves-empty-array.js - // test/built-ins/Promise/allSettled/resolves-to-array.js - // test/built-ins/Promise/allSettled/resolved-all-fulfilled.js - // test/built-ins/Promise/allSettled/resolved-all-rejected.js - // test/built-ins/Promise/allSettled/resolved-all-mixed.js - // test/built-ins/Promise/allSettled/resolve-non-thenable.js - expect( - await value(` - const input = [] - const empty = await Promise.allSettled(input) - const reason = { id: 4 } - const object = { id: 5 } - const outcomes = await Promise.allSettled([ - Promise.resolve(1), - Promise.reject(2), - 3, - Promise.reject(reason), - object, - ]) - return [ - empty instanceof Array, - empty.length, - empty !== input, - outcomes, - outcomes[4].value === object, - outcomes.map((item) => Object.keys(item)), - ] - `), - ).toEqual([ - true, - 0, - true, - [ - { status: "fulfilled", value: 1 }, - { status: "rejected", reason: 2 }, - { status: "fulfilled", value: 3 }, - { status: "rejected", reason: { id: 4 } }, - { status: "fulfilled", value: { id: 5 } }, - ], - true, - [ - ["status", "value"], - ["status", "reason"], - ["status", "value"], - ["status", "reason"], - ["status", "value"], - ], - ]) - }) - - test("Promise.race preserves fulfillment, rejection, and iterable order", async () => { - // Sources: - // test/built-ins/Promise/race/S25.4.4.3_A6.2_T1.js - // test/built-ins/Promise/race/S25.4.4.3_A7.1_T1.js - // test/built-ins/Promise/race/S25.4.4.3_A7.2_T1.js - // test/built-ins/Promise/race/S25.4.4.3_A7.3_T1.js - // test/built-ins/Promise/race/S25.4.4.3_A7.3_T2.js - expect( - await value(` - const observe = async (promise) => { - try { return ["fulfilled", await promise] } catch (reason) { return ["rejected", reason] } - } - return await Promise.all([ - observe(Promise.race([23])), - observe(Promise.race([Promise.reject(7)])), - observe(Promise.race([Promise.resolve(1), Promise.resolve(2)])), - observe(Promise.race([Promise.reject(3), Promise.resolve(4)])), - ]) - `), - ).toEqual([ - ["fulfilled", 23], - ["rejected", 7], - ["fulfilled", 1], - ["rejected", 3], - ]) - }) - - test("combinators consume supported string iterables", async () => { - // Sources: - // test/built-ins/Promise/all/iter-arg-is-string-resolve.js - // test/built-ins/Promise/allSettled/iter-arg-is-string-resolve.js - // test/built-ins/Promise/race/iter-arg-is-string-resolve.js - expect( - await value(` - return [ - await Promise.all("abc"), - await Promise.allSettled("ab"), - await Promise.race("abc"), - ] - `), - ).toEqual([ - ["a", "b", "c"], - [ - { status: "fulfilled", value: "a" }, - { status: "fulfilled", value: "b" }, - ], - "a", - ]) - }) - - test("Promise.resolve adopts values and preserves sandbox-promise identity", async () => { - // Sources: - // test/built-ins/Promise/resolve/S25.4.4.5_A2.1_T1.js - // test/built-ins/Promise/resolve/resolve-non-obj.js - // test/built-ins/Promise/resolve/resolve-non-thenable.js - expect( - await value(` - const object = { id: 1 } - const promise = Promise.resolve(1) - return [ - await Promise.resolve(23), - await Promise.resolve(Promise.resolve(24)), - (await Promise.resolve(object)) === object, - [promise].includes(Promise.resolve(promise)), - ] - `), - ).toEqual([23, 24, true, true]) - }) - - test("Promise.reject preserves primitive and object reasons", async () => { - // Sources: - // test/built-ins/Promise/reject/S25.4.4.4_A2.1_T1.js - const result = await value(` - const object = { reason: true } - const reasons = [undefined, null, false, true, 0, "", 42, object] - const observe = async (reason) => { - try { await Promise.reject(reason); return false } catch (caught) { return caught === reason } - } - return await Promise.all(reasons.map(observe)) - `) - expect(result).toEqual([true, true, true, true, true, true, true, true]) - }) - - test("Promise.all resolves duplicate members into every slot", async () => { - // Sources: - // test/built-ins/Promise/all/invoke-resolve-on-promises-every-iteration-of-promise.js - // test/built-ins/Promise/all/invoke-resolve-on-values-every-iteration-of-promise.js - // (adapted: CodeMode has no observable Promise.resolve hook, so per-iteration - // handling of a repeated member is asserted through the resolved slots) - expect( - await value(` - const settled = Promise.resolve(3) - const computed = (async () => "computed")() - return [ - await Promise.all([settled, settled, settled]), - await Promise.all([computed, "plain", computed]), - ] - `), - ).toEqual([ - [3, 3, 3], - ["computed", "plain", "computed"], - ]) - }) - - test("Promise.allSettled records duplicate members independently", async () => { - // Source: test/built-ins/Promise/allSettled/invoke-resolve-on-promises-every-iteration-of-promise.js - // (adapted: per-iteration handling of a repeated member is asserted through the - // outcome records instead of a Promise.resolve hook) - expect( - await value(` - const good = Promise.resolve(1) - const bad = Promise.reject(2) - return await Promise.allSettled([good, bad, good, bad]) - `), - ).toEqual([ - { status: "fulfilled", value: 1 }, - { status: "rejected", reason: 2 }, - { status: "fulfilled", value: 1 }, - { status: "rejected", reason: 2 }, - ]) - }) - - test("combinators adopt members that settled before the call", async () => { - // Sources: - // test/built-ins/Promise/all/reject-immed.js - // test/built-ins/Promise/allSettled/reject-immed.js - // test/built-ins/Promise/race/reject-immed.js - // (adapted: immediately-rejecting thenables become sandbox promises that settled, - // and were even observed, before the combinator call) - expect( - await value(` - const fulfilled = Promise.resolve("done") - const rejected = Promise.reject("failed") - try { await rejected } catch {} - const observe = async (promise) => { - try { return ["fulfilled", await promise] } catch (reason) { return ["rejected", reason] } - } - return [ - await observe(Promise.all([fulfilled, rejected])), - await Promise.allSettled([rejected, fulfilled]), - await observe(Promise.race([rejected, fulfilled])), - ] - `), - ).toEqual([ - ["rejected", "failed"], - [ - { status: "rejected", reason: "failed" }, - { status: "fulfilled", value: "done" }, - ], - ["rejected", "failed"], - ]) - }) - - test("combinator results follow input order, not settlement order", async () => { - // Sources: - // test/built-ins/Promise/all/resolve-non-thenable.js - // test/built-ins/Promise/allSettled/resolved-all-mixed.js - // (adapted: members are created, and therefore settle, in reverse of input order; - // deferred settlement is not expressible without host-async work in this corpus) - expect( - await value(` - const third = Promise.resolve("c") - const failing = (async () => { throw "b" })() - try { await failing } catch {} - const second = (async () => "b")() - const first = Promise.resolve("a") - return [ - await Promise.all([first, second, third]), - await Promise.allSettled([first, failing, third]), - ] - `), - ).toEqual([ - ["a", "b", "c"], - [ - { status: "fulfilled", value: "a" }, - { status: "rejected", reason: "b" }, - { status: "fulfilled", value: "c" }, - ], - ]) - }) - - test("Promise.race ignores a rejected loser once the first contender wins", async () => { - // Source: test/built-ins/Promise/race/reject-ignored-immed.js - // (adapted: the losing rejection comes from an async function instead of a thenable; - // the exact-equality check also asserts the loser leaves no unhandled-rejection warning) - expect( - await execute(` - const loser = (async () => { throw "lost" })() - return await Promise.race([Promise.resolve("won"), loser]) - `), - ).toEqual({ ok: true, value: "won", toolCalls: [] }) - }) - - test("Promise.race([]) returns a promise whose CodeMode failure is catchable", async () => { - // Sources: - // test/built-ins/Promise/race/S25.4.4.3_A2.1_T1.js - // test/built-ins/Promise/race/S25.4.4.3_A5.1_T1.js - // (adapted: upstream requires Promise.race([]) to never settle; CodeMode intentionally - // rejects with a catchable diagnostic instead of hanging, so this asserts the sandbox - // divergence rather than the spec never-settles behavior) - expect( - await value(` - const empty = Promise.race([]) - try { - await empty - return "settled" - } catch (error) { - return [empty instanceof Promise, error instanceof Error] - } - `), - ).toEqual([true, true]) - }) - - test("Promise.resolve passes the same sandbox promise through nested chains", async () => { - // Source: test/built-ins/Promise/resolve/S25.4.4.5_A2.2_T1.js - // (adapted: no executor construction, and identity is observed with Array includes - // because promises are not comparable data values in CodeMode) - expect( - await value(` - const promise = Promise.resolve({ id: 1 }) - return [ - [promise].includes(Promise.resolve(promise)), - [promise].includes(Promise.resolve(Promise.resolve(promise))), - (await Promise.resolve(Promise.resolve(promise))).id, - ] - `), - ).toEqual([true, true, 1]) - }) - - test("Promise.resolve of a rejected promise preserves identity and reason", async () => { - // Source: test/built-ins/Promise/resolve/S25.4.4.5_A2.3_T1.js - // (adapted: the source promise is already rejected instead of rejected later) - expect( - await value(` - const rejected = Promise.reject("oops") - const adopted = Promise.resolve(rejected) - const identity = [rejected].includes(adopted) - try { - await adopted - return "fulfilled" - } catch (reason) { - return [identity, reason] - } - `), - ).toEqual([true, "oops"]) - }) - - test("Promise.reject uses a promise reason without flattening it", async () => { - // Sources: - // test/built-ins/Promise/reject-via-fn-immed.js - // test/built-ins/Promise/reject-via-fn-deferred.js - // (adapted: the promise reason goes through Promise.reject instead of executor reject) - expect( - await value(` - const observe = async (reason) => { - try { - await Promise.reject(reason) - return "fulfilled" - } catch (caught) { - const identity = [reason].includes(caught) - try { return [identity, caught instanceof Promise, await caught] } - catch (inner) { return [identity, caught instanceof Promise, "rethrew " + inner] } - } - } - return [await observe(Promise.resolve(1)), await observe(Promise.reject("inner"))] - `), - ).toEqual([ - [true, true, 1], - [true, true, "rethrew inner"], - ]) - }) -}) - -describe("Test262 async functions and await", () => { - test("declaration, expression, and arrow forms return promises", async () => { - // Sources: - // test/language/statements/async-function/declaration-returns-promise.js - // test/language/expressions/async-function/expression-returns-promise.js - // test/language/expressions/async-arrow-function/arrow-returns-promise.js - expect( - await value(` - async function declaration() { return 1 } - const expression = async function() { return 2 } - const arrow = async () => 3 - const promises = [declaration(), expression(), arrow()] - return [promises.map((item) => item instanceof Promise), await Promise.all(promises)] - `), - ).toEqual([ - [true, true, true], - [1, 2, 3], - ]) - }) - - test("async bodies adopt returns and reject throws before and after await", async () => { - // Sources: - // test/language/statements/async-function/evaluation-body.js - // test/language/statements/async-function/evaluation-body-that-returns.js - // test/language/statements/async-function/evaluation-body-that-returns-after-await.js - // test/language/statements/async-function/evaluation-body-that-throws.js - // test/language/statements/async-function/evaluation-body-that-throws-after-await.js - expect( - await value(` - const order = [] - const plain = async () => { order.push("body"); return 42 } - const afterAwait = async () => { await Promise.resolve(); return 43 } - const throwsBefore = async () => { throw 1 } - const throwsAfter = async () => { await Promise.resolve(); throw 2 } - const observe = async (promise) => { - try { return ["fulfilled", await promise] } catch (reason) { return ["rejected", reason] } - } - const first = plain() - return [ - order, - await observe(first), - await observe(afterAwait()), - await observe(throwsBefore()), - await observe(throwsAfter()), - ] - `), - ).toEqual([["body"], ["fulfilled", 42], ["fulfilled", 43], ["rejected", 1], ["rejected", 2]]) - }) - - test("default-parameter throws reject instead of escaping the call", async () => { - // Source: test/language/statements/async-function/evaluation-default-that-throws.js - expect( - await value(` - const fail = () => { throw new Error("default") } - const run = async (value = fail()) => value - let returned = false - try { - const promise = run() - returned = promise instanceof Promise - await promise - return [returned, "fulfilled"] - } catch (error) { - return [returned, error.message] - } - `), - ).toEqual([true, "default"]) - }) - - test("async try/finally completion records override earlier completion", async () => { - // Sources: the try-{return,throw,reject}-finally-{return,throw,reject}.js matrix under - // test/language/statements/async-function, test/language/expressions/async-function, - // and test/language/expressions/async-arrow-function. - expect( - await value(` - const observe = async (promise) => { - try { return ["fulfilled", await promise] } catch (reason) { return ["rejected", reason] } - } - const returnReturn = async () => { try { return "early" } finally { return await Promise.resolve("override") } } - const returnThrow = async () => { try { return "early" } finally { throw "override" } } - const returnReject = async () => { try { return "early" } finally { await Promise.reject("override") } } - const throwReturn = async () => { try { throw "early" } finally { return await Promise.resolve("override") } } - const throwThrow = async () => { try { throw "early" } finally { throw "override" } } - const throwReject = async () => { try { throw "early" } finally { await Promise.reject("override") } } - const rejectReturn = async () => { try { await Promise.reject("early") } finally { return await Promise.resolve("override") } } - const rejectThrow = async () => { try { await Promise.reject("early") } finally { throw "override" } } - const rejectReject = async () => { try { await Promise.reject("early") } finally { await Promise.reject("override") } } - return await Promise.all([ - observe(returnReturn()), observe(returnThrow()), observe(returnReject()), - observe(throwReturn()), observe(throwThrow()), observe(throwReject()), - observe(rejectReturn()), observe(rejectThrow()), observe(rejectReject()), - ]) - `), - ).toEqual([ - ["fulfilled", "override"], - ["rejected", "override"], - ["rejected", "override"], - ["fulfilled", "override"], - ["rejected", "override"], - ["rejected", "override"], - ["fulfilled", "override"], - ["rejected", "override"], - ["rejected", "override"], - ]) - }) - - test("await preserves an object whose then property is not callable", async () => { - // Source: test/language/expressions/await/await-awaits-thenable-not-callable.js - expect( - await value(` - const thenable = { then: 42 } - return (await thenable) === thenable - `), - ).toBe(true) - }) - - test("await returns non-promise operands unchanged", async () => { - // Source: test/language/expressions/await/await-non-promise.js - // (adapted: only value pass-through is asserted here; the spec tick ordering around - // await of non-promises is covered by the failing interleaving test below) - expect( - await value(` - const object = { id: 1 } - const array = [1, 2] - return [ - await 1, - await "text", - await true, - (await null) === null, - (await undefined) === undefined, - (await object) === object, - (await array) === array, - ] - `), - ).toEqual([1, "text", true, true, true, true, true]) - }) -}) - -describe("Test262 expected Promise conformance", () => { - for (const name of ["all", "allSettled", "race", "any"] as const) { - test(`Promise.${name} rejects invalid input with TypeError`, async () => { - // Sources: - // test/built-ins/Promise/all/S25.4.4.1_A3.1_T1.js - // test/built-ins/Promise/all/S25.4.4.1_A3.1_T2.js - // test/built-ins/Promise/allSettled/iter-arg-is-number-reject.js - // test/built-ins/Promise/race/iter-arg-is-number-reject.js - // test/built-ins/Promise/any/iter-arg-is-number-reject.js - expect( - await value(` - try { - const promise = Promise.${name}(42) - const returned = promise instanceof Promise - await promise - return [returned, "fulfilled"] - } catch (error) { - return [true, error.name] - } - `), - ).toEqual([true, "TypeError"]) - }) - } - - test("Promise.all consumes sparse positions as undefined", async () => { - // Source: test/built-ins/Array/from/from-array.js (array iterator hole behavior) - expect( - await value(` - const input = [] - input[1] = 1 - const result = await Promise.all(input) - return [result.length, result[0] === undefined, result[1]] - `), - ).toEqual([2, true, 1]) - }) - - test("Promise.allSettled consumes sparse positions as undefined", async () => { - // Source: test/built-ins/Array/from/from-array.js (array iterator hole behavior) - expect( - await value(` - const input = [] - input[1] = 1 - const result = await Promise.allSettled(input) - return [result.length, result[0].status, result[0].value === undefined, result[1]] - `), - ).toEqual([2, "fulfilled", true, { status: "fulfilled", value: 1 }]) - }) - - test("Promise.race consumes a sparse first position as undefined", async () => { - // Source: test/built-ins/Array/from/from-array.js (array iterator hole behavior) - expect( - await value(` - const input = [] - input[1] = 1 - return (await Promise.race(input)) === undefined - `), - ).toBe(true) - }) - - test("Promise.any consumes a sparse first position as an undefined fulfillment", async () => { - // Source: test/built-ins/Array/from/from-array.js (array iterator hole behavior) - expect( - await value(` - const input = [] - input[1] = Promise.reject("loses") - return (await Promise.any(input)) === undefined - `), - ).toBe(true) - }) - - test("Promise.all settles after reactions attached to its inputs", async () => { - // Sources: - // test/built-ins/Promise/all/S25.4.4.1_A7.2_T1.js - // test/built-ins/Promise/all/S25.4.4.1_A8.1_T1.js - expect( - await value(` - const sequence = [1] - const input = Promise.resolve(1) - const aggregate = Promise.all([input]) - aggregate.then(() => sequence.push(4)) - input.then(() => sequence.push(3)).then(() => sequence.push(5)) - sequence.push(2) - await aggregate - await Promise.resolve() - return sequence - `), - ).toEqual([1, 2, 3, 4, 5]) - }) - - test("Promise.allSettled settles after reactions attached to its inputs", async () => { - // Sources: - // test/built-ins/Promise/allSettled/resolved-sequence.js - // test/built-ins/Promise/allSettled/resolved-sequence-extra-ticks.js - // test/built-ins/Promise/allSettled/resolved-sequence-mixed.js - // test/built-ins/Promise/allSettled/resolved-sequence-with-rejections.js - expect( - await value(` - const sequence = [1] - const input = Promise.resolve(1) - const aggregate = Promise.allSettled([input]) - aggregate.then(() => sequence.push(4)) - input.then(() => sequence.push(3)).then(() => sequence.push(5)) - sequence.push(2) - await aggregate - await Promise.resolve() - return sequence - `), - ).toEqual([1, 2, 3, 4, 5]) - }) - - test("Promise.race settles in a reaction after its winning input", async () => { - // Sources: - // test/built-ins/Promise/race/S25.4.4.3_A6.1_T1.js - // test/built-ins/Promise/race/resolved-sequence-extra-ticks.js - expect( - await value(` - const sequence = [1] - const race = Promise.race([1]) - race.then(() => sequence.push(4)) - Promise.resolve().then(() => sequence.push(3)).then(() => sequence.push(5)) - sequence.push(2) - await race - await Promise.resolve() - return sequence - `), - ).toEqual([1, 2, 3, 4, 5]) - }) - - test("then reactions route and propagate fulfillment and rejection", async () => { - // Sources: - // test/built-ins/Promise/prototype/then/prfm-fulfilled.js - // test/built-ins/Promise/prototype/then/prfm-rejected.js - // test/built-ins/Promise/prototype/then/rxn-handler-identity.js - // test/built-ins/Promise/prototype/then/rxn-handler-thrower.js - // test/built-ins/Promise/prototype/then/rxn-handler-fulfilled-return-normal.js - // test/built-ins/Promise/prototype/then/rxn-handler-fulfilled-return-abrupt.js - // test/built-ins/Promise/prototype/then/rxn-handler-rejected-return-normal.js - // test/built-ins/Promise/prototype/then/rxn-handler-rejected-return-abrupt.js - expect( - await value(` - const observe = async (promise) => { - try { return ["fulfilled", await promise] } catch (reason) { return ["rejected", reason] } - } - return await Promise.all([ - observe(Promise.resolve(1).then((value) => value + 1)), - observe(Promise.reject(2).then(undefined, (reason) => reason + 1)), - observe(Promise.resolve(3).then(undefined)), - observe(Promise.reject(4).then(undefined)), - observe(Promise.resolve(5).then(() => { throw 6 })), - observe(Promise.reject(7).then(undefined, () => { throw 8 })), - ]) - `), - ).toEqual([ - ["fulfilled", 2], - ["fulfilled", 3], - ["fulfilled", 3], - ["rejected", 4], - ["rejected", 6], - ["rejected", 8], - ]) - }) - - test("then reactions preserve breadth-first queue order", async () => { - // Source: test/built-ins/Promise/prototype/then/S25.4.4_A1.1_T1.js - expect( - await value(` - const sequence = [1] - const promise = Promise.resolve() - const first = promise.then(() => sequence.push(3)).then(() => sequence.push(5)).then(() => sequence.push(7)) - const second = promise.then(() => sequence.push(4)).then(() => sequence.push(6)).then(() => sequence.push(8)) - sequence.push(2) - await Promise.all([first, second]) - return sequence - `), - ).toEqual([1, 2, 3, 4, 5, 6, 7, 8]) - }) - - test("then rejects direct self-resolution for fulfilled and rejected sources", async () => { - // Sources: - // test/built-ins/Promise/prototype/then/resolve-settled-fulfilled-self.js - // test/built-ins/Promise/prototype/then/resolve-settled-rejected-self.js - // test/built-ins/Promise/prototype/then/resolve-pending-fulfilled-self.js - // test/built-ins/Promise/prototype/then/resolve-pending-rejected-self.js - expect( - await value(` - const observe = async (promise) => { - try { await promise; return "fulfilled" } catch (reason) { return reason.name } - } - let fulfilled - let rejected - fulfilled = Promise.resolve().then(() => fulfilled) - rejected = Promise.reject().then(undefined, () => rejected) - return await Promise.all([observe(fulfilled), observe(rejected)]) - `), - ).toEqual(["TypeError", "TypeError"]) - }) - - test("catch delegates rejection handling and preserves fulfillment", async () => { - // Sources: - // test/built-ins/Promise/prototype/catch/S25.4.5.1_A2.1_T1.js - // test/built-ins/Promise/prototype/catch/S25.4.5.1_A3.1_T1.js - // test/built-ins/Promise/prototype/catch/S25.4.5.1_A3.1_T2.js - expect( - await value(` - return [ - await Promise.resolve(1).catch(() => 2), - await Promise.reject(3).catch((reason) => reason + 1), - ] - `), - ).toEqual([1, 4]) - }) - - test("finally preserves or replaces the original settlement", async () => { - // Sources: - // test/built-ins/Promise/prototype/finally/resolution-value-no-override.js - // test/built-ins/Promise/prototype/finally/rejection-reason-no-fulfill.js - // test/built-ins/Promise/prototype/finally/rejection-reason-override-with-throw.js - expect( - await value(` - const observe = async (promise) => { - try { return ["fulfilled", await promise] } catch (reason) { return ["rejected", reason] } - } - return await Promise.all([ - observe(Promise.resolve(1).finally(() => 2)), - observe(Promise.reject(3).finally(() => 4)), - observe(Promise.reject(5).finally(() => { throw 6 })), - ]) - `), - ).toEqual([ - ["fulfilled", 1], - ["rejected", 3], - ["rejected", 6], - ]) - }) - - test("then ignores non-callable handlers", async () => { - // Sources: - // test/built-ins/Promise/prototype/then/S25.4.5.3_A4.1_T1.js - // test/built-ins/Promise/prototype/then/S25.4.5.3_A4.1_T2.js - // test/built-ins/Promise/prototype/then/S25.4.5.3_A5.1_T1.js - // test/built-ins/Promise/prototype/then/S25.4.5.3_A5.2_T1.js - // (adapted: only non-callable handlers are probed; callables that are not plain - // functions, such as tool references, intentionally throw in CodeMode) - expect( - await value(` - const observe = async (promise) => { - try { return ["fulfilled", await promise] } catch (reason) { return ["rejected", reason] } - } - return await Promise.all([ - observe(Promise.resolve(1).then(2)), - observe(Promise.resolve(4).then(null, null)), - observe(Promise.resolve(5).then({}, "x")), - observe(Promise.reject(3).then(null, "x")), - observe(Promise.reject(6).then(7, {})), - ]) - `), - ).toEqual([ - ["fulfilled", 1], - ["fulfilled", 4], - ["fulfilled", 5], - ["rejected", 3], - ["rejected", 6], - ]) - }) - - test("finally waits for a returned promise and preserves or replaces settlement", async () => { - // Sources: - // test/built-ins/Promise/prototype/finally/resolved-observable-then-calls.js - // test/built-ins/Promise/prototype/finally/rejected-observable-then-calls.js - // test/built-ins/Promise/prototype/finally/resolution-value-no-override.js - expect( - await value(` - const observe = async (promise) => { - try { return ["fulfilled", await promise] } catch (reason) { return ["rejected", reason] } - } - const order = [] - const cleanup = async () => { - await Promise.resolve() - order.push("cleanup") - } - const settled = await Promise.resolve("kept").finally(() => cleanup()) - order.push("settled:" + settled) - return [ - await observe(Promise.resolve(1).finally(() => Promise.resolve(99))), - order, - await observe(Promise.resolve(2).finally(() => Promise.reject(3))), - await observe(Promise.reject(4).finally(() => Promise.resolve(99))), - ] - `), - ).toEqual([ - ["fulfilled", 1], - ["cleanup", "settled:kept"], - ["rejected", 3], - ["rejected", 4], - ]) - }) - - test("then adopts a returned rejected promise", async () => { - // Sources: - // test/built-ins/Promise/prototype/then/rxn-handler-fulfilled-return-abrupt.js - // test/built-ins/Promise/resolve/resolve-promise.js - // (adapted: the fulfillment handler returns an already-rejected promise instead of - // throwing, and the rejection handler recovers with a fulfilled promise) - expect( - await value(` - const observe = async (promise) => { - try { return ["fulfilled", await promise] } catch (reason) { return ["rejected", reason] } - } - return await Promise.all([ - observe(Promise.resolve(1).then(() => Promise.reject("bad"))), - observe(Promise.reject(2).then(undefined, () => Promise.resolve("ok"))), - ]) - `), - ).toEqual([ - ["rejected", "bad"], - ["fulfilled", "ok"], - ]) - }) - - test("independent reactions on one source each observe the same settlement", async () => { - // Source: test/built-ins/Promise/prototype/then/S25.4.4_A2.1_T1.js - // (adapted: the multiple-reactions family is asserted through the values every - // reaction returns instead of a shared completion counter) - expect( - await value(` - const fulfilled = Promise.resolve(7) - const rejected = Promise.reject(8) - return await Promise.all([ - fulfilled.then((value) => "first:" + value), - fulfilled.then((value) => "second:" + value), - rejected.catch((reason) => "first:" + reason), - rejected.catch((reason) => "second:" + reason), - ]) - `), - ).toEqual(["first:7", "second:7", "first:8", "second:8"]) - }) - - test("await always resumes in a later reaction and interleaves async functions", async () => { - // Sources: - // test/language/expressions/await/async-await-interleaved.js - // test/language/expressions/await/await-non-promise.js - expect( - await value(` - const sequence = [] - const first = async () => { sequence.push("first:1"); await 0; sequence.push("first:2") } - const second = async () => { sequence.push("second:1"); await 0; sequence.push("second:2") } - await Promise.all([first(), second()]) - return sequence - `), - ).toEqual(["first:1", "second:1", "first:2", "second:2"]) - }) - - test("an async function rejects when it resolves with its own promise", async () => { - // Adapted from the self-resolution requirement represented by: - // test/built-ins/Promise/resolve-self.js - // test/built-ins/Promise/resolve/S25.4.4.5_A4.1_T1.js - expect( - await value(` - let promise - const run = async () => { - await Promise.resolve() - return promise - } - promise = run() - try { - await promise - return "fulfilled" - } catch (error) { - return error.name - } - `), - ).toBe("TypeError") - }) - - test.failing("Promise.resolve recursively assimilates callable thenables", async () => { - // Source: test/built-ins/Promise/resolve/resolve-thenable.js - expect( - await value(` - const value = { id: 1 } - const nested = { then: (resolve) => resolve(value) } - const thenable = { then: (resolve) => resolve(nested) } - return (await Promise.resolve(thenable)) === value - `), - ).toBe(true) - }) - - test.failing("Promise combinators assimilate callable thenable inputs", async () => { - // Sources: - // test/built-ins/Promise/all/reject-immed.js - // test/built-ins/Promise/all/reject-ignored-immed.js - // test/built-ins/Promise/allSettled/reject-ignored-immed.js - // test/built-ins/Promise/race/resolve-thenable.js - expect( - await value(` - const fulfills = { then: (resolve) => resolve(1) } - const rejects = { then: (_, reject) => reject(2) } - const resolvesFirst = { then: (resolve, reject) => { resolve(3); reject(4) } } - const observe = async (promise) => { - try { return ["fulfilled", await promise] } catch (reason) { return ["rejected", reason] } - } - return [ - await observe(Promise.all([fulfills, rejects])), - await Promise.allSettled([fulfills, resolvesFirst]), - await observe(Promise.race([rejects])), - ] - `), - ).toEqual([ - ["rejected", 2], - [ - { status: "fulfilled", value: 1 }, - { status: "fulfilled", value: 3 }, - ], - ["rejected", 2], - ]) - }) - - test.failing("await assimilates callable thenables", async () => { - // Source: test/language/expressions/await/await-awaits-thenables.js - expect( - await value(` - const thenable = { then: (resolve) => resolve(42) } - return await thenable - `), - ).toBe(42) - }) - - test.failing("await rejects when a callable thenable throws", async () => { - // Source: test/language/expressions/await/await-awaits-thenables-that-throw.js - expect( - await value(` - const error = { id: 1 } - const thenable = { then: () => { throw error } } - try { - await thenable - return false - } catch (caught) { - return caught === error - } - `), - ).toBe(true) - }) -}) - -describe("Test262 Promise.any", () => { - test("is a callable static that returns a promise", async () => { - // Sources: - // test/built-ins/Promise/any/is-function.js - // test/built-ins/Promise/any/returns-promise.js - expect( - await value(` - const promise = Promise.any([1]) - return [typeof Promise.any, promise instanceof Promise, await promise] - `), - ).toEqual(["function", true, 1]) - }) - - test("fulfills with the first fulfilled member, ignoring rejections", async () => { - // Sources: - // test/built-ins/Promise/any/resolved-sequence-mixed.js - // test/built-ins/Promise/any/resolved-sequence-with-rejections.js - // test/built-ins/Promise/any/reject-ignored-immed.js - expect( - await value(` - return [ - await Promise.any([Promise.reject("a"), Promise.resolve(1), Promise.resolve(2)]), - await Promise.any([Promise.reject("a"), "plain", Promise.reject("b")]), - ] - `), - ).toEqual([1, "plain"]) - }) - - test("a fulfillment wins over a later rejection of another member", async () => { - // Sources: - // test/built-ins/Promise/any/resolve-ignores-late-rejection.js - // test/built-ins/Promise/any/resolve-ignores-late-rejection-deferred.js - expect( - await value(` - let rejectLate - const late = new Promise((_, reject) => { rejectLate = reject }) - const result = await Promise.any([late, Promise.resolve("won")]) - rejectLate("too late") - return result - `), - ).toBe("won") - }) - - test("rejects with an AggregateError carrying the reasons in input order", async () => { - // Sources: - // test/built-ins/Promise/any/reject-all-mixed.js - // test/built-ins/Promise/any/reject-immed.js - // test/built-ins/Promise/any/reject-deferred.js - expect( - await value(` - let rejectLate - const late = new Promise((_, reject) => { rejectLate = reject }) - const aggregate = Promise.any([Promise.reject("first"), late, Promise.reject("third")]) - rejectLate("second") - try { - await aggregate - return "fulfilled" - } catch (error) { - return { - isAggregate: error instanceof AggregateError, - isError: error instanceof Error, - name: error.name, - message: error.message, - errors: error.errors, - } - } - `), - ).toEqual({ - isAggregate: true, - isError: true, - name: "AggregateError", - message: "All promises were rejected", - errors: ["first", "second", "third"], - }) - }) - - test("rejects an empty input with an empty AggregateError", async () => { - // Source: test/built-ins/Promise/any/iter-arg-is-empty-iterable-reject.js - expect( - await value(` - try { - await Promise.any([]) - return "fulfilled" - } catch (error) { - return [error instanceof AggregateError, error.errors.length] - } - `), - ).toEqual([true, 0]) - }) - - test("consumes a string input as its characters", async () => { - // Source: test/built-ins/Promise/any/iter-arg-is-string-resolve.js - expect(await value(`return await Promise.any("abc")`)).toBe("a") - }) - - test("rejects an empty string input with an empty AggregateError", async () => { - // Source: test/built-ins/Promise/any/iter-arg-is-empty-string-reject.js - expect( - await value(` - try { - await Promise.any("") - return "fulfilled" - } catch (error) { - return [error instanceof AggregateError, error.errors.length] - } - `), - ).toEqual([true, 0]) - }) - - test("fulfills with the first member that does not reject", async () => { - // Sources: - // test/built-ins/Promise/any/resolve-from-reject-catch.js - // test/built-ins/Promise/any/resolve-from-resolve-reject-catch.js - expect( - await value(` - return await Promise.any([ - Promise.reject("a"), - new Promise((resolve, reject) => reject("b")), - Promise.all([Promise.reject("c")]), - Promise.resolve(Promise.reject("d").catch((reason) => reason)), - ]) - `), - ).toBe("d") - }) - - test("settles after reactions attached to its inputs", async () => { - // Source: test/built-ins/Promise/any/resolved-sequence.js - expect( - await value(` - const sequence = [1] - const input = Promise.resolve(1) - const aggregate = Promise.any([input]) - aggregate.then(() => sequence.push(4)) - input.then(() => sequence.push(3)).then(() => sequence.push(5)) - sequence.push(2) - await aggregate - await Promise.resolve() - return sequence - `), - ).toEqual([1, 2, 3, 4, 5]) - }) -}) - -describe("Test262 AggregateError", () => { - test("constructs from an errors collection and an optional message", async () => { - // Sources: - // test/built-ins/AggregateError/errors-iterabletolist.js - // test/built-ins/AggregateError/message-undefined-no-prop.js - expect( - await value(` - const input = ["x", "y"] - const withMessage = new AggregateError(input, "msg") - const bare = new AggregateError([]) - return [ - withMessage.name, - withMessage.message, - withMessage.errors, - withMessage.errors !== input, - withMessage instanceof AggregateError, - withMessage instanceof Error, - bare.message, - bare.errors, - ] - `), - ).toEqual(["AggregateError", "msg", ["x", "y"], true, true, true, "", []]) - }) - - test("rejects a non-collection errors argument with TypeError", async () => { - // Source: test/built-ins/AggregateError/errors-iterabletolist-failures.js - expect( - await value(` - try { - new AggregateError(42) - return "constructed" - } catch (error) { - return error.name - } - `), - ).toBe("TypeError") - }) - - test("is callable without new", async () => { - // Source: test/built-ins/AggregateError/newtarget-is-undefined.js - expect( - await value(` - const error = AggregateError(["x"], "m") - return [error instanceof AggregateError, error instanceof Error, error.name, error.message, error.errors] - `), - ).toEqual([true, true, "AggregateError", "m", ["x"]]) - }) - - test("coerces a non-string message to a string", async () => { - // Source: test/built-ins/AggregateError/message-method-prop-cast.js (value coercion only; the - // upstream object-with-toString case is omitted because the sandbox has no user toString dispatch) - expect( - await value(` - return [ - new AggregateError([], 42).message, - new AggregateError([], false).message, - new AggregateError([], true).message, - new AggregateError([], null).message, - ] - `), - ).toEqual(["42", "false", "true", "null"]) - }) -}) - -describe("Test262 Promise constructor", () => { - test("constructs a promise, handing the executor callable resolve/reject", async () => { - // Sources: - // test/built-ins/Promise/constructor.js - // test/built-ins/Promise/exec-args.js - expect( - await value(` - let observed - const promise = new Promise((resolve, reject) => { - observed = [typeof resolve, typeof reject] - resolve("done") - }) - return [promise instanceof Promise, observed, await promise] - `), - ).toEqual([true, ["function", "function"], "done"]) - }) - - test("a missing or non-callable executor is a TypeError", async () => { - // Source: test/built-ins/Promise/executor-not-callable.js - expect( - await value(` - const outcomes = [] - for (const make of [() => new Promise(), () => new Promise(1), () => new Promise({})]) { - try { - make() - outcomes.push("constructed") - } catch (error) { - outcomes.push(error.name) - } - } - return outcomes - `), - ).toEqual(["TypeError", "TypeError", "TypeError"]) - }) - - test("resolves immediately or later through an escaping resolver", async () => { - // Sources: - // test/built-ins/Promise/resolve-non-thenable-immed.js - // test/built-ins/Promise/resolve-non-thenable-deferred.js - // test/built-ins/Promise/create-resolving-functions-resolve.js - expect( - await value(` - let settle - const deferred = new Promise((resolve) => { settle = resolve }) - const immediate = new Promise((resolve) => resolve("now")) - settle("later") - return [await immediate, await deferred] - `), - ).toEqual(["now", "later"]) - }) - - test("rejects through reject and through an abrupt executor completion", async () => { - // Sources: - // test/built-ins/Promise/reject-via-fn-immed.js - // test/built-ins/Promise/reject-via-abrupt.js - expect( - await value(` - const observe = async (promise) => { - try { return ["fulfilled", await promise] } catch (reason) { return ["rejected", reason.message ?? reason] } - } - return [ - await observe(new Promise((_, reject) => reject("nope"))), - await observe(new Promise(() => { throw new Error("boom") })), - ] - `), - ).toEqual([ - ["rejected", "nope"], - ["rejected", "boom"], - ]) - }) - - test("only the first settlement counts", async () => { - // Sources: - // test/built-ins/Promise/reject-ignored-via-fn-immed.js - // test/built-ins/Promise/resolve-ignored-via-fn-immed.js - expect( - await value(` - return [ - await new Promise((resolve, reject) => { resolve("first"); reject("second"); resolve("third") }), - await new Promise((resolve) => { resolve(resolve("inner") === undefined ? "unreached" : "also unreached") }), - ] - `), - ).toEqual(["first", "inner"]) - }) - - test("escaped resolvers keep first-settle-wins in both directions", async () => { - // Sources: - // test/built-ins/Promise/reject-ignored-via-fn-deferred.js - // test/built-ins/Promise/resolve-ignored-via-fn-deferred.js - expect( - await value(` - let resolveRejected, rejectRejected - const rejected = new Promise((resolve, reject) => { resolveRejected = resolve; rejectRejected = reject }) - rejectRejected("first") - const lateResolve = resolveRejected("late") - let resolveFulfilled, rejectFulfilled - const fulfilled = new Promise((resolve, reject) => { resolveFulfilled = resolve; rejectFulfilled = reject }) - resolveFulfilled() - const lateReject = rejectFulfilled(new Promise(() => {})) - try { - await rejected - return "fulfilled" - } catch (reason) { - return [reason, lateResolve === undefined, (await fulfilled) === undefined, lateReject === undefined] - } - `), - ).toEqual(["first", true, true, true]) - }) - - test("a queued reaction chain observes a later rejection through a handler-less then", async () => { - // Sources: - // test/built-ins/Promise/reject-via-fn-immed-queue.js - // test/built-ins/Promise/reject-via-fn-deferred-queue.js - // test/built-ins/Promise/reject-via-abrupt-queue.js - expect( - await value(` - const observe = (promise) => promise.then(() => "wrong").then(() => "also wrong", (reason) => "caught:" + reason) - let reject - const deferred = new Promise((_, r) => { reject = r }) - const chained = observe(deferred) - reject("boom") - return [ - await observe(new Promise((_, r) => r("immed"))), - await chained, - await observe(new Promise(() => { throw "abrupt" })), - ] - `), - ).toEqual(["caught:immed", "caught:boom", "caught:abrupt"]) - }) - - test("an exception after resolve is ignored", async () => { - // Source: test/built-ins/Promise/exception-after-resolve-in-executor.js - expect(await value(`return await new Promise((resolve) => { resolve("kept"); throw new Error("dropped") })`)).toBe( - "kept", - ) - }) - - test("resolving with a promise adopts its settlement", async () => { - // Sources: - // test/built-ins/Promise/resolve-thenable-immed.js (promise-adoption portion) - // test/built-ins/Promise/all/S25.4.4.1_A2.3_T1.js (resolution adoption semantics) - expect( - await value(` - const adoptedValue = await new Promise((resolve) => resolve(Promise.resolve("adopted"))) - try { - await new Promise((resolve) => resolve(Promise.reject("bad"))) - return [adoptedValue, "fulfilled"] - } catch (reason) { - return [adoptedValue, reason] - } - `), - ).toEqual(["adopted", "bad"]) - }) - - test("resolving with the promise itself rejects with TypeError", async () => { - // Source: test/built-ins/Promise/resolve-self.js - expect( - await value(` - let settle - const promise = new Promise((resolve) => { settle = resolve }) - settle(promise) - try { - await promise - return "fulfilled" - } catch (error) { - return error.name - } - `), - ).toBe("TypeError") - }) - - test("executor runs synchronously before the constructor returns", async () => { - // Source: test/built-ins/Promise/executor-call-context-strict.js (synchronous Call(executor) step) - expect( - await value(` - const sequence = [] - sequence.push("before") - new Promise((resolve) => { sequence.push("executor"); resolve() }) - sequence.push("after") - return sequence - `), - ).toEqual(["before", "executor", "after"]) - }) - - test.failing("calling Promise without new throws TypeError", async () => { - // Source: test/built-ins/Promise/undefined-newtarget.js - // The sandbox currently reports a generic Error ("Only tools are callable in CodeMode."). - expect( - await value(` - try { - Promise(() => {}) - return "called" - } catch (error) { - return error.name - } - `), - ).toBe("TypeError") - }) -}) diff --git a/packages/codemode/test/promise.test.ts b/packages/codemode/test/promise.test.ts index db607336de..545d463abf 100644 --- a/packages/codemode/test/promise.test.ts +++ b/packages/codemode/test/promise.test.ts @@ -3,9 +3,8 @@ import { Effect, Schema } from "effect" import { CodeMode, Tool, toolError } from "../src/index.js" // Wave 5 acceptance suite: first-class promise values. Un-awaited tool calls start eagerly on -// supervised fibers, `await` settles them, Promise.all/allSettled/race/resolve/reject are -// ordinary functions over arbitrary arrays mixing promises and plain values, and -// .then/.catch/.finally chain reactions onto any promise. +// supervised fibers, `await` settles them, and Promise.all/allSettled/race/resolve/reject are +// ordinary functions over arbitrary arrays mixing promises and plain values. type Trace = { starts: Array @@ -49,40 +48,6 @@ const failingTool = Tool.make({ run: () => Effect.fail(toolError("Lookup refused")), }) -const interruptedTool = Tool.make({ - description: "Interrupt this call", - input: Schema.Struct({}), - output: Schema.String, - run: () => Effect.interrupt, -}) - -const completedTool = (trace: Trace) => - Tool.make({ - description: "Return the number of completed sleepy calls", - input: Schema.Struct({}), - output: Schema.Number, - run: () => Effect.succeed(trace.completed), - }) - -/** Never settles, and holds interruption cleanup for `cleanupMs` so completion cleanup can outlast a timeout. */ -const stubbornTool = (trace: Trace) => - Tool.make({ - description: "Never settle; clean up slowly when interrupted", - input: Schema.Struct({ cleanupMs: Schema.Number }), - output: Schema.Number, - run: ({ cleanupMs }) => - Effect.never.pipe( - Effect.onInterrupt(() => - Effect.andThen( - Effect.sleep(cleanupMs), - Effect.sync(() => { - trace.interrupted += 1 - }), - ), - ), - ), - }) - const run = ( code: string, options: { trace?: Trace; limits?: CodeMode.ExecutionLimits } = {}, @@ -90,15 +55,7 @@ const run = ( const trace = options.trace ?? makeTrace() return Effect.runPromise( CodeMode.execute({ - tools: { - host: { - sleepy: sleepyTool(trace), - fail: failingTool, - interrupt: interruptedTool, - completed: completedTool(trace), - stubborn: stubbornTool(trace), - }, - }, + tools: { host: { sleepy: sleepyTool(trace), fail: failingTool } }, code, ...(options.limits ? { limits: options.limits } : {}), }), @@ -118,42 +75,6 @@ const error = async (code: string, options: { trace?: Trace; limits?: CodeMode.E } describe("first-class promise values", () => { - test("async functions return promises with isolated concurrent invocations", async () => { - expect( - await value(` - const load = async (id) => { - const result = await tools.host.sleepy({ id, ms: 20 }) - return [id, result] - } - const first = load(1) - const second = load(2) - return [first instanceof Promise, second instanceof Promise, await Promise.all([first, second])] - `), - ).toEqual([ - true, - true, - [ - [1, 1], - [2, 2], - ], - ]) - }) - - test("async function errors reject instead of throwing at the call site", async () => { - expect( - await value(` - const fail = async () => { throw new Error("boom") } - const promise = fail() - try { - await promise - return "no" - } catch (error) { - return error.message - } - `), - ).toBe("boom") - }) - test("an un-awaited tool call starts eagerly, in call order, before any await", async () => { const trace = makeTrace() const result = await value( @@ -185,7 +106,7 @@ describe("first-class promise values", () => { expect(result.toolCalls).toStrictEqual([{ name: "host.sleepy" }]) }) - test("await of a non-promise value passes it through unchanged", async () => { + test("await of a non-promise value is a passthrough no-op", async () => { expect(await value(`return await 42`)).toBe(42) expect(await value(`const x = await "s"; return x`)).toBe("s") expect(await value(`return await null`)).toBeNull() @@ -209,7 +130,8 @@ describe("first-class promise values", () => { }) test("an awaited failure is catchable exactly like a synchronous throw", async () => { - const result = await run(` + expect( + await value(` const p = tools.host.fail({}) try { await p @@ -217,195 +139,33 @@ describe("first-class promise values", () => { } catch (e) { return e.message } - `) - expect(result.ok).toBe(true) - if (!result.ok) return - expect(result.value).toBe("Lookup refused") - expect(result.warnings).toBeUndefined() + `), + ).toBe("Lookup refused") }) - test("a fire-and-forget call is interrupted when the program returns", async () => { + test("a fire-and-forget call completes before the execution ends", async () => { const trace = makeTrace() - const result = await run( + const result = await value( ` tools.host.sleepy({ id: 1, ms: 30 }) return "done" `, { trace }, ) - expect(result.ok).toBe(true) - if (!result.ok) return - expect(result.value).toBe("done") - expect(result.warnings).toBeUndefined() - expect(trace.completed).toBe(0) - expect(trace.interrupted).toBe(1) + expect(result).toBe("done") + expect(trace.completed).toBe(1) + expect(trace.interrupted).toBe(0) }) - test("a never-awaited failing call preserves the result and reports the rejection", async () => { - const result = await run(` + test("a never-awaited failing call surfaces as an unhandled-rejection diagnostic", async () => { + const diagnostic = await error(` tools.host.fail({}) return "done" `) - expect(result.ok).toBe(true) - if (!result.ok) return - expect(result.value).toBe("done") - expect(result.warnings).toStrictEqual([ - { kind: "ToolFailure", message: "Unhandled rejection from an un-awaited promise: Lookup refused" }, - ]) - expect(Schema.decodeUnknownSync(CodeMode.Result)(JSON.parse(JSON.stringify(result)))).toStrictEqual(result) - }) - - test("a never-awaited failing async function is reported with a successful result", async () => { - const result = await run(` - const fail = async () => { throw new Error("boom") } - fail() - return "done" - `) - expect(result.ok).toBe(true) - if (!result.ok) return - expect(result.value).toBe("done") - expect(result.warnings).toStrictEqual([ - { kind: "ExecutionFailure", message: "Unhandled rejection from an un-awaited promise: Uncaught: boom" }, - ]) - }) - - test("output truncation bounds warning diagnostics with an in-band marker", async () => { - const result = await run( - ` - for (let i = 0; i < 100; i += 1) Promise.reject(new Error("x".repeat(1_000))) - return "done" - `, - { limits: { maxOutputBytes: 64 } }, - ) - expect(result.ok).toBe(true) - if (!result.ok) return - expect(result.truncated).toBe(true) - expect(result.warnings).toStrictEqual([ - { kind: "Truncated", message: "100 additional warnings omitted by the output limit." }, - ]) - }) - - test("a budget-consuming value does not starve warnings", async () => { - const result = await run( - ` - Promise.reject(new Error("boom")) - return "x".repeat(500) - `, - { limits: { maxOutputBytes: 128 } }, - ) - expect(result.ok).toBe(true) - if (!result.ok) return - expect(result.truncated).toBe(true) - expect(typeof result.value).toBe("string") - expect(result.warnings).toStrictEqual([ - { kind: "ExecutionFailure", message: "Unhandled rejection from an un-awaited promise: Uncaught: boom" }, - ]) - }) - - test("an un-awaited async function's pending chain is interrupted at the return", async () => { - const trace = makeTrace() - const result = await run( - ` - const run = async () => { - await tools.host.sleepy({ id: 1, ms: 60000 }) - tools.host.fail({}) - } - run() - return "done" - `, - { trace }, - ) - expect(result.ok).toBe(true) - if (!result.ok) return - expect(result.value).toBe("done") - expect(result.warnings).toBeUndefined() - expect(trace.starts).toEqual([1]) - expect(trace.completed).toBe(0) - expect(trace.interrupted).toBe(1) - }) - - test("reports every unhandled rejection in promise creation order", async () => { - const result = await run(` - Promise.reject(new Error("first")) - tools.host.fail({}) - Promise.reject(new Error("third")) - return "done" - `) - expect(result.ok).toBe(true) - if (!result.ok) return - expect(result.warnings).toStrictEqual([ - { kind: "ExecutionFailure", message: "Unhandled rejection from an un-awaited promise: Uncaught: first" }, - { kind: "ToolFailure", message: "Unhandled rejection from an un-awaited promise: Lookup refused" }, - { kind: "ExecutionFailure", message: "Unhandled rejection from an un-awaited promise: Uncaught: third" }, - ]) - }) - - test("orders an async function rejection before promises created inside its body", async () => { - const result = await run(` - const outer = async () => { - Promise.reject(new Error("inner")) - throw new Error("outer") - } - outer() - return "done" - `) - expect(result.ok).toBe(true) - if (!result.ok) return - expect(result.warnings).toStrictEqual([ - { kind: "ExecutionFailure", message: "Unhandled rejection from an un-awaited promise: Uncaught: outer" }, - { kind: "ExecutionFailure", message: "Unhandled rejection from an un-awaited promise: Uncaught: inner" }, - ]) - }) - - test("un-awaited interruptions settle without becoming rejections", async () => { - const result = await run(` - tools.host.interrupt({}) - Promise.all([tools.host.interrupt({})]) - return "done" - `) - expect(result.ok).toBe(true) - if (!result.ok) return - expect(result.value).toBe("done") - expect(result.warnings).toBeUndefined() - }) - - test("a fatal program error cancels outstanding work without reporting unhandled rejections", async () => { - const trace = makeTrace() - const result = await run( - ` - tools.host.sleepy({ id: 1, ms: 1_000 }) - throw new Error("boom") - `, - { trace }, - ) - expect(result.ok).toBe(false) - if (result.ok) return - expect(result.error.message).toBe("Uncaught: boom") - expect("warnings" in result).toBe(false) - expect(trace.completed).toBe(0) - expect(trace.interrupted).toBe(1) - }) - - test("async-function promises remain owned by the execution after the function returns", async () => { - const trace = makeTrace() - expect( - await value( - ` - const launch = async () => { - tools.host.sleepy({ id: 1, ms: 60000 }) - Promise.all([tools.host.sleepy({ id: 2, ms: 60000 })]) - return "returned" - } - return await launch() - `, - { trace }, - ), - ).toBe("returned") - // Both calls outlive launch() itself - they belong to the execution, not the function - - // and are interrupted only when the whole program returns. - expect(trace.starts).toEqual([1, 2]) - expect(trace.completed).toBe(0) - expect(trace.interrupted).toBe(2) + expect(diagnostic.kind).toBe("ToolFailure") + expect(diagnostic.message).toContain("Unhandled rejection from an un-awaited tool call") + expect(diagnostic.message).toContain("Lookup refused") + expect(diagnostic.suggestions?.join(" ")).toContain("await tools.ns.tool(...)") }) }) @@ -417,28 +177,6 @@ describe("promises at data boundaries", () => { expect(diagnostic.message).toContain("await tools.ns.tool(...)") }) - test("collection helpers do not let un-awaited promises cross the result boundary", async () => { - const diagnostic = await error(`return Array.from([Promise.resolve(1)])`) - expect(diagnostic.kind).toBe("InvalidDataValue") - expect(diagnostic.message).toContain("un-awaited Promise") - }) - - test("invalid returned data cancels pending work", async () => { - const trace = makeTrace() - const result = await run( - ` - const pending = tools.host.sleepy({ id: 1, ms: 60_000 }) - return { pending } - `, - { trace, limits: { timeoutMs: 100 } }, - ) - expect(result.ok).toBe(false) - if (result.ok) return - expect(result.error.kind).toBe("InvalidDataValue") - expect(trace.completed).toBe(0) - expect(trace.interrupted).toBe(1) - }) - test("passing an un-awaited promise as a tool argument is a clear diagnostic", async () => { const diagnostic = await error(`return await tools.host.sleepy({ id: tools.host.sleepy({ id: 1 }) })`) expect(diagnostic.kind).toBe("InvalidDataValue") @@ -458,59 +196,6 @@ describe("promises at data boundaries", () => { }) describe("Promise.all over arbitrary arrays", () => { - test("combinators return promises that can be assigned and awaited later", async () => { - expect( - await value(` - const all = Promise.all([Promise.resolve(1)]) - const settled = Promise.allSettled([Promise.reject("no")]) - const race = Promise.race([Promise.resolve(2)]) - const promises = [all instanceof Promise, settled instanceof Promise, race instanceof Promise] - return [promises, await all, await settled, await race] - `), - ).toEqual([[true, true, true], [1], [{ status: "rejected", reason: "no" }], 2]) - }) - - test("separately-created aggregate batches overlap before either is awaited", async () => { - const trace = makeTrace() - expect( - await value( - ` - const first = Promise.all([tools.host.sleepy({ id: 1, ms: 40 })]) - const second = Promise.all([tools.host.sleepy({ id: 2, ms: 40 })]) - return [await first, await second] - `, - { trace }, - ), - ).toEqual([[1], [2]]) - expect(trace.starts).toEqual([1, 2]) - expect(trace.maxActive).toBeGreaterThan(1) - }) - - test("an aggregate created before a try block rejects at its later await", async () => { - expect( - await value(` - const aggregate = Promise.all([tools.host.fail({})]) - try { - await aggregate - return "no" - } catch (error) { - return error.message - } - `), - ).toBe("Lookup refused") - }) - - test("awaiting an aggregate repeatedly does not rerun its members", async () => { - const result = await run(` - const aggregate = Promise.all([tools.host.sleepy({ id: 7 })]) - return [await aggregate, await aggregate] - `) - expect(result.ok).toBe(true) - if (!result.ok) return - expect(result.value).toEqual([[7], [7]]) - expect(result.toolCalls).toStrictEqual([{ name: "host.sleepy" }]) - }) - test("mixes promises and plain values, preserving order", async () => { expect( await value(` @@ -547,20 +232,7 @@ describe("Promise.all over arbitrary arrays", () => { expect(trace.maxActive).toBeGreaterThan(1) }) - test("runs async map callbacks concurrently", async () => { - const trace = makeTrace() - const result = await value( - ` - const ids = [1, 2, 3, 4] - return await Promise.all(ids.map(async (id) => await tools.host.sleepy({ id, ms: 40 }))) - `, - { trace }, - ) - expect(result).toEqual([1, 2, 3, 4]) - expect(trace.maxActive).toBeGreaterThan(1) - }) - - test("does not cap live tool-call concurrency", async () => { + test("caps live tool-call concurrency at the fixed internal constant (8)", async () => { const trace = makeTrace() const result = await value( ` @@ -572,7 +244,8 @@ describe("Promise.all over arbitrary arrays", () => { { trace }, ) expect(result).toBe(20) - expect(trace.maxActive).toBe(20) + expect(trace.maxActive).toBeGreaterThan(1) + expect(trace.maxActive).toBeLessThanOrEqual(8) }) test("resolves the empty array", async () => { @@ -580,85 +253,16 @@ describe("Promise.all over arbitrary arrays", () => { }) test("rejects with the first failure, catchable in-program", async () => { - const result = await run(` + expect( + await value(` try { await Promise.all([tools.host.sleepy({ id: 1 }), tools.host.fail({})]) return "no" } catch (e) { return e.message } - `) - expect(result.ok).toBe(true) - if (!result.ok) return - expect(result.value).toBe("Lookup refused") - expect(result.warnings).toBeUndefined() - }) - - test("rejects before an earlier slow promise fulfills", async () => { - const trace = makeTrace() - expect( - await value( - ` - try { - await Promise.all([ - tools.host.sleepy({ id: 1, ms: 100 }), - tools.host.fail({}), - ]) - return -1 - } catch { - return await tools.host.completed({}) - } - `, - { trace }, - ), - ).toBe(0) - // The surviving member is observed (Promise.all handled it), so completion interrupts - // it instead of waiting for it. - expect(trace.completed).toBe(0) - expect(trace.interrupted).toBe(1) - }) - - test("fail-fast does not cancel a sibling the program still holds and awaits", async () => { - const trace = makeTrace() - expect( - await value( - ` - const slow = tools.host.sleepy({ id: 1, ms: 40 }) - try { - await Promise.all([slow, tools.host.fail({})]) - return "no" - } catch {} - return await slow - `, - { trace }, - ), - ).toBe(1) - expect(trace.completed).toBe(1) - expect(trace.interrupted).toBe(0) - }) - - test("a slower observed sibling is interrupted at completion after failing fast", async () => { - const trace = makeTrace() - expect( - await value( - ` - const failLater = async () => { - await tools.host.sleepy({ id: 1, ms: 40 }) - throw new Error("later") - } - const aggregate = Promise.all([Promise.reject(new Error("first")), failLater()]) - try { - await aggregate - return "no" - } catch (error) { - return error.message - } - `, - { trace }, - ), - ).toBe("first") - expect(trace.completed).toBe(0) - expect(trace.interrupted).toBe(1) + `), + ).toBe("Lookup refused") }) test("a non-collection argument is a clear error", async () => { @@ -700,64 +304,50 @@ describe("Promise.allSettled", () => { return settled.filter((s) => s.status === "rejected").length `) expect(result.ok).toBe(true) - if (!result.ok) return - expect(result.value).toBe(2) - expect(result.warnings).toBeUndefined() + if (result.ok) expect(result.value).toBe(2) }) }) describe("Promise.race", () => { - test("first settlement wins and a direct loser is interrupted at completion", async () => { + test("first settlement wins and losers are interrupted", async () => { const trace = makeTrace() const result = await value( ` const fast = tools.host.sleepy({ id: 1, ms: 10 }) - const slow = tools.host.sleepy({ id: 2, ms: 40 }) + const slow = tools.host.sleepy({ id: 2, ms: 5000 }) return await Promise.race([fast, slow]) `, { trace }, ) expect(result).toBe(1) - // The loser is observed (the race handled it), so the execution does not wait for it. - expect(trace.completed).toBe(1) expect(trace.interrupted).toBe(1) + expect(trace.completed).toBe(1) }) - test("a direct loser remains awaitable after the race settles", async () => { + test("awaiting an interrupted loser afterwards is a catchable program failure", async () => { expect( await value(` const fast = tools.host.sleepy({ id: 1, ms: 10 }) - const slow = tools.host.sleepy({ id: 2, ms: 40 }) + const slow = tools.host.sleepy({ id: 2, ms: 5000 }) const winner = await Promise.race([fast, slow]) - return { winner, loser: await slow } + try { + await slow + return "no" + } catch (e) { + return { winner, caught: e.message } + } `), - ).toEqual({ winner: 1, loser: 2 }) - }) - - test("a nested aggregate loser and its members are interrupted at completion", async () => { - const trace = makeTrace() - expect( - await value( - ` - const nested = Promise.all([ - tools.host.sleepy({ id: 1, ms: 40 }), - tools.host.sleepy({ id: 2, ms: 40 }), - ]) - return await Promise.race(["immediate", nested]) - `, - { trace }, - ), - ).toBe("immediate") - // The nested aggregate and its members are all observed, so nothing waits for them. - expect(trace.completed).toBe(0) - expect(trace.interrupted).toBe(2) + ).toEqual({ + winner: 1, + caught: "This tool call was interrupted because another value settled a Promise.race first.", + }) }) test("a rejection can win the race", async () => { expect( await value(` try { - await Promise.race([tools.host.fail({}), tools.host.sleepy({ id: 1, ms: 40 })]) + await Promise.race([tools.host.fail({}), tools.host.sleepy({ id: 1, ms: 5000 })]) return "no" } catch (e) { return e.message @@ -769,20 +359,11 @@ describe("Promise.race", () => { test("a plain value wins over pending promises", async () => { const trace = makeTrace() expect( - await value(`return await Promise.race([tools.host.sleepy({ id: 1, ms: 40 }), "immediate"])`, { trace }), + await value(`return await Promise.race([tools.host.sleepy({ id: 1, ms: 5000 }), "immediate"])`, { trace }), ).toBe("immediate") - expect(trace.completed).toBe(0) expect(trace.interrupted).toBe(1) }) - test("a rejected race loser is observed by the aggregate", async () => { - const result = await run(`return await Promise.race(["winner", tools.host.fail({})])`) - expect(result.ok).toBe(true) - if (!result.ok) return - expect(result.value).toBe("winner") - expect(result.warnings).toBeUndefined() - }) - test("an empty race is a clear error instead of hanging", async () => { const diagnostic = await error(`return await Promise.race([])`) expect(diagnostic.message).toContain("never settle") @@ -794,9 +375,6 @@ describe("Promise.resolve / Promise.reject", () => { expect(await value(`return await Promise.resolve(42)`)).toBe(42) expect(await value(`return await Promise.resolve(Promise.resolve("nested"))`)).toBe("nested") expect(await value(`return await Promise.resolve(tools.host.sleepy({ id: 3 }))`)).toBe(3) - expect(await value(`const promise = Promise.resolve(1); return [promise].includes(Promise.resolve(promise))`)).toBe( - true, - ) }) test("reject produces a promise whose await throws the reason", async () => { @@ -811,34 +389,6 @@ describe("Promise.resolve / Promise.reject", () => { `), ).toBe("nope") }) - - test("a rejection observed after settlement is handled", async () => { - expect( - await value(` - const rejected = Promise.reject(new Error("handled")) - await tools.host.sleepy({ id: 1 }) - try { - await rejected - return "no" - } catch (error) { - return error.message - } - `), - ).toBe("handled") - }) - - test("an abandoned rejected promise is reported as unhandled", async () => { - const result = await run(` - Promise.reject(new Error("abandoned")) - return "done" - `) - expect(result.ok).toBe(true) - if (!result.ok) return - expect(result.value).toBe("done") - expect(result.warnings).toStrictEqual([ - { kind: "ExecutionFailure", message: "Unhandled rejection from an un-awaited promise: Uncaught: abandoned" }, - ]) - }) }) describe("timeout interruption of forked calls", () => { @@ -872,187 +422,18 @@ describe("timeout interruption of forked calls", () => { expect(result.error.kind).toBe("TimeoutExceeded") expect(trace.interrupted).toBe(2) }) - - test("a non-settling race loser cannot hold the execution to the timeout", async () => { - const trace = makeTrace() - const result = await run(`return await Promise.race(["winner", tools.host.sleepy({ id: 1, ms: 60000 })])`, { - trace, - limits: { timeoutMs: 100 }, - }) - // Completion interrupts the observed loser immediately; the race result survives. - expect(result.ok).toBe(true) - if (!result.ok) return - expect(result.value).toBe("winner") - expect(result.warnings).toBeUndefined() - expect(trace.starts).toEqual([1]) - expect(trace.completed).toBe(0) - expect(trace.interrupted).toBe(1) - }) - - test("a timeout during completion cleanup keeps the computed value and warns", async () => { - const trace = makeTrace() - const result = await run( - ` - tools.host.stubborn({ cleanupMs: 400 }) - return "done" - `, - { trace, limits: { timeoutMs: 100 } }, - ) - expect(result.ok).toBe(true) - if (!result.ok) return - expect(result.value).toBe("done") - expect(result.warnings).toStrictEqual([ - { - kind: "TimeoutExceeded", - message: - "The program returned, but background work was still running at the 100ms timeout and was interrupted. Await all started promises.", - }, - ]) - expect(trace.interrupted).toBe(1) - expect(trace.completed).toBe(0) - }) - - test("a timeout during completion cleanup reports the timeout warning before settled rejections", async () => { - const result = await run( - ` - tools.host.fail({}) - tools.host.stubborn({ cleanupMs: 400 }) - return "done" - `, - { limits: { timeoutMs: 100 } }, - ) - expect(result.ok).toBe(true) - if (!result.ok) return - expect(result.value).toBe("done") - expect(result.warnings).toStrictEqual([ - { - kind: "TimeoutExceeded", - message: - "The program returned, but background work was still running at the 100ms timeout and was interrupted. Await all started promises.", - }, - { kind: "ToolFailure", message: "Unhandled rejection from an un-awaited promise: Lookup refused" }, - ]) - }) -}) - -describe("promise chaining", () => { - test("then transforms tool results and adopts returned promises across a chain", async () => { - expect( - await value(` - return await tools.host - .sleepy({ id: 2 }) - .then((id) => tools.host.sleepy({ id: id + 1 })) - .then((id) => id * 10) - `), - ).toBe(30) - }) - - test("handlers are deferred and run in attach order", async () => { - expect( - await value(` - const order = [] - const promise = Promise.resolve(1) - promise.then(() => order.push("h1")) - promise.then(() => order.push("h2")) - order.push("sync") - await promise - return order - `), - ).toEqual(["sync", "h1", "h2"]) - }) - - test("catch recovers a tool failure and preserves fulfillment", async () => { - expect( - await value(` - return [ - await tools.host.fail({}).catch((error) => error.message), - await tools.host.sleepy({ id: 4 }).catch(() => "unused"), - ] - `), - ).toEqual(["Lookup refused", 4]) - }) - - test("finally observes settlement without changing the value", async () => { - expect( - await value(` - const events = [] - const result = await tools.host.sleepy({ id: 5 }).finally(() => events.push("cleanup")) - return [result, events] - `), - ).toEqual([5, ["cleanup"]]) - }) - - test("a settled, un-awaited rejected chain tail warns exactly once", async () => { - const result = await run(` - Promise.reject(new Error("boom")).then((value) => value) - await Promise.resolve() - return "done" - `) - expect(result.ok).toBe(true) - if (!result.ok) return - expect(result.value).toBe("done") - // The source rejection belongs to the chain (no warning); only the derived tail warns. - expect(result.warnings).toStrictEqual([ - { kind: "ExecutionFailure", message: "Unhandled rejection from an un-awaited promise: Uncaught: boom" }, - ]) - }) - - test("a catch handler silences the chain's rejection warning", async () => { - const result = await run(` - Promise.reject(new Error("boom")).catch(() => "handled") - await Promise.resolve() - return "done" - `) - expect(result.ok).toBe(true) - if (!result.ok) return - expect(result.warnings).toBeUndefined() - }) - - test("non-plain-function handlers fail loudly instead of being ignored", async () => { - const diagnostic = await error(`return await tools.host.sleepy({ id: 1 }).then(tools.host.completed)`) - expect(diagnostic.message).toContain("Promise.prototype.then handlers must be plain functions") - }) - - test("chaining methods are opaque references until called", async () => { - expect(await value(`return typeof tools.host.sleepy({ id: 1 }).then`)).toBe("function") - }) -}) - -describe("combinator settlement timing", () => { - test("a combinator settling one reaction turn after the program returns is interrupted silently", async () => { - // The aggregate's one-turn settlement delay (V8 parity) means an immediately-returning - // program abandons it while still pending: interrupted like any pending work, so no - // rejection warning survives - the member itself was observed by the combinator. - const result = await run(` - Promise.all([Promise.reject(new Error("boom"))]) - return "done" - `) - expect(result.ok).toBe(true) - if (!result.ok) return - expect(result.value).toBe("done") - expect(result.warnings).toBeUndefined() - }) - - test("a combinator settles one reaction turn after its members, as in V8", async () => { - // Regression for the race winner flip: Promise.all's settlement burns a reaction turn, - // so a plain resolved value entered in the same race wins, and a fail-fast aggregate - // cannot beat it into rejection. - expect( - await value(` - const pending = tools.host.sleepy({ id: 9, ms: 60000 }) - const winner = await Promise.race([Promise.all([Promise.resolve(1)]), Promise.resolve(2)]) - try { - const raced = await Promise.race([Promise.all([Promise.reject("x"), pending]), Promise.resolve("ok")]) - return [winner, "fulfilled", raced] - } catch (reason) { - return [winner, "rejected", reason] - } - `), - ).toEqual([2, "fulfilled", "ok"]) - }) }) describe("unsupported promise surface", () => { + test(".then/.catch/.finally give a clear await-instead error", async () => { + for (const method of ["then", "catch", "finally"]) { + const diagnostic = await error(`return tools.host.sleepy({ id: 1 }).${method}((x) => x)`) + expect(diagnostic.kind).toBe("UnsupportedSyntax") + expect(diagnostic.message).toContain(`Promise.prototype.${method} is not supported`) + expect(diagnostic.message).toContain("await") + } + }) + test("other property reads on a promise hint at the missing await", async () => { const diagnostic = await error(`return tools.host.sleepy({ id: 1 }).value`) expect(diagnostic.kind).toBe("InvalidDataValue") @@ -1061,179 +442,15 @@ describe("unsupported promise surface", () => { }) test("unknown Promise statics list what is available", async () => { - const diagnostic = await error(`return await Promise.withResolvers()`) - expect(diagnostic.message).toContain("Promise.withResolvers is not available") - expect(diagnostic.message).toContain("Promise.any") - }) -}) - -describe("Promise.any", () => { - test("first tool success wins; failing and losing calls are handled silently", async () => { - const trace = makeTrace() - const result = await run( - ` - const winner = await Promise.any([ - tools.host.fail({}), - tools.host.sleepy({ id: 1, ms: 5 }), - tools.host.sleepy({ id: 2, ms: 60000 }), - ]) - return winner - `, - { trace }, - ) - expect(result.ok).toBe(true) - if (!result.ok) return - expect(result.value).toBe(1) - // The slow loser stays execution-owned and is interrupted at completion; the tool - // failure was observed by the aggregate, so no rejection warning survives. - expect(result.warnings).toBeUndefined() - expect(trace.interrupted).toBe(1) - }) - - test("all members failing rejects with catch-normalized reasons in input order", async () => { - expect( - await value(` - try { - await Promise.any([tools.host.fail({}), Promise.reject("plain")]) - return "fulfilled" - } catch (error) { - return [error.name, error.errors.map((reason) => reason.message ?? reason)] - } - `), - ).toEqual(["AggregateError", ["Lookup refused", "plain"]]) - }) - - test("settles one reaction turn after its deciding member, as in V8", async () => { - expect(await value(`return await Promise.race([Promise.any([Promise.resolve(1)]), Promise.resolve(2)])`)).toBe(2) - }) - - test("a tie is decided by settlement order, not input order", async () => { - // Handlers run in attach order, so `first` settles before `second` and wins - // despite its later input position - as in real JS. - expect( - await value(` - const first = Promise.resolve().then(() => "one") - const second = Promise.resolve().then(() => "two") - return await Promise.any([second, first]) - `), - ).toBe("one") - }) - - test("an abandoned rejecting aggregate is interrupted silently at the return", async () => { - const result = await run(` - Promise.any([Promise.reject(new Error("boom"))]) - return "done" - `) - expect(result.ok).toBe(true) - if (!result.ok) return - expect(result.value).toBe("done") - expect(result.warnings).toBeUndefined() - }) -}) - -describe("promise construction", () => { - test("a deferred gate coordinates tool results across async functions", async () => { - expect( - await value(` - let openGate - const gate = new Promise((resolve) => { openGate = resolve }) - const worker = (async () => { - const id = await gate - return id * 2 - })() - openGate(await tools.host.sleepy({ id: 21, ms: 5 })) - return await worker - `), - ).toBe(42) - }) - - test("the .then(resolve) bridge settles a constructed promise", async () => { - expect( - await value(` - const bridged = new Promise((resolve, reject) => { - tools.host.sleepy({ id: 7, ms: 5 }).then(resolve, reject) - }) - return await bridged - `), - ).toBe(7) - }) - - test("constructed promises participate in combinators", async () => { - expect( - await value(` - let settle - const manual = new Promise((resolve) => { settle = resolve }) - const race = Promise.race([manual, tools.host.sleepy({ id: 3, ms: 60000 })]) - const all = Promise.all([manual, "plain"]) - const any = Promise.any([manual, new Promise(() => {})]) - settle("manual") - return [await race, await all, await any] - `), - ).toEqual(["manual", ["manual", "plain"], "manual"]) - }) - - test("resolving with a pending promise adopts its later settlement", async () => { - expect( - await value(` - let innerResolve, innerReject - const adopted = new Promise((resolve) => resolve(new Promise((resolve) => { innerResolve = resolve }))) - const adoptedRejection = new Promise((resolve) => resolve(new Promise((_, reject) => { innerReject = reject }))) - innerResolve("later") - innerReject("bad") - try { - return [await adopted, await adoptedRejection] - } catch (reason) { - return [await adopted, reason] - } - `), - ).toEqual(["later", "bad"]) - }) - - test("an async executor's post-await resolve settles the promise", async () => { - expect( - await value(` - const result = new Promise(async (resolve) => { - const id = await tools.host.sleepy({ id: 5, ms: 5 }) - resolve(id * 2) - }) - return await result - `), - ).toBe(10) - }) - - test("a never-settled promise is abandoned silently at the return", async () => { - const result = await run(` - const forever = new Promise(() => {}) - forever.then(() => {}) - return "done" - `) - expect(result.ok).toBe(true) - if (!result.ok) return - expect(result.value).toBe("done") - expect(result.warnings).toBeUndefined() - }) - - test("an un-awaited constructed rejection is reported like any unhandled rejection", async () => { - const result = await run(` - new Promise((_, reject) => reject(new Error("dropped"))) - await Promise.resolve() - await Promise.resolve() - return "done" - `) - expect(result.ok).toBe(true) - if (!result.ok) return - expect(result.value).toBe("done") - expect(result.warnings).toHaveLength(1) - expect(result.warnings?.[0].message).toContain("Unhandled rejection") - expect(result.warnings?.[0].message).toContain("dropped") - }) - - test("resolver functions cannot cross the data boundary", async () => { - const diagnostic = await error(` - let escaped - new Promise((resolve) => { escaped = resolve }) - return { escaped } - `) - expect(diagnostic.kind).toBe("InvalidDataValue") + const diagnostic = await error(`return await Promise.any([tools.host.sleepy({ id: 1 })])`) + expect(diagnostic.message).toContain("Promise.any is not available") + expect(diagnostic.message).toContain("Promise.allSettled") + }) + + test("new Promise(...) points at tool calls instead", async () => { + const diagnostic = await error(`return new Promise((resolve) => resolve(1))`) + expect(diagnostic.kind).toBe("UnsupportedSyntax") + expect(diagnostic.message).toContain("new Promise(...) is not supported") + expect(diagnostic.message).toContain("already return promises") }) }) diff --git a/packages/codemode/test/signature.test.ts b/packages/codemode/test/signature.test.ts index dea0e890ac..232c6dcb22 100644 --- a/packages/codemode/test/signature.test.ts +++ b/packages/codemode/test/signature.test.ts @@ -342,7 +342,9 @@ describe("JSDoc signatures in catalogs and search results", () => { const runtime = CodeMode.make({ tools: { github: { list_issues: listIssues }, orders: { lookup: lookupOrder } } }) const search = async (query: string) => { - const result = await Effect.runPromise(runtime.execute(`return search({ query: ${JSON.stringify(query)} })`)) + const result = await Effect.runPromise( + runtime.execute(`return await tools.$codemode.search({ query: ${JSON.stringify(query)} })`), + ) expect(result.ok).toBe(true) if (!result.ok) throw new Error("search failed") return result.value as { items: Array<{ path: string; signature: string }>; remaining: number } @@ -434,7 +436,9 @@ describe("non-identifier tool paths", () => { }) test("search results return callable bracket-notation paths and signatures", async () => { - const result = await Effect.runPromise(runtime.execute(`return search({ query: "resolve library" })`)) + const result = await Effect.runPromise( + runtime.execute(`return await tools.$codemode.search({ query: "resolve library" })`), + ) expect(result.ok).toBe(true) if (!result.ok) throw new Error("search failed") diff --git a/packages/codemode/test/stdlib.test.ts b/packages/codemode/test/stdlib.test.ts index dc9710bd2f..f7831a0603 100644 --- a/packages/codemode/test/stdlib.test.ts +++ b/packages/codemode/test/stdlib.test.ts @@ -19,28 +19,6 @@ const error = async (code: string) => { return result.error } -describe("Number and Math", () => { - test("Math.random returns a number in [0, 1)", async () => { - expect(await value(`const n = Math.random(); return typeof n === "number" && n >= 0 && n < 1`)).toBe(true) - }) - - test("Number exposes native non-finite constants", async () => { - expect( - await value( - `return [Number.isNaN(Number.NaN), Number.POSITIVE_INFINITY === Infinity, Number.NEGATIVE_INFINITY === -Infinity]`, - ), - ).toEqual([true, true, true]) - }) - - test("Number valueOf returns its primitive receiver", async () => { - expect(await value(`return (42).valueOf()`)).toBe(42) - }) - - test("Number valueOf does not enable boxed numbers", async () => { - expect((await error(`return new Number(42)`)).kind).toBe("UnsupportedSyntax") - }) -}) - describe("Date", () => { test("Date.now() returns a number", async () => { expect(await value(`return typeof Date.now()`)).toBe("number") @@ -154,7 +132,9 @@ describe("RegExp", () => { ).toEqual(["1", "22"]) }) - test("an unmatched string pattern returns null", async () => { + test("string match: non-global carries index, global lists all matches", async () => { + expect(await value(`const m = "a1b22".match(/\\d+/); return [m[0], m.index]`)).toEqual(["1", 1]) + expect(await value(`return "a1b22".match(/\\d+/g)`)).toEqual(["1", "22"]) expect(await value(`return "abc".match(/\\d/)`)).toBeNull() }) @@ -162,6 +142,13 @@ describe("RegExp", () => { expect(await value(`return "a1b22".matchAll(/(\\d+)/g).map((m) => m[1])`)).toEqual(["1", "22"]) }) + test("replace and replaceAll with patterns and $1 substitution", async () => { + expect(await value(`return "a1b2".replace(/\\d/, "#")`)).toBe("a#b2") + expect(await value(`return "a1b2".replace(/\\d/g, "#")`)).toBe("a#b#") + expect(await value(`return "a1b2".replaceAll(/\\d/g, "#")`)).toBe("a#b#") + expect(await value(`return "hi bob".replace(/b(o)b/, "[$1]")`)).toBe("hi [o]") + }) + test("function replacers receive captures, offsets, input, and named groups", async () => { expect( await value(` @@ -227,6 +214,12 @@ describe("RegExp", () => { expect(await value(`try { "a".replaceAll(/a/, "b"); return "no" } catch { return "caught" }`)).toBe("caught") }) + test("split and search accept patterns", async () => { + expect(await value(`return "a1b22c".split(/\\d+/)`)).toEqual(["a", "b", "c"]) + expect(await value(`return "ab42".search(/\\d/)`)).toBe(2) + expect(await value(`return "ab".search(/\\d/)`)).toBe(-1) + }) + test("new RegExp constructs from strings; invalid patterns are catchable", async () => { expect(await value(`return new RegExp("a+", "i").test("AAA")`)).toBe(true) expect(await value(`try { new RegExp("("); return "no" } catch { return "caught" }`)).toBe("caught") @@ -593,103 +586,6 @@ describe("Set", () => { }) describe("stdlib integration", () => { - test("Object values and entries accept arrays", async () => { - expect(await value(`return [Object.values(["a", "b"]), Object.entries(["a", "b"])]`)).toEqual([ - ["a", "b"], - [ - ["0", "a"], - ["1", "b"], - ], - ]) - expect(await value(`const match = /a/.exec("ba"); return [Object.values(match), Object.entries(match)]`)).toEqual([ - ["a", 1], - [ - ["0", "a"], - ["index", 1], - ], - ]) - expect(await value(`return Object.keys(Object.values({ match: /a/.exec("ba") })[0])`)).toEqual(["0", "index"]) - }) - - test("Object.fromEntries accepts every supported entry collection", async () => { - expect( - await value(` - return [ - Object.fromEntries([["a", 1]]), - Object.fromEntries(new Map([["b", 2]])), - Object.fromEntries(new Set([["c", 3]])), - Object.fromEntries(new URLSearchParams("d=4")), - Object.fromEntries([{ 0: "e", 1: 5 }]), - Object.fromEntries(new Set([[{}, 6], [new Date(0), 7], [null, 8], [undefined, 9]])), - ] - `), - ).toEqual([ - { a: 1 }, - { b: 2 }, - { c: 3 }, - { d: "4" }, - { e: 5 }, - { "[object Object]": 6, "1970-01-01T00:00:00.000Z": 7, null: 8, undefined: 9 }, - ]) - expect(await value(`try { Object.fromEntries(new Set([Math.max])); return false } catch { return true }`)).toBe( - true, - ) - expect( - await value(`try { Object.fromEntries(new Map([["fn", Math.max]])); return false } catch { return true }`), - ).toBe(true) - }) - - test("deterministic Math methods match the host runtime", async () => { - const result = await value(` - return [ - Math.acos(0.5), Math.acosh(2), Math.asin(0.5), Math.asinh(2), Math.atan(1), Math.atan2(1, 2), Math.atanh(0.5), - Math.cos(0.5), Math.cosh(0.5), Math.sin(0.5), Math.sinh(0.5), Math.tan(0.5), Math.tanh(0.5), - Math.log1p(0.5), Math.expm1(0.5), Math.f16round(1.337), Math.fround(1.337), Math.clz32(1), Math.imul(2, 3), - ] - `) - expect(result).toEqual([ - Math.acos(0.5), - Math.acosh(2), - Math.asin(0.5), - Math.asinh(2), - Math.atan(1), - Math.atan2(1, 2), - Math.atanh(0.5), - Math.cos(0.5), - Math.cosh(0.5), - Math.sin(0.5), - Math.sinh(0.5), - Math.tan(0.5), - Math.tanh(0.5), - Math.log1p(0.5), - Math.expm1(0.5), - Math.f16round(1.337), - Math.fround(1.337), - Math.clz32(1), - Math.imul(2, 3), - ]) - }) - - test("Object.assign mutates and returns its target", async () => { - expect( - await value(` - const target = { a: 1 } - const result = Object.assign(target, { b: 2 }) - return { target, result, same: target === result } - `), - ).toEqual({ target: { a: 1, b: 2 }, result: { a: 1, b: 2 }, same: true }) - expect(await value(`try { Object.assign(null, { a: 1 }); return false } catch { return true }`)).toBe(true) - }) - - test("assignment resolves and reads its left side before evaluating the right side", async () => { - expect(await value(`let x = 1; x += (x = 5); return x`)).toBe(6) - expect(await value(`let i = 0; const values = [9]; values[i++] = i; return [values, i]`)).toEqual([[1], 1]) - expect(await value(`let i = 0; const values = [10, 20]; values[i++] += i; return [values, i]`)).toEqual([ - [11, 20], - 1, - ]) - }) - test("typeof reports constructors as functions and never throws", async () => { expect(await value(`return typeof Map`)).toBe("function") expect(await value(`return typeof ((x) => x)`)).toBe("function") @@ -756,43 +652,6 @@ describe("sandbox values at intra-sandbox checkpoints", () => { ) }) - test("Object.values/entries preserve nested object identity", async () => { - expect( - await value(` - const child = { selected: false } - const rows = { a: child } - Object.values(rows)[0].selected = true - return child.selected - `), - ).toBe(true) - expect( - await value(` - const child = { selected: false } - const rows = { a: child } - Object.entries(rows)[0][1].selected = true - return child.selected - `), - ).toBe(true) - }) - - test("Object enumeration preserves promises and callable references", async () => { - expect( - await value(` - const pending = Promise.resolve(1) - const source = { pending } - return [Object.keys(source), Object.hasOwn(source, "pending"), await Object.values(source)[0], await Object.entries(source)[0][1]] - `), - ).toEqual([["pending"], true, 1, 1]) - expect(await value(`return Object.values({ max: Math.max })[0](1, 2)`)).toBe(2) - }) - - test("Object enumeration rejects invalid receivers and gives promises an await hint", async () => { - const diagnostic = await error(`return Object.keys(Promise.resolve({ a: 1 }))`) - expect(diagnostic.kind).toBe("InvalidDataValue") - expect(diagnostic.message).toContain("await") - expect((await error(`return Object.keys(Math)`)).kind).toBe("InvalidDataValue") - }) - test("Object.assign keeps Maps usable", async () => { expect(await value(`const merged = Object.assign({}, { m: new Map([["a", 1]]) }); return merged.m.get("a")`)).toBe( 1, @@ -815,53 +674,6 @@ describe("sandbox values at intra-sandbox checkpoints", () => { expect(await value(`return Array.from([new Date(5)])[0].getTime()`)).toBe(5) }) - test("Array.from and Array.of preserve nested object identity", async () => { - expect( - await value(` - const child = { selected: false } - Array.from([child])[0].selected = true - return child.selected - `), - ).toBe(true) - expect( - await value(` - const child = { selected: false } - Array.of(child)[0].selected = true - return child.selected - `), - ).toBe(true) - }) - - test("Array.from and Array.of preserve promises and callable references", async () => { - expect( - await value(` - const pending = Promise.resolve(1) - return [await Array.from([pending])[0], await Array.of(pending)[0]] - `), - ).toEqual([1, 1]) - expect(await value(`return [Array.from([Math.max])[0](1, 2), Array.of(Math.max)[0](3, 4)]`)).toEqual([2, 4]) - }) - - test("Array.from preserves identity across supported collection shapes", async () => { - expect( - await value(` - const child = { selected: false } - const fromArrayLike = Array.from({ 0: child, length: 1 }) - const fromMap = Array.from(new Map([["child", child]])) - const fromSet = Array.from(new Set([child])) - fromArrayLike[0].selected = true - return [fromMap[0][1] === child, fromSet[0] === child, child.selected] - `), - ).toEqual([true, true, true]) - }) - - test("Array.from rejects invalid receivers and gives promises an await hint", async () => { - const diagnostic = await error(`return Array.from(Promise.resolve([1]))`) - expect(diagnostic.kind).toBe("InvalidDataValue") - expect(diagnostic.message).toContain("await") - expect((await error(`return Array.from(() => 1)`)).kind).toBe("InvalidDataValue") - }) - test("regexes stay callable through Object.values", async () => { expect(await value(`return Object.values({ r: /ab+/ })[0].test("abb")`)).toBe(true) }) diff --git a/packages/codemode/test/string-core-test262.test.ts b/packages/codemode/test/string-core-test262.test.ts deleted file mode 100644 index dc804af858..0000000000 --- a/packages/codemode/test/string-core-test262.test.ts +++ /dev/null @@ -1,580 +0,0 @@ -/* - * Portions adapted from Test262 at revision 250f204f23a9249ff204be2baec29600faae7b75: - * - test/built-ins/String/prototype/toLowerCase/S15.5.4.16_A2_T1.js - * - test/built-ins/String/prototype/toLowerCase/special_casing.js - * - test/built-ins/String/prototype/toLowerCase/special_casing_conditional.js - * - test/built-ins/String/prototype/toLowerCase/Final_Sigma_U180E.js - * - test/built-ins/String/prototype/toLowerCase/supplementary_plane.js - * - test/built-ins/String/prototype/toUpperCase/S15.5.4.18_A2_T1.js - * - test/built-ins/String/prototype/toUpperCase/special_casing.js - * - test/built-ins/String/prototype/toUpperCase/supplementary_plane.js - * - test/built-ins/String/prototype/trim/15.5.4.20-3-1.js - * - test/built-ins/String/prototype/trim/15.5.4.20-3-2.js - * - test/built-ins/String/prototype/trim/15.5.4.20-3-3.js - * - test/built-ins/String/prototype/trim/15.5.4.20-3-4.js - * - test/built-ins/String/prototype/trim/15.5.4.20-3-5.js - * - test/built-ins/String/prototype/trim/15.5.4.20-3-6.js - * - test/built-ins/String/prototype/trim/15.5.4.20-3-7.js - * - test/built-ins/String/prototype/trim/15.5.4.20-3-8.js - * - test/built-ins/String/prototype/trim/15.5.4.20-3-9.js - * - test/built-ins/String/prototype/trim/15.5.4.20-3-10.js - * - test/built-ins/String/prototype/trim/15.5.4.20-3-11.js - * - test/built-ins/String/prototype/trim/15.5.4.20-3-12.js - * - test/built-ins/String/prototype/trim/15.5.4.20-3-13.js - * - test/built-ins/String/prototype/trim/15.5.4.20-3-14.js - * - test/built-ins/String/prototype/trim/15.5.4.20-4-1.js - * - test/built-ins/String/prototype/trim/15.5.4.20-4-2.js - * - test/built-ins/String/prototype/trim/15.5.4.20-4-3.js - * - test/built-ins/String/prototype/trim/15.5.4.20-4-4.js - * - test/built-ins/String/prototype/trim/15.5.4.20-4-5.js - * - test/built-ins/String/prototype/trim/15.5.4.20-4-6.js - * - test/built-ins/String/prototype/trim/15.5.4.20-4-8.js - * - test/built-ins/String/prototype/trim/15.5.4.20-4-10.js - * - test/built-ins/String/prototype/trim/15.5.4.20-4-11.js - * - test/built-ins/String/prototype/trim/15.5.4.20-4-12.js - * - test/built-ins/String/prototype/trim/15.5.4.20-4-13.js - * - test/built-ins/String/prototype/trim/15.5.4.20-4-14.js - * - test/built-ins/String/prototype/trim/15.5.4.20-4-16.js - * - test/built-ins/String/prototype/trim/15.5.4.20-4-18.js - * - test/built-ins/String/prototype/trim/15.5.4.20-4-19.js - * - test/built-ins/String/prototype/trim/15.5.4.20-4-20.js - * - test/built-ins/String/prototype/trim/15.5.4.20-4-21.js - * - test/built-ins/String/prototype/trim/15.5.4.20-4-22.js - * - test/built-ins/String/prototype/trim/15.5.4.20-4-24.js - * - test/built-ins/String/prototype/trim/15.5.4.20-4-27.js - * - test/built-ins/String/prototype/trim/15.5.4.20-4-28.js - * - test/built-ins/String/prototype/trim/15.5.4.20-4-29.js - * - test/built-ins/String/prototype/trim/15.5.4.20-4-30.js - * - test/built-ins/String/prototype/trim/15.5.4.20-4-32.js - * - test/built-ins/String/prototype/trim/15.5.4.20-4-34.js - * - test/built-ins/String/prototype/trim/15.5.4.20-4-35.js - * - test/built-ins/String/prototype/trim/15.5.4.20-4-36.js - * - test/built-ins/String/prototype/trim/15.5.4.20-4-37.js - * - test/built-ins/String/prototype/trim/15.5.4.20-4-38.js - * - test/built-ins/String/prototype/trim/15.5.4.20-4-39.js - * - test/built-ins/String/prototype/trim/15.5.4.20-4-40.js - * - test/built-ins/String/prototype/trim/15.5.4.20-4-41.js - * - test/built-ins/String/prototype/trim/15.5.4.20-4-42.js - * - test/built-ins/String/prototype/trim/15.5.4.20-4-43.js - * - test/built-ins/String/prototype/trim/15.5.4.20-4-44.js - * - test/built-ins/String/prototype/trim/15.5.4.20-4-45.js - * - test/built-ins/String/prototype/trim/15.5.4.20-4-46.js - * - test/built-ins/String/prototype/trim/15.5.4.20-4-47.js - * - test/built-ins/String/prototype/trim/15.5.4.20-4-48.js - * - test/built-ins/String/prototype/trim/15.5.4.20-4-49.js - * - test/built-ins/String/prototype/trim/15.5.4.20-4-50.js - * - test/built-ins/String/prototype/trim/15.5.4.20-4-51.js - * - test/built-ins/String/prototype/trim/15.5.4.20-4-52.js - * - test/built-ins/String/prototype/trim/15.5.4.20-4-53.js - * - test/built-ins/String/prototype/trim/15.5.4.20-4-54.js - * - test/built-ins/String/prototype/trim/15.5.4.20-4-55.js - * - test/built-ins/String/prototype/trim/15.5.4.20-4-56.js - * - test/built-ins/String/prototype/trim/15.5.4.20-4-57.js - * - test/built-ins/String/prototype/trim/15.5.4.20-4-58.js - * - test/built-ins/String/prototype/trim/15.5.4.20-4-59.js - * - test/built-ins/String/prototype/trim/15.5.4.20-4-60.js - * - test/built-ins/String/prototype/trim/u180e.js - * - test/built-ins/String/prototype/trimStart/this-value-whitespace.js - * - test/built-ins/String/prototype/trimStart/this-value-line-terminator.js - * - test/built-ins/String/prototype/trimEnd/this-value-whitespace.js - * - test/built-ins/String/prototype/trimEnd/this-value-line-terminator.js - * - test/built-ins/String/prototype/repeat/repeat-string-n-times.js - * - test/built-ins/String/prototype/repeat/empty-string-returns-empty.js - * - test/built-ins/String/prototype/repeat/count-is-zero-returns-empty-string.js - * - test/built-ins/String/prototype/repeat/count-coerced-to-zero-returns-empty-string.js - * - test/built-ins/String/prototype/padStart/fill-string-empty.js - * - test/built-ins/String/prototype/padStart/normal-operation.js - * - test/built-ins/String/prototype/padStart/fill-string-omitted.js - * - test/built-ins/String/prototype/padStart/max-length-not-greater-than-string.js - * - test/built-ins/String/prototype/padEnd/fill-string-empty.js - * - test/built-ins/String/prototype/padEnd/normal-operation.js - * - test/built-ins/String/prototype/padEnd/fill-string-omitted.js - * - test/built-ins/String/prototype/padEnd/max-length-not-greater-than-string.js - * - test/built-ins/String/prototype/charAt/S15.5.4.4_A1_T4.js - * - test/built-ins/String/prototype/charAt/S15.5.4.4_A1_T7.js - * - test/built-ins/String/prototype/charAt/S15.5.4.4_A1_T8.js - * - test/built-ins/String/prototype/charAt/S15.5.4.4_A4_T1.js - * - test/built-ins/String/prototype/charAt/S15.5.4.4_A4_T2.js - * - test/built-ins/String/prototype/charAt/S15.5.4.4_A4_T3.js - * - test/built-ins/String/prototype/charAt/S9.4_A1.js - * - test/built-ins/String/prototype/charAt/S9.4_A2.js - * - test/built-ins/String/prototype/charAt/pos-rounding.js - * - test/built-ins/String/prototype/charCodeAt/S15.5.4.5_A1_T4.js - * - test/built-ins/String/prototype/charCodeAt/S15.5.4.5_A1_T7.js - * - test/built-ins/String/prototype/charCodeAt/S15.5.4.5_A1_T8.js - * - test/built-ins/String/prototype/charCodeAt/pos-rounding.js - * - test/built-ins/String/prototype/codePointAt/return-single-code-unit.js - * - test/built-ins/String/prototype/codePointAt/return-first-code-unit.js - * - test/built-ins/String/prototype/codePointAt/return-utf16-decode.js - * - test/built-ins/String/prototype/codePointAt/return-code-unit-coerced-position.js - * - test/built-ins/String/prototype/codePointAt/returns-undefined-on-position-less-than-zero.js - * - test/built-ins/String/prototype/codePointAt/returns-undefined-on-position-equal-or-more-than-size.js - * - test/built-ins/String/prototype/at/returns-code-unit.js - * - test/built-ins/String/prototype/at/returns-item.js - * - test/built-ins/String/prototype/at/returns-item-relative-index.js - * - test/built-ins/String/prototype/at/returns-undefined-for-out-of-range-index.js - * - test/built-ins/String/prototype/at/index-non-numeric-argument-tointeger.js - * - test/built-ins/String/prototype/concat/S15.5.4.6_A1_T4.js - * - test/built-ins/String/prototype/toString/string-primitive.js - * - test/built-ins/String/prototype/normalize/return-normalized-string.js - * - test/built-ins/String/prototype/normalize/return-normalized-string-using-default-parameter.js - * - test/built-ins/String/prototype/normalize/form-is-not-valid-throws.js - * - test/built-ins/String/prototype/localeCompare/15.5.4.9_CE.js - * - test/built-ins/String/fromCharCode/S15.5.3.2_A2.js - * - test/built-ins/String/fromCharCode/S15.5.3.2_A3_T1.js - * - test/built-ins/String/fromCharCode/S9.7_A1.js - * - test/built-ins/String/fromCharCode/S9.7_A2.1.js - * - test/built-ins/String/fromCharCode/S9.7_A2.2.js - * - test/built-ins/String/fromCharCode/S9.7_A3.2_T1.js - * - test/built-ins/String/fromCodePoint/arguments-is-empty.js - * - test/built-ins/String/fromCodePoint/return-string-value.js - * - test/built-ins/String/fromCodePoint/argument-is-not-integer.js - * - test/built-ins/String/fromCodePoint/number-is-out-of-range.js - * - * Copyright 2009 the Sputnik authors. All rights reserved. - * Copyright (C) 2009 the Sputnik authors. All rights reserved. - * Copyright (c) 2012 Ecma International. All rights reserved. - * Copyright 2012 Norbert Lindenberg. All rights reserved. - * Copyright 2012 Mozilla Corporation. All rights reserved. - * Copyright 2013 Microsoft Corporation. All rights reserved. - * Copyright (C) 2015 the V8 project authors. All rights reserved. - * Copyright (C) 2015 André Bargull. All rights reserved. - * Copyright (C) 2016 the V8 project authors. All rights reserved. - * Copyright (C) 2016 André Bargull. All rights reserved. - * Copyright (C) 2016 Jordan Harband. All rights reserved. - * Copyright (C) 2016 Mathias Bynens. All rights reserved. - * Copyright (c) 2017 Valerie Young. All rights reserved. - * Copyright (C) 2017 Valerie Young. All rights reserved. - * Copyright (C) 2020 Rick Waldron. All rights reserved. - * Copyright (C) 2022 Richard Gibson. All rights reserved. - * Test262 portions are governed by the BSD license in LICENSE.test262. - */ -import { describe, expect, test } from "bun:test" -import { Effect } from "effect" -import { CodeMode } from "../src/index.js" - -type Argument = string | number | undefined -type Outcome = "undefined" | "length" | "RangeError" -type Assertion = { - label: string - input?: string - args?: ReadonlyArray - expected?: string | number - outcome?: Outcome -} -type Vector = { - path: string - method: string - static?: boolean - assertions: ReadonlyArray -} - -const value = async (code: string) => { - const result = await Effect.runPromise(CodeMode.execute({ code, tools: {} })) - if (!result.ok) throw new Error(`expected success, got ${result.error.kind}: ${result.error.message}`) - return result.value -} - -const literal = (input: Argument) => { - if (input === undefined) return "undefined" - if (typeof input === "string") return JSON.stringify(input) - if (Number.isNaN(input)) return "NaN" - if (input === Infinity) return "Infinity" - if (input === -Infinity) return "-Infinity" - if (Object.is(input, -0)) return "-0" - return JSON.stringify(input) -} - -const vectors: Array = [] -const add = (path: string, method: string, assertions: ReadonlyArray, staticMethod = false) => { - vectors.push({ path, method, assertions, static: staticMethod }) -} -const assertion = (label: string, input: string, expected: string | number, args: ReadonlyArray = []) => ({ - label, - input, - args, - expected, -}) - -add("test/built-ins/String/prototype/toLowerCase/S15.5.4.16_A2_T1.js", "toLowerCase", [ - assertion("#1 direct value", "Hello, WoRlD!", "hello, world!"), - assertion("#2 String value", "Hello, WoRlD!", "hello, world!"), -]) -add("test/built-ins/String/prototype/toLowerCase/special_casing.js", "toLowerCase", [ - assertion( - "103 SpecialCasing mappings", - "\u00DF\u0130\uFB00\uFB01\uFB02\uFB03\uFB04\uFB05\uFB06\u0587\uFB13\uFB14\uFB15\uFB16\uFB17\u0149\u0390\u03B0\u01F0\u1E96\u1E97\u1E98\u1E99\u1E9A\u1F50\u1F52\u1F54\u1F56\u1FB6\u1FC6\u1FD2\u1FD3\u1FD6\u1FD7\u1FE2\u1FE3\u1FE4\u1FE6\u1FE7\u1FF6\u1F80\u1F81\u1F82\u1F83\u1F84\u1F85\u1F86\u1F87\u1F88\u1F89\u1F8A\u1F8B\u1F8C\u1F8D\u1F8E\u1F8F\u1F90\u1F91\u1F92\u1F93\u1F94\u1F95\u1F96\u1F97\u1F98\u1F99\u1F9A\u1F9B\u1F9C\u1F9D\u1F9E\u1F9F\u1FA0\u1FA1\u1FA2\u1FA3\u1FA4\u1FA5\u1FA6\u1FA7\u1FA8\u1FA9\u1FAA\u1FAB\u1FAC\u1FAD\u1FAE\u1FAF\u1FB3\u1FBC\u1FC3\u1FCC\u1FF3\u1FFC\u1FB2\u1FB4\u1FC2\u1FC4\u1FF2\u1FF4\u1FB7\u1FC7\u1FF7", - "\u00DF\u0069\u0307\uFB00\uFB01\uFB02\uFB03\uFB04\uFB05\uFB06\u0587\uFB13\uFB14\uFB15\uFB16\uFB17\u0149\u0390\u03B0\u01F0\u1E96\u1E97\u1E98\u1E99\u1E9A\u1F50\u1F52\u1F54\u1F56\u1FB6\u1FC6\u1FD2\u1FD3\u1FD6\u1FD7\u1FE2\u1FE3\u1FE4\u1FE6\u1FE7\u1FF6\u1F80\u1F81\u1F82\u1F83\u1F84\u1F85\u1F86\u1F87\u1F80\u1F81\u1F82\u1F83\u1F84\u1F85\u1F86\u1F87\u1F90\u1F91\u1F92\u1F93\u1F94\u1F95\u1F96\u1F97\u1F90\u1F91\u1F92\u1F93\u1F94\u1F95\u1F96\u1F97\u1FA0\u1FA1\u1FA2\u1FA3\u1FA4\u1FA5\u1FA6\u1FA7\u1FA0\u1FA1\u1FA2\u1FA3\u1FA4\u1FA5\u1FA6\u1FA7\u1FB3\u1FB3\u1FC3\u1FC3\u1FF3\u1FF3\u1FB2\u1FB4\u1FC2\u1FC4\u1FF2\u1FF4\u1FB7\u1FC7\u1FF7", - ), -]) -add("test/built-ins/String/prototype/toLowerCase/special_casing_conditional.js", "toLowerCase", [ - assertion("single sigma", "\u03A3", "\u03C3"), - assertion("preceded by cased", "A\u03A3", "a\u03C2"), - assertion("preceded by supplementary cased", "\uD835\uDCA2\u03A3", "\uD835\uDCA2\u03C2"), - assertion("preceded by full stop", "A.\u03A3", "a.\u03C2"), - assertion("preceded by soft hyphen", "A\u00AD\u03A3", "a\u00AD\u03C2"), - assertion("preceded by combining mark", "A\uD834\uDE42\u03A3", "a\uD834\uDE42\u03C2"), - assertion("preceded by uncased combining mark", "\u0345\u03A3", "\u0345\u03C3"), - assertion("preceded by cased and combining mark", "\u0391\u0345\u03A3", "\u03B1\u0345\u03C2"), - assertion("followed by cased", "A\u03A3B", "a\u03C3b"), - assertion("followed by supplementary cased", "A\u03A3\uD835\uDCA2", "a\u03C3\uD835\uDCA2"), - assertion("followed by full stop and cased", "A\u03A3.b", "a\u03C3.b"), - assertion("followed by soft hyphen and cased", "A\u03A3\u00ADB", "a\u03C3\u00ADb"), - assertion("followed by combining mark and cased", "A\u03A3\uD834\uDE42B", "a\u03C3\uD834\uDE42b"), - assertion("followed by uncased combining mark", "A\u03A3\u0345", "a\u03C2\u0345"), - assertion("followed by combining mark and cased Greek", "A\u03A3\u0345\u0391", "a\u03C3\u0345\u03B1"), -]) -add("test/built-ins/String/prototype/toLowerCase/Final_Sigma_U180E.js", "toLowerCase", [ - assertion("preceded by U+180E", "A\u180E\u03A3", "a\u180E\u03C2"), - assertion("preceded by U+180E and followed by cased", "A\u180E\u03A3B", "a\u180E\u03C3b"), - assertion("followed by U+180E", "A\u03A3\u180E", "a\u03C2\u180E"), - assertion("followed by U+180E and cased", "A\u03A3\u180EB", "a\u03C3\u180Eb"), - assertion("surrounded by U+180E", "A\u180E\u03A3\u180E", "a\u180E\u03C2\u180E"), - assertion("surrounded by U+180E and followed by cased", "A\u180E\u03A3\u180EB", "a\u180E\u03C3\u180Eb"), -]) -add("test/built-ins/String/prototype/toLowerCase/supplementary_plane.js", "toLowerCase", [ - assertion( - "40 Deseret mappings", - "\uD801\uDC00\uD801\uDC01\uD801\uDC02\uD801\uDC03\uD801\uDC04\uD801\uDC05\uD801\uDC06\uD801\uDC07\uD801\uDC08\uD801\uDC09\uD801\uDC0A\uD801\uDC0B\uD801\uDC0C\uD801\uDC0D\uD801\uDC0E\uD801\uDC0F\uD801\uDC10\uD801\uDC11\uD801\uDC12\uD801\uDC13\uD801\uDC14\uD801\uDC15\uD801\uDC16\uD801\uDC17\uD801\uDC18\uD801\uDC19\uD801\uDC1A\uD801\uDC1B\uD801\uDC1C\uD801\uDC1D\uD801\uDC1E\uD801\uDC1F\uD801\uDC20\uD801\uDC21\uD801\uDC22\uD801\uDC23\uD801\uDC24\uD801\uDC25\uD801\uDC26\uD801\uDC27", - "\uD801\uDC28\uD801\uDC29\uD801\uDC2A\uD801\uDC2B\uD801\uDC2C\uD801\uDC2D\uD801\uDC2E\uD801\uDC2F\uD801\uDC30\uD801\uDC31\uD801\uDC32\uD801\uDC33\uD801\uDC34\uD801\uDC35\uD801\uDC36\uD801\uDC37\uD801\uDC38\uD801\uDC39\uD801\uDC3A\uD801\uDC3B\uD801\uDC3C\uD801\uDC3D\uD801\uDC3E\uD801\uDC3F\uD801\uDC40\uD801\uDC41\uD801\uDC42\uD801\uDC43\uD801\uDC44\uD801\uDC45\uD801\uDC46\uD801\uDC47\uD801\uDC48\uD801\uDC49\uD801\uDC4A\uD801\uDC4B\uD801\uDC4C\uD801\uDC4D\uD801\uDC4E\uD801\uDC4F", - ), -]) -add("test/built-ins/String/prototype/toUpperCase/S15.5.4.18_A2_T1.js", "toUpperCase", [ - assertion("#1 direct value", "Hello, WoRlD!", "HELLO, WORLD!"), - assertion("#2 String value", "Hello, WoRlD!", "HELLO, WORLD!"), -]) -add("test/built-ins/String/prototype/toUpperCase/special_casing.js", "toUpperCase", [ - assertion( - "103 SpecialCasing mappings", - "\u00DF\u0130\uFB00\uFB01\uFB02\uFB03\uFB04\uFB05\uFB06\u0587\uFB13\uFB14\uFB15\uFB16\uFB17\u0149\u0390\u03B0\u01F0\u1E96\u1E97\u1E98\u1E99\u1E9A\u1F50\u1F52\u1F54\u1F56\u1FB6\u1FC6\u1FD2\u1FD3\u1FD6\u1FD7\u1FE2\u1FE3\u1FE4\u1FE6\u1FE7\u1FF6\u1F80\u1F81\u1F82\u1F83\u1F84\u1F85\u1F86\u1F87\u1F88\u1F89\u1F8A\u1F8B\u1F8C\u1F8D\u1F8E\u1F8F\u1F90\u1F91\u1F92\u1F93\u1F94\u1F95\u1F96\u1F97\u1F98\u1F99\u1F9A\u1F9B\u1F9C\u1F9D\u1F9E\u1F9F\u1FA0\u1FA1\u1FA2\u1FA3\u1FA4\u1FA5\u1FA6\u1FA7\u1FA8\u1FA9\u1FAA\u1FAB\u1FAC\u1FAD\u1FAE\u1FAF\u1FB3\u1FBC\u1FC3\u1FCC\u1FF3\u1FFC\u1FB2\u1FB4\u1FC2\u1FC4\u1FF2\u1FF4\u1FB7\u1FC7\u1FF7", - "\u0053\u0053\u0130\u0046\u0046\u0046\u0049\u0046\u004C\u0046\u0046\u0049\u0046\u0046\u004C\u0053\u0054\u0053\u0054\u0535\u0552\u0544\u0546\u0544\u0535\u0544\u053B\u054E\u0546\u0544\u053D\u02BC\u004E\u0399\u0308\u0301\u03A5\u0308\u0301\u004A\u030C\u0048\u0331\u0054\u0308\u0057\u030A\u0059\u030A\u0041\u02BE\u03A5\u0313\u03A5\u0313\u0300\u03A5\u0313\u0301\u03A5\u0313\u0342\u0391\u0342\u0397\u0342\u0399\u0308\u0300\u0399\u0308\u0301\u0399\u0342\u0399\u0308\u0342\u03A5\u0308\u0300\u03A5\u0308\u0301\u03A1\u0313\u03A5\u0342\u03A5\u0308\u0342\u03A9\u0342\u1F08\u0399\u1F09\u0399\u1F0A\u0399\u1F0B\u0399\u1F0C\u0399\u1F0D\u0399\u1F0E\u0399\u1F0F\u0399\u1F08\u0399\u1F09\u0399\u1F0A\u0399\u1F0B\u0399\u1F0C\u0399\u1F0D\u0399\u1F0E\u0399\u1F0F\u0399\u1F28\u0399\u1F29\u0399\u1F2A\u0399\u1F2B\u0399\u1F2C\u0399\u1F2D\u0399\u1F2E\u0399\u1F2F\u0399\u1F28\u0399\u1F29\u0399\u1F2A\u0399\u1F2B\u0399\u1F2C\u0399\u1F2D\u0399\u1F2E\u0399\u1F2F\u0399\u1F68\u0399\u1F69\u0399\u1F6A\u0399\u1F6B\u0399\u1F6C\u0399\u1F6D\u0399\u1F6E\u0399\u1F6F\u0399\u1F68\u0399\u1F69\u0399\u1F6A\u0399\u1F6B\u0399\u1F6C\u0399\u1F6D\u0399\u1F6E\u0399\u1F6F\u0399\u0391\u0399\u0391\u0399\u0397\u0399\u0397\u0399\u03A9\u0399\u03A9\u0399\u1FBA\u0399\u0386\u0399\u1FCA\u0399\u0389\u0399\u1FFA\u0399\u038F\u0399\u0391\u0342\u0399\u0397\u0342\u0399\u03A9\u0342\u0399", - ), -]) -add("test/built-ins/String/prototype/toUpperCase/supplementary_plane.js", "toUpperCase", [ - assertion( - "40 Deseret mappings", - "\uD801\uDC28\uD801\uDC29\uD801\uDC2A\uD801\uDC2B\uD801\uDC2C\uD801\uDC2D\uD801\uDC2E\uD801\uDC2F\uD801\uDC30\uD801\uDC31\uD801\uDC32\uD801\uDC33\uD801\uDC34\uD801\uDC35\uD801\uDC36\uD801\uDC37\uD801\uDC38\uD801\uDC39\uD801\uDC3A\uD801\uDC3B\uD801\uDC3C\uD801\uDC3D\uD801\uDC3E\uD801\uDC3F\uD801\uDC40\uD801\uDC41\uD801\uDC42\uD801\uDC43\uD801\uDC44\uD801\uDC45\uD801\uDC46\uD801\uDC47\uD801\uDC48\uD801\uDC49\uD801\uDC4A\uD801\uDC4B\uD801\uDC4C\uD801\uDC4D\uD801\uDC4E\uD801\uDC4F", - "\uD801\uDC00\uD801\uDC01\uD801\uDC02\uD801\uDC03\uD801\uDC04\uD801\uDC05\uD801\uDC06\uD801\uDC07\uD801\uDC08\uD801\uDC09\uD801\uDC0A\uD801\uDC0B\uD801\uDC0C\uD801\uDC0D\uD801\uDC0E\uD801\uDC0F\uD801\uDC10\uD801\uDC11\uD801\uDC12\uD801\uDC13\uD801\uDC14\uD801\uDC15\uD801\uDC16\uD801\uDC17\uD801\uDC18\uD801\uDC19\uD801\uDC1A\uD801\uDC1B\uD801\uDC1C\uD801\uDC1D\uD801\uDC1E\uD801\uDC1F\uD801\uDC20\uD801\uDC21\uD801\uDC22\uD801\uDC23\uD801\uDC24\uD801\uDC25\uD801\uDC26\uD801\uDC27", - ), -]) - -const whitespace = "\u0009\u000A\u000B\u000C\u000D\u0020\u00A0\u1680\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200A\u2028\u2029\u202F\u205F\u3000\uFEFF" -const lineTerminators = "\u000A\u000D\u2028\u2029" -const trim = (file: string, input: string, expected: string) => - add(`test/built-ins/String/prototype/trim/${file}`, "trim", [assertion("upstream assertion", input, expected)]) - -trim("15.5.4.20-3-1.js", lineTerminators, "") -trim("15.5.4.20-3-2.js", whitespace, "") -trim("15.5.4.20-3-3.js", whitespace + lineTerminators, "") -trim("15.5.4.20-3-4.js", whitespace + lineTerminators + "abc", "abc") -trim("15.5.4.20-3-5.js", "abc" + whitespace + lineTerminators, "abc") -trim("15.5.4.20-3-6.js", whitespace + lineTerminators + "abc" + whitespace + lineTerminators, "abc") -trim("15.5.4.20-3-7.js", "ab" + whitespace + lineTerminators + "cd", "ab" + whitespace + lineTerminators + "cd") -trim("15.5.4.20-3-8.js", "\0\u0000", "\0\u0000") -trim("15.5.4.20-3-9.js", "\0", "\0") -trim("15.5.4.20-3-10.js", "\u0000", "\u0000") -trim("15.5.4.20-3-11.js", "\0\u0000abc", "\0\u0000abc") -trim("15.5.4.20-3-12.js", "abc\0\u0000", "abc\0\u0000") -trim("15.5.4.20-3-13.js", "\0\u0000abc\0\u0000", "\0\u0000abc\0\u0000") -trim("15.5.4.20-3-14.js", "a\0\u0000bc", "a\0\u0000bc") -trim("15.5.4.20-4-1.js", "\u0009a bc \u0009", "a bc") -trim("15.5.4.20-4-2.js", " \u0009abc \u0009", "abc") -trim("15.5.4.20-4-3.js", "\u0009abc", "abc") -trim("15.5.4.20-4-4.js", "\u000Babc", "abc") -trim("15.5.4.20-4-5.js", "\u000Cabc", "abc") -trim("15.5.4.20-4-6.js", "\u0020abc", "abc") -trim("15.5.4.20-4-8.js", "\u00A0abc", "abc") -trim("15.5.4.20-4-10.js", "\uFEFFabc", "abc") -trim("15.5.4.20-4-11.js", "abc\u0009", "abc") -trim("15.5.4.20-4-12.js", "abc\u000B", "abc") -trim("15.5.4.20-4-13.js", "abc\u000C", "abc") -trim("15.5.4.20-4-14.js", "abc\u0020", "abc") -trim("15.5.4.20-4-16.js", "abc\u00A0", "abc") -trim("15.5.4.20-4-18.js", "abc\uFEFF", "abc") -trim("15.5.4.20-4-19.js", "\u0009abc\u0009", "abc") -trim("15.5.4.20-4-20.js", "\u000Babc\u000B", "abc") -trim("15.5.4.20-4-21.js", "\u000Cabc\u000C", "abc") -trim("15.5.4.20-4-22.js", "\u0020abc\u0020", "abc") -trim("15.5.4.20-4-24.js", "\u00A0abc\u00A0", "abc") -trim("15.5.4.20-4-27.js", "\u0009\u0009", "") -trim("15.5.4.20-4-28.js", "\u000B\u000B", "") -trim("15.5.4.20-4-29.js", "\u000C\u000C", "") -trim("15.5.4.20-4-30.js", "\u0020\u0020", "") -trim("15.5.4.20-4-32.js", "\u00A0\u00A0", "") -trim("15.5.4.20-4-34.js", "\uFEFF\uFEFF", "") -trim("15.5.4.20-4-35.js", "ab\u0009c", "ab\u0009c") -trim("15.5.4.20-4-36.js", "ab\u000Bc", "ab\u000Bc") -trim("15.5.4.20-4-37.js", "ab\u000Cc", "ab\u000Cc") -trim("15.5.4.20-4-38.js", "ab\u0020c", "ab\u0020c") -trim("15.5.4.20-4-39.js", "ab\u0085c", "ab\u0085c") -trim("15.5.4.20-4-40.js", "ab\u00A0c", "ab\u00A0c") -trim("15.5.4.20-4-41.js", "ab\u200Bc", "ab\u200Bc") -trim("15.5.4.20-4-42.js", "ab\uFEFFc", "ab\uFEFFc") -trim("15.5.4.20-4-43.js", "\u000Aabc", "abc") -trim("15.5.4.20-4-44.js", "\u000Dabc", "abc") -trim("15.5.4.20-4-45.js", "\u2028abc", "abc") -trim("15.5.4.20-4-46.js", "\u2029abc", "abc") -trim("15.5.4.20-4-47.js", "abc\u000A", "abc") -trim("15.5.4.20-4-48.js", "abc\u000D", "abc") -trim("15.5.4.20-4-49.js", "abc\u2028", "abc") -trim("15.5.4.20-4-50.js", "abc\u2029", "abc") -trim("15.5.4.20-4-51.js", "\u000Aabc\u000A", "abc") -trim("15.5.4.20-4-52.js", "\u000Dabc\u000D", "abc") -trim("15.5.4.20-4-53.js", "\u2028abc\u2028", "abc") -trim("15.5.4.20-4-54.js", "\u2029abc\u2029", "abc") -trim("15.5.4.20-4-55.js", "\u000A\u000A", "") -trim("15.5.4.20-4-56.js", "\u000D\u000D", "") -trim("15.5.4.20-4-57.js", "\u2028\u2028", "") -trim("15.5.4.20-4-58.js", "\u2029\u2029", "") -trim("15.5.4.20-4-59.js", "\u2029 abc", "abc") -trim("15.5.4.20-4-60.js", " ", "") -add("test/built-ins/String/prototype/trim/u180e.js", "trim", [ - assertion("trailing U+180E", "_\u180E", "_\u180E"), - assertion("only U+180E", "\u180E", "\u180E"), - assertion("leading U+180E", "\u180E_", "\u180E_"), -]) - -const directionalWhitespace = "\x09\x0A\x0B\x0C\x0D\x20\xA0\u1680\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200A\u202F\u205F\u3000\u2028\u2029\uFEFF" -add("test/built-ins/String/prototype/trimStart/this-value-whitespace.js", "trimStart", [ - assertion("all whitespace", directionalWhitespace + "a" + directionalWhitespace + "b" + directionalWhitespace, "a" + directionalWhitespace + "b" + directionalWhitespace), -]) -add("test/built-ins/String/prototype/trimStart/this-value-line-terminator.js", "trimStart", [ - assertion("all line terminators", lineTerminators + "a" + lineTerminators + "b" + lineTerminators, "a" + lineTerminators + "b" + lineTerminators), -]) -add("test/built-ins/String/prototype/trimEnd/this-value-whitespace.js", "trimEnd", [ - assertion("all whitespace", directionalWhitespace + "a" + directionalWhitespace + "b" + directionalWhitespace, directionalWhitespace + "a" + directionalWhitespace + "b"), -]) -add("test/built-ins/String/prototype/trimEnd/this-value-line-terminator.js", "trimEnd", [ - assertion("all line terminators", lineTerminators + "a" + lineTerminators + "b" + lineTerminators, lineTerminators + "a" + lineTerminators + "b"), -]) -add("test/built-ins/String/prototype/repeat/repeat-string-n-times.js", "repeat", [ - assertion("repeat once", "abc", "abc", [1]), - assertion("repeat three times", "abc", "abcabcabc", [3]), - { label: "repeat 10000 times length", input: ".", args: [10000], expected: 10000, outcome: "length" }, -]) -add("test/built-ins/String/prototype/repeat/empty-string-returns-empty.js", "repeat", [ - assertion("count 1", "", "", [1]), - assertion("count 3", "", "", [3]), - assertion("maximum 32-bit count", "", "", [0xffffffff]), -]) -add("test/built-ins/String/prototype/repeat/count-is-zero-returns-empty-string.js", "repeat", [ - assertion("zero", "foo", "", [0]), -]) -add("test/built-ins/String/prototype/repeat/count-coerced-to-zero-returns-empty-string.js", "repeat", [ - assertion("fraction truncates to zero", "abc", "", [0.9]), -]) -add("test/built-ins/String/prototype/padStart/fill-string-empty.js", "padStart", [assertion("empty fill", "abc", "abc", [5, ""])]) -add("test/built-ins/String/prototype/padStart/normal-operation.js", "padStart", [ - assertion("truncated multi-character fill", "abc", "defdabc", [7, "def"]), - assertion("single-character fill", "abc", "**abc", [5, "*"]), - assertion("truncated surrogate pair", "abc", "\uD83D\uDCA9\uD83Dabc", [6, "\uD83D\uDCA9"]), -]) -add("test/built-ins/String/prototype/padStart/fill-string-omitted.js", "padStart", [ - assertion("omitted fill", "abc", " abc", [5]), - assertion("undefined fill", "abc", " abc", [5, undefined]), -]) -add("test/built-ins/String/prototype/padStart/max-length-not-greater-than-string.js", "padStart", [ - assertion("NaN", "abc", "abc", [NaN, "def"]), - assertion("negative infinity", "abc", "abc", [-Infinity, "def"]), - assertion("zero", "abc", "abc", [0, "def"]), - assertion("negative one", "abc", "abc", [-1, "def"]), - assertion("equal length", "abc", "abc", [3, "def"]), - assertion("fraction truncates", "abc", "abc", [3.9999, "def"]), -]) -add("test/built-ins/String/prototype/padEnd/fill-string-empty.js", "padEnd", [assertion("empty fill", "abc", "abc", [5, ""])]) -add("test/built-ins/String/prototype/padEnd/normal-operation.js", "padEnd", [ - assertion("truncated multi-character fill", "abc", "abcdefd", [7, "def"]), - assertion("single-character fill", "abc", "abc**", [5, "*"]), - assertion("truncated surrogate pair", "abc", "abc\uD83D\uDCA9\uD83D", [6, "\uD83D\uDCA9"]), -]) -add("test/built-ins/String/prototype/padEnd/fill-string-omitted.js", "padEnd", [ - assertion("omitted fill", "abc", "abc ", [5]), - assertion("undefined fill", "abc", "abc ", [5, undefined]), -]) -add("test/built-ins/String/prototype/padEnd/max-length-not-greater-than-string.js", "padEnd", [ - assertion("NaN", "abc", "abc", [NaN, "def"]), - assertion("negative infinity", "abc", "abc", [-Infinity, "def"]), - assertion("zero", "abc", "abc", [0, "def"]), - assertion("negative one", "abc", "abc", [-1, "def"]), - assertion("equal length", "abc", "abc", [3, "def"]), - assertion("fraction truncates", "abc", "abc", [3.9999, "def"]), -]) - -add("test/built-ins/String/prototype/charAt/S15.5.4.4_A1_T4.js", "charAt", [assertion("omitted position", "lego", "l")]) -add("test/built-ins/String/prototype/charAt/S15.5.4.4_A1_T7.js", "charAt", [assertion("undefined position", "lego", "l", [undefined])]) -add("test/built-ins/String/prototype/charAt/S15.5.4.4_A1_T8.js", "charAt", [assertion("undefined position", "42", "4", [undefined])]) -add("test/built-ins/String/prototype/charAt/S15.5.4.4_A4_T1.js", "charAt", ["A", "B", "C", "A", "B", "C"].map((expected, position) => assertion(`position ${position}`, "ABCABC", expected, [position]))) -add("test/built-ins/String/prototype/charAt/S15.5.4.4_A4_T2.js", "charAt", [-2, -1].map((position) => assertion(`position ${position}`, "ABCABC", "", [position]))) -add("test/built-ins/String/prototype/charAt/S15.5.4.4_A4_T3.js", "charAt", [6, 7].map((position) => assertion(`position ${position}`, "ABCABC", "", [position]))) -add("test/built-ins/String/prototype/charAt/S9.4_A1.js", "charAt", [assertion("NaN position", "abc", "a", [NaN])]) -add("test/built-ins/String/prototype/charAt/S9.4_A2.js", "charAt", [ - assertion("positive zero", "abc", "a", [0]), - assertion("negative zero", "abc", "a", [-0]), -]) -add("test/built-ins/String/prototype/charAt/pos-rounding.js", "charAt", [ - assertion("-0.99999", "abc", "a", [-0.99999]), - assertion("-0.00001", "abc", "a", [-0.00001]), - assertion("0.00001", "abc", "a", [0.00001]), - assertion("0.99999", "abc", "a", [0.99999]), - assertion("1.00001", "abc", "b", [1.00001]), - assertion("1.99999", "abc", "b", [1.99999]), -]) - -add("test/built-ins/String/prototype/charCodeAt/S15.5.4.5_A1_T4.js", "charCodeAt", [assertion("omitted position", "smart", 0x73)]) -add("test/built-ins/String/prototype/charCodeAt/S15.5.4.5_A1_T7.js", "charCodeAt", [assertion("undefined position", "lego", 0x6c, [undefined])]) -add("test/built-ins/String/prototype/charCodeAt/S15.5.4.5_A1_T8.js", "charCodeAt", [assertion("undefined position", "42", 0x34, [undefined])]) -add("test/built-ins/String/prototype/charCodeAt/pos-rounding.js", "charCodeAt", [ - assertion("-0.99999", "abc", 0x61, [-0.99999]), - assertion("-0.00001", "abc", 0x61, [-0.00001]), - assertion("0.00001", "abc", 0x61, [0.00001]), - assertion("0.99999", "abc", 0x61, [0.99999]), - assertion("1.00001", "abc", 0x62, [1.00001]), - assertion("1.99999", "abc", 0x62, [1.99999]), -]) - -add("test/built-ins/String/prototype/codePointAt/return-single-code-unit.js", "codePointAt", [ - assertion("a", "abc", 97, [0]), assertion("b", "abc", 98, [1]), assertion("c", "abc", 99, [2]), - assertion("ordinary BMP", "\uAAAA\uBBBB", 0xaaaa, [0]), assertion("before high-surrogate range", "\uD7FF\uAAAA", 0xd7ff, [0]), - assertion("low surrogate", "\uDC00\uAAAA", 0xdc00, [0]), assertion("trailing D800", "123\uD800", 0xd800, [3]), - assertion("trailing DAAA", "123\uDAAA", 0xdaaa, [3]), assertion("trailing DBFF", "123\uDBFF", 0xdbff, [3]), -]) -add("test/built-ins/String/prototype/codePointAt/return-first-code-unit.js", "codePointAt", [ - assertion("D800 before DBFF", "\uD800\uDBFF", 0xd800, [0]), assertion("D800 before E000", "\uD800\uE000", 0xd800, [0]), - assertion("DAAA before DBFF", "\uDAAA\uDBFF", 0xdaaa, [0]), assertion("DAAA before E000", "\uDAAA\uE000", 0xdaaa, [0]), - assertion("DBFF before DBFF", "\uDBFF\uDBFF", 0xdbff, [0]), assertion("DBFF before E000", "\uDBFF\uE000", 0xdbff, [0]), - assertion("D800 before NUL", "\uD800\u0000", 0xd800, [0]), assertion("D800 before FFFF", "\uD800\uFFFF", 0xd800, [0]), - assertion("DAAA before NUL", "\uDAAA\u0000", 0xdaaa, [0]), assertion("DAAA before FFFF", "\uDAAA\uFFFF", 0xdaaa, [0]), - assertion("DBFF before FFFF", "\uDBFF\uFFFF", 0xdbff, [0]), -]) -add("test/built-ins/String/prototype/codePointAt/return-utf16-decode.js", "codePointAt", [ - assertion("U+10000", "\uD800\uDC00", 65536, [0]), assertion("U+101D0", "\uD800\uDDD0", 66000, [0]), - assertion("U+103FF", "\uD800\uDFFF", 66559, [0]), assertion("U+BA800", "\uDAAA\uDC00", 763904, [0]), - assertion("U+BA9D0", "\uDAAA\uDDD0", 764368, [0]), assertion("U+BABFF", "\uDAAA\uDFFF", 764927, [0]), - assertion("U+10FC00", "\uDBFF\uDC00", 1113088, [0]), assertion("U+10FDD0", "\uDBFF\uDDD0", 1113552, [0]), - assertion("U+10FFFF", "\uDBFF\uDFFF", 1114111, [0]), -]) -add("test/built-ins/String/prototype/codePointAt/return-code-unit-coerced-position.js", "codePointAt", [ - assertion("NaN", "\uD800\uDC00", 65536, [NaN]), assertion("undefined", "\uD800\uDC00", 65536, [undefined]), -]) -add("test/built-ins/String/prototype/codePointAt/returns-undefined-on-position-less-than-zero.js", "codePointAt", [ - { label: "negative one", input: "abc", args: [-1], outcome: "undefined" }, - { label: "negative infinity", input: "abc", args: [-Infinity], outcome: "undefined" }, -]) -add("test/built-ins/String/prototype/codePointAt/returns-undefined-on-position-equal-or-more-than-size.js", "codePointAt", [ - { label: "equal to size", input: "abc", args: [3], outcome: "undefined" }, - { label: "greater than size", input: "abc", args: [4], outcome: "undefined" }, - { label: "positive infinity", input: "abc", args: [Infinity], outcome: "undefined" }, -]) - -add("test/built-ins/String/prototype/at/returns-code-unit.js", "at", [ - assertion("position 0", "12\uD80034", "1", [0]), assertion("position 1", "12\uD80034", "2", [1]), - assertion("unpaired surrogate", "12\uD80034", "\uD800", [2]), assertion("position 3", "12\uD80034", "3", [3]), - assertion("position 4", "12\uD80034", "4", [4]), -]) -add("test/built-ins/String/prototype/at/returns-item.js", "at", ["1", "2", "3", "4", "5"].map((expected, position) => assertion(`position ${position}`, "12345", expected, [position]))) -add("test/built-ins/String/prototype/at/returns-item-relative-index.js", "at", [ - assertion("zero", "12345", "1", [0]), assertion("negative one", "12345", "5", [-1]), - assertion("negative three", "12345", "3", [-3]), assertion("negative four", "12345", "2", [-4]), -]) -add("test/built-ins/String/prototype/at/returns-undefined-for-out-of-range-index.js", "at", [-2, 0, 1].map((position) => ({ label: `position ${position}`, input: "", args: [position], outcome: "undefined" }))) -add("test/built-ins/String/prototype/at/index-non-numeric-argument-tointeger.js", "at", [assertion("undefined", "01", "0", [undefined])]) - -add("test/built-ins/String/prototype/concat/S15.5.4.6_A1_T4.js", "concat", [assertion("no arguments", "lego", "lego")]) -add("test/built-ins/String/prototype/toString/string-primitive.js", "toString", [ - assertion("empty string", "", ""), assertion("non-empty string", "str", "str"), -]) - -add("test/built-ins/String/prototype/normalize/return-normalized-string.js", "normalize", [ - assertion("NFC short", "\u1E9B\u0323", "\u1E9B\u0323", ["NFC"]), - assertion("NFD short", "\u1E9B\u0323", "\u017F\u0323\u0307", ["NFD"]), - assertion("NFKC short", "\u1E9B\u0323", "\u1E69", ["NFKC"]), - assertion("NFKD short", "\u1E9B\u0323", "\u0073\u0323\u0307", ["NFKD"]), - assertion("NFC long", "\u00C5\u2ADC\u0958\u2126\u0344", "\xC5\u2ADD\u0338\u0915\u093C\u03A9\u0308\u0301", ["NFC"]), - assertion("NFD long", "\u00C5\u2ADC\u0958\u2126\u0344", "A\u030A\u2ADD\u0338\u0915\u093C\u03A9\u0308\u0301", ["NFD"]), - assertion("NFKC long", "\u00C5\u2ADC\u0958\u2126\u0344", "\xC5\u2ADD\u0338\u0915\u093C\u03A9\u0308\u0301", ["NFKC"]), - assertion("NFKD long", "\u00C5\u2ADC\u0958\u2126\u0344", "A\u030A\u2ADD\u0338\u0915\u093C\u03A9\u0308\u0301", ["NFKD"]), -]) -add("test/built-ins/String/prototype/normalize/return-normalized-string-using-default-parameter.js", "normalize", [ - assertion("omitted", "\u00C5\u2ADC\u0958\u2126\u0344", "\xC5\u2ADD\u0338\u0915\u093C\u03A9\u0308\u0301"), - assertion("undefined", "\u00C5\u2ADC\u0958\u2126\u0344", "\xC5\u2ADD\u0338\u0915\u093C\u03A9\u0308\u0301", [undefined]), -]) -add("test/built-ins/String/prototype/normalize/form-is-not-valid-throws.js", "normalize", [ - { label: "bar", input: "foo", args: ["bar"], outcome: "RangeError" }, - { label: "NFC1", input: "foo", args: ["NFC1"], outcome: "RangeError" }, -]) - -add("test/built-ins/String/prototype/localeCompare/15.5.4.9_CE.js", "localeCompare", [ - assertion("D70", "o\u0308", 0, ["ö"]), assertion("reordered diaeresis", "ä\u0323", 0, ["a\u0323\u0308"]), - assertion("reordered marks", "a\u0308\u0323", 0, ["a\u0323\u0308"]), assertion("precomposed dot below", "ạ\u0308", 0, ["a\u0323\u0308"]), - assertion("breve after diaeresis", "ä\u0306", 0, ["a\u0308\u0306"]), assertion("diaeresis after breve", "ă\u0308", 0, ["a\u0306\u0308"]), - assertion("Hangul", "\u1111\u1171\u11B6", 0, ["퓛"]), assertion("angstrom compatibility", "Å", 0, ["Å"]), - assertion("angstrom decomposed", "Å", 0, ["A\u030A"]), assertion("reordered horn and dot", "x\u031B\u0323", 0, ["x\u0323\u031B"]), - assertion("Vietnamese precomposed 1", "ự", 0, ["ụ\u031B"]), assertion("Vietnamese decomposed", "ự", 0, ["u\u031B\u0323"]), - assertion("Vietnamese precomposed 2", "ự", 0, ["ư\u0323"]), assertion("Vietnamese reordered", "ự", 0, ["u\u0323\u031B"]), - assertion("cedilla", "Ç", 0, ["C\u0327"]), assertion("q reordered", "q\u0307\u0323", 0, ["q\u0323\u0307"]), - assertion("Hangul syllable", "가", 0, ["\u1100\u1161"]), assertion("ohm", "Ω", 0, ["Ω"]), - assertion("angstrom", "Å", 0, ["A\u030A"]), assertion("circumflex", "ô", 0, ["o\u0302"]), - assertion("s with marks", "ṩ", 0, ["s\u0323\u0307"]), assertion("d composed plus dot", "ḋ\u0323", 0, ["d\u0323\u0307"]), - assertion("d two precompositions", "ḋ\u0323", 0, ["ḍ\u0307"]), -]) - -add("test/built-ins/String/fromCharCode/S15.5.3.2_A2.js", "fromCharCode", [{ label: "no arguments", expected: "" }], true) -add("test/built-ins/String/fromCharCode/S15.5.3.2_A3_T1.js", "fromCharCode", [{ label: "ABBA", args: [65, 66, 66, 65], expected: "ABBA" }], true) -add("test/built-ins/String/fromCharCode/S9.7_A1.js", "fromCharCode", [ - { label: "NaN", args: [NaN], expected: 0 }, { label: "zero", args: [0], expected: 0 }, { label: "negative zero", args: [-0], expected: 0 }, - { label: "positive infinity", args: [Infinity], expected: 0 }, { label: "negative infinity", args: [-Infinity], expected: 0 }, -], true) -add("test/built-ins/String/fromCharCode/S9.7_A2.1.js", "fromCharCode", [ - [0, 0], [1, 1], [-1, 65535], [65535, 65535], [65534, 65534], [65536, 0], [4294967295, 65535], [4294967294, 65534], [4294967296, 0], -].map(([input, expected]) => ({ label: String(input), args: [input!], expected })), true) -add("test/built-ins/String/fromCharCode/S9.7_A2.2.js", "fromCharCode", [ - [-32767, 32769], [-32768, 32768], [-32769, 32767], [-65535, 1], [-65536, 0], [-65537, 65535], [65535, 65535], [65536, 0], [65537, 1], [131071, 65535], [131072, 0], [131073, 1], -].map(([input, expected]) => ({ label: String(input), args: [input!], expected })), true) -add("test/built-ins/String/fromCharCode/S9.7_A3.2_T1.js", "fromCharCode", [ - { label: "positive fraction", args: [1.2345], expected: 1 }, { label: "negative fraction", args: [-5.4321], expected: 65531 }, -], true) - -add("test/built-ins/String/fromCodePoint/arguments-is-empty.js", "fromCodePoint", [{ label: "no arguments", expected: "" }], true) -add("test/built-ins/String/fromCodePoint/return-string-value.js", "fromCodePoint", [ - { label: "NUL", args: [0], expected: "\x00" }, { label: "asterisk", args: [42], expected: "*" }, - { label: "AZ", args: [65, 90], expected: "AZ" }, { label: "Cyrillic", args: [0x404], expected: "\u0404" }, - { label: "hex supplementary", args: [0x2f804], expected: "\uD87E\uDC04" }, { label: "decimal supplementary", args: [194564], expected: "\uD87E\uDC04" }, - { label: "mixed supplementary", args: [0x1d306, 0x61, 0x1d307], expected: "\uD834\uDF06a\uD834\uDF07" }, - { label: "maximum code point", args: [1114111], expected: "\uDBFF\uDFFF" }, -], true) -add("test/built-ins/String/fromCodePoint/argument-is-not-integer.js", "fromCodePoint", [ - { label: "fraction", args: [3.14], outcome: "RangeError" }, { label: "fraction after valid", args: [42, 3.14], outcome: "RangeError" }, -], true) -add("test/built-ins/String/fromCodePoint/number-is-out-of-range.js", "fromCodePoint", [ - { label: "negative one", args: [-1], outcome: "RangeError" }, { label: "negative after valid", args: [1, -1], outcome: "RangeError" }, - { label: "above maximum", args: [1114112], outcome: "RangeError" }, { label: "infinity", args: [Infinity], outcome: "RangeError" }, -], true) - -describe("Test262-adapted core String behavior", () => { - for (const vector of vectors) { - test(vector.path, async () => { - const results = vector.assertions.map((item) => { - const args = (item.args ?? []).map(literal).join(", ") - const expression = vector.static - ? `String.${vector.method}(${args})` - : `${JSON.stringify(item.input)}.${vector.method}(${args})` - const observed = vector.static && vector.method === "fromCharCode" && typeof item.expected === "number" - ? `${expression}.charCodeAt(0)` - : expression - const checked = item.outcome === "undefined" - ? `${observed} === undefined` - : item.outcome === "length" - ? `${observed}.length` - : item.outcome === "RangeError" - ? `(() => { try { ${observed}; return false } catch (error) { return error instanceof RangeError } })()` - : observed - return `{ label: ${JSON.stringify(item.label)}, value: ${checked} }` - }) - const expected = vector.assertions.map((item) => ({ - label: item.label, - value: item.outcome === undefined || item.outcome === "length" ? item.expected! : true, - })) - expect(await value(`return [${results.join(",")}]`)).toEqual(expected) - }) - } -}) diff --git a/packages/codemode/test/string-regexp-test262.test.ts b/packages/codemode/test/string-regexp-test262.test.ts deleted file mode 100644 index 80c8908229..0000000000 --- a/packages/codemode/test/string-regexp-test262.test.ts +++ /dev/null @@ -1,625 +0,0 @@ -/* - * Portions adapted from Test262 at revision 250f204f23a9249ff204be2baec29600faae7b75: - * - test/built-ins/String/prototype/split/separator-regexp.js - * - test/built-ins/String/prototype/split/arguments-are-regexp-s-and-3-and-instance-is-string-a-b-c-de-f.js - * - test/built-ins/String/prototype/split/argument-is-regexp-s-and-instance-is-string-a-b-c-de-f.js - * - test/built-ins/String/prototype/split/argument-is-regexp-d-and-instance-is-string-dfe23iu-34-65.js - * - test/built-ins/String/prototype/split/argument-is-regexp-reg-exp-d-and-instance-is-string-dfe23iu-34-65.js - * - test/built-ins/String/prototype/split/argument-is-regexp-a-z-and-instance-is-string-abc.js - * - test/built-ins/String/prototype/split/argument-is-reg-exp-a-z-and-instance-is-string-abc.js - * - test/built-ins/String/prototype/split/arguments-are-regexp-l-and-undefined-and-instance-is-string-hello.js - * - test/built-ins/String/prototype/split/arguments-are-regexp-l-and-0-and-instance-is-string-hello.js - * - test/built-ins/String/prototype/split/arguments-are-regexp-l-and-1-and-instance-is-string-hello.js - * - test/built-ins/String/prototype/split/arguments-are-regexp-l-and-2-and-instance-is-string-hello.js - * - test/built-ins/String/prototype/split/arguments-are-regexp-l-and-3-and-instance-is-string-hello.js - * - test/built-ins/String/prototype/split/arguments-are-regexp-l-and-4-and-instance-is-string-hello.js - * - test/built-ins/String/prototype/split/argument-is-regexp-l-and-instance-is-string-hello.js - * - test/built-ins/String/prototype/split/argument-is-new-reg-exp-and-instance-is-string-hello.js - * - test/built-ins/String/prototype/split/arguments-are-new-reg-exp-and-0-and-instance-is-string-hello.js - * - test/built-ins/String/prototype/split/arguments-are-new-reg-exp-and-1-and-instance-is-string-hello.js - * - test/built-ins/String/prototype/split/arguments-are-new-reg-exp-and-2-and-instance-is-string-hello.js - * - test/built-ins/String/prototype/split/arguments-are-new-reg-exp-and-3-and-instance-is-string-hello.js - * - test/built-ins/String/prototype/split/arguments-are-new-reg-exp-and-4-and-instance-is-string-hello.js - * - test/built-ins/String/prototype/split/arguments-are-new-reg-exp-and-undefined-and-instance-is-string-hello.js - * - test/built-ins/String/prototype/split/call-split-2-instance-is-string-one-two-three-four-five.js - * - test/built-ins/String/prototype/split/separator-regexp-comma-instance-is-string-one-1-two-2-four-4.js - * - test/built-ins/String/prototype/split/argument-is-regexp-x-and-instance-is-string-a-b-c-de-f.js - * - test/built-ins/String/prototype/replace/regexp-capture-by-index.js - * - test/built-ins/String/prototype/replace/S15.5.4.11_A1_T17.js - * - test/built-ins/String/prototype/replace/S15.5.4.11_A2_T1.js - * - test/built-ins/String/prototype/replace/S15.5.4.11_A2_T2.js - * - test/built-ins/String/prototype/replace/S15.5.4.11_A2_T3.js - * - test/built-ins/String/prototype/replace/S15.5.4.11_A2_T4.js - * - test/built-ins/String/prototype/replace/S15.5.4.11_A2_T5.js - * - test/built-ins/String/prototype/replace/S15.5.4.11_A2_T6.js - * - test/built-ins/String/prototype/replace/S15.5.4.11_A2_T7.js - * - test/built-ins/String/prototype/replace/S15.5.4.11_A2_T8.js - * - test/built-ins/String/prototype/replace/S15.5.4.11_A2_T9.js - * - test/built-ins/String/prototype/replace/S15.5.4.11_A2_T10.js - * - test/built-ins/String/prototype/replace/S15.5.4.11_A3_T1.js - * - test/built-ins/String/prototype/replace/S15.5.4.11_A3_T2.js - * - test/built-ins/String/prototype/replace/S15.5.4.11_A3_T3.js - * - test/built-ins/String/prototype/replace/S15.5.4.11_A5_T1.js - * - test/built-ins/String/prototype/replaceAll/searchValue-replacer-RegExp-call.js - * - test/built-ins/String/prototype/replaceAll/searchValue-empty-string.js - * - test/built-ins/String/prototype/replaceAll/searchValue-empty-string-this-empty-string.js - * - test/built-ins/String/prototype/replaceAll/replaceValue-value-replaces-string.js - * - test/built-ins/String/prototype/replaceAll/getSubstitution-0x0024.js - * - test/built-ins/String/prototype/replaceAll/getSubstitution-0x0024-0x0024.js - * - test/built-ins/String/prototype/replaceAll/getSubstitution-0x0024-0x0026.js - * - test/built-ins/String/prototype/replaceAll/getSubstitution-0x0024-0x0060.js - * - test/built-ins/String/prototype/replaceAll/getSubstitution-0x0024-0x0027.js - * - test/built-ins/String/prototype/replaceAll/getSubstitution-0x0024N.js - * - test/built-ins/String/prototype/replaceAll/getSubstitution-0x0024NN.js - * - test/built-ins/String/prototype/replaceAll/getSubstitution-0x0024-0x003C.js - * - test/built-ins/String/prototype/match/S15.5.4.10_A1_T14.js - * - test/built-ins/String/prototype/match/S15.5.4.10_A2_T2.js - * - test/built-ins/String/prototype/match/S15.5.4.10_A2_T3.js - * - test/built-ins/String/prototype/match/S15.5.4.10_A2_T4.js - * - test/built-ins/String/prototype/match/S15.5.4.10_A2_T5.js - * - test/built-ins/String/prototype/match/S15.5.4.10_A2_T6.js - * - test/built-ins/String/prototype/match/S15.5.4.10_A2_T7.js - * - test/built-ins/String/prototype/match/S15.5.4.10_A2_T8.js - * - test/built-ins/String/prototype/match/S15.5.4.10_A2_T12.js - * - test/built-ins/String/prototype/matchAll/regexp-prototype-matchAll-v-u-flag.js - * - test/built-ins/String/prototype/search/S15.5.4.12_A1_T14.js - * - test/built-ins/String/prototype/search/S15.5.4.12_A2_T1.js - * - test/built-ins/String/prototype/search/S15.5.4.12_A2_T2.js - * - test/built-ins/String/prototype/search/S15.5.4.12_A2_T3.js - * - test/built-ins/String/prototype/search/S15.5.4.12_A2_T4.js - * - test/built-ins/String/prototype/search/S15.5.4.12_A2_T5.js - * - test/built-ins/String/prototype/search/S15.5.4.12_A2_T6.js - * - test/built-ins/String/prototype/search/S15.5.4.12_A2_T7.js - * - test/built-ins/String/prototype/search/S15.5.4.12_A3_T1.js - * - test/built-ins/String/prototype/search/S15.5.4.12_A3_T2.js - * - * Copyright 2009 the Sputnik authors. All rights reserved. - * Copyright (C) 2019 Leo Balter. All rights reserved. - * Copyright (C) 2020 Rick Waldron. All rights reserved. - * Copyright (C) 2023 Richard Gibson. All rights reserved. - * Copyright (C) 2024 Tan Meng. All rights reserved. - * Test262 portions are governed by the BSD license in LICENSE.test262. - */ -import { describe, expect, test } from "bun:test" -import { Effect } from "effect" -import { CodeMode } from "../src/index.js" - -type Vector = { - readonly path: string - readonly code: string - readonly expected: CodeMode.DataValue -} - -const value = async (code: string) => { - const result = await Effect.runPromise(CodeMode.execute({ code, tools: {} })) - if (!result.ok) throw new Error(`expected success, got ${result.error.kind}: ${result.error.message}`) - return result.value -} - -const run = (name: string, vectors: ReadonlyArray) => { - describe(name, () => { - for (const vector of vectors) { - test(vector.path, async () => { - expect(await value(vector.code)).toEqual(vector.expected) - }) - } - }) -} - -run("Test262-adapted regexp split behavior", [ - { - path: "test/built-ins/String/prototype/split/separator-regexp.js", - code: ` - return [ - "x".split(/^/), "x".split(/$/), "x".split(/.?/), "x".split(/.*/), "x".split(/.+/), - "x".split(/.*?/), "x".split(/.{1}/), "x".split(/.{1,}/), "x".split(/.{1,2}/), - "x".split(/()/), "x".split(/./), "x".split(/(?:)/), "x".split(/(...)/), - "x".split(/(|)/), "x".split(/[]/), "x".split(/[^]/), "x".split(/[.-.]/), - "x".split(/\\0/), "x".split(/\\b/), "x".split(/\\B/), "x".split(/\\d/), - "x".split(/\\D/), "x".split(/\\n/), "x".split(/\\r/), "x".split(/\\s/), - "x".split(/\\S/), "x".split(/\\v/), "x".split(/\\w/), "x".split(/\\W/), - ] - `, - expected: [ - ["x"], ["x"], ["", ""], ["", ""], ["", ""], ["x"], ["", ""], ["", ""], ["", ""], - ["x"], ["", ""], ["x"], ["x"], ["x"], ["x"], ["", ""], ["x"], ["x"], ["x"], - ["x"], ["x"], ["", ""], ["x"], ["x"], ["x"], ["", ""], ["x"], ["", ""], ["x"], - ], - }, - { - path: "test/built-ins/String/prototype/split/arguments-are-regexp-s-and-3-and-instance-is-string-a-b-c-de-f.js", - code: `return "a b c de f".split(/\\s/, 3)`, - expected: ["a", "b", "c"], - }, - { - path: "test/built-ins/String/prototype/split/argument-is-regexp-s-and-instance-is-string-a-b-c-de-f.js", - code: `return "a b c de f".split(/\\s/)`, - expected: ["a", "b", "c", "de", "f"], - }, - { - path: "test/built-ins/String/prototype/split/argument-is-regexp-d-and-instance-is-string-dfe23iu-34-65.js", - code: `return "dfe23iu 34 =+65--".split(/\\d+/)`, - expected: ["dfe", "iu ", " =+", "--"], - }, - { - path: "test/built-ins/String/prototype/split/argument-is-regexp-reg-exp-d-and-instance-is-string-dfe23iu-34-65.js", - code: `return "dfe23iu 34 =+65--".split(new RegExp("\\\\d+"))`, - expected: ["dfe", "iu ", " =+", "--"], - }, - { - path: "test/built-ins/String/prototype/split/argument-is-regexp-a-z-and-instance-is-string-abc.js", - code: `return "abc".split(/[a-z]/)`, - expected: ["", "", "", ""], - }, - { - path: "test/built-ins/String/prototype/split/argument-is-reg-exp-a-z-and-instance-is-string-abc.js", - code: `return "abc".split(new RegExp("[a-z]"))`, - expected: ["", "", "", ""], - }, - { - path: "test/built-ins/String/prototype/split/arguments-are-regexp-l-and-undefined-and-instance-is-string-hello.js", - code: `return "hello".split(/l/, undefined)`, - expected: ["he", "", "o"], - }, - { - path: "test/built-ins/String/prototype/split/arguments-are-regexp-l-and-0-and-instance-is-string-hello.js", - code: `return "hello".split(/l/, 0)`, - expected: [], - }, - { - path: "test/built-ins/String/prototype/split/arguments-are-regexp-l-and-1-and-instance-is-string-hello.js", - code: `return "hello".split(/l/, 1)`, - expected: ["he"], - }, - { - path: "test/built-ins/String/prototype/split/arguments-are-regexp-l-and-2-and-instance-is-string-hello.js", - code: `return "hello".split(/l/, 2)`, - expected: ["he", ""], - }, - { - path: "test/built-ins/String/prototype/split/arguments-are-regexp-l-and-3-and-instance-is-string-hello.js", - code: `return "hello".split(/l/, 3)`, - expected: ["he", "", "o"], - }, - { - path: "test/built-ins/String/prototype/split/arguments-are-regexp-l-and-4-and-instance-is-string-hello.js", - code: `return "hello".split(/l/, 4)`, - expected: ["he", "", "o"], - }, - { - path: "test/built-ins/String/prototype/split/argument-is-regexp-l-and-instance-is-string-hello.js", - code: `return "hello".split(/l/)`, - expected: ["he", "", "o"], - }, - { - path: "test/built-ins/String/prototype/split/argument-is-new-reg-exp-and-instance-is-string-hello.js", - code: `return "hello".split(new RegExp())`, - expected: ["h", "e", "l", "l", "o"], - }, - { - path: "test/built-ins/String/prototype/split/arguments-are-new-reg-exp-and-0-and-instance-is-string-hello.js", - code: `return "hello".split(new RegExp(), 0)`, - expected: [], - }, - { - path: "test/built-ins/String/prototype/split/arguments-are-new-reg-exp-and-1-and-instance-is-string-hello.js", - code: `return "hello".split(new RegExp(), 1)`, - expected: ["h"], - }, - { - path: "test/built-ins/String/prototype/split/arguments-are-new-reg-exp-and-2-and-instance-is-string-hello.js", - code: `return "hello".split(new RegExp(), 2)`, - expected: ["h", "e"], - }, - { - path: "test/built-ins/String/prototype/split/arguments-are-new-reg-exp-and-3-and-instance-is-string-hello.js", - code: `return "hello".split(new RegExp(), 3)`, - expected: ["h", "e", "l"], - }, - { - path: "test/built-ins/String/prototype/split/arguments-are-new-reg-exp-and-4-and-instance-is-string-hello.js", - code: `return "hello".split(new RegExp(), 4)`, - expected: ["h", "e", "l", "l"], - }, - { - path: "test/built-ins/String/prototype/split/arguments-are-new-reg-exp-and-undefined-and-instance-is-string-hello.js", - code: `return "hello".split(new RegExp(), undefined)`, - expected: ["h", "e", "l", "l", "o"], - }, - { - path: "test/built-ins/String/prototype/split/call-split-2-instance-is-string-one-two-three-four-five.js", - code: `return "one two three four five".split(/ /, 2)`, - expected: ["one", "two"], - }, - { - path: "test/built-ins/String/prototype/split/separator-regexp-comma-instance-is-string-one-1-two-2-four-4.js", - code: `return "one-1,two-2,four-4".split(/,/)`, - expected: ["one-1", "two-2", "four-4"], - }, - { - path: "test/built-ins/String/prototype/split/argument-is-regexp-x-and-instance-is-string-a-b-c-de-f.js", - code: `return "a b c de f".split(/X/)`, - expected: ["a b c de f"], - }, -]) - -run("Test262-adapted replace behavior", [ - { - path: "test/built-ins/String/prototype/replace/regexp-capture-by-index.js", - code: ` - const str = "foo-x-bar" - const patterns = ["x", /x/, /(x)/, /(x)($^)?/, /((((((((((x))))))))))/] - const replacements = ["|$0|", "|$00|", "|$000|", "|$1|", "|$01|", "|$010|", "|$2|", "|$02|", "|$020|", "|$10|", "|$100|", "|$20|", "|$200|"] - return replacements.flatMap((replacement) => patterns.map((pattern) => str.replace(pattern, replacement))) - `, - expected: [ - "foo-|$0|-bar", "foo-|$0|-bar", "foo-|$0|-bar", "foo-|$0|-bar", "foo-|$0|-bar", - "foo-|$00|-bar", "foo-|$00|-bar", "foo-|$00|-bar", "foo-|$00|-bar", "foo-|$00|-bar", - "foo-|$000|-bar", "foo-|$000|-bar", "foo-|$000|-bar", "foo-|$000|-bar", "foo-|$000|-bar", - "foo-|$1|-bar", "foo-|$1|-bar", "foo-|x|-bar", "foo-|x|-bar", "foo-|x|-bar", - "foo-|$01|-bar", "foo-|$01|-bar", "foo-|x|-bar", "foo-|x|-bar", "foo-|x|-bar", - "foo-|$010|-bar", "foo-|$010|-bar", "foo-|x0|-bar", "foo-|x0|-bar", "foo-|x0|-bar", - "foo-|$2|-bar", "foo-|$2|-bar", "foo-|$2|-bar", "foo-||-bar", "foo-|x|-bar", - "foo-|$02|-bar", "foo-|$02|-bar", "foo-|$02|-bar", "foo-||-bar", "foo-|x|-bar", - "foo-|$020|-bar", "foo-|$020|-bar", "foo-|$020|-bar", "foo-|0|-bar", "foo-|x0|-bar", - "foo-|$10|-bar", "foo-|$10|-bar", "foo-|x0|-bar", "foo-|x0|-bar", "foo-|x|-bar", - "foo-|$100|-bar", "foo-|$100|-bar", "foo-|x00|-bar", "foo-|x00|-bar", "foo-|x0|-bar", - "foo-|$20|-bar", "foo-|$20|-bar", "foo-|$20|-bar", "foo-|0|-bar", "foo-|x0|-bar", - "foo-|$200|-bar", "foo-|$200|-bar", "foo-|$200|-bar", "foo-|00|-bar", "foo-|x00|-bar", - ], - }, - { - path: "test/built-ins/String/prototype/replace/S15.5.4.11_A1_T17.js", - code: `return "asdf".replace(new RegExp(undefined, "g"), "1")`, - expected: "1a1s1d1f1", - }, - { - path: "test/built-ins/String/prototype/replace/S15.5.4.11_A2_T1.js", - code: `return "She sells seashells by the seashore.".replace(/sh/g, "sch")`, - expected: "She sells seaschells by the seaschore.", - }, - { - path: "test/built-ins/String/prototype/replace/S15.5.4.11_A2_T2.js", - code: `return "She sells seashells by the seashore.".replace(/sh/g, "$$sch")`, - expected: "She sells sea$schells by the sea$schore.", - }, - { - path: "test/built-ins/String/prototype/replace/S15.5.4.11_A2_T3.js", - code: `return "She sells seashells by the seashore.".replace(/sh/g, "$&sch")`, - expected: "She sells seashschells by the seashschore.", - }, - { - path: "test/built-ins/String/prototype/replace/S15.5.4.11_A2_T4.js", - code: `return "She sells seashells by the seashore.".replace(/sh/g, "$\`sch")`, - expected: "She sells seaShe sells seaschells by the seaShe sells seashells by the seaschore.", - }, - { - path: "test/built-ins/String/prototype/replace/S15.5.4.11_A2_T5.js", - code: `return "She sells seashells by the seashore.".replace(/sh/g, "$'sch")`, - expected: "She sells seaells by the seashore.schells by the seaore.schore.", - }, - { - path: "test/built-ins/String/prototype/replace/S15.5.4.11_A2_T6.js", - code: `return "She sells seashells by the seashore.".replace(/sh/, "sch")`, - expected: "She sells seaschells by the seashore.", - }, - { - path: "test/built-ins/String/prototype/replace/S15.5.4.11_A2_T7.js", - code: `return "She sells seashells by the seashore.".replace(/sh/, "$$sch")`, - expected: "She sells sea$schells by the seashore.", - }, - { - path: "test/built-ins/String/prototype/replace/S15.5.4.11_A2_T8.js", - code: `return "She sells seashells by the seashore.".replace(/sh/, "$&sch")`, - expected: "She sells seashschells by the seashore.", - }, - { - path: "test/built-ins/String/prototype/replace/S15.5.4.11_A2_T9.js", - code: `return "She sells seashells by the seashore.".replace(/sh/, "$\`sch")`, - expected: "She sells seaShe sells seaschells by the seashore.", - }, - { - path: "test/built-ins/String/prototype/replace/S15.5.4.11_A2_T10.js", - code: `return "She sells seashells by the seashore.".replace(/sh/, "$'sch")`, - expected: "She sells seaells by the seashore.schells by the seashore.", - }, - { - path: "test/built-ins/String/prototype/replace/S15.5.4.11_A3_T1.js", - code: `return "uid=31".replace(/(uid=)(\\d+)/, "$1115")`, - expected: "uid=115", - }, - { - path: "test/built-ins/String/prototype/replace/S15.5.4.11_A3_T2.js", - code: `return "uid=31".replace(/(uid=)(\\d+)/, "$1115")`, - expected: "uid=115", - }, - { - path: "test/built-ins/String/prototype/replace/S15.5.4.11_A3_T3.js", - code: `return "uid=31".replace(/(uid=)(\\d+)/, "$11A15")`, - expected: "uid=1A15", - }, - { - path: "test/built-ins/String/prototype/replace/S15.5.4.11_A5_T1.js", - code: `return "aaaaaaaaaa,aaaaaaaaaaaaaaa".replace(/^(a+)\\1*,\\1+$/, "$1")`, - expected: "aaaaa", - }, -]) - -run("Test262-adapted replaceAll behavior", [ - { - path: "test/built-ins/String/prototype/replaceAll/searchValue-replacer-RegExp-call.js", - code: ` - return [ - "abc abc abc".replaceAll(new RegExp("b", "g"), "z"), - "abc abc abc".replaceAll(new RegExp("b", "gy"), "z"), - "abc abc abc".replaceAll(new RegExp("b", "giy"), "z"), - "No Uppercase!".replaceAll(new RegExp("[A-Z]", "g"), ""), - "No Uppercase?".replaceAll(new RegExp("[A-Z]", "gy"), ""), - "NO UPPERCASE!".replaceAll(new RegExp("[A-Z]", "gy"), ""), - "abcabcabcabc".replaceAll(new RegExp("a(b)(ca)", "g"), "$2-$1"), - "abcabcabcabc".replaceAll(new RegExp("(a(.))", "g"), "$1$2$3"), - "aabacadaeafagahaiajakalamano a azaya".replaceAll(new RegExp("(((((((((((((a(.).).).).).).).).))))))", "g"), "($10)-($12)-($1)"), - "abcba".replaceAll(new RegExp("b", "g"), "$'"), - "abcba".replaceAll(new RegExp("b", "g"), "$\`"), - "abcba".replaceAll(new RegExp("(?b)", "g"), "($)"), - "abcba".replaceAll(new RegExp("(?b)", "g"), "($b)", "g"), "($)"), - "abcabcabcabc".replaceAll(new RegExp("a(b)(ca)", "g"), "($$)"), - "abcabcabcabc".replaceAll(new RegExp("a(b)(ca)", "g"), "($)"), - "abcabcabcabc".replaceAll(new RegExp("a(b)(ca)", "g"), "($$$$)"), - "abcabcabcabc".replaceAll(new RegExp("a(b)(ca)", "g"), "($$$)"), - "abcabcabcabc".replaceAll(new RegExp("a(b)(ca)", "g"), "($$&)"), - "abcabcabcabc".replaceAll(new RegExp("a(b)(ca)", "g"), "($$1)"), - "abcabcabcabc".replaceAll(new RegExp("a(b)(ca)", "g"), "($$\`)"), - "abcabcabcabc".replaceAll(new RegExp("a(b)(ca)", "g"), "($$')"), - "abcabcabcabc".replaceAll(new RegExp("a(?b)(ca)", "g"), "($$)"), - "abcabcabcabc".replaceAll(new RegExp("a(b)(ca)", "g"), "($&)"), - ] - `, - expected: [ - "azc azc azc", "abc abc abc", "abc abc abc", "o ppercase!", "o Uppercase?", " UPPERCASE!", - "ca-bbcca-bbc", "abb$3cabb$3cabb$3cabb$3c", - "(aabaca)-(aaba)-(aabacadaea)f(agahai)-(agah)-(agahaiajak)(alaman)-(alam)-(alamano a )azaya", - "acbacaa", "aacabca", "a(b)c(b)a", "a($)bc($)bc", "(abca)bc(abca)bc", - ], - }, - { - path: "test/built-ins/String/prototype/replaceAll/searchValue-empty-string.js", - code: `return ["aab c \\nx".replaceAll("", "_"), "a".replaceAll("", "_")]`, - expected: ["_a_a_b_ _c_ _ _\n_x_", "_a_"], - }, - { - path: "test/built-ins/String/prototype/replaceAll/searchValue-empty-string-this-empty-string.js", - code: `return "".replaceAll("", "abc")`, - expected: "abc", - }, - { - path: "test/built-ins/String/prototype/replaceAll/replaceValue-value-replaces-string.js", - code: `return ["aaab a a aac".replaceAll("aa", "z"), "aaab a a aac".replaceAll("aa", "a"), "aaab a a aac".replaceAll("a", "a"), "aaab a a aac".replaceAll("a", "z")]`, - expected: ["zab a a zc", "aab a a ac", "aaab a a aac", "zzzb z z zzc"], - }, - { - path: "test/built-ins/String/prototype/replaceAll/getSubstitution-0x0024.js", - code: ` - const str = "Ninguém é igual a ninguém. Todo o ser humano é um estranho ímpar." - return [str.replaceAll("ninguém", "$"), str.replaceAll("é", "$"), str.replaceAll("é", "$ -"), str.replaceAll("é", "$$$")] - `, - expected: [ - "Ninguém é igual a $. Todo o ser humano é um estranho ímpar.", - "Ningu$m $ igual a ningu$m. Todo o ser humano $ um estranho ímpar.", - "Ningu$ -m $ - igual a ningu$ -m. Todo o ser humano $ - um estranho ímpar.", - "Ningu$$m $$ igual a ningu$$m. Todo o ser humano $$ um estranho ímpar.", - ], - }, - { - path: "test/built-ins/String/prototype/replaceAll/getSubstitution-0x0024-0x0024.js", - code: ` - const str = "Ninguém é igual a ninguém. Todo o ser humano é um estranho ímpar." - return [str.replaceAll("ninguém", "$$"), str.replaceAll("é", "$$"), str.replaceAll("é", "$$ -"), str.replaceAll("é", "$$&"), str.replaceAll("é", "$$$"), str.replaceAll("é", "$$$$")] - `, - expected: [ - "Ninguém é igual a $. Todo o ser humano é um estranho ímpar.", - "Ningu$m $ igual a ningu$m. Todo o ser humano $ um estranho ímpar.", - "Ningu$ -m $ - igual a ningu$ -m. Todo o ser humano $ - um estranho ímpar.", - "Ningu$&m $& igual a ningu$&m. Todo o ser humano $& um estranho ímpar.", - "Ningu$$m $$ igual a ningu$$m. Todo o ser humano $$ um estranho ímpar.", - "Ningu$$m $$ igual a ningu$$m. Todo o ser humano $$ um estranho ímpar.", - ], - }, - { - path: "test/built-ins/String/prototype/replaceAll/getSubstitution-0x0024-0x0026.js", - code: ` - const str = "Ninguém é igual a ninguém. Todo o ser humano é um estranho ímpar." - return [str.replaceAll("ninguém", "$&"), str.replaceAll("ninguém", "($&)"), str.replaceAll("é", "($&)"), str.replaceAll("é", "($&) $&")] - `, - expected: [ - "Ninguém é igual a ninguém. Todo o ser humano é um estranho ímpar.", - "Ninguém é igual a (ninguém). Todo o ser humano é um estranho ímpar.", - "Ningu(é)m (é) igual a ningu(é)m. Todo o ser humano (é) um estranho ímpar.", - "Ningu(é) ém (é) é igual a ningu(é) ém. Todo o ser humano (é) é um estranho ímpar.", - ], - }, - { - path: "test/built-ins/String/prototype/replaceAll/getSubstitution-0x0024-0x0060.js", - code: ` - const str = "Ninguém é igual a ninguém. Todo o ser humano é um estranho ímpar." - return [str.replaceAll("ninguém", "$\`"), str.replaceAll("Ninguém", "$\`"), str.replaceAll("ninguém", "($\`)"), str.replaceAll("é", "($\`)")] - `, - expected: [ - "Ninguém é igual a Ninguém é igual a . Todo o ser humano é um estranho ímpar.", - " é igual a ninguém. Todo o ser humano é um estranho ímpar.", - "Ninguém é igual a (Ninguém é igual a ). Todo o ser humano é um estranho ímpar.", - "Ningu(Ningu)m (Ninguém ) igual a ningu(Ninguém é igual a ningu)m. Todo o ser humano (Ninguém é igual a ninguém. Todo o ser humano ) um estranho ímpar.", - ], - }, - { - path: "test/built-ins/String/prototype/replaceAll/getSubstitution-0x0024-0x0027.js", - code: ` - const str = "Ninguém é igual a ninguém. Todo o ser humano é um estranho ímpar." - return [str.replaceAll("ninguém", "$'"), str.replaceAll(".", "--- $'"), str.replaceAll("é", "($')")] - `, - expected: [ - "Ninguém é igual a . Todo o ser humano é um estranho ímpar.. Todo o ser humano é um estranho ímpar.", - "Ninguém é igual a ninguém--- Todo o ser humano é um estranho ímpar. Todo o ser humano é um estranho ímpar--- ", - "Ningu(m é igual a ninguém. Todo o ser humano é um estranho ímpar.)m ( igual a ninguém. Todo o ser humano é um estranho ímpar.) igual a ningu(m. Todo o ser humano é um estranho ímpar.)m. Todo o ser humano ( um estranho ímpar.) um estranho ímpar.", - ], - }, - { - path: "test/built-ins/String/prototype/replaceAll/getSubstitution-0x0024N.js", - code: ` - const str = "ABC AAA ABC AAA" - return ["$1", "$2", "$3", "$4", "$5", "$6", "$7", "$8", "$9"].map((replacement) => str.replaceAll("ABC", replacement)) - `, - expected: ["$1 AAA $1 AAA", "$2 AAA $2 AAA", "$3 AAA $3 AAA", "$4 AAA $4 AAA", "$5 AAA $5 AAA", "$6 AAA $6 AAA", "$7 AAA $7 AAA", "$8 AAA $8 AAA", "$9 AAA $9 AAA"], - }, - { - path: "test/built-ins/String/prototype/replaceAll/getSubstitution-0x0024NN.js", - code: ` - const str = "aaaaaaaaaaaaaaaa aaaaaaaa aaaaaaaaaaaaaaaa" - return [str.replaceAll("a", "$11"), str.replaceAll("a", "$29")] - `, - expected: [ - "$11$11$11$11$11$11$11$11$11$11$11$11$11$11$11$11 $11$11$11$11$11$11$11$11 $11$11$11$11$11$11$11$11$11$11$11$11$11$11$11$11", - "$29$29$29$29$29$29$29$29$29$29$29$29$29$29$29$29 $29$29$29$29$29$29$29$29 $29$29$29$29$29$29$29$29$29$29$29$29$29$29$29$29", - ], - }, - { - path: "test/built-ins/String/prototype/replaceAll/getSubstitution-0x0024-0x003C.js", - code: `return "aaaaaaaaaaaaaaaa aaaaaaaa aaaaaaaaaaaaaaaa".replaceAll("a", "$<")`, - expected: "$<$<$<$<$<$<$<$<$<$<$<$<$<$<$<$< $<$<$<$<$<$<$<$< $<$<$<$<$<$<$<$<$<$<$<$<$<$<$<$<", - }, -]) - -run("Test262-adapted match behavior", [ - { - path: "test/built-ins/String/prototype/match/S15.5.4.10_A1_T14.js", - code: `const match = "ABBABABAB77BBAA".match(new RegExp("77")); return [match[0], match.index]`, - expected: ["77", 9], - }, - { - path: "test/built-ins/String/prototype/match/S15.5.4.10_A2_T2.js", - code: `return "343443444".match(/34/g)`, - expected: ["34", "34", "34"], - }, - { - path: "test/built-ins/String/prototype/match/S15.5.4.10_A2_T3.js", - code: `return "123456abcde7890".match(/\\d{1}/g)`, - expected: ["1", "2", "3", "4", "5", "6", "7", "8", "9", "0"], - }, - { - path: "test/built-ins/String/prototype/match/S15.5.4.10_A2_T4.js", - code: `return "123456abcde7890".match(/\\d{2}/g)`, - expected: ["12", "34", "56", "78", "90"], - }, - { - path: "test/built-ins/String/prototype/match/S15.5.4.10_A2_T5.js", - code: `return "123456abcde7890".match(/\\D{2}/g)`, - expected: ["ab", "cd"], - }, - { - path: "test/built-ins/String/prototype/match/S15.5.4.10_A2_T6.js", - code: `const match = "Boston, Mass. 02134".match(/([\\d]{5})([- ]?[\\d]{4})?$/); return [match[0], match[1], match[2] === undefined, match.length, match.index]`, - expected: ["02134", "02134", true, 3, 14], - }, - { - path: "test/built-ins/String/prototype/match/S15.5.4.10_A2_T7.js", - code: `return "Boston, Mass. 02134".match(/([\\d]{5})([- ]?[\\d]{4})?$/g)`, - expected: ["02134"], - }, - { - path: "test/built-ins/String/prototype/match/S15.5.4.10_A2_T8.js", - code: `const match = "Boston, MA 02134".match(/([\\d]{5})([- ]?[\\d]{4})?$/); return [match[0], match[1], match[2] === undefined, match.length, match.index]`, - expected: ["02134", "02134", true, 3, 11], - }, - { - path: "test/built-ins/String/prototype/match/S15.5.4.10_A2_T12.js", - code: `return "Boston, MA 02134".match(/([\\d]{5})([- ]?[\\d]{4})?$/g)`, - expected: ["02134"], - }, -]) - -run("Test262-adapted matchAll behavior", [ - { - path: "test/built-ins/String/prototype/matchAll/regexp-prototype-matchAll-v-u-flag.js", - code: ` - const text = "𠮷a𠮷b𠮷" - const collect = (regex) => { - const matches = text.matchAll(regex) - return matches.map((match) => match[0]).concat(matches.map((match) => match.index)) - } - const empty = text.matchAll(/(?:)/gu) - const complex = "a𠮷b􏿿c".matchAll(/\\P{ASCII}/gu) - return [ - collect(/𠮷/g), - collect(/𠮷/gu), - collect(/\\p{Script=Han}/gu), - collect(/./gu), - empty.map((match) => match[0]).concat(empty.map((match) => match.index)).length, - complex.map((match) => match[0]), - ] - `, - expected: [ - ["𠮷", "𠮷", "𠮷", 0, 3, 6], - ["𠮷", "𠮷", "𠮷", 0, 3, 6], - ["𠮷", "𠮷", "𠮷", 0, 3, 6], - ["𠮷", "a", "𠮷", "b", "𠮷", 0, 2, 3, 5, 6], - 12, - ["𠮷", "􏿿"], - ], - }, -]) - -run("Test262-adapted search behavior", [ - { - path: "test/built-ins/String/prototype/search/S15.5.4.12_A1_T14.js", - code: `return "ABBABABAB77BBAA".search(new RegExp("77"))`, - expected: 9, - }, - { - path: "test/built-ins/String/prototype/search/S15.5.4.12_A2_T1.js", - code: `return "test string".search("string")`, - expected: 5, - }, - { - path: "test/built-ins/String/prototype/search/S15.5.4.12_A2_T2.js", - code: `return "test string".search("String")`, - expected: -1, - }, - { - path: "test/built-ins/String/prototype/search/S15.5.4.12_A2_T3.js", - code: `return "test string".search(/String/i)`, - expected: 5, - }, - { - path: "test/built-ins/String/prototype/search/S15.5.4.12_A2_T4.js", - code: `return "one two three four five".search(/Four/)`, - expected: -1, - }, - { - path: "test/built-ins/String/prototype/search/S15.5.4.12_A2_T5.js", - code: `return "one two three four five".search(/four/)`, - expected: 14, - }, - { - path: "test/built-ins/String/prototype/search/S15.5.4.12_A2_T6.js", - code: `return "test string".search("notexist")`, - expected: -1, - }, - { - path: "test/built-ins/String/prototype/search/S15.5.4.12_A2_T7.js", - code: `return "test string probe".search("string pro")`, - expected: 5, - }, - { - path: "test/built-ins/String/prototype/search/S15.5.4.12_A3_T1.js", - code: `const text = "power of the power of the power of the great sword"; return [text.search(/the/), text.search(/the/g)]`, - expected: [9, 9], - }, - { - path: "test/built-ins/String/prototype/search/S15.5.4.12_A3_T2.js", - code: `const text = "power of the power of the power of the great sword"; return [text.search(/of/), text.search(/of/g)]`, - expected: [6, 6], - }, -]) diff --git a/packages/codemode/test/string-search-test262.test.ts b/packages/codemode/test/string-search-test262.test.ts deleted file mode 100644 index 5dcd63c8ba..0000000000 --- a/packages/codemode/test/string-search-test262.test.ts +++ /dev/null @@ -1,727 +0,0 @@ -/* - * Portions adapted from Test262 at revision 250f204f23a9249ff204be2baec29600faae7b75: - * - test/built-ins/String/prototype/split/call-split-l-0-instance-is-string-hello.js - * - test/built-ins/String/prototype/split/call-split-l-1-instance-is-string-hello.js - * - test/built-ins/String/prototype/split/call-split-l-2-instance-is-string-hello.js - * - test/built-ins/String/prototype/split/call-split-l-3-instance-is-string-hello.js - * - test/built-ins/String/prototype/split/call-split-l-4-instance-is-string-hello.js - * - test/built-ins/String/prototype/split/call-split-l-na-n-instance-is-string-hello.js - * - test/built-ins/String/prototype/split/call-split-l-instance-is-string-hello.js - * - test/built-ins/String/prototype/split/call-split-ll-instance-is-string-hello.js - * - test/built-ins/String/prototype/split/call-split-h-instance-is-string-hello.js - * - test/built-ins/String/prototype/split/call-split-hello-instance-is-string-hello.js - * - test/built-ins/String/prototype/split/call-split-hellothere-instance-is-string-hello.js - * - test/built-ins/String/prototype/split/call-split-o-instance-is-string-hello.js - * - test/built-ins/String/prototype/split/call-split-x-instance-is-string-hello.js - * - test/built-ins/String/prototype/split/call-split-x-instance-is-empty-string.js - * - test/built-ins/String/prototype/split/call-split-4-instance-is-string-one-1-two-2-four-4.js - * - test/built-ins/String/prototype/split/call-split-on-instance-is-string-one-1-two-2-four-4.js - * - test/built-ins/String/prototype/split/call-split-instance-is-string-one-two-three-four-five.js - * - test/built-ins/String/prototype/split/call-split-instance-is-string-one-two-three.js - * - test/built-ins/String/prototype/split/call-split-instance-is-string.js - * - test/built-ins/String/prototype/split/instance-is-string-one-two-three-four-five.js - * - test/built-ins/String/prototype/split/instance-is-string.js - * - test/built-ins/String/prototype/split/separator-colon-instance-is-string-one-1-two-2-four-4.js - * - test/built-ins/String/prototype/split/separator-comma-instance-is-string-one-two-three-four-five.js - * - test/built-ins/String/prototype/split/separator-empty-string-instance-is-string.js - * - test/built-ins/String/prototype/split/call-split-without-arguments-and-instance-is-empty-string.js - * - test/built-ins/String/prototype/split/separator-undef.js - * - test/built-ins/String/prototype/slice/S15.5.4.13_A1_T6.js - * - test/built-ins/String/prototype/slice/S15.5.4.13_A1_T14.js - * - test/built-ins/String/prototype/slice/S15.5.4.13_A2_T1.js - * - test/built-ins/String/prototype/slice/S15.5.4.13_A2_T2.js - * - test/built-ins/String/prototype/slice/S15.5.4.13_A2_T3.js - * - test/built-ins/String/prototype/slice/S15.5.4.13_A2_T4.js - * - test/built-ins/String/prototype/slice/S15.5.4.13_A2_T5.js - * - test/built-ins/String/prototype/slice/S15.5.4.13_A2_T6.js - * - test/built-ins/String/prototype/slice/S15.5.4.13_A2_T7.js - * - test/built-ins/String/prototype/slice/S15.5.4.13_A2_T8.js - * - test/built-ins/String/prototype/slice/S15.5.4.13_A2_T9.js - * - test/built-ins/String/prototype/substring/S15.5.4.15_A1_T6.js - * - test/built-ins/String/prototype/substring/S15.5.4.15_A1_T14.js - * - test/built-ins/String/prototype/substring/S15.5.4.15_A2_T1.js - * - test/built-ins/String/prototype/substring/S15.5.4.15_A2_T2.js - * - test/built-ins/String/prototype/substring/S15.5.4.15_A2_T3.js - * - test/built-ins/String/prototype/substring/S15.5.4.15_A2_T4.js - * - test/built-ins/String/prototype/substring/S15.5.4.15_A2_T5.js - * - test/built-ins/String/prototype/substring/S15.5.4.15_A2_T6.js - * - test/built-ins/String/prototype/substring/S15.5.4.15_A2_T7.js - * - test/built-ins/String/prototype/substring/S15.5.4.15_A2_T8.js - * - test/built-ins/String/prototype/substring/S15.5.4.15_A2_T9.js - * - test/built-ins/String/prototype/substring/S15.5.4.15_A2_T10.js - * - test/built-ins/String/prototype/includes/String.prototype.includes_FailMissingLetter.js - * - test/built-ins/String/prototype/includes/String.prototype.includes_SuccessNoLocation.js - * - test/built-ins/String/prototype/includes/String.prototype.includes_FailBadLocation.js - * - test/built-ins/String/prototype/includes/String.prototype.includes_FailLocation.js - * - test/built-ins/String/prototype/includes/String.prototype.includes_Success.js - * - test/built-ins/String/prototype/includes/searchstring-found-with-position.js - * - test/built-ins/String/prototype/includes/searchstring-found-without-position.js - * - test/built-ins/String/prototype/includes/searchstring-not-found-with-position.js - * - test/built-ins/String/prototype/includes/searchstring-not-found-without-position.js - * - test/built-ins/String/prototype/includes/return-false-with-out-of-bounds-position.js - * - test/built-ins/String/prototype/includes/return-true-if-searchstring-is-empty.js - * - test/built-ins/String/prototype/includes/coerced-values-of-position.js - * - test/built-ins/String/prototype/startsWith/searchstring-found-with-position.js - * - test/built-ins/String/prototype/startsWith/searchstring-found-without-position.js - * - test/built-ins/String/prototype/startsWith/searchstring-not-found-with-position.js - * - test/built-ins/String/prototype/startsWith/searchstring-not-found-without-position.js - * - test/built-ins/String/prototype/startsWith/out-of-bounds-position.js - * - test/built-ins/String/prototype/startsWith/return-true-if-searchstring-is-empty.js - * - test/built-ins/String/prototype/startsWith/coerced-values-of-position.js - * - test/built-ins/String/prototype/endsWith/String.prototype.endsWith_Success.js - * - test/built-ins/String/prototype/endsWith/String.prototype.endsWith_Success_2.js - * - test/built-ins/String/prototype/endsWith/String.prototype.endsWith_Success_3.js - * - test/built-ins/String/prototype/endsWith/String.prototype.endsWith_Success_4.js - * - test/built-ins/String/prototype/endsWith/String.prototype.endsWith_Fail.js - * - test/built-ins/String/prototype/endsWith/String.prototype.endsWith_Fail_2.js - * - test/built-ins/String/prototype/endsWith/searchstring-found-with-position.js - * - test/built-ins/String/prototype/endsWith/searchstring-found-without-position.js - * - test/built-ins/String/prototype/endsWith/searchstring-not-found-with-position.js - * - test/built-ins/String/prototype/endsWith/searchstring-not-found-without-position.js - * - test/built-ins/String/prototype/endsWith/return-false-if-search-start-is-less-than-zero.js - * - test/built-ins/String/prototype/endsWith/return-true-if-searchstring-is-empty.js - * - test/built-ins/String/prototype/endsWith/coerced-values-of-position.js - * - test/built-ins/String/prototype/indexOf/S15.5.4.7_A2_T1.js - * - test/built-ins/String/prototype/indexOf/S15.5.4.7_A2_T2.js - * - test/built-ins/String/prototype/indexOf/S15.5.4.7_A2_T3.js - * - test/built-ins/String/prototype/indexOf/S15.5.4.7_A2_T4.js - * - test/built-ins/String/prototype/indexOf/S15.5.4.7_A3_T1.js - * - test/built-ins/String/prototype/indexOf/S15.5.4.7_A3_T3.js - * - test/built-ins/String/prototype/indexOf/position-tointeger.js - * - test/built-ins/String/prototype/indexOf/searchstring-tostring.js - * - test/built-ins/String/prototype/lastIndexOf/not-a-substring.js - * - * Copyright 2009 the Sputnik authors. All rights reserved. - * Copyright (c) 2014 Ryan Lewis. All rights reserved. - * Copyright (C) 2015 the V8 project authors. All rights reserved. - * Copyright (C) 2016 the V8 project authors. All rights reserved. - * Copyright (C) 2017 Josh Wolfe. All rights reserved. - * Copyright (C) 2020 Leo Balter. All rights reserved. - * Copyright (C) 2026 Garham Lee. All rights reserved. - * Test262 portions are governed by the BSD license in LICENSE.test262. - */ -import { describe, expect, test } from "bun:test" -import { Effect } from "effect" -import { CodeMode } from "../src/index.js" - -const value = async (code: string) => { - const result = await Effect.runPromise(CodeMode.execute({ code, tools: {} })) - if (!result.ok) throw new Error(`expected success, got ${result.error.kind}: ${result.error.message}`) - return result.value -} - -const cases = [ - { - path: "test/built-ins/String/prototype/split/call-split-l-0-instance-is-string-hello.js", - code: `const result = "hello".split("l", 0); return [result.length, result[0] === undefined]`, - expected: [0, true], - labels: [ - "The value of __split.length is expected to equal the value of __expected.length", - "The value of __split[0] is expected to equal the value of __expected[0]", - ], - }, - { - path: "test/built-ins/String/prototype/split/call-split-l-1-instance-is-string-hello.js", - code: `const result = "hello".split("l", 1); return [result.length, result[0]]`, - expected: [1, "he"], - labels: [ - "The value of __split.length is expected to equal the value of __expected.length", - "The value of __split[0] is expected to equal the value of __expected[0]", - ], - }, - { - path: "test/built-ins/String/prototype/split/call-split-l-2-instance-is-string-hello.js", - code: `const result = "hello".split("l", 2); return [result.length, result[0], result[1]]`, - expected: [2, "he", ""], - labels: [ - "The value of __split.length is expected to equal the value of __expected.length", - "The value of __split[index] is expected to equal the value of __expected[index]", - "The value of __split[index] is expected to equal the value of __expected[index]", - ], - }, - { - path: "test/built-ins/String/prototype/split/call-split-l-3-instance-is-string-hello.js", - code: `const result = "hello".split("l", 3); return [result.length, result[0], result[1], result[2]]`, - expected: [3, "he", "", "o"], - labels: [ - "The value of __split.length is expected to equal the value of __expected.length", - "The value of __split[index] is expected to equal the value of __expected[index]", - "The value of __split[index] is expected to equal the value of __expected[index]", - "The value of __split[index] is expected to equal the value of __expected[index]", - ], - }, - { - path: "test/built-ins/String/prototype/split/call-split-l-4-instance-is-string-hello.js", - code: `const result = "hello".split("l", 4); return [result.length, result[0], result[1], result[2]]`, - expected: [3, "he", "", "o"], - labels: [ - "The value of __split.length is expected to equal the value of __expected.length", - "The value of __split[index] is expected to equal the value of __expected[index]", - "The value of __split[index] is expected to equal the value of __expected[index]", - "The value of __split[index] is expected to equal the value of __expected[index]", - ], - }, - { - path: "test/built-ins/String/prototype/split/call-split-l-na-n-instance-is-string-hello.js", - code: `const result = "hello".split("l", NaN); return [result.length, result[0] === undefined]`, - expected: [0, true], - labels: [ - "The value of __split.length is expected to equal the value of __expected.length", - "The value of __split[0] is expected to equal the value of __expected[0]", - ], - }, - { - path: "test/built-ins/String/prototype/split/call-split-l-instance-is-string-hello.js", - code: `const result = "hello".split("l"); return [result.length, result[0], result[1], result[2]]`, - expected: [3, "he", "", "o"], - labels: [ - "The value of __split.length is 3", - 'The value of __split[0] is "he"', - 'The value of __split[1] is ""', - 'The value of __split[2] is "o"', - ], - }, - { - path: "test/built-ins/String/prototype/split/call-split-ll-instance-is-string-hello.js", - code: `const result = "hello".split("ll"); return [result.length, result[0], result[1]]`, - expected: [2, "he", "o"], - labels: ["The value of __split.length is 2", 'The value of __split[0] is "he"', 'The value of __split[1] is "o"'], - }, - { - path: "test/built-ins/String/prototype/split/call-split-h-instance-is-string-hello.js", - code: `const result = "hello".split("h"); return [result.length, result[0], result[1]]`, - expected: [2, "", "ello"], - labels: ["The value of __split.length is 2", 'The value of __split[0] is ""', 'The value of __split[1] is "ello"'], - }, - { - path: "test/built-ins/String/prototype/split/call-split-hello-instance-is-string-hello.js", - code: `const result = "hello".split("hello"); return [result.length, result[0], result[1]]`, - expected: [2, "", ""], - labels: ["The value of __split.length is 2", 'The value of __split[0] is ""', 'The value of __split[1] is ""'], - }, - { - path: "test/built-ins/String/prototype/split/call-split-hellothere-instance-is-string-hello.js", - code: `const result = "hello".split("hellothere"); return [result.length, result[0]]`, - expected: [1, "hello"], - labels: ["The value of __split.length is 1", 'The value of __split[0] is "hello"'], - }, - { - path: "test/built-ins/String/prototype/split/call-split-o-instance-is-string-hello.js", - code: `const result = "hello".split("o"); return [result.length, result[0], result[1]]`, - expected: [2, "hell", ""], - labels: ["The value of __split.length is 2", 'The value of __split[0] is "hell"', 'The value of __split[1] is ""'], - }, - { - path: "test/built-ins/String/prototype/split/call-split-x-instance-is-string-hello.js", - code: `const result = "hello".split("x"); return [result.length, result[0]]`, - expected: [1, "hello"], - labels: ["The value of __split.length is 1", 'The value of __split[0] is "hello"'], - }, - { - path: "test/built-ins/String/prototype/split/call-split-x-instance-is-empty-string.js", - code: `const result = "".split("x"); return [result.length, result[0]]`, - expected: [1, ""], - labels: ["The value of __split.length is 1", 'The value of __split[0] is ""'], - }, - { - path: "test/built-ins/String/prototype/split/call-split-4-instance-is-string-one-1-two-2-four-4.js", - code: `const result = "one-1 two-2 four-4".split("-4"); return [result.length, result[0], result[1]]`, - expected: [2, "one-1 two-2 four", ""], - labels: [ - "The value of __split.length is 2", - 'The value of __split[0] is "one-1 two-2 four"', - 'The value of __split[1] is ""', - ], - }, - { - path: "test/built-ins/String/prototype/split/call-split-on-instance-is-string-one-1-two-2-four-4.js", - code: `const result = "one-1 two-2 four-4".split("on"); return [result.length, result[0], result[1]]`, - expected: [2, "", "e-1 two-2 four-4"], - labels: [ - "The value of __split.length is 2", - 'The value of __split[0] is ""', - 'The value of __split[1] is "e-1 two-2 four-4"', - ], - }, - { - path: "test/built-ins/String/prototype/split/call-split-instance-is-string-one-two-three-four-five.js", - code: `const result = "one two three four five".split(" "); return [result.length, ...result]`, - expected: [5, "one", "two", "three", "four", "five"], - labels: [ - "The value of __split.length is 5", 'The value of __split[0] is "one"', 'The value of __split[1] is "two"', - 'The value of __split[2] is "three"', 'The value of __split[3] is "four"', 'The value of __split[4] is "five"', - ], - }, - { - path: "test/built-ins/String/prototype/split/call-split-instance-is-string-one-two-three.js", - code: `const result = "one two three".split(""); return [result[0], result[1], result[11], result[12]]`, - expected: ["o", "n", "e", "e"], - labels: [ - 'The value of __split[0] is "o"', 'The value of __split[1] is "n"', - 'The value of __split[11] is "e"', 'The value of __split[12] is "e"', - ], - }, - { - path: "test/built-ins/String/prototype/split/call-split-instance-is-string.js", - code: `const result = " ".split(" "); return [result.length, result[0], result[1]]`, - expected: [2, "", ""], - labels: ["The value of __split.length is 2", 'The value of __split[0] is ""', 'The value of __split[1] is ""'], - }, - { - path: "test/built-ins/String/prototype/split/instance-is-string-one-two-three-four-five.js", - code: `const result = "one,two,three,four,five".split(); return [result.length, result[0]]`, - expected: [1, "one,two,three,four,five"], - labels: ["The value of __split.length is 1", 'The value of __split[0] is "one,two,three,four,five"'], - }, - { - path: "test/built-ins/String/prototype/split/instance-is-string.js", - code: `const result = " ".split(); return [result.length, result[0]]`, - expected: [1, " "], - labels: ["The value of __split.length is 1", 'The value of __split[0] is " "'], - }, - { - path: "test/built-ins/String/prototype/split/separator-colon-instance-is-string-one-1-two-2-four-4.js", - code: `const result = "one-1,two-2,four-4".split(":"); return [result.length, result[0]]`, - expected: [1, "one-1,two-2,four-4"], - labels: ["The value of __split.length is 1", 'The value of __split[0] is "one-1,two-2,four-4"'], - }, - { - path: "test/built-ins/String/prototype/split/separator-comma-instance-is-string-one-two-three-four-five.js", - code: `const result = "one,two,three,four,five".split(","); return [result.length, ...result]`, - expected: [5, "one", "two", "three", "four", "five"], - labels: [ - "The value of __split.length is 5", - 'The value of __split[0] is "one"', - 'The value of __split[1] is "two"', - 'The value of __split[2] is "three"', - 'The value of __split[3] is "four"', - 'The value of __split[4] is "five"', - ], - }, - { - path: "test/built-ins/String/prototype/split/separator-empty-string-instance-is-string.js", - code: `const result = " ".split(""); return [result.length, result[0]]`, - expected: [1, " "], - labels: ["The value of __split.length is 1", 'The value of __split[0] is " "'], - }, - { - path: "test/built-ins/String/prototype/split/call-split-without-arguments-and-instance-is-empty-string.js", - code: `const result = "".split(); return [result.length, result[0]]`, - expected: [1, ""], - labels: ["The value of __split.length is 1", 'The value of __split[0] is ""'], - }, - { - path: "test/built-ins/String/prototype/split/separator-undef.js", - code: `const result = "undefined is not a function".split(); return [Array.isArray(result), result.length, result[0]]`, - expected: [true, 1, "undefined is not a function"], - labels: ["implicit separator, result is array", "implicit separator, result.length", "implicit separator, [0] is the same string"], - }, - { - path: "test/built-ins/String/prototype/slice/S15.5.4.13_A1_T6.js", - code: `return ["undefined".slice(undefined, 3)]`, - expected: ["und"], - labels: ['#1: new String("undefined").slice(x,3) === "und"'], - }, - { - path: "test/built-ins/String/prototype/slice/S15.5.4.13_A1_T14.js", - code: `return ["report".slice(undefined)]`, - expected: ["report"], - labels: ['#1: "report".slice(function(){}()) === "report"'], - }, - { - path: "test/built-ins/String/prototype/slice/S15.5.4.13_A2_T1.js", - code: `return [typeof "this is a string object".slice()]`, - expected: ["string"], - labels: ['#1: typeof __string.slice() === "string"'], - }, - { - path: "test/built-ins/String/prototype/slice/S15.5.4.13_A2_T2.js", - code: `return ["this is a string object".slice(NaN, Infinity)]`, - expected: ["this is a string object"], - labels: ['#1: __string.slice(NaN, Infinity) === "this is a string object"'], - }, - { - path: "test/built-ins/String/prototype/slice/S15.5.4.13_A2_T3.js", - code: `return ["".slice(1, 0)]`, - expected: [""], - labels: ['#1: __string.slice(1,0) === ""'], - }, - { - path: "test/built-ins/String/prototype/slice/S15.5.4.13_A2_T4.js", - code: `return ["this is a string object".slice(Infinity, NaN)]`, - expected: [""], - labels: ['#1: __string.slice(Infinity, NaN) === ""'], - }, - { - path: "test/built-ins/String/prototype/slice/S15.5.4.13_A2_T5.js", - code: `return ["this is a string object".slice(Infinity, Infinity)]`, - expected: [""], - labels: ['#1: __string.slice(Infinity, Infinity) === ""'], - }, - { - path: "test/built-ins/String/prototype/slice/S15.5.4.13_A2_T6.js", - code: `return ["this is a string object".slice(-0.01, 0)]`, - expected: [""], - labels: ['#1: __string.slice(-0.01,0) === ""'], - }, - { - path: "test/built-ins/String/prototype/slice/S15.5.4.13_A2_T7.js", - code: `const text = "this is a string object"; return [text.slice(text.length, text.length)]`, - expected: [""], - labels: ['#1: __string.slice(__string.length, __string.length) === ""'], - }, - { - path: "test/built-ins/String/prototype/slice/S15.5.4.13_A2_T8.js", - code: `const text = "this is a string object"; return [text.slice(text.length + 1, 0)]`, - expected: [""], - labels: ['#1: __string.slice(__string.length+1, 0) === ""'], - }, - { - path: "test/built-ins/String/prototype/slice/S15.5.4.13_A2_T9.js", - code: `return ["this is a string object".slice(-Infinity, -Infinity)]`, - expected: [""], - labels: ['#1: __string.slice(-Infinity, -Infinity) === ""'], - }, - { - path: "test/built-ins/String/prototype/substring/S15.5.4.15_A1_T6.js", - code: `return ["undefined".substring(undefined, 3)]`, - expected: ["und"], - labels: ['#1: new String("undefined").substring(x,3) === "und"'], - }, - { - path: "test/built-ins/String/prototype/substring/S15.5.4.15_A1_T14.js", - code: `return ["report".substring(undefined)]`, - expected: ["report"], - labels: ['#1: "report".substring(function(){}()) === "report"'], - }, - { - path: "test/built-ins/String/prototype/substring/S15.5.4.15_A2_T1.js", - code: `return [typeof "this is a string object".substring()]`, - expected: ["string"], - labels: ['#1: typeof __string.substring() === "string"'], - }, - { - path: "test/built-ins/String/prototype/substring/S15.5.4.15_A2_T2.js", - code: `return ["this is a string object".substring(NaN, Infinity)]`, - expected: ["this is a string object"], - labels: ['#1: __string.substring(NaN, Infinity) === "this is a string object"'], - }, - { - path: "test/built-ins/String/prototype/substring/S15.5.4.15_A2_T3.js", - code: `return ["".substring(1, 0)]`, - expected: [""], - labels: ['#1: __string.substring(1,0) === ""'], - }, - { - path: "test/built-ins/String/prototype/substring/S15.5.4.15_A2_T4.js", - code: `return ["this is a string object".substring(Infinity, NaN)]`, - expected: ["this is a string object"], - labels: ['#1: __string.substring(Infinity, NaN) === "this is a string object"'], - }, - { - path: "test/built-ins/String/prototype/substring/S15.5.4.15_A2_T5.js", - code: `return ["this is a string object".substring(Infinity, Infinity)]`, - expected: [""], - labels: ['#1: __string.substring(Infinity, Infinity) === ""'], - }, - { - path: "test/built-ins/String/prototype/substring/S15.5.4.15_A2_T6.js", - code: `return ["this is a string object".substring(-0.01, 0)]`, - expected: [""], - labels: ['#1: __string.substring(-0.01,0) === ""'], - }, - { - path: "test/built-ins/String/prototype/substring/S15.5.4.15_A2_T7.js", - code: `const text = "this is a string object"; return [text.substring(text.length, text.length)]`, - expected: [""], - labels: ['#1: __string.substring(__string.length, __string.length) === ""'], - }, - { - path: "test/built-ins/String/prototype/substring/S15.5.4.15_A2_T8.js", - code: `const text = "this is a string object"; return [text.substring(text.length + 1, 0)]`, - expected: ["this is a string object"], - labels: ['#1: __string.substring(__string.length+1, 0) === "this is a string object"'], - }, - { - path: "test/built-ins/String/prototype/substring/S15.5.4.15_A2_T9.js", - code: `return ["this is a string object".substring(-Infinity, -Infinity)]`, - expected: [""], - labels: ['#1: __string.substring(-Infinity, -Infinity) === ""'], - }, - { - path: "test/built-ins/String/prototype/substring/S15.5.4.15_A2_T10.js", - code: `return ["this_is_a_string object".substring(0, 8)]`, - expected: ["this_is_"], - labels: ['#1: __string.substring(0,8) === "this_is_"'], - }, - { - path: "test/built-ins/String/prototype/includes/String.prototype.includes_FailMissingLetter.js", - code: `return ["word".includes("a", 0)]`, expected: [false], labels: ['"word".includes("a", 0)'], - }, - { - path: "test/built-ins/String/prototype/includes/String.prototype.includes_SuccessNoLocation.js", - code: `return ["word".includes("w")]`, expected: [true], labels: ['"word".includes("w")'], - }, - { - path: "test/built-ins/String/prototype/includes/String.prototype.includes_FailBadLocation.js", - code: `return ["word".includes("w", 5)]`, expected: [false], labels: ['"word".includes("w", 5)'], - }, - { - path: "test/built-ins/String/prototype/includes/String.prototype.includes_FailLocation.js", - code: `return ["word".includes("o", 3)]`, expected: [false], labels: ['"word".includes("o", 3)'], - }, - { - path: "test/built-ins/String/prototype/includes/String.prototype.includes_Success.js", - code: `return ["word".includes("w", 0)]`, expected: [true], labels: ['"word".includes("w", 0)'], - }, - { - path: "test/built-ins/String/prototype/includes/searchstring-found-with-position.js", - code: `const text = "The future is cool!"; return [text.includes("The future", 0), text.includes(" is ", 1), text.includes("cool!", 10)]`, - expected: [true, true, true], - labels: [ - 'Returns true for str.includes("The future", 0)', - 'Returns true for str.includes(" is ", 1)', - 'Returns true for str.includes("cool!", 10)', - ], - }, - { - path: "test/built-ins/String/prototype/includes/searchstring-found-without-position.js", - code: `const text = "The future is cool!"; return [text.includes("The future"), text.includes("is cool!"), text.includes(text)]`, - expected: [true, true, true], - labels: [ - 'Returns true for str.includes("The future")', - 'Returns true for str.includes("is cool!")', - "Returns true for str.includes(str)", - ], - }, - { - path: "test/built-ins/String/prototype/includes/searchstring-not-found-with-position.js", - code: `const text = "The future is cool!"; return [text.includes("The future", 1), text.includes(text, 1)]`, - expected: [false, false], - labels: ['Returns false on str.includes("The future", 1)', "Returns false on str.includes(str, 1)"], - }, - { - path: "test/built-ins/String/prototype/includes/searchstring-not-found-without-position.js", - code: `const text = "The future is cool!"; return [text.includes("Flash"), text.includes("FUTURE")]`, - expected: [false, false], labels: ["Flash if not included", "includes is case sensitive"], - }, - { - path: "test/built-ins/String/prototype/includes/return-false-with-out-of-bounds-position.js", - code: `const text = "The future is cool!"; return [ - text.includes("!", text.length + 1), text.includes("!", 100), text.includes("!", Infinity), text.includes("!", text.length), - ]`, - expected: [false, false, false, false], - labels: [ - 'str.includes("!", str.length + 1) returns false', 'str.includes("!", 100) returns false', - 'str.includes("!", Infinity) returns false', 'str.includes("!", str.length) returns false', - ], - }, - { - path: "test/built-ins/String/prototype/includes/return-true-if-searchstring-is-empty.js", - code: `const text = "The future is cool!"; return [text.includes("", text.length), text.includes(""), text.includes("", Infinity)]`, - expected: [true, true, true], - labels: ['str.includes("", str.length) returns true', 'str.includes("") returns true', 'str.includes("", Infinity) returns true'], - }, - { - path: "test/built-ins/String/prototype/includes/coerced-values-of-position.js", - code: `const text = "The future is cool!"; return [ - text.includes("The future", NaN), text.includes("The future", undefined), text.includes("The future", 0.4), - text.includes("The future", -1), text.includes("The future", 1.4), - ]`, - expected: [true, true, true, true, false], - labels: ["NaN coerced to 0", "undefined coerced to 0", "0.4 coerced to 0", "negative position", "1.4 coerced to 1"], - }, - { - path: "test/built-ins/String/prototype/startsWith/searchstring-found-with-position.js", - code: `const text = "The future is cool!"; return [text.startsWith("The future", 0), text.startsWith("future", 4), text.startsWith(" is cool!", 10)]`, - expected: [true, true, true], - labels: [ - 'str.startsWith("The future", 0) === true', 'str.startsWith("future", 4) === true', - 'str.startsWith(" is cool!", 10) === true', - ], - }, - { - path: "test/built-ins/String/prototype/startsWith/searchstring-found-without-position.js", - code: `const text = "The future is cool!"; return [text.startsWith("The "), text.startsWith("The future"), text.startsWith(text)]`, - expected: [true, true, true], - labels: ['str.startsWith("The ") === true', 'str.startsWith("The future") === true', "str.startsWith(str) === true"], - }, - { - path: "test/built-ins/String/prototype/startsWith/searchstring-not-found-with-position.js", - code: `const text = "The future is cool!"; return [text.startsWith("The future", 1), text.startsWith(text, 1)]`, - expected: [false, false], - labels: ['str.startsWith("The future", 1) === false', "str.startsWith(str, 1) === false"], - }, - { - path: "test/built-ins/String/prototype/startsWith/searchstring-not-found-without-position.js", - code: `const text = "The future is cool!"; return [text.startsWith("Flash"), text.startsWith("THE FUTURE"), text.startsWith("future is cool!")]`, - expected: [false, false, false], - labels: ['str.startsWith("Flash") === false', "startsWith is case sensitive", 'str.startsWith("future is cool!") === false'], - }, - { - path: "test/built-ins/String/prototype/startsWith/out-of-bounds-position.js", - code: `const text = "The future is cool!"; return [ - text.startsWith("!", text.length), text.startsWith("!", 100), text.startsWith("!", Infinity), - text.startsWith("The future", -1), text.startsWith("The future", -Infinity), - ]`, - expected: [false, false, false, true, true], - labels: [ - 'str.startsWith("!", str.length) returns false', 'str.startsWith("!", 100) returns false', - 'str.startsWith("!", Infinity) returns false', "position argument < 0 will search from the start of the string (-1)", - "position argument < 0 will search from the start of the string (-Infinity)", - ], - }, - { - path: "test/built-ins/String/prototype/startsWith/return-true-if-searchstring-is-empty.js", - code: `const text = "The future is cool!"; return [text.startsWith(""), text.startsWith("", text.length), text.startsWith("", Infinity)]`, - expected: [true, true, true], - labels: ['str.startsWith("") returns true', 'str.startsWith("", str.length) returns true', 'str.startsWith("", Infinity) returns true'], - }, - { - path: "test/built-ins/String/prototype/startsWith/coerced-values-of-position.js", - code: `const text = "The future is cool!"; return [ - text.startsWith("The future", NaN), text.startsWith("The future", undefined), - text.startsWith("The future", 0.4), text.startsWith("The future", 1.4), - ]`, - expected: [true, true, true, false], - labels: ["NaN coerced to 0", "undefined coerced to 0", "0.4 coerced to 0", "1.4 coerced to 1"], - }, - { - path: "test/built-ins/String/prototype/endsWith/String.prototype.endsWith_Success.js", - code: `return ["word".endsWith("d")]`, expected: [true], labels: ['"word".endsWith("d")'], - }, - { - path: "test/built-ins/String/prototype/endsWith/String.prototype.endsWith_Success_2.js", - code: `return ["word".endsWith("d", 4)]`, expected: [true], labels: ['"word".endsWith("d", 4)'], - }, - { - path: "test/built-ins/String/prototype/endsWith/String.prototype.endsWith_Success_3.js", - code: `return ["word".endsWith("d", 25)]`, expected: [true], labels: ['"word".endsWith("d", 25)'], - }, - { - path: "test/built-ins/String/prototype/endsWith/String.prototype.endsWith_Success_4.js", - code: `return ["word".endsWith("r", 3)]`, expected: [true], labels: ['"word".endsWith("r", 3)'], - }, - { - path: "test/built-ins/String/prototype/endsWith/String.prototype.endsWith_Fail.js", - code: `return ["word".endsWith("r")]`, expected: [false], labels: ['"word".endsWith("r")'], - }, - { - path: "test/built-ins/String/prototype/endsWith/String.prototype.endsWith_Fail_2.js", - code: `return ["word".endsWith("d", 3)]`, expected: [false], labels: ['"word".endsWith("d", 3)'], - }, - { - path: "test/built-ins/String/prototype/endsWith/searchstring-found-with-position.js", - code: `const text = "The future is cool!"; return [text.endsWith("The future", 10), text.endsWith("future", 10), text.endsWith(" is cool!", text.length)]`, - expected: [true, true, true], - labels: [ - 'str.endsWith("The future", 10) === true', 'str.endsWith("future", 10) === true', - 'str.endsWith(" is cool!", str.length) === true', - ], - }, - { - path: "test/built-ins/String/prototype/endsWith/searchstring-found-without-position.js", - code: `const text = "The future is cool!"; return [text.endsWith("cool!"), text.endsWith("!"), text.endsWith(text)]`, - expected: [true, true, true], - labels: ['str.endsWith("cool!") === true', 'str.endsWith("!") === true', "str.endsWith(str) === true"], - }, - { - path: "test/built-ins/String/prototype/endsWith/searchstring-not-found-with-position.js", - code: `const text = "The future is cool!"; return [text.endsWith("is cool!", text.length - 1), text.endsWith("!", 1)]`, - expected: [false, false], - labels: ['str.endsWith("is cool!", str.length - 1) === false', 'str.endsWith("!", 1) === false'], - }, - { - path: "test/built-ins/String/prototype/endsWith/searchstring-not-found-without-position.js", - code: `const text = "The future is cool!"; return [text.endsWith("is Flash!"), text.endsWith("IS COOL!"), text.endsWith("The future")]`, - expected: [false, false, false], - labels: ['str.endsWith("is Flash!") === false', "endsWith is case sensitive", 'str.endsWith("The future") === false'], - }, - { - path: "test/built-ins/String/prototype/endsWith/return-false-if-search-start-is-less-than-zero.js", - code: `return ["web".endsWith("w", 0), "Bob".endsWith(" Bob")]`, - expected: [false, false], - labels: ['"web".endsWith("w", 0) returns false', '"Bob".endsWith(" Bob") returns false'], - }, - { - path: "test/built-ins/String/prototype/endsWith/return-true-if-searchstring-is-empty.js", - code: `const text = "The future is cool!"; return [ - text.endsWith(""), text.endsWith("", text.length), text.endsWith("", Infinity), - text.endsWith("", -1), text.endsWith("", -Infinity), - ]`, - expected: [true, true, true, true, true], - labels: [ - 'str.endsWith("") returns true', 'str.endsWith("", str.length) returns true', 'str.endsWith("", Infinity) returns true', - 'str.endsWith("", -1) returns true', 'str.endsWith("", -Infinity) returns true', - ], - }, - { - path: "test/built-ins/String/prototype/endsWith/coerced-values-of-position.js", - code: `const text = "The future is cool!"; return [ - text.endsWith("", NaN), text.endsWith("", undefined), text.endsWith("The future", 10.4), - ]`, - expected: [true, true, true], - labels: ["NaN coerced to 0", "undefined coerced to 0", "10.4 coerced to 10"], - }, - { - path: "test/built-ins/String/prototype/indexOf/S15.5.4.7_A2_T1.js", - code: `return ["abcd".indexOf("abcdab")]`, expected: [-1], labels: ['#1: "abcd".indexOf("abcdab")===-1'], - }, - { - path: "test/built-ins/String/prototype/indexOf/S15.5.4.7_A2_T2.js", - code: `return ["abcd".indexOf("abcdab", 0)]`, expected: [-1], labels: ['#1: "abcd".indexOf("abcdab",0)===-1'], - }, - { - path: "test/built-ins/String/prototype/indexOf/S15.5.4.7_A2_T3.js", - code: `return ["abcd".indexOf("abcdab", 99)]`, expected: [-1], labels: ['#1: "abcd".indexOf("abcdab",99)===-1'], - }, - { - path: "test/built-ins/String/prototype/indexOf/S15.5.4.7_A2_T4.js", - code: `return ["abcd".indexOf("abcdab", NaN)]`, expected: [-1], labels: ['#1: "abcd".indexOf("abcdab",NaN)===-1'], - }, - { - path: "test/built-ins/String/prototype/indexOf/S15.5.4.7_A3_T1.js", - code: `return ["$$abcdabcd".indexOf("ab", NaN)]`, expected: [2], labels: ['#1: "$$abcdabcd".indexOf("ab",NaN)===2'], - }, - { - path: "test/built-ins/String/prototype/indexOf/S15.5.4.7_A3_T3.js", - code: `return ["$$abcdabcd".indexOf("ab", -Infinity)]`, expected: [2], labels: ['#1: "$$abcdabcd".indexOf("ab", function(){return -Infinity;}())===2'], - }, - { - path: "test/built-ins/String/prototype/indexOf/position-tointeger.js", - code: `return [ - "aaaa".indexOf("aa", 0), "aaaa".indexOf("aa", 1), "aaaa".indexOf("aa", -0.9), - "aaaa".indexOf("aa", 0.9), "aaaa".indexOf("aa", 1.9), "aaaa".indexOf("aa", NaN), - "aaaa".indexOf("aa", Infinity), "aaaa".indexOf("aa", undefined), - "aaaa".indexOf("aa", 2), "aaaa".indexOf("aa", 2.9), - ]`, - expected: [0, 1, 0, 0, 1, 0, -1, 0, 2, 2], - labels: [ - "position 0", "position 1", "ToInteger: truncate towards 0 (-0.9)", "ToInteger: truncate towards 0 (0.9)", - "ToInteger: truncate towards 0 (1.9)", "ToInteger: NaN => 0", "position Infinity", - "ToInteger: undefined => NaN => 0", "position 2", "ToInteger: truncate towards 0 (2.9)", - ], - }, - { - path: "test/built-ins/String/prototype/indexOf/searchstring-tostring.js", - code: `return ["foo".indexOf(""), "__foo__".indexOf("foo")]`, - expected: [0, 2], labels: ['"foo".indexOf("")', '"__foo__".indexOf("foo")'], - }, - { - path: "test/built-ins/String/prototype/lastIndexOf/not-a-substring.js", - code: `return ["abc".lastIndexOf("d")]`, - expected: [-1], - labels: ["String.prototype.lastIndexOf returns -1 when searchString is shorter than this and searchString is not a substring of this."], - }, -] as const - -describe("Test262-adapted String search and extraction behavior", () => { - for (const item of cases) { - test(item.path, async () => { - const actual = await value(item.code) - if (!Array.isArray(actual)) throw new Error(`expected assertion values for ${item.path}`) - expect(actual.length, "adapted assertion count").toBe(item.expected.length) - item.expected.forEach((expected, index) => expect(actual[index], item.labels[index]!).toEqual(expected)) - }) - } -}) diff --git a/packages/codemode/tsconfig.json b/packages/codemode/tsconfig.json index 0cbc049d87..fe5c4d217b 100644 --- a/packages/codemode/tsconfig.json +++ b/packages/codemode/tsconfig.json @@ -2,7 +2,6 @@ "$schema": "https://json.schemastore.org/tsconfig", "extends": "@tsconfig/bun/tsconfig.json", "compilerOptions": { - "noUncheckedIndexedAccess": false, - "noUnusedLocals": true + "noUncheckedIndexedAccess": false } } diff --git a/packages/console/app/package.json b/packages/console/app/package.json index 35d31f9342..5023021661 100644 --- a/packages/console/app/package.json +++ b/packages/console/app/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-app", - "version": "1.17.18", + "version": "1.18.11", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/console/app/src/asset/lander/desktop-tabs-landscape.mp4 b/packages/console/app/src/asset/lander/desktop-tabs-landscape.mp4 new file mode 100644 index 0000000000..c6e710c024 Binary files /dev/null and b/packages/console/app/src/asset/lander/desktop-tabs-landscape.mp4 differ diff --git a/packages/console/app/src/component/desktop-promo.css b/packages/console/app/src/component/desktop-promo.css new file mode 100644 index 0000000000..5e9a4863c0 --- /dev/null +++ b/packages/console/app/src/component/desktop-promo.css @@ -0,0 +1,103 @@ +[data-component="desktop-promo"] { + --promo-background: hsl(0, 20%, 99%); + --promo-background-weak: hsl(0, 8%, 97%); + --promo-text: hsl(0, 1%, 39%); + --promo-text-strong: hsl(0, 5%, 12%); + --promo-border: hsla(0, 100%, 3%, 0.12); + + position: fixed; + z-index: 20; + right: 1.5rem; + bottom: 1.5rem; + width: min(28rem, calc(100vw - 2rem)); + padding: 4px; + overflow: hidden; + color: var(--promo-text); + border: 1px solid var(--promo-border); + border-radius: 8px; + background: var(--promo-background); + box-shadow: 0 0.75rem 2rem rgb(0 0 0 / 15%); + font-family: var(--font-mono); + + @media (prefers-color-scheme: dark) { + --promo-background: hsl(0, 9%, 7%); + --promo-background-weak: hsl(0, 6%, 10%); + --promo-text: hsl(0, 4%, 71%); + --promo-text-strong: hsl(0, 15%, 94%); + --promo-border: hsl(0, 4%, 23%); + } + + @media (max-width: 40rem) { + right: 1rem; + bottom: 1rem; + } + + [data-slot="desktop-promo-link"] { + display: block; + color: var(--promo-text); + text-decoration: none; + } + + [data-slot="desktop-promo-link"]:focus-visible { + outline: 2px solid var(--promo-text-strong); + outline-offset: -3px; + } + + video { + display: block; + width: 100%; + aspect-ratio: 16 / 9; + object-fit: cover; + border-radius: 4px; + background: var(--promo-background-weak); + } + + [data-slot="desktop-promo-copy"] { + display: flex; + flex-direction: column; + gap: 0.35rem; + padding: 1rem; + font-size: 0.875rem; + line-height: 1.4; + } + + [data-slot="desktop-promo-copy"] strong { + color: var(--promo-text-strong); + font-weight: 500; + } + + [data-slot="desktop-promo-close"] { + position: absolute; + top: 0.5rem; + right: 0.5rem; + display: grid; + width: 2rem; + height: 2rem; + padding: 0; + place-items: center; + cursor: pointer; + color: white; + border: none; + border-radius: 0.25rem; + background: rgb(0 0 0 / 70%); + opacity: 0; + transition: + opacity 150ms ease, + background 150ms ease; + } + + &:hover [data-slot="desktop-promo-close"], + [data-slot="desktop-promo-close"]:focus-visible { + opacity: 1; + } + + [data-slot="desktop-promo-close"]:hover { + background: rgb(0 0 0 / 90%); + } + + @media (hover: none) { + [data-slot="desktop-promo-close"] { + opacity: 1; + } + } +} diff --git a/packages/console/app/src/component/desktop-promo.tsx b/packages/console/app/src/component/desktop-promo.tsx new file mode 100644 index 0000000000..504088e3c7 --- /dev/null +++ b/packages/console/app/src/component/desktop-promo.tsx @@ -0,0 +1,60 @@ +import "./desktop-promo.css" +import { A, useLocation } from "@solidjs/router" +import { createSignal, Show } from "solid-js" +import { getRequestEvent } from "solid-js/web" +import desktopPromoVideo from "~/asset/lander/desktop-tabs-landscape.mp4" +import { useI18n } from "~/context/i18n" +import { useLanguage } from "~/context/language" +import { strip } from "~/lib/language" + +const DISMISSED_COOKIE = "desktop_promo_dismissed" + +export function DesktopPromo() { + const i18n = useI18n() + const language = useLanguage() + const location = useLocation() + const request = getRequestEvent()?.request + const cookie = request?.headers.get("cookie") ?? (typeof document === "object" ? document.cookie : "") + const [visible, setVisible] = createSignal( + !cookie.split(";").some((value) => value.trim() === `${DISMISSED_COOKIE}=1`), + ) + const hostname = request ? new URL(request.url).hostname : typeof window === "object" ? window.location.hostname : "" + const primaryHost = + hostname === "opencode.ai" || hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1" + + return ( + + + + ) +} diff --git a/packages/console/app/src/i18n/ar.ts b/packages/console/app/src/i18n/ar.ts index ddb66b4135..dd03a553f5 100644 --- a/packages/console/app/src/i18n/ar.ts +++ b/packages/console/app/src/i18n/ar.ts @@ -102,10 +102,14 @@ export const dict = { "temp.logoDarkAlt": "شعار opencode الداكن", "home.banner.badge": "جديد", - "home.banner.text": "تطبيق سطح المكتب متاح بنسخة تجريبية", - "home.banner.platforms": "على macOS، Windows، وLinux", + "home.banner.text": "نقدّم علامات التبويب لتطبيق سطح المكتب.", + "home.banner.platforms": "متاحة على macOS وWindows وLinux", "home.banner.downloadNow": "حمّل الآن", "home.banner.downloadBetaNow": "حمّل النسخة التجريبية لتطبيق سطح المكتب الآن", + "home.promo.title": "نقدّم علامات التبويب لتطبيق سطح المكتب", + "home.promo.body": "نظّم عملك وجلساتك النشطة باستخدام علامات التبويب.", + "home.promo.cta": "نزّل أحدث إصدار للبدء.", + "home.promo.close": "إغلاق إعلان تطبيق سطح المكتب", "home.hero.title": "وكيل برمجة بالذكاء الاصطناعي مفتوح المصدر", "home.hero.subtitle.a": "نماذج مجانية مضمّنة أو اربط أي نموذج من أي مزوّد،", @@ -248,8 +252,9 @@ export const dict = { "zen.privacy.exceptionsLink": "الاستثناءات التالية", "go.title": "OpenCode Go | نماذج برمجة منخفضة التكلفة للجميع", + "go.banner.text": "يحصل GPT 5.6 Luna على حدود استخدام مضاعفة لفترة محدودة", "go.meta.description": - "يبدأ Go من $5 للشهر الأول، ثم $10/شهر، مع حدود طلب سخية لمدة 5 ساعات لـ GLM-5.2 وGLM-5.1 وKimi K2.7 Code وKimi K2.6 وMiMo-V2.5-Pro وMiMo-V2.5 وQwen3.7 Max وQwen3.7 Plus وQwen3.6 Plus وMiniMax M2.7 وMiniMax M3 وDeepSeek V4 Pro وDeepSeek V4 Flash.", + "يبدأ Go بسعر $5 للشهر الأول، ثم $10/شهر، مع حدود استخدام سخية ووصول موثوق إلى نماذج البرمجة الرائدة.", "go.hero.title": "نماذج برمجة منخفضة التكلفة للجميع", "go.hero.body": "يجلب Go البرمجة الوكيلة للمبرمجين حول العالم. يوفر حدودًا سخية ووصولًا موثوقًا إلى أقوى النماذج مفتوحة المصدر، حتى تتمكن من البناء باستخدام وكلاء أقوياء دون القلق بشأن التكلفة أو التوفر.", @@ -266,7 +271,6 @@ export const dict = { "go.graph.go": "Go", "go.graph.label": "الطلبات كل 5 ساعات", "go.graph.usageLimits": "حدود الاستخدام", - "go.graph.tick": "{{n}}x", "go.graph.aria": "الطلبات كل 5 ساعات: {{free}} مقابل {{go}}", "go.testimonials.brand.zen": "Zen", @@ -297,8 +301,7 @@ export const dict = { "go.problem.item1": "أسعار اشتراك منخفضة التكلفة", "go.problem.item2": "حدود سخية ووصول موثوق", "go.problem.item3": "مصمم لأكبر عدد ممكن من المبرمجين", - "go.problem.item4": - "يتضمن GLM-5.2 وGLM-5.1 وKimi K2.7 Code وKimi K2.6 وMiMo-V2.5-Pro وMiMo-V2.5 وQwen3.7 Max وQwen3.7 Plus وQwen3.6 Plus وMiniMax M2.7 وMiniMax M3 وDeepSeek V4 Pro وDeepSeek V4 Flash", + "go.problem.item4": "مجموعة منسقة من النماذج المختبرة للبرمجة الوكيلة", "go.how.title": "كيف يعمل Go", "go.how.body": "يبدأ Go من $5 للشهر الأول، ثم $10/شهر. يمكنك استخدامه مع OpenCode أو أي وكيل.", "go.how.step1.title": "أنشئ حسابًا", @@ -322,7 +325,7 @@ export const dict = { "go.faq.a2": "يتضمن Go النماذج المدرجة أدناه، مع حدود سخية وإتاحة موثوقة.", "go.faq.q3": "هل Go هو نفسه Zen؟", "go.faq.a3": - "لا. Zen هو الدفع حسب الاستخدام، بينما يبدأ Go من $5 للشهر الأول، ثم $10/شهر، مع حدود سخية ووصول موثوق إلى نماذج المصدر المفتوح GLM-5.2 وGLM-5.1 وKimi K2.7 Code وKimi K2.6 وMiMo-V2.5-Pro وMiMo-V2.5 وQwen3.7 Max وQwen3.7 Plus وQwen3.6 Plus وMiniMax M2.7 وMiniMax M3 وDeepSeek V4 Pro وDeepSeek V4 Flash.", + "لا. يعتمد Zen على الدفع حسب الاستخدام، بينما يبدأ Go بسعر $5 للشهر الأول، ثم $10/شهر، مع حدود سخية ووصول موثوق إلى مجموعة منسقة من النماذج.", "go.faq.q4": "كم تكلفة Go؟", "go.faq.a4.p1.beforePricing": "تكلفة Go", "go.faq.a4.p1.pricingLink": "$5 للشهر الأول", @@ -331,8 +334,20 @@ export const dict = { "go.faq.a4.p2.accountLink": "حسابك", "go.faq.a4.p3": "ألغِ في أي وقت.", "go.faq.q5": "ماذا عن البيانات والخصوصية؟", - "go.faq.a5.body": - "تم تصميم الخطة بشكل أساسي للمستخدمين الدوليين، مع استضافة النماذج في الولايات المتحدة والاتحاد الأوروبي وسنغافورة للحصول على وصول عالمي مستقر. يتبع مزودونا سياسة عدم الاحتفاظ بالبيانات ولا يستخدمون بياناتك لتدريب النماذج.", + "go.faq.a5.model": "النموذج", + "go.faq.a5.training": "تدريب النموذج", + "go.faq.a5.retention": "الاحتفاظ بالبيانات", + "go.faq.a5.retention30": "30 يومًا", + "go.faq.a5.retention0": "0 أيام", + "go.faq.a5.used": "مستخدَمة", + "go.faq.a5.notUsed": "غير مستخدَمة", + "go.faq.a5.noAgreement": "لا توجد اتفاقية", + "go.faq.a5.grokRetention": + "تعطّل ZDR ميزات API مهمة تعتمد على البيانات المخزنة، بما في ذلك Responses API ذات الحالة، وFiles and Collections، وBatch API.", + "go.faq.a5.gptRetention": + "تُنشأ سجلات مراقبة إساءة الاستخدام لكل استخدام لميزات API، ويُحتفظ بها لمدة تصل إلى 30 يومًا.", + "go.faq.a5.learnMore": "اعرف المزيد", + "go.faq.a5.deepseekRetention": "تُجدَّد اتفاقية ZDR شهريًا. الاتفاقية الحالية سارية حتى 31 أغسطس 2026.", "go.faq.a5.beforeExceptions": "تتم استضافة نماذج Go في الولايات المتحدة. يتبع المزودون سياسة عدم الاحتفاظ بالبيانات ولا يستخدمون بياناتك لتدريب النماذج، مع", "go.faq.a5.exceptionsLink": "الاستثناءات التالية", @@ -345,7 +360,7 @@ export const dict = { "go.faq.q9": "ما الفرق بين النماذج المجانية وGo؟", "go.faq.a9": - "تشمل النماذج المجانية Big Pickle بالإضافة إلى النماذج الترويجية المتاحة في ذلك الوقت، مع حصة 200 طلب/يوم. يتضمن Go نماذج GLM-5.2 وGLM-5.1 وKimi K2.7 Code وKimi K2.6 وMiMo-V2.5-Pro وMiMo-V2.5 وQwen3.7 Max وQwen3.7 Plus وQwen3.6 Plus وMiniMax M2.7 وMiniMax M3 وDeepSeek V4 Pro وDeepSeek V4 Flash مع حصص طلبات أعلى مطبقة عبر نوافذ متجددة (5 ساعات، أسبوعيًا، وشهريًا)، تعادل تقريبًا 12 دولارًا كل 5 ساعات، و30 دولارًا في الأسبوع، و60 دولارًا في الشهر (تختلف أعداد الطلبات الفعلية حسب النموذج والاستخدام).", + "تشمل النماذج المجانية Big Pickle بالإضافة إلى النماذج الترويجية المتاحة في ذلك الوقت، مع حصة قدرها 200 طلب/يوم. يقدّم Go مجموعة منسقة من النماذج مع حصص طلبات أعلى مطبقة عبر نوافذ متجددة (5 ساعات، وأسبوعية، وشهرية)، تعادل تقريبًا $12 لكل 5 ساعات، و$30 في الأسبوع، و$60 في الشهر (تختلف أعداد الطلبات الفعلية حسب النموذج والاستخدام).", "zen.api.error.rateLimitExceeded": "تم تجاوز حد الطلبات. يرجى المحاولة مرة أخرى لاحقًا.", "zen.api.error.modelNotSupported": "النموذج {{model}} غير مدعوم", @@ -354,6 +369,7 @@ export const dict = { "zen.api.error.providerNotSupported": "المزود {{provider}} غير مدعوم", "zen.api.error.missingApiKey": "مفتاح API مفقود.", "zen.api.error.invalidApiKey": "مفتاح API غير صالح.", + "zen.api.error.requestBlockedByUpstreamProvider": "حظر المزود الخارجي الطلب.", "zen.api.error.subscriptionQuotaExceeded": "تم تجاوز حصة الاشتراك. أعد المحاولة خلال {{retryIn}}.", "zen.api.error.goSubscriptionRollingLimitExceeded": "تم الوصول إلى حد الاستخدام لمدة 5 ساعات. تتم إعادة التعيين خلال {{retryIn}}. لمواصلة استخدام هذا النموذج الآن، فعّل الاستخدام من رصيدك المتاح: {{consoleGoUrl}}", @@ -369,7 +385,7 @@ export const dict = { "لقد وصلت إلى حد الإنفاق الشهري البالغ ${{amount}}. إدارة حدودك هنا: {{membersUrl}}", "zen.api.error.modelDisabled": "النموذج معطل", "zen.api.error.regionNotAllowed": - "هذا النموذج مستضاف في الصين. إذا كنت ترغب في استخدام هذا النموذج، فعّله في إعداداتك: {{consoleGoUrl}}", + "لا يتوفر أحدث إصدار من هذا النموذج إلا مستضافًا في الصين، ويتطلب تفعيلًا صريحًا: {{consoleGoUrl}}", "zen.api.error.trialEnded": "انتهى العرض المجاني لـ {{model}}. يمكنك مواصلة استخدام النموذج بالاشتراك في OpenCode Go - {{link}}", @@ -659,7 +675,7 @@ export const dict = { "workspace.lite.promo.price": "$5 للشهر الأول", "workspace.lite.promo.modelsTitle": "ما يتضمنه", "workspace.lite.promo.footer": - "تم تصميم الخطة بشكل أساسي للمستخدمين الدوليين، مع استضافة النماذج في الولايات المتحدة والاتحاد الأوروبي وسنغافورة للحصول على وصول عالمي مستقر. قد تتغير الأسعار وحدود الاستخدام بناءً على تعلمنا من الاستخدام المبكر والملاحظات.", + "صُممت الخطة بشكل أساسي للمستخدمين الدوليين، وتوفر وصولًا عالميًا مستقرًا. قد تتغير الأسعار وحدود الاستخدام بينما نتعلم من الاستخدام المبكر والملاحظات.", "workspace.lite.promo.subscribe": "الاشتراك في Go", "workspace.lite.promo.subscribing": "جارٍ إعادة التوجيه...", "workspace.lite.promo.otherMethods": "طرق دفع أخرى", @@ -700,11 +716,11 @@ export const dict = { "download.title": "OpenCode | تنزيل", "download.meta.description": "نزّل OpenCode لـ macOS، Windows، وLinux", - "download.hero.title": "تنزيل OpenCode", + "download.hero.title": "تنزيل OpenCode لسطح المكتب", "download.hero.subtitle": "متاح في نسخة تجريبية لـ macOS، Windows، وLinux", "download.hero.button": "تنزيل لـ {{os}}", "download.section.terminal": "OpenCode للطرفية", - "download.section.desktop": "OpenCode لسطح المكتب (Beta)", + "download.section.desktop": "OpenCode لسطح المكتب", "download.section.extensions": "امتدادات OpenCode", "download.section.integrations": "تكاملات OpenCode", "download.action.download": "تنزيل", diff --git a/packages/console/app/src/i18n/br.ts b/packages/console/app/src/i18n/br.ts index 3c00dc8643..dac06753e9 100644 --- a/packages/console/app/src/i18n/br.ts +++ b/packages/console/app/src/i18n/br.ts @@ -102,10 +102,14 @@ export const dict = { "temp.logoDarkAlt": "logo opencode escuro", "home.banner.badge": "Novo", - "home.banner.text": "App desktop disponível em beta", - "home.banner.platforms": "no macOS, Windows e Linux", + "home.banner.text": "Conheça as abas no app desktop.", + "home.banner.platforms": "Disponível no macOS, Windows e Linux", "home.banner.downloadNow": "Baixar agora", "home.banner.downloadBetaNow": "Baixe agora o beta do desktop", + "home.promo.title": "Conheça as abas no app desktop", + "home.promo.body": "Organize seu trabalho e suas sessões ativas com abas.", + "home.promo.cta": "Baixe a versão mais recente para começar.", + "home.promo.close": "Fechar anúncio do app desktop", "home.hero.title": "O agente de codificação de código aberto com IA", "home.hero.subtitle.a": "Modelos grátis incluídos ou conecte qualquer modelo de qualquer provedor,", @@ -252,8 +256,9 @@ export const dict = { "zen.privacy.exceptionsLink": "seguintes exceções", "go.title": "OpenCode Go | Modelos de codificação de baixo custo para todos", + "go.banner.text": "GPT 5.6 Luna tem limites de uso 2x maiores por tempo limitado", "go.meta.description": - "O Go começa em $5 no primeiro mês, depois $10/mês, com limites generosos de solicitação de 5 horas para GLM-5.2, GLM-5.1, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro e DeepSeek V4 Flash.", + "O Go começa em $5 no primeiro mês, depois $10/mês, com limites generosos de uso e acesso confiável aos principais modelos de codificação.", "go.hero.title": "Modelos de codificação de baixo custo para todos", "go.hero.body": "O Go traz a codificação com agentes para programadores em todo o mundo. Oferecendo limites generosos e acesso confiável aos modelos de código aberto mais capazes, para que você possa construir com agentes poderosos sem se preocupar com custos ou disponibilidade.", @@ -270,7 +275,6 @@ export const dict = { "go.graph.go": "Go", "go.graph.label": "Requisições por 5 horas", "go.graph.usageLimits": "Limites de uso", - "go.graph.tick": "{{n}}x", "go.graph.aria": "Requisições por 5h: {{free}} vs {{go}}", "go.testimonials.brand.zen": "Zen", @@ -302,8 +306,7 @@ export const dict = { "go.problem.item1": "Preço de assinatura de baixo custo", "go.problem.item2": "Limites generosos e acesso confiável", "go.problem.item3": "Feito para o maior número possível de programadores", - "go.problem.item4": - "Inclui GLM-5.2, GLM-5.1, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro e DeepSeek V4 Flash", + "go.problem.item4": "Uma seleção de modelos testados para codificação com agentes", "go.how.title": "Como o Go funciona", "go.how.body": "O Go começa em $5 no primeiro mês, depois $10/mês. Você pode usá-lo com o OpenCode ou qualquer agente.", @@ -329,7 +332,7 @@ export const dict = { "go.faq.a2": "O Go inclui os modelos listados abaixo, com limites generosos e acesso confiável.", "go.faq.q3": "O Go é o mesmo que o Zen?", "go.faq.a3": - "Não. Zen é pay-as-you-go, enquanto o Go começa em $5 no primeiro mês, depois $10/mês, com limites generosos e acesso confiável aos modelos open source GLM-5.2, GLM-5.1, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro e DeepSeek V4 Flash.", + "Não. Zen é pay-as-you-go, enquanto o Go começa em $5 no primeiro mês, depois $10/mês, com limites generosos e acesso confiável a uma seleção de modelos.", "go.faq.q4": "Quanto custa o Go?", "go.faq.a4.p1.beforePricing": "O Go custa", "go.faq.a4.p1.pricingLink": "$5 no primeiro mês", @@ -338,8 +341,21 @@ export const dict = { "go.faq.a4.p2.accountLink": "conta", "go.faq.a4.p3": "Cancele a qualquer momento.", "go.faq.q5": "E sobre dados e privacidade?", - "go.faq.a5.body": - "O plano é projetado principalmente para usuários internacionais, com modelos hospedados nos EUA, UE e Singapura para acesso global estável. Nossos provedores seguem uma política de retenção zero e não usam seus dados para treinamento de modelos.", + "go.faq.a5.model": "Modelo", + "go.faq.a5.training": "Treinamento de modelos", + "go.faq.a5.retention": "Retenção de dados", + "go.faq.a5.retention30": "30 dias", + "go.faq.a5.retention0": "0 dias", + "go.faq.a5.used": "Usado", + "go.faq.a5.notUsed": "Não usado", + "go.faq.a5.noAgreement": "Sem acordo", + "go.faq.a5.grokRetention": + "O ZDR desativa recursos importantes da API que dependem de dados armazenados, incluindo a Responses API com estado, Files and Collections e a Batch API.", + "go.faq.a5.gptRetention": + "Logs de monitoramento de abuso são gerados para todo uso de recursos da API e retidos por até 30 dias.", + "go.faq.a5.learnMore": "Saiba mais", + "go.faq.a5.deepseekRetention": + "O acordo de ZDR é renovado mensalmente. O acordo atual é válido até 31 de agosto de 2026.", "go.faq.a5.beforeExceptions": "Os modelos Go são hospedados nos EUA. Os provedores seguem uma política de retenção zero e não usam seus dados para treinamento de modelos, com as", "go.faq.a5.exceptionsLink": "seguintes exceções", @@ -353,7 +369,7 @@ export const dict = { "go.faq.q9": "Qual a diferença entre os modelos gratuitos e o Go?", "go.faq.a9": - "Os modelos gratuitos incluem Big Pickle e modelos promocionais disponíveis no momento, com uma cota de 200 requisições/dia. O Go inclui GLM-5.2, GLM-5.1, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro e DeepSeek V4 Flash com cotas de requisição mais altas aplicadas em janelas móveis (5 horas, semanal e mensal), aproximadamente equivalentes a $12 por 5 horas, $30 por semana e $60 por mês (as contagens reais de requisições variam de acordo com o modelo e o uso).", + "Os modelos gratuitos incluem Big Pickle e modelos promocionais disponíveis no momento, com uma cota de 200 requisições/dia. O Go oferece uma seleção de modelos com cotas de requisição mais altas aplicadas em janelas móveis (5 horas, semanal e mensal), aproximadamente equivalentes a $12 por 5 horas, $30 por semana e $60 por mês (as contagens reais de requisições variam de acordo com o modelo e o uso).", "zen.api.error.rateLimitExceeded": "Limite de taxa excedido. Por favor, tente novamente mais tarde.", "zen.api.error.modelNotSupported": "Modelo {{model}} não suportado", @@ -362,6 +378,7 @@ export const dict = { "zen.api.error.providerNotSupported": "Provedor {{provider}} não suportado", "zen.api.error.missingApiKey": "Chave de API ausente.", "zen.api.error.invalidApiKey": "Chave de API inválida.", + "zen.api.error.requestBlockedByUpstreamProvider": "Solicitação bloqueada pelo provedor upstream.", "zen.api.error.subscriptionQuotaExceeded": "Cota de assinatura excedida. Tente novamente em {{retryIn}}.", "zen.api.error.goSubscriptionRollingLimitExceeded": "Limite de uso de 5 horas atingido. Será reiniciado em {{retryIn}}. Para continuar usando este modelo agora, habilite o uso a partir do seu saldo disponível: {{consoleGoUrl}}", @@ -377,7 +394,7 @@ export const dict = { "Você atingiu seu limite de gastos mensais de ${{amount}}. Gerencie seus limites aqui: {{membersUrl}}", "zen.api.error.modelDisabled": "O modelo está desabilitado", "zen.api.error.regionNotAllowed": - "Este modelo está hospedado na China. Se você quiser usar este modelo, ative-o nas suas configurações: {{consoleGoUrl}}", + "A versão mais recente deste modelo está disponível apenas com hospedagem na China e requer adesão explícita: {{consoleGoUrl}}", "zen.api.error.trialEnded": "A promoção gratuita do {{model}} terminou. Você pode continuar usando o modelo assinando o OpenCode Go - {{link}}", @@ -669,7 +686,7 @@ export const dict = { "workspace.lite.promo.price": "$5 no primeiro mês", "workspace.lite.promo.modelsTitle": "O que está incluído", "workspace.lite.promo.footer": - "O plano é projetado principalmente para usuários internacionais, com modelos hospedados nos EUA, UE e Singapura para acesso global estável. Preços e limites de uso podem mudar conforme aprendemos com o uso inicial e feedback.", + "O plano foi desenvolvido principalmente para usuários internacionais e oferece acesso global estável. Os preços e limites de uso podem mudar à medida que aprendemos com o uso inicial e o feedback recebido.", "workspace.lite.promo.subscribe": "Assinar Go", "workspace.lite.promo.subscribing": "Redirecionando...", "workspace.lite.promo.otherMethods": "Outros métodos de pagamento", @@ -711,11 +728,11 @@ export const dict = { "download.title": "OpenCode | Baixar", "download.meta.description": "Baixe o OpenCode para macOS, Windows e Linux", - "download.hero.title": "Baixar OpenCode", + "download.hero.title": "Baixar OpenCode Desktop", "download.hero.subtitle": "Disponível em Beta para macOS, Windows e Linux", "download.hero.button": "Baixar para {{os}}", "download.section.terminal": "OpenCode Terminal", - "download.section.desktop": "OpenCode Desktop (Beta)", + "download.section.desktop": "OpenCode Desktop", "download.section.extensions": "Extensões OpenCode", "download.section.integrations": "Integrações OpenCode", "download.action.download": "Baixar", diff --git a/packages/console/app/src/i18n/da.ts b/packages/console/app/src/i18n/da.ts index b87aa04cd9..4e3d230f90 100644 --- a/packages/console/app/src/i18n/da.ts +++ b/packages/console/app/src/i18n/da.ts @@ -102,10 +102,14 @@ export const dict = { "temp.logoDarkAlt": "opencode logo dark", "home.banner.badge": "Ny", - "home.banner.text": "Desktop-app tilgængelig i beta", - "home.banner.platforms": "på macOS, Windows og Linux", + "home.banner.text": "Introduktion af Tabs til Desktop.", + "home.banner.platforms": "Tilgængelig på macOS, Windows og Linux", "home.banner.downloadNow": "Download nu", "home.banner.downloadBetaNow": "Download desktop-betaen nu", + "home.promo.title": "Introduktion af Tabs til Desktop", + "home.promo.body": "Organiser dit arbejde og aktive sessioner med faner.", + "home.promo.cta": "Download den nyeste version for at komme i gang.", + "home.promo.close": "Luk meddelelsen om Desktop-appen", "home.hero.title": "Den open source AI-kodningsagent", "home.hero.subtitle.a": "Gratis modeller inkluderet, eller forbind enhver model fra enhver udbyder,", @@ -250,8 +254,9 @@ export const dict = { "zen.privacy.exceptionsLink": "følgende undtagelser", "go.title": "OpenCode Go | Kodningsmodeller til lav pris for alle", + "go.banner.text": "GPT 5.6 Luna får fordoblet brugsgrænse i en begrænset periode", "go.meta.description": - "Go starter ved $5 for den første måned, derefter $10/måned, med generøse 5-timers anmodningsgrænser for GLM-5.2, GLM-5.1, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro og DeepSeek V4 Flash.", + "Go starter ved $5 for den første måned, derefter $10/måned, med generøse brugsgrænser og pålidelig adgang til førende kodningsmodeller.", "go.hero.title": "Kodningsmodeller til lav pris for alle", "go.hero.body": "Go bringer agentisk kodning til programmører over hele verden. Med generøse grænser og pålidelig adgang til de mest kapable open source-modeller, så du kan bygge med kraftfulde agenter uden at bekymre dig om omkostninger eller tilgængelighed.", @@ -268,7 +273,6 @@ export const dict = { "go.graph.go": "Go", "go.graph.label": "Forespørgsler pr. 5 timer", "go.graph.usageLimits": "Brugsgrænser", - "go.graph.tick": "{{n}}x", "go.graph.aria": "Forespørgsler pr. 5t: {{free}} vs {{go}}", "go.testimonials.brand.zen": "Zen", @@ -299,8 +303,7 @@ export const dict = { "go.problem.item1": "Lavpris abonnementspriser", "go.problem.item2": "Generøse grænser og pålidelig adgang", "go.problem.item3": "Bygget til så mange programmører som muligt", - "go.problem.item4": - "Inkluderer GLM-5.2, GLM-5.1, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro og DeepSeek V4 Flash", + "go.problem.item4": "Et kurateret modeludvalg testet til agentisk kodning", "go.how.title": "Hvordan Go virker", "go.how.body": "Go starter ved $5 for den første måned, derefter $10/måned. Du kan bruge det med OpenCode eller enhver agent.", @@ -326,7 +329,7 @@ export const dict = { "go.faq.a2": "Go inkluderer modellerne nedenfor med generøse grænser og pålidelig adgang.", "go.faq.q3": "Er Go det samme som Zen?", "go.faq.a3": - "Nej. Zen er pay-as-you-go, mens Go starter ved $5 for den første måned, derefter $10/måned, med generøse grænser og pålidelig adgang til open source-modellerne GLM-5.2, GLM-5.1, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro og DeepSeek V4 Flash.", + "Nej. Zen er pay-as-you-go, mens Go starter ved $5 for den første måned, derefter $10/måned, med generøse grænser og pålidelig adgang til et kurateret modeludvalg.", "go.faq.q4": "Hvad koster Go?", "go.faq.a4.p1.beforePricing": "Go koster", "go.faq.a4.p1.pricingLink": "$5 første måned", @@ -335,8 +338,22 @@ export const dict = { "go.faq.a4.p2.accountLink": "konto", "go.faq.a4.p3": "Annuller til enhver tid.", "go.faq.q5": "Hvad med data og privatliv?", - "go.faq.a5.body": - "Planen er primært designet til internationale brugere, med modeller hostet i USA, EU og Singapore for stabil global adgang. Vores udbydere følger en nulopbevaringspolitik og bruger ikke dine data til modeltræning.", + "go.faq.a5.model": "Model", + "go.faq.a5.training": "Modeltræning", + "go.faq.a5.retention": "Dataopbevaring", + "go.faq.a5.retention30": "30 dage", + "go.faq.a5.retention0": "0 dage", + "go.faq.a5.used": "Brugt", + "go.faq.a5.notUsed": "Ikke brugt", + "go.faq.a5.noAgreement": "Ingen aftale", + "go.faq.a5.grokRetention": + "ZDR deaktiverer vigtige API-funktioner, der afhænger af lagrede data, herunder den tilstandsbevarende Responses API, Files and Collections og Batch API.", + "go.faq.a5.gptRetention": + "Logfiler til overvågning af misbrug genereres ved al brug af API-funktioner og opbevares i op til 30 dage.", + "go.faq.a5.learnMore": "Læs mere", + "go.faq.a5.deepseekRetention": + "ZDR-aftalen fornyes månedligt. Den nuværende aftale er gyldig til og med 31. august 2026.", + "go.faq.a5.beforeExceptions": "Go-modeller hostes i USA. Udbydere følger en nulopbevaringspolitik og bruger ikke dine data til modeltræning, med de", "go.faq.a5.exceptionsLink": "følgende undtagelser", @@ -349,7 +366,7 @@ export const dict = { "go.faq.q9": "Hvad er forskellen på gratis modeller og Go?", "go.faq.a9": - "Gratis modeller inkluderer Big Pickle plus salgsfremmende modeller tilgængelige på det tidspunkt, med en kvote på 200 forespørgsler/dag. Go inkluderer GLM-5.2, GLM-5.1, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro og DeepSeek V4 Flash med højere anmodningskvoter håndhævet over rullende vinduer (5-timers, ugentlig og månedlig), nogenlunde svarende til $12 pr. 5 timer, $30 pr. uge og $60 pr. måned (faktiske anmodningstal varierer efter model og brug).", + "Gratis modeller inkluderer Big Pickle plus kampagnemodeller, der er tilgængelige på det pågældende tidspunkt, med en kvote på 200 forespørgsler/dag. Go tilbyder et kurateret modeludvalg med højere forespørgselskvoter håndhævet over rullende perioder (5 timer, ugentligt og månedligt), omtrent svarende til $12 pr. 5 timer, $30 pr. uge og $60 pr. måned (det faktiske antal forespørgsler varierer efter model og brug).", "zen.api.error.rateLimitExceeded": "Hastighedsgrænse overskredet. Prøv venligst igen senere.", "zen.api.error.modelNotSupported": "Model {{model}} understøttes ikke", @@ -358,6 +375,7 @@ export const dict = { "zen.api.error.providerNotSupported": "Udbyder {{provider}} understøttes ikke", "zen.api.error.missingApiKey": "Manglende API-nøgle.", "zen.api.error.invalidApiKey": "Ugyldig API-nøgle.", + "zen.api.error.requestBlockedByUpstreamProvider": "Anmodningen blev blokeret af upstream-udbyderen.", "zen.api.error.subscriptionQuotaExceeded": "Abonnementskvote overskredet. Prøv igen om {{retryIn}}.", "zen.api.error.goSubscriptionRollingLimitExceeded": "Forbrugsgrænsen for 5 timer er nået. Nulstilles om {{retryIn}}. For at fortsætte med at bruge denne model nu, aktivér forbrug fra din tilgængelige saldo: {{consoleGoUrl}}", @@ -373,7 +391,7 @@ export const dict = { "Du har nået din månedlige forbrugsgrænse på ${{amount}}. Administrer dine grænser her: {{membersUrl}}", "zen.api.error.modelDisabled": "Modellen er deaktiveret", "zen.api.error.regionNotAllowed": - "Denne model hostes i Kina. Hvis du vil bruge denne model, skal du aktivere den i dine indstillinger: {{consoleGoUrl}}", + "Den nyeste version af denne model er kun tilgængelig som hostet i Kina og kræver, at du aktivt tilvælger den: {{consoleGoUrl}}", "zen.api.error.trialEnded": "Den gratis kampagne for {{model}} er afsluttet. Du kan fortsætte med at bruge modellen ved at abonnere på OpenCode Go - {{link}}", @@ -665,7 +683,7 @@ export const dict = { "workspace.lite.promo.price": "$5 for den første måned", "workspace.lite.promo.modelsTitle": "Hvad er inkluderet", "workspace.lite.promo.footer": - "Planen er primært designet til internationale brugere, med modeller hostet i USA, EU og Singapore for stabil global adgang. Priser og forbrugsgrænser kan ændre sig, efterhånden som vi lærer af tidlig brug og feedback.", + "Planen er primært udviklet til internationale brugere og giver stabil adgang i hele verden. Priser og forbrugsgrænser kan ændre sig, efterhånden som vi lærer af de første brugserfaringer og tilbagemeldinger.", "workspace.lite.promo.subscribe": "Abonner på Go", "workspace.lite.promo.subscribing": "Omdirigerer...", "workspace.lite.promo.otherMethods": "Andre betalingsmetoder", @@ -706,11 +724,11 @@ export const dict = { "download.title": "OpenCode | Download", "download.meta.description": "Download OpenCode til macOS, Windows og Linux", - "download.hero.title": "Download OpenCode", + "download.hero.title": "Download OpenCode Desktop", "download.hero.subtitle": "Tilgængelig i beta til macOS, Windows og Linux", "download.hero.button": "Download til {{os}}", "download.section.terminal": "OpenCode Terminal", - "download.section.desktop": "OpenCode Desktop (Beta)", + "download.section.desktop": "OpenCode Desktop", "download.section.extensions": "OpenCode Extensions", "download.section.integrations": "OpenCode Integrations", "download.action.download": "Download", diff --git a/packages/console/app/src/i18n/de.ts b/packages/console/app/src/i18n/de.ts index 5579796f3a..61184358da 100644 --- a/packages/console/app/src/i18n/de.ts +++ b/packages/console/app/src/i18n/de.ts @@ -102,10 +102,14 @@ export const dict = { "temp.logoDarkAlt": "OpenCode Logo dunkel", "home.banner.badge": "Neu", - "home.banner.text": "Desktop-App in der Beta verfügbar", - "home.banner.platforms": "auf macOS, Windows und Linux", + "home.banner.text": "Neu: Tabs für Desktop.", + "home.banner.platforms": "Verfügbar für macOS, Windows und Linux", "home.banner.downloadNow": "Jetzt herunterladen", "home.banner.downloadBetaNow": "Desktop-Beta jetzt herunterladen", + "home.promo.title": "Neu: Tabs für Desktop", + "home.promo.body": "Organisiere deine Arbeit und aktiven Sitzungen mit Tabs.", + "home.promo.cta": "Lade die neueste Version herunter, um loszulegen.", + "home.promo.close": "Ankündigung zur Desktop-App schließen", "home.hero.title": "Der Open-Source AI-Coding-Agent", "home.hero.subtitle.a": "Kostenlose Modelle inklusive oder verbinde jedes Modell eines beliebigen Anbieters,", @@ -252,8 +256,9 @@ export const dict = { "zen.privacy.exceptionsLink": "folgenden Ausnahmen", "go.title": "OpenCode Go | Kostengünstige Coding-Modelle für alle", + "go.banner.text": "GPT 5.6 Luna erhält für begrenzte Zeit 2x Nutzungslimits", "go.meta.description": - "Go beginnt bei $5 für den ersten Monat, danach $10/Monat, mit großzügigen 5-Stunden-Anfragelimits für GLM-5.2, GLM-5.1, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro und DeepSeek V4 Flash.", + "Go beginnt bei $5 für deinen ersten Monat, danach $10/Monat, mit großzügigen Nutzungslimits und zuverlässigem Zugang zu führenden Coding-Modellen.", "go.hero.title": "Kostengünstige Coding-Modelle für alle", "go.hero.body": "Go bringt Agentic Coding zu Programmierern auf der ganzen Welt. Mit großzügigen Limits und zuverlässigem Zugang zu den leistungsfähigsten Open-Source-Modellen, damit du mit leistungsstarken Agenten entwickeln kannst, ohne dir Gedanken über Kosten oder Verfügbarkeit zu machen.", @@ -270,7 +275,6 @@ export const dict = { "go.graph.go": "Go", "go.graph.label": "Anfragen pro 5 Stunden", "go.graph.usageLimits": "Nutzungslimits", - "go.graph.tick": "{{n}}x", "go.graph.aria": "Anfragen pro 5h: {{free}} vs {{go}}", "go.testimonials.brand.zen": "Zen", @@ -301,8 +305,7 @@ export const dict = { "go.problem.item1": "Kostengünstiges Abonnement", "go.problem.item2": "Großzügige Limits und zuverlässiger Zugang", "go.problem.item3": "Für so viele Programmierer wie möglich gebaut", - "go.problem.item4": - "Beinhaltet GLM-5.2, GLM-5.1, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro und DeepSeek V4 Flash", + "go.problem.item4": "Eine kuratierte, für Agentic Coding getestete Modellauswahl", "go.how.title": "Wie Go funktioniert", "go.how.body": "Go beginnt bei $5 für den ersten Monat, danach $10/Monat. Du kannst es mit OpenCode oder jedem Agenten nutzen.", @@ -328,7 +331,7 @@ export const dict = { "go.faq.a2": "Go umfasst die unten aufgeführten Modelle mit großzügigen Limits und zuverlässigem Zugriff.", "go.faq.q3": "Ist Go dasselbe wie Zen?", "go.faq.a3": - "Nein. Zen ist Pay-as-you-go, während Go bei $5 für den ersten Monat beginnt, danach $10/Monat, mit großzügigen Limits und zuverlässigem Zugang zu den Open-Source-Modellen GLM-5.2, GLM-5.1, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro und DeepSeek V4 Flash.", + "Nein. Zen ist Pay-as-you-go, während Go bei $5 für deinen ersten Monat beginnt, danach $10/Monat, mit großzügigen Limits und zuverlässigem Zugang zu einer kuratierten Modellauswahl.", "go.faq.q4": "Wie viel kostet Go?", "go.faq.a4.p1.beforePricing": "Go kostet", "go.faq.a4.p1.pricingLink": "$5 im ersten Monat", @@ -337,8 +340,21 @@ export const dict = { "go.faq.a4.p2.accountLink": "Konto verwalten", "go.faq.a4.p3": "Jederzeit kündbar.", "go.faq.q5": "Was ist mit Daten und Privatsphäre?", - "go.faq.a5.body": - "Der Plan ist primär für internationale Nutzer konzipiert, mit Modellen gehostet in den USA, der EU und Singapur für stabilen globalen Zugang. Unsere Anbieter verfolgen eine Zero-Retention-Politik und nutzen deine Daten nicht für das Training von Modellen.", + "go.faq.a5.model": "Modell", + "go.faq.a5.training": "Modelltraining", + "go.faq.a5.retention": "Datenaufbewahrung", + "go.faq.a5.retention30": "30 Tage", + "go.faq.a5.retention0": "0 Tage", + "go.faq.a5.used": "Verwendet", + "go.faq.a5.notUsed": "Nicht verwendet", + "go.faq.a5.noAgreement": "Keine Vereinbarung", + "go.faq.a5.grokRetention": + "ZDR deaktiviert wichtige API-Funktionen, die von gespeicherten Daten abhängen, einschließlich der zustandsbehafteten Responses API, Files and Collections und der Batch API.", + "go.faq.a5.gptRetention": + "Für die Nutzung aller API-Funktionen werden Protokolle zur Missbrauchsüberwachung erstellt und bis zu 30 Tage lang aufbewahrt.", + "go.faq.a5.learnMore": "Mehr erfahren", + "go.faq.a5.deepseekRetention": + "Die ZDR-Vereinbarung wird monatlich erneuert. Die aktuelle Vereinbarung gilt bis einschließlich 31. August 2026.", "go.faq.a5.beforeExceptions": "Go-Modelle werden in den USA gehostet. Anbieter verfolgen eine Zero-Retention-Politik und nutzen deine Daten nicht für das Training von Modellen, mit den", "go.faq.a5.exceptionsLink": "folgenden Ausnahmen", @@ -352,7 +368,7 @@ export const dict = { "go.faq.q9": "Was ist der Unterschied zwischen kostenlosen Modellen und Go?", "go.faq.a9": - "Kostenlose Modelle beinhalten Big Pickle sowie Werbemodelle, die zum jeweiligen Zeitpunkt verfügbar sind, mit einem Kontingent von 200 Anfragen/Tag. Go beinhaltet GLM-5.2, GLM-5.1, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro und DeepSeek V4 Flash mit höheren Anfragekontingenten, die über rollierende Zeitfenster (5 Stunden, wöchentlich und monatlich) durchgesetzt werden, grob äquivalent zu $12 pro 5 Stunden, $30 pro Woche und $60 pro Monat (tatsächliche Anfragezahlen variieren je nach Modell und Nutzung).", + "Kostenlose Modelle beinhalten Big Pickle sowie Werbemodelle, die zum jeweiligen Zeitpunkt verfügbar sind, mit einem Kontingent von 200 Anfragen/Tag. Go bietet eine kuratierte Modellauswahl mit höheren Anfragekontingenten, die über rollierende Zeitfenster (5 Stunden, wöchentlich und monatlich) durchgesetzt werden, grob äquivalent zu $12 pro 5 Stunden, $30 pro Woche und $60 pro Monat (tatsächliche Anfragezahlen variieren je nach Modell und Nutzung).", "zen.api.error.rateLimitExceeded": "Ratenlimit überschritten. Bitte versuche es später erneut.", "zen.api.error.modelNotSupported": "Modell {{model}} wird nicht unterstützt", @@ -361,6 +377,7 @@ export const dict = { "zen.api.error.providerNotSupported": "Anbieter {{provider}} wird nicht unterstützt", "zen.api.error.missingApiKey": "Fehlender API-Key.", "zen.api.error.invalidApiKey": "Ungültiger API-Key.", + "zen.api.error.requestBlockedByUpstreamProvider": "Anfrage vom vorgelagerten Anbieter blockiert.", "zen.api.error.subscriptionQuotaExceeded": "Abonnement-Quote überschritten. Erneuter Versuch in {{retryIn}}.", "zen.api.error.goSubscriptionRollingLimitExceeded": "5-Stunden-Nutzungslimit erreicht. Wird in {{retryIn}} zurückgesetzt. Um dieses Modell jetzt weiter zu nutzen, aktiviere die Nutzung über dein verfügbares Guthaben: {{consoleGoUrl}}", @@ -376,7 +393,7 @@ export const dict = { "Du hast dein monatliches Ausgabenlimit von ${{amount}} erreicht. Verwalte deine Limits hier: {{membersUrl}}", "zen.api.error.modelDisabled": "Modell ist deaktiviert", "zen.api.error.regionNotAllowed": - "Dieses Modell wird in China gehostet. Wenn du dieses Modell verwenden möchtest, aktiviere es in deinen Einstellungen: {{consoleGoUrl}}", + "Die neueste Version dieses Modells ist nur verfügbar, wenn sie in China gehostet wird, und muss ausdrücklich aktiviert werden: {{consoleGoUrl}}", "zen.api.error.trialEnded": "Die kostenlose Aktion für {{model}} ist beendet. Du kannst das Modell weiterhin nutzen, indem du OpenCode Go abonnierst - {{link}}", @@ -668,7 +685,7 @@ export const dict = { "workspace.lite.promo.price": "$5 im ersten Monat", "workspace.lite.promo.modelsTitle": "Was enthalten ist", "workspace.lite.promo.footer": - "Der Plan wurde hauptsächlich für internationale Nutzer entwickelt, wobei die Modelle in den USA, der EU und Singapur gehostet werden, um einen stabilen weltweiten Zugriff zu gewährleisten. Preise und Nutzungslimits können sich ändern, während wir aus der frühen Nutzung und dem Feedback lernen.", + "Der Plan richtet sich in erster Linie an internationale Nutzer und bietet stabilen weltweiten Zugriff. Preise und Nutzungslimits können sich ändern, wenn wir Erkenntnisse aus der ersten Nutzung und dem Feedback gewinnen.", "workspace.lite.promo.subscribe": "Go abonnieren", "workspace.lite.promo.subscribing": "Leite weiter...", "workspace.lite.promo.otherMethods": "Andere Zahlungsmethoden", @@ -711,11 +728,11 @@ export const dict = { "download.title": "OpenCode | Download", "download.meta.description": "Lade OpenCode für macOS, Windows und Linux herunter", - "download.hero.title": "OpenCode herunterladen", + "download.hero.title": "OpenCode Desktop herunterladen", "download.hero.subtitle": "In Beta verfügbar für macOS, Windows und Linux", "download.hero.button": "Download für {{os}}", "download.section.terminal": "OpenCode Terminal", - "download.section.desktop": "OpenCode Desktop (Beta)", + "download.section.desktop": "OpenCode Desktop", "download.section.extensions": "OpenCode Extensions", "download.section.integrations": "OpenCode Integrationen", "download.action.download": "Download", diff --git a/packages/console/app/src/i18n/en.ts b/packages/console/app/src/i18n/en.ts index 9cd10f6407..813473c367 100644 --- a/packages/console/app/src/i18n/en.ts +++ b/packages/console/app/src/i18n/en.ts @@ -102,11 +102,16 @@ export const dict = { "temp.logoDarkAlt": "opencode logo dark", "home.banner.badge": "New", - "home.banner.text": "Desktop app available in beta", - "home.banner.platforms": "on macOS, Windows, and Linux", + "home.banner.text": "Introducing tabs for desktop.", + "home.banner.platforms": "Available on macOS, Windows, and Linux", "home.banner.downloadNow": "Download now", "home.banner.downloadBetaNow": "Download the desktop beta now", + "home.promo.title": "Introducing Tabs for Desktop", + "home.promo.body": "Organize your work and active sessions with tabs.", + "home.promo.cta": "Download the latest to get started.", + "home.promo.close": "Dismiss desktop app announcement", + "home.hero.title": "The open source AI coding agent", "home.hero.subtitle.a": "Free models included or connect any model from any provider,", "home.hero.subtitle.b": "including Claude, GPT, Gemini and more.", @@ -248,8 +253,9 @@ export const dict = { "zen.privacy.exceptionsLink": "following exceptions", "go.title": "OpenCode Go | Low cost coding models for everyone", + "go.banner.text": "GPT 5.6 Luna gets 2× usage limits for a limited time", "go.meta.description": - "Go starts at $5 for your first month, then $10/month, with generous 5-hour request limits for GLM-5.2, GLM-5.1, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, and DeepSeek V4 Flash.", + "Go starts at $5 for your first month, then $10/month, with generous usage limits and reliable access to leading coding models.", "go.hero.title": "Low cost coding models for everyone", "go.hero.body": "Go brings agentic coding to programmers around the world. Offering generous limits and reliable access to the most capable open-source models, so you can build with powerful agents without worrying about cost or availability.", @@ -264,8 +270,8 @@ export const dict = { "go.graph.freePill": "Big Pickle and free models", "go.graph.go": "Go", "go.graph.label": "Requests per 5 hour", - "go.graph.usageLimits": "Usage limits", "go.graph.tick": "{{n}}x", + "go.graph.usageLimits": "Usage limits", "go.graph.aria": "Requests per 5h: {{free}} vs {{go}}", "go.testimonials.brand.zen": "Zen", @@ -296,8 +302,7 @@ export const dict = { "go.problem.item1": "Low cost subscription pricing", "go.problem.item2": "Generous limits and reliable access", "go.problem.item3": "Built for as many programmers as possible", - "go.problem.item4": - "Includes GLM-5.2, GLM-5.1, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, and DeepSeek V4 Flash", + "go.problem.item4": "A curated model lineup tested for agentic coding", "go.how.title": "How Go works", "go.how.body": "Go starts at $5 for your first month, then $10/month. You can use it with OpenCode or any agent.", "go.how.step1.title": "Create an account", @@ -322,7 +327,7 @@ export const dict = { "go.faq.a2": "Go includes the models listed below, with generous limits and reliable access.", "go.faq.q3": "Is Go the same as Zen?", "go.faq.a3": - "No. Zen is pay-as-you-go, while Go starts at $5 for your first month, then $10/month, with generous limits and reliable access to open-source models GLM-5.2, GLM-5.1, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, and DeepSeek V4 Flash.", + "No. Zen is pay-as-you-go, while Go starts at $5 for your first month, then $10/month, with generous limits and reliable access to a curated model lineup.", "go.faq.q4": "How much does Go cost?", "go.faq.a4.p1.beforePricing": "Go costs", "go.faq.a4.p1.pricingLink": "$5 first month", @@ -331,8 +336,21 @@ export const dict = { "go.faq.a4.p2.accountLink": "account", "go.faq.a4.p3": "Cancel any time.", "go.faq.q5": "What about data and privacy?", - "go.faq.a5.body": - "The plan is designed primarily for international users, with models hosted in the US, EU, and Singapore for stable global access. Our providers follow a zero-retention policy and do not use your data for model training.", + "go.faq.a5.model": "Model", + "go.faq.a5.training": "Model training", + "go.faq.a5.retention": "Data retention", + "go.faq.a5.retention30": "30 days", + "go.faq.a5.retention0": "0 days", + "go.faq.a5.used": "Used", + "go.faq.a5.notUsed": "Not used", + "go.faq.a5.noAgreement": "No agreement", + "go.faq.a5.grokRetention": + "ZDR disables important API features that depend on stored data, including the stateful Responses API, Files and Collections, and the Batch API.", + "go.faq.a5.gptRetention": + "Abuse monitoring logs are generated for all API feature usage and retained for up to 30 days.", + "go.faq.a5.deepseekRetention": + "ZDR agreement is renewed monthly. The current agreement is valid through August 31, 2026.", + "go.faq.a5.learnMore": "Learn more", "go.faq.a5.beforeExceptions": "Go models are hosted in the US. Providers follow a zero-retention policy and do not use your data for model training, with the", @@ -346,7 +364,7 @@ export const dict = { "go.faq.q9": "What is the difference between free models and Go?", "go.faq.a9": - "Free models include Big Pickle plus promotional models available at the time, with a quota of 200 requests/day. Go includes GLM-5.2, GLM-5.1, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, and DeepSeek V4 Flash with higher request quotas enforced across rolling windows (5-hour, weekly, and monthly), roughly equivalent to $12 per 5 hours, $30 per week, and $60 per month (actual request counts vary by model and usage).", + "Free models include Big Pickle plus promotional models available at the time, with a quota of 200 requests/day. Go offers a curated model lineup with higher request quotas enforced across rolling windows (5-hour, weekly, and monthly), roughly equivalent to $12 per 5 hours, $30 per week, and $60 per month (actual request counts vary by model and usage).", "zen.api.error.rateLimitExceeded": "Rate limit exceeded. Please try again later.", "zen.api.error.modelNotSupported": "Model {{model}} is not supported", @@ -355,6 +373,7 @@ export const dict = { "zen.api.error.providerNotSupported": "Provider {{provider}} not supported", "zen.api.error.missingApiKey": "Missing API key.", "zen.api.error.invalidApiKey": "Invalid API key.", + "zen.api.error.requestBlockedByUpstreamProvider": "Request blocked by upstream provider.", "zen.api.error.subscriptionQuotaExceeded": "Subscription quota exceeded. Retry in {{retryIn}}.", "zen.api.error.goSubscriptionRollingLimitExceeded": "5-hour usage limit reached. Resets in {{retryIn}}. To continue using this model now, enable usage from your available balance: {{consoleGoUrl}}", @@ -370,7 +389,7 @@ export const dict = { "You have reached your monthly spending limit of ${{amount}}. Manage your limits here: {{membersUrl}}", "zen.api.error.modelDisabled": "Model is disabled", "zen.api.error.regionNotAllowed": - "This model is hosted in China. If you would like to use this model, enable it in your settings: {{consoleGoUrl}}", + "The latest version of this model is only available hosted in China and requires explicit opt in: {{consoleGoUrl}}", "zen.api.error.trialEnded": "Free promotion has ended for {{model}}. You can continue using the model by subscribing to OpenCode Go - {{link}}", @@ -662,7 +681,7 @@ export const dict = { "workspace.lite.promo.price": "$5 for your first month", "workspace.lite.promo.modelsTitle": "What's Included", "workspace.lite.promo.footer": - "The plan is designed primarily for international users, with models hosted in the US, EU, and Singapore for stable global access. Pricing and usage limits may change as we learn from early usage and feedback.", + "The plan is designed primarily for international users and provides stable global access. Pricing and usage limits may change as we learn from early usage and feedback.", "workspace.lite.promo.subscribe": "Subscribe to Go", "workspace.lite.promo.subscribing": "Redirecting...", "workspace.lite.promo.otherMethods": "Other payment methods", @@ -703,11 +722,11 @@ export const dict = { "download.title": "OpenCode | Download", "download.meta.description": "Download OpenCode for macOS, Windows, and Linux", - "download.hero.title": "Download OpenCode", + "download.hero.title": "Download OpenCode Desktop", "download.hero.subtitle": "Available in Beta for macOS, Windows, and Linux", "download.hero.button": "Download for {{os}}", "download.section.terminal": "OpenCode Terminal", - "download.section.desktop": "OpenCode Desktop (Beta)", + "download.section.desktop": "OpenCode Desktop", "download.section.extensions": "OpenCode Extensions", "download.section.integrations": "OpenCode Integrations", "download.action.download": "Download", @@ -772,6 +791,10 @@ export const dict = { "enterprise.faq.q4": "Is my data secure with OpenCode Enterprise?", "enterprise.faq.a4": "Yes. OpenCode does not store your code or context data. All processing happens locally or through direct API calls to your AI provider. With central config and SSO integration, your data remains secure within your organization's infrastructure.", + "enterprise.faq.q5": "Where can I find your security and compliance documentation?", + "enterprise.faq.a5.before": + "Our Trust Center has everything: SOC 2 Type 2 report, security policies, subprocessor list, and answers to common security questions. Visit", + "enterprise.faq.a5.after": "to review or request documents under NDA.", "brand.title": "OpenCode | Brand", "brand.meta.description": "OpenCode brand guidelines", diff --git a/packages/console/app/src/i18n/es.ts b/packages/console/app/src/i18n/es.ts index 96d760e0b6..be5c24685c 100644 --- a/packages/console/app/src/i18n/es.ts +++ b/packages/console/app/src/i18n/es.ts @@ -102,10 +102,14 @@ export const dict = { "temp.logoDarkAlt": "logo de opencode oscuro", "home.banner.badge": "Nuevo", - "home.banner.text": "Aplicación de escritorio disponible en beta", - "home.banner.platforms": "en macOS, Windows y Linux", + "home.banner.text": "Presentamos Tabs para Desktop.", + "home.banner.platforms": "Disponible en macOS, Windows y Linux", "home.banner.downloadNow": "Descargar ahora", "home.banner.downloadBetaNow": "Descargar la beta de escritorio ahora", + "home.promo.title": "Presentamos Tabs para Desktop", + "home.promo.body": "Organiza tu trabajo y tus sesiones activas con pestañas.", + "home.promo.cta": "Descarga la última versión para empezar.", + "home.promo.close": "Cerrar el anuncio de la aplicación Desktop", "home.hero.title": "El agente de codificación IA de código abierto", "home.hero.subtitle.a": "Modelos gratuitos incluidos o conecta cualquier modelo de cualquier proveedor,", @@ -253,8 +257,9 @@ export const dict = { "zen.privacy.exceptionsLink": "siguientes excepciones", "go.title": "OpenCode Go | Modelos de programación de bajo coste para todos", + "go.banner.text": "GPT 5.6 Luna tiene límites de uso 2x mayores por tiempo limitado", "go.meta.description": - "Go comienza en $5 el primer mes, luego 10 $/mes, con generosos límites de solicitudes de 5 horas para GLM-5.2, GLM-5.1, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro y DeepSeek V4 Flash.", + "Go comienza en $5 el primer mes, luego 10 $/mes, con límites de uso generosos y acceso fiable a modelos de programación líderes.", "go.hero.title": "Modelos de programación de bajo coste para todos", "go.hero.body": "Go lleva la programación agéntica a programadores de todo el mundo. Ofrece límites generosos y acceso fiable a los modelos de código abierto más capaces, para que puedas crear con agentes potentes sin preocuparte por el coste o la disponibilidad.", @@ -271,7 +276,6 @@ export const dict = { "go.graph.go": "Go", "go.graph.label": "Solicitudes por 5 horas", "go.graph.usageLimits": "Límites de uso", - "go.graph.tick": "{{n}}x", "go.graph.aria": "Solicitudes por 5h: {{free}} vs {{go}}", "go.testimonials.brand.zen": "Zen", @@ -303,8 +307,7 @@ export const dict = { "go.problem.item1": "Precios de suscripción de bajo coste", "go.problem.item2": "Límites generosos y acceso fiable", "go.problem.item3": "Creado para tantos programadores como sea posible", - "go.problem.item4": - "Incluye GLM-5.2, GLM-5.1, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro y DeepSeek V4 Flash", + "go.problem.item4": "Una selección de modelos probados para programación agéntica", "go.how.title": "Cómo funciona Go", "go.how.body": "Go comienza en $5 el primer mes, luego 10 $/mes. Puedes usarlo con OpenCode o cualquier agente.", "go.how.step1.title": "Crear una cuenta", @@ -329,7 +332,7 @@ export const dict = { "go.faq.a2": "Go incluye los modelos que se indican abajo, con límites generosos y acceso confiable.", "go.faq.q3": "¿Es Go lo mismo que Zen?", "go.faq.a3": - "No. Zen es pago por uso, mientras que Go comienza en $5 el primer mes, luego 10 $/mes, con límites generosos y acceso fiable a los modelos de código abierto GLM-5.2, GLM-5.1, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro y DeepSeek V4 Flash.", + "No. Zen es de pago por uso, mientras que Go comienza en $5 el primer mes, luego 10 $/mes, con límites generosos y acceso fiable a una selección de modelos.", "go.faq.q4": "¿Cuánto cuesta Go?", "go.faq.a4.p1.beforePricing": "Go cuesta", "go.faq.a4.p1.pricingLink": "$5 el primer mes", @@ -338,8 +341,21 @@ export const dict = { "go.faq.a4.p2.accountLink": "cuenta", "go.faq.a4.p3": "Cancela en cualquier momento.", "go.faq.q5": "¿Qué pasa con los datos y la privacidad?", - "go.faq.a5.body": - "El plan está diseñado principalmente para usuarios internacionales, con modelos alojados en EE. UU., UE y Singapur para un acceso global estable. Nuestros proveedores siguen una política de retención cero y no utilizan tus datos para el entrenamiento de modelos.", + "go.faq.a5.model": "Modelo", + "go.faq.a5.training": "Entrenamiento del modelo", + "go.faq.a5.retention": "Retención de datos", + "go.faq.a5.retention30": "30 días", + "go.faq.a5.retention0": "0 días", + "go.faq.a5.used": "Utilizado", + "go.faq.a5.notUsed": "No utilizado", + "go.faq.a5.noAgreement": "Sin acuerdo", + "go.faq.a5.grokRetention": + "ZDR deshabilita funciones importantes de la API que dependen de datos almacenados, incluidas la Responses API con estado, Files and Collections y la Batch API.", + "go.faq.a5.gptRetention": + "Se generan registros de supervisión de abusos para todo el uso de funciones de la API y se conservan durante un máximo de 30 días.", + "go.faq.a5.learnMore": "Más información", + "go.faq.a5.deepseekRetention": + "El acuerdo de ZDR se renueva mensualmente. El acuerdo actual es válido hasta el 31 de agosto de 2026.", "go.faq.a5.beforeExceptions": "Los modelos de Go están alojados en EE. UU. Los proveedores siguen una política de retención cero y no utilizan tus datos para el entrenamiento de modelos, con las", "go.faq.a5.exceptionsLink": "siguientes excepciones", @@ -353,7 +369,7 @@ export const dict = { "go.faq.q9": "¿Cuál es la diferencia entre los modelos gratuitos y Go?", "go.faq.a9": - "Los modelos gratuitos incluyen Big Pickle más modelos promocionales disponibles en el momento, con una cuota de 200 solicitudes/día. Go incluye GLM-5.2, GLM-5.1, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro y DeepSeek V4 Flash con cuotas de solicitud más altas aplicadas a través de ventanas móviles (5 horas, semanal y mensual), aproximadamente equivalente a 12 $ por 5 horas, 30 $ por semana y 60 $ por mes (los recuentos reales de solicitudes varían según el modelo y el uso).", + "Los modelos gratuitos incluyen Big Pickle y los modelos promocionales disponibles en ese momento, con una cuota de 200 solicitudes/día. Go ofrece una selección de modelos con cuotas de solicitudes más altas aplicadas en ventanas móviles (de 5 horas, semanales y mensuales), aproximadamente equivalentes a 12 $ por 5 horas, 30 $ por semana y 60 $ por mes (la cantidad real de solicitudes varía según el modelo y el uso).", "zen.api.error.rateLimitExceeded": "Límite de tasa excedido. Por favor, inténtalo de nuevo más tarde.", "zen.api.error.modelNotSupported": "Modelo {{model}} no soportado", @@ -362,6 +378,7 @@ export const dict = { "zen.api.error.providerNotSupported": "Proveedor {{provider}} no soportado", "zen.api.error.missingApiKey": "Falta la clave API.", "zen.api.error.invalidApiKey": "Clave API inválida.", + "zen.api.error.requestBlockedByUpstreamProvider": "El proveedor externo bloqueó la solicitud.", "zen.api.error.subscriptionQuotaExceeded": "Cuota de suscripción excedida. Reintenta en {{retryIn}}.", "zen.api.error.goSubscriptionRollingLimitExceeded": "Límite de uso de 5 horas alcanzado. Se restablece en {{retryIn}}. Para seguir usando este modelo ahora, habilita el uso desde tu saldo disponible: {{consoleGoUrl}}", @@ -377,7 +394,7 @@ export const dict = { "Has alcanzado tu límite de gasto mensual de ${{amount}}. Gestiona tus límites aquí: {{membersUrl}}", "zen.api.error.modelDisabled": "El modelo está deshabilitado", "zen.api.error.regionNotAllowed": - "Este modelo está alojado en China. Si quieres usar este modelo, actívalo en tu configuración: {{consoleGoUrl}}", + "La versión más reciente de este modelo solo está disponible alojada en China y requiere una aceptación explícita: {{consoleGoUrl}}", "zen.api.error.trialEnded": "La promoción gratuita de {{model}} ha finalizado. Puedes seguir usando el modelo suscribiéndote a OpenCode Go - {{link}}", @@ -669,7 +686,7 @@ export const dict = { "workspace.lite.promo.price": "$5 el primer mes", "workspace.lite.promo.modelsTitle": "Qué incluye", "workspace.lite.promo.footer": - "El plan está diseñado principalmente para usuarios internacionales, con modelos alojados en EE. UU., la UE y Singapur para un acceso global estable. Los precios y los límites de uso pueden cambiar a medida que aprendemos del uso inicial y los comentarios.", + "El plan está diseñado principalmente para usuarios internacionales y ofrece un acceso global estable. Los precios y los límites de uso pueden cambiar a medida que aprendemos del uso inicial y de los comentarios recibidos.", "workspace.lite.promo.subscribe": "Suscribirse a Go", "workspace.lite.promo.subscribing": "Redirigiendo...", "workspace.lite.promo.otherMethods": "Otros métodos de pago", @@ -711,11 +728,11 @@ export const dict = { "download.title": "OpenCode | Descargar", "download.meta.description": "Descarga OpenCode para macOS, Windows y Linux", - "download.hero.title": "Descargar OpenCode", + "download.hero.title": "Descargar OpenCode Desktop", "download.hero.subtitle": "Disponible en Beta para macOS, Windows y Linux", "download.hero.button": "Descargar para {{os}}", "download.section.terminal": "OpenCode Terminal", - "download.section.desktop": "OpenCode Desktop (Beta)", + "download.section.desktop": "OpenCode Desktop", "download.section.extensions": "Extensiones OpenCode", "download.section.integrations": "Integraciones OpenCode", "download.action.download": "Descargar", diff --git a/packages/console/app/src/i18n/fr.ts b/packages/console/app/src/i18n/fr.ts index 039692ef6b..37bf2d5eb3 100644 --- a/packages/console/app/src/i18n/fr.ts +++ b/packages/console/app/src/i18n/fr.ts @@ -100,10 +100,14 @@ export const dict = { "temp.logoDarkAlt": "opencode logo dark", "home.banner.badge": "Nouveau", - "home.banner.text": "Application desktop disponible en bêta", - "home.banner.platforms": "sur macOS, Windows et Linux", + "home.banner.text": "Découvrez Tabs pour Desktop.", + "home.banner.platforms": "Disponible sur macOS, Windows et Linux", "home.banner.downloadNow": "Télécharger maintenant", "home.banner.downloadBetaNow": "Télécharger la bêta desktop maintenant", + "home.promo.title": "Découvrez Tabs pour Desktop", + "home.promo.body": "Organisez votre travail et vos sessions actives avec des onglets.", + "home.promo.cta": "Téléchargez la dernière version pour commencer.", + "home.promo.close": "Fermer l’annonce de l’application Desktop", "home.hero.title": "L'agent de code IA open source", "home.hero.subtitle.a": @@ -254,8 +258,9 @@ export const dict = { "zen.privacy.exceptionsLink": "exceptions suivantes", "go.title": "OpenCode Go | Modèles de code à faible coût pour tous", + "go.banner.text": "GPT 5.6 Luna bénéficie de limites d’utilisation 2x supérieures pour une durée limitée", "go.meta.description": - "Go commence à $5 pour le premier mois, puis 10 $/mois, avec des limites de requêtes généreuses sur 5 heures pour GLM-5.2, GLM-5.1, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro et DeepSeek V4 Flash.", + "Go commence à $5 pour le premier mois, puis 10 $/mois, avec des limites d'utilisation généreuses et un accès fiable aux principaux modèles de codage.", "go.hero.title": "Modèles de code à faible coût pour tous", "go.hero.body": "Go apporte le codage agentique aux programmeurs du monde entier. Offrant des limites généreuses et un accès fiable aux modèles open source les plus capables, pour que vous puissiez construire avec des agents puissants sans vous soucier du coût ou de la disponibilité.", @@ -272,7 +277,6 @@ export const dict = { "go.graph.go": "Go", "go.graph.label": "Requêtes par tranche de 5 heures", "go.graph.usageLimits": "Limites d'utilisation", - "go.graph.tick": "{{n}}x", "go.graph.aria": "Requêtes par 5h : {{free}} vs {{go}}", "go.testimonials.brand.zen": "Zen", @@ -303,8 +307,7 @@ export const dict = { "go.problem.item1": "Prix d'abonnement bas", "go.problem.item2": "Limites généreuses et accès fiable", "go.problem.item3": "Conçu pour autant de programmeurs que possible", - "go.problem.item4": - "Inclut GLM-5.2, GLM-5.1, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro et DeepSeek V4 Flash", + "go.problem.item4": "Une sélection de modèles testés pour le codage agentique", "go.how.title": "Comment fonctionne Go", "go.how.body": "Go commence à $5 pour le premier mois, puis 10 $/mois. Vous pouvez l'utiliser avec OpenCode ou n'importe quel agent.", @@ -330,7 +333,7 @@ export const dict = { "go.faq.a2": "Go inclut les modèles ci-dessous, avec des limites généreuses et un accès fiable.", "go.faq.q3": "Est-ce que Go est la même chose que Zen ?", "go.faq.a3": - "Non. Zen est un paiement à l'utilisation, tandis que Go commence à $5 pour le premier mois, puis 10 $/mois, avec des limites généreuses et un accès fiable aux modèles open source GLM-5.2, GLM-5.1, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro et DeepSeek V4 Flash.", + "Non. Zen est un paiement à l'utilisation, tandis que Go commence à $5 pour le premier mois, puis 10 $/mois, avec des limites généreuses et un accès fiable à une sélection de modèles.", "go.faq.q4": "Combien coûte Go ?", "go.faq.a4.p1.beforePricing": "Go coûte", "go.faq.a4.p1.pricingLink": "$5 le premier mois", @@ -339,8 +342,22 @@ export const dict = { "go.faq.a4.p2.accountLink": "compte", "go.faq.a4.p3": "Annulez à tout moment.", "go.faq.q5": "Et pour les données et la confidentialité ?", - "go.faq.a5.body": - "Le plan est conçu principalement pour les utilisateurs internationaux, avec des modèles hébergés aux États-Unis, dans l'UE et à Singapour pour un accès mondial stable. Nos fournisseurs suivent une politique de rétention zéro et n'utilisent pas vos données pour l'entraînement des modèles.", + "go.faq.a5.model": "Modèle", + "go.faq.a5.training": "Entraînement des modèles", + "go.faq.a5.retention": "Conservation des données", + "go.faq.a5.retention30": "30 jours", + "go.faq.a5.retention0": "0 jour", + "go.faq.a5.used": "Utilisé", + "go.faq.a5.notUsed": "Non utilisé", + "go.faq.a5.noAgreement": "Aucun accord", + "go.faq.a5.grokRetention": + "Le ZDR désactive d'importantes fonctionnalités API qui dépendent des données stockées, notamment Responses API avec état, Files and Collections et Batch API.", + "go.faq.a5.gptRetention": + "Des journaux de surveillance des abus sont générés pour toute utilisation des fonctionnalités API et conservés pendant un maximum de 30 jours.", + "go.faq.a5.learnMore": "En savoir plus", + "go.faq.a5.deepseekRetention": + "L’accord ZDR est renouvelé chaque mois. L’accord actuel est valable jusqu’au 31 août 2026.", + "go.faq.a5.beforeExceptions": "Les modèles Go sont hébergés aux États-Unis. Les fournisseurs suivent une politique de rétention zéro et n'utilisent pas vos données pour l'entraînement des modèles, avec les", "go.faq.a5.exceptionsLink": "exceptions suivantes", @@ -353,7 +370,7 @@ export const dict = { "Oui, vous pouvez utiliser Go avec n'importe quel agent. Suivez les instructions de configuration dans votre agent de code préféré.", "go.faq.q9": "Quelle est la différence entre les modèles gratuits et Go ?", "go.faq.a9": - "Les modèles gratuits incluent Big Pickle ainsi que des modèles promotionnels disponibles à ce moment-là, avec un quota de 200 requêtes/jour. Go inclut GLM-5.2, GLM-5.1, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro et DeepSeek V4 Flash avec des quotas de requêtes plus élevés appliqués sur des fenêtres glissantes (5 heures, hebdomadaire et mensuelle), à peu près équivalent à 12 $ par 5 heures, 30 $ par semaine et 60 $ par mois (le nombre réel de requêtes varie selon le modèle et l'utilisation).", + "Les modèles gratuits incluent Big Pickle ainsi que les modèles promotionnels disponibles à ce moment-là, avec un quota de 200 requêtes/jour. Go propose une sélection de modèles avec des quotas de requêtes plus élevés appliqués sur des fenêtres glissantes (5 heures, hebdomadaire et mensuelle), à peu près équivalents à 12 $ par 5 heures, 30 $ par semaine et 60 $ par mois (le nombre réel de requêtes varie selon le modèle et l'utilisation).", "zen.api.error.rateLimitExceeded": "Limite de débit dépassée. Veuillez réessayer plus tard.", "zen.api.error.modelNotSupported": "Modèle {{model}} non pris en charge", @@ -362,6 +379,7 @@ export const dict = { "zen.api.error.providerNotSupported": "Fournisseur {{provider}} non pris en charge", "zen.api.error.missingApiKey": "Clé API manquante.", "zen.api.error.invalidApiKey": "Clé API invalide.", + "zen.api.error.requestBlockedByUpstreamProvider": "Requête bloquée par le fournisseur en amont.", "zen.api.error.subscriptionQuotaExceeded": "Quota d'abonnement dépassé. Réessayez dans {{retryIn}}.", "zen.api.error.goSubscriptionRollingLimitExceeded": "Limite d'utilisation sur 5 heures atteinte. Réinitialisation dans {{retryIn}}. Pour continuer à utiliser ce modèle dès maintenant, activez l'utilisation depuis votre solde disponible : {{consoleGoUrl}}", @@ -377,7 +395,7 @@ export const dict = { "Vous avez atteint votre limite de dépense mensuelle de {{amount}} $. Gérez vos limites ici : {{membersUrl}}", "zen.api.error.modelDisabled": "Le modèle est désactivé", "zen.api.error.regionNotAllowed": - "Ce modèle est hébergé en Chine. Si vous souhaitez utiliser ce modèle, activez-le dans vos paramètres : {{consoleGoUrl}}", + "La dernière version de ce modèle est uniquement disponible avec un hébergement en Chine et nécessite votre consentement explicite : {{consoleGoUrl}}", "zen.api.error.trialEnded": "La promotion gratuite de {{model}} est terminée. Vous pouvez continuer à utiliser le modèle en vous abonnant à OpenCode Go - {{link}}", @@ -675,7 +693,7 @@ export const dict = { "workspace.lite.promo.price": "$5 le premier mois", "workspace.lite.promo.modelsTitle": "Ce qui est inclus", "workspace.lite.promo.footer": - "Le plan est conçu principalement pour les utilisateurs internationaux, avec des modèles hébergés aux États-Unis, dans l'UE et à Singapour pour un accès mondial stable. Les tarifs et les limites d'utilisation peuvent changer à mesure que nous apprenons des premières utilisations et des commentaires.", + "Ce forfait est principalement conçu pour les utilisateurs internationaux et offre un accès mondial stable. Les tarifs et les limites d'utilisation peuvent évoluer à mesure que nous tirons les enseignements des premières utilisations et des retours reçus.", "workspace.lite.promo.subscribe": "S'abonner à Go", "workspace.lite.promo.subscribing": "Redirection...", "workspace.lite.promo.otherMethods": "Autres méthodes de paiement", @@ -718,11 +736,11 @@ export const dict = { "download.title": "OpenCode | Téléchargement", "download.meta.description": "Téléchargez OpenCode pour macOS, Windows et Linux", - "download.hero.title": "Télécharger OpenCode", + "download.hero.title": "Télécharger OpenCode Desktop", "download.hero.subtitle": "Disponible en bêta pour macOS, Windows et Linux", "download.hero.button": "Télécharger pour {{os}}", "download.section.terminal": "OpenCode Terminal", - "download.section.desktop": "OpenCode Desktop (Bêta)", + "download.section.desktop": "OpenCode Desktop", "download.section.extensions": "Extensions OpenCode", "download.section.integrations": "Intégrations OpenCode", "download.action.download": "Télécharger", diff --git a/packages/console/app/src/i18n/it.ts b/packages/console/app/src/i18n/it.ts index bd080c44d3..501d012333 100644 --- a/packages/console/app/src/i18n/it.ts +++ b/packages/console/app/src/i18n/it.ts @@ -102,10 +102,14 @@ export const dict = { "temp.logoDarkAlt": "logo scuro di opencode", "home.banner.badge": "Nuovo", - "home.banner.text": "App desktop disponibile in beta", - "home.banner.platforms": "su macOS, Windows e Linux", + "home.banner.text": "Scopri Tabs per Desktop.", + "home.banner.platforms": "Disponibile su macOS, Windows e Linux", "home.banner.downloadNow": "Scarica ora", "home.banner.downloadBetaNow": "Scarica ora la beta desktop", + "home.promo.title": "Scopri Tabs per Desktop", + "home.promo.body": "Organizza il tuo lavoro e le sessioni attive con le schede.", + "home.promo.cta": "Scarica l’ultima versione per iniziare.", + "home.promo.close": "Chiudi l’annuncio dell’app Desktop", "home.hero.title": "L'agente di coding IA open source", "home.hero.subtitle.a": "Modelli gratuiti inclusi o collega qualsiasi modello da qualsiasi provider,", @@ -250,8 +254,9 @@ export const dict = { "zen.privacy.exceptionsLink": "seguenti eccezioni", "go.title": "OpenCode Go | Modelli di coding a basso costo per tutti", + "go.banner.text": "GPT 5.6 Luna offre limiti di utilizzo 2x superiori per un periodo limitato", "go.meta.description": - "Go inizia a $5 per il primo mese, poi $10/mese, con generosi limiti di richiesta di 5 ore per GLM-5.2, GLM-5.1, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro e DeepSeek V4 Flash.", + "Go inizia a $5 per il primo mese, poi $10/mese, con limiti di utilizzo generosi e un accesso affidabile ai principali modelli di coding.", "go.hero.title": "Modelli di coding a basso costo per tutti", "go.hero.body": "Go porta il coding agentico ai programmatori di tutto il mondo. Offrendo limiti generosi e un accesso affidabile ai modelli open source più capaci, in modo da poter costruire con agenti potenti senza preoccuparsi dei costi o della disponibilità.", @@ -268,7 +273,6 @@ export const dict = { "go.graph.go": "Go", "go.graph.label": "Richieste ogni 5 ore", "go.graph.usageLimits": "Limiti di utilizzo", - "go.graph.tick": "{{n}}x", "go.graph.aria": "Richieste ogni 5h: {{free}} vs {{go}}", "go.testimonials.brand.zen": "Zen", @@ -299,8 +303,7 @@ export const dict = { "go.problem.item1": "Prezzo di abbonamento a basso costo", "go.problem.item2": "Limiti generosi e accesso affidabile", "go.problem.item3": "Costruito per il maggior numero possibile di programmatori", - "go.problem.item4": - "Include GLM-5.2, GLM-5.1, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro e DeepSeek V4 Flash", + "go.problem.item4": "Una selezione curata di modelli testati per il coding agentico", "go.how.title": "Come funziona Go", "go.how.body": "Go inizia a $5 per il primo mese, poi $10/mese. Puoi usarlo con OpenCode o qualsiasi agente.", "go.how.step1.title": "Crea un account", @@ -325,7 +328,7 @@ export const dict = { "go.faq.a2": "Go include i modelli elencati di seguito, con limiti generosi e accesso affidabile.", "go.faq.q3": "Go è lo stesso di Zen?", "go.faq.a3": - "No. Zen è a consumo, mentre Go inizia a $5 per il primo mese, poi $10/mese, con limiti generosi e accesso affidabile ai modelli open source GLM-5.2, GLM-5.1, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro e DeepSeek V4 Flash.", + "No. Zen è a consumo, mentre Go inizia a $5 per il primo mese, poi $10/mese, con limiti generosi e un accesso affidabile a una selezione curata di modelli.", "go.faq.q4": "Quanto costa Go?", "go.faq.a4.p1.beforePricing": "Go costa", "go.faq.a4.p1.pricingLink": "$5 il primo mese", @@ -334,8 +337,21 @@ export const dict = { "go.faq.a4.p2.accountLink": "account", "go.faq.a4.p3": "Annulla in qualsiasi momento.", "go.faq.q5": "E per quanto riguarda dati e privacy?", - "go.faq.a5.body": - "Il piano è progettato principalmente per gli utenti internazionali, con modelli ospitati negli Stati Uniti, UE e Singapore per un accesso globale stabile. I nostri provider seguono una policy di zero-retention e non usano i tuoi dati per l'addestramento dei modelli.", + "go.faq.a5.model": "Modello", + "go.faq.a5.training": "Addestramento del modello", + "go.faq.a5.retention": "Conservazione dei dati", + "go.faq.a5.retention30": "30 giorni", + "go.faq.a5.retention0": "0 giorni", + "go.faq.a5.used": "Utilizzato", + "go.faq.a5.notUsed": "Non utilizzato", + "go.faq.a5.noAgreement": "Nessun accordo", + "go.faq.a5.grokRetention": + "ZDR disabilita importanti funzionalità API che dipendono dai dati archiviati, tra cui la Responses API con stato, Files and Collections e Batch API.", + "go.faq.a5.gptRetention": + "I log di monitoraggio degli abusi vengono generati per l'utilizzo di tutte le funzionalità API e conservati per un massimo di 30 giorni.", + "go.faq.a5.learnMore": "Scopri di più", + "go.faq.a5.deepseekRetention": + "L'accordo ZDR viene rinnovato mensilmente. L'accordo attuale è valido fino al 31 agosto 2026.", "go.faq.a5.beforeExceptions": "I modelli Go sono ospitati negli Stati Uniti. I provider seguono una policy di zero-retention e non usano i tuoi dati per l'addestramento dei modelli, con le", "go.faq.a5.exceptionsLink": "seguenti eccezioni", @@ -349,7 +365,7 @@ export const dict = { "go.faq.q9": "Qual è la differenza tra i modelli gratuiti e Go?", "go.faq.a9": - "I modelli gratuiti includono Big Pickle più modelli promozionali disponibili al momento, con una quota di 200 richieste/giorno. Go include GLM-5.2, GLM-5.1, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro e DeepSeek V4 Flash con quote di richiesta più elevate applicate su finestre mobili (5 ore, settimanale e mensile), approssimativamente equivalenti a $12 ogni 5 ore, $30 a settimana e $60 al mese (il conteggio effettivo delle richieste varia in base al modello e all'utilizzo).", + "I modelli gratuiti includono Big Pickle più i modelli promozionali disponibili al momento, con una quota di 200 richieste/giorno. Go offre una selezione curata di modelli con quote di richiesta più elevate applicate su finestre mobili (5 ore, settimanale e mensile), approssimativamente equivalenti a $12 ogni 5 ore, $30 a settimana e $60 al mese (il conteggio effettivo delle richieste varia in base al modello e all'utilizzo).", "zen.api.error.rateLimitExceeded": "Limite di richieste superato. Riprova più tardi.", "zen.api.error.modelNotSupported": "Modello {{model}} non supportato", @@ -358,6 +374,7 @@ export const dict = { "zen.api.error.providerNotSupported": "Provider {{provider}} non supportato", "zen.api.error.missingApiKey": "Chiave API mancante.", "zen.api.error.invalidApiKey": "Chiave API non valida.", + "zen.api.error.requestBlockedByUpstreamProvider": "Richiesta bloccata dal provider upstream.", "zen.api.error.subscriptionQuotaExceeded": "Quota dell'abbonamento superata. Riprova tra {{retryIn}}.", "zen.api.error.goSubscriptionRollingLimitExceeded": "Limite di utilizzo di 5 ore raggiunto. Si reimposta tra {{retryIn}}. Per continuare a usare questo modello ora, abilita l'utilizzo dal tuo saldo disponibile: {{consoleGoUrl}}", @@ -373,7 +390,7 @@ export const dict = { "Hai raggiunto il tuo limite di spesa mensile di ${{amount}}. Gestisci i tuoi limiti qui: {{membersUrl}}", "zen.api.error.modelDisabled": "Il modello è disabilitato", "zen.api.error.regionNotAllowed": - "Questo modello è ospitato in Cina. Se vuoi usare questo modello, abilitalo nelle tue impostazioni: {{consoleGoUrl}}", + "La versione più recente di questo modello è disponibile solo con hosting in Cina e richiede un consenso esplicito: {{consoleGoUrl}}", "zen.api.error.trialEnded": "La promozione gratuita di {{model}} è terminata. Puoi continuare a usare il modello abbonandoti a OpenCode Go - {{link}}", @@ -667,7 +684,7 @@ export const dict = { "workspace.lite.promo.price": "$5 il primo mese", "workspace.lite.promo.modelsTitle": "Cosa è incluso", "workspace.lite.promo.footer": - "Il piano è progettato principalmente per gli utenti internazionali, con modelli ospitati in US, EU e Singapore per un accesso globale stabile. I prezzi e i limiti di utilizzo potrebbero cambiare man mano che impariamo dall'utilizzo iniziale e dal feedback.", + "Il piano è pensato principalmente per gli utenti internazionali e offre un accesso globale stabile. I prezzi e i limiti di utilizzo potrebbero cambiare in base a quanto apprenderemo dall'utilizzo iniziale e dai feedback.", "workspace.lite.promo.subscribe": "Abbonati a Go", "workspace.lite.promo.subscribing": "Reindirizzamento...", "workspace.lite.promo.otherMethods": "Altri metodi di pagamento", @@ -709,11 +726,11 @@ export const dict = { "download.title": "OpenCode | Download", "download.meta.description": "Scarica OpenCode per macOS, Windows e Linux", - "download.hero.title": "Scarica OpenCode", + "download.hero.title": "Scarica OpenCode Desktop", "download.hero.subtitle": "Disponibile in Beta per macOS, Windows e Linux", "download.hero.button": "Scarica per {{os}}", "download.section.terminal": "OpenCode Terminal", - "download.section.desktop": "OpenCode Desktop (Beta)", + "download.section.desktop": "OpenCode Desktop", "download.section.extensions": "OpenCode Extensions", "download.section.integrations": "OpenCode Integrations", "download.action.download": "Scarica", diff --git a/packages/console/app/src/i18n/ja.ts b/packages/console/app/src/i18n/ja.ts index c3e7ff2d4c..e8f8fa5661 100644 --- a/packages/console/app/src/i18n/ja.ts +++ b/packages/console/app/src/i18n/ja.ts @@ -102,10 +102,14 @@ export const dict = { "temp.logoDarkAlt": "opencodeのロゴ(ダーク)", "home.banner.badge": "新着", - "home.banner.text": "デスクトップアプリのベータ版が利用可能", - "home.banner.platforms": "macOS、Windows、Linux で", + "home.banner.text": "デスクトップ版にタブが登場。", + "home.banner.platforms": "macOS、Windows、Linux で利用できます", "home.banner.downloadNow": "今すぐダウンロード", "home.banner.downloadBetaNow": "デスクトップベータ版を今すぐダウンロード", + "home.promo.title": "デスクトップ版にタブが登場", + "home.promo.body": "タブで作業とアクティブなセッションを整理できます。", + "home.promo.cta": "始めるには最新版をダウンロードしてください。", + "home.promo.close": "デスクトップアプリのお知らせを閉じる", "home.hero.title": "オープンソースのAIコーディングエージェント", "home.hero.subtitle.a": "無料モデルが含まれています。また、任意のプロバイダーの任意のモデルに接続でき、", @@ -249,8 +253,9 @@ export const dict = { "zen.privacy.exceptionsLink": "以下の例外", "go.title": "OpenCode Go | すべての人のための低価格なコーディングモデル", + "go.banner.text": "GPT 5.6 Lunaの利用上限が期間限定で2倍に", "go.meta.description": - "Goは最初の月$5、その後$10/月で、GLM-5.2、GLM-5.1、Kimi K2.7 Code、Kimi K2.6、MiMo-V2.5-Pro、MiMo-V2.5、Qwen3.7 Max、Qwen3.7 Plus、Qwen3.6 Plus、MiniMax M2.7、MiniMax M3、DeepSeek V4 Pro、DeepSeek V4 Flashに対して5時間のゆとりあるリクエスト上限があります。", + "Goは最初の月$5、その後$10/月で、主要なコーディングモデルへのゆとりある利用上限と安定したアクセスを提供します。", "go.hero.title": "すべての人のための低価格なコーディングモデル", "go.hero.body": "Goは、世界中のプログラマーにエージェント型コーディングをもたらします。最も高性能なオープンソースモデルへの十分な制限と安定したアクセスを提供し、コストや可用性を気にすることなく強力なエージェントで構築できます。", @@ -267,7 +272,6 @@ export const dict = { "go.graph.go": "Go", "go.graph.label": "5時間あたりのリクエスト数", "go.graph.usageLimits": "利用制限", - "go.graph.tick": "{{n}}倍", "go.graph.aria": "5時間あたりのリクエスト数: {{free}} 対 {{go}}", "go.testimonials.brand.zen": "Zen", @@ -299,8 +303,7 @@ export const dict = { "go.problem.item1": "低価格なサブスクリプション料金", "go.problem.item2": "十分な制限と安定したアクセス", "go.problem.item3": "できるだけ多くのプログラマーのために構築", - "go.problem.item4": - "GLM-5.2、GLM-5.1、Kimi K2.7 Code、Kimi K2.6、MiMo-V2.5-Pro、MiMo-V2.5、Qwen3.7 Max、Qwen3.7 Plus、Qwen3.6 Plus、MiniMax M2.7、MiniMax M3、DeepSeek V4 Pro、DeepSeek V4 Flashを含む", + "go.problem.item4": "エージェント型コーディング向けにテストされた厳選モデルラインナップ", "go.how.title": "Goの仕組み", "go.how.body": "Goは最初の月$5、その後$10/月で始まります。OpenCodeまたは任意のエージェントで使えます。", "go.how.step1.title": "アカウントを作成", @@ -325,7 +328,7 @@ export const dict = { "go.faq.a2": "Go には、十分な利用上限と安定したアクセスを備えた、以下のモデルが含まれます。", "go.faq.q3": "GoはZenと同じですか?", "go.faq.a3": - "いいえ。Zenは従量課金制ですが、Goは最初の月$5、その後$10/月で始まり、GLM-5.2、GLM-5.1、Kimi K2.7 Code、Kimi K2.6、MiMo-V2.5-Pro、MiMo-V2.5、Qwen3.7 Max、Qwen3.7 Plus、Qwen3.6 Plus、MiniMax M2.7、MiniMax M3、DeepSeek V4 Pro、DeepSeek V4 Flashのオープンソースモデルに対して、ゆとりある上限と信頼できるアクセスを提供します。", + "いいえ。Zenは従量課金制ですが、Goは最初の月$5、その後$10/月で、厳選されたモデルラインナップへのゆとりある上限と安定したアクセスを提供します。", "go.faq.q4": "Goの料金は?", "go.faq.a4.p1.beforePricing": "Goは", "go.faq.a4.p1.pricingLink": "最初の月$5", @@ -334,8 +337,19 @@ export const dict = { "go.faq.a4.p2.accountLink": "アカウント", "go.faq.a4.p3": "いつでもキャンセル可能です。", "go.faq.q5": "データとプライバシーは?", - "go.faq.a5.body": - "このプランは主に海外ユーザー向けに設計されており、米国、EU、シンガポールでホストされたモデルにより安定したグローバルアクセスを提供します。プロバイダーはゼロ保持ポリシーに従い、お客様のデータをモデルのトレーニングに使用しません。", + "go.faq.a5.model": "モデル", + "go.faq.a5.training": "モデルのトレーニング", + "go.faq.a5.retention": "データ保持", + "go.faq.a5.retention30": "30日", + "go.faq.a5.retention0": "0日", + "go.faq.a5.used": "使用あり", + "go.faq.a5.notUsed": "使用なし", + "go.faq.a5.noAgreement": "契約なし", + "go.faq.a5.grokRetention": + "ZDRでは、保存データに依存する重要なAPI機能(ステートフルなResponses API、Files and Collections、Batch APIなど)が無効になります。", + "go.faq.a5.gptRetention": "不正使用監視ログはすべてのAPI機能の使用時に生成され、最大30日間保持されます。", + "go.faq.a5.learnMore": "詳しく見る", + "go.faq.a5.deepseekRetention": "ZDR契約は毎月更新されます。現在の契約は2026年8月31日まで有効です。", "go.faq.a5.beforeExceptions": "Goのモデルは米国でホストされています。プロバイダーはゼロ保持ポリシーに従い、モデルのトレーニングにデータを使用しません(", "go.faq.a5.exceptionsLink": "以下の例外", @@ -349,7 +363,7 @@ export const dict = { "go.faq.q9": "無料モデルとGoの違いは何ですか?", "go.faq.a9": - "無料モデルにはBig Pickleと、その時点で利用可能なプロモーションモデルが含まれ、1日200リクエストの制限があります。GoにはGLM-5.2、GLM-5.1、Kimi K2.7 Code、Kimi K2.6、MiMo-V2.5-Pro、MiMo-V2.5、Qwen3.7 Max、Qwen3.7 Plus、Qwen3.6 Plus、MiniMax M2.7、MiniMax M3、DeepSeek V4 Pro、DeepSeek V4 Flashが含まれ、ローリングウィンドウ(5時間、週間、月間)全体でより高いリクエスト制限が適用されます。これは概算で5時間あたり$12、週間$30、月間$60相当です(実際のリクエスト数はモデルと使用状況により異なります)。", + "無料モデルにはBig Pickleと、その時点で利用可能なプロモーションモデルが含まれ、1日200リクエストの制限があります。Goでは厳選されたモデルラインナップを利用でき、ローリングウィンドウ(5時間、週間、月間)全体でより高いリクエスト制限が適用されます。これは概算で5時間あたり$12、週間$30、月間$60相当です(実際のリクエスト数はモデルと使用状況により異なります)。", "zen.api.error.rateLimitExceeded": "レート制限を超えました。後でもう一度お試しください。", "zen.api.error.modelNotSupported": "モデル {{model}} はサポートされていません", @@ -358,6 +372,7 @@ export const dict = { "zen.api.error.providerNotSupported": "プロバイダー {{provider}} はサポートされていません", "zen.api.error.missingApiKey": "APIキーがありません。", "zen.api.error.invalidApiKey": "無効なAPIキーです。", + "zen.api.error.requestBlockedByUpstreamProvider": "上流プロバイダーによりリクエストがブロックされました。", "zen.api.error.subscriptionQuotaExceeded": "サブスクリプションの制限を超えました。{{retryIn}} 後に再試行してください。", "zen.api.error.goSubscriptionRollingLimitExceeded": @@ -374,7 +389,7 @@ export const dict = { "月額の利用上限 ${{amount}} に達しました。こちらから上限を管理してください: {{membersUrl}}", "zen.api.error.modelDisabled": "モデルが無効です", "zen.api.error.regionNotAllowed": - "このモデルは中国でホストされています。このモデルを使用したい場合は、設定で有効にしてください: {{consoleGoUrl}}", + "このモデルの最新バージョンは中国でのみホスト提供されており、利用するには明示的なオプトインが必要です: {{consoleGoUrl}}", "zen.api.error.trialEnded": "{{model}} の無料プロモーションは終了しました。OpenCode Go を購読するとモデルを引き続き使用できます - {{link}}", @@ -667,7 +682,7 @@ export const dict = { "workspace.lite.promo.price": "初月$5", "workspace.lite.promo.modelsTitle": "含まれるもの", "workspace.lite.promo.footer": - "このプランは主にグローバルユーザー向けに設計されており、米国、EU、シンガポールでホストされたモデルにより安定したグローバルアクセスを提供します。料金と利用制限は、初期の利用状況やフィードバックに基づいて変更される可能性があります。", + "このプランは主に海外のユーザー向けに設計されており、世界中から安定してご利用いただけます。料金と利用上限は、初期の利用状況やフィードバックを踏まえて変更される場合があります。", "workspace.lite.promo.subscribe": "Goを購読する", "workspace.lite.promo.subscribing": "リダイレクト中...", "workspace.lite.promo.otherMethods": "その他の支払い方法", @@ -708,11 +723,11 @@ export const dict = { "download.title": "OpenCode | ダウンロード", "download.meta.description": "OpenCode を macOS、Windows、Linux 向けにダウンロード", - "download.hero.title": "OpenCode をダウンロード", + "download.hero.title": "OpenCode デスクトップ版をダウンロード", "download.hero.subtitle": "macOS、Windows、Linux 向けベータ版を利用可能", "download.hero.button": "{{os}} 向けダウンロード", "download.section.terminal": "OpenCode Terminal", - "download.section.desktop": "OpenCode Desktop (Beta)", + "download.section.desktop": "OpenCode Desktop", "download.section.extensions": "OpenCode Extensions", "download.section.integrations": "OpenCode Integrations", "download.action.download": "ダウンロード", diff --git a/packages/console/app/src/i18n/ko.ts b/packages/console/app/src/i18n/ko.ts index 0245d0c970..ab76958985 100644 --- a/packages/console/app/src/i18n/ko.ts +++ b/packages/console/app/src/i18n/ko.ts @@ -102,10 +102,14 @@ export const dict = { "temp.logoDarkAlt": "opencode 어두운 로고", "home.banner.badge": "신규", - "home.banner.text": "데스크톱 앱 베타 버전 출시", - "home.banner.platforms": "macOS, Windows, Linux 지원", + "home.banner.text": "데스크톱 탭을 소개합니다.", + "home.banner.platforms": "macOS, Windows, Linux에서 사용할 수 있습니다", "home.banner.downloadNow": "지금 다운로드", "home.banner.downloadBetaNow": "데스크톱 베타 다운로드", + "home.promo.title": "데스크톱 탭을 소개합니다", + "home.promo.body": "탭으로 작업과 활성 세션을 정리하세요.", + "home.promo.cta": "최신 버전을 다운로드하여 시작하세요.", + "home.promo.close": "데스크톱 앱 안내 닫기", "home.hero.title": "오픈 소스 AI 코딩 에이전트", "home.hero.subtitle.a": "무료 모델이 포함되어 있으며, 어떤 제공자의 모델이든 연결 가능합니다.", @@ -246,8 +250,9 @@ export const dict = { "zen.privacy.exceptionsLink": "다음 예외", "go.title": "OpenCode Go | 모두를 위한 저비용 코딩 모델", + "go.banner.text": "GPT 5.6 Luna 사용 한도가 한시적으로 2배 확대됩니다", "go.meta.description": - "Go는 첫 달 $5, 이후 $10/월로 시작하며, GLM-5.2, GLM-5.1, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, DeepSeek V4 Flash에 대해 넉넉한 5시간 요청 한도를 제공합니다.", + "Go는 첫 달 $5, 이후 $10/월로 시작하며, 넉넉한 사용 한도와 주요 코딩 모델에 대한 안정적인 액세스를 제공합니다.", "go.hero.title": "모두를 위한 저비용 코딩 모델", "go.hero.body": "Go는 전 세계 프로그래머들에게 에이전트 코딩을 제공합니다. 가장 유능한 오픈 소스 모델에 대한 넉넉한 한도와 안정적인 액세스를 제공하므로, 비용이나 가용성 걱정 없이 강력한 에이전트로 빌드할 수 있습니다.", @@ -264,7 +269,6 @@ export const dict = { "go.graph.go": "Go", "go.graph.label": "5시간당 요청 수", "go.graph.usageLimits": "사용 한도", - "go.graph.tick": "{{n}}배", "go.graph.aria": "5시간당 요청 수: {{free}} 대 {{go}}", "go.testimonials.brand.zen": "Zen", @@ -296,8 +300,7 @@ export const dict = { "go.problem.item1": "저렴한 구독 가격", "go.problem.item2": "넉넉한 한도와 안정적인 액세스", "go.problem.item3": "가능한 한 많은 프로그래머를 위해 제작됨", - "go.problem.item4": - "GLM-5.2, GLM-5.1, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, DeepSeek V4 Flash 포함", + "go.problem.item4": "에이전트 코딩용으로 테스트된 엄선된 모델 라인업", "go.how.title": "Go 작동 방식", "go.how.body": "Go는 첫 달 $5, 이후 $10/월로 시작합니다. OpenCode 또는 어떤 에이전트와도 함께 사용할 수 있습니다.", "go.how.step1.title": "계정 생성", @@ -321,7 +324,7 @@ export const dict = { "go.faq.a2": "Go에는 넉넉한 한도와 안정적인 액세스를 제공하는 아래 모델이 포함됩니다.", "go.faq.q3": "Go는 Zen과 같은가요?", "go.faq.a3": - "아니요. Zen은 종량제인 반면, Go는 첫 달 $5, 이후 $10/월로 시작하며, GLM-5.2, GLM-5.1, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, DeepSeek V4 Flash 오픈 소스 모델에 대한 넉넉한 한도와 안정적인 액세스를 제공합니다.", + "아니요. Zen은 종량제인 반면, Go는 첫 달 $5, 이후 $10/월로 시작하며, 엄선된 모델 라인업에 대한 넉넉한 한도와 안정적인 액세스를 제공합니다.", "go.faq.q4": "Go 비용은 얼마인가요?", "go.faq.a4.p1.beforePricing": "Go 비용은", "go.faq.a4.p1.pricingLink": "첫 달 $5", @@ -330,8 +333,19 @@ export const dict = { "go.faq.a4.p2.accountLink": "계정", "go.faq.a4.p3": "언제든지 취소할 수 있습니다.", "go.faq.q5": "데이터와 프라이버시는 어떤가요?", - "go.faq.a5.body": - "이 플랜은 주로 글로벌 사용자를 위해 설계되었으며, 안정적인 글로벌 액세스를 위해 미국, EU, 싱가포르에 모델이 호스팅되어 있습니다. 당사의 제공자들은 데이터 보존 금지 정책을 따르며 모델 학습에 데이터를 사용하지 않습니다.", + "go.faq.a5.model": "모델", + "go.faq.a5.training": "모델 학습", + "go.faq.a5.retention": "데이터 보존", + "go.faq.a5.retention30": "30일", + "go.faq.a5.retention0": "0일", + "go.faq.a5.used": "사용됨", + "go.faq.a5.notUsed": "사용되지 않음", + "go.faq.a5.noAgreement": "합의 없음", + "go.faq.a5.grokRetention": + "ZDR은 저장된 데이터에 의존하는 중요한 API 기능(상태 저장형 Responses API, Files and Collections, Batch API 포함)을 비활성화합니다.", + "go.faq.a5.gptRetention": "모든 API 기능 사용에 대해 악용 모니터링 로그가 생성되며 최대 30일 동안 보존됩니다.", + "go.faq.a5.learnMore": "자세히 알아보기", + "go.faq.a5.deepseekRetention": "ZDR 계약은 매월 갱신됩니다. 현재 계약은 2026년 8월 31일까지 유효합니다.", "go.faq.a5.beforeExceptions": "Go 모델은 미국에서 호스팅됩니다. 제공자들은 데이터 보존 금지 정책을 따르며 모델 학습에 데이터를 사용하지 않습니다. 단,", "go.faq.a5.exceptionsLink": "다음 예외", @@ -344,7 +358,7 @@ export const dict = { "go.faq.q9": "무료 모델과 Go의 차이점은 무엇인가요?", "go.faq.a9": - "무료 모델에는 Big Pickle과 당시 사용 가능한 프로모션 모델이 포함되며, 하루 200회 요청 할당량이 적용됩니다. Go는 GLM-5.2, GLM-5.1, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, DeepSeek V4 Flash를 포함하며, 롤링 윈도우(5시간, 주간, 월간)에 걸쳐 더 높은 요청 할당량을 적용합니다. 이는 대략 5시간당 $12, 주당 $30, 월 $60에 해당합니다(실제 요청 수는 모델 및 사용량에 따라 다름).", + "무료 모델에는 Big Pickle과 당시 사용 가능한 프로모션 모델이 포함되며, 하루 200회 요청 할당량이 적용됩니다. Go는 엄선된 모델 라인업을 제공하며, 롤링 윈도우(5시간, 주간, 월간)에 걸쳐 더 높은 요청 할당량을 적용합니다. 이는 대략 5시간당 $12, 주당 $30, 월 $60에 해당합니다(실제 요청 수는 모델 및 사용량에 따라 다름).", "zen.api.error.rateLimitExceeded": "속도 제한을 초과했습니다. 나중에 다시 시도해 주세요.", "zen.api.error.modelNotSupported": "{{model}} 모델은 지원되지 않습니다", @@ -353,6 +367,7 @@ export const dict = { "zen.api.error.providerNotSupported": "{{provider}} 제공자는 지원되지 않습니다", "zen.api.error.missingApiKey": "API 키가 누락되었습니다.", "zen.api.error.invalidApiKey": "유효하지 않은 API 키입니다.", + "zen.api.error.requestBlockedByUpstreamProvider": "상위 제공자가 요청을 차단했습니다.", "zen.api.error.subscriptionQuotaExceeded": "구독 할당량을 초과했습니다. {{retryIn}} 후 다시 시도해 주세요.", "zen.api.error.goSubscriptionRollingLimitExceeded": "5시간 사용 한도에 도달했습니다. {{retryIn}} 후 초기화됩니다. 이 모델을 지금 계속 사용하려면 사용 가능한 잔액에서 사용을 활성화하세요: {{consoleGoUrl}}", @@ -368,7 +383,7 @@ export const dict = { "월간 지출 한도인 ${{amount}}에 도달했습니다. 한도 관리를 여기서 하세요: {{membersUrl}}", "zen.api.error.modelDisabled": "모델이 비활성화되었습니다", "zen.api.error.regionNotAllowed": - "이 모델은 중국에서 호스팅됩니다. 이 모델을 사용하려면 설정에서 활성화하세요: {{consoleGoUrl}}", + "이 모델의 최신 버전은 중국에서 호스팅되는 경우에만 사용할 수 있으며, 명시적으로 사용에 동의해야 합니다: {{consoleGoUrl}}", "zen.api.error.trialEnded": "{{model}}의 무료 프로모션이 종료되었습니다. OpenCode Go를 구독하면 모델을 계속 사용할 수 있습니다 - {{link}}", @@ -659,7 +674,7 @@ export const dict = { "workspace.lite.promo.price": "첫 달 $5", "workspace.lite.promo.modelsTitle": "포함 내역", "workspace.lite.promo.footer": - "이 플랜은 주로 글로벌 사용자를 위해 설계되었으며, 안정적인 글로벌 액세스를 위해 미국, EU 및 싱가포르에 모델이 호스팅되어 있습니다. 가격 및 사용 한도는 초기 사용을 통해 학습하고 피드백을 수집함에 따라 변경될 수 있습니다.", + "이 플랜은 주로 해외 사용자를 위해 설계되었으며, 전 세계에서 안정적으로 이용할 수 있습니다. 초기 이용 현황과 피드백을 반영하는 과정에서 가격과 사용 한도가 변경될 수 있습니다.", "workspace.lite.promo.subscribe": "Go 구독하기", "workspace.lite.promo.subscribing": "리디렉션 중...", "workspace.lite.promo.otherMethods": "기타 결제 수단", @@ -700,11 +715,11 @@ export const dict = { "download.title": "OpenCode | 다운로드", "download.meta.description": "macOS, Windows, Linux용 OpenCode 다운로드", - "download.hero.title": "OpenCode 다운로드", + "download.hero.title": "OpenCode 데스크톱 다운로드", "download.hero.subtitle": "macOS, Windows, Linux용 베타 버전 사용 가능", "download.hero.button": "{{os}}용 다운로드", "download.section.terminal": "OpenCode 터미널", - "download.section.desktop": "OpenCode 데스크톱 (베타)", + "download.section.desktop": "OpenCode 데스크톱", "download.section.extensions": "OpenCode 확장 프로그램", "download.section.integrations": "OpenCode 통합", "download.action.download": "다운로드", diff --git a/packages/console/app/src/i18n/no.ts b/packages/console/app/src/i18n/no.ts index bc2f9a935f..5e7e025032 100644 --- a/packages/console/app/src/i18n/no.ts +++ b/packages/console/app/src/i18n/no.ts @@ -102,10 +102,14 @@ export const dict = { "temp.logoDarkAlt": "opencode logo mørk", "home.banner.badge": "Ny", - "home.banner.text": "Desktop-app tilgjengelig i beta", - "home.banner.platforms": "på macOS, Windows og Linux", + "home.banner.text": "Vi introduserer Tabs for Desktop.", + "home.banner.platforms": "Tilgjengelig på macOS, Windows og Linux", "home.banner.downloadNow": "Last ned nå", "home.banner.downloadBetaNow": "Last ned desktop-betaen nå", + "home.promo.title": "Vi introduserer Tabs for Desktop", + "home.promo.body": "Organiser arbeidet ditt og aktive økter med faner.", + "home.promo.cta": "Last ned den nyeste versjonen for å komme i gang.", + "home.promo.close": "Lukk kunngjøringen om Desktop-appen", "home.hero.title": "Den åpne kildekode AI-kodingsagenten", "home.hero.subtitle.a": @@ -250,8 +254,9 @@ export const dict = { "zen.privacy.exceptionsLink": "følgende unntak", "go.title": "OpenCode Go | Rimelige kodemodeller for alle", + "go.banner.text": "GPT 5.6 Luna får 2x bruksgrense i en begrenset periode", "go.meta.description": - "Go starter på $5 for den første måneden, deretter $10/måned, med sjenerøse 5-timers forespørselsgrenser for GLM-5.2, GLM-5.1, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro og DeepSeek V4 Flash.", + "Go starter på $5 for den første måneden, deretter $10/måned, med sjenerøse bruksgrenser og pålitelig tilgang til ledende kodemodeller.", "go.hero.title": "Rimelige kodemodeller for alle", "go.hero.body": "Go bringer agent-koding til programmerere over hele verden. Med rause grenser og pålitelig tilgang til de mest kapable åpen kildekode-modellene, kan du bygge med kraftige agenter uten å bekymre deg for kostnader eller tilgjengelighet.", @@ -268,7 +273,6 @@ export const dict = { "go.graph.go": "Go", "go.graph.label": "Forespørsler per 5 timer", "go.graph.usageLimits": "Bruksgrenser", - "go.graph.tick": "{{n}}x", "go.graph.aria": "Forespørsler per 5t: {{free}} vs {{go}}", "go.testimonials.brand.zen": "Zen", @@ -299,8 +303,7 @@ export const dict = { "go.problem.item1": "Rimelig abonnementspris", "go.problem.item2": "Rause grenser og pålitelig tilgang", "go.problem.item3": "Bygget for så mange programmerere som mulig", - "go.problem.item4": - "Inkluderer GLM-5.2, GLM-5.1, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro og DeepSeek V4 Flash", + "go.problem.item4": "Et kuratert modellutvalg testet for agent-koding", "go.how.title": "Hvordan Go fungerer", "go.how.body": "Go starter på $5 for den første måneden, deretter $10/måned. Du kan bruke det med OpenCode eller hvilken som helst agent.", @@ -326,7 +329,7 @@ export const dict = { "go.faq.a2": "Go inkluderer modellene nedenfor, med høye grenser og pålitelig tilgang.", "go.faq.q3": "Er Go det samme som Zen?", "go.faq.a3": - "Nei. Zen er betaling etter bruk, mens Go starter på $5 for den første måneden, deretter $10/måned, med sjenerøse grenser og pålitelig tilgang til åpen kildekode-modellene GLM-5.2, GLM-5.1, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro og DeepSeek V4 Flash.", + "Nei. Zen er betaling etter bruk, mens Go starter på $5 for den første måneden, deretter $10/måned, med sjenerøse grenser og pålitelig tilgang til et kuratert modellutvalg.", "go.faq.q4": "Hva koster Go?", "go.faq.a4.p1.beforePricing": "Go koster", "go.faq.a4.p1.pricingLink": "$5 første måned", @@ -335,8 +338,22 @@ export const dict = { "go.faq.a4.p2.accountLink": "konto", "go.faq.a4.p3": "Avslutt når som helst.", "go.faq.q5": "Hva med data og personvern?", - "go.faq.a5.body": - "Planen er primært designet for internasjonale brukere, med modeller driftet i USA, EU og Singapore for stabil global tilgang. Våre leverandører følger en policy om null oppbevaring og bruker ikke dataene dine til modelltrening.", + "go.faq.a5.model": "Modell", + "go.faq.a5.training": "Modelltrening", + "go.faq.a5.retention": "Dataoppbevaring", + "go.faq.a5.retention30": "30 dager", + "go.faq.a5.retention0": "0 dager", + "go.faq.a5.used": "Brukes", + "go.faq.a5.notUsed": "Brukes ikke", + "go.faq.a5.noAgreement": "Ingen avtale", + "go.faq.a5.grokRetention": + "ZDR deaktiverer viktige API-funksjoner som er avhengige av lagrede data, inkludert den tilstandsbaserte Responses API, Files and Collections og Batch API.", + "go.faq.a5.gptRetention": + "Logger for overvåking av misbruk genereres for all bruk av API-funksjoner og oppbevares i opptil 30 dager.", + "go.faq.a5.learnMore": "Les mer", + "go.faq.a5.deepseekRetention": + "ZDR-avtalen fornyes månedlig. Den gjeldende avtalen er gyldig til og med 31. august 2026.", + "go.faq.a5.beforeExceptions": "Go-modeller hostes i USA. Leverandører følger en policy om null oppbevaring og bruker ikke dataene dine til modelltrening, med", "go.faq.a5.exceptionsLink": "følgende unntak", @@ -350,7 +367,7 @@ export const dict = { "go.faq.q9": "Hva er forskjellen mellom gratis modeller og Go?", "go.faq.a9": - "Gratis modeller inkluderer Big Pickle pluss kampanjemodeller tilgjengelig på det tidspunktet, med en kvote på 200 forespørsler/dag. Go inkluderer GLM-5.2, GLM-5.1, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro og DeepSeek V4 Flash med høyere kvoter håndhevet over rullerende vinduer (5 timer, ukentlig og månedlig), omtrent tilsvarende $12 per 5 timer, $30 per uke og $60 per måned (faktiske forespørselsantall varierer etter modell og bruk).", + "Gratis modeller inkluderer Big Pickle pluss kampanjemodeller som er tilgjengelige på det tidspunktet, med en kvote på 200 forespørsler/dag. Go tilbyr et kuratert modellutvalg med høyere forespørselskvoter som håndheves over rullerende vinduer (5 timer, ukentlig og månedlig), omtrent tilsvarende $12 per 5 timer, $30 per uke og $60 per måned (faktiske forespørselsantall varierer etter modell og bruk).", "zen.api.error.rateLimitExceeded": "Rate limit overskredet. Vennligst prøv igjen senere.", "zen.api.error.modelNotSupported": "Modell {{model}} støttes ikke", @@ -359,6 +376,7 @@ export const dict = { "zen.api.error.providerNotSupported": "Leverandør {{provider}} støttes ikke", "zen.api.error.missingApiKey": "Mangler API-nøkkel.", "zen.api.error.invalidApiKey": "Ugyldig API-nøkkel.", + "zen.api.error.requestBlockedByUpstreamProvider": "Forespørselen ble blokkert av leverandøren.", "zen.api.error.subscriptionQuotaExceeded": "Abonnementskvote overskredet. Prøv igjen om {{retryIn}}.", "zen.api.error.goSubscriptionRollingLimitExceeded": "5-timers bruksgrense nådd. Tilbakestilles om {{retryIn}}. For å fortsette å bruke denne modellen nå, aktiver bruk fra din tilgjengelige saldo: {{consoleGoUrl}}", @@ -374,7 +392,7 @@ export const dict = { "Du har nådd din månedlige utgiftsgrense på ${{amount}}. Administrer grensene dine her: {{membersUrl}}", "zen.api.error.modelDisabled": "Modellen er deaktivert", "zen.api.error.regionNotAllowed": - "Denne modellen hostes i Kina. Hvis du vil bruke denne modellen, aktiver den i innstillingene dine: {{consoleGoUrl}}", + "Den nyeste versjonen av denne modellen er bare tilgjengelig som en tjeneste driftet i Kina og krever at du uttrykkelig samtykker: {{consoleGoUrl}}", "zen.api.error.trialEnded": "Den gratis kampanjen for {{model}} er avsluttet. Du kan fortsette å bruke modellen ved å abonnere på OpenCode Go - {{link}}", @@ -666,7 +684,7 @@ export const dict = { "workspace.lite.promo.price": "$5 for den første måneden", "workspace.lite.promo.modelsTitle": "Hva som er inkludert", "workspace.lite.promo.footer": - "Planen er primært designet for internasjonale brukere, med modeller driftet i USA, EU og Singapore for stabil global tilgang. Priser og bruksgrenser kan endres etter hvert som vi lærer fra tidlig bruk og tilbakemeldinger.", + "Planen er primært utviklet for internasjonale brukere og gir stabil global tilgang. Priser og bruksgrenser kan endres etter hvert som vi lærer av tidlig bruk og tilbakemeldinger.", "workspace.lite.promo.subscribe": "Abonner på Go", "workspace.lite.promo.subscribing": "Omdirigerer...", "workspace.lite.promo.otherMethods": "Andre betalingsmetoder", @@ -707,11 +725,11 @@ export const dict = { "download.title": "OpenCode | Last ned", "download.meta.description": "Last ned OpenCode for macOS, Windows og Linux", - "download.hero.title": "Last ned OpenCode", + "download.hero.title": "Last ned OpenCode Desktop", "download.hero.subtitle": "Tilgjengelig i beta for macOS, Windows og Linux", "download.hero.button": "Last ned for {{os}}", "download.section.terminal": "OpenCode Terminal", - "download.section.desktop": "OpenCode Desktop (Beta)", + "download.section.desktop": "OpenCode Desktop", "download.section.extensions": "OpenCode Extensions", "download.section.integrations": "OpenCode Integrations", "download.action.download": "Last ned", diff --git a/packages/console/app/src/i18n/pl.ts b/packages/console/app/src/i18n/pl.ts index 6d7df39d2b..2aa536cd82 100644 --- a/packages/console/app/src/i18n/pl.ts +++ b/packages/console/app/src/i18n/pl.ts @@ -101,10 +101,14 @@ export const dict = { "temp.logoDarkAlt": "ciemne logo opencode", "home.banner.badge": "Nowość", - "home.banner.text": "Aplikacja desktopowa dostępna w wersji beta", - "home.banner.platforms": "na macOS, Windows i Linux", + "home.banner.text": "Przedstawiamy karty w aplikacji desktopowej.", + "home.banner.platforms": "Dostępne na macOS, Windows i Linux", "home.banner.downloadNow": "Pobierz teraz", "home.banner.downloadBetaNow": "Pobierz betę wersji desktopowej", + "home.promo.title": "Przedstawiamy karty w aplikacji desktopowej", + "home.promo.body": "Organizuj swoją pracę i aktywne sesje za pomocą kart.", + "home.promo.cta": "Pobierz najnowszą wersję, aby rozpocząć.", + "home.promo.close": "Zamknij ogłoszenie o aplikacji desktopowej", "home.hero.title": "Open source'owy agent AI do kodowania", "home.hero.subtitle.a": "Darmowe modele w zestawie lub podłącz dowolny model od dowolnego dostawcy,", @@ -251,8 +255,9 @@ export const dict = { "zen.privacy.exceptionsLink": "następującymi wyjątkami", "go.title": "OpenCode Go | Niskokosztowe modele do kodowania dla każdego", + "go.banner.text": "GPT 5.6 Luna oferuje 2x wyższe limity użycia przez ograniczony czas", "go.meta.description": - "Go zaczyna się od $5 za pierwszy miesiąc, potem $10/miesiąc, z hojnymi 5-godzinnymi limitami zapytań dla GLM-5.2, GLM-5.1, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro i DeepSeek V4 Flash.", + "Go kosztuje $5 za pierwszy miesiąc, a następnie $10/miesiąc, oferując hojne limity użycia i niezawodny dostęp do wiodących modeli do kodowania.", "go.hero.title": "Niskokosztowe modele do kodowania dla każdego", "go.hero.body": "Go udostępnia programowanie z agentami programistom na całym świecie. Oferuje hojne limity i niezawodny dostęp do najzdolniejszych modeli open source, dzięki czemu możesz budować za pomocą potężnych agentów, nie martwiąc się o koszty czy dostępność.", @@ -269,7 +274,6 @@ export const dict = { "go.graph.go": "Go", "go.graph.label": "Żądania na 5 godzin", "go.graph.usageLimits": "Limity użycia", - "go.graph.tick": "{{n}}x", "go.graph.aria": "Żądania na 5h: {{free}} vs {{go}}", "go.testimonials.brand.zen": "Zen", @@ -300,8 +304,7 @@ export const dict = { "go.problem.item1": "Niskokosztowa cena subskrypcji", "go.problem.item2": "Hojne limity i niezawodny dostęp", "go.problem.item3": "Stworzony dla jak największej liczby programistów", - "go.problem.item4": - "Zawiera GLM-5.2, GLM-5.1, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro i DeepSeek V4 Flash", + "go.problem.item4": "Starannie dobrany zestaw modeli przetestowanych pod kątem kodowania z agentami", "go.how.title": "Jak działa Go", "go.how.body": "Go zaczyna się od $5 za pierwszy miesiąc, potem $10/miesiąc. Możesz go używać z OpenCode lub dowolnym agentem.", @@ -327,7 +330,7 @@ export const dict = { "go.faq.a2": "Go obejmuje poniższe modele z wysokimi limitami i niezawodnym dostępem.", "go.faq.q3": "Czy Go to to samo co Zen?", "go.faq.a3": - "Nie. Zen to model płatności za użycie, podczas gdy Go zaczyna się od $5 za pierwszy miesiąc, potem $10/miesiąc, z hojnymi limitami i niezawodnym dostępem do modeli open source GLM-5.2, GLM-5.1, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro i DeepSeek V4 Flash.", + "Nie. Zen działa w modelu płatności za użycie, natomiast Go kosztuje $5 za pierwszy miesiąc, a następnie $10/miesiąc, oferując hojne limity i niezawodny dostęp do starannie dobranego zestawu modeli.", "go.faq.q4": "Ile kosztuje Go?", "go.faq.a4.p1.beforePricing": "Go kosztuje", "go.faq.a4.p1.pricingLink": "$5 za pierwszy miesiąc", @@ -336,8 +339,21 @@ export const dict = { "go.faq.a4.p2.accountLink": "koncie", "go.faq.a4.p3": "Anuluj w dowolnym momencie.", "go.faq.q5": "A co z danymi i prywatnością?", - "go.faq.a5.body": - "Plan został zaprojektowany głównie dla użytkowników międzynarodowych, z modelami hostowanymi w USA, UE i Singapurze, aby zapewnić stabilny globalny dostęp. Nasi dostawcy stosują politykę zerowej retencji i nie używają Twoich danych do trenowania modeli.", + "go.faq.a5.model": "Model", + "go.faq.a5.training": "Trenowanie modelu", + "go.faq.a5.retention": "Retencja danych", + "go.faq.a5.retention30": "30 dni", + "go.faq.a5.retention0": "0 dni", + "go.faq.a5.used": "Wykorzystywane", + "go.faq.a5.notUsed": "Niewykorzystywane", + "go.faq.a5.noAgreement": "Brak umowy", + "go.faq.a5.grokRetention": + "ZDR wyłącza ważne funkcje API zależne od przechowywanych danych, w tym stanowy Responses API, Files and Collections oraz Batch API.", + "go.faq.a5.gptRetention": + "Dzienniki monitorowania nadużyć są generowane dla każdego użycia funkcji API i przechowywane przez maksymalnie 30 dni.", + "go.faq.a5.learnMore": "Dowiedz się więcej", + "go.faq.a5.deepseekRetention": "Umowa ZDR jest odnawiana co miesiąc. Obecna umowa obowiązuje do 31 sierpnia 2026 r.", + "go.faq.a5.beforeExceptions": "Modele Go są hostowane w USA. Dostawcy stosują politykę zerowej retencji i nie używają Twoich danych do trenowania modeli, z", "go.faq.a5.exceptionsLink": "następującymi wyjątkami", @@ -351,7 +367,7 @@ export const dict = { "go.faq.q9": "Jaka jest różnica między darmowymi modelami a Go?", "go.faq.a9": - "Darmowe modele obejmują Big Pickle oraz modele promocyjne dostępne w danym momencie, z limitem 200 zapytań/dzień. Go zawiera GLM-5.2, GLM-5.1, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro i DeepSeek V4 Flash z wyższymi limitami zapytań egzekwowanymi w oknach kroczących (5-godzinnych, tygodniowych i miesięcznych), w przybliżeniu równoważnymi $12 na 5 godzin, $30 tygodniowo i $60 miesięcznie (rzeczywista liczba zapytań zależy od modelu i użycia).", + "Darmowe modele obejmują Big Pickle oraz modele promocyjne dostępne w danym momencie, z limitem 200 zapytań/dzień. Go oferuje starannie dobrany zestaw modeli z wyższymi limitami zapytań egzekwowanymi w oknach kroczących (5-godzinnych, tygodniowych i miesięcznych), odpowiadającymi w przybliżeniu $12 na 5 godzin, $30 tygodniowo i $60 miesięcznie (rzeczywista liczba zapytań zależy od modelu i użycia).", "zen.api.error.rateLimitExceeded": "Przekroczono limit zapytań. Spróbuj ponownie później.", "zen.api.error.modelNotSupported": "Model {{model}} nie jest obsługiwany", @@ -360,6 +376,7 @@ export const dict = { "zen.api.error.providerNotSupported": "Dostawca {{provider}} nie jest obsługiwany", "zen.api.error.missingApiKey": "Brak klucza API.", "zen.api.error.invalidApiKey": "Nieprawidłowy klucz API.", + "zen.api.error.requestBlockedByUpstreamProvider": "Żądanie zablokowane przez dostawcę zewnętrznego.", "zen.api.error.subscriptionQuotaExceeded": "Przekroczono limit subskrypcji. Spróbuj ponownie za {{retryIn}}.", "zen.api.error.goSubscriptionRollingLimitExceeded": "Osiągnięto 5-godzinny limit użycia. Resetuje się za {{retryIn}}. Aby nadal korzystać z tego modelu, włącz użycie z dostępnego salda: {{consoleGoUrl}}", @@ -375,7 +392,7 @@ export const dict = { "Osiągnąłeś swój miesięczny limit wydatków w wysokości ${{amount}}. Zarządzaj swoimi limitami tutaj: {{membersUrl}}", "zen.api.error.modelDisabled": "Model jest wyłączony", "zen.api.error.regionNotAllowed": - "Ten model jest hostowany w Chinach. Jeśli chcesz korzystać z tego modelu, włącz go w swoich ustawieniach: {{consoleGoUrl}}", + "Najnowsza wersja tego modelu jest dostępna wyłącznie jako usługa hostowana w Chinach i wymaga jawnej zgody użytkownika: {{consoleGoUrl}}", "zen.api.error.trialEnded": "Bezpłatna promocja {{model}} dobiegła końca. Możesz dalej korzystać z modelu, subskrybując OpenCode Go - {{link}}", @@ -667,7 +684,7 @@ export const dict = { "workspace.lite.promo.price": "$5 za pierwszy miesiąc", "workspace.lite.promo.modelsTitle": "Co zawiera", "workspace.lite.promo.footer": - "Plan został zaprojektowany głównie dla użytkowników międzynarodowych, z modelami hostowanymi w USA, UE i Singapurze, aby zapewnić stabilny globalny dostęp. Ceny i limity użycia mogą ulec zmianie w miarę analizy wczesnego użycia i zbierania opinii.", + "Plan został opracowany przede wszystkim z myślą o użytkownikach z całego świata i zapewnia stabilny globalny dostęp. Ceny i limity użycia mogą ulec zmianie w miarę zdobywania doświadczeń na podstawie początkowego korzystania z usługi i otrzymywanych opinii.", "workspace.lite.promo.subscribe": "Subskrybuj Go", "workspace.lite.promo.subscribing": "Przekierowywanie...", "workspace.lite.promo.otherMethods": "Inne metody płatności", @@ -708,11 +725,11 @@ export const dict = { "download.title": "OpenCode | Pobierz", "download.meta.description": "Pobierz OpenCode na macOS, Windows i Linux", - "download.hero.title": "Pobierz OpenCode", + "download.hero.title": "Pobierz OpenCode Desktop", "download.hero.subtitle": "Dostępne w wersji Beta na macOS, Windows i Linux", "download.hero.button": "Pobierz na {{os}}", "download.section.terminal": "Terminal OpenCode", - "download.section.desktop": "Pulpit OpenCode (Beta)", + "download.section.desktop": "Pulpit OpenCode", "download.section.extensions": "Rozszerzenia OpenCode", "download.section.integrations": "Integracje OpenCode", "download.action.download": "Pobierz", diff --git a/packages/console/app/src/i18n/ru.ts b/packages/console/app/src/i18n/ru.ts index 90d3c6c8c5..330713cd19 100644 --- a/packages/console/app/src/i18n/ru.ts +++ b/packages/console/app/src/i18n/ru.ts @@ -102,10 +102,14 @@ export const dict = { "temp.logoDarkAlt": "темный логотип opencode", "home.banner.badge": "Новое", - "home.banner.text": "Доступно десктопное приложение (бета)", - "home.banner.platforms": "на macOS, Windows и Linux", + "home.banner.text": "Представляем вкладки в десктопном приложении.", + "home.banner.platforms": "Доступно на macOS, Windows и Linux", "home.banner.downloadNow": "Скачать", "home.banner.downloadBetaNow": "Скачать бету для десктопа", + "home.promo.title": "Представляем вкладки в десктопном приложении", + "home.promo.body": "Организуйте работу и активные сессии с помощью вкладок.", + "home.promo.cta": "Скачайте последнюю версию, чтобы начать.", + "home.promo.close": "Закрыть объявление о десктопном приложении", "home.hero.title": "AI-агент с открытым кодом для программирования", "home.hero.subtitle.a": "Бесплатные модели включены, или подключите любую модель от любого провайдера,", @@ -254,8 +258,9 @@ export const dict = { "zen.privacy.exceptionsLink": "следующими исключениями", "go.title": "OpenCode Go | Недорогие модели для кодинга для всех", + "go.banner.text": "GPT 5.6 Luna получает 2x лимиты использования на ограниченное время", "go.meta.description": - "Go начинается с $5 за первый месяц, затем $10/месяц, с щедрыми лимитами запросов за 5 часов для GLM-5.2, GLM-5.1, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro и DeepSeek V4 Flash.", + "Go стоит $5 за первый месяц, затем $10/месяц и предлагает щедрые лимиты использования и надежный доступ к ведущим моделям для кодинга.", "go.hero.title": "Недорогие модели для кодинга для всех", "go.hero.body": "Go открывает доступ к агентам-программистам разработчикам по всему миру. Предлагая щедрые лимиты и надежный доступ к наиболее способным моделям с открытым исходным кодом, вы можете создавать проекты с мощными агентами, не беспокоясь о затратах или доступности.", @@ -272,7 +277,6 @@ export const dict = { "go.graph.go": "Go", "go.graph.label": "Запросов за 5 часов", "go.graph.usageLimits": "Лимиты использования", - "go.graph.tick": "{{n}}x", "go.graph.aria": "Запросов за 5ч: {{free}} против {{go}}", "go.testimonials.brand.zen": "Zen", @@ -304,8 +308,7 @@ export const dict = { "go.problem.item1": "Недорогая подписка", "go.problem.item2": "Щедрые лимиты и надежный доступ", "go.problem.item3": "Создан для максимального числа программистов", - "go.problem.item4": - "Включает GLM-5.2, GLM-5.1, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro и DeepSeek V4 Flash", + "go.problem.item4": "Отобранные модели, протестированные для агентного программирования", "go.how.title": "Как работает Go", "go.how.body": "Go начинается с $5 за первый месяц, затем $10/месяц. Вы можете использовать его с OpenCode или любым агентом.", @@ -331,7 +334,7 @@ export const dict = { "go.faq.a2": "Go включает перечисленные ниже модели с щедрыми лимитами и надежным доступом.", "go.faq.q3": "Go — это то же самое, что и Zen?", "go.faq.a3": - "Нет. Zen - это оплата по мере использования, в то время как Go начинается с $5 за первый месяц, затем $10/месяц, с щедрыми лимитами и надежным доступом к моделям с открытым исходным кодом GLM-5.2, GLM-5.1, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro и DeepSeek V4 Flash.", + "Нет. Zen оплачивается по мере использования, а Go стоит $5 за первый месяц, затем $10/месяц и предлагает щедрые лимиты и надежный доступ к отобранным моделям.", "go.faq.q4": "Сколько стоит Go?", "go.faq.a4.p1.beforePricing": "Go стоит", "go.faq.a4.p1.pricingLink": "$5 за первый месяц", @@ -340,8 +343,22 @@ export const dict = { "go.faq.a4.p2.accountLink": "аккаунте", "go.faq.a4.p3": "Отмена в любое время.", "go.faq.q5": "Как насчет данных и приватности?", - "go.faq.a5.body": - "План разработан в первую очередь для международных пользователей, с моделями, размещенными в США, ЕС и Сингапуре для стабильного глобального доступа. Наши провайдеры следуют политике нулевого хранения и не используют ваши данные для обучения моделей.", + "go.faq.a5.model": "Модель", + "go.faq.a5.training": "Обучение моделей", + "go.faq.a5.retention": "Хранение данных", + "go.faq.a5.retention30": "30 дней", + "go.faq.a5.retention0": "0 дней", + "go.faq.a5.used": "Используется", + "go.faq.a5.notUsed": "Не используется", + "go.faq.a5.noAgreement": "Нет соглашения", + "go.faq.a5.grokRetention": + "ZDR отключает важные функции API, зависящие от сохраненных данных, включая Responses API с сохранением состояния, Files and Collections и Batch API.", + "go.faq.a5.gptRetention": + "Журналы мониторинга злоупотреблений создаются при любом использовании функций API и хранятся до 30 дней.", + "go.faq.a5.learnMore": "Подробнее", + "go.faq.a5.deepseekRetention": + "Соглашение ZDR продлевается ежемесячно. Текущее соглашение действует до 31 августа 2026 года.", + "go.faq.a5.beforeExceptions": "Модели Go размещены в США. Провайдеры следуют политике нулевого хранения и не используют ваши данные для обучения моделей, за", "go.faq.a5.exceptionsLink": "следующими исключениями", @@ -355,7 +372,7 @@ export const dict = { "go.faq.q9": "В чем разница между бесплатными моделями и Go?", "go.faq.a9": - "Бесплатные модели включают Big Pickle плюс промо-модели, доступные на данный момент, с квотой 200 запросов/день. Go включает GLM-5.2, GLM-5.1, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro и DeepSeek V4 Flash с более высокими квотами запросов, применяемыми в скользящих окнах (5 часов, неделя и месяц), что примерно эквивалентно $12 за 5 часов, $30 в неделю и $60 в месяц (фактическое количество запросов зависит от модели и использования).", + "Бесплатные модели включают Big Pickle и доступные на данный момент промо-модели с квотой 200 запросов/день. Go предлагает набор отобранных моделей с более высокими квотами запросов, применяемыми в скользящих окнах (5 часов, неделя и месяц), что примерно эквивалентно $12 за 5 часов, $30 в неделю и $60 в месяц (фактическое количество запросов зависит от модели и использования).", "zen.api.error.rateLimitExceeded": "Превышен лимит запросов. Пожалуйста, попробуйте позже.", "zen.api.error.modelNotSupported": "Модель {{model}} не поддерживается", @@ -364,6 +381,7 @@ export const dict = { "zen.api.error.providerNotSupported": "Провайдер {{provider}} не поддерживается", "zen.api.error.missingApiKey": "Отсутствует API ключ.", "zen.api.error.invalidApiKey": "Неверный API ключ.", + "zen.api.error.requestBlockedByUpstreamProvider": "Запрос заблокирован вышестоящим провайдером.", "zen.api.error.subscriptionQuotaExceeded": "Квота подписки превышена. Повторите попытку через {{retryIn}}.", "zen.api.error.goSubscriptionRollingLimitExceeded": "Достигнут лимит использования за 5 часов. Сбросится через {{retryIn}}. Чтобы продолжить использовать эту модель сейчас, включите оплату с доступного баланса: {{consoleGoUrl}}", @@ -379,7 +397,7 @@ export const dict = { "Вы достигли ежемесячного лимита расходов в ${{amount}}. Управляйте лимитами здесь: {{membersUrl}}", "zen.api.error.modelDisabled": "Модель отключена", "zen.api.error.regionNotAllowed": - "Эта модель размещена в Китае. Если вы хотите использовать эту модель, включите её в настройках: {{consoleGoUrl}}", + "Последняя версия этой модели размещена только в Китае. Чтобы использовать её, необходимо явно подтвердить согласие: {{consoleGoUrl}}", "zen.api.error.trialEnded": "Бесплатная акция для {{model}} завершена. Вы можете продолжить использование модели, подписавшись на OpenCode Go - {{link}}", @@ -673,7 +691,7 @@ export const dict = { "workspace.lite.promo.price": "$5 за первый месяц", "workspace.lite.promo.modelsTitle": "Что включено", "workspace.lite.promo.footer": - "План предназначен в первую очередь для международных пользователей. Модели размещены в США, ЕС и Сингапуре для стабильного глобального доступа. Цены и лимиты использования могут меняться по мере того, как мы изучаем раннее использование и собираем отзывы.", + "План предназначен в первую очередь для пользователей по всему миру и обеспечивает стабильный глобальный доступ. Цены и лимиты использования могут меняться по мере изучения первых результатов использования и отзывов.", "workspace.lite.promo.subscribe": "Подписаться на Go", "workspace.lite.promo.subscribing": "Перенаправление...", "workspace.lite.promo.otherMethods": "Другие способы оплаты", @@ -716,11 +734,11 @@ export const dict = { "download.title": "OpenCode | Скачать", "download.meta.description": "Скачать OpenCode для macOS, Windows и Linux", - "download.hero.title": "Скачать OpenCode", + "download.hero.title": "Скачать OpenCode Desktop", "download.hero.subtitle": "Доступна бета для macOS, Windows и Linux", "download.hero.button": "Скачать для {{os}}", "download.section.terminal": "OpenCode Terminal", - "download.section.desktop": "OpenCode Desktop (Beta)", + "download.section.desktop": "OpenCode Desktop", "download.section.extensions": "Расширения OpenCode", "download.section.integrations": "Интеграции OpenCode", "download.action.download": "Скачать", diff --git a/packages/console/app/src/i18n/th.ts b/packages/console/app/src/i18n/th.ts index bf7c2e58e4..d3ff91e97a 100644 --- a/packages/console/app/src/i18n/th.ts +++ b/packages/console/app/src/i18n/th.ts @@ -102,10 +102,14 @@ export const dict = { "temp.logoDarkAlt": "โลโก้ opencode แบบมืด", "home.banner.badge": "ใหม่", - "home.banner.text": "แอปเดสก์ท็อปพร้อมใช้งานในเวอร์ชันเบต้า", - "home.banner.platforms": "บน macOS, Windows และ Linux", + "home.banner.text": "ขอแนะนำแท็บสำหรับเดสก์ท็อป.", + "home.banner.platforms": "พร้อมใช้งานบน macOS, Windows และ Linux", "home.banner.downloadNow": "ดาวน์โหลดตอนนี้", "home.banner.downloadBetaNow": "ดาวน์โหลดเบต้าเดสก์ท็อปตอนนี้", + "home.promo.title": "ขอแนะนำแท็บสำหรับเดสก์ท็อป", + "home.promo.body": "จัดระเบียบงานและเซสชันที่ใช้งานอยู่ด้วยแท็บ", + "home.promo.cta": "ดาวน์โหลดเวอร์ชันล่าสุดเพื่อเริ่มต้นใช้งาน", + "home.promo.close": "ปิดประกาศเกี่ยวกับแอปเดสก์ท็อป", "home.hero.title": "เอเจนต์เขียนโค้ดด้วย AI แบบโอเพนซอร์ส", "home.hero.subtitle.a": "มีโมเดลฟรีรวมอยู่ หรือเชื่อมต่อโมเดลใดก็ได้จากผู้ให้บริการรายใดก็ได้,", @@ -249,8 +253,9 @@ export const dict = { "zen.privacy.exceptionsLink": "ข้อยกเว้นดังนี้", "go.title": "OpenCode Go | โมเดลเขียนโค้ดราคาประหยัดสำหรับทุกคน", + "go.banner.text": "GPT 5.6 Luna เพิ่มโควตาการใช้งานเป็น 2 เท่าในช่วงเวลาจำกัด", "go.meta.description": - "Go เริ่มต้นที่ $5 สำหรับเดือนแรก จากนั้น $10/เดือน พร้อมขีดจำกัดคำขอ 5 ชั่วโมงที่เอื้อเฟื้อสำหรับ GLM-5.2, GLM-5.1, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro และ DeepSeek V4 Flash", + "Go เริ่มต้นที่ $5 สำหรับเดือนแรก จากนั้น $10/เดือน พร้อมขีดจำกัดการใช้งานที่เอื้อเฟื้อและการเข้าถึงโมเดลเขียนโค้ดชั้นนำอย่างเชื่อถือได้", "go.hero.title": "โมเดลเขียนโค้ดราคาประหยัดสำหรับทุกคน", "go.hero.body": "Go นำการเขียนโค้ดแบบเอเจนต์มาสู่นักเขียนโปรแกรมทั่วโลก เสนอขีดจำกัดที่กว้างขวางและการเข้าถึงโมเดลโอเพนซอร์สที่มีความสามารถสูงสุดได้อย่างน่าเชื่อถือ เพื่อให้คุณสามารถสร้างสรรค์ด้วยเอเจนต์ที่ทรงพลังโดยไม่ต้องกังวลเรื่องค่าใช้จ่ายหรือความพร้อมใช้งาน", @@ -266,7 +271,6 @@ export const dict = { "go.graph.go": "Go", "go.graph.label": "คำขอต่อ 5 ชั่วโมง", "go.graph.usageLimits": "ขีดจำกัดการใช้งาน", - "go.graph.tick": "{{n}}x", "go.graph.aria": "คำขอต่อ 5 ชม.: {{free}} vs {{go}}", "go.testimonials.brand.zen": "Zen", @@ -297,8 +301,7 @@ export const dict = { "go.problem.item1": "ราคาการสมัครสมาชิกที่ต่ำ", "go.problem.item2": "ขีดจำกัดที่กว้างขวางและการเข้าถึงที่เชื่อถือได้", "go.problem.item3": "สร้างขึ้นเพื่อโปรแกรมเมอร์จำนวนมากที่สุดเท่าที่จะเป็นไปได้", - "go.problem.item4": - "รวมถึง GLM-5.2, GLM-5.1, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro และ DeepSeek V4 Flash", + "go.problem.item4": "ชุดโมเดลที่คัดสรรและผ่านการทดสอบสำหรับการเขียนโค้ดแบบเอเจนต์", "go.how.title": "Go ทำงานอย่างไร", "go.how.body": "Go เริ่มต้นที่ $5 สำหรับเดือนแรก จากนั้น $10/เดือน คุณสามารถใช้กับ OpenCode หรือเอเจนต์ใดก็ได้", "go.how.step1.title": "สร้างบัญชี", @@ -323,7 +326,7 @@ export const dict = { "go.faq.a2": "Go รวมโมเดลด้านล่างนี้ พร้อมขีดจำกัดที่มากและการเข้าถึงที่เชื่อถือได้", "go.faq.q3": "Go เหมือนกับ Zen หรือไม่?", "go.faq.a3": - "ไม่ Zen เป็นแบบจ่ายตามการใช้งาน ในขณะที่ Go เริ่มต้นที่ $5 สำหรับเดือนแรก จากนั้น $10/เดือน พร้อมขีดจำกัดที่เอื้อเฟื้อและการเข้าถึงโมเดลโอเพนซอร์ส GLM-5.2, GLM-5.1, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro และ DeepSeek V4 Flash อย่างเชื่อถือได้", + "ไม่ Zen เป็นแบบจ่ายตามการใช้งาน ขณะที่ Go เริ่มต้นที่ $5 สำหรับเดือนแรก จากนั้น $10/เดือน พร้อมขีดจำกัดที่เอื้อเฟื้อและการเข้าถึงชุดโมเดลที่คัดสรรอย่างเชื่อถือได้", "go.faq.q4": "Go ราคาเท่าไหร่?", "go.faq.a4.p1.beforePricing": "Go ราคา", "go.faq.a4.p1.pricingLink": "$5 เดือนแรก", @@ -332,8 +335,21 @@ export const dict = { "go.faq.a4.p2.accountLink": "บัญชีของคุณ", "go.faq.a4.p3": "ยกเลิกได้ตลอดเวลา", "go.faq.q5": "แล้วเรื่องข้อมูลและความเป็นส่วนตัวล่ะ?", - "go.faq.a5.body": - "แผนนี้ออกแบบมาเพื่อผู้ใช้งานระหว่างประเทศเป็นหลัก โดยมีโมเดลโฮสต์ในสหรัฐอเมริกา สหภาพยุโรป และสิงคโปร์ เพื่อการเข้าถึงทั่วโลกที่เสถียร ผู้ให้บริการของเราปฏิบัติตามนโยบายไม่เก็บรักษาข้อมูลและไม่ใช้ข้อมูลของคุณสำหรับการฝึกโมเดล", + "go.faq.a5.model": "โมเดล", + "go.faq.a5.training": "การฝึกโมเดล", + "go.faq.a5.retention": "การเก็บรักษาข้อมูล", + "go.faq.a5.retention30": "30 วัน", + "go.faq.a5.retention0": "0 วัน", + "go.faq.a5.used": "นำไปใช้", + "go.faq.a5.notUsed": "ไม่นำไปใช้", + "go.faq.a5.noAgreement": "ไม่มีข้อตกลง", + "go.faq.a5.grokRetention": + "ZDR ปิดใช้งานฟีเจอร์ API สำคัญที่ต้องอาศัยข้อมูลที่จัดเก็บไว้ ซึ่งรวมถึง Responses API แบบมีสถานะ, Files and Collections และ Batch API", + "go.faq.a5.gptRetention": + "ระบบจะสร้างบันทึกการตรวจสอบการใช้งานในทางที่ผิดสำหรับการใช้งานฟีเจอร์ API ทั้งหมด และเก็บรักษาไว้นานสูงสุด 30 วัน", + "go.faq.a5.learnMore": "ดูข้อมูลเพิ่มเติม", + "go.faq.a5.deepseekRetention": "ข้อตกลง ZDR จะต่ออายุทุกเดือน ข้อตกลงปัจจุบันมีผลใช้ถึงวันที่ 31 สิงหาคม 2026", + "go.faq.a5.beforeExceptions": "โมเดล Go โฮสต์ในสหรัฐอเมริกา ผู้ให้บริการปฏิบัติตามนโยบายไม่เก็บรักษาข้อมูล (zero-retention policy) และไม่ใช้ข้อมูลของคุณสำหรับการฝึกโมเดล โดยมี", "go.faq.a5.exceptionsLink": "ข้อยกเว้นดังนี้", @@ -346,7 +362,7 @@ export const dict = { "go.faq.q9": "ความแตกต่างระหว่างโมเดลฟรีและ Go คืออะไร?", "go.faq.a9": - "โมเดลฟรีรวมถึง Big Pickle บวกกับโมเดลโปรโมชั่นที่มีให้ในขณะนั้น ด้วยโควต้า 200 คำขอ/วัน Go รวมถึง GLM-5.2, GLM-5.1, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro และ DeepSeek V4 Flash ที่มีโควต้าคำขอสูงกว่า ซึ่งบังคับใช้ผ่านช่วงเวลาหมุนเวียน (5 ชั่วโมง, รายสัปดาห์ และรายเดือน) เทียบเท่าประมาณ $12 ต่อ 5 ชั่วโมง, $30 ต่อสัปดาห์ และ $60 ต่อเดือน (จำนวนคำขอจริงจะแตกต่างกันไปตามโมเดลและการใช้งาน)", + "โมเดลฟรีประกอบด้วย Big Pickle และโมเดลโปรโมชันที่มีให้บริการในขณะนั้น โดยมีโควตา 200 คำขอ/วัน Go นำเสนอชุดโมเดลที่คัดสรร พร้อมโควตาคำขอที่สูงกว่าซึ่งบังคับใช้ตามกรอบเวลาแบบต่อเนื่อง (5 ชั่วโมง, รายสัปดาห์ และรายเดือน) เทียบเท่าประมาณ $12 ต่อ 5 ชั่วโมง, $30 ต่อสัปดาห์ และ $60 ต่อเดือน (จำนวนคำขอจริงแตกต่างกันไปตามโมเดลและการใช้งาน)", "zen.api.error.rateLimitExceeded": "เกินขีดจำกัดอัตราการใช้งาน กรุณาลองใหม่ในภายหลัง", "zen.api.error.modelNotSupported": "ไม่รองรับโมเดล {{model}}", @@ -355,6 +371,7 @@ export const dict = { "zen.api.error.providerNotSupported": "ไม่รองรับผู้ให้บริการ {{provider}}", "zen.api.error.missingApiKey": "ไม่มี API key", "zen.api.error.invalidApiKey": "API key ไม่ถูกต้อง", + "zen.api.error.requestBlockedByUpstreamProvider": "คำขอถูกบล็อกโดยผู้ให้บริการต้นทาง", "zen.api.error.subscriptionQuotaExceeded": "โควต้าการสมัครสมาชิกเกินขีดจำกัด ลองใหม่ในอีก {{retryIn}}", "zen.api.error.goSubscriptionRollingLimitExceeded": "ถึงขีดจำกัดการใช้งานในรอบ 5 ชั่วโมงแล้ว จะรีเซ็ตในอีก {{retryIn}} หากต้องการใช้โมเดลนี้ต่อทันที ให้เปิดใช้งานจากยอดเงินคงเหลือของคุณ: {{consoleGoUrl}}", @@ -370,7 +387,7 @@ export const dict = { "คุณถึงขีดจำกัดการใช้จ่ายรายเดือนที่ ${{amount}} แล้ว จัดการขีดจำกัดของคุณที่นี่: {{membersUrl}}", "zen.api.error.modelDisabled": "โมเดลถูกปิดใช้งาน", "zen.api.error.regionNotAllowed": - "โมเดลนี้โฮสต์อยู่ในประเทศจีน หากคุณต้องการใช้โมเดลนี้ ให้เปิดใช้งานในการตั้งค่าของคุณ: {{consoleGoUrl}}", + "โมเดลเวอร์ชันล่าสุดนี้ให้บริการเฉพาะผ่านระบบที่โฮสต์ในประเทศจีน และต้องให้ความยินยอมอย่างชัดแจ้งก่อนใช้งาน: {{consoleGoUrl}}", "zen.api.error.trialEnded": "โปรโมชันฟรีสำหรับ {{model}} สิ้นสุดแล้ว คุณสามารถใช้โมเดลต่อได้โดยสมัครสมาชิก OpenCode Go - {{link}}", @@ -662,7 +679,7 @@ export const dict = { "workspace.lite.promo.price": "$5 สำหรับเดือนแรก", "workspace.lite.promo.modelsTitle": "สิ่งที่รวมอยู่ด้วย", "workspace.lite.promo.footer": - "แผนนี้ออกแบบมาสำหรับผู้ใช้งานต่างประเทศเป็นหลัก โดยมีโมเดลโฮสต์อยู่ในสหรัฐอเมริกา สหภาพยุโรป และสิงคโปร์ เพื่อการเข้าถึงที่เสถียรทั่วโลก ราคาและขีดจำกัดการใช้งานอาจมีการเปลี่ยนแปลงตามที่เราได้เรียนรู้จากการใช้งานในช่วงแรกและข้อเสนอแนะ", + "แผนนี้ออกแบบมาสำหรับผู้ใช้งานต่างประเทศเป็นหลักและให้การเข้าถึงที่เสถียรทั่วโลก ราคาและขีดจำกัดการใช้งานอาจเปลี่ยนแปลงได้ตามสิ่งที่เราเรียนรู้จากการใช้งานและข้อเสนอแนะในช่วงแรก", "workspace.lite.promo.subscribe": "สมัครสมาชิก Go", "workspace.lite.promo.subscribing": "กำลังเปลี่ยนเส้นทาง...", "workspace.lite.promo.otherMethods": "วิธีการชำระเงินอื่นๆ", @@ -703,11 +720,11 @@ export const dict = { "download.title": "OpenCode | ดาวน์โหลด", "download.meta.description": "ดาวน์โหลด OpenCode สำหรับ macOS, Windows และ Linux", - "download.hero.title": "ดาวน์โหลด OpenCode", + "download.hero.title": "ดาวน์โหลด OpenCode สำหรับเดสก์ท็อป", "download.hero.subtitle": "พร้อมใช้งานในเวอร์ชันเบต้าสำหรับ macOS, Windows และ Linux", "download.hero.button": "ดาวน์โหลดสำหรับ {{os}}", "download.section.terminal": "OpenCode Terminal", - "download.section.desktop": "OpenCode Desktop (Beta)", + "download.section.desktop": "OpenCode Desktop", "download.section.extensions": "ส่วนขยาย OpenCode", "download.section.integrations": "การเชื่อมต่อ OpenCode", "download.action.download": "ดาวน์โหลด", diff --git a/packages/console/app/src/i18n/tr.ts b/packages/console/app/src/i18n/tr.ts index 7bc45ee2e2..5d0d2819dd 100644 --- a/packages/console/app/src/i18n/tr.ts +++ b/packages/console/app/src/i18n/tr.ts @@ -102,10 +102,14 @@ export const dict = { "temp.logoDarkAlt": "opencode koyu logo", "home.banner.badge": "Yeni", - "home.banner.text": "Masaüstü uygulaması beta olarak mevcut", - "home.banner.platforms": "macOS, Windows ve Linux'ta", + "home.banner.text": "Masaüstü uygulamasında sekmelerle tanışın.", + "home.banner.platforms": "macOS, Windows ve Linux'ta kullanılabilir", "home.banner.downloadNow": "Şimdi indir", "home.banner.downloadBetaNow": "Masaüstü betayı şimdi indir", + "home.promo.title": "Masaüstü uygulamasında sekmelerle tanışın", + "home.promo.body": "Çalışmalarınızı ve etkin oturumlarınızı sekmelerle düzenleyin.", + "home.promo.cta": "Başlamak için en son sürümü indirin.", + "home.promo.close": "Masaüstü uygulaması duyurusunu kapat", "home.hero.title": "Açık kaynaklı yapay zeka kodlama ajanı", "home.hero.subtitle.a": "Ücretsiz modeller dahil veya herhangi bir sağlayıcıdan herhangi bir modeli bağlayın,", @@ -252,8 +256,9 @@ export const dict = { "zen.privacy.exceptionsLink": "aşağıdaki istisnalar", "go.title": "OpenCode Go | Herkes için düşük maliyetli kodlama modelleri", + "go.banner.text": "GPT 5.6 Luna sınırlı bir süre için 2x kullanım limiti sunuyor", "go.meta.description": - "Go ilk ay $5, sonrasında ayda 10$ fiyatıyla başlar; GLM-5.2, GLM-5.1, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro ve DeepSeek V4 Flash için cömert 5 saatlik istek limitleri sunar.", + "Go ilk ay $5, sonrasında ayda 10$ fiyatıyla başlar; cömert kullanım limitleri ve önde gelen kodlama modellerine güvenilir erişim sunar.", "go.hero.title": "Herkes için düşük maliyetli kodlama modelleri", "go.hero.body": "Go, dünya çapındaki programcılara ajan tabanlı kodlama getiriyor. En yetenekli açık kaynaklı modellere cömert limitler ve güvenilir erişim sunarak, maliyet veya erişilebilirlik konusunda endişelenmeden güçlü ajanlarla geliştirme yapmanızı sağlar.", @@ -270,7 +275,6 @@ export const dict = { "go.graph.go": "Go", "go.graph.label": "5 saat başına istekler", "go.graph.usageLimits": "Kullanım limitleri", - "go.graph.tick": "{{n}}x", "go.graph.aria": "5 saatlik istekler: {{free}} vs {{go}}", "go.testimonials.brand.zen": "Zen", @@ -302,8 +306,7 @@ export const dict = { "go.problem.item1": "Düşük maliyetli abonelik fiyatlandırması", "go.problem.item2": "Cömert limitler ve güvenilir erişim", "go.problem.item3": "Mümkün olduğunca çok programcı için geliştirildi", - "go.problem.item4": - "GLM-5.2, GLM-5.1, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro ve DeepSeek V4 Flash içerir", + "go.problem.item4": "Ajan tabanlı kodlama için test edilmiş, özenle seçilmiş model seçenekleri", "go.how.title": "Go nasıl çalışır?", "go.how.body": "Go ilk ay $5, sonrasında ayda 10$ fiyatıyla başlar. OpenCode veya herhangi bir ajanla kullanabilirsiniz.", @@ -329,7 +332,7 @@ export const dict = { "go.faq.a2": "Go, aşağıda listelenen modelleri cömert limitler ve güvenilir erişimle sunar.", "go.faq.q3": "Go, Zen ile aynı mı?", "go.faq.a3": - "Hayır. Zen kullandıkça öde modelidir, Go ise ilk ay $5, sonrasında ayda 10$ fiyatıyla başlar; GLM-5.2, GLM-5.1, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro ve DeepSeek V4 Flash açık kaynak modellerine cömert limitler ve güvenilir erişim sunar.", + "Hayır. Zen kullandıkça öde modelidir; Go ise ilk ay $5, sonrasında ayda 10$ fiyatıyla başlar ve özenle seçilmiş model seçeneklerine cömert limitlerle güvenilir erişim sunar.", "go.faq.q4": "Go ne kadar?", "go.faq.a4.p1.beforePricing": "Go'nun maliyeti", "go.faq.a4.p1.pricingLink": "İlk ay $5", @@ -338,8 +341,22 @@ export const dict = { "go.faq.a4.p2.accountLink": "hesabınızdan", "go.faq.a4.p3": "yönetebilirsiniz. İstediğiniz zaman iptal edin.", "go.faq.q5": "Veri ve gizlilik ne olacak?", - "go.faq.a5.body": - "Bu plan öncelikle uluslararası kullanıcılar için tasarlanmış olup, istikrarlı küresel erişim için modeller ABD, AB ve Singapur'da barındırılmaktadır. Sağlayıcılarımız sıfır saklama politikası izler ve verilerinizi model eğitimi için kullanmaz.", + "go.faq.a5.model": "Model", + "go.faq.a5.training": "Model eğitimi", + "go.faq.a5.retention": "Veri saklama", + "go.faq.a5.retention30": "30 gün", + "go.faq.a5.retention0": "0 gün", + "go.faq.a5.used": "Kullanılır", + "go.faq.a5.notUsed": "Kullanılmaz", + "go.faq.a5.noAgreement": "Anlaşma yok", + "go.faq.a5.grokRetention": + "ZDR, durum bilgisi tutan Responses API, Files and Collections ve Batch API dahil olmak üzere saklanan verilere bağlı önemli API özelliklerini devre dışı bırakır.", + "go.faq.a5.gptRetention": + "Tüm API özelliklerinin kullanımı için kötüye kullanım izleme günlükleri oluşturulur ve 30 güne kadar saklanır.", + "go.faq.a5.learnMore": "Daha fazla bilgi", + "go.faq.a5.deepseekRetention": + "ZDR anlaşması aylık olarak yenilenir. Mevcut anlaşma 31 Ağustos 2026 tarihine kadar geçerlidir.", + "go.faq.a5.beforeExceptions": "Go modelleri ABD'de barındırılmaktadır. Sağlayıcılar sıfır saklama politikası izler ve verilerinizi model eğitimi için kullanmaz; şu", "go.faq.a5.exceptionsLink": "aşağıdaki istisnalar", @@ -353,7 +370,7 @@ export const dict = { "go.faq.q9": "Ücretsiz modeller ve Go arasındaki fark nedir?", "go.faq.a9": - "Ücretsiz modeller, günlük 200 istek kotası ile Big Pickle ve o sırada mevcut olan promosyonel modelleri içerir. Go ise GLM-5.2, GLM-5.1, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro ve DeepSeek V4 Flash modellerini; yuvarlanan pencereler (5 saatlik, haftalık ve aylık) üzerinden uygulanan daha yüksek istek kotalarıyla içerir. Bu kotalar kabaca her 5 saatte 12$, haftada 30$ ve ayda 60$ değerine eşdeğerdir (gerçek istek sayıları modele ve kullanıma göre değişir).", + "Ücretsiz modeller, günlük 200 istek kotasıyla Big Pickle'ı ve o sırada mevcut olan promosyonel modelleri içerir. Go ise kayan zaman aralıklarında (5 saatlik, haftalık ve aylık) uygulanan daha yüksek istek kotalarıyla özenle seçilmiş model seçenekleri sunar. Bu kotalar kabaca her 5 saatte 12$, haftada 30$ ve ayda 60$ değerine eşdeğerdir (gerçek istek sayıları modele ve kullanıma göre değişir).", "zen.api.error.rateLimitExceeded": "İstek limiti aşıldı. Lütfen daha sonra tekrar deneyin.", "zen.api.error.modelNotSupported": "{{model}} modeli desteklenmiyor", @@ -362,6 +379,7 @@ export const dict = { "zen.api.error.providerNotSupported": "{{provider}} sağlayıcısı desteklenmiyor", "zen.api.error.missingApiKey": "API anahtarı eksik.", "zen.api.error.invalidApiKey": "Geçersiz API anahtarı.", + "zen.api.error.requestBlockedByUpstreamProvider": "İstek üst sağlayıcı tarafından engellendi.", "zen.api.error.subscriptionQuotaExceeded": "Abonelik kotası aşıldı. {{retryIn}} içinde tekrar deneyin.", "zen.api.error.goSubscriptionRollingLimitExceeded": "5 saatlik kullanım limitine ulaşıldı. {{retryIn}} içinde sıfırlanır. Bu modeli şimdi kullanmaya devam etmek için kullanılabilir bakiyenizden kullanımı etkinleştirin: {{consoleGoUrl}}", @@ -377,7 +395,7 @@ export const dict = { "Aylık ${{amount}} harcama limitinize ulaştınız. Limitlerinizi buradan yönetin: {{membersUrl}}", "zen.api.error.modelDisabled": "Model devre dışı", "zen.api.error.regionNotAllowed": - "Bu model Çin'de barındırılıyor. Bu modeli kullanmak istiyorsanız ayarlarınızdan etkinleştirin: {{consoleGoUrl}}", + "Bu modelin en son sürümü yalnızca Çin'de barındırılıyor ve kullanabilmek için açıkça onay vermeniz gerekiyor: {{consoleGoUrl}}", "zen.api.error.trialEnded": "{{model}} için ücretsiz promosyon sona erdi. OpenCode Go'ya abone olarak modeli kullanmaya devam edebilirsiniz - {{link}}", @@ -669,7 +687,7 @@ export const dict = { "workspace.lite.promo.price": "İlk ay $5", "workspace.lite.promo.modelsTitle": "Neler Dahil", "workspace.lite.promo.footer": - "Plan öncelikle uluslararası kullanıcılar için tasarlanmıştır; modeller istikrarlı küresel erişim için ABD, AB ve Singapur'da barındırılmaktadır. Erken kullanımdan öğrendikçe ve geri bildirim topladıkça fiyatlandırma ve kullanım limitleri değişebilir.", + "Plan öncelikle uluslararası kullanıcılar için tasarlanmıştır ve istikrarlı küresel erişim sağlar. Erken kullanım ve geri bildirimlerden öğrendiklerimiz doğrultusunda fiyatlandırma ve kullanım limitleri değişebilir.", "workspace.lite.promo.subscribe": "Go'ya Abone Ol", "workspace.lite.promo.subscribing": "Yönlendiriliyor...", "workspace.lite.promo.otherMethods": "Diğer ödeme yöntemleri", @@ -711,11 +729,11 @@ export const dict = { "download.title": "OpenCode | İndir", "download.meta.description": "OpenCode'u macOS, Windows ve Linux için indirin", - "download.hero.title": "OpenCode'u İndir", + "download.hero.title": "OpenCode Desktop'u İndir", "download.hero.subtitle": "macOS, Windows ve Linux için Beta olarak sunuluyor", "download.hero.button": "{{os}} için indir", "download.section.terminal": "OpenCode Terminal", - "download.section.desktop": "OpenCode Desktop (Beta)", + "download.section.desktop": "OpenCode Desktop", "download.section.extensions": "OpenCode Eklentileri", "download.section.integrations": "OpenCode Entegrasyonları", "download.action.download": "İndir", diff --git a/packages/console/app/src/i18n/uk.ts b/packages/console/app/src/i18n/uk.ts index 8486fa81b5..48a0dbe787 100644 --- a/packages/console/app/src/i18n/uk.ts +++ b/packages/console/app/src/i18n/uk.ts @@ -102,10 +102,14 @@ export const dict = { "temp.logoDarkAlt": "темний логотип opencode", "home.banner.badge": "Нове", - "home.banner.text": "Десктопний застосунок доступний у бета-версії", - "home.banner.platforms": "на macOS, Windows та Linux", + "home.banner.text": "Представляємо вкладки в десктопному застосунку.", + "home.banner.platforms": "Доступно на macOS, Windows та Linux", "home.banner.downloadNow": "Завантажити зараз", "home.banner.downloadBetaNow": "Завантажити бета-версію десктопного застосунку", + "home.promo.title": "Представляємо вкладки в десктопному застосунку", + "home.promo.body": "Упорядковуйте роботу й активні сесії за допомогою вкладок.", + "home.promo.cta": "Завантажте останню версію, щоб почати.", + "home.promo.close": "Закрити оголошення про десктопний застосунок", "home.hero.title": "Відкритий AI-агент для кодування", "home.hero.subtitle.a": "Безкоштовні моделі включено або підключіть будь-яку модель від будь-якого провайдера,", @@ -251,8 +255,9 @@ export const dict = { "zen.privacy.exceptionsLink": "такими винятками", "go.title": "OpenCode Go | Недорогі моделі кодування для всіх", + "go.banner.text": "GPT 5.6 Luna отримує 2x ліміти використання протягом обмеженого часу", "go.meta.description": - "Go починається від $5 за перший місяць, потім $10/місяць, з generous 5-годинними лімітами запитів для GLM-5.2, GLM-5.1, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro та DeepSeek V4 Flash.", + "Go починається від $5 за перший місяць, потім $10/місяць, зі щедрими лімітами використання та надійним доступом до провідних моделей для кодування.", "go.hero.title": "Недорогі моделі кодування для всіх", "go.hero.body": "Go надає агентне програмування програмістам у всьому світі, пропонуючи щедрі ліміти та надійний доступ до найкращих моделей з відкритим кодом.", @@ -269,7 +274,6 @@ export const dict = { "go.graph.go": "Go", "go.graph.label": "Запитів за 5 годин", "go.graph.usageLimits": "Ліміти використання", - "go.graph.tick": "{{n}}x", "go.graph.aria": "Запитів за 5 год: {{free}} vs {{go}}", "go.testimonials.brand.zen": "Zen", @@ -300,8 +304,7 @@ export const dict = { "go.problem.item1": "Недорога підписка", "go.problem.item2": "Щедрі ліміти та надійний доступ", "go.problem.item3": "Створено для якомога більшої кількості програмістів", - "go.problem.item4": - "Включає GLM-5.2, GLM-5.1, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro та DeepSeek V4 Flash", + "go.problem.item4": "Добірка моделей, протестованих для агентного кодування", "go.how.title": "Як працює Go", "go.how.body": "Go починається від $5 за перший місяць, потім $10/місяць. Використовуйте з OpenCode або будь-яким агентом.", @@ -327,7 +330,7 @@ export const dict = { "go.faq.a2": "Go включає моделі, перелічені нижче, із щедрими лімітами та надійним доступом.", "go.faq.q3": "Чи Go те саме, що Zen?", "go.faq.a3": - "Ні. Zen — це плата за використання, тоді як Go починається від $5 за перший місяць, потім $10/місяць, із щедрими лімітами та надійним доступом до моделей з відкритим кодом GLM-5.2, GLM-5.1, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro та DeepSeek V4 Flash.", + "Ні. Zen — це плата за використання, тоді як Go починається від $5 за перший місяць, потім $10/місяць, із щедрими лімітами та надійним доступом до добірки моделей.", "go.faq.q4": "Скільки коштує Go?", "go.faq.a4.p1.beforePricing": "Go коштує", "go.faq.a4.p1.pricingLink": "$5 за перший місяць", @@ -336,8 +339,21 @@ export const dict = { "go.faq.a4.p2.accountLink": "обліковому записі", "go.faq.a4.p3": "Скасуйте в будь-який час.", "go.faq.q5": "А як щодо даних та конфіденційності?", - "go.faq.a5.body": - "План розроблений переважно для міжнародних користувачів, з моделями в США, ЄС та Сінгапурі. Провайдери дотримуються політики нульового зберігання.", + "go.faq.a5.model": "Модель", + "go.faq.a5.training": "Навчання моделей", + "go.faq.a5.retention": "Зберігання даних", + "go.faq.a5.retention30": "30 днів", + "go.faq.a5.retention0": "0 днів", + "go.faq.a5.used": "Використовується", + "go.faq.a5.notUsed": "Не використовується", + "go.faq.a5.noAgreement": "Немає угоди", + "go.faq.a5.grokRetention": + "ZDR вимикає важливі функції API, які залежать від збережених даних, зокрема Responses API зі збереженням стану, Files and Collections та Batch API.", + "go.faq.a5.gptRetention": + "Журнали моніторингу зловживань створюються для всіх випадків використання функцій API та зберігаються до 30 днів.", + "go.faq.a5.learnMore": "Докладніше", + "go.faq.a5.deepseekRetention": "Угода ZDR поновлюється щомісяця. Поточна угода дійсна до 31 серпня 2026 року.", + "go.faq.a5.beforeExceptions": "Моделі Go розміщені в США. Провайдери дотримуються політики нульового зберігання та не використовують ваші дані для навчання моделей, за", "go.faq.a5.exceptionsLink": "такими винятками", @@ -350,7 +366,7 @@ export const dict = { "go.faq.q9": "Яка різниця між безкоштовними моделями та Go?", "go.faq.a9": - "Безкоштовні моделі включають Big Pickle та акційні моделі з лімітом 200 запитів/день. Go включає GLM-5.2, GLM-5.1, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro та DeepSeek V4 Flash із вищими лімітами.", + "Безкоштовні моделі включають Big Pickle та доступні на той момент акційні моделі з квотою 200 запитів/день. Go пропонує добірку моделей із вищими квотами запитів, що застосовуються протягом ковзних періодів (5 годин, тижня та місяця), приблизно еквівалентними $12 за 5 годин, $30 на тиждень і $60 на місяць (фактична кількість запитів залежить від моделі та використання).", "zen.api.error.rateLimitExceeded": "Перевищено ліміт запитів. Спробуйте пізніше.", "zen.api.error.modelNotSupported": "Модель {{model}} не підтримується", @@ -359,6 +375,7 @@ export const dict = { "zen.api.error.providerNotSupported": "Провайдер {{provider}} не підтримується", "zen.api.error.missingApiKey": "Відсутній ключ API.", "zen.api.error.invalidApiKey": "Недійсний ключ API.", + "zen.api.error.requestBlockedByUpstreamProvider": "Запит заблоковано зовнішнім провайдером.", "zen.api.error.subscriptionQuotaExceeded": "Перевищено квоту підписки. Повторіть через {{retryIn}}.", "zen.api.error.goSubscriptionRollingLimitExceeded": "Досягнуто 5-годинного ліміту використання. Скидається через {{retryIn}}. Щоб продовжити, увімкніть використання з доступного балансу: {{consoleGoUrl}}", @@ -374,7 +391,7 @@ export const dict = { "Ви досягли місячного ліміту витрат ${{amount}}. Керуйте лімітами: {{membersUrl}}", "zen.api.error.modelDisabled": "Модель вимкнено", "zen.api.error.regionNotAllowed": - "Ця модель розміщена в Китаї. Якщо ви хочете використовувати цю модель, увімкніть її в налаштуваннях: {{consoleGoUrl}}", + "Остання версія цієї моделі доступна лише на серверах у Китаї, і для її використання потрібно надати явну згоду: {{consoleGoUrl}}", "zen.api.error.trialEnded": "Безкоштовна акція для {{model}} закінчилася. Ви можете продовжити використання, підписавшись на OpenCode Go — {{link}}", @@ -662,7 +679,8 @@ export const dict = { "workspace.lite.promo.description": "OpenCode Go починається від {{price}}, потім $10/місяць, із щедрими лімітами.", "workspace.lite.promo.price": "$5 за перший місяць", "workspace.lite.promo.modelsTitle": "Що включено", - "workspace.lite.promo.footer": "План призначений для міжнародних користувачів. Ціни можуть змінюватися.", + "workspace.lite.promo.footer": + "План призначений насамперед для міжнародних користувачів і забезпечує стабільний глобальний доступ. Ціни та ліміти використання можуть змінюватися з урахуванням перших даних про використання та відгуків.", "workspace.lite.promo.subscribe": "Підписатися на Go", "workspace.lite.promo.subscribing": "Перенаправлення...", "workspace.lite.promo.otherMethods": "Інші способи оплати", @@ -670,11 +688,11 @@ export const dict = { "download.title": "OpenCode | Завантажити", "download.meta.description": "Завантажте OpenCode для macOS, Windows та Linux", - "download.hero.title": "Завантажити OpenCode", + "download.hero.title": "Завантажити OpenCode Desktop", "download.hero.subtitle": "Доступно в бета-версії для macOS, Windows та Linux", "download.hero.button": "Завантажити для {{os}}", "download.section.terminal": "Термінал OpenCode", - "download.section.desktop": "Десктоп OpenCode (Бета)", + "download.section.desktop": "Десктоп OpenCode", "download.section.extensions": "Розширення OpenCode", "download.section.integrations": "Інтеграції OpenCode", "download.action.download": "Завантажити", diff --git a/packages/console/app/src/i18n/zh.ts b/packages/console/app/src/i18n/zh.ts index 5bcb84ac82..78f2081aeb 100644 --- a/packages/console/app/src/i18n/zh.ts +++ b/packages/console/app/src/i18n/zh.ts @@ -103,10 +103,14 @@ export const dict = { "temp.logoDarkAlt": "opencode logo 暗色", "home.banner.badge": "新", - "home.banner.text": "桌面应用 Beta 版现已推出", - "home.banner.platforms": "支持 macOS, Windows, 和 Linux", + "home.banner.text": "桌面版标签页现已推出。", + "home.banner.platforms": "适用于 macOS、Windows 和 Linux", "home.banner.downloadNow": "立即下载", "home.banner.downloadBetaNow": "立即下载桌面 Beta 版", + "home.promo.title": "桌面版标签页现已推出", + "home.promo.body": "使用标签页整理工作和活跃会话。", + "home.promo.cta": "下载最新版本即可开始使用。", + "home.promo.close": "关闭桌面应用公告", "home.hero.title": "开源 AI 编程代理", "home.hero.subtitle.a": "内置免费模型,或连接任意提供商的任意模型,", @@ -240,8 +244,8 @@ export const dict = { "zen.privacy.exceptionsLink": "以下例外情况除外", "go.title": "OpenCode Go | 人人可用的低成本编程模型", - "go.meta.description": - "Go 首月 $5,之后 $10/月,提供对 GLM-5.2、GLM-5.1、Kimi K2.7 Code、Kimi K2.6、MiMo-V2.5-Pro、MiMo-V2.5、Qwen3.7 Max、Qwen3.7 Plus、Qwen3.6 Plus、MiniMax M2.7、MiniMax M3、DeepSeek V4 Pro 和 DeepSeek V4 Flash 的 5 小时充裕请求额度。", + "go.banner.text": "GPT 5.6 Luna 限时享受 2 倍使用额度", + "go.meta.description": "Go 首月 $5,之后 $10/月,提供充裕的使用限额,并可可靠访问领先的编程模型。", "go.hero.title": "人人可用的低成本编程模型", "go.hero.body": "Go 将代理编程带给全世界的程序员。提供充裕的限额和对最强大的开源模型的可靠访问,让您可以利用强大的代理进行构建,而无需担心成本或可用性。", @@ -257,7 +261,6 @@ export const dict = { "go.graph.go": "Go", "go.graph.label": "每 5 小时请求数", "go.graph.usageLimits": "使用限制", - "go.graph.tick": "{{n}}x", "go.graph.aria": "每 5 小时请求数: {{free}} vs {{go}}", "go.testimonials.brand.zen": "Zen", @@ -288,8 +291,7 @@ export const dict = { "go.problem.item1": "低成本订阅定价", "go.problem.item2": "充裕的限额和可靠的访问", "go.problem.item3": "为尽可能多的程序员打造", - "go.problem.item4": - "包含 GLM-5.2, GLM-5.1, Kimi K2.7 Code、Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7、MiniMax M3、DeepSeek V4 Pro 和 DeepSeek V4 Flash", + "go.problem.item4": "经过代理编程测试的精选模型阵容", "go.how.title": "Go 如何工作", "go.how.body": "Go 起价为首月 $5,之后 $10/月。您可以将其与 OpenCode 或任何代理搭配使用。", "go.how.step1.title": "创建账户", @@ -310,8 +312,7 @@ export const dict = { "go.faq.q2": "Go 包含哪些模型?", "go.faq.a2": "Go 包含下方列出的模型,提供充足的限额和可靠的访问。", "go.faq.q3": "Go 和 Zen 一样吗?", - "go.faq.a3": - "不。Zen 是按量付费,而 Go 首月 $5,之后 $10/月,提供充裕的额度,并可可靠地访问 GLM-5.2、GLM-5.1、Kimi K2.7 Code、Kimi K2.6、MiMo-V2.5-Pro、MiMo-V2.5、Qwen3.7 Max、Qwen3.7 Plus、Qwen3.6 Plus、MiniMax M2.7、MiniMax M3、DeepSeek V4 Pro 和 DeepSeek V4 Flash 等开源模型。", + "go.faq.a3": "不。Zen 是按量付费,而 Go 首月 $5,之后 $10/月,提供充裕的限额,并可可靠访问精选模型阵容。", "go.faq.q4": "Go 多少钱?", "go.faq.a4.p1.beforePricing": "Go 费用为", "go.faq.a4.p1.pricingLink": "首月 $5", @@ -320,8 +321,19 @@ export const dict = { "go.faq.a4.p2.accountLink": "账户", "go.faq.a4.p3": "中管理订阅。随时取消。", "go.faq.q5": "数据和隐私如何?", - "go.faq.a5.body": - "该计划主要面向国际用户设计,模型部署在美国、欧盟和新加坡,以确保稳定的全球访问。我们的提供商遵循零留存政策,不使用您的数据进行模型训练。", + "go.faq.a5.model": "模型", + "go.faq.a5.training": "模型训练", + "go.faq.a5.retention": "数据留存", + "go.faq.a5.retention30": "30 天", + "go.faq.a5.retention0": "0 天", + "go.faq.a5.used": "使用", + "go.faq.a5.notUsed": "不使用", + "go.faq.a5.noAgreement": "无协议", + "go.faq.a5.grokRetention": + "ZDR 会禁用依赖所存储数据的重要 API 功能,包括有状态的 Responses API、Files and Collections 和 Batch API。", + "go.faq.a5.gptRetention": "所有 API 功能的使用都会生成滥用监控日志,并最多保留 30 天。", + "go.faq.a5.learnMore": "了解更多", + "go.faq.a5.deepseekRetention": "ZDR 协议每月续签。当前协议有效期至 2026 年 8 月 31 日。", "go.faq.a5.beforeExceptions": "Go 模型托管在美国。提供商遵循零留存政策,不使用您的数据进行模型训练,", "go.faq.a5.exceptionsLink": "以下例外情况除外", "go.faq.q6": "我可以充值余额吗?", @@ -333,7 +345,7 @@ export const dict = { "go.faq.q9": "免费模型和 Go 之间的区别是什么?", "go.faq.a9": - "免费模型包含 Big Pickle 加上当时可用的促销模型,每天有 200 次请求的配额。Go 包含 GLM-5.2, GLM-5.1, Kimi K2.7 Code、Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7、MiniMax M3、DeepSeek V4 Pro 和 DeepSeek V4 Flash,并在滚动窗口(5 小时、每周和每月)内执行更高的请求配额,大致相当于每 5 小时 $12、每周 $30 和每月 $60(实际请求计数因模型和使用情况而异)。", + "免费模型包含 Big Pickle 加上当时可用的促销模型,每天有 200 次请求的配额。Go 提供精选模型阵容,并在滚动窗口(5 小时、每周和每月)内执行更高的请求配额,大致相当于每 5 小时 $12、每周 $30 和每月 $60(实际请求计数因模型和使用情况而异)。", "zen.api.error.rateLimitExceeded": "超出速率限制。请稍后重试。", "zen.api.error.modelNotSupported": "不支持模型 {{model}}", @@ -342,6 +354,7 @@ export const dict = { "zen.api.error.providerNotSupported": "不支持提供商 {{provider}}", "zen.api.error.missingApiKey": "缺少 API 密钥。", "zen.api.error.invalidApiKey": "无效的 API 密钥。", + "zen.api.error.requestBlockedByUpstreamProvider": "请求被上游提供商阻止。", "zen.api.error.subscriptionQuotaExceeded": "超出订阅配额。请在 {{retryIn}} 后重试。", "zen.api.error.goSubscriptionRollingLimitExceeded": "已达到 5 小时使用限额。将在 {{retryIn}} 后重置。如需立即继续使用该模型,请启用从可用余额扣费:{{consoleGoUrl}}", @@ -355,7 +368,7 @@ export const dict = { "您的工作区已达到每月支出限额 ${{amount}}。请在此处管理您的限额:{{billingUrl}}", "zen.api.error.userMonthlyLimitReached": "您已达到每月支出限额 ${{amount}}。请在此处管理您的限额:{{membersUrl}}", "zen.api.error.modelDisabled": "模型已禁用", - "zen.api.error.regionNotAllowed": "该模型部署在中国。如果你想使用该模型,请在设置中启用它:{{consoleGoUrl}}", + "zen.api.error.regionNotAllowed": "该模型的最新版本仅在中国提供托管服务,需明确选择启用:{{consoleGoUrl}}", "zen.api.error.trialEnded": "{{model}} 的限免活动已结束。您可以订阅 OpenCode Go 继续使用该模型 - {{link}}", "black.meta.title": "OpenCode Black | 访问全球顶尖编程模型", @@ -641,7 +654,7 @@ export const dict = { "workspace.lite.promo.price": "首月 $5", "workspace.lite.promo.modelsTitle": "包含模型", "workspace.lite.promo.footer": - "该计划主要面向国际用户设计,模型部署在美国、欧盟和新加坡,以确保全球范围内的稳定访问体验。定价和使用额度可能会根据早期用户的使用情况和反馈持续调整与优化。", + "该计划主要面向国际用户,提供稳定的全球访问体验。随着我们持续了解早期使用情况并收集反馈,定价和使用限额可能会有所调整。", "workspace.lite.promo.subscribe": "订阅 Go", "workspace.lite.promo.subscribing": "正在重定向...", "workspace.lite.promo.otherMethods": "其他付款方式", @@ -682,11 +695,11 @@ export const dict = { "download.title": "OpenCode | 下载", "download.meta.description": "下载适用于 macOS, Windows, 和 Linux 的 OpenCode", - "download.hero.title": "下载 OpenCode", + "download.hero.title": "下载 OpenCode 桌面版", "download.hero.subtitle": "适用于 macOS, Windows, 和 Linux 的 Beta 版", "download.hero.button": "下载 {{os}} 版", "download.section.terminal": "OpenCode 终端", - "download.section.desktop": "OpenCode 桌面版 (Beta)", + "download.section.desktop": "OpenCode 桌面版", "download.section.extensions": "OpenCode 扩展", "download.section.integrations": "OpenCode 集成", "download.action.download": "下载", diff --git a/packages/console/app/src/i18n/zht.ts b/packages/console/app/src/i18n/zht.ts index eaa5763d46..190a927d3f 100644 --- a/packages/console/app/src/i18n/zht.ts +++ b/packages/console/app/src/i18n/zht.ts @@ -103,10 +103,14 @@ export const dict = { "temp.logoDarkAlt": "opencode 深色標誌", "home.banner.badge": "新", - "home.banner.text": "桌面應用已推出 Beta", - "home.banner.platforms": "支援 macOS、Windows 與 Linux", + "home.banner.text": "桌面版分頁功能全新推出。", + "home.banner.platforms": "適用於 macOS、Windows 與 Linux", "home.banner.downloadNow": "立即下載", "home.banner.downloadBetaNow": "立即下載桌面 Beta 版", + "home.promo.title": "桌面版分頁功能全新推出", + "home.promo.body": "使用分頁整理工作和作用中的工作階段。", + "home.promo.cta": "下載最新版本即可開始使用。", + "home.promo.close": "關閉桌面應用程式公告", "home.hero.title": "開源 AI 編碼代理", "home.hero.subtitle.a": "內建免費模型,或連接任意供應商的任意模型,", @@ -240,8 +244,8 @@ export const dict = { "zen.privacy.exceptionsLink": "以下例外情況", "go.title": "OpenCode Go | 低成本全民編碼模型", - "go.meta.description": - "Go 首月 $5,之後 $10/月,提供對 GLM-5.2、GLM-5.1、Kimi K2.7 Code、Kimi K2.6、MiMo-V2.5-Pro、MiMo-V2.5、Qwen3.7 Max、Qwen3.7 Plus、Qwen3.6 Plus、MiniMax M2.7、MiniMax M3、DeepSeek V4 Pro 和 DeepSeek V4 Flash 的 5 小時充裕請求額度。", + "go.banner.text": "GPT 5.6 Luna 限時享有 2 倍使用額度", + "go.meta.description": "Go 首月 $5,之後 $10/月,提供充裕的使用限額,並可穩定存取領先的編碼模型。", "go.hero.title": "低成本全民編碼模型", "go.hero.body": "Go 將代理編碼帶給全世界的程式設計師。提供寬裕的限額以及對最強大開源模型的穩定存取,讓你可以使用強大的代理進行構建,而無需擔心成本或可用性。", @@ -257,7 +261,6 @@ export const dict = { "go.graph.go": "Go", "go.graph.label": "每 5 小時請求數", "go.graph.usageLimits": "使用限制", - "go.graph.tick": "{{n}}x", "go.graph.aria": "每 5 小時請求數:{{free}} vs {{go}}", "go.testimonials.brand.zen": "Zen", @@ -288,8 +291,7 @@ export const dict = { "go.problem.item1": "低成本訂閱定價", "go.problem.item2": "寬裕的限額與穩定存取", "go.problem.item3": "專為盡可能多的程式設計師打造", - "go.problem.item4": - "包含 GLM-5.2、GLM-5.1、Kimi K2.7 Code、Kimi K2.6、MiMo-V2.5-Pro、MiMo-V2.5、Qwen3.7 Max、Qwen3.7 Plus、Qwen3.6 Plus、MiniMax M2.7、MiniMax M3、DeepSeek V4 Pro 與 DeepSeek V4 Flash", + "go.problem.item4": "針對代理編碼測試的精選模型陣容", "go.how.title": "Go 如何運作", "go.how.body": "Go 起價為首月 $5,之後 $10/月。您可以將其與 OpenCode 或任何代理搭配使用。", "go.how.step1.title": "建立帳號", @@ -310,8 +312,7 @@ export const dict = { "go.faq.q2": "Go 包含哪些模型?", "go.faq.a2": "Go 包含下方列出的模型,提供充足的額度與穩定的存取。", "go.faq.q3": "Go 與 Zen 一樣嗎?", - "go.faq.a3": - "不。Zen 是按量付費,而 Go 首月 $5,之後 $10/月,提供充裕的額度,並可可靠地存取 GLM-5.2、GLM-5.1、Kimi K2.7 Code、Kimi K2.6、MiMo-V2.5-Pro、MiMo-V2.5、Qwen3.7 Max、Qwen3.7 Plus、Qwen3.6 Plus、MiniMax M2.7、MiniMax M3、DeepSeek V4 Pro 和 DeepSeek V4 Flash 等開源模型。", + "go.faq.a3": "不。Zen 是按量付費,而 Go 首月 $5,之後 $10/月,提供充裕的限額,並可穩定存取精選模型陣容。", "go.faq.q4": "Go 費用是多少?", "go.faq.a4.p1.beforePricing": "Go 費用為", "go.faq.a4.p1.pricingLink": "首月 $5", @@ -320,8 +321,19 @@ export const dict = { "go.faq.a4.p2.accountLink": "帳戶", "go.faq.a4.p3": "中管理訂閱。隨時取消。", "go.faq.q5": "資料與隱私怎麼辦?", - "go.faq.a5.body": - "該方案主要面向國際用戶設計,模型託管在美國、歐盟和新加坡,以確保全球穩定存取。我們的供應商遵循零留存政策,不會將你的資料用於模型訓練。", + "go.faq.a5.model": "模型", + "go.faq.a5.training": "模型訓練", + "go.faq.a5.retention": "資料保留", + "go.faq.a5.retention30": "30 天", + "go.faq.a5.retention0": "0 天", + "go.faq.a5.used": "使用", + "go.faq.a5.notUsed": "不使用", + "go.faq.a5.noAgreement": "無協議", + "go.faq.a5.grokRetention": + "ZDR 會停用依賴儲存資料的重要 API 功能,包括具狀態的 Responses API、Files and Collections 與 Batch API。", + "go.faq.a5.gptRetention": "所有 API 功能的使用都會產生濫用監控日誌,並保留最多 30 天。", + "go.faq.a5.learnMore": "了解更多", + "go.faq.a5.deepseekRetention": "ZDR 協議每月續簽。目前的協議有效至 2026 年 8 月 31 日。", "go.faq.a5.beforeExceptions": "Go 模型託管在美國。供應商遵循零留存政策,不會將你的資料用於模型訓練,但有", "go.faq.a5.exceptionsLink": "以下例外", "go.faq.q6": "我可以儲值額度嗎?", @@ -333,7 +345,7 @@ export const dict = { "go.faq.q9": "免費模型與 Go 有什麼區別?", "go.faq.a9": - "免費模型包括 Big Pickle 以及當時可用的促銷模型,配額為 200 次請求/天。Go 包括 GLM-5.2、GLM-5.1、Kimi K2.7 Code、Kimi K2.6、MiMo-V2.5-Pro、MiMo-V2.5、Qwen3.7 Max、Qwen3.7 Plus、Qwen3.6 Plus、MiniMax M2.7、MiniMax M3、DeepSeek V4 Pro 與 DeepSeek V4 Flash,並在滾動視窗(5 小時、每週和每月)內執行更高的請求配額,大約相當於每 5 小時 $12、每週 $30 和每月 $60(實際請求數因模型和使用情況而異)。", + "免費模型包括 Big Pickle 以及當時可用的促銷模型,配額為 200 次請求/天。Go 提供精選模型陣容,並在滾動視窗(5 小時、每週和每月)內提供更高的請求配額,大約相當於每 5 小時 $12、每週 $30 和每月 $60(實際請求數因模型和使用情況而異)。", "zen.api.error.rateLimitExceeded": "超出頻率限制。請稍後再試。", "zen.api.error.modelNotSupported": "不支援模型 {{model}}", @@ -342,6 +354,7 @@ export const dict = { "zen.api.error.providerNotSupported": "不支援供應商 {{provider}}", "zen.api.error.missingApiKey": "缺少 API 金鑰。", "zen.api.error.invalidApiKey": "無效的 API 金鑰。", + "zen.api.error.requestBlockedByUpstreamProvider": "請求遭上游供應商封鎖。", "zen.api.error.subscriptionQuotaExceeded": "超出訂閱配額。請在 {{retryIn}} 後重試。", "zen.api.error.goSubscriptionRollingLimitExceeded": "已達 5 小時使用上限,將在 {{retryIn}} 後重置。若要立即繼續使用此模型,請從可用餘額啟用使用量:{{consoleGoUrl}}", @@ -355,7 +368,7 @@ export const dict = { "你的工作區已達到每月支出限額 ${{amount}}。請在此處管理你的限額:{{billingUrl}}", "zen.api.error.userMonthlyLimitReached": "你已達到每月支出限額 ${{amount}}。請在此處管理你的限額:{{membersUrl}}", "zen.api.error.modelDisabled": "模型已停用", - "zen.api.error.regionNotAllowed": "此模型部署於中國。如果你想使用此模型,請在設定中啟用它:{{consoleGoUrl}}", + "zen.api.error.regionNotAllowed": "此模型的最新版本僅於中國託管,且需明確選擇啟用:{{consoleGoUrl}}", "zen.api.error.trialEnded": "{{model}} 的限免活动已結束。您可以訂閱 OpenCode Go 繼續使用該模型 - {{link}}", "black.meta.title": "OpenCode Black | 存取全球最佳編碼模型", @@ -641,7 +654,7 @@ export const dict = { "workspace.lite.promo.price": "首月 $5", "workspace.lite.promo.modelsTitle": "包含模型", "workspace.lite.promo.footer": - "該計畫主要面向國際用戶設計,模型部署在美國、歐盟和新加坡,以確保全球範圍內的穩定存取體驗。定價和使用額度可能會根據早期用戶的使用情況和回饋持續調整與優化。", + "此方案主要為國際使用者設計,提供穩定的全球存取服務。隨著我們從初期使用情況和回饋中持續了解需求,價格和使用額度可能會有所調整。", "workspace.lite.promo.subscribe": "訂閱 Go", "workspace.lite.promo.subscribing": "重新導向中...", "workspace.lite.promo.otherMethods": "其他付款方式", @@ -682,11 +695,11 @@ export const dict = { "download.title": "OpenCode | 下載", "download.meta.description": "下載適用於 macOS、Windows 與 Linux 的 OpenCode", - "download.hero.title": "下載 OpenCode", + "download.hero.title": "下載 OpenCode 桌面版", "download.hero.subtitle": "適用於 macOS、Windows 與 Linux 的 Beta 版現已提供", "download.hero.button": "下載 {{os}} 版", "download.section.terminal": "OpenCode 終端", - "download.section.desktop": "OpenCode 桌面版(Beta)", + "download.section.desktop": "OpenCode 桌面版", "download.section.extensions": "OpenCode 擴充功能", "download.section.integrations": "OpenCode 整合", "download.action.download": "下載", diff --git a/packages/console/app/src/lib/stats-proxy.ts b/packages/console/app/src/lib/stats-proxy.ts index d63b966d23..48e95bd74a 100644 --- a/packages/console/app/src/lib/stats-proxy.ts +++ b/packages/console/app/src/lib/stats-proxy.ts @@ -18,7 +18,8 @@ export async function statsProxy(evt: APIEvent) { if ( targetUrl.pathname.startsWith(`${dataPath}/_build/`) || targetUrl.pathname === `${dataPath}/banner.jpg` || - targetUrl.pathname === `${dataPath}/banner.png` + targetUrl.pathname === `${dataPath}/banner.png` || + targetUrl.pathname === `${dataPath}/sitemap.xml` ) { targetUrl.pathname = targetUrl.pathname.slice(dataPath.length) } @@ -97,7 +98,8 @@ function isDataBypassPath(pathname: string) { pathname.startsWith(`${dataPath}/api/`) || pathname.startsWith(`${dataPath}/_server`) || pathname === `${dataPath}/banner.jpg` || - pathname === `${dataPath}/banner.png` + pathname === `${dataPath}/banner.png` || + pathname === `${dataPath}/sitemap.xml` ) } diff --git a/packages/console/app/src/routes/brand/index.css b/packages/console/app/src/routes/brand/index.css index 8a32651591..0e39f3742f 100644 --- a/packages/console/app/src/routes/brand/index.css +++ b/packages/console/app/src/routes/brand/index.css @@ -376,7 +376,6 @@ align-items: center; gap: 16px; opacity: 0; - transition: opacity 0.2s ease; @media (max-width: 40rem) { position: static; diff --git a/packages/console/app/src/routes/download/index.css b/packages/console/app/src/routes/download/index.css index b2176c34a2..1dde6e6105 100644 --- a/packages/console/app/src/routes/download/index.css +++ b/packages/console/app/src/routes/download/index.css @@ -284,7 +284,7 @@ } [data-component="content"] { - padding: 6rem 5rem; + padding: 3rem 5rem 6rem; @media (max-width: 60rem) { padding: 4rem 1.5rem; @@ -316,67 +316,43 @@ /* Download Hero Section */ [data-component="download-hero"] { - /* display: grid; */ - display: none; - grid-template-columns: 260px 1fr; + display: grid; + grid-template-columns: 255px 1fr; gap: 4rem; - padding-bottom: 2rem; + padding: 4px; + border: 1px solid var(--color-border-weak); + border-radius: 8px; margin-bottom: 4rem; @media (max-width: 50rem) { grid-template-columns: 1fr; - gap: 1.5rem; - padding-bottom: 2rem; + gap: 0; margin-bottom: 2rem; } - [data-component="hero-icon"] { - display: flex; - justify-content: flex-end; - align-items: center; - - @media (max-width: 40rem) { - display: none; - } - - [data-slot="icon-placeholder"] { - width: 120px; - height: 120px; - background: var(--color-background-weak); - border: 1px solid var(--color-border-weak); - border-radius: 24px; + [data-component="hero-video"] { + video { + display: block; + width: 100%; + aspect-ratio: 1; + object-fit: cover; + border-radius: 4px; @media (max-width: 50rem) { - width: 80px; - height: 80px; + aspect-ratio: 16 / 9; } } - - img { - width: 120px; - height: 120px; - border-radius: 24px; - box-shadow: - 0 1.467px 2.847px 0 rgba(0, 0, 0, 0.42), - 0 0.779px 1.512px 0 rgba(0, 0, 0, 0.34), - 0 0.324px 0.629px 0 rgba(0, 0, 0, 0.24); - - @media (max-width: 50rem) { - width: 80px; - height: 80px; - border-radius: 16px; - } - } - - @media (max-width: 50rem) { - justify-content: flex-start; - } } [data-component="hero-text"] { display: flex; flex-direction: column; justify-content: center; + padding-right: 3rem; + + @media (max-width: 50rem) { + padding: 1.5rem; + } h1 { font-size: 1.5rem; diff --git a/packages/console/app/src/routes/download/index.tsx b/packages/console/app/src/routes/download/index.tsx index b5c202a5ec..c69895e4f2 100644 --- a/packages/console/app/src/routes/download/index.tsx +++ b/packages/console/app/src/routes/download/index.tsx @@ -11,7 +11,7 @@ import { LocaleLinks } from "~/component/locale-links" import { config } from "~/config" import { useI18n } from "~/context/i18n" import { useLanguage } from "~/context/language" -import desktopAppIcon from "../../asset/lander/opencode-desktop-icon.png" +import desktopTabsVideo from "../../asset/lander/desktop-tabs-landscape.mp4" import type { DownloadPlatform } from "./types" type OS = "macOS" | "Windows" | "Linux" | null @@ -93,12 +93,14 @@ export default function Download() {
-
- +
+

{i18n.t("download.hero.title")}

-

{i18n.t("download.hero.subtitle")}

+

+ {i18n.t("home.promo.body")} {i18n.t("home.promo.cta")} +

{i18n.t("enterprise.faq.a4")} +
  • + + {i18n.t("enterprise.faq.a5.before")} trust.opencode.ai{" "} + {i18n.t("enterprise.faq.a5.after")} + +
  • diff --git a/packages/console/app/src/routes/go/index.css b/packages/console/app/src/routes/go/index.css index 45b61d217e..bcf7c896a4 100644 --- a/packages/console/app/src/routes/go/index.css +++ b/packages/console/app/src/routes/go/index.css @@ -327,6 +327,37 @@ body { } } + [data-component="desktop-app-banner"] { + display: flex; + align-items: center; + gap: 12px; + margin-bottom: 32px; + + [data-slot="badge"] { + background: var(--color-background-strong); + color: var(--color-text-inverted); + font-weight: 500; + padding: 4px 8px; + line-height: 1; + flex-shrink: 0; + } + + [data-slot="content"] { + display: flex; + align-items: center; + gap: 1ch; + } + + [data-slot="text"] { + color: var(--color-text-strong); + line-height: 1.4; + + @media (max-width: 30.625rem) { + display: none; + } + } + } + [data-slot="hero-copy"] { img { margin-bottom: 24px; @@ -492,6 +523,12 @@ body { @media (prefers-color-scheme: light) { color: color-mix(in srgb, var(--color-text-weak) 82%, var(--color-text-strong)); } + + @media (max-width: 60rem) { + &:not([data-tick="1"], [data-tick="25"], [data-tick="100"], [data-tick="250"]) { + display: none; + } + } } [data-slot="ylabels"] [data-ylabel] { @@ -628,8 +665,8 @@ body { stroke: none; } - [data-bar][data-kind="free"] { - fill: var(--color-text-strong); + [data-bar][data-kind="promo"] { + fill: color-mix(in srgb, var(--bar-go) 50%, transparent); } [data-val] { @@ -652,11 +689,6 @@ body { transform-origin: center; } - [data-point][data-kind="free"] { - fill: var(--color-background); - stroke: var(--color-text-strong); - } - [data-point][data-kind="go"] { fill: var(--color-background-interactive); stroke: var(--color-text-strong); @@ -1028,28 +1060,47 @@ body { margin-bottom: 32px; [data-slot="faq-models"] { + margin: 16px 0 0; + padding-left: 20px; + + li { + margin-bottom: 0; + list-style: disc; + } + } + + [data-slot="faq-model-table"] { + max-width: 560px; margin-top: 16px; overflow-x: auto; table { width: 100%; - min-width: 28rem; border-collapse: collapse; + line-height: 150%; } th, td { - padding: 12px 16px; + padding: 8px 12px; + border-bottom: 1px solid var(--color-border-weak); text-align: left; white-space: nowrap; } th { - background: var(--color-background-weak); color: var(--color-text-strong); font-weight: 500; } } + + [data-slot="faq-retention-notes"] { + margin-top: 16px; + + p { + margin: 8px 0 0; + } + } } } diff --git a/packages/console/app/src/routes/go/index.tsx b/packages/console/app/src/routes/go/index.tsx index 960099fe49..17c78f214d 100644 --- a/packages/console/app/src/routes/go/index.tsx +++ b/packages/console/app/src/routes/go/index.tsx @@ -23,20 +23,25 @@ const checkLoggedIn = query(async () => { }, "checkLoggedIn.get") const models = [ - { name: "GLM-5.2", provider: "DeepInfra, Fireworks AI, Z.ai" }, - { name: "GLM-5.1", provider: "DeepInfra, Fireworks AI, Z.ai" }, - { name: "Kimi K2.7 Code", provider: "Moonshot AI" }, - { name: "Kimi K2.6", provider: "Moonshot AI" }, - { name: "MiMo-V2.5-Pro", provider: "Xiaomi MiMo" }, - { name: "MiMo-V2.5", provider: "Xiaomi MiMo" }, - { name: "Qwen3.7 Max", provider: "Alibaba Cloud Model Studio" }, - { name: "Qwen3.7 Plus", provider: "Alibaba Cloud Model Studio" }, - { name: "Qwen3.6 Plus", provider: "Alibaba Cloud Model Studio" }, - { name: "MiniMax M3", provider: "MiniMax" }, - { name: "MiniMax M2.7", provider: "MiniMax" }, - { name: "DeepSeek V4 Pro", provider: "DeepSeek" }, - { name: "DeepSeek V4 Flash", provider: "DeepSeek" }, -] + { name: "Grok 4.5", training: "go.faq.a5.notUsed", retention: "go.faq.a5.retention30" }, + { name: "GPT 5.6 Luna", training: "go.faq.a5.notUsed", retention: "go.faq.a5.retention30" }, + { name: "GLM-5.2", training: "go.faq.a5.notUsed", retention: "go.faq.a5.retention0" }, + { name: "GLM-5.1", training: "go.faq.a5.notUsed", retention: "go.faq.a5.retention0" }, + { name: "Kimi K3", training: "go.faq.a5.notUsed", retention: "go.faq.a5.retention0" }, + { name: "Kimi K2.7 Code", training: "go.faq.a5.notUsed", retention: "go.faq.a5.retention0" }, + { name: "Kimi K2.6", training: "go.faq.a5.notUsed", retention: "go.faq.a5.retention0" }, + { name: "MiMo-V2.5-Pro", training: "go.faq.a5.notUsed", retention: "go.faq.a5.retention0" }, + { name: "MiMo-V2.5", training: "go.faq.a5.notUsed", retention: "go.faq.a5.retention0" }, + { name: "Qwen3.8 Max", training: "go.faq.a5.notUsed", retention: "go.faq.a5.retention0" }, + { name: "Qwen3.7 Max", training: "go.faq.a5.notUsed", retention: "go.faq.a5.retention0" }, + { name: "Qwen3.7 Plus", training: "go.faq.a5.notUsed", retention: "go.faq.a5.retention0" }, + { name: "Qwen3.6 Plus", training: "go.faq.a5.notUsed", retention: "go.faq.a5.retention0" }, + { name: "MiniMax M3", training: "go.faq.a5.notUsed", retention: "go.faq.a5.retention0" }, + { name: "MiniMax M2.7", training: "go.faq.a5.notUsed", retention: "go.faq.a5.retention0" }, + { name: "DeepSeek V4 Pro", training: "go.faq.a5.notUsed", retention: "go.faq.a5.retention0" }, + { name: "DeepSeek V4 Flash", training: "go.faq.a5.notUsed", retention: "go.faq.a5.retention0" }, + { name: "Hy3", training: "go.faq.a5.notUsed", retention: "go.faq.a5.retention0" }, +] as const function LimitsGraph(props: { href: string }) { let root!: HTMLElement @@ -59,15 +64,17 @@ function LimitsGraph(props: { href: string }) { onCleanup(() => observer.disconnect()) }) - const free = 200 + const baseline = 100 const graph = [ + { id: "grok-4.5", name: "Grok 4.5", req: 120, d: "50ms" }, + { id: "kimi-k3", name: "Kimi K3", req: 110, d: "75ms" }, + { id: "qwen3.8-max", name: "Qwen3.8 Max", req: 160, d: "90ms" }, { id: "glm-5.2", name: "GLM-5.2", req: 880, d: "100ms" }, - { id: "qwen3.7-max", name: "Qwen3.7 Max", req: 950, d: "110ms" }, - { id: "kimi-k2.7-code", name: "Kimi K2.7 Code", req: 1150, d: "150ms" }, { id: "minimax-m3", name: "MiniMax M3", req: 3200, d: "210ms" }, - { id: "mimo-v2.5-pro", name: "MiMo-V2.5-Pro", req: 3250, d: "240ms" }, { id: "deepseek-v4-pro", name: "DeepSeek V4 Pro", req: 3450, d: "270ms" }, + { id: "gpt-5.6-luna", name: "GPT 5.6 Luna (2x usage)", req: 4100, baseReq: 2050, d: "290ms" }, { id: "qwen3.7-plus", name: "Qwen3.7 Plus", req: 4300, d: "300ms" }, + { id: "hy3", name: "Hy3", req: 4300, d: "320ms" }, { id: "mimo-v2.5", name: "MiMo-V2.5", req: 30100, d: "340ms" }, { id: "deepseek-v4-flash", name: "DeepSeek V4 Flash", req: 31650, d: "340ms" }, ] @@ -79,15 +86,13 @@ function LimitsGraph(props: { href: string }) { const bottom = 44 const plot = w - left - right - const ratio = (n: number) => n / free + const ratio = (n: number) => n / baseline const rmax = Math.max(1, ...graph.map((m) => ratio(m.req))) const log = (n: number) => Math.log10(Math.max(n, 1)) const base = 24 const p = 2.2 const x = (r: number) => left + base + Math.pow(log(r) / log(rmax), p) * (plot - base) - const start = (x(1) / w) * 100 - - const ticks = [1, 5, 10, 25, 50, 100].filter((t) => t <= rmax) + const ticks = [1, 5, 10, 25, 50, 100, 250].filter((t) => t <= rmax) const labels = (() => { const set = new Set() let last = -Infinity @@ -108,10 +113,8 @@ function LimitsGraph(props: { href: string }) { const bh = 8 const gap = 20 const step = bh + gap - const h = 330 + Math.max(0, graph.length - 8) * step - const sep = bh + 40 - const fy = top + 22 - const gy = (i: number) => fy + sep + step * i + const gy = (i: number) => top + 22 + step * i + const h = gy(graph.length - 1) + bottom const my = graph.length < 2 ? gy(0) : (gy(0) + gy(graph.length - 1)) / 2 const px = (n: number) => `${(n / w) * 100}%` const py = (n: number) => `${(n / h) * 100}%` @@ -121,10 +124,9 @@ function LimitsGraph(props: { href: string }) { return (
    - - {(t) => ( - - - - )} - + {(t) => } - - - - {(m, i) => ( + {m.baseReq && ( + + )} )} @@ -170,9 +173,6 @@ function LimitsGraph(props: { href: string }) {