Compare commits

..

17 Commits

Author SHA1 Message Date
Aiden Cline b7e47110f6 chore(cli): remove unused dependencies
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
2026-06-27 03:44:48 +00:00
Aiden Cline 36c416e143 fix(mcp): request refresh token scope (#34125) 2026-06-26 22:27:43 -05:00
Aiden Cline e1e0304a96 fix(mcp): surface OAuth completion errors (#34145) 2026-06-26 22:21:16 -05:00
opencode-agent[bot] 71c3a7c8f2 chore: generate 2026-06-27 02:47:33 +00:00
James Long ecdfff5a42 refactor(core): separate out location node functionality and integrate into v2 (#34119) 2026-06-26 22:46:07 -04:00
opencode-agent[bot] 4b948c5d74 fix(app): hide home session archive action (#34136)
Co-authored-by: 𝓛𝓲𝓽𝓽𝓵𝓮 𝓕𝓻𝓪𝓷𝓴 <little-frank@opencord.local>
2026-06-27 02:23:36 +00:00
opencode-agent[bot] cd56c51e2d fix(app): keep bare slash as plain inline code (#34122)
Co-authored-by: 𝓛𝓲𝓽𝓽𝓵𝓮 𝓕𝓻𝓪𝓷𝓴 <little-frank@opencord.local>
2026-06-26 22:21:01 +00:00
opencode-agent[bot] 43e39d7f68 fix(tui): use generated event union (#34118)
Co-authored-by: 𝓛𝓲𝓽𝓽𝓵𝓮 𝓕𝓻𝓪𝓷𝓴 <little-frank@opencord.local>
2026-06-26 18:04:53 -04:00
Frank 7a17925495 zen: new inference 2026-06-26 16:25:07 -04:00
Brendan Allan 5acb2530b4 fix(app): centralize notification state (#34105) 2026-06-26 19:24:33 +00:00
opencode-agent[bot] 44a6787359 chore: generate 2026-06-26 19:18:18 +00:00
Kit Langton 42e6b7db32 feat(sdk): expose live event stream (#34098) 2026-06-26 21:16:33 +02:00
opencode-agent[bot] 2c02f8bace chore: update nix node_modules hashes 2026-06-26 18:53:34 +00:00
Affan Ali 2ec20e576b fix(app): slow tooltip display for models (#30745)
Co-authored-by: affanali2k3 <affanalikhanxx@gmail.com>
2026-06-27 02:53:08 +08:00
opencode-agent[bot] 20f47fec7a chore: generate 2026-06-26 18:51:42 +00:00
Kit Langton 65210f2d97 feat(api): add finite durable session history pages (#34097) 2026-06-26 14:49:47 -04:00
Brendan Allan af0b7ffae7 fix(app): animate prompt selectors after loading (#34101) 2026-06-27 02:36:01 +08:00
273 changed files with 4916 additions and 11409 deletions
+2 -7
View File
@@ -6,7 +6,6 @@ on:
branches: branches:
- ci - ci
- dev - dev
- v2
- beta - beta
- fix/npm-native-binary-install - fix/npm-native-binary-install
- snapshot-* - snapshot-*
@@ -32,9 +31,6 @@ permissions:
contents: write contents: write
packages: write packages: write
env:
OPENCODE_CHANNEL: ${{ (github.ref_name == 'v2' && 'next') || '' }}
jobs: jobs:
version: version:
runs-on: blacksmith-4vcpu-ubuntu-2404 runs-on: blacksmith-4vcpu-ubuntu-2404
@@ -126,7 +122,7 @@ jobs:
- build-cli - build-cli
- version - version
runs-on: blacksmith-4vcpu-windows-2025 runs-on: blacksmith-4vcpu-windows-2025
if: github.repository == 'anomalyco/opencode' && github.ref_name != 'v2' if: github.repository == 'anomalyco/opencode'
env: env:
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
@@ -225,7 +221,7 @@ jobs:
needs: needs:
- build-cli - build-cli
- version - version
if: github.repository == 'anomalyco/opencode' && github.ref_name != 'v2' if: github.repository == 'anomalyco/opencode'
continue-on-error: false continue-on-error: false
env: env:
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
@@ -451,7 +447,6 @@ jobs:
path: packages/opencode/dist path: packages/opencode/dist
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
if: github.ref_name != 'v2'
with: with:
name: opencode-cli-signed-windows name: opencode-cli-signed-windows
path: packages/opencode/dist path: packages/opencode/dist
+5 -2
View File
@@ -4,7 +4,6 @@ on:
push: push:
branches: branches:
- dev - dev
- v2
pull_request: pull_request:
workflow_dispatch: workflow_dispatch:
@@ -75,9 +74,13 @@ jobs:
working-directory: packages/client working-directory: packages/client
run: bun run check:generated run: bun run check:generated
- name: Run HttpApi exerciser gates
if: runner.os == 'Linux'
working-directory: packages/opencode
run: bun run test:httpapi
e2e: e2e:
name: e2e (${{ matrix.settings.name }}) name: e2e (${{ matrix.settings.name }})
if: github.ref_name != 'v2' && github.head_ref != 'v2'
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
+1 -10
View File
@@ -94,7 +94,7 @@
"name": "@opencode-ai/cli", "name": "@opencode-ai/cli",
"version": "1.17.11", "version": "1.17.11",
"bin": { "bin": {
"opencode2": "./bin/opencode2.cjs", "lildax": "./bin/lildax.cjs",
}, },
"dependencies": { "dependencies": {
"@effect/platform-node": "catalog:", "@effect/platform-node": "catalog:",
@@ -104,17 +104,13 @@
"@opencode-ai/tui": "workspace:*", "@opencode-ai/tui": "workspace:*",
"@opentui/core": "catalog:", "@opentui/core": "catalog:",
"@opentui/solid": "catalog:", "@opentui/solid": "catalog:",
"@parcel/watcher": "2.5.1",
"effect": "catalog:", "effect": "catalog:",
"jsonc-parser": "3.3.1",
"semver": "catalog:",
"solid-js": "catalog:", "solid-js": "catalog:",
}, },
"devDependencies": { "devDependencies": {
"@opencode-ai/script": "workspace:*", "@opencode-ai/script": "workspace:*",
"@tsconfig/bun": "catalog:", "@tsconfig/bun": "catalog:",
"@types/bun": "catalog:", "@types/bun": "catalog:",
"@types/semver": "catalog:",
"@typescript/native-preview": "catalog:", "@typescript/native-preview": "catalog:",
}, },
}, },
@@ -320,7 +316,6 @@
"ai-gateway-provider": "3.1.2", "ai-gateway-provider": "3.1.2",
"bun-pty": "0.4.8", "bun-pty": "0.4.8",
"cross-spawn": "catalog:", "cross-spawn": "catalog:",
"diff": "catalog:",
"drizzle-orm": "catalog:", "drizzle-orm": "catalog:",
"effect": "catalog:", "effect": "catalog:",
"fuzzysort": "3.1.0", "fuzzysort": "3.1.0",
@@ -939,7 +934,6 @@
"@opentui/core": "catalog:", "@opentui/core": "catalog:",
"@opentui/keymap": "catalog:", "@opentui/keymap": "catalog:",
"@opentui/solid": "catalog:", "@opentui/solid": "catalog:",
"@solid-primitives/event-bus": "1.1.2",
"clipboardy": "4.0.0", "clipboardy": "4.0.0",
"diff": "catalog:", "diff": "catalog:",
"effect": "catalog:", "effect": "catalog:",
@@ -1055,7 +1049,6 @@
"@modelcontextprotocol/sdk@1.29.0": "patches/@modelcontextprotocol%2Fsdk@1.29.0.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", "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", "@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", "@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", "@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", "@tanstack/solid-virtual@3.13.28": "patches/@tanstack%2Fsolid-virtual@3.13.28.patch",
@@ -5983,8 +5976,6 @@
"@openauthjs/openauth/jose": ["jose@5.9.6", "", {}, "sha512-AMlnetc9+CV9asI19zHmrgS/WYsWUwCn2R7RzlbJWD7F9eWYUTGyBmU9o6PxngtLGOiDGPRu+Uc4fhKzbpteZQ=="], "@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/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.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-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=="], "@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=="],
+4 -4
View File
@@ -1,8 +1,8 @@
{ {
"nodeModules": { "nodeModules": {
"x86_64-linux": "sha256-JXQ9PAqqRlJtHa8T3ZxqdRRyxC+0ip+2wSnehvKXUbI=", "x86_64-linux": "sha256-rB/CcrBUQVZ08nBFSYA8u2w88rQmTpKxKPkIreDEKgI=",
"aarch64-linux": "sha256-nI+RaxDXmAcjhSjCtIvyi292xBEg0E5NXaoyGrJE69s=", "aarch64-linux": "sha256-ZRTphtic8Ip96MnILteFgZAUxjK9O4YfJu2O6u/0H8k=",
"aarch64-darwin": "sha256-UleKpm7Khf3kibm4BqZJ9bmu0N2kOjNd77g1Bd2tmfw=", "aarch64-darwin": "sha256-VK5XIzraP0HtqnPwPCejiDKer4ewtNtX1vxP5uuyjSk=",
"x86_64-darwin": "sha256-V4AQH868dOfVhEj1mKTnZlhpZwFqBYsHS28Tx9VXHkE=" "x86_64-darwin": "sha256-ZLPHqcCZB1EmxQk95cmUpiODTTKOyi7PSF0yr/rDk6Y="
} }
} }
+2 -4
View File
@@ -2,12 +2,11 @@
"$schema": "https://json.schemastore.org/package.json", "$schema": "https://json.schemastore.org/package.json",
"name": "opencode", "name": "opencode",
"description": "AI-powered development tool", "description": "AI-powered development tool",
"version": "0.0.0",
"private": true, "private": true,
"type": "module", "type": "module",
"packageManager": "bun@1.3.14", "packageManager": "bun@1.3.14",
"scripts": { "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:desktop": "bun --cwd packages/desktop dev",
"dev:web": "bun --cwd packages/app dev", "dev:web": "bun --cwd packages/app dev",
"dev:console": "ulimit -n 10240 2>/dev/null; bun run --cwd packages/console/app dev", "dev:console": "ulimit -n 10240 2>/dev/null; bun run --cwd packages/console/app dev",
@@ -154,7 +153,6 @@
"@tanstack/solid-virtual@3.13.28": "patches/@tanstack%2Fsolid-virtual@3.13.28.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", "@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", "@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", "@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"
} }
} }
+22 -77
View File
@@ -3,7 +3,6 @@ import { batch, createEffect, onCleanup, onMount } from "solid-js"
import { createStore } from "solid-js/store" import { createStore } from "solid-js/store"
import { makeEventListener } from "@solid-primitives/event-listener" import { makeEventListener } from "@solid-primitives/event-listener"
import { Tooltip } from "@opencode-ai/ui/tooltip" import { Tooltip } from "@opencode-ai/ui/tooltip"
import { TooltipV2 } from "@opencode-ai/ui/v2/tooltip-v2"
import { useLanguage } from "@/context/language" import { useLanguage } from "@/context/language"
type Mem = Performance & { type Mem = Performance & {
@@ -52,62 +51,31 @@ const bad = (n: number | undefined, limit: number, low = false) => {
const session = (path: string) => path.includes("/session") const session = (path: string) => path.includes("/session")
function Cell(props: { function Cell(props: { bad?: boolean; dim?: boolean; label: string; tip: string; value: string; wide?: boolean }) {
bad?: boolean
dim?: boolean
inline?: boolean
label: string
tip: string
value: string
wide?: boolean
}) {
const content = () => (
<div
classList={{
"flex min-w-0 items-center": true,
"min-h-[20px] w-fit flex-row justify-start gap-1.5 px-1.5 py-0.5 text-left": !!props.inline,
"justify-center text-center": !props.inline,
"min-h-[42px] w-full flex-col rounded-[8px] px-0.5 py-1": !props.inline,
"col-span-2": !!props.wide && !props.inline,
}}
>
<div
classList={{
"text-[10px] leading-none font-black uppercase tracking-[0.04em] opacity-70": true,
}}
>
{props.label}
</div>
<div
classList={{
"uppercase leading-none font-bold tabular-nums": true,
"text-[11px]": !!props.inline,
"text-[13px] sm:text-[14px]": !props.inline,
"text-text-on-critical-base": !!props.bad,
"opacity-70": !!props.dim,
}}
>
{props.value}
</div>
</div>
)
if (props.inline) {
return (
<TooltipV2 value={props.tip} placement="top">
{content()}
</TooltipV2>
)
}
return ( return (
<Tooltip value={props.tip} placement="top"> <Tooltip value={props.tip} placement="top">
{content()} <div
classList={{
"flex min-h-[42px] w-full min-w-0 flex-col items-center justify-center rounded-[8px] px-0.5 py-1 text-center": true,
"col-span-2": !!props.wide,
}}
>
<div class="text-[10px] leading-none font-black uppercase tracking-[0.04em] opacity-70">{props.label}</div>
<div
classList={{
"text-[13px] leading-none font-bold tabular-nums sm:text-[14px]": true,
"text-text-on-critical-base": !!props.bad,
"opacity-70": !!props.dim,
}}
>
{props.value}
</div>
</div>
</Tooltip> </Tooltip>
) )
} }
export function DebugBar(props: { inline?: boolean } = {}) { export function DebugBar() {
const language = useLanguage() const language = useLanguage()
const location = useLocation() const location = useLocation()
const routing = useIsRouting() const routing = useIsRouting()
@@ -133,7 +101,7 @@ export function DebugBar(props: { inline?: boolean } = {}) {
}, },
}) })
const na = () => language.t("debugBar.na").toUpperCase() const na = () => language.t("debugBar.na")
const heap = () => (state.heap.limit ? (state.heap.used ?? 0) / state.heap.limit : undefined) const heap = () => (state.heap.limit ? (state.heap.used ?? 0) / state.heap.limit : undefined)
const heapv = () => { const heapv = () => {
const value = heap() const value = heap()
@@ -395,30 +363,15 @@ export function DebugBar(props: { inline?: boolean } = {}) {
return ( return (
<aside <aside
aria-label={language.t("debugBar.ariaLabel")} aria-label={language.t("debugBar.ariaLabel")}
classList={{ class="pointer-events-auto fixed bottom-3 right-3 z-50 hidden w-[308px] max-w-[calc(100vw-1.5rem)] overflow-hidden rounded-xl border border-border-base bg-surface-raised-stronger-non-alpha p-0.5 text-text-strong shadow-[var(--shadow-lg-border-base)] md:block sm:bottom-4 sm:right-4 sm:w-[324px]"
"pointer-events-auto hidden overflow-hidden text-text-strong md:block": true,
"mt-[-6px] w-full shrink-0 px-3 py-1": !!props.inline,
"fixed bottom-3 right-3 z-50 w-[308px] max-w-[calc(100vw-1.5rem)] rounded-xl border border-border-base bg-surface-raised-stronger-non-alpha p-0.5 shadow-[var(--shadow-lg-border-base)] sm:bottom-4 sm:right-4 sm:w-[324px]":
!props.inline,
}}
> >
<div <div class="grid grid-cols-5 gap-px font-mono">
classList={{
"font-mono": true,
"gap-[9px]": !!props.inline,
"gap-px": !props.inline,
"flex w-full flex-nowrap items-center justify-start": !!props.inline,
"grid-cols-5": !props.inline,
grid: !props.inline,
}}
>
<Cell <Cell
label={language.t("debugBar.nav.label")} label={language.t("debugBar.nav.label")}
tip={language.t("debugBar.nav.tip")} tip={language.t("debugBar.nav.tip")}
value={navv()} value={navv()}
bad={bad(state.nav.dur, 400)} bad={bad(state.nav.dur, 400)}
dim={state.nav.dur === undefined && !state.nav.pending} dim={state.nav.dur === undefined && !state.nav.pending}
inline={props.inline}
/> />
<Cell <Cell
label={language.t("debugBar.fps.label")} label={language.t("debugBar.fps.label")}
@@ -426,7 +379,6 @@ export function DebugBar(props: { inline?: boolean } = {}) {
value={state.fps === undefined ? na() : `${Math.round(state.fps)}`} value={state.fps === undefined ? na() : `${Math.round(state.fps)}`}
bad={bad(state.fps, 50, true)} bad={bad(state.fps, 50, true)}
dim={state.fps === undefined} dim={state.fps === undefined}
inline={props.inline}
/> />
<Cell <Cell
label={language.t("debugBar.frame.label")} label={language.t("debugBar.frame.label")}
@@ -434,7 +386,6 @@ export function DebugBar(props: { inline?: boolean } = {}) {
value={time(state.gap) ?? na()} value={time(state.gap) ?? na()}
bad={bad(state.gap, 50)} bad={bad(state.gap, 50)}
dim={state.gap === undefined} dim={state.gap === undefined}
inline={props.inline}
/> />
<Cell <Cell
label={language.t("debugBar.jank.label")} label={language.t("debugBar.jank.label")}
@@ -442,7 +393,6 @@ export function DebugBar(props: { inline?: boolean } = {}) {
value={state.jank === undefined ? na() : `${state.jank}`} value={state.jank === undefined ? na() : `${state.jank}`}
bad={bad(state.jank, 8)} bad={bad(state.jank, 8)}
dim={state.jank === undefined} dim={state.jank === undefined}
inline={props.inline}
/> />
<Cell <Cell
label={language.t("debugBar.long.label")} label={language.t("debugBar.long.label")}
@@ -450,7 +400,6 @@ export function DebugBar(props: { inline?: boolean } = {}) {
value={longv()} value={longv()}
bad={bad(state.long.block, 200)} bad={bad(state.long.block, 200)}
dim={state.long.count === undefined} dim={state.long.count === undefined}
inline={props.inline}
/> />
<Cell <Cell
label={language.t("debugBar.delay.label")} label={language.t("debugBar.delay.label")}
@@ -458,7 +407,6 @@ export function DebugBar(props: { inline?: boolean } = {}) {
value={time(state.delay) ?? na()} value={time(state.delay) ?? na()}
bad={bad(state.delay, 100)} bad={bad(state.delay, 100)}
dim={state.delay === undefined} dim={state.delay === undefined}
inline={props.inline}
/> />
<Cell <Cell
label={language.t("debugBar.inp.label")} label={language.t("debugBar.inp.label")}
@@ -466,7 +414,6 @@ export function DebugBar(props: { inline?: boolean } = {}) {
value={time(state.inp) ?? na()} value={time(state.inp) ?? na()}
bad={bad(state.inp, 200)} bad={bad(state.inp, 200)}
dim={state.inp === undefined} dim={state.inp === undefined}
inline={props.inline}
/> />
<Cell <Cell
label={language.t("debugBar.cls.label")} label={language.t("debugBar.cls.label")}
@@ -474,7 +421,6 @@ export function DebugBar(props: { inline?: boolean } = {}) {
value={state.cls === undefined ? na() : state.cls.toFixed(2)} value={state.cls === undefined ? na() : state.cls.toFixed(2)}
bad={bad(state.cls, 0.1)} bad={bad(state.cls, 0.1)}
dim={state.cls === undefined} dim={state.cls === undefined}
inline={props.inline}
/> />
<Cell <Cell
label={language.t("debugBar.mem.label")} label={language.t("debugBar.mem.label")}
@@ -489,7 +435,6 @@ export function DebugBar(props: { inline?: boolean } = {}) {
value={heapv()} value={heapv()}
bad={bad(heap(), 0.8)} bad={bad(heap(), 0.8)}
dim={state.heap.used === undefined} dim={state.heap.used === undefined}
inline={props.inline}
wide wide
/> />
</div> </div>
@@ -65,7 +65,6 @@ export function DialogConnectProvider(props: { provider: string; directory?: Acc
const [store, setStore] = createStore({ const [store, setStore] = createStore({
methodIndex: undefined as undefined | number, methodIndex: undefined as undefined | number,
authorization: undefined as undefined | ProviderAuthAuthorization, authorization: undefined as undefined | ProviderAuthAuthorization,
promptInputs: undefined as undefined | Record<string, string>,
state: "pending" as undefined | "pending" | "complete" | "error" | "prompt", state: "pending" as undefined | "pending" | "complete" | "error" | "prompt",
error: undefined as string | undefined, error: undefined as string | undefined,
}) })
@@ -74,7 +73,6 @@ export function DialogConnectProvider(props: { provider: string; directory?: Acc
| { type: "method.select"; index: number } | { type: "method.select"; index: number }
| { type: "method.reset" } | { type: "method.reset" }
| { type: "auth.prompt" } | { type: "auth.prompt" }
| { type: "auth.inputs"; inputs: Record<string, string> }
| { type: "auth.pending" } | { type: "auth.pending" }
| { type: "auth.complete"; authorization: ProviderAuthAuthorization } | { type: "auth.complete"; authorization: ProviderAuthAuthorization }
| { type: "auth.error"; error: string } | { type: "auth.error"; error: string }
@@ -85,7 +83,6 @@ export function DialogConnectProvider(props: { provider: string; directory?: Acc
if (action.type === "method.select") { if (action.type === "method.select") {
draft.methodIndex = action.index draft.methodIndex = action.index
draft.authorization = undefined draft.authorization = undefined
draft.promptInputs = undefined
draft.state = undefined draft.state = undefined
draft.error = undefined draft.error = undefined
return return
@@ -93,7 +90,6 @@ export function DialogConnectProvider(props: { provider: string; directory?: Acc
if (action.type === "method.reset") { if (action.type === "method.reset") {
draft.methodIndex = undefined draft.methodIndex = undefined
draft.authorization = undefined draft.authorization = undefined
draft.promptInputs = undefined
draft.state = undefined draft.state = undefined
draft.error = undefined draft.error = undefined
return return
@@ -103,12 +99,6 @@ export function DialogConnectProvider(props: { provider: string; directory?: Acc
draft.error = undefined draft.error = undefined
return return
} }
if (action.type === "auth.inputs") {
draft.promptInputs = action.inputs
draft.state = undefined
draft.error = undefined
return
}
if (action.type === "auth.pending") { if (action.type === "auth.pending") {
draft.state = "pending" draft.state = "pending"
draft.error = undefined draft.error = undefined
@@ -161,15 +151,6 @@ export function DialogConnectProvider(props: { provider: string; directory?: Acc
const method = methods()[index] const method = methods()[index]
dispatch({ type: "method.select", 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.type === "oauth") {
if (method.prompts?.length && !inputs) { if (method.prompts?.length && !inputs) {
dispatch({ type: "auth.prompt" }) dispatch({ type: "auth.prompt" })
@@ -209,7 +190,7 @@ export function DialogConnectProvider(props: { provider: string; directory?: Acc
} }
} }
function AuthPromptsView() { function OAuthPromptsView() {
const [formStore, setFormStore] = createStore({ const [formStore, setFormStore] = createStore({
value: {} as Record<string, string>, value: {} as Record<string, string>,
index: 0, index: 0,
@@ -217,7 +198,8 @@ export function DialogConnectProvider(props: { provider: string; directory?: Acc
const prompts = createMemo<NonNullable<ProviderAuthMethod["prompts"]>>(() => { const prompts = createMemo<NonNullable<ProviderAuthMethod["prompts"]>>(() => {
const value = method() const value = method()
return value?.prompts ?? [] if (value?.type !== "oauth") return []
return value.prompts ?? []
}) })
const matches = (prompt: NonNullable<ReturnType<typeof prompts>[number]>, value: Record<string, string>) => { const matches = (prompt: NonNullable<ReturnType<typeof prompts>[number]>, value: Record<string, string>) => {
if (!prompt.when) return true if (!prompt.when) return true
@@ -248,10 +230,6 @@ export function DialogConnectProvider(props: { provider: string; directory?: Acc
setFormStore("index", next) setFormStore("index", next)
return return
} }
if (method()?.type === "api") {
dispatch({ type: "auth.inputs", inputs: value })
return
}
await selectMethod(store.methodIndex, value) await selectMethod(store.methodIndex, value)
} }
@@ -436,7 +414,6 @@ export function DialogConnectProvider(props: { provider: string; directory?: Acc
auth: { auth: {
type: "api", type: "api",
key: apiKey, key: apiKey,
...(store.promptInputs ? { metadata: store.promptInputs } : {}),
}, },
}) })
await complete() await complete()
@@ -645,7 +622,7 @@ export function DialogConnectProvider(props: { provider: string; directory?: Acc
</div> </div>
</Match> </Match>
<Match when={store.state === "prompt"}> <Match when={store.state === "prompt"}>
<AuthPromptsView /> <OAuthPromptsView />
</Match> </Match>
<Match when={store.state === "error"}> <Match when={store.state === "error"}>
<div class="text-14-regular text-text-base"> <div class="text-14-regular text-text-base">
+20 -57
View File
@@ -35,8 +35,6 @@ import { DockShellForm, DockTray } from "@opencode-ai/ui/dock-surface"
import { Icon } from "@opencode-ai/ui/icon" import { Icon } from "@opencode-ai/ui/icon"
import { ProviderIcon } from "@opencode-ai/ui/provider-icon" import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip" import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip"
import { KeybindV2 } from "@opencode-ai/ui/v2/keybind-v2"
import { TooltipV2 } from "@opencode-ai/ui/v2/tooltip-v2"
import { IconButton } from "@opencode-ai/ui/icon-button" import { IconButton } from "@opencode-ai/ui/icon-button"
import { Select } from "@opencode-ai/ui/select" import { Select } from "@opencode-ai/ui/select"
import { useDialog } from "@opencode-ai/ui/context/dialog" import { useDialog } from "@opencode-ai/ui/context/dialog"
@@ -1364,7 +1362,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
shouldAnimate: providersShouldFadeIn(), shouldAnimate: providersShouldFadeIn(),
paid: props.controls.model.paid, paid: props.controls.model.paid,
title: language.t("command.model.choose"), title: language.t("command.model.choose"),
keybind: command.keybindParts("model.choose"), keybind: command.keybind("model.choose"),
model: props.controls.model.selection, model: props.controls.model.selection,
providerID: props.controls.model.selection.current()?.provider?.id, providerID: props.controls.model.selection.current()?.provider?.id,
modelName: props.controls.model.selection.current()?.name ?? language.t("dialog.model.select.title"), modelName: props.controls.model.selection.current()?.name ?? language.t("dialog.model.select.title"),
@@ -1381,7 +1379,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
const showAgentControl = createMemo(() => props.controls.agents.visible && props.controls.agents.options.length > 0) const showAgentControl = createMemo(() => props.controls.agents.visible && props.controls.agents.options.length > 0)
const agentControlState = createMemo<ComposerAgentControlState>(() => ({ const agentControlState = createMemo<ComposerAgentControlState>(() => ({
title: language.t("command.agent.cycle"), title: language.t("command.agent.cycle"),
keybind: command.keybindParts("agent.cycle"), keybind: command.keybind("agent.cycle"),
options: props.controls.agents.options, options: props.controls.agents.options,
current: props.controls.agents.current, current: props.controls.agents.current,
style: control(), style: control(),
@@ -1499,14 +1497,10 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
<div class="flex h-11 items-center px-2"> <div class="flex h-11 items-center px-2">
<div class="flex min-w-0 flex-1 items-center gap-0"> <div class="flex min-w-0 flex-1 items-center gap-0">
{fileAttachmentInput()} {fileAttachmentInput()}
<TooltipV2 <TooltipKeybind
placement="top" placement="top"
value={ title={language.t("prompt.action.attachFile")}
<> keybind={command.keybind("file.attach")}
{language.t("prompt.action.attachFile")}
<KeybindV2 keys={command.keybindParts("file.attach")} variant="neutral" />
</>
}
> >
<IconButton <IconButton
data-action="prompt-attach" data-action="prompt-attach"
@@ -1520,7 +1514,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
tabIndex={store.mode === "normal" ? undefined : -1} tabIndex={store.mode === "normal" ? undefined : -1}
aria-label={language.t("prompt.action.attachFile")} aria-label={language.t("prompt.action.attachFile")}
/> />
</TooltipV2> </TooltipKeybind>
<Show when={showAgentControl()}> <Show when={showAgentControl()}>
<ComposerAgentControl state={agentControlState()} /> <ComposerAgentControl state={agentControlState()} />
</Show> </Show>
@@ -1535,15 +1529,11 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
!props.controls.model.selection.variant.current() && !store.variantOpen, !props.controls.model.selection.variant.current() && !store.variantOpen,
}} }}
> >
<TooltipV2 <TooltipKeybind
placement="top" placement="top"
gutter={4} gutter={4}
value={ title={language.t("command.model.variant.cycle")}
<> keybind={command.keybind("model.variant.cycle")}
{language.t("command.model.variant.cycle")}
<KeybindV2 keys={command.keybindParts("model.variant.cycle")} variant="neutral" />
</>
}
> >
<Select <Select
size="normal" size="normal"
@@ -1561,11 +1551,11 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
triggerProps={{ "data-action": "prompt-model-variant" }} triggerProps={{ "data-action": "prompt-model-variant" }}
variant="ghost" variant="ghost"
/> />
</TooltipV2> </TooltipKeybind>
</div> </div>
</Show> </Show>
</div> </div>
<TooltipV2 placement="top" inactive={!working() && blank()} value={tip()}> <Tooltip placement="top" inactive={!working() && blank()} value={tip()}>
<IconButton <IconButton
data-action="prompt-submit" data-action="prompt-submit"
type="submit" type="submit"
@@ -1580,7 +1570,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
}} }}
aria-label={stopping() ? language.t("prompt.action.stop") : language.t("prompt.action.send")} aria-label={stopping() ? language.t("prompt.action.stop") : language.t("prompt.action.send")}
/> />
</TooltipV2> </Tooltip>
</div> </div>
</DockShellForm> </DockShellForm>
</div> </div>
@@ -1926,7 +1916,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
type ComposerAgentControlState = { type ComposerAgentControlState = {
title: string title: string
keybind: string[] keybind: string
options: string[] options: string[]
current: string current: string
style: JSX.CSSProperties | undefined style: JSX.CSSProperties | undefined
@@ -1938,7 +1928,7 @@ type ComposerModelControlState = {
shouldAnimate: boolean shouldAnimate: boolean
paid: boolean paid: boolean
title: string title: string
keybind: string[] keybind: string
model: ReturnType<typeof useLocal>["model"] model: ReturnType<typeof useLocal>["model"]
providerID?: string providerID?: string
modelName: string modelName: string
@@ -1953,16 +1943,7 @@ function ComposerAgentControl(props: { state: ComposerAgentControlState }) {
<div class="pointer-events-none absolute left-2 top-1/2 z-10 flex size-4 -translate-y-1/2 items-center justify-center text-v2-icon-icon-muted"> <div class="pointer-events-none absolute left-2 top-1/2 z-10 flex size-4 -translate-y-1/2 items-center justify-center text-v2-icon-icon-muted">
<Icon name="sliders" size="small" /> <Icon name="sliders" size="small" />
</div> </div>
<TooltipV2 <TooltipKeybind placement="top" gutter={4} title={props.state.title} keybind={props.state.keybind}>
placement="top"
gutter={4}
value={
<>
{props.state.title}
<KeybindV2 keys={props.state.keybind} variant="neutral" />
</>
}
>
<Select <Select
size="normal" size="normal"
options={props.state.options} options={props.state.options}
@@ -1974,7 +1955,7 @@ function ComposerAgentControl(props: { state: ComposerAgentControlState }) {
triggerProps={{ "data-action": "prompt-agent" }} triggerProps={{ "data-action": "prompt-agent" }}
variant="ghost" variant="ghost"
/> />
</TooltipV2> </TooltipKeybind>
</div> </div>
) )
} }
@@ -1985,16 +1966,7 @@ function ComposerModelControl(props: { state: ComposerModelControlState }) {
<Show <Show
when={props.state.paid} when={props.state.paid}
fallback={ fallback={
<TooltipV2 <TooltipKeybind placement="top" gutter={4} title={props.state.title} keybind={props.state.keybind}>
placement="top"
gutter={4}
value={
<>
{props.state.title}
<KeybindV2 keys={props.state.keybind} variant="neutral" />
</>
}
>
<Button <Button
data-action="prompt-model" data-action="prompt-model"
as="div" as="div"
@@ -2019,19 +1991,10 @@ function ComposerModelControl(props: { state: ComposerModelControlState }) {
<Icon name="chevron-down" size="small" class="text-v2-icon-icon-muted" /> <Icon name="chevron-down" size="small" class="text-v2-icon-icon-muted" />
</span> </span>
</Button> </Button>
</TooltipV2> </TooltipKeybind>
} }
> >
<TooltipV2 <TooltipKeybind placement="top" gutter={4} title={props.state.title} keybind={props.state.keybind}>
placement="top"
gutter={4}
value={
<>
{props.state.title}
<KeybindV2 keys={props.state.keybind} variant="neutral" />
</>
}
>
<ModelSelectorPopover <ModelSelectorPopover
model={props.state.model} model={props.state.model}
triggerAs={Button} triggerAs={Button}
@@ -2060,7 +2023,7 @@ function ComposerModelControl(props: { state: ComposerModelControlState }) {
<Icon name="chevron-down" size="small" class="text-v2-icon-icon-muted" /> <Icon name="chevron-down" size="small" class="text-v2-icon-icon-muted" />
</span> </span>
</ModelSelectorPopover> </ModelSelectorPopover>
</TooltipV2> </TooltipKeybind>
</Show> </Show>
</Show> </Show>
) )
+1 -3
View File
@@ -99,8 +99,6 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
const path = () => `${location.pathname}${location.search}${location.hash}` const path = () => `${location.pathname}${location.search}${location.hash}`
const creating = createMemo(() => { const creating = createMemo(() => {
const route = layout.route()
if (route.type === "draft" || route.type === "dir-new-sesssion") return true
if (!params.dir) return false if (!params.dir) return false
if (params.id) return false if (params.id) return false
const parts = location.pathname.replace(/\/+$/, "").split("/") const parts = location.pathname.replace(/\/+$/, "").split("/")
@@ -468,7 +466,7 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
}} }}
onReorder={(keys) => tabsStoreActions.reorder(keys)} onReorder={(keys) => tabsStoreActions.reorder(keys)}
/> />
<Show when={!creating()}> <Show when={!(creating() && params.dir)}>
<TooltipV2 <TooltipV2
placement="bottom" placement="bottom"
value={ value={
+8 -115
View File
@@ -1,5 +1,5 @@
import { describe, expect, test } from "bun:test" import { describe, expect, test } from "bun:test"
import { coalesceServerEvents, enqueueServerEvent, resumeStreamAfterPageShow } from "./server-sdk" import { coalesceServerEvents, resumeStreamAfterPageShow } from "./server-sdk"
import type { Event } from "@opencode-ai/sdk/v2/client" import type { Event } from "@opencode-ai/sdk/v2/client"
describe("resumeStreamAfterPageShow", () => { describe("resumeStreamAfterPageShow", () => {
@@ -15,23 +15,19 @@ describe("resumeStreamAfterPageShow", () => {
}) })
describe("coalesceServerEvents", () => { describe("coalesceServerEvents", () => {
const delta = (value: string, field = "text", partID = "part") => ({ const delta = (value: string, field = "text") => ({
directory: "/repo", directory: "/repo",
payload: { payload: {
type: "message.part.delta", type: "message.part.delta",
properties: { messageID: "msg", partID, field, delta: value }, properties: { messageID: "msg", partID: "part", field, delta: value },
} as Event, } as Event,
}) })
test("merges adjacent deltas for the same field", () => { test("merges adjacent deltas for the same field", () => {
const first = delta("hello ") const result = coalesceServerEvents([delta("hello "), delta("world")])
const second = delta("world")
first.payload.id = "first"
second.payload.id = "second"
const result = coalesceServerEvents([first, second])
expect(result).toHaveLength(1) expect(result).toHaveLength(1)
expect(result[0]?.payload).toMatchObject({ id: "second", properties: { delta: "hello world" } }) expect(result[0]?.payload).toMatchObject({ properties: { delta: "hello world" } })
}) })
test("preserves event boundaries and distinct fields", () => { test("preserves event boundaries and distinct fields", () => {
@@ -49,112 +45,9 @@ describe("coalesceServerEvents", () => {
]) ])
}) })
test("preserves event ID order across interleaved deltas", () => { test("drops stale deltas", () => {
const first = delta("a") const result = coalesceServerEvents([delta("stale")], new Set(["/repo:msg:part"]))
const other = delta("b", "text", "other")
const last = delta("c")
first.payload.id = "1"
other.payload.id = "2"
last.payload.id = "3"
const result = coalesceServerEvents([first, other, last]) expect(result).toEqual([])
expect(result.map((event) => event.payload.id)).toEqual(["1", "2", "3"])
})
})
describe("enqueueServerEvent", () => {
const partUpdated = (text: string) =>
({
type: "message.part.updated",
properties: {
sessionID: "session",
part: { id: "part", sessionID: "session", messageID: "message", type: "text", text },
},
}) as Event
test("preserves part updates across message remove and re-add barriers", () => {
const events: Array<{ directory: string; payload: Event }> = []
const enqueue = (payload: Event) => enqueueServerEvent(events, { directory: "/repo", payload })
enqueue(partUpdated("old"))
enqueue({ type: "message.removed", properties: { sessionID: "session", messageID: "message" } } as Event)
enqueue({
type: "message.updated",
properties: {
sessionID: "session",
info: {
id: "message",
sessionID: "session",
role: "user",
time: { created: 1 },
agent: "build",
model: { providerID: "provider", modelID: "model" },
},
},
} as Event)
enqueue(partUpdated("new"))
expect(events.map((event) => event.payload.type)).toEqual([
"message.part.updated",
"message.removed",
"message.updated",
"message.part.updated",
])
})
test("preserves deltas after a replacement snapshot", () => {
const events: Array<{ directory: string; payload: Event }> = []
const enqueue = (payload: Event) => enqueueServerEvent(events, { directory: "/repo", payload })
enqueue(partUpdated("a"))
enqueue(partUpdated("ab"))
enqueue({
type: "message.part.delta",
properties: { sessionID: "session", messageID: "message", partID: "part", field: "text", delta: "c" },
} as Event)
const result = coalesceServerEvents(events)
expect(result.map((event) => event.payload.type)).toEqual(["message.part.updated", "message.part.delta"])
expect(result[0]?.payload).toMatchObject({ properties: { part: { text: "ab" } } })
expect(result[1]?.payload).toMatchObject({ properties: { delta: "c" } })
})
test("preserves updates after session deletion", () => {
const events: Array<{ directory: string; payload: Event }> = []
const enqueue = (payload: Event) => enqueueServerEvent(events, { directory: "/repo", payload })
enqueue(partUpdated("old"))
enqueue({
type: "session.deleted",
properties: { sessionID: "session", info: { id: "session" } },
} as Event)
enqueue(partUpdated("new"))
expect(events.map((event) => event.payload.type)).toEqual([
"message.part.updated",
"session.deleted",
"message.part.updated",
])
})
test("does not coalesce edge-triggered session statuses", () => {
const events: Array<{ directory: string; payload: Event }> = []
const enqueue = (status: "retry" | "busy") =>
enqueueServerEvent(events, {
directory: "/repo",
payload: {
type: "session.status",
properties: {
sessionID: "session",
status: status === "retry" ? { type: "retry", attempt: 1, message: "retry", next: 1 } : { type: "busy" },
},
} as Event,
})
enqueue("retry")
enqueue("busy")
expect(events).toHaveLength(2)
}) })
}) })
+50 -41
View File
@@ -17,56 +17,34 @@ const isAbortError = (error: unknown) =>
const isStreamClosed = (error: unknown, signal?: AbortSignal) => isAbortError(error) || signal?.aborted === true const isStreamClosed = (error: unknown, signal?: AbortSignal) => isAbortError(error) || signal?.aborted === true
type QueuedServerEvent = { directory: string; payload: Event } type QueuedServerEvent = { directory: string; payload: Event }
const coalescedKey = (event: QueuedServerEvent) => { const deltaKey = (directory: string, messageID: string, partID: string) => `${directory}:${messageID}:${partID}`
if (event.payload.type === "lsp.updated") return `lsp.updated:${event.directory}`
if (event.payload.type === "message.part.updated") {
const part = event.payload.properties.part
return `message.part.updated:${event.directory}:${part.messageID}:${part.id}`
}
return undefined
}
export function enqueueServerEvent(queue: QueuedServerEvent[], event: QueuedServerEvent) { export function coalesceServerEvents(events: QueuedServerEvent[], stale?: Set<string>) {
const key = coalescedKey(event)
const previous = queue[queue.length - 1]
if (key && previous && coalescedKey(previous) === key) {
queue[queue.length - 1] = event
return false
}
queue.push(event)
return true
}
export function coalesceServerEvents(events: QueuedServerEvent[]) {
const output: QueuedServerEvent[] = [] const output: QueuedServerEvent[] = []
const deltas = new Map<string, number>()
events.forEach((event) => { events.forEach((event) => {
if (stale && event.payload.type === "message.part.delta") {
const props = event.payload.properties
if (stale.has(deltaKey(event.directory, props.messageID, props.partID))) return
}
if (event.payload.type !== "message.part.delta") { if (event.payload.type !== "message.part.delta") {
deltas.clear()
output.push(event) output.push(event)
return return
} }
const props = event.payload.properties const props = event.payload.properties
const previous = output[output.length - 1] const id = `${deltaKey(event.directory, props.messageID, props.partID)}:${props.field}`
if ( const index = deltas.get(id)
!previous || const existing = index === undefined ? undefined : output[index]
previous.payload.type !== "message.part.delta" || if (!existing || existing.payload.type !== "message.part.delta") {
previous.directory !== event.directory || deltas.set(id, output.length)
previous.payload.properties.messageID !== props.messageID ||
previous.payload.properties.partID !== props.partID ||
previous.payload.properties.field !== props.field
) {
output.push({ output.push({
directory: event.directory, directory: event.directory,
payload: { ...event.payload, properties: { ...props } }, payload: { ...event.payload, properties: { ...props } },
}) })
return return
} }
output[output.length - 1] = { existing.payload.properties.delta += props.delta
directory: event.directory,
payload: {
...event.payload,
properties: { ...props, delta: previous.payload.properties.delta + props.delta },
},
}
}) })
return output return output
} }
@@ -107,9 +85,20 @@ function createServerSdkContextBase(server: ServerConnection.Any, scope: ServerS
let queue: Queued[] = [] let queue: Queued[] = []
let buffer: Queued[] = [] let buffer: Queued[] = []
const coalesced = new Map<string, number>()
const staleDeltas = new Set<string>()
let timer: ReturnType<typeof setTimeout> | undefined let timer: ReturnType<typeof setTimeout> | undefined
let last = 0 let last = 0
const key = (directory: string, payload: Event) => {
if (payload.type === "session.status") return `session.status:${directory}:${payload.properties.sessionID}`
if (payload.type === "lsp.updated") return `lsp.updated:${directory}`
if (payload.type === "message.part.updated") {
const part = payload.properties.part
return `message.part.updated:${directory}:${part.messageID}:${part.id}`
}
}
const flush = () => { const flush = () => {
if (timer) clearTimeout(timer) if (timer) clearTimeout(timer)
timer = undefined timer = undefined
@@ -117,12 +106,15 @@ function createServerSdkContextBase(server: ServerConnection.Any, scope: ServerS
if (queue.length === 0) return if (queue.length === 0) return
const events = queue const events = queue
const skip = staleDeltas.size > 0 ? new Set(staleDeltas) : undefined
queue = buffer queue = buffer
buffer = events buffer = events
queue.length = 0 queue.length = 0
coalesced.clear()
staleDeltas.clear()
last = Date.now() last = Date.now()
const output = coalesceServerEvents(events) const output = coalesceServerEvents(events, skip)
batch(() => { batch(() => {
output.forEach((event) => emitter.emit(event.directory, event.payload)) output.forEach((event) => emitter.emit(event.directory, event.payload))
}) })
@@ -192,12 +184,29 @@ function createServerSdkContextBase(server: ServerConnection.Any, scope: ServerS
for await (const event of events.stream) { for await (const event of events.stream) {
resetHeartbeat() resetHeartbeat()
streamErrorLogged = false streamErrorLogged = false
if (event.payload.type !== "sync") { const directory = event.directory ?? "global"
const directory = event.directory ?? "global" if (event.payload.type === "sync") {
const payload = event.payload as Event continue
if (enqueueServerEvent(queue, { directory, payload })) schedule()
} }
const payload = event.payload as Event
const k = key(directory, payload)
if (k) {
const i = coalesced.get(k)
if (i !== undefined) {
queue[i] = { directory, payload }
if (payload.type === "message.part.updated") {
const part = payload.properties.part
staleDeltas.add(deltaKey(directory, part.messageID, part.id))
}
continue
}
coalesced.set(k, queue.length)
}
queue.push({ directory, payload })
schedule()
if (Date.now() - yielded < STREAM_YIELD_MS) continue if (Date.now() - yielded < STREAM_YIELD_MS) continue
yielded = Date.now() yielded = Date.now()
await wait(0) await wait(0)
File diff suppressed because it is too large Load Diff
+66 -537
View File
@@ -18,67 +18,44 @@ import { rootSession } from "@/utils/session-route"
import { dropSessionCaches, pickSessionCacheEvictions, SESSION_CACHE_LIMIT } from "./global-sync/session-cache" import { dropSessionCaches, pickSessionCacheEvictions, SESSION_CACHE_LIMIT } from "./global-sync/session-cache"
const cmp = (a: string, b: string) => (a < b ? -1 : a > b ? 1 : 0) const cmp = (a: string, b: string) => (a < b ? -1 : a > b ? 1 : 0)
const cmpMessage = (a: Message, b: Message) => a.time.created - b.time.created || cmp(a.id, b.id)
const SKIP_PARTS = new Set(["patch", "step-start", "step-finish"]) const SKIP_PARTS = new Set(["patch", "step-start", "step-finish"])
const initialMessagePageSize = 2 const initialMessagePageSize = 2
const historyMessagePageSize = 200 const historyMessagePageSize = 200
const sessionInfoLimit = 2_048 const sessionInfoLimit = 2_048
const emptyIDs: ReadonlySet<string> = new Set()
type OptimisticItem = { type OptimisticItem = {
message: Message message: Message
parts: Part[] parts: Part[]
confirmedParts?: Part[]
confirmedMessage?: boolean
} }
type MessagePage = { const hasParts = (parts: Part[] | undefined, want: Part[]) => {
session: Message[] if (!parts) return want.length === 0
part: { id: string; part: Part[] }[] return want.every((part) => Binary.search(parts, part.id, (item) => item.id).found)
cursor?: string
complete: boolean
} }
// Most markers describe the current HTTP attempt; deltaParts persists non-durable stream state across retries. function mergeOptimisticPage(
type MessageLoadState = { page: { session: Message[]; part: { id: string; part: Part[] }[]; cursor?: string; complete: boolean },
touchedMessages: Set<string> items: OptimisticItem[],
removedMessages: Set<string> ) {
retainedMessages: Set<string> if (items.length === 0) return { ...page, confirmed: [] as string[] }
touchedParts: Map<string, Set<string>>
deltaParts: Map<string, Set<string>>
carriedDeltaParts: Map<string, Set<string>>
removedParts: Map<string, Set<string>>
optimisticParts: Map<string, Set<string>>
orphanParents: Set<string>
clearedMessageParts: Set<string>
}
function mergeOptimisticPage(page: MessagePage, items: OptimisticItem[]) {
if (items.length === 0) return { ...page, observed: [] as { messageID: string; parts: Part[] }[] }
const session = [...page.session] const session = [...page.session]
const part = new Map(page.part.map((item) => [item.id, item.part])) const part = new Map(page.part.map((item) => [item.id, item.part]))
const observed: { messageID: string; parts: Part[] }[] = [] const confirmed: string[] = []
for (const item of items) { for (const item of items) {
const result = Binary.search(session, item.message.id, (message) => message.id) const result = Binary.search(session, item.message.id, (message) => message.id)
if (!result.found) session.splice(result.index, 0, item.message) if (!result.found) session.splice(result.index, 0, item.message)
const current = part.get(item.message.id) const current = part.get(item.message.id)
const confirmed = result.found if (result.found && hasParts(current, item.parts)) {
? item.parts.filter((part) => Binary.search(current ?? [], part.id, (value) => value.id).found) confirmed.push(item.message.id)
: [] continue
if (result.found) observed.push({ messageID: item.message.id, parts: confirmed }) }
part.set( part.set(item.message.id, merge(current ?? [], item.parts))
item.message.id,
merge(
result.found ? (current ?? []) : merge(item.confirmedParts ?? [], current ?? []),
item.parts.filter((part) => !confirmed.includes(part)),
),
)
} }
return { return {
...page, ...page,
session, session,
part: [...part.entries()].sort((a, b) => cmp(a[0], b[0])).map(([id, parts]) => ({ id, part: parts })), part: [...part.entries()].sort((a, b) => cmp(a[0], b[0])).map(([id, parts]) => ({ id, part: parts })),
observed, confirmed,
} }
} }
@@ -98,38 +75,7 @@ function merge<T extends { id: string }>(a: readonly T[], b: readonly T[]) {
return [...items.values()].sort((x, y) => cmp(x.id, y.id)) return [...items.values()].sort((x, y) => cmp(x.id, y.id))
} }
function reconcileFetched<T extends { id: string }>( export function createServerSession(client: OpencodeClient) {
fetched: T[],
current: readonly T[],
options: {
touched?: ReadonlySet<string>
retained?: ReadonlySet<string>
preserveUnfetched?: boolean | ((item: T) => boolean)
} = {},
) {
const result = new Map(fetched.map((item) => [item.id, item]))
const live = new Map(current.map((item) => [item.id, item]))
if (options.preserveUnfetched) {
for (const item of current) {
if (!result.has(item.id) && (options.preserveUnfetched === true || options.preserveUnfetched(item)))
result.set(item.id, item)
}
}
for (const id of options.retained ?? emptyIDs) {
if (result.has(id)) continue
const item = live.get(id)
if (item) result.set(id, item)
}
// Events observed while the request is pending are the freshest client state for those identities.
for (const id of options.touched ?? emptyIDs) {
const item = live.get(id)
if (item) result.set(id, item)
if (!item) result.delete(id)
}
return [...result.values()].sort((a, b) => cmp(a.id, b.id))
}
export function createServerSession(client: OpencodeClient, options?: { retry?: typeof retry }) {
const [data, setData] = createStore({ const [data, setData] = createStore({
info: {} as Record<string, Session | undefined>, info: {} as Record<string, Session | undefined>,
session_status: {} as Record<string, SessionStatus>, session_status: {} as Record<string, SessionStatus>,
@@ -149,32 +95,10 @@ export function createServerSession(client: OpencodeClient, options?: { retry?:
const inflightDiff = new Map<string, Promise<void>>() const inflightDiff = new Map<string, Promise<void>>()
const inflightTodo = new Map<string, Promise<void>>() const inflightTodo = new Map<string, Promise<void>>()
const optimistic = new Map<string, Map<string, OptimisticItem>>() const optimistic = new Map<string, Map<string, OptimisticItem>>()
const messageLoads = new Map<string, MessageLoadState>()
const pendingParts = new Map<string, Map<string, Set<string>>>()
const orphanParts = new Map<string, Set<string>>()
const removedMessages = new Map<string, Set<string>>()
const deltaBases = new Map<string, { base: string; sessionID: string }>()
const deleteMessageParts = (
cache: { part: Record<string, Part[] | undefined>; part_text_accum_delta: Record<string, string | undefined> },
messageID: string,
) => {
for (const part of cache.part[messageID] ?? []) {
delete cache.part_text_accum_delta[part.id]
deltaBases.delete(part.id)
}
delete cache.part[messageID]
}
const seen = new Set<string>() const seen = new Set<string>()
const infoSeen = new Set<string>() const infoSeen = new Set<string>()
const pinned = new Map<string, number>() const pinned = new Map<string, number>()
const generations = new Map<string, object>() const generations = new Map<string, number>()
const generation = (sessionID: string) => {
const current = generations.get(sessionID)
if (current) return current
const created = {}
generations.set(sessionID, created)
return created
}
const [meta, setMeta] = createStore({ const [meta, setMeta] = createStore({
limit: {} as Record<string, number | undefined>, limit: {} as Record<string, number | undefined>,
cursor: {} as Record<string, string | undefined>, cursor: {} as Record<string, string | undefined>,
@@ -191,11 +115,6 @@ export function createServerSession(client: OpencodeClient, options?: { retry?:
const preserve = new Set([ const preserve = new Set([
...pinned.keys(), ...pinned.keys(),
...requests.keys(), ...requests.keys(),
...inflight.keys(),
...inflightDiff.keys(),
...inflightTodo.keys(),
...messageLoads.keys(),
...optimistic.keys(),
...Object.entries(data.permission) ...Object.entries(data.permission)
.filter(([, items]) => items.length > 0) .filter(([, items]) => items.length > 0)
.map(([sessionID]) => sessionID), .map(([sessionID]) => sessionID),
@@ -219,7 +138,6 @@ export function createServerSession(client: OpencodeClient, options?: { retry?:
if (!preserve.has(sessionID)) stale.push(sessionID) if (!preserve.has(sessionID)) stale.push(sessionID)
} }
stale.forEach((sessionID) => infoSeen.delete(sessionID)) stale.forEach((sessionID) => infoSeen.delete(sessionID))
stale.forEach((sessionID) => generations.delete(sessionID))
setData( setData(
"info", "info",
produce((draft) => stale.forEach((sessionID) => delete draft[sessionID])), produce((draft) => stale.forEach((sessionID) => delete draft[sessionID])),
@@ -233,27 +151,21 @@ export function createServerSession(client: OpencodeClient, options?: { retry?:
if (cached && !options?.force) return Promise.resolve(cached) if (cached && !options?.force) return Promise.resolve(cached)
const pending = requests.get(sessionID) const pending = requests.get(sessionID)
if (pending) return pending if (pending) return pending
const active = generation(sessionID) const generation = generations.get(sessionID) ?? 0
const request = client.session.get({ sessionID }).then((result) => { const request = client.session.get({ sessionID }).then((result) => {
if (!result.data) throw new Error(`Session not found: ${sessionID}`) if (!result.data) throw new Error(`Session not found: ${sessionID}`)
if (generations.get(sessionID) !== active) return result.data if ((generations.get(sessionID) ?? 0) !== generation) return result.data
return remember(result.data) return remember(result.data)
}) })
requests.set(sessionID, request) requests.set(sessionID, request)
const cleanup = () => { void request.then(
if (requests.get(sessionID) === request) requests.delete(sessionID) () => {
if ( if (requests.get(sessionID) === request) requests.delete(sessionID)
generations.get(sessionID) === active && },
!data.info[sessionID] && () => {
!requests.has(sessionID) && if (requests.get(sessionID) === request) requests.delete(sessionID)
!messageLoads.has(sessionID) && },
!inflight.has(sessionID) && )
!inflightDiff.has(sessionID) &&
!inflightTodo.has(sessionID)
)
generations.delete(sessionID)
}
void request.then(cleanup, cleanup)
return request return request
} }
@@ -283,121 +195,15 @@ export function createServerSession(client: OpencodeClient, options?: { retry?:
if (items.size === 0) optimistic.delete(sessionID) if (items.size === 0) optimistic.delete(sessionID)
} }
const clearOptimisticPart = (sessionID: string, messageID: string, partID: string) => {
const items = optimistic.get(sessionID)
const item = items?.get(messageID)
if (!items || !item) return
const parts = item.parts.filter((part) => part.id !== partID)
const confirmedParts = item.confirmedParts?.filter((part) => part.id !== partID)
if (parts.length === 0) {
clearOptimistic(sessionID, messageID)
return
}
items.set(messageID, { ...item, parts, confirmedParts, confirmedMessage: true })
}
const confirmOptimisticPart = (sessionID: string, messageID: string, part: Part) => {
const items = optimistic.get(sessionID)
const item = items?.get(messageID)
if (!items || !item) return
const parts = item.parts.filter((value) => value.id !== part.id)
if (parts.length === 0) {
clearOptimistic(sessionID, messageID)
return
}
items.set(messageID, {
...item,
parts,
confirmedParts: merge(item.confirmedParts ?? [], [part]),
confirmedMessage: true,
})
}
const confirmOptimistic = (sessionID: string, messageID: string, confirmedParts: Part[]) => {
const items = optimistic.get(sessionID)
const item = items?.get(messageID)
if (!items || !item) return
const confirmed = new Set(confirmedParts.map((part) => part.id))
const parts = item.parts.filter((part) => !confirmed.has(part.id))
if (parts.length === 0) {
clearOptimistic(sessionID, messageID)
return
}
items.set(messageID, {
...item,
parts,
confirmedParts: merge(item.confirmedParts ?? [], confirmedParts),
confirmedMessage: true,
})
}
const trackPartChange = (sessionID: string, messageID: string, partID: string) => {
const load = messageLoads.get(sessionID)
if (!load) return
// A part event keeps an existing parent when the fetched page omits it without overriding fetched metadata.
const messages = data.message[sessionID]
if (messages && Binary.search(messages, messageID, (message) => message.id).found)
load.retainedMessages.add(messageID)
const parts = load.touchedParts.get(messageID)
if (parts) {
parts.add(partID)
return
}
load.touchedParts.set(messageID, new Set([partID]))
}
const resetMessageLoad = (sessionID: string, load: MessageLoadState) => {
load.touchedMessages.clear()
load.retainedMessages.clear()
load.touchedParts.clear()
load.carriedDeltaParts.clear()
load.clearedMessageParts.clear()
for (const messageID of load.removedMessages) {
load.touchedMessages.add(messageID)
load.clearedMessageParts.add(messageID)
}
for (const [messageID, parts] of load.deltaParts) {
load.touchedParts.set(messageID, new Set(parts))
load.carriedDeltaParts.set(messageID, new Set(parts))
const messages = data.message[sessionID]
if (messages && Binary.search(messages, messageID, (message) => message.id).found)
load.retainedMessages.add(messageID)
}
for (const [messageID, parts] of load.removedParts) {
const touched = load.touchedParts.get(messageID) ?? new Set<string>()
parts.forEach((partID) => touched.add(partID))
load.touchedParts.set(messageID, touched)
const messages = data.message[sessionID]
if (messages && Binary.search(messages, messageID, (message) => message.id).found)
load.retainedMessages.add(messageID)
}
for (const [messageID, parts] of load.optimisticParts) {
load.removedMessages.delete(messageID)
load.clearedMessageParts.add(messageID)
load.touchedMessages.add(messageID)
const touched = load.touchedParts.get(messageID) ?? new Set<string>()
parts.forEach((partID) => touched.add(partID))
load.touchedParts.set(messageID, touched)
}
}
const evict = (sessionIDs: string[]) => { const evict = (sessionIDs: string[]) => {
if (sessionIDs.length === 0) return if (sessionIDs.length === 0) return
const evicted = new Set(sessionIDs)
for (const [partID, item] of deltaBases) {
if (evicted.has(item.sessionID)) deltaBases.delete(partID)
}
sessionIDs.forEach((sessionID) => { sessionIDs.forEach((sessionID) => {
generations.delete(sessionID) generations.set(sessionID, (generations.get(sessionID) ?? 0) + 1)
clearOptimistic(sessionID) clearOptimistic(sessionID)
requests.delete(sessionID) requests.delete(sessionID)
inflight.delete(sessionID) inflight.delete(sessionID)
inflightDiff.delete(sessionID) inflightDiff.delete(sessionID)
inflightTodo.delete(sessionID) inflightTodo.delete(sessionID)
messageLoads.delete(sessionID)
pendingParts.delete(sessionID)
orphanParts.delete(sessionID)
removedMessages.delete(sessionID)
}) })
setData( setData(
produce((draft) => { produce((draft) => {
@@ -424,7 +230,6 @@ export function createServerSession(client: OpencodeClient, options?: { retry?:
...inflight.keys(), ...inflight.keys(),
...inflightDiff.keys(), ...inflightDiff.keys(),
...inflightTodo.keys(), ...inflightTodo.keys(),
...messageLoads.keys(),
...optimistic.keys(), ...optimistic.keys(),
...Object.entries(data.permission) ...Object.entries(data.permission)
.filter(([, items]) => items.length > 0) .filter(([, items]) => items.length > 0)
@@ -442,11 +247,8 @@ export function createServerSession(client: OpencodeClient, options?: { retry?:
pickSessionCacheEvictions({ seen, keep: sessionID, limit: SESSION_CACHE_LIMIT, preserve: protectedSessions() }), pickSessionCacheEvictions({ seen, keep: sessionID, limit: SESSION_CACHE_LIMIT, preserve: protectedSessions() }),
) )
const fetchMessages = async (sessionID: string, limit: number, before?: string, onAttempt?: () => void) => { const fetchMessages = async (sessionID: string, limit: number, before?: string) => {
const response = await (options?.retry ?? retry)(() => { const response = await retry(() => client.session.messages({ sessionID, limit, before }))
onAttempt?.()
return client.session.messages({ sessionID, limit, before })
})
const items = (response.data ?? []).filter((item) => !!item?.info?.id) const items = (response.data ?? []).filter((item) => !!item?.info?.id)
return { return {
session: items.map((item) => cleanMessage(item.info)).sort((a, b) => cmp(a.id, b.id)), session: items.map((item) => cleanMessage(item.info)).sort((a, b) => cmp(a.id, b.id)),
@@ -459,153 +261,30 @@ export function createServerSession(client: OpencodeClient, options?: { retry?:
} }
} }
const replaceMessages = (sessionID: string, messages: Message[]) => {
const messageIDs = new Set(messages.map((message) => message.id))
const dropped = (data.message[sessionID] ?? []).filter((message) => !messageIDs.has(message.id))
setData("message", sessionID, reconcile(messages, { key: "id" }))
setData(
produce((draft) => {
for (const message of dropped) deleteMessageParts(draft, message.id)
}),
)
return messageIDs
}
const replaceParts = (
sessionID: string,
items: MessagePage["part"],
messageIDs: Set<string>,
load?: MessageLoadState,
) => {
for (const item of items) {
if (!messageIDs.has(item.id)) continue
const fetched = load?.clearedMessageParts.has(item.id)
? []
: item.part.filter((part) => !SKIP_PARTS.has(part.type))
const fetchedIDs = new Set(fetched.map((part) => part.id))
const pending = pendingParts.get(sessionID)?.get(item.id)
const touched = new Set([...(load?.touchedParts.get(item.id) ?? []), ...(pending ?? [])])
for (const part of fetched) {
const accumulated = data.part_text_accum_delta[part.id]
const base = deltaBases.get(part.id)?.base
const preserveDelta =
base !== undefined &&
accumulated !== undefined &&
"text" in part &&
typeof part.text === "string" &&
part.text.startsWith(base) &&
accumulated.startsWith(part.text) &&
accumulated !== part.text
if (preserveDelta) touched.add(part.id)
if (load?.carriedDeltaParts.get(item.id)?.has(part.id) && !preserveDelta) touched.delete(part.id)
}
for (const partID of load?.carriedDeltaParts.get(item.id) ?? []) {
if (!fetchedIDs.has(partID)) touched.delete(partID)
}
const parts = reconcileFetched(fetched, data.part[item.id] ?? [], { touched })
if (!parts.length) {
orphanParts.get(sessionID)?.delete(item.id)
setData(produce((draft) => deleteMessageParts(draft, item.id)))
continue
}
const partIDs = new Set(parts.map((part) => part.id))
setData(
"part_text_accum_delta",
produce((draft) => {
for (const part of data.part[item.id] ?? []) {
if (!partIDs.has(part.id) || !touched.has(part.id)) {
delete draft[part.id]
deltaBases.delete(part.id)
}
}
}),
)
setData("part", item.id, reconcile(parts, { key: "id" }))
orphanParts.get(sessionID)?.delete(item.id)
}
}
const applyMessagePage = (
sessionID: string,
page: MessagePage,
load: MessageLoadState | undefined,
preserveUnfetched: boolean | ((message: Message) => boolean),
cleanupOrphans: boolean,
) => {
const merged = mergeOptimisticPage(page, [...(optimistic.get(sessionID)?.values() ?? [])])
merged.observed.forEach((item) => {
if (!load?.clearedMessageParts.has(item.messageID)) confirmOptimistic(sessionID, item.messageID, item.parts)
})
const touchedMessages = new Set([...(load?.touchedMessages ?? []), ...(removedMessages.get(sessionID) ?? [])])
const messages = reconcileFetched(merged.session, data.message[sessionID] ?? [], {
touched: touchedMessages,
retained: load?.retainedMessages,
preserveUnfetched,
})
batch(() => {
const messageIDs = replaceMessages(sessionID, messages)
replaceParts(sessionID, merged.part, messageIDs, load)
const orphans = orphanParts.get(sessionID)
if (cleanupOrphans && page.complete && orphans) {
for (const messageID of orphans) {
if (!messageIDs.has(messageID)) setData(produce((draft) => deleteMessageParts(draft, messageID)))
}
orphanParts.delete(sessionID)
}
setMeta("limit", sessionID, messages.length)
setMeta("cursor", sessionID, merged.cursor)
setMeta("complete", sessionID, merged.complete)
setMeta("at", sessionID, Date.now())
})
}
const loadMessages = async (sessionID: string, limit: number, before?: string, mode?: "replace" | "prepend") => { const loadMessages = async (sessionID: string, limit: number, before?: string, mode?: "replace" | "prepend") => {
if (meta.loading[sessionID]) return if (meta.loading[sessionID]) return
const active = generation(sessionID) const generation = generations.get(sessionID) ?? 0
const load: MessageLoadState = {
touchedMessages: new Set(),
removedMessages: new Set(),
retainedMessages: new Set(),
touchedParts: new Map(),
deltaParts: new Map(),
carriedDeltaParts: new Map(),
removedParts: new Map(),
optimisticParts: new Map(),
orphanParents: new Set(),
clearedMessageParts: new Set(),
}
messageLoads.set(sessionID, load)
setMeta("loading", sessionID, true) setMeta("loading", sessionID, true)
let applied = false await fetchMessages(sessionID, limit, before)
await fetchMessages(sessionID, limit, before, () => resetMessageLoad(sessionID, load))
.then((page) => { .then((page) => {
if (generations.get(sessionID) !== active) return if ((generations.get(sessionID) ?? 0) !== generation) return
const first = page.session.reduce<Message | undefined>( const next = mergeOptimisticPage(page, [...(optimistic.get(sessionID)?.values() ?? [])])
(oldest, message) => (!oldest || cmpMessage(message, oldest) < 0 ? message : oldest), next.confirmed.forEach((messageID) => clearOptimistic(sessionID, messageID))
undefined, const messages = mode === "prepend" ? merge(data.message[sessionID] ?? [], next.session) : next.session
) batch(() => {
const preserveUnfetched = setData("message", sessionID, reconcile(messages, { key: "id" }))
mode === "prepend" || (!page.complete && (!first || ((message: Message) => cmpMessage(message, first) < 0))) for (const item of next.part) {
applyMessagePage( const parts = item.part.filter((part) => !SKIP_PARTS.has(part.type))
sessionID, if (parts.length) setData("part", item.id, reconcile(parts, { key: "id" }))
page, }
messageLoads.get(sessionID) === load ? load : undefined, setMeta("limit", sessionID, messages.length)
preserveUnfetched, setMeta("cursor", sessionID, next.cursor)
mode !== "prepend", setMeta("complete", sessionID, next.complete)
) setMeta("at", sessionID, Date.now())
applied = true })
}) })
.finally(() => { .finally(() => {
if (!applied && generations.get(sessionID) === active && messageLoads.get(sessionID) === load) { if ((generations.get(sessionID) ?? 0) === generation) setMeta("loading", sessionID, false)
for (const messageID of load.orphanParents) {
if (!orphanParts.get(sessionID)?.has(messageID)) continue
setData(produce((draft) => deleteMessageParts(draft, messageID)))
orphanParts.get(sessionID)?.delete(messageID)
}
if (orphanParts.get(sessionID)?.size === 0) orphanParts.delete(sessionID)
}
if (messageLoads.get(sessionID) === load) messageLoads.delete(sessionID)
if (generations.get(sessionID) === active) setMeta("loading", sessionID, false)
}) })
} }
@@ -660,13 +339,7 @@ export function createServerSession(client: OpencodeClient, options?: { retry?:
const eventID = eventSessionID(event) const eventID = eventSessionID(event)
if (eventID) { if (eventID) {
touch(eventID) touch(eventID)
if ( if (!data.info[eventID]) void resolve(eventID).catch(() => {})
!data.info[eventID] &&
event.type !== "session.created" &&
event.type !== "session.updated" &&
event.type !== "session.deleted"
)
void resolve(eventID).catch(() => {})
} }
switch (event.type) { switch (event.type) {
case "session.created": case "session.created":
@@ -705,21 +378,6 @@ export function createServerSession(client: OpencodeClient, options?: { retry?:
} }
case "message.updated": { case "message.updated": {
const info = cleanMessage((event.properties as { info: Message }).info) const info = cleanMessage((event.properties as { info: Message }).info)
const load = messageLoads.get(info.sessionID)
load?.touchedMessages.add(info.id)
load?.removedMessages.delete(info.id)
const items = optimistic.get(info.sessionID)
const item = items?.get(info.id)
if (items && item) {
if (item.parts.length === 0) clearOptimistic(info.sessionID, info.id)
if (item.parts.length > 0) items.set(info.id, { ...item, confirmedMessage: true })
}
const orphans = orphanParts.get(info.sessionID)
orphans?.delete(info.id)
if (orphans?.size === 0) orphanParts.delete(info.sessionID)
const removedMessagesForSession = removedMessages.get(info.sessionID)
removedMessagesForSession?.delete(info.id)
if (removedMessagesForSession?.size === 0) removedMessages.delete(info.sessionID)
const messages = data.message[info.sessionID] const messages = data.message[info.sessionID]
if (!messages) { if (!messages) {
setData("message", info.sessionID, [info]) setData("message", info.sessionID, [info])
@@ -737,20 +395,6 @@ export function createServerSession(client: OpencodeClient, options?: { retry?:
} }
case "message.removed": { case "message.removed": {
const props = event.properties as { sessionID: string; messageID: string } const props = event.properties as { sessionID: string; messageID: string }
const load = messageLoads.get(props.sessionID)
load?.touchedMessages.add(props.messageID)
load?.removedMessages.add(props.messageID)
load?.clearedMessageParts.add(props.messageID)
load?.deltaParts.delete(props.messageID)
load?.carriedDeltaParts.delete(props.messageID)
load?.removedParts.delete(props.messageID)
load?.optimisticParts.delete(props.messageID)
pendingParts.get(props.sessionID)?.delete(props.messageID)
if (pendingParts.get(props.sessionID)?.size === 0) pendingParts.delete(props.sessionID)
const removedMessagesForSession = removedMessages.get(props.sessionID) ?? new Set<string>()
removedMessagesForSession.add(props.messageID)
removedMessages.set(props.sessionID, removedMessagesForSession)
clearOptimistic(props.sessionID, props.messageID)
setData( setData(
produce((draft) => { produce((draft) => {
const messages = draft.message[props.sessionID] const messages = draft.message[props.sessionID]
@@ -758,7 +402,8 @@ export function createServerSession(client: OpencodeClient, options?: { retry?:
const result = Binary.search(messages, props.messageID, (message) => message.id) const result = Binary.search(messages, props.messageID, (message) => message.id)
if (result.found) messages.splice(result.index, 1) if (result.found) messages.splice(result.index, 1)
} }
deleteMessageParts(draft, props.messageID) for (const part of draft.part[props.messageID] ?? []) delete draft.part_text_accum_delta[part.id]
delete draft.part[props.messageID]
}), }),
) )
return return
@@ -766,42 +411,6 @@ export function createServerSession(client: OpencodeClient, options?: { retry?:
case "message.part.updated": { case "message.part.updated": {
const part = (event.properties as { part: Part }).part const part = (event.properties as { part: Part }).part
if (SKIP_PARTS.has(part.type)) return if (SKIP_PARTS.has(part.type)) return
const messages = data.message[part.sessionID]
const load = messageLoads.get(part.sessionID)
const missing = !messages || !Binary.search(messages, part.messageID, (message) => message.id).found
// Outside a page load, accepting a part without its ordered parent event would create an unbounded orphan.
if (
missing &&
(!load ||
load.clearedMessageParts.has(part.messageID) ||
removedMessages.get(part.sessionID)?.has(part.messageID))
)
return
if (missing) {
const orphans = orphanParts.get(part.sessionID) ?? new Set<string>()
orphans.add(part.messageID)
orphanParts.set(part.sessionID, orphans)
load?.orphanParents.add(part.messageID)
}
const deltas = load?.deltaParts.get(part.messageID)
deltas?.delete(part.id)
if (deltas?.size === 0) load?.deltaParts.delete(part.messageID)
const carried = load?.carriedDeltaParts.get(part.messageID)
carried?.delete(part.id)
if (carried?.size === 0) load?.carriedDeltaParts.delete(part.messageID)
const removed = load?.removedParts.get(part.messageID)
removed?.delete(part.id)
if (removed?.size === 0) load?.removedParts.delete(part.messageID)
const pending = pendingParts.get(part.sessionID)?.get(part.messageID)
pending?.delete(part.id)
if (pending?.size === 0) pendingParts.get(part.sessionID)?.delete(part.messageID)
if (pendingParts.get(part.sessionID)?.size === 0) pendingParts.delete(part.sessionID)
const optimistic = load?.optimisticParts.get(part.messageID)
optimistic?.delete(part.id)
if (optimistic?.size === 0) load?.optimisticParts.delete(part.messageID)
deltaBases.delete(part.id)
trackPartChange(part.sessionID, part.messageID, part.id)
confirmOptimisticPart(part.sessionID, part.messageID, part)
setData( setData(
"part_text_accum_delta", "part_text_accum_delta",
produce((draft) => void delete draft[part.id]), produce((draft) => void delete draft[part.id]),
@@ -822,34 +431,10 @@ export function createServerSession(client: OpencodeClient, options?: { retry?:
return return
} }
case "message.part.removed": { case "message.part.removed": {
const props = event.properties as { sessionID: string; messageID: string; partID: string } const props = event.properties as { messageID: string; partID: string }
// Part removal is event-only on the server, so its tombstone lasts until a later update or eviction.
const pending = pendingParts.get(props.sessionID) ?? new Map<string, Set<string>>()
const parts = pending.get(props.messageID) ?? new Set<string>()
parts.add(props.partID)
pending.set(props.messageID, parts)
pendingParts.set(props.sessionID, pending)
const deltas = messageLoads.get(props.sessionID)?.deltaParts.get(props.messageID)
deltas?.delete(props.partID)
if (deltas?.size === 0) messageLoads.get(props.sessionID)?.deltaParts.delete(props.messageID)
const load = messageLoads.get(props.sessionID)
const carried = load?.carriedDeltaParts.get(props.messageID)
carried?.delete(props.partID)
if (carried?.size === 0) load?.carriedDeltaParts.delete(props.messageID)
if (load) {
const parts = load.removedParts.get(props.messageID) ?? new Set<string>()
parts.add(props.partID)
load.removedParts.set(props.messageID, parts)
const optimistic = load.optimisticParts.get(props.messageID)
optimistic?.delete(props.partID)
if (optimistic?.size === 0) load.optimisticParts.delete(props.messageID)
}
trackPartChange(props.sessionID, props.messageID, props.partID)
clearOptimisticPart(props.sessionID, props.messageID, props.partID)
setData( setData(
produce((draft) => { produce((draft) => {
delete draft.part_text_accum_delta[props.partID] delete draft.part_text_accum_delta[props.partID]
deltaBases.delete(props.partID)
const parts = draft.part[props.messageID] const parts = draft.part[props.messageID]
if (!parts) return if (!parts) return
const result = Binary.search(parts, props.partID, (part) => part.id) const result = Binary.search(parts, props.partID, (part) => part.id)
@@ -860,31 +445,13 @@ export function createServerSession(client: OpencodeClient, options?: { retry?:
return return
} }
case "message.part.delta": { case "message.part.delta": {
const props = event.properties as { const props = event.properties as { messageID: string; partID: string; field: string; delta: string }
sessionID: string
messageID: string
partID: string
field: string
delta: string
}
const parts = data.part[props.messageID] const parts = data.part[props.messageID]
if (!parts) return if (!parts) return
const result = Binary.search(parts, props.partID, (part) => part.id) const result = Binary.search(parts, props.partID, (part) => part.id)
if (!result.found) return if (!result.found) return
trackPartChange(props.sessionID, props.messageID, props.partID)
const load = messageLoads.get(props.sessionID)
if (load) {
const parts = load.deltaParts.get(props.messageID) ?? new Set<string>()
parts.add(props.partID)
load.deltaParts.set(props.messageID, parts)
const carried = load.carriedDeltaParts.get(props.messageID)
carried?.delete(props.partID)
if (carried?.size === 0) load.carriedDeltaParts.delete(props.messageID)
}
const field = props.field as keyof (typeof parts)[number] const field = props.field as keyof (typeof parts)[number]
const current = parts[result.index]?.[field] const current = parts[result.index]?.[field]
if (!deltaBases.has(props.partID) && typeof current === "string")
deltaBases.set(props.partID, { base: current, sessionID: props.sessionID })
setData( setData(
"part_text_accum_delta", "part_text_accum_delta",
props.partID, props.partID,
@@ -992,70 +559,32 @@ export function createServerSession(client: OpencodeClient, options?: { retry?:
}, },
optimistic: { optimistic: {
add(input: { sessionID: string; message: Message; parts: Part[] }) { add(input: { sessionID: string; message: Message; parts: Part[] }) {
const parts = input.parts
.filter((part) => !!part?.id && !SKIP_PARTS.has(part.type))
.sort((a, b) => cmp(a.id, b.id))
const load = messageLoads.get(input.sessionID)
if (load?.clearedMessageParts.has(input.message.id)) {
const touched = load.touchedParts.get(input.message.id) ?? new Set<string>()
parts.forEach((part) => touched.add(part.id))
load.touchedParts.set(input.message.id, touched)
}
if (load) {
load.removedMessages.delete(input.message.id)
load.optimisticParts.set(input.message.id, new Set(parts.map((part) => part.id)))
}
const items = optimistic.get(input.sessionID) const items = optimistic.get(input.sessionID)
const removedMessagesForSession = removedMessages.get(input.sessionID) if (items) items.set(input.message.id, input)
removedMessagesForSession?.delete(input.message.id) if (!items) optimistic.set(input.sessionID, new Map([[input.message.id, input]]))
if (removedMessagesForSession?.size === 0) removedMessages.delete(input.sessionID)
if (items) items.set(input.message.id, { ...input, parts, confirmedParts: [] })
if (!items)
optimistic.set(input.sessionID, new Map([[input.message.id, { ...input, parts, confirmedParts: [] }]]))
setData("message", input.sessionID, (messages = []) => merge(messages, [input.message])) setData("message", input.sessionID, (messages = []) => merge(messages, [input.message]))
setData( setData(
"part_text_accum_delta", "part",
produce((draft) => { input.message.id,
for (const part of [...(data.part[input.message.id] ?? []), ...parts]) { input.parts.filter((part) => !!part?.id).sort((a, b) => cmp(a.id, b.id)),
delete draft[part.id]
deltaBases.delete(part.id)
}
}),
) )
setData("part", input.message.id, parts)
}, },
remove(input: { sessionID: string; messageID: string }) { remove(input: { sessionID: string; messageID: string }) {
const item = optimistic.get(input.sessionID)?.get(input.messageID)
if (!item) return
messageLoads.get(input.sessionID)?.optimisticParts.delete(input.messageID)
clearOptimistic(input.sessionID, input.messageID) clearOptimistic(input.sessionID, input.messageID)
if (item.confirmedMessage) {
const partIDs = new Set(item.parts.map((part) => part.id))
setData(
produce((draft) => {
for (const part of item.parts) {
delete draft.part_text_accum_delta[part.id]
deltaBases.delete(part.id)
}
const parts = draft.part[input.messageID]
if (!parts) return
draft.part[input.messageID] = parts.filter((part) => !partIDs.has(part.id))
if (draft.part[input.messageID]?.length === 0) delete draft.part[input.messageID]
}),
)
return
}
setData("message", input.sessionID, (messages) => messages?.filter((message) => message.id !== input.messageID)) setData("message", input.sessionID, (messages) => messages?.filter((message) => message.id !== input.messageID))
setData(produce((draft) => deleteMessageParts(draft, input.messageID))) setData(
"part",
produce((draft) => void delete draft[input.messageID]),
)
}, },
}, },
diff(sessionID: string, options?: { force?: boolean }) { diff(sessionID: string, options?: { force?: boolean }) {
touch(sessionID) touch(sessionID)
if (data.session_diff[sessionID] !== undefined && !options?.force) return Promise.resolve() if (data.session_diff[sessionID] !== undefined && !options?.force) return Promise.resolve()
return runInflight(inflightDiff, sessionID, () => { return runInflight(inflightDiff, sessionID, () => {
const active = generation(sessionID) const generation = generations.get(sessionID) ?? 0
return retry(() => client.session.diff({ sessionID })).then((result) => { return retry(() => client.session.diff({ sessionID })).then((result) => {
if (generations.get(sessionID) !== active) return if ((generations.get(sessionID) ?? 0) !== generation) return
setData("session_diff", sessionID, reconcile(cleanDiffs(result.data), { key: "file" })) setData("session_diff", sessionID, reconcile(cleanDiffs(result.data), { key: "file" }))
}) })
}) })
@@ -1064,9 +593,9 @@ export function createServerSession(client: OpencodeClient, options?: { retry?:
touch(sessionID) touch(sessionID)
if (data.todo[sessionID] !== undefined && !options?.force) return Promise.resolve() if (data.todo[sessionID] !== undefined && !options?.force) return Promise.resolve()
return runInflight(inflightTodo, sessionID, () => { return runInflight(inflightTodo, sessionID, () => {
const active = generation(sessionID) const generation = generations.get(sessionID) ?? 0
return retry(() => client.session.todo({ sessionID })).then((result) => { return retry(() => client.session.todo({ sessionID })).then((result) => {
if (generations.get(sessionID) !== active) return if ((generations.get(sessionID) ?? 0) !== generation) return
setData("todo", sessionID, reconcile(result.data ?? [], { key: "id" })) setData("todo", sessionID, reconcile(result.data ?? [], { key: "id" }))
}) })
}) })
+15 -17
View File
@@ -163,29 +163,27 @@ export const { use: useTabs, provider: TabsProvider } = createSimpleContext({
}, },
newDraft(draft: Omit<DraftTab, "type" | "draftID">, prompt?: string) { newDraft(draft: Omit<DraftTab, "type" | "draftID">, prompt?: string) {
const draftID = uuid() const draftID = uuid()
void startTransition(() => { setStore(
setStore( produce((tabs) => {
produce((tabs) => { tabs.push({ type: "draft", draftID, ...draft })
tabs.push({ type: "draft", draftID, ...draft }) }),
}), )
) navigate(prompt ? `${draftHref(draftID)}&prompt=${encodeURIComponent(prompt)}` : draftHref(draftID))
navigate(prompt ? `${draftHref(draftID)}&prompt=${encodeURIComponent(prompt)}` : draftHref(draftID))
})
}, },
updateDraft(draftID: string, draft: Partial<Omit<DraftTab, "type" | "draftID">>) { updateDraft(draftID: string, draft: Partial<Omit<DraftTab, "type" | "draftID">>) {
void startTransition(() => { setStore(
setStore( (tab) => tab.type === "draft" && tab.draftID === draftID,
(tab) => tab.type === "draft" && tab.draftID === draftID, produce((tab) => Object.assign(tab, draft)),
produce((tab) => Object.assign(tab, draft)), )
)
})
}, },
promoteDraft(draftID: string, session: Omit<SessionTab, "type">) { promoteDraft(draftID: string, session: Omit<SessionTab, "type">) {
// Keep the replacement and navigation atomic so /new-session never renders // We're viewing this draft when /new-session?draftId=… points at it. Promoting
// after its backing draft tab has been removed from the store. // replaces the draft tab with a session tab, so the draft route would stop resolving
// and fall back home. Navigate to the new session first so we leave /new-session
// before the draft is removed from the store.
const active = location.pathname === "/new-session" && location.query.draftId === draftID const active = location.pathname === "/new-session" && location.query.draftId === draftID
const next = { type: "session" as const, ...session } const next = { type: "session" as const, ...session }
void startTransition(() => { startTransition(() => {
setStore( setStore(
produce((tabs) => { produce((tabs) => {
const index = tabs.findIndex((tab) => tab.type === "draft" && tab.draftID === draftID) const index = tabs.findIndex((tab) => tab.type === "draft" && tab.draftID === draftID)
+6 -10
View File
@@ -302,7 +302,6 @@ export function NewHome() {
function selectProject(conn: ServerConnection.Any, directory: string) { function selectProject(conn: ServerConnection.Any, directory: string) {
const key = ServerConnection.key(conn) const key = ServerConnection.key(conn)
if (global.servers.health[key]?.healthy === false) return
if ( if (
!global !global
.ensureServerCtx(conn) .ensureServerCtx(conn)
@@ -409,7 +408,7 @@ export function NewHome() {
return ( return (
<div class="rounded-[10px] shadow-[var(--v2-elevation-raised)] m-2 min-h-0 lg:overflow-hidden bg-v2-background-bg-base self-stretch flex-1"> <div class="rounded-[10px] shadow-[var(--v2-elevation-raised)] m-2 min-h-0 lg:overflow-hidden bg-v2-background-bg-base self-stretch flex-1">
<div class="mx-auto grid h-full w-full max-w-[1080px] grid-rows-[auto_minmax(0,1fr)_auto] gap-4 px-3 lg:grid-cols-[280px_minmax(0,720px)] lg:grid-rows-1 lg:gap-8 lg:px-6"> <div class="mx-auto grid h-full w-full max-w-[1080px] grid-rows-[auto_minmax(0,1fr)_auto] gap-4 px-3 pb-3 lg:grid-cols-[280px_minmax(0,720px)] lg:grid-rows-1 lg:gap-8 lg:px-6 lg:pb-16">
<HomeProjectColumn <HomeProjectColumn
projects={projects()} projects={projects()}
selected={selection()} selected={selection()}
@@ -456,7 +455,7 @@ export function NewHome() {
onClose={closeSearch} onClose={closeSearch}
onSelect={selectSearchSession} onSelect={selectSearchSession}
/> />
<ScrollView class="mt-3 -mr-3 min-h-0 flex-1"> <ScrollView class="mt-3 min-h-0 flex-1">
<Show <Show
when={!sessionLoad.isLoading} when={!sessionLoad.isLoading}
fallback={ fallback={
@@ -469,7 +468,7 @@ export function NewHome() {
when={groups().length > 0} when={groups().length > 0}
fallback={<HomeSessionsEmpty onNewSession={newSessionProject() ? openNewSession : undefined} />} fallback={<HomeSessionsEmpty onNewSession={newSessionProject() ? openNewSession : undefined} />}
> >
<div class="flex flex-col gap-6 pt-3 pr-3 pb-16"> <div class="pt-3 flex flex-col gap-6">
<For each={groups()}> <For each={groups()}>
{(group, index) => ( {(group, index) => (
<div class="flex min-w-0 flex-col gap-4"> <div class="flex min-w-0 flex-col gap-4">
@@ -768,18 +767,15 @@ function HomeProjectRow(props: {
clearNotifications: (server: ServerConnection.Any, project: LocalProject) => void clearNotifications: (server: ServerConnection.Any, project: LocalProject) => void
language: ReturnType<typeof useLanguage> language: ReturnType<typeof useLanguage>
}) { }) {
const global = useGlobal()
const serverUnreachable = () => global.servers.health[ServerConnection.key(props.server)]?.healthy === false
const [state, setState] = createStore({ menuOpen: false }) const [state, setState] = createStore({ menuOpen: false })
return ( return (
<div class="group/project relative flex h-7 min-w-0 items-center rounded-[6px]"> <div class="group/project relative flex h-7 min-w-0 items-center rounded-[6px]">
<button <button
type="button" type="button"
data-component="home-project-row" data-component="home-project-row"
class={`${HOME_PROJECT_NAV_ROW} pr-16 disabled:opacity-60`} class={`${HOME_PROJECT_NAV_ROW} pr-16`}
data-selected={props.selected ? "" : undefined} data-selected={props.selected ? "" : undefined}
aria-current={props.selected ? "page" : undefined} aria-current={props.selected ? "page" : undefined}
disabled={serverUnreachable()}
onClick={() => props.selectProject(props.server, props.project.worktree)} onClick={() => props.selectProject(props.server, props.project.worktree)}
> >
<HomeProjectAvatar project={props.project} /> <HomeProjectAvatar project={props.project} />
@@ -969,7 +965,7 @@ function HomeSessionSearch(props: {
style={{ style={{
top: "-6px", top: "-6px",
left: "-6px", left: "-6px",
width: "calc(100% + 12px)", width: "calc(100% + 14px)",
}} }}
> >
<div class="flex flex-col pt-9"> <div class="flex flex-col pt-9">
@@ -1137,7 +1133,7 @@ function HomeSessionSearchResultRow(props: {
function HomeSessionGroupHeader(props: { title: string; onNewSession?: () => void }) { function HomeSessionGroupHeader(props: { title: string; onNewSession?: () => void }) {
const language = useLanguage() const language = useLanguage()
return ( return (
<div class="flex h-7 min-w-0 items-center justify-between pl-3"> <div class="flex h-7 min-w-0 items-center justify-between pl-[18px]">
<div class={HOME_SECTION_LABEL}>{props.title}</div> <div class={HOME_SECTION_LABEL}>{props.title}</div>
<Show when={props.onNewSession}> <Show when={props.onNewSession}>
{(onNewSession) => ( {(onNewSession) => (
+1 -1
View File
@@ -36,7 +36,7 @@ export default function NewLayout(props: ParentProps) {
<main class="flex-1 min-h-0 min-w-0 overflow-x-hidden flex flex-col items-start contain-strict"> <main class="flex-1 min-h-0 min-w-0 overflow-x-hidden flex flex-col items-start contain-strict">
<Suspense>{props.children}</Suspense> <Suspense>{props.children}</Suspense>
</main> </main>
{import.meta.env.DEV && <DebugBar inline />} {import.meta.env.DEV && <DebugBar />}
<HelpButton /> <HelpButton />
<ToastRegion v2 /> <ToastRegion v2 />
</div> </div>
-98
View File
@@ -1,98 +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. When behavior, copy, keyboard interaction, or layout is unclear, compare the local V2 TUI with the latest released legacy TUI.
- Run both versions in separate Terminal Control sessions and save PNG-only captures at equivalent states:
```bash
# From packages/cli: local V2 TUI
termctrl start opencode-v2-dev --host opentui --cols 112 --rows 34 -- bun dev --standalone
# Released legacy TUI behavior reference
termctrl start opencode-legacy --host opentui --cols 112 --rows 34 -- bunx opencode-ai@latest
termctrl save opencode-v2-dev --format png --out /tmp/opencode/v2.png
termctrl save opencode-legacy --format png --out /tmp/opencode/legacy.png
```
- Use the same viewport and send equivalent inputs to both sessions before comparing screenshots. The released CLI is a behavioral reference, not a source of V2 API design; keep the local implementation on V2 endpoints.
- Stop both sessions after comparison: `termctrl stop opencode-v2-dev` and `termctrl stop opencode-legacy`.
## Interactive debugging
- This package is the V2 CLI adapter. Run its `dev` script when testing the TUI; do not use the repository-root `bun dev`, which launches the legacy `packages/opencode` CLI.
- Run commands from `packages/cli`. Use `bun dev --standalone` for most debugging so the TUI starts with a private V2 server instead of depending on the background service.
- Use `termctrl` for interactive checks instead of starting the TUI as a blocking foreground process. It provides a real PTY, handles OpenTUI's host handshake, and can save reviewable screenshots.
- Use a dedicated session name and do not reuse or kill an unrelated session.
```bash
termctrl start opencode-v2-dev --host opentui --cols 112 --rows 34 -- bun dev --standalone
termctrl wait opencode-v2-dev "Ask anything" --timeout 20000
termctrl show opencode-v2-dev
```
- Wait for visible text before interacting instead of relying on fixed sleeps. Use the text expected from the screen under test, such as `Ask anything` or `Connect a provider`.
- Drive the running TUI with `termctrl send`. Prefix typed input with `text:` and send control keys separately so the interaction matches real terminal input.
```bash
termctrl send opencode-v2-dev 'text:example prompt' enter
termctrl send opencode-v2-dev ctrl-c
```
- Use `termctrl show` after each meaningful interaction and inspect the full visible screen for rendering errors, stale state, error toasts, and unexpected exits.
- Save PNG evidence for every user-visible bug and fix. Do not save text captures; inspect the rendered PNG. Write temporary captures outside the repository unless the artifact is intended to be committed.
```bash
termctrl save opencode-v2-dev --format png --out /tmp/opencode/v2-tui.png
```
- For resize-sensitive changes, resize the viewport, wait for the expected content, and capture the screen again:
```bash
termctrl resize opencode-v2-dev --cols 100 --rows 30
termctrl show opencode-v2-dev
```
- Source changes may require restarting the process. Use `termctrl restart opencode-v2-dev` rather than assuming the running TUI reloaded the change.
- To exercise background-service behavior, omit `--standalone`. Service lifecycle commands are available through `bun dev service start`, `bun dev service status`, and `bun dev service stop`.
- Always clean up the Terminal Control session when the check is complete:
```bash
termctrl stop opencode-v2-dev
```
## Server/API debugging
- Use `bun dev api --help` from `packages/cli` to inspect the API debugging command. It sends one request to the V2 server using the same daemon discovery/auth path as the CLI.
- Use `bun dev api` to introspect the server-side data backing the TUI. This is useful when debugging UI bugs: compare what the screen renders with the raw session, message, event, agent, or health data returned by the API to determine whether the bug is in the server state, the client data layer, or the TUI rendering.
- `bun dev api` accepts either an OpenAPI operation ID or a raw HTTP method plus path:
```bash
bun dev api get /health
bun dev api get /openapi.json
bun dev api <operationId> --param key=value
```
- Pass JSON request bodies with `--data`/`-d`; the command sets `content-type: application/json` automatically unless you provide a header. Add extra headers with `--header`/`-H name:value`.
- If no compatible background server is registered, `bun dev api` starts one through the daemon service. Use `bun dev service status`, `bun dev service restart`, and `bun dev service stop` when you need explicit lifecycle control.
- Prefer raw method/path calls for quick server debugging and operation IDs when exercising documented OpenAPI routes with path or query parameters.
## Debugger
- To debug the V2 CLI or TUI with Bun's inspector, launch the CLI entrypoint through Terminal Control with an inspector URL, then attach a debugger to that URL:
```bash
termctrl start opencode-v2-debug --host opentui --cols 112 --rows 34 -- \
bun run --inspect=ws://localhost:6499/ src/index.ts --standalone
```
- Use `--inspect-wait` or `--inspect-brk` when execution must pause until the debugger attaches.
- Use `termctrl logs opencode-v2-debug` for inspector output or startup failures emitted before the TUI renderer starts. Use `termctrl show` for the visible full-screen TUI.
## Verification
- Run `bun typecheck` from `packages/cli` after CLI adapter changes.
- Run `bun typecheck` and `bun test` from `packages/tui` after shared TUI changes. Do not run tests from the repository root.
- Treat automated checks and Terminal Control smoke tests as complementary. For user-visible changes, verify initial render, the changed interaction, Ctrl-C exit behavior, and save a screenshot of the corrected state.
@@ -31,11 +31,11 @@ function run(target) {
const envPath = process.env.OPENCODE_BIN_PATH const envPath = process.env.OPENCODE_BIN_PATH
const scriptDir = path.dirname(fs.realpathSync(__filename)) 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 platform = { darwin: "darwin", linux: "linux", win32: "windows" }[os.platform()] || os.platform()
const arch = { x64: "x64", arm64: "arm64", arm: "arm" }[os.arch()] || os.arch() const arch = { x64: "x64", arm64: "arm64", arm: "arm" }[os.arch()] || os.arch()
const base = "@opencode-ai/cli-" + platform + "-" + arch const base = "@opencode-ai/cli-" + platform + "-" + arch
const binary = platform === "windows" ? "opencode2.exe" : "opencode2" const binary = platform === "windows" ? "lildax.exe" : "lildax"
function supportsAvx2() { function supportsAvx2() {
if (arch !== "x64") return false if (arch !== "x64") return false
@@ -121,7 +121,7 @@ function findBinary(startDir) {
const resolved = envPath || (fs.existsSync(cached) ? cached : findBinary(scriptDir)) const resolved = envPath || (fs.existsSync(cached) ? cached : findBinary(scriptDir))
if (!resolved) { if (!resolved) {
console.error( 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 ") + names.map((name) => `"${name}"`).join(" or ") +
" package", " package",
) )
+1 -5
View File
@@ -5,7 +5,7 @@
"type": "module", "type": "module",
"license": "MIT", "license": "MIT",
"bin": { "bin": {
"opencode2": "./bin/opencode2.cjs" "lildax": "./bin/lildax.cjs"
}, },
"files": [ "files": [
"bin" "bin"
@@ -23,17 +23,13 @@
"@opencode-ai/tui": "workspace:*", "@opencode-ai/tui": "workspace:*",
"@opentui/core": "catalog:", "@opentui/core": "catalog:",
"@opentui/solid": "catalog:", "@opentui/solid": "catalog:",
"@parcel/watcher": "2.5.1",
"effect": "catalog:", "effect": "catalog:",
"jsonc-parser": "3.3.1",
"semver": "catalog:",
"solid-js": "catalog:" "solid-js": "catalog:"
}, },
"devDependencies": { "devDependencies": {
"@opencode-ai/script": "workspace:*", "@opencode-ai/script": "workspace:*",
"@tsconfig/bun": "catalog:", "@tsconfig/bun": "catalog:",
"@types/bun": "catalog:", "@types/bun": "catalog:",
"@types/semver": "catalog:",
"@typescript/native-preview": "catalog:" "@typescript/native-preview": "catalog:"
} }
} }
+1 -1
View File
@@ -10,7 +10,7 @@ import pkg from "../package.json"
import { modelsData } from "./generate" import { modelsData } from "./generate"
const dir = path.resolve(import.meta.dirname, "..") const dir = path.resolve(import.meta.dirname, "..")
const binary = "opencode2" const binary = "lildax"
process.chdir(dir) process.chdir(dir)
await rm("dist", { recursive: true, force: true }) await rm("dist", { recursive: true, force: true })
+6 -7
View File
@@ -25,15 +25,14 @@ for (const filepath of new Bun.Glob("*/package.json").scanSync({ cwd: "./dist" }
} }
console.log("binaries", binaries) console.log("binaries", binaries)
const version = Object.values(binaries)[0] const version = Object.values(binaries)[0]
const name = pkg.name
await $`mkdir -p ./dist/${name}/bin` await $`mkdir -p ./dist/${pkg.name}/bin`
await $`cp ./bin/opencode2.cjs ./dist/${name}/bin/opencode2` await $`cp ./bin/lildax.cjs ./dist/${pkg.name}/bin/lildax`
await Bun.file(`./dist/${name}/package.json`).write( await Bun.file(`./dist/${pkg.name}/package.json`).write(
JSON.stringify( JSON.stringify(
{ {
name, name: pkg.name,
bin: { opencode2: "./bin/opencode2" }, bin: { lildax: "./bin/lildax" },
version, version,
license: pkg.license, license: pkg.license,
repository: { type: "git", url: "git+https://github.com/anomalyco/opencode.git" }, 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), publish(`./dist/${name.replace("@opencode-ai/", "")}`, name, version),
), ),
) )
await publish(`./dist/${name}`, name, version) await publish(`./dist/${pkg.name}`, pkg.name, version)
-11
View File
@@ -5,16 +5,6 @@ declare const OPENCODE_CLI_NAME: string | undefined
export const Commands = Spec.make(typeof OPENCODE_CLI_NAME === "string" ? OPENCODE_CLI_NAME : "opencode", { export const Commands = Spec.make(typeof OPENCODE_CLI_NAME === "string" ? OPENCODE_CLI_NAME : "opencode", {
description: "OpenCode 2.0 preview command line interface", description: "OpenCode 2.0 preview command line interface",
params: {
directory: Argument.string("directory").pipe(
Argument.withDescription("Directory to start OpenCode in"),
Argument.optional,
),
standalone: Flag.boolean("standalone").pipe(
Flag.withDescription("Run with a private server instead of the background service"),
Flag.withDefault(false),
),
},
commands: [ commands: [
Spec.make("api", { Spec.make("api", {
description: "Make a request to the running server", description: "Make a request to the running server",
@@ -56,7 +46,6 @@ export const Commands = Spec.make(typeof OPENCODE_CLI_NAME === "string" ? OPENCO
hostname: Flag.string("hostname").pipe(Flag.withDefault("127.0.0.1")), hostname: Flag.string("hostname").pipe(Flag.withDefault("127.0.0.1")),
port: Flag.integer("port").pipe(Flag.optional), port: Flag.integer("port").pipe(Flag.optional),
register: Flag.boolean("register").pipe(Flag.withDefault(false)), register: Flag.boolean("register").pipe(Flag.withDefault(false)),
stdio: Flag.boolean("stdio").pipe(Flag.withDefault(false)),
}, },
}), }),
], ],
+4 -18
View File
@@ -1,27 +1,13 @@
import { Commands } from "../commands" import { Commands } from "../commands"
import { Runtime } from "../../framework/runtime" import { Runtime } from "../../framework/runtime"
import { Effect, Option } from "effect" import { Effect } from "effect"
import { Daemon } from "../../services/daemon" import { Daemon } from "../../services/daemon"
import { Standalone } from "../../services/standalone"
import { Updater } from "../../services/updater"
export default Runtime.handler(Commands, (input) => export default Runtime.handler(Commands, () =>
Effect.gen(function* () { Effect.gen(function* () {
const directory = Option.getOrUndefined(input.directory)
if (directory !== undefined) process.chdir(directory)
const updater = yield* Updater.Service
yield* updater.check()
const daemon = yield* Daemon.Service const daemon = yield* Daemon.Service
const transport = yield* (input.standalone ? Standalone.transport() : daemon.transport()) const transport = yield* daemon.transport()
const { runTui } = yield* Effect.promise(() => import("../../tui")) const { runTui } = yield* Effect.promise(() => import("../../tui"))
yield* runTui( yield* runTui(transport)
transport,
input.standalone
? undefined
: async () => {
await Effect.runPromise(daemon.stop())
return Effect.runPromise(daemon.transport())
},
)
}), }),
) )
+6 -41
View File
@@ -6,12 +6,9 @@ import * as Effect from "effect/Effect"
import { HttpRouter, HttpServer } from "effect/unstable/http" import { HttpRouter, HttpServer } from "effect/unstable/http"
import { createServer } from "node:http" import { createServer } from "node:http"
import { createRoutes } from "@opencode-ai/server/routes" import { createRoutes } from "@opencode-ai/server/routes"
import { ServerAuth } from "@opencode-ai/server/auth"
import { createOpencodeClient } from "@opencode-ai/sdk/v2/client"
import { Commands } from "../commands" import { Commands } from "../commands"
import { Runtime } from "../../framework/runtime" import { Runtime } from "../../framework/runtime"
import { Daemon } from "../../services/daemon" import { Daemon } from "../../services/daemon"
import { Updater } from "../../services/updater"
export default Runtime.handler( export default Runtime.handler(
Commands.commands.serve, Commands.commands.serve,
@@ -19,43 +16,15 @@ export default Runtime.handler(
return yield* Effect.scoped( return yield* Effect.scoped(
Effect.gen(function* () { Effect.gen(function* () {
const daemon = yield* Daemon.Service const daemon = yield* Daemon.Service
const standalonePassword = process.env.OPENCODE_SERVER_PASSWORD const address = yield* listen(input.hostname, input.port, yield* daemon.password())
if (input.stdio) delete process.env.OPENCODE_SERVER_PASSWORD
const password = input.stdio ? standalonePassword : yield* daemon.password()
if (!password) return yield* Effect.fail(new Error("Missing server password"))
const address = yield* listen(input.hostname, input.port, password)
yield* Effect.tryPromise(() =>
createOpencodeClient({
baseUrl: HttpServer.formatAddress(address),
headers: ServerAuth.headers({ password }),
}).v2.location.get(undefined, { throwOnError: true }),
)
if (input.register) yield* daemon.register(address) if (input.register) yield* daemon.register(address)
const url = HttpServer.formatAddress(address) console.log(`server listening on ${HttpServer.formatAddress(address)}`)
console.log(input.stdio ? JSON.stringify({ url }) : `server listening on ${url}`) return yield* Effect.never
const updater = yield* Updater.Service }),
yield* updater.check().pipe(Effect.forkScoped)
return yield* (input.stdio ? waitForStdinClose() : Effect.never)
}).pipe(Effect.annotateLogs({ role: "server" })),
) )
}), }),
) )
function waitForStdinClose() {
return Effect.callback<void>((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()
})
})
}
function listen(hostname: string, port: Option.Option<number>, password: string) { function listen(hostname: string, port: Option.Option<number>, password: string) {
if (Option.isSome(port)) return bind(hostname, port.value, password) if (Option.isSome(port)) return bind(hostname, port.value, password)
const next = (port: number): ReturnType<typeof bind> => const next = (port: number): ReturnType<typeof bind> =>
@@ -66,15 +35,11 @@ function listen(hostname: string, port: Option.Option<number>, password: string)
} }
function bind(hostname: string, port: number, password: string) { function bind(hostname: string, port: number, password: string) {
const server = createServer()
return Layer.build( return Layer.build(
HttpRouter.serve(createRoutes(password), { disableListenLog: true, disableLogger: true }).pipe( HttpRouter.serve(createRoutes(password), { disableListenLog: true, disableLogger: true }).pipe(
Layer.provideMerge(NodeHttpServer.layer(() => server, { port, host: hostname })), Layer.provideMerge(NodeHttpServer.layer(() => createServer(), { port, host: hostname })),
Layer.provide(Credential.defaultLayer), Layer.provide(Credential.defaultLayer),
Layer.provide(PermissionSaved.defaultLayer), Layer.provide(PermissionSaved.defaultLayer),
), ),
).pipe( ).pipe(Effect.map((context) => Context.get(context, HttpServer.HttpServer).address))
Effect.tap(() => Effect.addFinalizer(() => Effect.sync(() => server.closeAllConnections()))),
Effect.map((context) => Context.get(context, HttpServer.HttpServer).address),
)
} }
@@ -8,6 +8,6 @@ export default Runtime.handler(
Commands.commands.service.commands.status, Commands.commands.service.commands.status,
Effect.fn("cli.service.status")(function* () { Effect.fn("cli.service.status")(function* () {
const url = yield* (yield* Daemon.Service).status() const url = yield* (yield* Daemon.Service).status()
process.stdout.write((url ? url : "stopped") + EOL) process.stdout.write((url ? `running ${url}` : "stopped") + EOL)
}), }),
) )
+3 -5
View File
@@ -2,8 +2,6 @@ import * as Effect from "effect/Effect"
import * as Command from "effect/unstable/cli/Command" import * as Command from "effect/unstable/cli/Command"
import { Spec } from "./spec" import { Spec } from "./spec"
import { Daemon } from "../services/daemon" import { Daemon } from "../services/daemon"
import { Updater } from "../services/updater"
import { Scope } from "effect"
export type Input<Value> = export type Input<Value> =
Value extends Spec.Node<infer _Name, infer Command, infer _Commands> Value extends Spec.Node<infer _Name, infer Command, infer _Commands>
@@ -12,11 +10,11 @@ export type Input<Value> =
? Input ? Input
: never : never
type RuntimeHandler = (input: unknown) => Effect.Effect<void, unknown, Daemon.Service | Updater.Service | Scope.Scope> type RuntimeHandler = (input: unknown) => Effect.Effect<void, unknown, Daemon.Service>
type Loader<Node extends Spec.Any> = () => Promise<{ type Loader<Node extends Spec.Any> = () => Promise<{
default: (input: Input<Node>) => Effect.Effect<void, any, Daemon.Service | Updater.Service | Scope.Scope> default: (input: Input<Node>) => Effect.Effect<void, any, Daemon.Service>
}> }>
type ProvidedCommand = Command.Command<string, unknown, unknown, unknown, Daemon.Service | Updater.Service | Scope.Scope> type ProvidedCommand = Command.Command<string, unknown, unknown, unknown, Daemon.Service>
export type Handlers<Node extends Spec.Any> = keyof Node["commands"] extends never export type Handlers<Node extends Spec.Any> = keyof Node["commands"] extends never
? Loader<Node> ? Loader<Node>
+1 -17
View File
@@ -2,21 +2,10 @@
import * as NodeRuntime from "@effect/platform-node/NodeRuntime" import * as NodeRuntime from "@effect/platform-node/NodeRuntime"
import * as NodeServices from "@effect/platform-node/NodeServices" import * as NodeServices from "@effect/platform-node/NodeServices"
import { NodeFileSystem } from "@effect/platform-node"
import * as Effect from "effect/Effect" import * as Effect from "effect/Effect"
import { Layer, Logger, References } from "effect"
import { Commands } from "./commands/commands" import { Commands } from "./commands/commands"
import { Runtime } from "./framework/runtime" import { Runtime } from "./framework/runtime"
import { Daemon } from "./services/daemon" import { Daemon } from "./services/daemon"
import { Logging } from "@opencode-ai/core/observability/logging"
import { Updater } from "./services/updater"
import { InstallationChannel, InstallationVersion, InstallationLocal } from "@opencode-ai/core/installation/version"
const LoggingLayer = Logger.layer(Logging.loggers(), { mergeWithExisting: false }).pipe(
Layer.provide(NodeFileSystem.layer),
Layer.orDie,
Layer.merge(Layer.succeed(References.MinimumLogLevel, Logging.minimumLogLevel())),
)
const Handlers = Runtime.handlers(Commands, { const Handlers = Runtime.handlers(Commands, {
$: () => import("./commands/handlers/default"), $: () => import("./commands/handlers/default"),
@@ -35,14 +24,9 @@ const Handlers = Runtime.handlers(Commands, {
serve: () => import("./commands/handlers/serve"), serve: () => import("./commands/handlers/serve"),
}) })
Effect.logInfo("cli starting", { version: InstallationVersion, channel: InstallationChannel, local: InstallationLocal }).pipe( Runtime.run(Commands, Handlers, { version: "local" }).pipe(
Effect.flatMap(() => Runtime.run(Commands, Handlers, { version: InstallationVersion })),
Effect.annotateLogs({ role: "cli" }),
Effect.provide(Daemon.defaultLayer), Effect.provide(Daemon.defaultLayer),
Effect.provide(Updater.defaultLayer),
Effect.provide(LoggingLayer),
Effect.provide(NodeServices.layer), Effect.provide(NodeServices.layer),
Effect.scoped, Effect.scoped,
Effect.tap(() => Effect.sync(() => process.exit(0))),
NodeRuntime.runMain, NodeRuntime.runMain,
) )
+12 -24
View File
@@ -1,5 +1,5 @@
import { Global } from "@opencode-ai/core/global" import { Global } from "@opencode-ai/core/global"
import { InstallationChannel, InstallationVersion } from "@opencode-ai/core/installation/version" import { InstallationVersion } from "@opencode-ai/core/installation/version"
import { createOpencodeClient } from "@opencode-ai/sdk/v2/client" import { createOpencodeClient } from "@opencode-ai/sdk/v2/client"
import { ServerAuth } from "@opencode-ai/server/auth" import { ServerAuth } from "@opencode-ai/server/auth"
import { Context, Effect, FileSystem, Layer, Option, Schedule, Schema, Scope } from "effect" import { Context, Effect, FileSystem, Layer, Option, Schedule, Schema, Scope } from "effect"
@@ -28,10 +28,6 @@ const Registration = Schema.Struct({
}) })
type Registration = typeof Registration.Type type Registration = typeof Registration.Type
const Config = Schema.Struct({
password: Schema.optional(Schema.String),
})
function sameRegistration(left: Registration, right: Registration) { function sameRegistration(left: Registration, right: Registration) {
return left.id === right.id && left.version === right.version && left.url === right.url && left.pid === right.pid return left.id === right.id && left.version === right.version && left.url === right.url && left.pid === right.pid
} }
@@ -41,30 +37,22 @@ export const layer = Layer.effect(
Effect.gen(function* () { Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem const fs = yield* FileSystem.FileSystem
const directory = Global.Path.state const directory = Global.Path.state
const file = path.join(directory, InstallationChannel === "local" ? "server-local.json" : "server.json") const file = path.join(directory, "server.json")
const configFile = path.join(Global.Path.config, "service.json") const passwordFile = path.join(directory, "password")
const legacyPasswordFile = path.join(directory, "password")
const decodeRegistration = Schema.decodeUnknownEffect(Schema.fromJsonString(Registration)) const decodeRegistration = Schema.decodeUnknownEffect(Schema.fromJsonString(Registration))
const decodeConfig = Schema.decodeUnknownEffect(Schema.fromJsonString(Config))
const password = Effect.fn("cli.daemon.password")(function* (value?: string) { const password = Effect.fn("cli.daemon.password")(function* (value?: string) {
const config = yield* fs const existing = yield* fs.readFileString(passwordFile).pipe(Effect.catch(() => Effect.succeed(undefined)))
.readFileString(configFile) if (value === undefined && existing) return existing
.pipe(Effect.flatMap(decodeConfig), Effect.catch(() => Effect.succeed(undefined)))
if (value === undefined && config?.password) return config.password
const legacy = yield* fs
.readFileString(legacyPasswordFile)
.pipe(Effect.catch(() => Effect.succeed(undefined)))
const next = value ?? legacy ?? randomBytes(32).toString("base64url")
// Keep one private credential across server restarts so discovered clients // Keep one private credential across server restarts so discovered clients
// can reconnect without exposing a password flag or environment variable. // can reconnect without exposing a password flag or environment variable.
const temp = configFile + ".tmp" const generated = value ?? randomBytes(32).toString("base64url")
yield* fs.writeFileString(temp, JSON.stringify({ password: next }, null, 2) + "\n", { mode: 0o600 }) const temp = passwordFile + ".tmp"
yield* fs.rename(temp, configFile) yield* fs.makeDirectory(directory, { recursive: true })
if (legacy) yield* fs.remove(legacyPasswordFile).pipe(Effect.ignore) yield* fs.writeFileString(temp, generated, { mode: 0o600 })
return next yield* fs.rename(temp, passwordFile)
return generated
}) })
const registration = Effect.fnUntraced(function* () { const registration = Effect.fnUntraced(function* () {
@@ -123,7 +111,7 @@ export const layer = Layer.effect(
const existing = yield* healthy().pipe(Effect.option) const existing = yield* healthy().pipe(Effect.option)
const found = Option.getOrUndefined(existing) const found = Option.getOrUndefined(existing)
const compiled = path.basename(process.execPath).replace(/\.exe$/, "") !== "bun" const compiled = path.basename(process.execPath).replace(/\.exe$/, "") !== "bun"
if (found?.version === InstallationVersion) return found.url if (found?.version === InstallationVersion && compiled) return found.url
if (found) yield* stopProcess(found).pipe(Effect.ignore) if (found) yield* stopProcess(found).pipe(Effect.ignore)
const entrypoint = compiled ? undefined : process.argv[1] const entrypoint = compiled ? undefined : process.argv[1]
-41
View File
@@ -1,41 +0,0 @@
import { ServerAuth } from "@opencode-ai/server/auth"
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
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))
function command(password: string) {
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")
return ChildProcess.make(process.execPath, [...entrypoint, "serve", "--stdio", "--port", "0"], {
cwd: process.cwd(),
env: { OPENCODE_SERVER_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",
})
}
export const transport = Effect.fn("cli.standalone.transport")(
function* () {
const password = randomBytes(32).toString("base64url")
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner
const proc = yield* spawner.spawn(command(password))
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, headers: ServerAuth.headers({ password }), pid: proc.pid }
},
Effect.provide(CrossSpawnSpawner.defaultLayer),
)
export * as Standalone from "./standalone"
-33
View File
@@ -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")
})
})
-158
View File
@@ -1,158 +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<void>
}
export class Service extends Context.Service<Service, Interface>()("@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<Method, string[]> = {
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 const defaultLayer = layer.pipe(Layer.provide(AppProcess.defaultLayer), Layer.provide(Global.defaultLayer))
export * as Updater from "./updater"
+28 -38
View File
@@ -2,45 +2,35 @@ import { run } from "@opencode-ai/tui"
import { TuiConfig } from "@opencode-ai/tui/config" import { TuiConfig } from "@opencode-ai/tui/config"
import { Effect } from "effect" import { Effect } from "effect"
import { Global } from "@opencode-ai/core/global" import { Global } from "@opencode-ai/core/global"
import { loadBuiltinPlugins } from "@opencode-ai/tui/builtins"
import { createOpencodeClient } from "@opencode-ai/sdk/v2/client"
type Transport = { url: string; headers: RequestInit["headers"] } export function runTui(transport: { url: string; headers: RequestInit["headers"] }) {
export function runTui(transport: Transport, reload?: () => Promise<Transport>) {
const config = TuiConfig.resolve({}, { terminalSuspend: false }) const config = TuiConfig.resolve({}, { terminalSuspend: false })
let disposeSlots: (() => void) | undefined return run({
return Effect.gen(function* () { ...transport,
const options = { baseUrl: transport.url, headers: transport.headers } args: {},
const client = createOpencodeClient(options) config,
const directory = yield* Effect.tryPromise(() => fetch: gracefulFetch,
client.v2.fs.list({ location: { directory: process.cwd() } }, { throwOnError: true }), pluginHost: {
).pipe( async start() {},
Effect.map((response) => response.data.location.directory), async dispose() {},
Effect.catch(() => },
Effect.tryPromise(() => client.v2.location.get(undefined, { throwOnError: true })).pipe(
Effect.map((response) => response.data.directory),
),
),
)
return yield* run({
client: createOpencodeClient({ ...options, directory }),
reload: reload
? async () => {
const next = await reload()
return createOpencodeClient({ baseUrl: next.url, headers: next.headers, directory })
}
: undefined,
args: {},
config,
pluginHost: {
async start(input) {
disposeSlots = await loadBuiltinPlugins(input.api, input.runtime)
},
async dispose() {
disposeSlots?.()
},
},
})
}).pipe(Effect.provide(Global.defaultLayer)) }).pipe(Effect.provide(Global.defaultLayer))
} }
const legacyDefaults: Record<string, unknown> = {
"/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 },
)
@@ -1,15 +0,0 @@
import { Effect } from "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 transport = yield* Standalone.transport()
console.log(`${transport.pid} ${transport.url}`)
return yield* Effect.never
}),
),
)
-65
View File
@@ -1,65 +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,
stdin: "ignore",
stdout: "pipe",
stderr: "pipe",
})
const line = await Promise.race([readLine(owner.stdout), Bun.sleep(10_000).then(() => undefined)])
const [rawPID, url] = line?.split(" ") ?? []
const pid = Number(rawPID)
try {
expect(pid).toBeGreaterThan(0)
expect(url).toStartWith("http://127.0.0.1:")
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<Uint8Array>) {
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<boolean> {
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
}
}
+1 -1
View File
@@ -7,7 +7,7 @@ Private generation target for clients derived directly from OpenCode's authorita
- `@opencode-ai/client`: zero-Effect Promise client using `fetch`. - `@opencode-ai/client`: zero-Effect Promise client using `fetch`.
- `@opencode-ai/client/effect`: rich Effect network client using an environment-provided `HttpClient`. - `@opencode-ai/client/effect`: rich Effect network client using an environment-provided `HttpClient`.
The generated surface includes every standard HTTP group from Server's concrete API. The build compiler reads `@opencode-ai/server/api`; the generated Effect runtime imports a client-local projection built from Protocol, with a generation-equivalence test preventing transport drift. Custom transports such as the PTY WebSocket connection remain outside the generic HTTP client. Run `bun run generate` after changing the contract and `bun run check:generated` to detect committed-output drift. The generated surface starts with the Session group from Server's concrete API. The build compiler reads `@opencode-ai/server/api`; the generated Effect runtime imports a client-local projection built from Protocol, with a generation-equivalence test preventing transport drift. Run `bun run generate` after changing the contract and `bun run check:generated` to detect committed-output drift.
The Effect entrypoint uses canonical decoded values such as `Session.ID`, `Location.Ref`, and `Prompt`. These datatypes come from the lightweight `@opencode-ai/schema` package and are re-exported so callers depend only on the client surface. Protocol owns endpoint construction and middleware placement; Server supplies the concrete middleware keys used by the build-time API. The Effect entrypoint uses canonical decoded values such as `Session.ID`, `Location.Ref`, and `Prompt`. These datatypes come from the lightweight `@opencode-ai/schema` package and are re-exported so callers depend only on the client surface. Protocol owns endpoint construction and middleware placement; Server supplies the concrete middleware keys used by the build-time API.
+4 -2
View File
@@ -1,10 +1,12 @@
import { NodeFileSystem } from "@effect/platform-node" import { NodeFileSystem } from "@effect/platform-node"
import { compile, emitEffectImported, emitPromise, write } from "@opencode-ai/httpapi-codegen" import { compile, emitEffectImported, emitPromise, write } from "@opencode-ai/httpapi-codegen"
import { ClientApi, endpointNames, groupNames, omitEndpoints } from "../src/contract" import { ClientApi } from "../src/contract"
import { Effect } from "effect" import { Effect } from "effect"
import { fileURLToPath } from "url" import { fileURLToPath } from "url"
const contract = compile(ClientApi, { groupNames, endpointNames, omitEndpoints }) const contract = compile(ClientApi, {
groupNames: { "server.session": "sessions", "server.event": "events" },
})
await Effect.runPromise( await Effect.runPromise(
Effect.all( Effect.all(
+5 -37
View File
@@ -1,6 +1,6 @@
import { makeDefaultApi } from "@opencode-ai/protocol/api" import { makeDefaultApi } from "@opencode-ai/protocol/api"
import { InvalidRequestError, SessionNotFoundError } from "@opencode-ai/protocol/errors" import { InvalidRequestError, SessionNotFoundError } from "@opencode-ai/protocol/errors"
import { HttpApiMiddleware } from "effect/unstable/httpapi" import { HttpApi, HttpApiMiddleware } from "effect/unstable/httpapi"
class LocationMiddleware extends HttpApiMiddleware.Service<LocationMiddleware>()( class LocationMiddleware extends HttpApiMiddleware.Service<LocationMiddleware>()(
"@opencode-ai/client/LocationMiddleware", "@opencode-ai/client/LocationMiddleware",
@@ -11,43 +11,11 @@ class SessionLocationMiddleware extends HttpApiMiddleware.Service<SessionLocatio
{ error: [InvalidRequestError, SessionNotFoundError] }, { error: [InvalidRequestError, SessionNotFoundError] },
) {} ) {}
export const ClientApi = makeDefaultApi({ const Api = makeDefaultApi({
locationMiddleware: LocationMiddleware, locationMiddleware: LocationMiddleware,
sessionLocationMiddleware: SessionLocationMiddleware, sessionLocationMiddleware: SessionLocationMiddleware,
}) })
export const groupNames = { export const SessionGroup = Api.groups["server.session"]
"server.health": "health", export const EventGroup = Api.groups["server.event"]
"server.location": "location", export const ClientApi = HttpApi.make("opencode-client").add(SessionGroup).add(EventGroup)
"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"])
-12
View File
@@ -2,24 +2,12 @@
// Core or Server. Preserve these datatype exports so internal model reorganizations do not require caller migrations. // Core or Server. Preserve these datatype exports so internal model reorganizations do not require caller migrations.
export * from "./generated-effect/index" export * from "./generated-effect/index"
export { Agent } from "@opencode-ai/schema/agent" export { Agent } from "@opencode-ai/schema/agent"
export { Command } from "@opencode-ai/schema/command"
export { Credential } from "@opencode-ai/schema/credential"
export { FileSystem } from "@opencode-ai/schema/filesystem"
export { Integration } from "@opencode-ai/schema/integration"
export { Location } from "@opencode-ai/schema/location" export { Location } from "@opencode-ai/schema/location"
export { Model } from "@opencode-ai/schema/model" export { Model } from "@opencode-ai/schema/model"
export { Permission } from "@opencode-ai/schema/permission"
export { PermissionSaved } from "@opencode-ai/schema/permission-saved"
export { Project } from "@opencode-ai/schema/project"
export { ProjectCopy } from "@opencode-ai/schema/project-copy"
export { Provider } from "@opencode-ai/schema/provider" export { Provider } from "@opencode-ai/schema/provider"
export { Pty } from "@opencode-ai/schema/pty"
export { Question } from "@opencode-ai/schema/question"
export { Reference } from "@opencode-ai/schema/reference"
export { AbsolutePath, RelativePath } from "@opencode-ai/schema/schema" export { AbsolutePath, RelativePath } from "@opencode-ai/schema/schema"
export { Session } from "@opencode-ai/schema/session" export { Session } from "@opencode-ai/schema/session"
export { SessionInput } from "@opencode-ai/schema/session-input" export { SessionInput } from "@opencode-ai/schema/session-input"
export { SessionMessage } from "@opencode-ai/schema/session-message" export { SessionMessage } from "@opencode-ai/schema/session-message"
export { Skill } from "@opencode-ai/schema/skill"
export { Prompt } from "@opencode-ai/schema/prompt" export { Prompt } from "@opencode-ai/schema/prompt"
export type { OpenCodeEvent } from "@opencode-ai/protocol/groups/event" export type { OpenCodeEvent } from "@opencode-ai/protocol/groups/event"
+128 -618
View File
@@ -13,513 +13,202 @@ const mapClientError = <E>(error: E) =>
? new ClientError({ cause: error }) ? new ClientError({ cause: error })
: error : error
const Endpoint0_0 = (raw: RawClient["server.health"]) => () => type Endpoint0_0Request = Parameters<RawClient["server.session"]["session.list"]>[0]
raw["health.get"]({}).pipe(Effect.mapError(mapClientError)) type Endpoint0_0Input = {
readonly workspace?: Endpoint0_0Request["query"]["workspace"]
const adaptGroup0 = (raw: RawClient["server.health"]) => ({ get: Endpoint0_0(raw) }) readonly limit?: Endpoint0_0Request["query"]["limit"]
readonly order?: Endpoint0_0Request["query"]["order"]
type Endpoint1_0Request = Parameters<RawClient["server.location"]["location.get"]>[0] readonly search?: Endpoint0_0Request["query"]["search"]
type Endpoint1_0Input = { readonly location?: Endpoint1_0Request["query"]["location"] } readonly directory?: Endpoint0_0Request["query"]["directory"]
const Endpoint1_0 = (raw: RawClient["server.location"]) => (input?: Endpoint1_0Input) => readonly project?: Endpoint0_0Request["query"]["project"]
raw["location.get"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)) readonly subpath?: Endpoint0_0Request["query"]["subpath"]
readonly cursor?: Endpoint0_0Request["query"]["cursor"]
const adaptGroup1 = (raw: RawClient["server.location"]) => ({ get: Endpoint1_0(raw) })
type Endpoint2_0Request = Parameters<RawClient["server.agent"]["agent.list"]>[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<RawClient["server.session"]["session.list"]>[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) => const Endpoint0_0 = (raw: RawClient["server.session"]) => (input?: Endpoint0_0Input) =>
raw["session.list"]({ raw["session.list"]({
query: { query: {
workspace: input?.["workspace"], workspace: input?.workspace,
limit: input?.["limit"], limit: input?.limit,
order: input?.["order"], order: input?.order,
search: input?.["search"], search: input?.search,
directory: input?.["directory"], directory: input?.directory,
project: input?.["project"], project: input?.project,
subpath: input?.["subpath"], subpath: input?.subpath,
cursor: input?.["cursor"], cursor: input?.cursor,
}, },
}).pipe(Effect.mapError(mapClientError)) }).pipe(Effect.mapError(mapClientError))
type Endpoint3_1Request = Parameters<RawClient["server.session"]["session.create"]>[0] type Endpoint0_1Request = Parameters<RawClient["server.session"]["session.create"]>[0]
type Endpoint3_1Input = { type Endpoint0_1Input = {
readonly id?: Endpoint3_1Request["payload"]["id"] readonly id?: Endpoint0_1Request["payload"]["id"]
readonly agent?: Endpoint3_1Request["payload"]["agent"] readonly agent?: Endpoint0_1Request["payload"]["agent"]
readonly model?: Endpoint3_1Request["payload"]["model"] readonly model?: Endpoint0_1Request["payload"]["model"]
readonly location?: Endpoint3_1Request["payload"]["location"] readonly location?: Endpoint0_1Request["payload"]["location"]
} }
const Endpoint3_1 = (raw: RawClient["server.session"]) => (input?: Endpoint3_1Input) => const Endpoint0_1 = (raw: RawClient["server.session"]) => (input?: Endpoint0_1Input) =>
raw["session.create"]({ raw["session.create"]({
payload: { id: input?.["id"], agent: input?.["agent"], model: input?.["model"], location: input?.["location"] }, payload: { id: input?.id, agent: input?.agent, model: input?.model, location: input?.location },
}).pipe( }).pipe(
Effect.mapError(mapClientError), Effect.mapError(mapClientError),
Effect.map((value) => value.data), Effect.map((value) => value.data),
) )
const Endpoint3_2 = (raw: RawClient["server.session"]) => () => const Endpoint0_2 = (raw: RawClient["server.session"]) => () =>
raw["session.active"]({}).pipe( raw["session.active"]({}).pipe(
Effect.mapError(mapClientError), Effect.mapError(mapClientError),
Effect.map((value) => value.data), Effect.map((value) => value.data),
) )
type Endpoint3_3Request = Parameters<RawClient["server.session"]["session.get"]>[0] type Endpoint0_3Request = Parameters<RawClient["server.session"]["session.get"]>[0]
type Endpoint3_3Input = { readonly sessionID: Endpoint3_3Request["params"]["sessionID"] } type Endpoint0_3Input = { readonly sessionID: Endpoint0_3Request["params"]["sessionID"] }
const Endpoint3_3 = (raw: RawClient["server.session"]) => (input: Endpoint3_3Input) => const Endpoint0_3 = (raw: RawClient["server.session"]) => (input: Endpoint0_3Input) =>
raw["session.get"]({ params: { sessionID: input["sessionID"] } }).pipe( raw["session.get"]({ params: { sessionID: input.sessionID } }).pipe(
Effect.mapError(mapClientError), Effect.mapError(mapClientError),
Effect.map((value) => value.data), Effect.map((value) => value.data),
) )
type Endpoint3_4Request = Parameters<RawClient["server.session"]["session.switchAgent"]>[0] type Endpoint0_4Request = Parameters<RawClient["server.session"]["session.switchAgent"]>[0]
type Endpoint3_4Input = { type Endpoint0_4Input = {
readonly sessionID: Endpoint3_4Request["params"]["sessionID"] readonly sessionID: Endpoint0_4Request["params"]["sessionID"]
readonly agent: Endpoint3_4Request["payload"]["agent"] readonly agent: Endpoint0_4Request["payload"]["agent"]
} }
const Endpoint3_4 = (raw: RawClient["server.session"]) => (input: Endpoint3_4Input) => const Endpoint0_4 = (raw: RawClient["server.session"]) => (input: Endpoint0_4Input) =>
raw["session.switchAgent"]({ params: { sessionID: input["sessionID"] }, payload: { agent: input["agent"] } }).pipe( raw["session.switchAgent"]({ params: { sessionID: input.sessionID }, payload: { agent: input.agent } }).pipe(
Effect.mapError(mapClientError), Effect.mapError(mapClientError),
) )
type Endpoint3_5Request = Parameters<RawClient["server.session"]["session.switchModel"]>[0] type Endpoint0_5Request = Parameters<RawClient["server.session"]["session.switchModel"]>[0]
type Endpoint3_5Input = { type Endpoint0_5Input = {
readonly sessionID: Endpoint3_5Request["params"]["sessionID"] readonly sessionID: Endpoint0_5Request["params"]["sessionID"]
readonly model: Endpoint3_5Request["payload"]["model"] readonly model: Endpoint0_5Request["payload"]["model"]
} }
const Endpoint3_5 = (raw: RawClient["server.session"]) => (input: Endpoint3_5Input) => const Endpoint0_5 = (raw: RawClient["server.session"]) => (input: Endpoint0_5Input) =>
raw["session.switchModel"]({ params: { sessionID: input["sessionID"] }, payload: { model: input["model"] } }).pipe( raw["session.switchModel"]({ params: { sessionID: input.sessionID }, payload: { model: input.model } }).pipe(
Effect.mapError(mapClientError), Effect.mapError(mapClientError),
) )
type Endpoint3_6Request = Parameters<RawClient["server.session"]["session.rename"]>[0] type Endpoint0_6Request = Parameters<RawClient["server.session"]["session.prompt"]>[0]
type Endpoint3_6Input = { type Endpoint0_6Input = {
readonly sessionID: Endpoint3_6Request["params"]["sessionID"] readonly sessionID: Endpoint0_6Request["params"]["sessionID"]
readonly title: Endpoint3_6Request["payload"]["title"] readonly id?: Endpoint0_6Request["payload"]["id"]
readonly prompt: Endpoint0_6Request["payload"]["prompt"]
readonly delivery?: Endpoint0_6Request["payload"]["delivery"]
readonly resume?: Endpoint0_6Request["payload"]["resume"]
} }
const Endpoint3_6 = (raw: RawClient["server.session"]) => (input: Endpoint3_6Input) => const Endpoint0_6 = (raw: RawClient["server.session"]) => (input: Endpoint0_6Input) =>
raw["session.rename"]({ params: { sessionID: input["sessionID"] }, payload: { title: input["title"] } }).pipe(
Effect.mapError(mapClientError),
)
type Endpoint3_7Request = Parameters<RawClient["server.session"]["session.prompt"]>[0]
type Endpoint3_7Input = {
readonly sessionID: Endpoint3_7Request["params"]["sessionID"]
readonly id?: Endpoint3_7Request["payload"]["id"]
readonly prompt: Endpoint3_7Request["payload"]["prompt"]
readonly delivery?: Endpoint3_7Request["payload"]["delivery"]
readonly resume?: Endpoint3_7Request["payload"]["resume"]
}
const Endpoint3_7 = (raw: RawClient["server.session"]) => (input: Endpoint3_7Input) =>
raw["session.prompt"]({ raw["session.prompt"]({
params: { sessionID: input["sessionID"] }, params: { sessionID: input.sessionID },
payload: { id: input["id"], prompt: input["prompt"], delivery: input["delivery"], resume: input["resume"] }, payload: { id: input.id, prompt: input.prompt, delivery: input.delivery, resume: input.resume },
}).pipe( }).pipe(
Effect.mapError(mapClientError), Effect.mapError(mapClientError),
Effect.map((value) => value.data), Effect.map((value) => value.data),
) )
type Endpoint3_8Request = Parameters<RawClient["server.session"]["session.compact"]>[0] type Endpoint0_7Request = Parameters<RawClient["server.session"]["session.compact"]>[0]
type Endpoint3_8Input = { readonly sessionID: Endpoint3_8Request["params"]["sessionID"] } type Endpoint0_7Input = { readonly sessionID: Endpoint0_7Request["params"]["sessionID"] }
const Endpoint3_8 = (raw: RawClient["server.session"]) => (input: Endpoint3_8Input) => const Endpoint0_7 = (raw: RawClient["server.session"]) => (input: Endpoint0_7Input) =>
raw["session.compact"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError)) raw["session.compact"]({ params: { sessionID: input.sessionID } }).pipe(Effect.mapError(mapClientError))
type Endpoint3_9Request = Parameters<RawClient["server.session"]["session.wait"]>[0] type Endpoint0_8Request = Parameters<RawClient["server.session"]["session.wait"]>[0]
type Endpoint3_9Input = { readonly sessionID: Endpoint3_9Request["params"]["sessionID"] } type Endpoint0_8Input = { readonly sessionID: Endpoint0_8Request["params"]["sessionID"] }
const Endpoint3_9 = (raw: RawClient["server.session"]) => (input: Endpoint3_9Input) => const Endpoint0_8 = (raw: RawClient["server.session"]) => (input: Endpoint0_8Input) =>
raw["session.wait"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError)) raw["session.wait"]({ params: { sessionID: input.sessionID } }).pipe(Effect.mapError(mapClientError))
type Endpoint3_10Request = Parameters<RawClient["server.session"]["session.revert.stage"]>[0] type Endpoint0_9Request = Parameters<RawClient["server.session"]["session.revert.stage"]>[0]
type Endpoint3_10Input = { type Endpoint0_9Input = {
readonly sessionID: Endpoint3_10Request["params"]["sessionID"] readonly sessionID: Endpoint0_9Request["params"]["sessionID"]
readonly messageID: Endpoint3_10Request["payload"]["messageID"] readonly messageID: Endpoint0_9Request["payload"]["messageID"]
readonly files?: Endpoint3_10Request["payload"]["files"] readonly files?: Endpoint0_9Request["payload"]["files"]
} }
const Endpoint3_10 = (raw: RawClient["server.session"]) => (input: Endpoint3_10Input) => const Endpoint0_9 = (raw: RawClient["server.session"]) => (input: Endpoint0_9Input) =>
raw["session.revert.stage"]({ raw["session.revert.stage"]({
params: { sessionID: input["sessionID"] }, params: { sessionID: input.sessionID },
payload: { messageID: input["messageID"], files: input["files"] }, payload: { messageID: input.messageID, files: input.files },
}).pipe( }).pipe(
Effect.mapError(mapClientError), Effect.mapError(mapClientError),
Effect.map((value) => value.data), Effect.map((value) => value.data),
) )
type Endpoint3_11Request = Parameters<RawClient["server.session"]["session.revert.clear"]>[0] type Endpoint0_10Request = Parameters<RawClient["server.session"]["session.revert.clear"]>[0]
type Endpoint3_11Input = { readonly sessionID: Endpoint3_11Request["params"]["sessionID"] } type Endpoint0_10Input = { readonly sessionID: Endpoint0_10Request["params"]["sessionID"] }
const Endpoint3_11 = (raw: RawClient["server.session"]) => (input: Endpoint3_11Input) => const Endpoint0_10 = (raw: RawClient["server.session"]) => (input: Endpoint0_10Input) =>
raw["session.revert.clear"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError)) raw["session.revert.clear"]({ params: { sessionID: input.sessionID } }).pipe(Effect.mapError(mapClientError))
type Endpoint3_12Request = Parameters<RawClient["server.session"]["session.revert.commit"]>[0] type Endpoint0_11Request = Parameters<RawClient["server.session"]["session.revert.commit"]>[0]
type Endpoint3_12Input = { readonly sessionID: Endpoint3_12Request["params"]["sessionID"] } type Endpoint0_11Input = { readonly sessionID: Endpoint0_11Request["params"]["sessionID"] }
const Endpoint3_12 = (raw: RawClient["server.session"]) => (input: Endpoint3_12Input) => const Endpoint0_11 = (raw: RawClient["server.session"]) => (input: Endpoint0_11Input) =>
raw["session.revert.commit"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError)) raw["session.revert.commit"]({ params: { sessionID: input.sessionID } }).pipe(Effect.mapError(mapClientError))
type Endpoint3_13Request = Parameters<RawClient["server.session"]["session.context"]>[0] type Endpoint0_12Request = Parameters<RawClient["server.session"]["session.context"]>[0]
type Endpoint3_13Input = { readonly sessionID: Endpoint3_13Request["params"]["sessionID"] } type Endpoint0_12Input = { readonly sessionID: Endpoint0_12Request["params"]["sessionID"] }
const Endpoint3_13 = (raw: RawClient["server.session"]) => (input: Endpoint3_13Input) => const Endpoint0_12 = (raw: RawClient["server.session"]) => (input: Endpoint0_12Input) =>
raw["session.context"]({ params: { sessionID: input["sessionID"] } }).pipe( raw["session.context"]({ params: { sessionID: input.sessionID } }).pipe(
Effect.mapError(mapClientError), Effect.mapError(mapClientError),
Effect.map((value) => value.data), Effect.map((value) => value.data),
) )
type Endpoint3_14Request = Parameters<RawClient["server.session"]["session.history"]>[0] type Endpoint0_13Request = Parameters<RawClient["server.session"]["session.history"]>[0]
type Endpoint3_14Input = { type Endpoint0_13Input = {
readonly sessionID: Endpoint3_14Request["params"]["sessionID"] readonly sessionID: Endpoint0_13Request["params"]["sessionID"]
readonly limit?: Endpoint3_14Request["query"]["limit"] readonly limit?: Endpoint0_13Request["query"]["limit"]
readonly after?: Endpoint3_14Request["query"]["after"] readonly after?: Endpoint0_13Request["query"]["after"]
} }
const Endpoint3_14 = (raw: RawClient["server.session"]) => (input: Endpoint3_14Input) => const Endpoint0_13 = (raw: RawClient["server.session"]) => (input: Endpoint0_13Input) =>
raw["session.history"]({ raw["session.history"]({
params: { sessionID: input["sessionID"] }, params: { sessionID: input.sessionID },
query: { limit: input["limit"], after: input["after"] }, query: { limit: input.limit, after: input.after },
}).pipe(Effect.mapError(mapClientError)) }).pipe(Effect.mapError(mapClientError))
type Endpoint3_15Request = Parameters<RawClient["server.session"]["session.events"]>[0] type Endpoint0_14Request = Parameters<RawClient["server.session"]["session.events"]>[0]
type Endpoint3_15Input = { type Endpoint0_14Input = {
readonly sessionID: Endpoint3_15Request["params"]["sessionID"] readonly sessionID: Endpoint0_14Request["params"]["sessionID"]
readonly after?: Endpoint3_15Request["query"]["after"] readonly after?: Endpoint0_14Request["query"]["after"]
} }
const Endpoint3_15 = (raw: RawClient["server.session"]) => (input: Endpoint3_15Input) => const Endpoint0_14 = (raw: RawClient["server.session"]) => (input: Endpoint0_14Input) =>
Stream.unwrap( Stream.unwrap(
raw["session.events"]({ params: { sessionID: input["sessionID"] }, query: { after: input["after"] } }).pipe( raw["session.events"]({ params: { sessionID: input.sessionID }, query: { after: input.after } }).pipe(
Effect.mapError(mapClientError), Effect.mapError(mapClientError),
Effect.map((stream) => stream.pipe(Stream.mapError(mapClientError))), Effect.map((stream) => stream.pipe(Stream.mapError(mapClientError))),
), ),
) )
type Endpoint3_16Request = Parameters<RawClient["server.session"]["session.interrupt"]>[0] type Endpoint0_15Request = Parameters<RawClient["server.session"]["session.interrupt"]>[0]
type Endpoint3_16Input = { readonly sessionID: Endpoint3_16Request["params"]["sessionID"] } type Endpoint0_15Input = { readonly sessionID: Endpoint0_15Request["params"]["sessionID"] }
const Endpoint3_16 = (raw: RawClient["server.session"]) => (input: Endpoint3_16Input) => const Endpoint0_15 = (raw: RawClient["server.session"]) => (input: Endpoint0_15Input) =>
raw["session.interrupt"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError)) raw["session.interrupt"]({ params: { sessionID: input.sessionID } }).pipe(Effect.mapError(mapClientError))
type Endpoint3_17Request = Parameters<RawClient["server.session"]["session.message"]>[0] type Endpoint0_16Request = Parameters<RawClient["server.session"]["session.message"]>[0]
type Endpoint3_17Input = { type Endpoint0_16Input = {
readonly sessionID: Endpoint3_17Request["params"]["sessionID"] readonly sessionID: Endpoint0_16Request["params"]["sessionID"]
readonly messageID: Endpoint3_17Request["params"]["messageID"] readonly messageID: Endpoint0_16Request["params"]["messageID"]
} }
const Endpoint3_17 = (raw: RawClient["server.session"]) => (input: Endpoint3_17Input) => const Endpoint0_16 = (raw: RawClient["server.session"]) => (input: Endpoint0_16Input) =>
raw["session.message"]({ params: { sessionID: input["sessionID"], messageID: input["messageID"] } }).pipe( raw["session.message"]({ params: { sessionID: input.sessionID, messageID: input.messageID } }).pipe(
Effect.mapError(mapClientError), Effect.mapError(mapClientError),
Effect.map((value) => value.data), Effect.map((value) => value.data),
) )
const adaptGroup3 = (raw: RawClient["server.session"]) => ({ const adaptGroup0 = (raw: RawClient["server.session"]) => ({
list: Endpoint3_0(raw), list: Endpoint0_0(raw),
create: Endpoint3_1(raw), create: Endpoint0_1(raw),
active: Endpoint3_2(raw), active: Endpoint0_2(raw),
get: Endpoint3_3(raw), get: Endpoint0_3(raw),
switchAgent: Endpoint3_4(raw), switchAgent: Endpoint0_4(raw),
switchModel: Endpoint3_5(raw), switchModel: Endpoint0_5(raw),
rename: Endpoint3_6(raw), prompt: Endpoint0_6(raw),
prompt: Endpoint3_7(raw), compact: Endpoint0_7(raw),
compact: Endpoint3_8(raw), wait: Endpoint0_8(raw),
wait: Endpoint3_9(raw), stage: Endpoint0_9(raw),
stage: Endpoint3_10(raw), clear: Endpoint0_10(raw),
clear: Endpoint3_11(raw), commit: Endpoint0_11(raw),
commit: Endpoint3_12(raw), context: Endpoint0_12(raw),
context: Endpoint3_13(raw), history: Endpoint0_13(raw),
history: Endpoint3_14(raw), events: Endpoint0_14(raw),
events: Endpoint3_15(raw), interrupt: Endpoint0_15(raw),
interrupt: Endpoint3_16(raw), message: Endpoint0_16(raw),
message: Endpoint3_17(raw),
}) })
type Endpoint4_0Request = Parameters<RawClient["server.message"]["session.messages"]>[0] const Endpoint1_0 = (raw: RawClient["server.event"]) => () =>
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<RawClient["server.model"]["model.list"]>[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<RawClient["server.provider"]["provider.list"]>[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<RawClient["server.provider"]["provider.get"]>[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<RawClient["server.integration"]["integration.list"]>[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<RawClient["server.integration"]["integration.get"]>[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<RawClient["server.integration"]["integration.connect.key"]>[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<RawClient["server.integration"]["integration.connect.oauth"]>[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<RawClient["server.integration"]["integration.attempt.status"]>[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<RawClient["server.integration"]["integration.attempt.complete"]>[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<RawClient["server.integration"]["integration.attempt.cancel"]>[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<RawClient["server.credential"]["credential.update"]>[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<RawClient["server.credential"]["credential.remove"]>[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<RawClient["server.permission"]["permission.request.list"]>[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<RawClient["server.permission"]["permission.saved.list"]>[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<RawClient["server.permission"]["permission.saved.remove"]>[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<RawClient["server.permission"]["session.permission.create"]>[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<RawClient["server.permission"]["session.permission.list"]>[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<RawClient["server.permission"]["session.permission.get"]>[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<RawClient["server.permission"]["session.permission.reply"]>[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<RawClient["server.fs"]["fs.list"]>[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<RawClient["server.fs"]["fs.find"]>[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<RawClient["server.command"]["command.list"]>[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<RawClient["server.skill"]["skill.list"]>[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( Stream.unwrap(
raw["event.subscribe"]({}).pipe( raw["event.subscribe"]({}).pipe(
Effect.mapError(mapClientError), Effect.mapError(mapClientError),
@@ -527,190 +216,11 @@ const Endpoint13_0 = (raw: RawClient["server.event"]) => () =>
), ),
) )
const adaptGroup13 = (raw: RawClient["server.event"]) => ({ subscribe: Endpoint13_0(raw) }) const adaptGroup1 = (raw: RawClient["server.event"]) => ({ subscribe: Endpoint1_0(raw) })
type Endpoint14_0Request = Parameters<RawClient["server.pty"]["pty.list"]>[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<RawClient["server.pty"]["pty.create"]>[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<RawClient["server.pty"]["pty.get"]>[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<RawClient["server.pty"]["pty.update"]>[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<RawClient["server.pty"]["pty.remove"]>[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<RawClient["server.question"]["question.request.list"]>[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<RawClient["server.question"]["session.question.list"]>[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<RawClient["server.question"]["session.question.reply"]>[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<RawClient["server.question"]["session.question.reject"]>[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<RawClient["server.reference"]["reference.list"]>[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<RawClient["server.projectCopy"]["projectCopy.create"]>[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<RawClient["server.projectCopy"]["projectCopy.remove"]>[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<RawClient["server.projectCopy"]["projectCopy.refresh"]>[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) => ({ const adaptClient = (raw: RawClient) => ({
health: adaptGroup0(raw["server.health"]), sessions: adaptGroup0(raw["server.session"]),
location: adaptGroup1(raw["server.location"]), events: adaptGroup1(raw["server.event"]),
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 }) => export const make = (options?: { readonly baseUrl?: URL | string }) =>
+18 -638
View File
@@ -1,9 +1,4 @@
import type { import type {
HealthGetOutput,
LocationGetInput,
LocationGetOutput,
AgentsListInput,
AgentsListOutput,
SessionsListInput, SessionsListInput,
SessionsListOutput, SessionsListOutput,
SessionsCreateInput, SessionsCreateInput,
@@ -15,8 +10,6 @@ import type {
SessionsSwitchAgentOutput, SessionsSwitchAgentOutput,
SessionsSwitchModelInput, SessionsSwitchModelInput,
SessionsSwitchModelOutput, SessionsSwitchModelOutput,
SessionsRenameInput,
SessionsRenameOutput,
SessionsPromptInput, SessionsPromptInput,
SessionsPromptOutput, SessionsPromptOutput,
SessionsCompactInput, SessionsCompactInput,
@@ -39,81 +32,7 @@ import type {
SessionsInterruptOutput, SessionsInterruptOutput,
SessionsMessageInput, SessionsMessageInput,
SessionsMessageOutput, 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, 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" } from "./types"
import { ClientError } from "./client-error" import { ClientError } from "./client-error"
@@ -249,41 +168,6 @@ export function make(options: ClientOptions) {
}) })
return { return {
health: {
get: (requestOptions?: RequestOptions) =>
request<HealthGetOutput>(
{ method: "GET", path: `/api/health`, successStatus: 200, declaredStatuses: [401, 400], empty: false },
requestOptions,
),
},
location: {
get: (input?: LocationGetInput, requestOptions?: RequestOptions) =>
request<LocationGetOutput>(
{
method: "GET",
path: `/api/location`,
query: { location: input?.["location"] },
successStatus: 200,
declaredStatuses: [401, 400],
empty: false,
},
requestOptions,
),
},
agents: {
list: (input?: AgentsListInput, requestOptions?: RequestOptions) =>
request<AgentsListOutput>(
{
method: "GET",
path: `/api/agent`,
query: { location: input?.["location"] },
successStatus: 200,
declaredStatuses: [401, 400],
empty: false,
},
requestOptions,
),
},
sessions: { sessions: {
list: (input?: SessionsListInput, requestOptions?: RequestOptions) => list: (input?: SessionsListInput, requestOptions?: RequestOptions) =>
request<SessionsListOutput>( request<SessionsListOutput>(
@@ -291,14 +175,14 @@ export function make(options: ClientOptions) {
method: "GET", method: "GET",
path: `/api/session`, path: `/api/session`,
query: { query: {
workspace: input?.["workspace"], workspace: input?.workspace,
limit: input?.["limit"], limit: input?.limit,
order: input?.["order"], order: input?.order,
search: input?.["search"], search: input?.search,
directory: input?.["directory"], directory: input?.directory,
project: input?.["project"], project: input?.project,
subpath: input?.["subpath"], subpath: input?.subpath,
cursor: input?.["cursor"], cursor: input?.cursor,
}, },
successStatus: 200, successStatus: 200,
declaredStatuses: [400, 401], declaredStatuses: [400, 401],
@@ -311,12 +195,7 @@ export function make(options: ClientOptions) {
{ {
method: "POST", method: "POST",
path: `/api/session`, path: `/api/session`,
body: { body: { id: input?.id, agent: input?.agent, model: input?.model, location: input?.location },
id: input?.["id"],
agent: input?.["agent"],
model: input?.["model"],
location: input?.["location"],
},
successStatus: 200, successStatus: 200,
declaredStatuses: [401, 400], declaredStatuses: [401, 400],
empty: false, empty: false,
@@ -350,7 +229,7 @@ export function make(options: ClientOptions) {
{ {
method: "POST", method: "POST",
path: `/api/session/${encodeURIComponent(input.sessionID)}/agent`, path: `/api/session/${encodeURIComponent(input.sessionID)}/agent`,
body: { agent: input["agent"] }, body: { agent: input.agent },
successStatus: 204, successStatus: 204,
declaredStatuses: [404, 400, 401], declaredStatuses: [404, 400, 401],
empty: true, empty: true,
@@ -362,19 +241,7 @@ export function make(options: ClientOptions) {
{ {
method: "POST", method: "POST",
path: `/api/session/${encodeURIComponent(input.sessionID)}/model`, path: `/api/session/${encodeURIComponent(input.sessionID)}/model`,
body: { model: input["model"] }, body: { model: input.model },
successStatus: 204,
declaredStatuses: [404, 400, 401],
empty: true,
},
requestOptions,
),
rename: (input: SessionsRenameInput, requestOptions?: RequestOptions) =>
request<SessionsRenameOutput>(
{
method: "POST",
path: `/api/session/${encodeURIComponent(input.sessionID)}/rename`,
body: { title: input["title"] },
successStatus: 204, successStatus: 204,
declaredStatuses: [404, 400, 401], declaredStatuses: [404, 400, 401],
empty: true, empty: true,
@@ -386,7 +253,7 @@ export function make(options: ClientOptions) {
{ {
method: "POST", method: "POST",
path: `/api/session/${encodeURIComponent(input.sessionID)}/prompt`, path: `/api/session/${encodeURIComponent(input.sessionID)}/prompt`,
body: { id: input["id"], prompt: input["prompt"], delivery: input["delivery"], resume: input["resume"] }, body: { id: input.id, prompt: input.prompt, delivery: input.delivery, resume: input.resume },
successStatus: 200, successStatus: 200,
declaredStatuses: [409, 404, 400, 401], declaredStatuses: [409, 404, 400, 401],
empty: false, empty: false,
@@ -420,9 +287,9 @@ export function make(options: ClientOptions) {
{ {
method: "POST", method: "POST",
path: `/api/session/${encodeURIComponent(input.sessionID)}/revert/stage`, path: `/api/session/${encodeURIComponent(input.sessionID)}/revert/stage`,
body: { messageID: input["messageID"], files: input["files"] }, body: { messageID: input.messageID, files: input.files },
successStatus: 200, successStatus: 200,
declaredStatuses: [404, 409, 500, 400, 401], declaredStatuses: [404, 500, 400, 401],
empty: false, empty: false,
}, },
requestOptions, requestOptions,
@@ -433,7 +300,7 @@ export function make(options: ClientOptions) {
method: "POST", method: "POST",
path: `/api/session/${encodeURIComponent(input.sessionID)}/revert/clear`, path: `/api/session/${encodeURIComponent(input.sessionID)}/revert/clear`,
successStatus: 204, successStatus: 204,
declaredStatuses: [404, 409, 500, 400, 401], declaredStatuses: [404, 500, 400, 401],
empty: true, empty: true,
}, },
requestOptions, requestOptions,
@@ -444,7 +311,7 @@ export function make(options: ClientOptions) {
method: "POST", method: "POST",
path: `/api/session/${encodeURIComponent(input.sessionID)}/revert/commit`, path: `/api/session/${encodeURIComponent(input.sessionID)}/revert/commit`,
successStatus: 204, successStatus: 204,
declaredStatuses: [404, 409, 400, 401], declaredStatuses: [404, 400, 401],
empty: true, empty: true,
}, },
requestOptions, requestOptions,
@@ -465,7 +332,7 @@ export function make(options: ClientOptions) {
{ {
method: "GET", method: "GET",
path: `/api/session/${encodeURIComponent(input.sessionID)}/history`, path: `/api/session/${encodeURIComponent(input.sessionID)}/history`,
query: { limit: input["limit"], after: input["after"] }, query: { limit: input.limit, after: input.after },
successStatus: 200, successStatus: 200,
declaredStatuses: [404, 400, 401], declaredStatuses: [404, 400, 401],
empty: false, empty: false,
@@ -477,7 +344,7 @@ export function make(options: ClientOptions) {
{ {
method: "GET", method: "GET",
path: `/api/session/${encodeURIComponent(input.sessionID)}/event`, path: `/api/session/${encodeURIComponent(input.sessionID)}/event`,
query: { after: input["after"] }, query: { after: input.after },
successStatus: 200, successStatus: 200,
declaredStatuses: [404, 400, 401], declaredStatuses: [404, 400, 401],
empty: false, empty: false,
@@ -507,321 +374,6 @@ export function make(options: ClientOptions) {
requestOptions, requestOptions,
).then((value) => value.data), ).then((value) => value.data),
}, },
messages: {
list: (input: MessagesListInput, requestOptions?: RequestOptions) =>
request<MessagesListOutput>(
{
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<ModelsListOutput>(
{
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<ProvidersListOutput>(
{
method: "GET",
path: `/api/provider`,
query: { location: input?.["location"] },
successStatus: 200,
declaredStatuses: [503, 401, 400],
empty: false,
},
requestOptions,
),
get: (input: ProvidersGetInput, requestOptions?: RequestOptions) =>
request<ProvidersGetOutput>(
{
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<IntegrationsListOutput>(
{
method: "GET",
path: `/api/integration`,
query: { location: input?.["location"] },
successStatus: 200,
declaredStatuses: [401, 400],
empty: false,
},
requestOptions,
),
get: (input: IntegrationsGetInput, requestOptions?: RequestOptions) =>
request<IntegrationsGetOutput>(
{
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<IntegrationsConnectKeyOutput>(
{
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<IntegrationsConnectOauthOutput>(
{
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<IntegrationsAttemptStatusOutput>(
{
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<IntegrationsAttemptCompleteOutput>(
{
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<IntegrationsAttemptCancelOutput>(
{
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<CredentialsUpdateOutput>(
{
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<CredentialsRemoveOutput>(
{
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<PermissionsListRequestsOutput>(
{
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<PermissionsRemoveSavedOutput>(
{
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<PermissionsReplyOutput>(
{
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<FilesListOutput>(
{
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<FilesFindOutput>(
{
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<CommandsListOutput>(
{
method: "GET",
path: `/api/command`,
query: { location: input?.["location"] },
successStatus: 200,
declaredStatuses: [401, 400],
empty: false,
},
requestOptions,
),
},
skills: {
list: (input?: SkillsListInput, requestOptions?: RequestOptions) =>
request<SkillsListOutput>(
{
method: "GET",
path: `/api/skill`,
query: { location: input?.["location"] },
successStatus: 200,
declaredStatuses: [401, 400],
empty: false,
},
requestOptions,
),
},
events: { events: {
subscribe: (requestOptions?: RequestOptions): AsyncIterable<EventsSubscribeOutput> => subscribe: (requestOptions?: RequestOptions): AsyncIterable<EventsSubscribeOutput> =>
sse<EventsSubscribeOutput>( sse<EventsSubscribeOutput>(
@@ -829,178 +381,6 @@ export function make(options: ClientOptions) {
requestOptions, requestOptions,
), ),
}, },
ptys: {
list: (input?: PtysListInput, requestOptions?: RequestOptions) =>
request<PtysListOutput>(
{
method: "GET",
path: `/api/pty`,
query: { location: input?.["location"] },
successStatus: 200,
declaredStatuses: [401, 400],
empty: false,
},
requestOptions,
),
create: (input?: PtysCreateInput, requestOptions?: RequestOptions) =>
request<PtysCreateOutput>(
{
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<PtysGetOutput>(
{
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<PtysUpdateOutput>(
{
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<PtysRemoveOutput>(
{
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<QuestionsListRequestsOutput>(
{
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<QuestionsReplyOutput>(
{
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<QuestionsRejectOutput>(
{
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<ReferencesListOutput>(
{
method: "GET",
path: `/api/reference`,
query: { location: input?.["location"] },
successStatus: 200,
declaredStatuses: [401, 400],
empty: false,
},
requestOptions,
),
},
projectCopies: {
create: (input: ProjectCopiesCreateInput, requestOptions?: RequestOptions) =>
request<ProjectCopiesCreateOutput>(
{
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<ProjectCopiesRemoveOutput>(
{
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<ProjectCopiesRefreshOutput>(
{
method: "POST",
path: `/experimental/project/${encodeURIComponent(input.projectID)}/copy/refresh`,
query: { location: input["location"] },
successStatus: 204,
declaredStatuses: [400, 401],
empty: true,
},
requestOptions,
),
},
} }
} }
File diff suppressed because it is too large Load Diff
@@ -19,7 +19,8 @@ import { SessionMessage } from "@opencode-ai/schema/session-message"
import { Workspace } from "@opencode-ai/schema/workspace" import { Workspace } from "@opencode-ai/schema/workspace"
import { Api } from "@opencode-ai/server/api" import { Api } from "@opencode-ai/server/api"
import { compile, emitPromise } from "@opencode-ai/httpapi-codegen" import { compile, emitPromise } from "@opencode-ai/httpapi-codegen"
import { ClientApi, endpointNames, groupNames, omitEndpoints } from "../src/contract" import { HttpApi } from "effect/unstable/httpapi"
import { EventGroup, SessionGroup } from "../src/contract"
test("Core and Server reuse the authoritative Schema and Protocol values", () => { test("Core and Server reuse the authoritative Schema and Protocol values", () => {
expect(AgentV2.ID).toBe(Agent.ID) expect(AgentV2.ID).toBe(Agent.ID)
@@ -30,16 +31,18 @@ test("Core and Server reuse the authoritative Schema and Protocol values", () =>
expect(CoreSessionMessage.Message).toBe(SessionMessage.Message) expect(CoreSessionMessage.Message).toBe(SessionMessage.Message)
expect(CorePrompt).toBe(Prompt) expect(CorePrompt).toBe(Prompt)
expect(Api.groups["server.session"].identifier).toBe("server.session") expect(Api.groups["server.session"].identifier).toBe("server.session")
expect(Object.keys(ClientApi.groups)).toEqual(Object.keys(Api.groups)) expect(SessionGroup.identifier).toBe(Api.groups["server.session"].identifier)
expect(EventGroup.identifier).toBe(Api.groups["server.event"].identifier)
expect(Session.ID.create()).toStartWith("ses_") expect(Session.ID.create()).toStartWith("ses_")
expect(Project.ID.global).toBe("global") expect(Project.ID.global).toBe("global")
expect(Provider.ID.anthropic).toBe("anthropic") expect(Provider.ID.anthropic).toBe("anthropic")
expect(Workspace.ID.create()).toStartWith("wrk_") expect(Workspace.ID.create()).toStartWith("wrk_")
}) })
test("client and Server contracts generate identically", () => { test("client and Server Session contracts generate identically", () => {
const server = compile(Api, { groupNames, endpointNames, omitEndpoints }) const options = { groupNames: { "server.session": "sessions" } }
const client = compile(ClientApi, { groupNames, endpointNames, omitEndpoints }) const server = compile(HttpApi.make("server").add(Api.groups["server.session"]), options)
const client = compile(HttpApi.make("client").add(SessionGroup), options)
expect(emitPromise(client)).toEqual(emitPromise(server)) expect(emitPromise(client)).toEqual(emitPromise(server))
}) })
-37
View File
@@ -1,43 +1,6 @@
import { expect, test } from "bun:test" import { expect, test } from "bun:test"
import { isSessionNotFoundError, isUnauthorizedError, OpenCode } from "../src" 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",
"location",
"agents",
"sessions",
"messages",
"models",
"providers",
"integrations",
"credentials",
"permissions",
"files",
"commands",
"skills",
"events",
"ptys",
"questions",
"references",
"projectCopies",
])
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("sessions.get returns the wire projection", async () => { test("sessions.get returns the wire projection", async () => {
const client = OpenCode.make({ const client = OpenCode.make({
baseUrl: "http://localhost:3000", baseUrl: "http://localhost:3000",
+2 -3
View File
@@ -16,8 +16,8 @@
"opencode": "./bin/opencode" "opencode": "./bin/opencode"
}, },
"exports": { "exports": {
"./effect/layer-node": "./src/effect/layer-node.ts", "./effect/layer-node": "./src/effect/layer-node/index.ts",
"./effect/app-node": "./src/effect/app-node.ts", "./effect/node": "./src/effect/node.ts",
"./session/runner": "./src/session/runner/index.ts", "./session/runner": "./src/session/runner/index.ts",
"./system-context": "./src/system-context/index.ts", "./system-context": "./src/system-context/index.ts",
"./*": "./src/*.ts" "./*": "./src/*.ts"
@@ -104,7 +104,6 @@
"ai-gateway-provider": "3.1.2", "ai-gateway-provider": "3.1.2",
"bun-pty": "0.4.8", "bun-pty": "0.4.8",
"cross-spawn": "catalog:", "cross-spawn": "catalog:",
"diff": "catalog:",
"drizzle-orm": "catalog:", "drizzle-orm": "catalog:",
"effect": "catalog:", "effect": "catalog:",
"fuzzysort": "3.1.0", "fuzzysort": "3.1.0",
+1 -1
View File
@@ -1,6 +1,6 @@
export * as AgentV2 from "./agent" export * as AgentV2 from "./agent"
import { makeLocationNode } from "./effect/app-node" import { makeLocationNode } from "./effect/node"
import { Array, Context, Effect, Layer, Types } from "effect" import { Array, Context, Effect, Layer, Types } from "effect"
import { Agent } from "@opencode-ai/schema/agent" import { Agent } from "@opencode-ai/schema/agent"
import { State } from "./state" import { State } from "./state"
+1 -1
View File
@@ -1,6 +1,6 @@
export * as AISDK from "./aisdk" export * as AISDK from "./aisdk"
import { makeLocationNode } from "./effect/app-node" import { makeLocationNode } from "./effect/node"
import type { LanguageModelV3 } from "@ai-sdk/provider" import type { LanguageModelV3 } from "@ai-sdk/provider"
import { Cause, Context, Effect, Layer, Schema, Scope } from "effect" import { Cause, Context, Effect, Layer, Schema, Scope } from "effect"
import { ModelV2 } from "./model" import { ModelV2 } from "./model"
+1 -1
View File
@@ -2,7 +2,7 @@ export * as BackgroundJob from "./background-job"
import { Cause, Clock, Context, Deferred, Effect, Exit, Layer, Scope, SynchronizedRef } from "effect" import { Cause, Clock, Context, Deferred, Effect, Exit, Layer, Scope, SynchronizedRef } from "effect"
import { Identifier } from "./id/id" import { Identifier } from "./id/id"
import { makeGlobalNode } from "./effect/app-node" import { makeGlobalNode } from "./effect/node"
export type Status = "running" | "completed" | "error" | "cancelled" export type Status = "running" | "completed" | "error" | "cancelled"
+1 -1
View File
@@ -1,6 +1,6 @@
export * as Catalog from "./catalog" export * as Catalog from "./catalog"
import { makeLocationNode } from "./effect/app-node" import { makeLocationNode } from "./effect/node"
import { Array, Context, Effect, Layer, Option, Order, pipe, Schema } from "effect" import { Array, Context, Effect, Layer, Option, Order, pipe, Schema } from "effect"
import { Catalog } from "@opencode-ai/schema/catalog" import { Catalog } from "@opencode-ai/schema/catalog"
import { ModelV2 } from "./model" import { ModelV2 } from "./model"
+1 -1
View File
@@ -1,6 +1,6 @@
export * as CommandV2 from "./command" export * as CommandV2 from "./command"
import { makeLocationNode } from "./effect/app-node" import { makeLocationNode } from "./effect/node"
import { Context, Effect, Layer, Types } from "effect" import { Context, Effect, Layer, Types } from "effect"
import { Command } from "@opencode-ai/schema/command" import { Command } from "@opencode-ai/schema/command"
import { State } from "./state" import { State } from "./state"
+1 -1
View File
@@ -1,6 +1,6 @@
export * as Config from "./config" export * as Config from "./config"
import { makeLocationNode } from "./effect/app-node" import { makeLocationNode } from "./effect/node"
import path from "path" import path from "path"
import { type ParseError, parse } from "jsonc-parser" import { type ParseError, parse } from "jsonc-parser"
import { Context, Effect, Layer, Option, Schema } from "effect" import { Context, Effect, Layer, Option, Schema } from "effect"
+1 -1
View File
@@ -5,7 +5,7 @@ import { Context, Effect, Layer, Schema } from "effect"
import { Credential } from "@opencode-ai/schema/credential" import { Credential } from "@opencode-ai/schema/credential"
import { Integration } from "@opencode-ai/schema/integration" import { Integration } from "@opencode-ai/schema/integration"
import { Database } from "./database/database" import { Database } from "./database/database"
import { makeGlobalNode } from "./effect/app-node" import { makeGlobalNode } from "./effect/node"
import { CredentialTable } from "./credential/sql" import { CredentialTable } from "./credential/sql"
export const ID = Credential.ID export const ID = Credential.ID
+2 -2
View File
@@ -24,8 +24,8 @@ import {
import * as NodeChildProcess from "node:child_process" import * as NodeChildProcess from "node:child_process"
import { PassThrough } from "node:stream" import { PassThrough } from "node:stream"
import launch from "cross-spawn" import launch from "cross-spawn"
import { makeGlobalNode } from "./effect/app-node" import { makeGlobalNode } from "./effect/node"
import { filesystem, path } from "./effect/app-node-platform" import { filesystem, path } from "./effect/layer-node-platform"
const toError = (err: unknown): Error => (err instanceof globalThis.Error ? err : new globalThis.Error(String(err))) const toError = (err: unknown): Error => (err instanceof globalThis.Error ? err : new globalThis.Error(String(err)))
+1 -1
View File
@@ -8,7 +8,7 @@ import { Flag } from "../flag/flag"
import { isAbsolute, join } from "path" import { isAbsolute, join } from "path"
import { DatabaseMigration } from "./migration" import { DatabaseMigration } from "./migration"
import { InstallationChannel } from "../installation/version" import { InstallationChannel } from "../installation/version"
import { makeGlobalNode } from "../effect/app-node" import { makeGlobalNode } from "../effect/node"
const makeDatabase = EffectDrizzleSqlite.makeWithDefaults() const makeDatabase = EffectDrizzleSqlite.makeWithDefaults()
type DatabaseShape = Effect.Success<typeof makeDatabase> type DatabaseShape = Effect.Success<typeof makeDatabase>
@@ -3,7 +3,7 @@ import { LLMClient, RequestExecutor } from "@opencode-ai/llm/route"
import { FileSystem, Path } from "effect" import { FileSystem, Path } from "effect"
import { FetchHttpClient } from "effect/unstable/http" import { FetchHttpClient } from "effect/unstable/http"
import { HttpClient } from "effect/unstable/http" import { HttpClient } from "effect/unstable/http"
import { makeGlobalNode } from "./app-node" import { makeGlobalNode } from "./node"
export const filesystem = makeGlobalNode({ service: FileSystem.FileSystem, layer: NodeFileSystem.layer, deps: [] }) export const filesystem = makeGlobalNode({ service: FileSystem.FileSystem, layer: NodeFileSystem.layer, deps: [] })
export const path = makeGlobalNode({ service: Path.Path, layer: NodePath.layer, deps: [] }) export const path = makeGlobalNode({ service: Path.Path, layer: NodePath.layer, deps: [] })
@@ -15,4 +15,4 @@ export const requestExecutor = makeGlobalNode({
}) })
export const llmClient = makeGlobalNode({ service: LLMClient.Service, layer: LLMClient.layer, deps: [requestExecutor] }) export const llmClient = makeGlobalNode({ service: LLMClient.Service, layer: LLMClient.layer, deps: [requestExecutor] })
export * as LayerNodePlatform from "./app-node-platform" export * as LayerNodePlatform from "./layer-node-platform"
@@ -0,0 +1,2 @@
export * from "./layer-node"
export * as LayerNodeTree from "./layer-node-tree"
@@ -0,0 +1,118 @@
import { Layer } from "effect"
import { LayerNode } from "./layer-node"
type AnyNode = LayerNode.Node<unknown, unknown, any>
type RuntimeLayer = Layer.Layer<never, unknown, unknown>
export function hoist<A, E, T extends LayerNode.Tag>(
root: LayerNode.Node<A, E, any>,
tag: T,
): {
readonly node: LayerNode.Node<A, E>
readonly hoisted: LayerNode.Node<unknown, E>
} {
const visited = new Map<AnyNode, AnyNode>()
const hoisted = new Map<string, AnyNode>()
const visiting = new Set<AnyNode>()
const stack: AnyNode[] = []
const visit = (node: AnyNode): AnyNode => {
if (node.kind === "group") {
return { ...node, dependencies: node.dependencies.map(visit) }
}
const existingNode = visited.get(node)
if (existingNode) return existingNode
if (node.tag === tag) {
const existing = hoisted.get(node.name)
if (existing && existing !== node) {
throw new Error(`Tag ${tag} has conflicting implementations for ${node.name}`)
}
hoisted.set(node.name, node)
const empty = LayerNode.group([])
visited.set(node, empty)
return empty
}
if (node.kind === "unbound") {
return node
}
if (visiting.has(node)) {
const start = stack.indexOf(node)
throw new Error(
`Cycle detected in layer tree: ${[...stack.slice(start), node].map((item) => item.name).join(" -> ")}`,
)
}
visiting.add(node)
stack.push(node)
try {
const dependencies = node.dependencies.map(visit)
const clone = { ...node, dependencies }
visited.set(node, clone)
return clone
} finally {
stack.pop()
visiting.delete(node)
}
}
return {
node: visit(root) as LayerNode.Node<A, E>,
hoisted: LayerNode.group(Array.from(hoisted.values())) as LayerNode.Node<unknown, E>,
}
}
export function compile<A, E>(
root: LayerNode.Node<A, E, any>,
replacements?: ReadonlyMap<Layer.Any, Layer.Any>,
): Layer.Layer<A, E> {
const cache = new Map<AnyNode, RuntimeLayer>()
const compileNode = (node: AnyNode): RuntimeLayer => {
if (node.kind === "unbound") throw new Error(`Unbound layer node: ${node.name}`)
const cached = cache.get(node)
if (cached) return cached
const dependencies = node.dependencies.flatMap(flatten).map(compileNode)
const implementation = (replacements?.get(node.implementation!) ?? node.implementation!) as RuntimeLayer
const layer =
dependencies.length === 0
? implementation
: implementation.pipe(Layer.provide(dependencies as [RuntimeLayer, ...RuntimeLayer[]]))
cache.set(node, layer)
return layer
}
const layers = flatten(root).map((node) => compileNode(node))
const layer = layers.reduce<RuntimeLayer>((result, layer) => layer.pipe(Layer.provideMerge(result)), Layer.empty)
return layer as Layer.Layer<A, E>
}
export function bind<A, E, T extends LayerNode.Tag | undefined>(
root: LayerNode.Node<A, E, T>,
source: AnyNode,
replacement: AnyNode,
): LayerNode.Node<A, E, T> {
if (source.kind !== "unbound") throw new Error(`Cannot bind non-unbound layer node: ${source.name}`)
if (source.name !== replacement.name) {
throw new Error(`Cannot bind ${source.name} to ${replacement.name}`)
}
if (source.tag !== replacement.tag) {
throw new Error(`Cannot bind ${source.name} across tags`)
}
const visited = new Map<AnyNode, AnyNode>()
const visit = (node: AnyNode): AnyNode => {
if (node === source) return replacement
const existing = visited.get(node)
if (existing) return existing
if (node.kind === "unbound") return node
const clone = { ...node, dependencies: node.dependencies.map(visit) }
visited.set(node, clone)
return clone
}
return visit(root) as LayerNode.Node<A, E, T>
}
function flatten(node: AnyNode): readonly AnyNode[] {
return node.kind === "group" ? node.dependencies.flatMap(flatten) : [node]
}
export * as LayerNodeTree from "./layer-node-tree"
@@ -1,7 +1,6 @@
import { Brand, Context, Layer } from "effect" import { Brand, Context, Layer } from "effect"
type AnyNode = Node<unknown, unknown, any> type AnyNode = Node<unknown, unknown, any>
type RuntimeLayer = Layer.Layer<never, unknown, unknown>
type NodeList<Item extends AnyNode = AnyNode> = readonly [] | readonly [Item, ...Item[]] type NodeList<Item extends AnyNode = AnyNode> = readonly [] | readonly [Item, ...Item[]]
export type Output<Item> = [Item] extends [never] ? never : Item extends Node<infer A, unknown, any> ? A : never export type Output<Item> = [Item] extends [never] ? never : Item extends Node<infer A, unknown, any> ? A : never
export type Error<Item> = [Item] extends [never] ? never : Item extends Node<unknown, infer E, any> ? E : never export type Error<Item> = [Item] extends [never] ? never : Item extends Node<unknown, infer E, any> ? E : never
@@ -35,39 +34,6 @@ type NodeIdentity =
| { readonly name: string; readonly service?: never } | { readonly name: string; readonly service?: never }
type DistributiveOmit<A, K extends PropertyKey> = A extends unknown ? Omit<A, K> : never type DistributiveOmit<A, K extends PropertyKey> = A extends unknown ? Omit<A, K> : never
export type TagConfig = Readonly<Record<string, readonly string[]>>
type TagNames<Config extends TagConfig> = keyof Config & string
type NodeInTags<Names extends string> = Node<unknown, unknown, Tag<Names> | undefined>
type CheckTags<Items extends NodeList, Names extends string> = [Exclude<Items[number], NodeInTags<Names>>] extends [
never,
]
? unknown
: { readonly "Invalid tag dependencies": Exclude<Items[number], NodeInTags<Names>> }
export interface Tags<Config extends TagConfig> {
readonly values: { readonly [Name in TagNames<Config>]: Tag<Name> }
readonly make: <Name extends TagNames<Config>>(
name: Name,
) => <const Implementation extends Layer.Any, const Items extends NodeList>(
input: DistributiveOmit<MakeInput<Implementation, Items, Tag<Name>>, "tag"> &
CheckTags<Items, Name | Extract<Config[Name][number], string>>,
) => Node<Layer.Success<Implementation>, Layer.Error<Implementation> | Error<Items[number]>, Tag<Name>>
}
export function tags<const Config extends { readonly [Name in keyof Config]: readonly (keyof Config & string)[] }>(
config: Config,
): Tags<Config> {
const names = Object.keys(config) as TagNames<Config>[]
const values = Object.fromEntries(names.map((name) => [name, makeTag(name)])) as Tags<Config>["values"]
return {
values,
make: ((name: TagNames<Config>) => (input: DistributiveOmit<MakeInput<Layer.Any, NodeList, Tag>, "tag">) =>
make({ ...input, tag: values[name] })) as Tags<Config>["make"],
}
}
// Nodes ---------------------------------------------------------------------
type MakeInput< type MakeInput<
Implementation extends Layer.Any, Implementation extends Layer.Any,
Items extends NodeList, Items extends NodeList,
@@ -111,6 +77,37 @@ export function group<const Items extends readonly AnyNode[]>(
return { kind: "group", name: "group", dependencies } return { kind: "group", name: "group", dependencies }
} }
export type TagConfig = Readonly<Record<string, readonly string[]>>
type TagNames<Config extends TagConfig> = keyof Config & string
type NodeInTags<Names extends string> = Node<unknown, unknown, Tag<Names> | undefined>
type CheckTags<Items extends NodeList, Names extends string> = [Exclude<Items[number], NodeInTags<Names>>] extends [
never,
]
? unknown
: { readonly "Invalid tag dependencies": Exclude<Items[number], NodeInTags<Names>> }
export interface Tags<Config extends TagConfig> {
readonly values: { readonly [Name in TagNames<Config>]: Tag<Name> }
readonly make: <Name extends TagNames<Config>>(
name: Name,
) => <const Implementation extends Layer.Any, const Items extends NodeList>(
input: DistributiveOmit<MakeInput<Implementation, Items, Tag<Name>>, "tag"> &
CheckTags<Items, Name | Extract<Config[Name][number], string>>,
) => Node<Layer.Success<Implementation>, Layer.Error<Implementation> | Error<Items[number]>, Tag<Name>>
}
export function tags<const Config extends { readonly [Name in keyof Config]: readonly (keyof Config & string)[] }>(
config: Config,
): Tags<Config> {
const names = Object.keys(config) as TagNames<Config>[]
const values = Object.fromEntries(names.map((name) => [name, makeTag(name)])) as Tags<Config>["values"]
return {
values,
make: ((name: TagNames<Config>) => (input: DistributiveOmit<MakeInput<Layer.Any, NodeList, Tag>, "tag">) =>
make({ ...input, tag: values[name] })) as Tags<Config>["make"],
}
}
export type Replacement = { export type Replacement = {
readonly source: Layer.Any readonly source: Layer.Any
readonly replacement: Layer.Any readonly replacement: Layer.Any
@@ -127,147 +124,4 @@ export function replace<A, E, R, E2>(
return { source, replacement } return { source, replacement }
} }
// Tree -----------------------------------------------------------------------
type Visit<Result> = (node: AnyNode, context: VisitContext<Result>) => Result
type VisitContext<Result> = {
readonly cache: Map<AnyNode, Result>
readonly visit: (node: AnyNode) => Result
}
function walk<Result>(
root: AnyNode,
visit: Visit<Result>,
options: {
readonly cache?: Map<AnyNode, Result>
readonly resolve?: (node: AnyNode) => AnyNode
readonly detectCycles?: boolean
} = {},
) {
const cache = options.cache ?? new Map<AnyNode, Result>()
const visiting = new Set<AnyNode>()
const stack: AnyNode[] = []
const recur = (node: AnyNode): Result => {
const target = options.resolve?.(node) ?? node
const cached = cache.get(target)
if (cached !== undefined || cache.has(target)) return cached!
if (options.detectCycles !== false && visiting.has(target)) {
const start = stack.indexOf(target)
throw new Error(
`Cycle detected in layer tree: ${[...stack.slice(start), target].map((item) => item.name).join(" -> ")}`,
)
}
visiting.add(target)
stack.push(target)
try {
const result = visit(target, { cache, visit: recur })
if (!cache.has(target)) cache.set(target, result)
return result
} finally {
stack.pop()
visiting.delete(target)
}
}
return recur(root)
}
export function hoist<A, E, T extends Tag>(
root: Node<A, E, any>,
tag: T,
): {
readonly node: Node<A, E>
readonly hoisted: Node<unknown, E>
} {
const hoisted = new Map<string, AnyNode>()
const node = walk<AnyNode>(root, (node, context) => {
if (node.kind === "group") {
return { ...node, dependencies: node.dependencies.map(context.visit) }
}
if (node.tag === tag) {
const existing = hoisted.get(node.name)
if (existing && existing !== node) {
throw new Error(`Tag ${tag} has conflicting implementations for ${node.name}`)
}
hoisted.set(node.name, node)
return group([])
}
if (node.kind === "unbound") {
return node
}
return { ...node, dependencies: node.dependencies.map(context.visit) }
})
return {
node: node as Node<A, E>,
hoisted: group(Array.from(hoisted.values())) as Node<unknown, E>,
}
}
export function compile<A, E>(
root: Node<A, E, any>,
replacements?: ReadonlyMap<Layer.Any, Layer.Any>,
): Layer.Layer<A, E> {
const cache = new Map<AnyNode, RuntimeLayer>()
const compileNode = (node: AnyNode) =>
walk<RuntimeLayer>(
node,
(node, context) => {
if (node.kind === "unbound") throw new Error(`Unbound layer node: ${node.name}`)
const dependencies = node.dependencies.flatMap(flatten).map(context.visit)
const implementation = (replacements?.get(node.implementation!) ?? node.implementation!) as RuntimeLayer
return dependencies.length === 0
? implementation
: implementation.pipe(Layer.provide(dependencies as [RuntimeLayer, ...RuntimeLayer[]]))
},
{ cache },
)
const layers = flatten(root).map((node) => compileNode(node))
const layer = layers.reduce<RuntimeLayer>((result, layer) => layer.pipe(Layer.provideMerge(result)), Layer.empty)
return layer as Layer.Layer<A, E>
}
export function hasUnbound(root: Node<unknown, unknown, any>, source: AnyNode): boolean {
if (source.kind !== "unbound") throw new Error(`Cannot check non-unbound layer node: ${source.name}`)
return walk<boolean>(root, (node, context) => {
if (node === source) return true
return node.dependencies.some(context.visit)
})
}
export function bind<A, E, T extends Tag | undefined>(
root: Node<A, E, T>,
source: AnyNode,
replacement: AnyNode,
): Node<A, E, T> {
if (source.kind !== "unbound") throw new Error(`Cannot bind non-unbound layer node: ${source.name}`)
if (source.name !== replacement.name) {
throw new Error(`Cannot bind ${source.name} to ${replacement.name}`)
}
if (source.tag !== replacement.tag) {
throw new Error(`Cannot bind ${source.name} across tags`)
}
return walk<AnyNode>(
root,
(target, context) => {
if (target.kind === "unbound") return target
const dependencies: AnyNode[] = []
const clone = { ...target, dependencies }
context.cache.set(target, clone)
dependencies.push(...target.dependencies.map(context.visit))
return clone
},
{ detectCycles: false, resolve: (node) => (node === source ? replacement : node) },
) as Node<A, E, T>
}
function flatten(node: AnyNode): readonly AnyNode[] {
return node.kind === "group" ? node.dependencies.flatMap(flatten) : [node]
}
export * as LayerNode from "./layer-node" export * as LayerNode from "./layer-node"
@@ -1,24 +1,18 @@
import { Layer } from "effect" import { Layer } from "effect"
import { buildLocationServiceMap } from "../location-services" import { buildLocationServiceMap } from "../location-services"
import { LocationServiceMap } from "../location-service-map" import { LocationServiceMap } from "../location-service-map"
import { LayerNode } from "./layer-node" import { LayerNode, LayerNodeTree } from "./layer-node"
import { makeGlobalNode } from "./app-node" import { makeGlobalNode } from "./node"
export function build<A, E>(root: LayerNode.Node<A, E, any>, replacements?: readonly LayerNode.Replacement[]) { export function build<A, E>(root: LayerNode.Node<A, E, any>, replacements?: readonly LayerNode.Replacement[]) {
const replacementMap = new Map(replacements?.map((item) => [item.source, item.replacement])) const replacementMap = new Map(replacements?.map((item) => [item.source, item.replacement]))
if (!LayerNode.hasUnbound(root, LocationServiceMap.node)) {
// If the location service map is not needed, we shouldn't pull it
// in. Compile the graph normally
return LayerNode.compile(root, replacementMap)
}
const locationMap = buildLocationServiceMap(replacementMap) const locationMap = buildLocationServiceMap(replacementMap)
const locationMapNode = makeGlobalNode({ service: LocationServiceMap.Service, layer: locationMap, deps: [] }) const locationMapNode = makeGlobalNode({ service: LocationServiceMap.Service, layer: locationMap, deps: [] })
const app = LayerNode.bind(root, LocationServiceMap.node, locationMapNode) const app = LayerNodeTree.bind(root, LocationServiceMap.node, locationMapNode)
return LayerNode.compile(app, replacementMap) return LayerNodeTree.compile(app, replacementMap)
} }
export * as AppNodeBuilder from "./app-node-builder" export * as NodeBuild from "./node-build"
@@ -11,4 +11,4 @@ export type LocationNode<A, E = never> = LayerNode.Node<A, E, (typeof tags.value
export const makeGlobalNode = tags.make("global") export const makeGlobalNode = tags.make("global")
export const makeLocationNode = tags.make("location") export const makeLocationNode = tags.make("location")
export * as Node from "./app-node" export * as Node from "./node"
+1 -1
View File
@@ -7,7 +7,7 @@ import { and, asc, eq, gt, inArray } from "drizzle-orm"
import { Database } from "./database/database" import { Database } from "./database/database"
import { EventSequenceTable, EventTable } from "./event/sql" import { EventSequenceTable, EventTable } from "./event/sql"
import { Location } from "./location" import { Location } from "./location"
import { makeGlobalNode } from "./effect/app-node" import { makeGlobalNode } from "./effect/node"
import { isDeepStrictEqual } from "node:util" import { isDeepStrictEqual } from "node:util"
import { Durable } from "@opencode-ai/schema/durable-event-manifest" import { Durable } from "@opencode-ai/schema/durable-event-manifest"
+1 -1
View File
@@ -1,6 +1,6 @@
export * as FileMutation from "./file-mutation" export * as FileMutation from "./file-mutation"
import { makeLocationNode } from "./effect/app-node" import { makeLocationNode } from "./effect/node"
import { Context, Effect, Layer, Schema } from "effect" import { Context, Effect, Layer, Schema } from "effect"
import { dirname } from "path" import { dirname } from "path"
import { KeyedMutex } from "./effect/keyed-mutex" import { KeyedMutex } from "./effect/keyed-mutex"
+1 -1
View File
@@ -1,6 +1,6 @@
export * as FileSystem from "./filesystem" export * as FileSystem from "./filesystem"
import { makeLocationNode } from "./effect/app-node" import { makeLocationNode } from "./effect/node"
import path from "path" import path from "path"
import { Context, Effect, Layer, Schema } from "effect" import { Context, Effect, Layer, Schema } from "effect"
import { FSUtil } from "./fs-util" import { FSUtil } from "./fs-util"
+1 -1
View File
@@ -1,6 +1,6 @@
export * as FileSystemSearch from "./search" export * as FileSystemSearch from "./search"
import { makeLocationNode } from "../effect/app-node" import { makeLocationNode } from "../effect/node"
import path from "path" import path from "path"
import { Context, Effect, Layer, Scope } from "effect" import { Context, Effect, Layer, Scope } from "effect"
import { Fff } from "#fff" import { Fff } from "#fff"
+1 -1
View File
@@ -3,7 +3,7 @@ export * as Watcher from "./watcher"
// @ts-ignore // @ts-ignore
import { createWrapper } from "@parcel/watcher/wrapper" import { createWrapper } from "@parcel/watcher/wrapper"
import type ParcelWatcher from "@parcel/watcher" import type ParcelWatcher from "@parcel/watcher"
import { makeLocationNode } from "../effect/app-node" import { makeLocationNode } from "../effect/node"
import { Cause, Context, Effect, Layer } from "effect" import { Cause, Context, Effect, Layer } from "effect"
import { FileSystemWatcher } from "@opencode-ai/schema/filesystem-watcher" import { FileSystemWatcher } from "@opencode-ai/schema/filesystem-watcher"
import path from "path" import path from "path"
+2 -2
View File
@@ -7,8 +7,8 @@ import { Context, Effect, FileSystem, Layer, Schema } from "effect"
import type { PlatformError } from "effect/PlatformError" import type { PlatformError } from "effect/PlatformError"
import { Glob } from "./util/glob" import { Glob } from "./util/glob"
import { serviceUse } from "./effect/service-use" import { serviceUse } from "./effect/service-use"
import { makeGlobalNode } from "./effect/app-node" import { makeGlobalNode } from "./effect/node"
import { filesystem } from "./effect/app-node-platform" import { filesystem } from "./effect/layer-node-platform"
export namespace FSUtil { export namespace FSUtil {
export class FileSystemError extends Schema.TaggedErrorClass<FileSystemError>()("FileSystemError", { export class FileSystemError extends Schema.TaggedErrorClass<FileSystemError>()("FileSystemError", {
+1 -1
View File
@@ -7,7 +7,7 @@ import { ChildProcess } from "effect/unstable/process"
import { AbsolutePath, RelativePath } from "./schema" import { AbsolutePath, RelativePath } from "./schema"
import { FSUtil } from "./fs-util" import { FSUtil } from "./fs-util"
import { AppProcess } from "./process" import { AppProcess } from "./process"
import { makeGlobalNode } from "./effect/app-node" import { makeGlobalNode } from "./effect/node"
import { File } from "./file" import { File } from "./file"
import { KeyedMutex } from "./effect/keyed-mutex" import { KeyedMutex } from "./effect/keyed-mutex"
+1 -1
View File
@@ -5,7 +5,7 @@ import os from "os"
import { Context, Effect, Layer } from "effect" import { Context, Effect, Layer } from "effect"
import { Flock } from "./util/flock" import { Flock } from "./util/flock"
import { Flag } from "./flag/flag" import { Flag } from "./flag/flag"
import { makeGlobalNode } from "./effect/app-node" import { makeGlobalNode } from "./effect/node"
const app = "opencode" const app = "opencode"
const data = path.join(xdgData!, app) const data = path.join(xdgData!, app)
+1 -1
View File
@@ -1,6 +1,6 @@
export * as Image from "./image" export * as Image from "./image"
import { makeLocationNode } from "./effect/app-node" import { makeLocationNode } from "./effect/node"
import { Context, Effect, Layer, Schema } from "effect" import { Context, Effect, Layer, Schema } from "effect"
import { Config } from "./config" import { Config } from "./config"
import { FileSystem } from "./filesystem" import { FileSystem } from "./filesystem"
+1 -1
View File
@@ -9,7 +9,7 @@ import { Location } from "./location"
import { AbsolutePath } from "./schema" import { AbsolutePath } from "./schema"
import { SystemContext } from "./system-context/index" import { SystemContext } from "./system-context/index"
import { SystemContextRegistry } from "./system-context/registry" import { SystemContextRegistry } from "./system-context/registry"
import { makeLocationNode } from "./effect/app-node" import { makeLocationNode } from "./effect/node"
class File extends Schema.Class<File>("InstructionContext.File")({ class File extends Schema.Class<File>("InstructionContext.File")({
path: AbsolutePath, path: AbsolutePath,
+1 -1
View File
@@ -1,6 +1,6 @@
export * as Integration from "./integration" export * as Integration from "./integration"
import { makeLocationNode } from "./effect/app-node" import { makeLocationNode } from "./effect/node"
import { import {
Cause, Cause,
Clock, Clock,
+1 -1
View File
@@ -1,6 +1,6 @@
export * as LocationMutation from "./location-mutation" export * as LocationMutation from "./location-mutation"
import { makeLocationNode } from "./effect/app-node" import { makeLocationNode } from "./effect/node"
import path from "path" import path from "path"
import { Context, Effect, Layer, Schema } from "effect" import { Context, Effect, Layer, Schema } from "effect"
import { FSUtil } from "./fs-util" import { FSUtil } from "./fs-util"
+1 -1
View File
@@ -1,6 +1,6 @@
import { Context, Effect, Layer, LayerMap } from "effect" import { Context, Effect, Layer, LayerMap } from "effect"
import { LayerNode } from "./effect/layer-node" import { LayerNode } from "./effect/layer-node"
import { Node } from "./effect/app-node" import { Node } from "./effect/node"
import { Location } from "./location" import { Location } from "./location"
import type { LocationError, LocationServices } from "./location-services" import type { LocationError, LocationServices } from "./location-services"
+6 -8
View File
@@ -4,8 +4,8 @@ import { AISDK } from "./aisdk"
import { Catalog } from "./catalog" import { Catalog } from "./catalog"
import { CommandV2 } from "./command" import { CommandV2 } from "./command"
import { Config } from "./config" import { Config } from "./config"
import { LayerNode } from "./effect/layer-node" import { LayerNode, LayerNodeTree } from "./effect/layer-node"
import { Node } from "./effect/app-node" import { Node } from "./effect/node"
import { FileMutation } from "./file-mutation" import { FileMutation } from "./file-mutation"
import { FileSystem } from "./filesystem" import { FileSystem } from "./filesystem"
import { FileSystemSearch } from "./filesystem/search" import { FileSystemSearch } from "./filesystem/search"
@@ -26,7 +26,6 @@ import { Reference } from "./reference"
import { ReferenceGuidance } from "./reference/guidance" import { ReferenceGuidance } from "./reference/guidance"
import * as SessionRunnerLLM from "./session/runner/llm" import * as SessionRunnerLLM from "./session/runner/llm"
import { SessionRunnerModel } from "./session/runner/model" import { SessionRunnerModel } from "./session/runner/model"
import { SessionRuntime } from "./session/runtime"
import { SessionTodo } from "./session/todo" import { SessionTodo } from "./session/todo"
import { SkillV2 } from "./skill" import { SkillV2 } from "./skill"
import { SkillGuidance } from "./skill/guidance" import { SkillGuidance } from "./skill/guidance"
@@ -76,7 +75,6 @@ export const locationServices = LayerNode.group([
BuiltInTools.node, BuiltInTools.node,
SessionRunnerModel.node, SessionRunnerModel.node,
Snapshot.node, Snapshot.node,
SessionRuntime.node,
SessionRunnerLLM.node, SessionRunnerLLM.node,
]) ])
@@ -90,11 +88,11 @@ export function buildLocationServiceMap(
LocationServiceMap.Service, LocationServiceMap.Service,
LayerMap.make( LayerMap.make(
(ref: Location.Ref) => { (ref: Location.Ref) => {
const location = LayerNode.hoist( const location = LayerNodeTree.hoist(
LayerNode.bind(locationServices, Location.node, Location.boundNode(ref)), LayerNodeTree.bind(locationServices, Location.node, Location.boundNode(ref)),
Node.tags.values.global, Node.tags.values.global,
) )
return LayerNode.compile(location.node, replacements).pipe( return LayerNodeTree.compile(location.node, replacements).pipe(
Layer.fresh, Layer.fresh,
Layer.tap(() => Layer.tap(() =>
Effect.logInfo("booting location services", { Effect.logInfo("booting location services", {
@@ -102,7 +100,7 @@ export function buildLocationServiceMap(
workspaceID: ref.workspaceID, workspaceID: ref.workspaceID,
}), }),
), ),
Layer.provide(LayerNode.compile(location.hoisted, replacements)), Layer.provide(LayerNodeTree.compile(location.hoisted, replacements)),
) )
}, },
{ idleTimeToLive: "60 minutes" }, { idleTimeToLive: "60 minutes" },
+1 -1
View File
@@ -2,7 +2,7 @@ import { Context, Effect, Layer } from "effect"
import { Info, Ref, response } from "@opencode-ai/schema/location" import { Info, Ref, response } from "@opencode-ai/schema/location"
import { Project } from "./project" import { Project } from "./project"
import { LayerNode } from "./effect/layer-node" import { LayerNode } from "./effect/layer-node"
import { makeLocationNode, tags } from "./effect/app-node" import { makeLocationNode, tags } from "./effect/node"
export * as Location from "./location" export * as Location from "./location"
+2 -2
View File
@@ -9,8 +9,8 @@ import { Hash } from "./util/hash"
import { FSUtil } from "./fs-util" import { FSUtil } from "./fs-util"
import { InstallationChannel, InstallationVersion } from "./installation/version" import { InstallationChannel, InstallationVersion } from "./installation/version"
import { EventV2 } from "./event" import { EventV2 } from "./event"
import { makeGlobalNode } from "./effect/app-node" import { makeGlobalNode } from "./effect/node"
import { httpClient } from "./effect/app-node-platform" import { httpClient } from "./effect/layer-node-platform"
export const CatalogModelStatus = Schema.Literals(["alpha", "beta", "deprecated"]) export const CatalogModelStatus = Schema.Literals(["alpha", "beta", "deprecated"])
export type CatalogModelStatus = typeof CatalogModelStatus.Type export type CatalogModelStatus = typeof CatalogModelStatus.Type
+2 -2
View File
@@ -7,8 +7,8 @@ import { NodeFileSystem } from "@effect/platform-node"
import { FSUtil } from "./fs-util" import { FSUtil } from "./fs-util"
import { Global } from "./global" import { Global } from "./global"
import { EffectFlock } from "./util/effect-flock" import { EffectFlock } from "./util/effect-flock"
import { makeGlobalNode } from "./effect/app-node" import { makeGlobalNode } from "./effect/node"
import { filesystem } from "./effect/app-node-platform" import { filesystem } from "./effect/layer-node-platform"
import { makeRuntime } from "./effect/runtime" import { makeRuntime } from "./effect/runtime"
import { NpmConfig } from "./npm-config" import { NpmConfig } from "./npm-config"
+1 -1
View File
@@ -1,6 +1,6 @@
export * as PermissionV2 from "./permission" export * as PermissionV2 from "./permission"
import { makeLocationNode } from "./effect/app-node" import { makeLocationNode } from "./effect/node"
import { Context, Deferred, Effect as EffectRuntime, Layer, Schema } from "effect" import { Context, Deferred, Effect as EffectRuntime, Layer, Schema } from "effect"
import { Permission } from "@opencode-ai/schema/permission" import { Permission } from "@opencode-ai/schema/permission"
import { EventV2 } from "./event" import { EventV2 } from "./event"
+1 -1
View File
@@ -3,7 +3,7 @@ export * as PermissionSaved from "./saved"
import { eq } from "drizzle-orm" import { eq } from "drizzle-orm"
import { Context, Effect, Layer, Schema } from "effect" import { Context, Effect, Layer, Schema } from "effect"
import { Database } from "../database/database" import { Database } from "../database/database"
import { makeGlobalNode } from "../effect/app-node" import { makeGlobalNode } from "../effect/node"
import { ProjectV2 } from "../project" import { ProjectV2 } from "../project"
import { PermissionTable } from "./sql" import { PermissionTable } from "./sql"
import { PermissionSaved } from "@opencode-ai/schema/permission-saved" import { PermissionSaved } from "@opencode-ai/schema/permission-saved"
+1 -9
View File
@@ -1,6 +1,6 @@
export * as PluginV2 from "./plugin" export * as PluginV2 from "./plugin"
import { makeLocationNode } from "./effect/app-node" import { makeLocationNode } from "./effect/node"
import { Context, Deferred, Effect, Exit, Layer, Scope } from "effect" import { Context, Deferred, Effect, Exit, Layer, Scope } from "effect"
import type { Plugin as PluginRuntime } from "@opencode-ai/plugin/v2/effect" import type { Plugin as PluginRuntime } from "@opencode-ai/plugin/v2/effect"
import { Plugin } from "@opencode-ai/schema/plugin" import { Plugin } from "@opencode-ai/schema/plugin"
@@ -13,10 +13,7 @@ import { Integration } from "./integration"
import { KeyedMutex } from "./effect/keyed-mutex" import { KeyedMutex } from "./effect/keyed-mutex"
import { PluginHost } from "./plugin/host" import { PluginHost } from "./plugin/host"
import { Reference } from "./reference" import { Reference } from "./reference"
import { SessionV2 } from "./session"
import { SessionRuntime } from "./session/runtime"
import { SkillV2 } from "./skill" import { SkillV2 } from "./skill"
import { Location } from "./location"
import { State } from "./state" import { State } from "./state"
export const ID = Plugin.ID export const ID = Plugin.ID
@@ -152,8 +149,6 @@ export const locationLayer = layer.pipe(
Layer.provideMerge(CommandV2.locationLayer), Layer.provideMerge(CommandV2.locationLayer),
Layer.provideMerge(Integration.locationLayer), Layer.provideMerge(Integration.locationLayer),
Layer.provideMerge(Reference.locationLayer), Layer.provideMerge(Reference.locationLayer),
Layer.provideMerge(SessionV2.defaultLayer),
Layer.provideMerge(SessionRuntime.layer),
Layer.provideMerge(SkillV2.locationLayer), Layer.provideMerge(SkillV2.locationLayer),
) )
@@ -168,9 +163,6 @@ export const node = makeLocationNode({
CommandV2.node, CommandV2.node,
Integration.node, Integration.node,
Reference.node, Reference.node,
SessionV2.node,
SessionRuntime.node,
SkillV2.node, SkillV2.node,
Location.node,
], ],
}) })
-61
View File
@@ -13,11 +13,7 @@ import { PluginV2 } from "../plugin"
import { ProviderV2 } from "../provider" import { ProviderV2 } from "../provider"
import { Reference } from "../reference" import { Reference } from "../reference"
import type { DeepMutable } from "../schema" import type { DeepMutable } from "../schema"
import { SessionV2 } from "../session"
import { SessionMessage } from "../session/message"
import { SessionRuntime } from "../session/runtime"
import { SkillV2 } from "../skill" import { SkillV2 } from "../skill"
import { Location } from "../location"
const mutable = <T>(value: T) => value as DeepMutable<T> const mutable = <T>(value: T) => value as DeepMutable<T>
@@ -28,10 +24,7 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Int
const commands = yield* CommandV2.Service const commands = yield* CommandV2.Service
const integration = yield* Integration.Service const integration = yield* Integration.Service
const reference = yield* Reference.Service const reference = yield* Reference.Service
const session = yield* SessionV2.Service
const sessionRuntime = yield* SessionRuntime.Service
const skill = yield* SkillV2.Service const skill = yield* SkillV2.Service
const location = yield* Location.Service
return { return {
options: {}, options: {},
@@ -212,60 +205,6 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Int
}), }),
), ),
}, },
session: {
create: (input) =>
session.create(
input.parentID
? {
id: input.id ? SessionV2.ID.make(input.id) : undefined,
parentID: SessionV2.ID.make(input.parentID),
title: input.title,
agent: input.agent ? AgentV2.ID.make(input.agent) : undefined,
model: input.model
? {
id: ModelV2.ID.make(input.model.id),
providerID: ProviderV2.ID.make(input.model.providerID),
variant: input.model.variant ? ModelV2.VariantID.make(input.model.variant) : undefined,
}
: undefined,
}
: {
id: input.id ? SessionV2.ID.make(input.id) : undefined,
location: { directory: location.directory, workspaceID: location.workspaceID },
title: input.title,
agent: input.agent ? AgentV2.ID.make(input.agent) : undefined,
model: input.model
? {
id: ModelV2.ID.make(input.model.id),
providerID: ProviderV2.ID.make(input.model.providerID),
variant: input.model.variant ? ModelV2.VariantID.make(input.model.variant) : undefined,
}
: undefined,
},
),
get: (sessionID) => session.get(SessionV2.ID.make(sessionID)),
messages: (input) =>
session.messages({
sessionID: SessionV2.ID.make(input.sessionID),
limit: input.limit,
order: input.order,
cursor: input.cursor
? { id: SessionMessage.ID.make(input.cursor.id), direction: input.cursor.direction }
: undefined,
}),
context: (sessionID) => session.context(SessionV2.ID.make(sessionID)),
prompt: (input) =>
sessionRuntime.prompt({
id: input.id ? SessionMessage.ID.make(input.id) : undefined,
sessionID: SessionV2.ID.make(input.sessionID),
prompt: input.prompt,
delivery: input.delivery,
resume: input.resume,
}),
resume: (sessionID) => sessionRuntime.resume(SessionV2.ID.make(sessionID)),
wait: (sessionID) => sessionRuntime.wait(SessionV2.ID.make(sessionID)),
interrupt: (sessionID) => sessionRuntime.interrupt(SessionV2.ID.make(sessionID)),
},
skill: { skill: {
reload: skill.reload, reload: skill.reload,
transform: (callback) => transform: (callback) =>
+16 -19
View File
@@ -1,7 +1,7 @@
export * as PluginInternal from "./internal" export * as PluginInternal from "./internal"
import { makeLocationNode } from "../effect/app-node" import { makeLocationNode } from "../effect/node"
import { httpClient } from "../effect/app-node-platform" import { httpClient } from "../effect/layer-node-platform"
import type { PluginContext } from "@opencode-ai/plugin/v2/effect" import type { PluginContext } from "@opencode-ai/plugin/v2/effect"
import { Effect, Layer, Scope } from "effect" import { Effect, Layer, Scope } from "effect"
import { AgentV2 } from "../agent" import { AgentV2 } from "../agent"
@@ -25,7 +25,6 @@ import { Npm } from "../npm"
import { PluginV2 } from "../plugin" import { PluginV2 } from "../plugin"
import { Reference } from "../reference" import { Reference } from "../reference"
import { SkillV2 } from "../skill" import { SkillV2 } from "../skill"
import { State } from "../state"
import { FetchHttpClient, HttpClient } from "effect/unstable/http" import { FetchHttpClient, HttpClient } from "effect/unstable/http"
import { AgentPlugin } from "./agent" import { AgentPlugin } from "./agent"
import { CommandPlugin } from "./command" import { CommandPlugin } from "./command"
@@ -105,22 +104,20 @@ const layer = Layer.effectDiscard(
return plugin.add(PluginV2.ID.make(loaded.id), loaded.effect) return plugin.add(PluginV2.ID.make(loaded.id), loaded.effect)
} }
yield* State.batch( yield* Effect.gen(function* () {
Effect.gen(function* () { yield* add(ConfigReferencePlugin.Plugin)
yield* add(ConfigReferencePlugin.Plugin) yield* add(AgentPlugin.Plugin)
yield* add(AgentPlugin.Plugin) yield* add(CommandPlugin.Plugin)
yield* add(CommandPlugin.Plugin) yield* add(SkillPlugin.Plugin)
yield* add(SkillPlugin.Plugin) yield* add(ModelsDevPlugin)
yield* add(ModelsDevPlugin) yield* add(ConfigAgentPlugin.Plugin)
yield* add(ConfigAgentPlugin.Plugin) yield* add(ConfigCommandPlugin.Plugin)
yield* add(ConfigCommandPlugin.Plugin) yield* add(ConfigSkillPlugin.Plugin)
yield* add(ConfigSkillPlugin.Plugin) for (const item of ProviderPlugins) yield* add(item)
for (const item of ProviderPlugins) yield* add(item) yield* add(ConfigExternalPlugin.Plugin)
yield* add(ConfigExternalPlugin.Plugin) yield* add(ConfigProviderPlugin.Plugin)
yield* add(ConfigProviderPlugin.Plugin) yield* add(VariantPlugin.Plugin)
yield* add(VariantPlugin.Plugin) }).pipe(Effect.withSpan("PluginInternal.boot"), Effect.forkScoped({ startImmediately: true }))
}),
).pipe(Effect.withSpan("PluginInternal.boot"), Effect.forkScoped({ startImmediately: true }))
}), }),
) )
-10
View File
@@ -81,16 +81,6 @@ export function fromPromise(plugin: Plugin) {
transform: transform(host.reference), transform: transform(host.reference),
reload: () => run(host.reference.reload()), reload: () => run(host.reference.reload()),
}, },
session: {
create: (input) => run(host.session.create(input)),
get: (sessionID) => run(host.session.get(sessionID)),
messages: (input) => run(host.session.messages(input)),
context: (sessionID) => run(host.session.context(sessionID)),
prompt: (input) => run(host.session.prompt(input)),
resume: (sessionID) => run(host.session.resume(sessionID)),
wait: (sessionID) => run(host.session.wait(sessionID)),
interrupt: (sessionID) => run(host.session.interrupt(sessionID)),
},
skill: { skill: {
transform: transform(host.skill), transform: transform(host.skill),
reload: () => run(host.skill.reload()), reload: () => run(host.skill.reload()),
@@ -1,4 +1,4 @@
import { Duration, Effect, Schema, Semaphore, Stream } from "effect" import { Duration, Effect, Schema, Stream } from "effect"
import type { Scope } from "effect" import type { Scope } from "effect"
import type { IntegrationOAuthMethodRegistration } from "@opencode-ai/plugin/v2/effect/integration" import type { IntegrationOAuthMethodRegistration } from "@opencode-ai/plugin/v2/effect/integration"
import { define } from "@opencode-ai/plugin/v2/effect/plugin" import { define } from "@opencode-ai/plugin/v2/effect/plugin"
@@ -79,7 +79,6 @@ export const OpencodePlugin = define<HttpClient.HttpClient | EventV2.Service | S
effect: Effect.fn(function* (ctx) { effect: Effect.fn(function* (ctx) {
const events = yield* EventV2.Service const events = yield* EventV2.Service
const http = yield* HttpClient.HttpClient const http = yield* HttpClient.HttpClient
const loading = Semaphore.makeUnsafe(1)
let connected = false let connected = false
let providers: typeof ConfigV1.Info.Type.provider | undefined let providers: typeof ConfigV1.Info.Type.provider | undefined
@@ -106,7 +105,7 @@ export const OpencodePlugin = define<HttpClient.HttpClient | EventV2.Service | S
draft.method.update({ integrationID: "opencode", method: { type: "key", label: "API key (service account)" } }) draft.method.update({ integrationID: "opencode", method: { type: "key", label: "API key (service account)" } })
}) })
connected = (yield* ctx.integration.connection.active("opencode")) !== undefined yield* load()
yield* ctx.catalog.transform((catalog) => { yield* ctx.catalog.transform((catalog) => {
for (const [providerID, item] of Object.entries(providers ?? {})) { for (const [providerID, item] of Object.entries(providers ?? {})) {
catalog.provider.update(providerID, (provider) => { catalog.provider.update(providerID, (provider) => {
@@ -177,13 +176,11 @@ export const OpencodePlugin = define<HttpClient.HttpClient | EventV2.Service | S
} }
}) })
const refresh = () => loading.withPermit(load().pipe(Effect.andThen(ctx.catalog.reload())))
yield* events.subscribe(Integration.Event.ConnectionUpdated).pipe( yield* events.subscribe(Integration.Event.ConnectionUpdated).pipe(
Stream.filter((event) => event.data.integrationID === Integration.ID.make("opencode")), Stream.filter((event) => event.data.integrationID === Integration.ID.make("opencode")),
Stream.runForEach(refresh), Stream.runForEach(() => load().pipe(Effect.andThen(ctx.catalog.reload()))),
Effect.forkScoped({ startImmediately: true }), Effect.forkScoped({ startImmediately: true }),
) )
yield* refresh().pipe(Effect.forkScoped)
}), }),
}) })
+1 -1
View File
@@ -1,6 +1,6 @@
export * as Policy from "./policy" export * as Policy from "./policy"
import { makeLocationNode } from "./effect/app-node" import { makeLocationNode } from "./effect/node"
import { Context, Effect as EffectRuntime, Layer, Schema } from "effect" import { Context, Effect as EffectRuntime, Layer, Schema } from "effect"
import { Wildcard } from "./util/wildcard" import { Wildcard } from "./util/wildcard"
import { Location } from "./location" import { Location } from "./location"
+1 -20
View File
@@ -3,7 +3,7 @@ import type { PlatformError } from "effect/PlatformError"
import { ChildProcess } from "effect/unstable/process" import { ChildProcess } from "effect/unstable/process"
import { ChildProcessSpawner } from "effect/unstable/process/ChildProcessSpawner" import { ChildProcessSpawner } from "effect/unstable/process/ChildProcessSpawner"
import { CrossSpawnSpawner } from "./cross-spawn-spawner" import { CrossSpawnSpawner } from "./cross-spawn-spawner"
import { makeGlobalNode } from "./effect/app-node" import { makeGlobalNode } from "./effect/node"
export class AppProcessError extends Schema.TaggedErrorClass<AppProcessError>()("AppProcessError", { export class AppProcessError extends Schema.TaggedErrorClass<AppProcessError>()("AppProcessError", {
command: Schema.String, command: Schema.String,
@@ -20,7 +20,6 @@ export class AppProcessError extends Schema.TaggedErrorClass<AppProcessError>()(
} }
export interface RunOptions { export interface RunOptions {
readonly combineOutput?: boolean
readonly maxOutputBytes?: number readonly maxOutputBytes?: number
readonly maxErrorBytes?: number readonly maxErrorBytes?: number
readonly signal?: AbortSignal readonly signal?: AbortSignal
@@ -38,10 +37,8 @@ export interface RunStreamOptions {
export interface RunResult { export interface RunResult {
readonly command: string readonly command: string
readonly exitCode: number readonly exitCode: number
readonly output?: Buffer
readonly stdout: Buffer readonly stdout: Buffer
readonly stderr: Buffer readonly stderr: Buffer
readonly outputTruncated?: boolean
readonly stdoutTruncated: boolean readonly stdoutTruncated: boolean
readonly stderrTruncated: boolean readonly stderrTruncated: boolean
} }
@@ -146,22 +143,6 @@ export const layer = Layer.effect(
const collect = Effect.scoped( const collect = Effect.scoped(
Effect.gen(function* () { Effect.gen(function* () {
const handle = yield* spawner.spawn(command) const handle = yield* spawner.spawn(command)
if (options?.combineOutput) {
const [output, exitCode] = yield* Effect.all(
[collectStream(handle.all, options.maxOutputBytes), handle.exitCode],
{ concurrency: "unbounded" },
)
return {
command: description,
exitCode,
output: output.buffer,
stdout: Buffer.alloc(0),
stderr: Buffer.alloc(0),
outputTruncated: output.truncated,
stdoutTruncated: false,
stderrTruncated: false,
} satisfies RunResult
}
const [stdout, stderr, exitCode] = yield* Effect.all( const [stdout, stderr, exitCode] = yield* Effect.all(
[ [
collectStream(handle.stdout, options?.maxOutputBytes), collectStream(handle.stdout, options?.maxOutputBytes),
+1 -1
View File
@@ -6,7 +6,7 @@ import path from "path"
import { AbsolutePath } from "./schema" import { AbsolutePath } from "./schema"
import { FSUtil } from "./fs-util" import { FSUtil } from "./fs-util"
import { Git } from "./git" import { Git } from "./git"
import { makeGlobalNode } from "./effect/app-node" import { makeGlobalNode } from "./effect/node"
import { Hash } from "./util/hash" import { Hash } from "./util/hash"
import { ProjectDirectories } from "./project/directories" import { ProjectDirectories } from "./project/directories"
import { ProjectSchema } from "./project/schema" import { ProjectSchema } from "./project/schema"
+1 -1
View File
@@ -5,7 +5,7 @@ import path from "path"
import { AbsolutePath } from "../schema" import { AbsolutePath } from "../schema"
import { FSUtil } from "../fs-util" import { FSUtil } from "../fs-util"
import { Git } from "../git" import { Git } from "../git"
import { makeLocationNode } from "../effect/app-node" import { makeLocationNode } from "../effect/node"
import { Project } from "../project" import { Project } from "../project"
import { ProjectDirectories } from "./directories" import { ProjectDirectories } from "./directories"
import { makeGitWorktreeStrategy } from "./copy-strategies" import { makeGitWorktreeStrategy } from "./copy-strategies"
+1 -1
View File
@@ -3,7 +3,7 @@ export * as ProjectDirectories from "./directories"
import { and, asc, desc, eq, isNotNull, isNull, ne, or } from "drizzle-orm" import { and, asc, desc, eq, isNotNull, isNull, ne, or } from "drizzle-orm"
import { Context, Effect, Layer, Schema } from "effect" import { Context, Effect, Layer, Schema } from "effect"
import { Database } from "../database/database" import { Database } from "../database/database"
import { makeGlobalNode } from "../effect/app-node" import { makeGlobalNode } from "../effect/node"
import { AbsolutePath, optional } from "../schema" import { AbsolutePath, optional } from "../schema"
import { ProjectSchema } from "./schema" import { ProjectSchema } from "./schema"
import { ProjectDirectoryTable } from "./sql" import { ProjectDirectoryTable } from "./sql"
+1 -1
View File
@@ -1,6 +1,6 @@
export * as Pty from "./pty" export * as Pty from "./pty"
import { makeLocationNode } from "./effect/app-node" import { makeLocationNode } from "./effect/node"
import type { Disp, Proc } from "#pty" import type { Disp, Proc } from "#pty"
import { Context, Effect, Layer, Schema, Types } from "effect" import { Context, Effect, Layer, Schema, Types } from "effect"
import { Pty } from "@opencode-ai/schema/pty" import { Pty } from "@opencode-ai/schema/pty"
+1 -1
View File
@@ -4,7 +4,7 @@ import { WorkspaceV2 } from "../workspace"
import { PtyTicket } from "@opencode-ai/schema/pty-ticket" import { PtyTicket } from "@opencode-ai/schema/pty-ticket"
import { PtyID } from "./schema" import { PtyID } from "./schema"
import { Cache, Context, Duration, Effect, Layer } from "effect" import { Cache, Context, Duration, Effect, Layer } from "effect"
import { makeGlobalNode } from "../effect/app-node" import { makeGlobalNode } from "../effect/node"
const DEFAULT_TTL = Duration.seconds(60) const DEFAULT_TTL = Duration.seconds(60)
const CAPACITY = 10_000 const CAPACITY = 10_000
@@ -0,0 +1,3 @@
export * as PublicEventManifest from "./public-event-manifest"
export { ServerDefinitions as Definitions } from "@opencode-ai/schema/event-manifest"
+1 -1
View File
@@ -1,6 +1,6 @@
export * as QuestionV2 from "./question" export * as QuestionV2 from "./question"
import { makeLocationNode } from "./effect/app-node" import { makeLocationNode } from "./effect/node"
import { Context, Deferred, Effect, Layer, Schema } from "effect" import { Context, Deferred, Effect, Layer, Schema } from "effect"
import { Question } from "@opencode-ai/schema/question" import { Question } from "@opencode-ai/schema/question"
import { EventV2 } from "./event" import { EventV2 } from "./event"
+1 -1
View File
@@ -1,6 +1,6 @@
export * as Reference from "./reference" export * as Reference from "./reference"
import { makeLocationNode } from "./effect/app-node" import { makeLocationNode } from "./effect/node"
import { Context, Effect, Layer, Scope, Types } from "effect" import { Context, Effect, Layer, Scope, Types } from "effect"
import { Reference } from "@opencode-ai/schema/reference" import { Reference } from "@opencode-ai/schema/reference"
import { Global } from "./global" import { Global } from "./global"
+1 -1
View File
@@ -1,6 +1,6 @@
export * as ReferenceGuidance from "./guidance" export * as ReferenceGuidance from "./guidance"
import { makeLocationNode } from "../effect/app-node" import { makeLocationNode } from "../effect/node"
import { Context, Effect, Layer, Schema } from "effect" import { Context, Effect, Layer, Schema } from "effect"
import { Reference } from "../reference" import { Reference } from "../reference"
import { SystemContext } from "../system-context/index" import { SystemContext } from "../system-context/index"
+1 -1
View File
@@ -5,7 +5,7 @@ import { Git } from "./git"
import { Global } from "./global" import { Global } from "./global"
import { Repository } from "./repository" import { Repository } from "./repository"
import { AbsolutePath } from "./schema" import { AbsolutePath } from "./schema"
import { makeGlobalNode } from "./effect/app-node" import { makeGlobalNode } from "./effect/node"
import { EffectFlock } from "./util/effect-flock" import { EffectFlock } from "./util/effect-flock"
export type Result = { export type Result = {
+1 -1
View File
@@ -3,7 +3,7 @@ export * as Ripgrep from "./ripgrep"
import { Context, Effect, Fiber, Layer, Schema, Stream } from "effect" import { Context, Effect, Fiber, Layer, Schema, Stream } from "effect"
import { ChildProcess } from "effect/unstable/process" import { ChildProcess } from "effect/unstable/process"
import { Entry, Match } from "@opencode-ai/schema/filesystem" import { Entry, Match } from "@opencode-ai/schema/filesystem"
import { makeGlobalNode } from "./effect/app-node" import { makeGlobalNode } from "./effect/node"
import { AppProcess, collectStream, waitForAbort } from "./process" import { AppProcess, collectStream, waitForAbort } from "./process"
import { NonNegativeInt, PositiveInt, RelativePath } from "./schema" import { NonNegativeInt, PositiveInt, RelativePath } from "./schema"
import { RipgrepBinary } from "./ripgrep/binary" import { RipgrepBinary } from "./ripgrep/binary"
+2 -2
View File
@@ -4,8 +4,8 @@ import { FetchHttpClient, HttpClient, HttpClientRequest } from "effect/unstable/
import { ChildProcess } from "effect/unstable/process" import { ChildProcess } from "effect/unstable/process"
import { ChildProcessSpawner } from "effect/unstable/process/ChildProcessSpawner" import { ChildProcessSpawner } from "effect/unstable/process/ChildProcessSpawner"
import { CrossSpawnSpawner } from "../cross-spawn-spawner" import { CrossSpawnSpawner } from "../cross-spawn-spawner"
import { makeGlobalNode } from "../effect/app-node" import { makeGlobalNode } from "../effect/node"
import { httpClient } from "../effect/app-node-platform" import { httpClient } from "../effect/layer-node-platform"
import { FSUtil } from "../fs-util" import { FSUtil } from "../fs-util"
import { Global } from "../global" import { Global } from "../global"
import { which } from "../util/which" import { which } from "../util/which"
+58 -73
View File
@@ -25,9 +25,10 @@ import { ProjectTable } from "./project/sql"
import path from "path" import path from "path"
import { fromRow } from "./session/info" import { fromRow } from "./session/info"
import { SessionRunner } from "./session/runner/index" import { SessionRunner } from "./session/runner/index"
import { SessionRuntimeCoordinator } from "./session/runtime-coordinator"
import { SessionStore } from "./session/store" import { SessionStore } from "./session/store"
import { makeGlobalNode } from "./effect/app-node" import { SessionExecution } from "./session/execution"
import { makeGlobalNode } from "./effect/node"
import { LocationServiceMap } from "./location-service-map"
import { MessageDecodeError } from "./session/error" import { MessageDecodeError } from "./session/error"
import { SessionEvent } from "./session/event" import { SessionEvent } from "./session/event"
import { SessionInput } from "./session/input" import { SessionInput } from "./session/input"
@@ -75,14 +76,12 @@ const ListAllInput = Schema.Struct(ListInputBase)
export const ListInput = Schema.Union([ListDirectoryInput, ListProjectInput, ListAllInput]) export const ListInput = Schema.Union([ListDirectoryInput, ListProjectInput, ListAllInput])
export type ListInput = typeof ListInput.Type export type ListInput = typeof ListInput.Type
type CreateBaseInput = { type CreateInput = {
id?: SessionSchema.ID id?: SessionSchema.ID
title?: string
agent?: AgentV2.ID agent?: AgentV2.ID
model?: ModelV2.Ref model?: ModelV2.Ref
location: Location.Ref
} }
type CreateInput = CreateBaseInput &
({ location: Location.Ref; parentID?: never } | { parentID: SessionSchema.ID; location?: never })
type CompactInput = { type CompactInput = {
sessionID: SessionSchema.ID sessionID: SessionSchema.ID
@@ -96,7 +95,7 @@ export class NotFoundError extends Schema.TaggedErrorClass<NotFoundError>()("Ses
export class OperationUnavailableError extends Schema.TaggedErrorClass<OperationUnavailableError>()( export class OperationUnavailableError extends Schema.TaggedErrorClass<OperationUnavailableError>()(
"Session.OperationUnavailableError", "Session.OperationUnavailableError",
{ {
operation: Schema.Literals(["move", "shell", "skill", "switchAgent", "compact"]), operation: Schema.Literals(["move", "shell", "skill", "switchAgent", "compact", "wait"]),
}, },
) {} ) {}
@@ -106,9 +105,6 @@ export class PromptConflictError extends Schema.TaggedErrorClass<PromptConflictE
sessionID: SessionSchema.ID, sessionID: SessionSchema.ID,
messageID: SessionMessage.ID, messageID: SessionMessage.ID,
}) {} }) {}
export class BusyError extends Schema.TaggedErrorClass<BusyError>()("Session.BusyError", {
sessionID: SessionSchema.ID,
}) {}
export const MessageNotFoundError = SessionRevert.MessageNotFoundError export const MessageNotFoundError = SessionRevert.MessageNotFoundError
export type MessageNotFoundError = SessionRevert.MessageNotFoundError export type MessageNotFoundError = SessionRevert.MessageNotFoundError
@@ -116,7 +112,7 @@ export type Error = NotFoundError | MessageDecodeError | OperationUnavailableErr
export interface Interface { export interface Interface {
readonly list: (input?: ListInput) => Effect.Effect<SessionSchema.Info[]> readonly list: (input?: ListInput) => Effect.Effect<SessionSchema.Info[]>
readonly create: (input: CreateInput) => Effect.Effect<SessionSchema.Info, NotFoundError> readonly create: (input: CreateInput) => Effect.Effect<SessionSchema.Info>
readonly get: (sessionID: SessionSchema.ID) => Effect.Effect<SessionSchema.Info, NotFoundError> readonly get: (sessionID: SessionSchema.ID) => Effect.Effect<SessionSchema.Info, NotFoundError>
readonly messages: (input: { readonly messages: (input: {
sessionID: SessionSchema.ID sessionID: SessionSchema.ID
@@ -148,7 +144,6 @@ export interface Interface {
sessionID: SessionSchema.ID sessionID: SessionSchema.ID
model: ModelV2.Ref model: ModelV2.Ref
}) => Effect.Effect<void, NotFoundError> }) => Effect.Effect<void, NotFoundError>
readonly rename: (input: { sessionID: SessionSchema.ID; title: string }) => Effect.Effect<void, NotFoundError>
readonly prompt: (input: { readonly prompt: (input: {
id?: SessionMessage.ID id?: SessionMessage.ID
sessionID: SessionSchema.ID sessionID: SessionSchema.ID
@@ -169,7 +164,7 @@ export interface Interface {
resume?: boolean resume?: boolean
}) => Effect.Effect<void, OperationUnavailableError> }) => Effect.Effect<void, OperationUnavailableError>
readonly compact: (input: CompactInput) => Effect.Effect<void, NotFoundError | OperationUnavailableError> readonly compact: (input: CompactInput) => Effect.Effect<void, NotFoundError | OperationUnavailableError>
readonly wait: (id: SessionSchema.ID) => Effect.Effect<void, NotFoundError> readonly wait: (id: SessionSchema.ID) => Effect.Effect<void, NotFoundError | OperationUnavailableError>
readonly active: Effect.Effect<ReadonlySet<SessionSchema.ID>> readonly active: Effect.Effect<ReadonlySet<SessionSchema.ID>>
readonly resume: (sessionID: SessionSchema.ID) => Effect.Effect<void, NotFoundError | SessionRunner.RunError> readonly resume: (sessionID: SessionSchema.ID) => Effect.Effect<void, NotFoundError | SessionRunner.RunError>
readonly interrupt: (sessionID: SessionSchema.ID) => Effect.Effect<void> readonly interrupt: (sessionID: SessionSchema.ID) => Effect.Effect<void>
@@ -178,9 +173,9 @@ export interface Interface {
sessionID: SessionSchema.ID sessionID: SessionSchema.ID
messageID: SessionMessage.ID messageID: SessionMessage.ID
files?: boolean files?: boolean
}) => Effect.Effect<Revert.State, NotFoundError | MessageNotFoundError | BusyError | Snapshot.Error> }) => Effect.Effect<Revert.State, NotFoundError | MessageNotFoundError | Snapshot.Error>
readonly clear: (sessionID: SessionSchema.ID) => Effect.Effect<void, NotFoundError | BusyError | Snapshot.Error> readonly clear: (sessionID: SessionSchema.ID) => Effect.Effect<void, NotFoundError | Snapshot.Error>
readonly commit: (sessionID: SessionSchema.ID) => Effect.Effect<void, NotFoundError | BusyError> readonly commit: (sessionID: SessionSchema.ID) => Effect.Effect<void, NotFoundError>
} }
} }
@@ -193,8 +188,9 @@ export const layer = Layer.effect(
const db = database.db const db = database.db
const events = yield* EventV2.Service const events = yield* EventV2.Service
const projects = yield* ProjectV2.Service const projects = yield* ProjectV2.Service
const runtimeCoordinator = yield* SessionRuntimeCoordinator.Service const execution = yield* SessionExecution.Service
const store = yield* SessionStore.Service const store = yield* SessionStore.Service
const locations = yield* LocationServiceMap.Service
const decodeMessage = Schema.decodeUnknownEffect(SessionMessage.Message) const decodeMessage = Schema.decodeUnknownEffect(SessionMessage.Message)
const isDurableSessionEvent = Schema.is(SessionEvent.Durable) const isDurableSessionEvent = Schema.is(SessionEvent.Durable)
const decode = (row: typeof SessionMessageTable.$inferSelect) => const decode = (row: typeof SessionMessageTable.$inferSelect) =>
@@ -213,12 +209,7 @@ export const layer = Layer.effect(
const sessionID = input.id ?? SessionSchema.ID.create() const sessionID = input.id ?? SessionSchema.ID.create()
const recorded = yield* store.get(sessionID) const recorded = yield* store.get(sessionID)
if (recorded) return recorded if (recorded) return recorded
const parent = input.parentID ? yield* store.get(input.parentID) : undefined const project = yield* projects.resolve(input.location.directory)
if (input.parentID && parent === undefined) return yield* new NotFoundError({ sessionID: input.parentID })
const location = parent?.location ?? input.location
if (location === undefined)
return yield* Effect.die(new Error("V2Session.create requires either location or an existing parentID"))
const project = yield* projects.resolve(location.directory)
yield* db yield* db
.insert(ProjectTable) .insert(ProjectTable)
.values({ id: project.id, worktree: project.directory, vcs: project.vcs?.type, sandboxes: [] }) .values({ id: project.id, worktree: project.directory, vcs: project.vcs?.type, sandboxes: [] })
@@ -231,11 +222,10 @@ export const layer = Layer.effect(
slug: Slug.create(), slug: Slug.create(),
version: InstallationVersion, version: InstallationVersion,
projectID: project.id, projectID: project.id,
parentID: input.parentID, directory: input.location.directory,
directory: location.directory, path: path.relative(project.directory, input.location.directory).replaceAll("\\", "/"),
path: path.relative(project.directory, location.directory).replaceAll("\\", "/"), workspaceID: input.location.workspaceID ? WorkspaceV2.ID.make(input.location.workspaceID) : undefined,
workspaceID: location.workspaceID ? WorkspaceV2.ID.make(location.workspaceID) : undefined, title: `New session - ${new Date(now).toISOString()}`,
title: input.title ?? `New session - ${new Date(now).toISOString()}`,
agent: input.agent, agent: input.agent,
model: input.model model: input.model
? { ? {
@@ -248,22 +238,24 @@ export const layer = Layer.effect(
tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } }, tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
time: { created: now, updated: now }, time: { created: now, updated: now },
}) })
const projected = yield* events.publish(SessionV1.Event.Created, { sessionID, info }, { location }).pipe( const projected = yield* events
Effect.as({ type: "created" } as const), .publish(SessionV1.Event.Created, { sessionID, info }, { location: input.location })
Effect.catchDefect((defect) => { .pipe(
if (!(defect instanceof SessionProjector.SessionAlreadyProjected)) { Effect.as({ type: "created" } as const),
return Effect.die(defect) Effect.catchDefect((defect) => {
} if (!(defect instanceof SessionProjector.SessionAlreadyProjected)) {
// Concurrent creation lost the projection race. The existing Session identity wins. return Effect.die(defect)
return store }
.get(sessionID) // Concurrent creation lost the projection race. The existing Session identity wins.
.pipe( return store
Effect.flatMap((session) => .get(sessionID)
session ? Effect.succeed({ type: "existing", session } as const) : Effect.die(defect), .pipe(
), Effect.flatMap((session) =>
) session ? Effect.succeed({ type: "existing", session } as const) : Effect.die(defect),
}), ),
) )
}),
)
if (projected.type === "existing") return projected.session if (projected.type === "existing") return projected.session
// TODO: Restore recorded sessions onto replacement synchronized workspaces in a future API slice. // TODO: Restore recorded sessions onto replacement synchronized workspaces in a future API slice.
return yield* result.get(sessionID).pipe(Effect.orDie) return yield* result.get(sessionID).pipe(Effect.orDie)
@@ -387,8 +379,7 @@ export const layer = Layer.effect(
) )
if (!SessionInput.equivalent(admitted, expected)) if (!SessionInput.equivalent(admitted, expected))
return yield* new PromptConflictError({ sessionID: input.sessionID, messageID }) return yield* new PromptConflictError({ sessionID: input.sessionID, messageID })
if (input.resume !== false) if (input.resume !== false) yield* execution.wake(admitted.sessionID)
return yield* Effect.die("SessionV2.prompt with resume moved to SessionRuntime.Service")
return admitted return admitted
}), }),
), ),
@@ -409,13 +400,7 @@ export const layer = Layer.effect(
}) })
}), }),
switchModel: Effect.fn("V2Session.switchModel")(function* (input) { switchModel: Effect.fn("V2Session.switchModel")(function* (input) {
const session = yield* result.get(input.sessionID) yield* result.get(input.sessionID)
if (
session.model?.providerID === input.model.providerID &&
session.model.id === input.model.id &&
(session.model.variant ?? "default") === (input.model.variant ?? "default")
)
return
yield* events.publish(SessionEvent.ModelSwitched, { yield* events.publish(SessionEvent.ModelSwitched, {
sessionID: input.sessionID, sessionID: input.sessionID,
messageID: SessionMessage.ID.create(), messageID: SessionMessage.ID.create(),
@@ -423,40 +408,41 @@ export const layer = Layer.effect(
model: input.model, model: input.model,
}) })
}), }),
rename: Effect.fn("V2Session.rename")(function* (input) {
yield* result.get(input.sessionID)
yield* events.publish(SessionEvent.Renamed, {
sessionID: input.sessionID,
timestamp: yield* DateTime.now,
title: input.title,
})
}),
compact: Effect.fn("V2Session.compact")(function* (input) { compact: Effect.fn("V2Session.compact")(function* (input) {
yield* result.get(input.sessionID) yield* result.get(input.sessionID)
return yield* new OperationUnavailableError({ operation: "compact" }) return yield* new OperationUnavailableError({ operation: "compact" })
}), }),
wait: Effect.fn("V2Session.wait")(function* (sessionID) { wait: Effect.fn("V2Session.wait")(function* (sessionID) {
yield* result.get(sessionID) yield* result.get(sessionID)
return yield* Effect.die("SessionV2.wait moved to SessionRuntime.Service") return yield* new OperationUnavailableError({ operation: "wait" })
}), }),
active: runtimeCoordinator.active, active: execution.active,
resume: Effect.fn("V2Session.resume")(function* (sessionID) { resume: Effect.fn("V2Session.resume")(function* (sessionID) {
yield* result.get(sessionID) yield* result.get(sessionID)
return yield* Effect.die("SessionV2.resume moved to SessionRuntime.Service") yield* execution.resume(sessionID)
}), }),
interrupt: Effect.fn("V2Session.interrupt")(() => Effect.die("SessionV2.interrupt moved to SessionRuntime.Service")), interrupt: Effect.fn("V2Session.interrupt")((sessionID) =>
Effect.uninterruptible(execution.interrupt(sessionID)),
),
revert: { revert: {
stage: Effect.fn("V2Session.revert.stage")(function* (input) { stage: Effect.fn("V2Session.revert.stage")(function* (input) {
yield* result.get(input.sessionID) const session = yield* result.get(input.sessionID)
return yield* Effect.die("SessionV2.revert.stage moved to SessionRuntime.Service") return yield* SessionRevert.stage({ session, messageID: input.messageID, files: input.files }).pipe(
Effect.provideService(Database.Service, database),
Effect.provideService(EventV2.Service, events),
Effect.provide(locations.get(session.location)),
)
}), }),
clear: Effect.fn("V2Session.revert.clear")(function* (sessionID) { clear: Effect.fn("V2Session.revert.clear")(function* (sessionID) {
yield* result.get(sessionID) const session = yield* result.get(sessionID)
return yield* Effect.die("SessionV2.revert.clear moved to SessionRuntime.Service") yield* SessionRevert.clear(session).pipe(
Effect.provideService(EventV2.Service, events),
Effect.provide(locations.get(session.location)),
)
}), }),
commit: Effect.fn("V2Session.revert.commit")(function* (sessionID) { commit: Effect.fn("V2Session.revert.commit")(function* (sessionID) {
const session = yield* result.get(sessionID) const session = yield* result.get(sessionID)
return yield* SessionRevert.commit(session).pipe(Effect.provideService(EventV2.Service, events)) yield* SessionRevert.commit(session).pipe(Effect.provideService(EventV2.Service, events))
}), }),
}, },
}) })
@@ -471,7 +457,6 @@ export const defaultLayer = layer.pipe(
Layer.provide(EventV2.defaultLayer), Layer.provide(EventV2.defaultLayer),
Layer.provide(Database.defaultLayer), Layer.provide(Database.defaultLayer),
Layer.provide(ProjectV2.defaultLayer), Layer.provide(ProjectV2.defaultLayer),
Layer.provide(SessionRuntimeCoordinator.defaultLayer),
Layer.orDie, Layer.orDie,
) )
@@ -479,7 +464,6 @@ const resolvePrompt = (input: PromptInput.Prompt) =>
Prompt.make({ Prompt.make({
text: input.text, text: input.text,
agents: input.agents, agents: input.agents,
system: input.system,
files: input.files?.map((file) => { files: input.files?.map((file) => {
const dataMime = file.uri.match(/^data:([^;,]+)[;,]/i)?.[1] const dataMime = file.uri.match(/^data:([^;,]+)[;,]/i)?.[1]
const target = URL.canParse(file.uri) ? new URL(file.uri).pathname : (file.name ?? file.uri) const target = URL.canParse(file.uri) ? new URL(file.uri).pathname : (file.name ?? file.uri)
@@ -497,8 +481,9 @@ export const node = makeGlobalNode({
Database.node, Database.node,
EventV2.node, EventV2.node,
ProjectV2.node, ProjectV2.node,
SessionRuntimeCoordinator.node, SessionExecution.node,
SessionStore.node, SessionStore.node,
LocationServiceMap.node,
SessionProjector.node, SessionProjector.node,
], ],
}) })
+1 -4
View File
@@ -2,7 +2,7 @@ export * as SessionExecution from "./execution"
import { Context, Effect, Layer } from "effect" import { Context, Effect, Layer } from "effect"
import { LayerNode } from "../effect/layer-node" import { LayerNode } from "../effect/layer-node"
import { Node } from "../effect/app-node" import { Node } from "../effect/node"
import { SessionRunner } from "./runner/index" import { SessionRunner } from "./runner/index"
import { SessionSchema } from "./schema" import { SessionSchema } from "./schema"
@@ -15,8 +15,6 @@ export interface Interface {
readonly wake: (sessionID: SessionSchema.ID) => Effect.Effect<void> readonly wake: (sessionID: SessionSchema.ID) => Effect.Effect<void>
/** Interrupt active work owned by this process. Idle interruption is a no-op. */ /** Interrupt active work owned by this process. Idle interruption is a no-op. */
readonly interrupt: (sessionID: SessionSchema.ID) => Effect.Effect<void> readonly interrupt: (sessionID: SessionSchema.ID) => Effect.Effect<void>
/** Resolves once this process owns no active execution for the Session. Returns immediately when idle and never starts work. */
readonly awaitIdle: (sessionID: SessionSchema.ID) => Effect.Effect<void>
} }
/** Routes execution from a Session ID to the runner owned by that Session's Location. */ /** Routes execution from a Session ID to the runner owned by that Session's Location. */
@@ -32,6 +30,5 @@ export const noopLayer = Layer.succeed(
resume: () => Effect.void, resume: () => Effect.void,
wake: () => Effect.void, wake: () => Effect.void,
interrupt: () => Effect.void, interrupt: () => Effect.void,
awaitIdle: () => Effect.void,
}), }),
) )
+1 -2
View File
@@ -1,6 +1,6 @@
import { Cause, Effect, Layer } from "effect" import { Cause, Effect, Layer } from "effect"
import { LocationServiceMap } from "../../location-service-map" import { LocationServiceMap } from "../../location-service-map"
import { makeGlobalNode } from "../../effect/app-node" import { makeGlobalNode } from "../../effect/node"
import { SessionRunCoordinator } from "../run-coordinator" import { SessionRunCoordinator } from "../run-coordinator"
import { SessionRunner } from "../runner" import { SessionRunner } from "../runner"
import { SessionSchema } from "../schema" import { SessionSchema } from "../schema"
@@ -33,7 +33,6 @@ export const layer = Layer.effect(
interrupt: coordinator.interrupt, interrupt: coordinator.interrupt,
resume: coordinator.run, resume: coordinator.run,
wake: coordinator.wake, wake: coordinator.wake,
awaitIdle: coordinator.awaitIdle,
}) })
}), }),
) )

Some files were not shown because too many files have changed in this diff Show More