Compare commits

..

1 Commits

Author SHA1 Message Date
Aiden Cline f07cace1b4 chore(ui): remove unused dependencies
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
2026-06-27 03:44:49 +00:00
171 changed files with 1006 additions and 5683 deletions
+2 -23
View File
@@ -317,7 +317,6 @@
"ai-gateway-provider": "3.1.2",
"bun-pty": "0.4.8",
"cross-spawn": "catalog:",
"diff": "catalog:",
"drizzle-orm": "catalog:",
"effect": "catalog:",
"fuzzysort": "3.1.0",
@@ -958,41 +957,26 @@
"dependencies": {
"@kobalte/core": "catalog:",
"@pierre/diffs": "catalog:",
"@shikijs/stream": "catalog:",
"@shikijs/transformers": "3.9.2",
"@solid-primitives/bounds": "0.1.3",
"@solid-primitives/event-listener": "2.4.5",
"@solid-primitives/media": "2.3.3",
"@solid-primitives/resize-observer": "2.1.3",
"diff": "catalog:",
"dompurify": "3.3.1",
"fuzzysort": "catalog:",
"katex": "0.16.27",
"luxon": "catalog:",
"marked": "catalog:",
"marked-katex-extension": "5.1.6",
"marked-shiki": "catalog:",
"morphdom": "2.7.8",
"motion": "12.34.5",
"motion-dom": "12.34.3",
"motion-utils": "12.29.2",
"remeda": "catalog:",
"remend": "catalog:",
"shiki": "catalog:",
"solid-list": "catalog:",
"strip-ansi": "7.1.2",
},
"devDependencies": {
"@solidjs/meta": "catalog:",
"@tailwindcss/vite": "catalog:",
"@tsconfig/node22": "catalog:",
"@types/bun": "catalog:",
"@types/katex": "0.16.7",
"@types/luxon": "catalog:",
"@typescript/native-preview": "catalog:",
"solid-js": "catalog:",
"tailwindcss": "catalog:",
"tw-animate-css": "1.4.0",
"typescript": "catalog:",
"vite": "catalog:",
"vite-plugin-icons-spritesheet": "3.0.1",
@@ -1051,7 +1035,6 @@
"@modelcontextprotocol/sdk@1.29.0": "patches/@modelcontextprotocol%2Fsdk@1.29.0.patch",
"gcp-metadata@8.1.2": "patches/gcp-metadata@8.1.2.patch",
"@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch",
"effect@4.0.0-beta.83": "patches/effect@4.0.0-beta.83.patch",
"@npmcli/agent@4.0.2": "patches/@npmcli%2Fagent@4.0.2.patch",
"@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",
@@ -4449,9 +4432,9 @@
"motion": ["motion@12.34.5", "", { "dependencies": { "framer-motion": "^12.34.5", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-N06NLJ9IeBHeielRqIvYvjPfXuRdyTxa+9++BgpGa+hY2D7TcMkI6QzV3jaRuv0aZRXgMa7cPy9YcBUBisPzAQ=="],
"motion-dom": ["motion-dom@12.34.3", "", { "dependencies": { "motion-utils": "^12.29.2" } }, "sha512-sYgFe+pR9aIM7o4fhs2aXtOI+oqlUd33N9Yoxcgo1Fv7M20sRkHtCmzE/VRNIcq7uNJ+qio+Xubt1FXH3pQ+eQ=="],
"motion-dom": ["motion-dom@12.40.0", "", { "dependencies": { "motion-utils": "^12.39.0" } }, "sha512-HxU3ZaBwNPVQUBQf1xxgq+7JrPNZvjLVxgbpEZL7RrWJnsxOf0/OM+yrHG9ogLQ31Do/r57Oz2gQWPK+6q62mg=="],
"motion-utils": ["motion-utils@12.29.2", "", {}, "sha512-G3kc34H2cX2gI63RqU+cZq+zWRRPSsNIOjpdl9TN4AQwC4sgwYPl/Q/Obf/d53nOm569T0fYK+tcoSV50BWx8A=="],
"motion-utils": ["motion-utils@12.39.0", "", {}, "sha512-8nadJAJjTtqRkmRF36FoJTrywK9nnFmnPwnSMyxaOCU7GDjN9RTMJIxx9De8ErM+vpPhMccr/6fo5WciyQLnMQ=="],
"mrmime": ["mrmime@2.0.1", "", {}, "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ=="],
@@ -7035,10 +7018,6 @@
"lazystream/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="],
"motion/framer-motion/motion-dom": ["motion-dom@12.40.0", "", { "dependencies": { "motion-utils": "^12.39.0" } }, "sha512-HxU3ZaBwNPVQUBQf1xxgq+7JrPNZvjLVxgbpEZL7RrWJnsxOf0/OM+yrHG9ogLQ31Do/r57Oz2gQWPK+6q62mg=="],
"motion/framer-motion/motion-utils": ["motion-utils@12.39.0", "", {}, "sha512-8nadJAJjTtqRkmRF36FoJTrywK9nnFmnPwnSMyxaOCU7GDjN9RTMJIxx9De8ErM+vpPhMccr/6fo5WciyQLnMQ=="],
"opencode-gitlab-auth/open/wsl-utils": ["wsl-utils@0.1.0", "", { "dependencies": { "is-wsl": "^3.1.0" } }, "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw=="],
"p-locate/p-limit/yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
+4 -4
View File
@@ -1,8 +1,8 @@
{
"nodeModules": {
"x86_64-linux": "sha256-JXQ9PAqqRlJtHa8T3ZxqdRRyxC+0ip+2wSnehvKXUbI=",
"aarch64-linux": "sha256-nI+RaxDXmAcjhSjCtIvyi292xBEg0E5NXaoyGrJE69s=",
"aarch64-darwin": "sha256-UleKpm7Khf3kibm4BqZJ9bmu0N2kOjNd77g1Bd2tmfw=",
"x86_64-darwin": "sha256-V4AQH868dOfVhEj1mKTnZlhpZwFqBYsHS28Tx9VXHkE="
"x86_64-linux": "sha256-rB/CcrBUQVZ08nBFSYA8u2w88rQmTpKxKPkIreDEKgI=",
"aarch64-linux": "sha256-ZRTphtic8Ip96MnILteFgZAUxjK9O4YfJu2O6u/0H8k=",
"aarch64-darwin": "sha256-VK5XIzraP0HtqnPwPCejiDKer4ewtNtX1vxP5uuyjSk=",
"x86_64-darwin": "sha256-ZLPHqcCZB1EmxQk95cmUpiODTTKOyi7PSF0yr/rDk6Y="
}
}
+1 -2
View File
@@ -153,7 +153,6 @@
"@tanstack/solid-virtual@3.13.28": "patches/@tanstack%2Fsolid-virtual@3.13.28.patch",
"@pierre/trees@1.0.0-beta.4": "patches/@pierre%2Ftrees@1.0.0-beta.4.patch",
"@modelcontextprotocol/sdk@1.29.0": "patches/@modelcontextprotocol%2Fsdk@1.29.0.patch",
"@tanstack/virtual-core@3.17.0": "patches/@tanstack%2Fvirtual-core@3.17.0.patch",
"effect@4.0.0-beta.83": "patches/effect@4.0.0-beta.83.patch"
"@tanstack/virtual-core@3.17.0": "patches/@tanstack%2Fvirtual-core@3.17.0.patch"
}
}
+22 -77
View File
@@ -3,7 +3,6 @@ import { batch, createEffect, onCleanup, onMount } from "solid-js"
import { createStore } from "solid-js/store"
import { makeEventListener } from "@solid-primitives/event-listener"
import { Tooltip } from "@opencode-ai/ui/tooltip"
import { TooltipV2 } from "@opencode-ai/ui/v2/tooltip-v2"
import { useLanguage } from "@/context/language"
type Mem = Performance & {
@@ -52,62 +51,31 @@ const bad = (n: number | undefined, limit: number, low = false) => {
const session = (path: string) => path.includes("/session")
function Cell(props: {
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>
)
}
function Cell(props: { bad?: boolean; dim?: boolean; label: string; tip: string; value: string; wide?: boolean }) {
return (
<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>
)
}
export function DebugBar(props: { inline?: boolean } = {}) {
export function DebugBar() {
const language = useLanguage()
const location = useLocation()
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 heapv = () => {
const value = heap()
@@ -395,30 +363,15 @@ export function DebugBar(props: { inline?: boolean } = {}) {
return (
<aside
aria-label={language.t("debugBar.ariaLabel")}
classList={{
"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,
}}
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]"
>
<div
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,
}}
>
<div class="grid grid-cols-5 gap-px font-mono">
<Cell
label={language.t("debugBar.nav.label")}
tip={language.t("debugBar.nav.tip")}
value={navv()}
bad={bad(state.nav.dur, 400)}
dim={state.nav.dur === undefined && !state.nav.pending}
inline={props.inline}
/>
<Cell
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)}`}
bad={bad(state.fps, 50, true)}
dim={state.fps === undefined}
inline={props.inline}
/>
<Cell
label={language.t("debugBar.frame.label")}
@@ -434,7 +386,6 @@ export function DebugBar(props: { inline?: boolean } = {}) {
value={time(state.gap) ?? na()}
bad={bad(state.gap, 50)}
dim={state.gap === undefined}
inline={props.inline}
/>
<Cell
label={language.t("debugBar.jank.label")}
@@ -442,7 +393,6 @@ export function DebugBar(props: { inline?: boolean } = {}) {
value={state.jank === undefined ? na() : `${state.jank}`}
bad={bad(state.jank, 8)}
dim={state.jank === undefined}
inline={props.inline}
/>
<Cell
label={language.t("debugBar.long.label")}
@@ -450,7 +400,6 @@ export function DebugBar(props: { inline?: boolean } = {}) {
value={longv()}
bad={bad(state.long.block, 200)}
dim={state.long.count === undefined}
inline={props.inline}
/>
<Cell
label={language.t("debugBar.delay.label")}
@@ -458,7 +407,6 @@ export function DebugBar(props: { inline?: boolean } = {}) {
value={time(state.delay) ?? na()}
bad={bad(state.delay, 100)}
dim={state.delay === undefined}
inline={props.inline}
/>
<Cell
label={language.t("debugBar.inp.label")}
@@ -466,7 +414,6 @@ export function DebugBar(props: { inline?: boolean } = {}) {
value={time(state.inp) ?? na()}
bad={bad(state.inp, 200)}
dim={state.inp === undefined}
inline={props.inline}
/>
<Cell
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)}
bad={bad(state.cls, 0.1)}
dim={state.cls === undefined}
inline={props.inline}
/>
<Cell
label={language.t("debugBar.mem.label")}
@@ -489,7 +435,6 @@ export function DebugBar(props: { inline?: boolean } = {}) {
value={heapv()}
bad={bad(heap(), 0.8)}
dim={state.heap.used === undefined}
inline={props.inline}
wide
/>
</div>
@@ -65,7 +65,6 @@ export function DialogConnectProvider(props: { provider: string; directory?: Acc
const [store, setStore] = createStore({
methodIndex: undefined as undefined | number,
authorization: undefined as undefined | ProviderAuthAuthorization,
promptInputs: undefined as undefined | Record<string, string>,
state: "pending" as undefined | "pending" | "complete" | "error" | "prompt",
error: undefined as string | undefined,
})
@@ -74,7 +73,6 @@ export function DialogConnectProvider(props: { provider: string; directory?: Acc
| { type: "method.select"; index: number }
| { type: "method.reset" }
| { type: "auth.prompt" }
| { type: "auth.inputs"; inputs: Record<string, string> }
| { type: "auth.pending" }
| { type: "auth.complete"; authorization: ProviderAuthAuthorization }
| { type: "auth.error"; error: string }
@@ -85,7 +83,6 @@ export function DialogConnectProvider(props: { provider: string; directory?: Acc
if (action.type === "method.select") {
draft.methodIndex = action.index
draft.authorization = undefined
draft.promptInputs = undefined
draft.state = undefined
draft.error = undefined
return
@@ -93,7 +90,6 @@ export function DialogConnectProvider(props: { provider: string; directory?: Acc
if (action.type === "method.reset") {
draft.methodIndex = undefined
draft.authorization = undefined
draft.promptInputs = undefined
draft.state = undefined
draft.error = undefined
return
@@ -103,12 +99,6 @@ export function DialogConnectProvider(props: { provider: string; directory?: Acc
draft.error = undefined
return
}
if (action.type === "auth.inputs") {
draft.promptInputs = action.inputs
draft.state = undefined
draft.error = undefined
return
}
if (action.type === "auth.pending") {
draft.state = "pending"
draft.error = undefined
@@ -161,15 +151,6 @@ export function DialogConnectProvider(props: { provider: string; directory?: Acc
const method = methods()[index]
dispatch({ type: "method.select", index })
if (method.type === "api" && method.prompts?.length) {
if (!inputs) {
dispatch({ type: "auth.prompt" })
return
}
dispatch({ type: "auth.inputs", inputs })
return
}
if (method.type === "oauth") {
if (method.prompts?.length && !inputs) {
dispatch({ type: "auth.prompt" })
@@ -209,7 +190,7 @@ export function DialogConnectProvider(props: { provider: string; directory?: Acc
}
}
function AuthPromptsView() {
function OAuthPromptsView() {
const [formStore, setFormStore] = createStore({
value: {} as Record<string, string>,
index: 0,
@@ -217,7 +198,8 @@ export function DialogConnectProvider(props: { provider: string; directory?: Acc
const prompts = createMemo<NonNullable<ProviderAuthMethod["prompts"]>>(() => {
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>) => {
if (!prompt.when) return true
@@ -248,10 +230,6 @@ export function DialogConnectProvider(props: { provider: string; directory?: Acc
setFormStore("index", next)
return
}
if (method()?.type === "api") {
dispatch({ type: "auth.inputs", inputs: value })
return
}
await selectMethod(store.methodIndex, value)
}
@@ -436,7 +414,6 @@ export function DialogConnectProvider(props: { provider: string; directory?: Acc
auth: {
type: "api",
key: apiKey,
...(store.promptInputs ? { metadata: store.promptInputs } : {}),
},
})
await complete()
@@ -645,7 +622,7 @@ export function DialogConnectProvider(props: { provider: string; directory?: Acc
</div>
</Match>
<Match when={store.state === "prompt"}>
<AuthPromptsView />
<OAuthPromptsView />
</Match>
<Match when={store.state === "error"}>
<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 { ProviderIcon } from "@opencode-ai/ui/provider-icon"
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 { Select } from "@opencode-ai/ui/select"
import { useDialog } from "@opencode-ai/ui/context/dialog"
@@ -1364,7 +1362,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
shouldAnimate: providersShouldFadeIn(),
paid: props.controls.model.paid,
title: language.t("command.model.choose"),
keybind: command.keybindParts("model.choose"),
keybind: command.keybind("model.choose"),
model: props.controls.model.selection,
providerID: props.controls.model.selection.current()?.provider?.id,
modelName: props.controls.model.selection.current()?.name ?? language.t("dialog.model.select.title"),
@@ -1381,7 +1379,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
const showAgentControl = createMemo(() => props.controls.agents.visible && props.controls.agents.options.length > 0)
const agentControlState = createMemo<ComposerAgentControlState>(() => ({
title: language.t("command.agent.cycle"),
keybind: command.keybindParts("agent.cycle"),
keybind: command.keybind("agent.cycle"),
options: props.controls.agents.options,
current: props.controls.agents.current,
style: control(),
@@ -1499,14 +1497,10 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
<div class="flex h-11 items-center px-2">
<div class="flex min-w-0 flex-1 items-center gap-0">
{fileAttachmentInput()}
<TooltipV2
<TooltipKeybind
placement="top"
value={
<>
{language.t("prompt.action.attachFile")}
<KeybindV2 keys={command.keybindParts("file.attach")} variant="neutral" />
</>
}
title={language.t("prompt.action.attachFile")}
keybind={command.keybind("file.attach")}
>
<IconButton
data-action="prompt-attach"
@@ -1520,7 +1514,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
tabIndex={store.mode === "normal" ? undefined : -1}
aria-label={language.t("prompt.action.attachFile")}
/>
</TooltipV2>
</TooltipKeybind>
<Show when={showAgentControl()}>
<ComposerAgentControl state={agentControlState()} />
</Show>
@@ -1535,15 +1529,11 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
!props.controls.model.selection.variant.current() && !store.variantOpen,
}}
>
<TooltipV2
<TooltipKeybind
placement="top"
gutter={4}
value={
<>
{language.t("command.model.variant.cycle")}
<KeybindV2 keys={command.keybindParts("model.variant.cycle")} variant="neutral" />
</>
}
title={language.t("command.model.variant.cycle")}
keybind={command.keybind("model.variant.cycle")}
>
<Select
size="normal"
@@ -1561,11 +1551,11 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
triggerProps={{ "data-action": "prompt-model-variant" }}
variant="ghost"
/>
</TooltipV2>
</TooltipKeybind>
</div>
</Show>
</div>
<TooltipV2 placement="top" inactive={!working() && blank()} value={tip()}>
<Tooltip placement="top" inactive={!working() && blank()} value={tip()}>
<IconButton
data-action="prompt-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")}
/>
</TooltipV2>
</Tooltip>
</div>
</DockShellForm>
</div>
@@ -1926,7 +1916,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
type ComposerAgentControlState = {
title: string
keybind: string[]
keybind: string
options: string[]
current: string
style: JSX.CSSProperties | undefined
@@ -1938,7 +1928,7 @@ type ComposerModelControlState = {
shouldAnimate: boolean
paid: boolean
title: string
keybind: string[]
keybind: string
model: ReturnType<typeof useLocal>["model"]
providerID?: 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">
<Icon name="sliders" size="small" />
</div>
<TooltipV2
placement="top"
gutter={4}
value={
<>
{props.state.title}
<KeybindV2 keys={props.state.keybind} variant="neutral" />
</>
}
>
<TooltipKeybind placement="top" gutter={4} title={props.state.title} keybind={props.state.keybind}>
<Select
size="normal"
options={props.state.options}
@@ -1974,7 +1955,7 @@ function ComposerAgentControl(props: { state: ComposerAgentControlState }) {
triggerProps={{ "data-action": "prompt-agent" }}
variant="ghost"
/>
</TooltipV2>
</TooltipKeybind>
</div>
)
}
@@ -1985,16 +1966,7 @@ function ComposerModelControl(props: { state: ComposerModelControlState }) {
<Show
when={props.state.paid}
fallback={
<TooltipV2
placement="top"
gutter={4}
value={
<>
{props.state.title}
<KeybindV2 keys={props.state.keybind} variant="neutral" />
</>
}
>
<TooltipKeybind placement="top" gutter={4} title={props.state.title} keybind={props.state.keybind}>
<Button
data-action="prompt-model"
as="div"
@@ -2019,19 +1991,10 @@ function ComposerModelControl(props: { state: ComposerModelControlState }) {
<Icon name="chevron-down" size="small" class="text-v2-icon-icon-muted" />
</span>
</Button>
</TooltipV2>
</TooltipKeybind>
}
>
<TooltipV2
placement="top"
gutter={4}
value={
<>
{props.state.title}
<KeybindV2 keys={props.state.keybind} variant="neutral" />
</>
}
>
<TooltipKeybind placement="top" gutter={4} title={props.state.title} keybind={props.state.keybind}>
<ModelSelectorPopover
model={props.state.model}
triggerAs={Button}
@@ -2060,7 +2023,7 @@ function ComposerModelControl(props: { state: ComposerModelControlState }) {
<Icon name="chevron-down" size="small" class="text-v2-icon-icon-muted" />
</span>
</ModelSelectorPopover>
</TooltipV2>
</TooltipKeybind>
</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 creating = createMemo(() => {
const route = layout.route()
if (route.type === "draft" || route.type === "dir-new-sesssion") return true
if (!params.dir) return false
if (params.id) return false
const parts = location.pathname.replace(/\/+$/, "").split("/")
@@ -468,7 +466,7 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
}}
onReorder={(keys) => tabsStoreActions.reorder(keys)}
/>
<Show when={!creating()}>
<Show when={!(creating() && params.dir)}>
<TooltipV2
placement="bottom"
value={
+8 -115
View File
@@ -1,5 +1,5 @@
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"
describe("resumeStreamAfterPageShow", () => {
@@ -15,23 +15,19 @@ describe("resumeStreamAfterPageShow", () => {
})
describe("coalesceServerEvents", () => {
const delta = (value: string, field = "text", partID = "part") => ({
const delta = (value: string, field = "text") => ({
directory: "/repo",
payload: {
type: "message.part.delta",
properties: { messageID: "msg", partID, field, delta: value },
properties: { messageID: "msg", partID: "part", field, delta: value },
} as Event,
})
test("merges adjacent deltas for the same field", () => {
const first = delta("hello ")
const second = delta("world")
first.payload.id = "first"
second.payload.id = "second"
const result = coalesceServerEvents([first, second])
const result = coalesceServerEvents([delta("hello "), delta("world")])
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", () => {
@@ -49,112 +45,9 @@ describe("coalesceServerEvents", () => {
])
})
test("preserves event ID order across interleaved deltas", () => {
const first = delta("a")
const other = delta("b", "text", "other")
const last = delta("c")
first.payload.id = "1"
other.payload.id = "2"
last.payload.id = "3"
test("drops stale deltas", () => {
const result = coalesceServerEvents([delta("stale")], new Set(["/repo:msg:part"]))
const result = coalesceServerEvents([first, other, last])
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)
expect(result).toEqual([])
})
})
+50 -41
View File
@@ -17,56 +17,34 @@ const isAbortError = (error: unknown) =>
const isStreamClosed = (error: unknown, signal?: AbortSignal) => isAbortError(error) || signal?.aborted === true
type QueuedServerEvent = { directory: string; payload: Event }
const coalescedKey = (event: QueuedServerEvent) => {
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
}
const deltaKey = (directory: string, messageID: string, partID: string) => `${directory}:${messageID}:${partID}`
export function enqueueServerEvent(queue: QueuedServerEvent[], event: QueuedServerEvent) {
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[]) {
export function coalesceServerEvents(events: QueuedServerEvent[], stale?: Set<string>) {
const output: QueuedServerEvent[] = []
const deltas = new Map<string, number>()
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") {
deltas.clear()
output.push(event)
return
}
const props = event.payload.properties
const previous = output[output.length - 1]
if (
!previous ||
previous.payload.type !== "message.part.delta" ||
previous.directory !== event.directory ||
previous.payload.properties.messageID !== props.messageID ||
previous.payload.properties.partID !== props.partID ||
previous.payload.properties.field !== props.field
) {
const id = `${deltaKey(event.directory, props.messageID, props.partID)}:${props.field}`
const index = deltas.get(id)
const existing = index === undefined ? undefined : output[index]
if (!existing || existing.payload.type !== "message.part.delta") {
deltas.set(id, output.length)
output.push({
directory: event.directory,
payload: { ...event.payload, properties: { ...props } },
})
return
}
output[output.length - 1] = {
directory: event.directory,
payload: {
...event.payload,
properties: { ...props, delta: previous.payload.properties.delta + props.delta },
},
}
existing.payload.properties.delta += props.delta
})
return output
}
@@ -107,9 +85,20 @@ function createServerSdkContextBase(server: ServerConnection.Any, scope: ServerS
let queue: Queued[] = []
let buffer: Queued[] = []
const coalesced = new Map<string, number>()
const staleDeltas = new Set<string>()
let timer: ReturnType<typeof setTimeout> | undefined
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 = () => {
if (timer) clearTimeout(timer)
timer = undefined
@@ -117,12 +106,15 @@ function createServerSdkContextBase(server: ServerConnection.Any, scope: ServerS
if (queue.length === 0) return
const events = queue
const skip = staleDeltas.size > 0 ? new Set(staleDeltas) : undefined
queue = buffer
buffer = events
queue.length = 0
coalesced.clear()
staleDeltas.clear()
last = Date.now()
const output = coalesceServerEvents(events)
const output = coalesceServerEvents(events, skip)
batch(() => {
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) {
resetHeartbeat()
streamErrorLogged = false
if (event.payload.type !== "sync") {
const directory = event.directory ?? "global"
const payload = event.payload as Event
if (enqueueServerEvent(queue, { directory, payload })) schedule()
const directory = event.directory ?? "global"
if (event.payload.type === "sync") {
continue
}
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
yielded = Date.now()
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"
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 initialMessagePageSize = 2
const historyMessagePageSize = 200
const sessionInfoLimit = 2_048
const emptyIDs: ReadonlySet<string> = new Set()
type OptimisticItem = {
message: Message
parts: Part[]
confirmedParts?: Part[]
confirmedMessage?: boolean
}
type MessagePage = {
session: Message[]
part: { id: string; part: Part[] }[]
cursor?: string
complete: boolean
const hasParts = (parts: Part[] | undefined, want: Part[]) => {
if (!parts) return want.length === 0
return want.every((part) => Binary.search(parts, part.id, (item) => item.id).found)
}
// Most markers describe the current HTTP attempt; deltaParts persists non-durable stream state across retries.
type MessageLoadState = {
touchedMessages: Set<string>
removedMessages: Set<string>
retainedMessages: Set<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[] }[] }
function mergeOptimisticPage(
page: { session: Message[]; part: { id: string; part: Part[] }[]; cursor?: string; complete: boolean },
items: OptimisticItem[],
) {
if (items.length === 0) return { ...page, confirmed: [] as string[] }
const session = [...page.session]
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) {
const result = Binary.search(session, item.message.id, (message) => message.id)
if (!result.found) session.splice(result.index, 0, item.message)
const current = part.get(item.message.id)
const confirmed = result.found
? item.parts.filter((part) => Binary.search(current ?? [], part.id, (value) => value.id).found)
: []
if (result.found) observed.push({ messageID: item.message.id, parts: confirmed })
part.set(
item.message.id,
merge(
result.found ? (current ?? []) : merge(item.confirmedParts ?? [], current ?? []),
item.parts.filter((part) => !confirmed.includes(part)),
),
)
if (result.found && hasParts(current, item.parts)) {
confirmed.push(item.message.id)
continue
}
part.set(item.message.id, merge(current ?? [], item.parts))
}
return {
...page,
session,
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))
}
function reconcileFetched<T extends { id: string }>(
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 }) {
export function createServerSession(client: OpencodeClient) {
const [data, setData] = createStore({
info: {} as Record<string, Session | undefined>,
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 inflightTodo = new Map<string, Promise<void>>()
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 infoSeen = new Set<string>()
const pinned = new Map<string, number>()
const generations = new Map<string, object>()
const generation = (sessionID: string) => {
const current = generations.get(sessionID)
if (current) return current
const created = {}
generations.set(sessionID, created)
return created
}
const generations = new Map<string, number>()
const [meta, setMeta] = createStore({
limit: {} as Record<string, number | undefined>,
cursor: {} as Record<string, string | undefined>,
@@ -191,11 +115,6 @@ export function createServerSession(client: OpencodeClient, options?: { retry?:
const preserve = new Set([
...pinned.keys(),
...requests.keys(),
...inflight.keys(),
...inflightDiff.keys(),
...inflightTodo.keys(),
...messageLoads.keys(),
...optimistic.keys(),
...Object.entries(data.permission)
.filter(([, items]) => items.length > 0)
.map(([sessionID]) => sessionID),
@@ -219,7 +138,6 @@ export function createServerSession(client: OpencodeClient, options?: { retry?:
if (!preserve.has(sessionID)) stale.push(sessionID)
}
stale.forEach((sessionID) => infoSeen.delete(sessionID))
stale.forEach((sessionID) => generations.delete(sessionID))
setData(
"info",
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)
const pending = requests.get(sessionID)
if (pending) return pending
const active = generation(sessionID)
const generation = generations.get(sessionID) ?? 0
const request = client.session.get({ sessionID }).then((result) => {
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)
})
requests.set(sessionID, request)
const cleanup = () => {
if (requests.get(sessionID) === request) requests.delete(sessionID)
if (
generations.get(sessionID) === active &&
!data.info[sessionID] &&
!requests.has(sessionID) &&
!messageLoads.has(sessionID) &&
!inflight.has(sessionID) &&
!inflightDiff.has(sessionID) &&
!inflightTodo.has(sessionID)
)
generations.delete(sessionID)
}
void request.then(cleanup, cleanup)
void request.then(
() => {
if (requests.get(sessionID) === request) requests.delete(sessionID)
},
() => {
if (requests.get(sessionID) === request) requests.delete(sessionID)
},
)
return request
}
@@ -283,121 +195,15 @@ export function createServerSession(client: OpencodeClient, options?: { retry?:
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[]) => {
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) => {
generations.delete(sessionID)
generations.set(sessionID, (generations.get(sessionID) ?? 0) + 1)
clearOptimistic(sessionID)
requests.delete(sessionID)
inflight.delete(sessionID)
inflightDiff.delete(sessionID)
inflightTodo.delete(sessionID)
messageLoads.delete(sessionID)
pendingParts.delete(sessionID)
orphanParts.delete(sessionID)
removedMessages.delete(sessionID)
})
setData(
produce((draft) => {
@@ -424,7 +230,6 @@ export function createServerSession(client: OpencodeClient, options?: { retry?:
...inflight.keys(),
...inflightDiff.keys(),
...inflightTodo.keys(),
...messageLoads.keys(),
...optimistic.keys(),
...Object.entries(data.permission)
.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() }),
)
const fetchMessages = async (sessionID: string, limit: number, before?: string, onAttempt?: () => void) => {
const response = await (options?.retry ?? retry)(() => {
onAttempt?.()
return client.session.messages({ sessionID, limit, before })
})
const fetchMessages = async (sessionID: string, limit: number, before?: string) => {
const response = await retry(() => client.session.messages({ sessionID, limit, before }))
const items = (response.data ?? []).filter((item) => !!item?.info?.id)
return {
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") => {
if (meta.loading[sessionID]) return
const active = generation(sessionID)
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)
const generation = generations.get(sessionID) ?? 0
setMeta("loading", sessionID, true)
let applied = false
await fetchMessages(sessionID, limit, before, () => resetMessageLoad(sessionID, load))
await fetchMessages(sessionID, limit, before)
.then((page) => {
if (generations.get(sessionID) !== active) return
const first = page.session.reduce<Message | undefined>(
(oldest, message) => (!oldest || cmpMessage(message, oldest) < 0 ? message : oldest),
undefined,
)
const preserveUnfetched =
mode === "prepend" || (!page.complete && (!first || ((message: Message) => cmpMessage(message, first) < 0)))
applyMessagePage(
sessionID,
page,
messageLoads.get(sessionID) === load ? load : undefined,
preserveUnfetched,
mode !== "prepend",
)
applied = true
if ((generations.get(sessionID) ?? 0) !== generation) return
const next = mergeOptimisticPage(page, [...(optimistic.get(sessionID)?.values() ?? [])])
next.confirmed.forEach((messageID) => clearOptimistic(sessionID, messageID))
const messages = mode === "prepend" ? merge(data.message[sessionID] ?? [], next.session) : next.session
batch(() => {
setData("message", sessionID, reconcile(messages, { key: "id" }))
for (const item of next.part) {
const parts = item.part.filter((part) => !SKIP_PARTS.has(part.type))
if (parts.length) setData("part", item.id, reconcile(parts, { key: "id" }))
}
setMeta("limit", sessionID, messages.length)
setMeta("cursor", sessionID, next.cursor)
setMeta("complete", sessionID, next.complete)
setMeta("at", sessionID, Date.now())
})
})
.finally(() => {
if (!applied && generations.get(sessionID) === active && messageLoads.get(sessionID) === load) {
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)
if ((generations.get(sessionID) ?? 0) === generation) setMeta("loading", sessionID, false)
})
}
@@ -660,13 +339,7 @@ export function createServerSession(client: OpencodeClient, options?: { retry?:
const eventID = eventSessionID(event)
if (eventID) {
touch(eventID)
if (
!data.info[eventID] &&
event.type !== "session.created" &&
event.type !== "session.updated" &&
event.type !== "session.deleted"
)
void resolve(eventID).catch(() => {})
if (!data.info[eventID]) void resolve(eventID).catch(() => {})
}
switch (event.type) {
case "session.created":
@@ -705,21 +378,6 @@ export function createServerSession(client: OpencodeClient, options?: { retry?:
}
case "message.updated": {
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]
if (!messages) {
setData("message", info.sessionID, [info])
@@ -737,20 +395,6 @@ export function createServerSession(client: OpencodeClient, options?: { retry?:
}
case "message.removed": {
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(
produce((draft) => {
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)
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
@@ -766,42 +411,6 @@ export function createServerSession(client: OpencodeClient, options?: { retry?:
case "message.part.updated": {
const part = (event.properties as { part: Part }).part
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(
"part_text_accum_delta",
produce((draft) => void delete draft[part.id]),
@@ -822,34 +431,10 @@ export function createServerSession(client: OpencodeClient, options?: { retry?:
return
}
case "message.part.removed": {
const props = event.properties as { sessionID: string; 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)
const props = event.properties as { messageID: string; partID: string }
setData(
produce((draft) => {
delete draft.part_text_accum_delta[props.partID]
deltaBases.delete(props.partID)
const parts = draft.part[props.messageID]
if (!parts) return
const result = Binary.search(parts, props.partID, (part) => part.id)
@@ -860,31 +445,13 @@ export function createServerSession(client: OpencodeClient, options?: { retry?:
return
}
case "message.part.delta": {
const props = event.properties as {
sessionID: string
messageID: string
partID: string
field: string
delta: string
}
const props = event.properties as { messageID: string; partID: string; field: string; delta: string }
const parts = data.part[props.messageID]
if (!parts) return
const result = Binary.search(parts, props.partID, (part) => part.id)
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 current = parts[result.index]?.[field]
if (!deltaBases.has(props.partID) && typeof current === "string")
deltaBases.set(props.partID, { base: current, sessionID: props.sessionID })
setData(
"part_text_accum_delta",
props.partID,
@@ -992,70 +559,32 @@ export function createServerSession(client: OpencodeClient, options?: { retry?:
},
optimistic: {
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 removedMessagesForSession = removedMessages.get(input.sessionID)
removedMessagesForSession?.delete(input.message.id)
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: [] }]]))
if (items) items.set(input.message.id, input)
if (!items) optimistic.set(input.sessionID, new Map([[input.message.id, input]]))
setData("message", input.sessionID, (messages = []) => merge(messages, [input.message]))
setData(
"part_text_accum_delta",
produce((draft) => {
for (const part of [...(data.part[input.message.id] ?? []), ...parts]) {
delete draft[part.id]
deltaBases.delete(part.id)
}
}),
"part",
input.message.id,
input.parts.filter((part) => !!part?.id).sort((a, b) => cmp(a.id, b.id)),
)
setData("part", input.message.id, parts)
},
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)
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(produce((draft) => deleteMessageParts(draft, input.messageID)))
setData(
"part",
produce((draft) => void delete draft[input.messageID]),
)
},
},
diff(sessionID: string, options?: { force?: boolean }) {
touch(sessionID)
if (data.session_diff[sessionID] !== undefined && !options?.force) return Promise.resolve()
return runInflight(inflightDiff, sessionID, () => {
const active = generation(sessionID)
const generation = generations.get(sessionID) ?? 0
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" }))
})
})
@@ -1064,9 +593,9 @@ export function createServerSession(client: OpencodeClient, options?: { retry?:
touch(sessionID)
if (data.todo[sessionID] !== undefined && !options?.force) return Promise.resolve()
return runInflight(inflightTodo, sessionID, () => {
const active = generation(sessionID)
const generation = generations.get(sessionID) ?? 0
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" }))
})
})
+15 -17
View File
@@ -163,29 +163,27 @@ export const { use: useTabs, provider: TabsProvider } = createSimpleContext({
},
newDraft(draft: Omit<DraftTab, "type" | "draftID">, prompt?: string) {
const draftID = uuid()
void startTransition(() => {
setStore(
produce((tabs) => {
tabs.push({ type: "draft", draftID, ...draft })
}),
)
navigate(prompt ? `${draftHref(draftID)}&prompt=${encodeURIComponent(prompt)}` : draftHref(draftID))
})
setStore(
produce((tabs) => {
tabs.push({ type: "draft", draftID, ...draft })
}),
)
navigate(prompt ? `${draftHref(draftID)}&prompt=${encodeURIComponent(prompt)}` : draftHref(draftID))
},
updateDraft(draftID: string, draft: Partial<Omit<DraftTab, "type" | "draftID">>) {
void startTransition(() => {
setStore(
(tab) => tab.type === "draft" && tab.draftID === draftID,
produce((tab) => Object.assign(tab, draft)),
)
})
setStore(
(tab) => tab.type === "draft" && tab.draftID === draftID,
produce((tab) => Object.assign(tab, draft)),
)
},
promoteDraft(draftID: string, session: Omit<SessionTab, "type">) {
// Keep the replacement and navigation atomic so /new-session never renders
// after its backing draft tab has been removed from the store.
// We're viewing this draft when /new-session?draftId=… points at it. Promoting
// 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 next = { type: "session" as const, ...session }
void startTransition(() => {
startTransition(() => {
setStore(
produce((tabs) => {
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) {
const key = ServerConnection.key(conn)
if (global.servers.health[key]?.healthy === false) return
if (
!global
.ensureServerCtx(conn)
@@ -409,7 +408,7 @@ export function NewHome() {
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="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
projects={projects()}
selected={selection()}
@@ -456,7 +455,7 @@ export function NewHome() {
onClose={closeSearch}
onSelect={selectSearchSession}
/>
<ScrollView class="mt-3 -mr-3 min-h-0 flex-1">
<ScrollView class="mt-3 min-h-0 flex-1">
<Show
when={!sessionLoad.isLoading}
fallback={
@@ -469,7 +468,7 @@ export function NewHome() {
when={groups().length > 0}
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()}>
{(group, index) => (
<div class="flex min-w-0 flex-col gap-4">
@@ -768,18 +767,15 @@ function HomeProjectRow(props: {
clearNotifications: (server: ServerConnection.Any, project: LocalProject) => void
language: ReturnType<typeof useLanguage>
}) {
const global = useGlobal()
const serverUnreachable = () => global.servers.health[ServerConnection.key(props.server)]?.healthy === false
const [state, setState] = createStore({ menuOpen: false })
return (
<div class="group/project relative flex h-7 min-w-0 items-center rounded-[6px]">
<button
type="button"
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}
aria-current={props.selected ? "page" : undefined}
disabled={serverUnreachable()}
onClick={() => props.selectProject(props.server, props.project.worktree)}
>
<HomeProjectAvatar project={props.project} />
@@ -969,7 +965,7 @@ function HomeSessionSearch(props: {
style={{
top: "-6px",
left: "-6px",
width: "calc(100% + 12px)",
width: "calc(100% + 14px)",
}}
>
<div class="flex flex-col pt-9">
@@ -1137,7 +1133,7 @@ function HomeSessionSearchResultRow(props: {
function HomeSessionGroupHeader(props: { title: string; onNewSession?: () => void }) {
const language = useLanguage()
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>
<Show when={props.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">
<Suspense>{props.children}</Suspense>
</main>
{import.meta.env.DEV && <DebugBar inline />}
{import.meta.env.DEV && <DebugBar />}
<HelpButton />
<ToastRegion v2 />
</div>
+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/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.
+4 -2
View File
@@ -1,10 +1,12 @@
import { NodeFileSystem } from "@effect/platform-node"
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 { fileURLToPath } from "url"
const contract = compile(ClientApi, { groupNames, endpointNames, omitEndpoints })
const contract = compile(ClientApi, {
groupNames: { "server.session": "sessions", "server.event": "events" },
})
await Effect.runPromise(
Effect.all(
+5 -37
View File
@@ -1,6 +1,6 @@
import { makeDefaultApi } from "@opencode-ai/protocol/api"
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>()(
"@opencode-ai/client/LocationMiddleware",
@@ -11,43 +11,11 @@ class SessionLocationMiddleware extends HttpApiMiddleware.Service<SessionLocatio
{ error: [InvalidRequestError, SessionNotFoundError] },
) {}
export const ClientApi = makeDefaultApi({
const Api = makeDefaultApi({
locationMiddleware: LocationMiddleware,
sessionLocationMiddleware: SessionLocationMiddleware,
})
export const groupNames = {
"server.health": "health",
"server.location": "location",
"server.agent": "agents",
"server.session": "sessions",
"server.message": "messages",
"server.model": "models",
"server.provider": "providers",
"server.integration": "integrations",
"server.credential": "credentials",
"server.permission": "permissions",
"server.fs": "files",
"server.command": "commands",
"server.skill": "skills",
"server.event": "events",
"server.pty": "ptys",
"server.question": "questions",
"server.reference": "references",
"server.projectCopy": "projectCopies",
} as const
export const endpointNames = {
"session.messages": "list",
"integration.connect.key": "connectKey",
"integration.connect.oauth": "connectOauth",
"integration.attempt.status": "attemptStatus",
"integration.attempt.complete": "attemptComplete",
"integration.attempt.cancel": "attemptCancel",
"permission.request.list": "listRequests",
"permission.saved.list": "listSaved",
"permission.saved.remove": "removeSaved",
"question.request.list": "listRequests",
} as const
export const omitEndpoints = new Set(["fs.read", "pty.connect", "pty.connectToken"])
export const SessionGroup = Api.groups["server.session"]
export const EventGroup = Api.groups["server.event"]
export const ClientApi = HttpApi.make("opencode-client").add(SessionGroup).add(EventGroup)
-12
View File
@@ -2,24 +2,12 @@
// Core or Server. Preserve these datatype exports so internal model reorganizations do not require caller migrations.
export * from "./generated-effect/index"
export { Agent } from "@opencode-ai/schema/agent"
export { Command } from "@opencode-ai/schema/command"
export { Credential } from "@opencode-ai/schema/credential"
export { FileSystem } from "@opencode-ai/schema/filesystem"
export { Integration } from "@opencode-ai/schema/integration"
export { Location } from "@opencode-ai/schema/location"
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 { 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 { Session } from "@opencode-ai/schema/session"
export { SessionInput } from "@opencode-ai/schema/session-input"
export { SessionMessage } from "@opencode-ai/schema/session-message"
export { Skill } from "@opencode-ai/schema/skill"
export { Prompt } from "@opencode-ai/schema/prompt"
export type { OpenCodeEvent } from "@opencode-ai/protocol/groups/event"
+128 -607
View File
@@ -13,502 +13,202 @@ const mapClientError = <E>(error: E) =>
? new ClientError({ cause: error })
: error
const Endpoint0_0 = (raw: RawClient["server.health"]) => () =>
raw["health.get"]({}).pipe(Effect.mapError(mapClientError))
const adaptGroup0 = (raw: RawClient["server.health"]) => ({ get: Endpoint0_0(raw) })
type Endpoint1_0Request = Parameters<RawClient["server.location"]["location.get"]>[0]
type Endpoint1_0Input = { readonly location?: Endpoint1_0Request["query"]["location"] }
const Endpoint1_0 = (raw: RawClient["server.location"]) => (input?: Endpoint1_0Input) =>
raw["location.get"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError))
const adaptGroup1 = (raw: RawClient["server.location"]) => ({ get: Endpoint1_0(raw) })
type Endpoint2_0Request = Parameters<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"]
type Endpoint0_0Request = Parameters<RawClient["server.session"]["session.list"]>[0]
type Endpoint0_0Input = {
readonly workspace?: Endpoint0_0Request["query"]["workspace"]
readonly limit?: Endpoint0_0Request["query"]["limit"]
readonly order?: Endpoint0_0Request["query"]["order"]
readonly search?: Endpoint0_0Request["query"]["search"]
readonly directory?: Endpoint0_0Request["query"]["directory"]
readonly project?: Endpoint0_0Request["query"]["project"]
readonly subpath?: Endpoint0_0Request["query"]["subpath"]
readonly cursor?: Endpoint0_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"]({
query: {
workspace: input?.["workspace"],
limit: input?.["limit"],
order: input?.["order"],
search: input?.["search"],
directory: input?.["directory"],
project: input?.["project"],
subpath: input?.["subpath"],
cursor: input?.["cursor"],
workspace: input?.workspace,
limit: input?.limit,
order: input?.order,
search: input?.search,
directory: input?.directory,
project: input?.project,
subpath: input?.subpath,
cursor: input?.cursor,
},
}).pipe(Effect.mapError(mapClientError))
type Endpoint3_1Request = Parameters<RawClient["server.session"]["session.create"]>[0]
type Endpoint3_1Input = {
readonly id?: Endpoint3_1Request["payload"]["id"]
readonly agent?: Endpoint3_1Request["payload"]["agent"]
readonly model?: Endpoint3_1Request["payload"]["model"]
readonly location?: Endpoint3_1Request["payload"]["location"]
type Endpoint0_1Request = Parameters<RawClient["server.session"]["session.create"]>[0]
type Endpoint0_1Input = {
readonly id?: Endpoint0_1Request["payload"]["id"]
readonly agent?: Endpoint0_1Request["payload"]["agent"]
readonly model?: Endpoint0_1Request["payload"]["model"]
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"]({
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(
Effect.mapError(mapClientError),
Effect.map((value) => value.data),
)
const Endpoint3_2 = (raw: RawClient["server.session"]) => () =>
const Endpoint0_2 = (raw: RawClient["server.session"]) => () =>
raw["session.active"]({}).pipe(
Effect.mapError(mapClientError),
Effect.map((value) => value.data),
)
type Endpoint3_3Request = Parameters<RawClient["server.session"]["session.get"]>[0]
type Endpoint3_3Input = { readonly sessionID: Endpoint3_3Request["params"]["sessionID"] }
const Endpoint3_3 = (raw: RawClient["server.session"]) => (input: Endpoint3_3Input) =>
raw["session.get"]({ params: { sessionID: input["sessionID"] } }).pipe(
type Endpoint0_3Request = Parameters<RawClient["server.session"]["session.get"]>[0]
type Endpoint0_3Input = { readonly sessionID: Endpoint0_3Request["params"]["sessionID"] }
const Endpoint0_3 = (raw: RawClient["server.session"]) => (input: Endpoint0_3Input) =>
raw["session.get"]({ params: { sessionID: input.sessionID } }).pipe(
Effect.mapError(mapClientError),
Effect.map((value) => value.data),
)
type Endpoint3_4Request = Parameters<RawClient["server.session"]["session.switchAgent"]>[0]
type Endpoint3_4Input = {
readonly sessionID: Endpoint3_4Request["params"]["sessionID"]
readonly agent: Endpoint3_4Request["payload"]["agent"]
type Endpoint0_4Request = Parameters<RawClient["server.session"]["session.switchAgent"]>[0]
type Endpoint0_4Input = {
readonly sessionID: Endpoint0_4Request["params"]["sessionID"]
readonly agent: Endpoint0_4Request["payload"]["agent"]
}
const Endpoint3_4 = (raw: RawClient["server.session"]) => (input: Endpoint3_4Input) =>
raw["session.switchAgent"]({ params: { sessionID: input["sessionID"] }, payload: { agent: input["agent"] } }).pipe(
const Endpoint0_4 = (raw: RawClient["server.session"]) => (input: Endpoint0_4Input) =>
raw["session.switchAgent"]({ params: { sessionID: input.sessionID }, payload: { agent: input.agent } }).pipe(
Effect.mapError(mapClientError),
)
type Endpoint3_5Request = Parameters<RawClient["server.session"]["session.switchModel"]>[0]
type Endpoint3_5Input = {
readonly sessionID: Endpoint3_5Request["params"]["sessionID"]
readonly model: Endpoint3_5Request["payload"]["model"]
type Endpoint0_5Request = Parameters<RawClient["server.session"]["session.switchModel"]>[0]
type Endpoint0_5Input = {
readonly sessionID: Endpoint0_5Request["params"]["sessionID"]
readonly model: Endpoint0_5Request["payload"]["model"]
}
const Endpoint3_5 = (raw: RawClient["server.session"]) => (input: Endpoint3_5Input) =>
raw["session.switchModel"]({ params: { sessionID: input["sessionID"] }, payload: { model: input["model"] } }).pipe(
const Endpoint0_5 = (raw: RawClient["server.session"]) => (input: Endpoint0_5Input) =>
raw["session.switchModel"]({ params: { sessionID: input.sessionID }, payload: { model: input.model } }).pipe(
Effect.mapError(mapClientError),
)
type Endpoint3_6Request = Parameters<RawClient["server.session"]["session.prompt"]>[0]
type Endpoint3_6Input = {
readonly sessionID: Endpoint3_6Request["params"]["sessionID"]
readonly id?: Endpoint3_6Request["payload"]["id"]
readonly prompt: Endpoint3_6Request["payload"]["prompt"]
readonly delivery?: Endpoint3_6Request["payload"]["delivery"]
readonly resume?: Endpoint3_6Request["payload"]["resume"]
type Endpoint0_6Request = Parameters<RawClient["server.session"]["session.prompt"]>[0]
type Endpoint0_6Input = {
readonly sessionID: Endpoint0_6Request["params"]["sessionID"]
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.prompt"]({
params: { sessionID: input["sessionID"] },
payload: { id: input["id"], prompt: input["prompt"], delivery: input["delivery"], resume: input["resume"] },
params: { sessionID: input.sessionID },
payload: { id: input.id, prompt: input.prompt, delivery: input.delivery, resume: input.resume },
}).pipe(
Effect.mapError(mapClientError),
Effect.map((value) => value.data),
)
type Endpoint3_7Request = Parameters<RawClient["server.session"]["session.compact"]>[0]
type Endpoint3_7Input = { readonly sessionID: Endpoint3_7Request["params"]["sessionID"] }
const Endpoint3_7 = (raw: RawClient["server.session"]) => (input: Endpoint3_7Input) =>
raw["session.compact"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError))
type Endpoint0_7Request = Parameters<RawClient["server.session"]["session.compact"]>[0]
type Endpoint0_7Input = { readonly sessionID: Endpoint0_7Request["params"]["sessionID"] }
const Endpoint0_7 = (raw: RawClient["server.session"]) => (input: Endpoint0_7Input) =>
raw["session.compact"]({ params: { sessionID: input.sessionID } }).pipe(Effect.mapError(mapClientError))
type Endpoint3_8Request = Parameters<RawClient["server.session"]["session.wait"]>[0]
type Endpoint3_8Input = { readonly sessionID: Endpoint3_8Request["params"]["sessionID"] }
const Endpoint3_8 = (raw: RawClient["server.session"]) => (input: Endpoint3_8Input) =>
raw["session.wait"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError))
type Endpoint0_8Request = Parameters<RawClient["server.session"]["session.wait"]>[0]
type Endpoint0_8Input = { readonly sessionID: Endpoint0_8Request["params"]["sessionID"] }
const Endpoint0_8 = (raw: RawClient["server.session"]) => (input: Endpoint0_8Input) =>
raw["session.wait"]({ params: { sessionID: input.sessionID } }).pipe(Effect.mapError(mapClientError))
type Endpoint3_9Request = Parameters<RawClient["server.session"]["session.revert.stage"]>[0]
type Endpoint3_9Input = {
readonly sessionID: Endpoint3_9Request["params"]["sessionID"]
readonly messageID: Endpoint3_9Request["payload"]["messageID"]
readonly files?: Endpoint3_9Request["payload"]["files"]
type Endpoint0_9Request = Parameters<RawClient["server.session"]["session.revert.stage"]>[0]
type Endpoint0_9Input = {
readonly sessionID: Endpoint0_9Request["params"]["sessionID"]
readonly messageID: Endpoint0_9Request["payload"]["messageID"]
readonly files?: Endpoint0_9Request["payload"]["files"]
}
const Endpoint3_9 = (raw: RawClient["server.session"]) => (input: Endpoint3_9Input) =>
const Endpoint0_9 = (raw: RawClient["server.session"]) => (input: Endpoint0_9Input) =>
raw["session.revert.stage"]({
params: { sessionID: input["sessionID"] },
payload: { messageID: input["messageID"], files: input["files"] },
params: { sessionID: input.sessionID },
payload: { messageID: input.messageID, files: input.files },
}).pipe(
Effect.mapError(mapClientError),
Effect.map((value) => value.data),
)
type Endpoint3_10Request = Parameters<RawClient["server.session"]["session.revert.clear"]>[0]
type Endpoint3_10Input = { readonly sessionID: Endpoint3_10Request["params"]["sessionID"] }
const Endpoint3_10 = (raw: RawClient["server.session"]) => (input: Endpoint3_10Input) =>
raw["session.revert.clear"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError))
type Endpoint0_10Request = Parameters<RawClient["server.session"]["session.revert.clear"]>[0]
type Endpoint0_10Input = { readonly sessionID: Endpoint0_10Request["params"]["sessionID"] }
const Endpoint0_10 = (raw: RawClient["server.session"]) => (input: Endpoint0_10Input) =>
raw["session.revert.clear"]({ params: { sessionID: input.sessionID } }).pipe(Effect.mapError(mapClientError))
type Endpoint3_11Request = Parameters<RawClient["server.session"]["session.revert.commit"]>[0]
type Endpoint3_11Input = { readonly sessionID: Endpoint3_11Request["params"]["sessionID"] }
const Endpoint3_11 = (raw: RawClient["server.session"]) => (input: Endpoint3_11Input) =>
raw["session.revert.commit"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError))
type Endpoint0_11Request = Parameters<RawClient["server.session"]["session.revert.commit"]>[0]
type Endpoint0_11Input = { readonly sessionID: Endpoint0_11Request["params"]["sessionID"] }
const Endpoint0_11 = (raw: RawClient["server.session"]) => (input: Endpoint0_11Input) =>
raw["session.revert.commit"]({ params: { sessionID: input.sessionID } }).pipe(Effect.mapError(mapClientError))
type Endpoint3_12Request = Parameters<RawClient["server.session"]["session.context"]>[0]
type Endpoint3_12Input = { readonly sessionID: Endpoint3_12Request["params"]["sessionID"] }
const Endpoint3_12 = (raw: RawClient["server.session"]) => (input: Endpoint3_12Input) =>
raw["session.context"]({ params: { sessionID: input["sessionID"] } }).pipe(
type Endpoint0_12Request = Parameters<RawClient["server.session"]["session.context"]>[0]
type Endpoint0_12Input = { readonly sessionID: Endpoint0_12Request["params"]["sessionID"] }
const Endpoint0_12 = (raw: RawClient["server.session"]) => (input: Endpoint0_12Input) =>
raw["session.context"]({ params: { sessionID: input.sessionID } }).pipe(
Effect.mapError(mapClientError),
Effect.map((value) => value.data),
)
type Endpoint3_13Request = Parameters<RawClient["server.session"]["session.history"]>[0]
type Endpoint3_13Input = {
readonly sessionID: Endpoint3_13Request["params"]["sessionID"]
readonly limit?: Endpoint3_13Request["query"]["limit"]
readonly after?: Endpoint3_13Request["query"]["after"]
type Endpoint0_13Request = Parameters<RawClient["server.session"]["session.history"]>[0]
type Endpoint0_13Input = {
readonly sessionID: Endpoint0_13Request["params"]["sessionID"]
readonly limit?: Endpoint0_13Request["query"]["limit"]
readonly after?: Endpoint0_13Request["query"]["after"]
}
const Endpoint3_13 = (raw: RawClient["server.session"]) => (input: Endpoint3_13Input) =>
const Endpoint0_13 = (raw: RawClient["server.session"]) => (input: Endpoint0_13Input) =>
raw["session.history"]({
params: { sessionID: input["sessionID"] },
query: { limit: input["limit"], after: input["after"] },
params: { sessionID: input.sessionID },
query: { limit: input.limit, after: input.after },
}).pipe(Effect.mapError(mapClientError))
type Endpoint3_14Request = Parameters<RawClient["server.session"]["session.events"]>[0]
type Endpoint3_14Input = {
readonly sessionID: Endpoint3_14Request["params"]["sessionID"]
readonly after?: Endpoint3_14Request["query"]["after"]
type Endpoint0_14Request = Parameters<RawClient["server.session"]["session.events"]>[0]
type Endpoint0_14Input = {
readonly sessionID: Endpoint0_14Request["params"]["sessionID"]
readonly after?: Endpoint0_14Request["query"]["after"]
}
const Endpoint3_14 = (raw: RawClient["server.session"]) => (input: Endpoint3_14Input) =>
const Endpoint0_14 = (raw: RawClient["server.session"]) => (input: Endpoint0_14Input) =>
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.map((stream) => stream.pipe(Stream.mapError(mapClientError))),
),
)
type Endpoint3_15Request = Parameters<RawClient["server.session"]["session.interrupt"]>[0]
type Endpoint3_15Input = { readonly sessionID: Endpoint3_15Request["params"]["sessionID"] }
const Endpoint3_15 = (raw: RawClient["server.session"]) => (input: Endpoint3_15Input) =>
raw["session.interrupt"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError))
type Endpoint0_15Request = Parameters<RawClient["server.session"]["session.interrupt"]>[0]
type Endpoint0_15Input = { readonly sessionID: Endpoint0_15Request["params"]["sessionID"] }
const Endpoint0_15 = (raw: RawClient["server.session"]) => (input: Endpoint0_15Input) =>
raw["session.interrupt"]({ params: { sessionID: input.sessionID } }).pipe(Effect.mapError(mapClientError))
type Endpoint3_16Request = Parameters<RawClient["server.session"]["session.message"]>[0]
type Endpoint3_16Input = {
readonly sessionID: Endpoint3_16Request["params"]["sessionID"]
readonly messageID: Endpoint3_16Request["params"]["messageID"]
type Endpoint0_16Request = Parameters<RawClient["server.session"]["session.message"]>[0]
type Endpoint0_16Input = {
readonly sessionID: Endpoint0_16Request["params"]["sessionID"]
readonly messageID: Endpoint0_16Request["params"]["messageID"]
}
const Endpoint3_16 = (raw: RawClient["server.session"]) => (input: Endpoint3_16Input) =>
raw["session.message"]({ params: { sessionID: input["sessionID"], messageID: input["messageID"] } }).pipe(
const Endpoint0_16 = (raw: RawClient["server.session"]) => (input: Endpoint0_16Input) =>
raw["session.message"]({ params: { sessionID: input.sessionID, messageID: input.messageID } }).pipe(
Effect.mapError(mapClientError),
Effect.map((value) => value.data),
)
const adaptGroup3 = (raw: RawClient["server.session"]) => ({
list: Endpoint3_0(raw),
create: Endpoint3_1(raw),
active: Endpoint3_2(raw),
get: Endpoint3_3(raw),
switchAgent: Endpoint3_4(raw),
switchModel: Endpoint3_5(raw),
prompt: Endpoint3_6(raw),
compact: Endpoint3_7(raw),
wait: Endpoint3_8(raw),
stage: Endpoint3_9(raw),
clear: Endpoint3_10(raw),
commit: Endpoint3_11(raw),
context: Endpoint3_12(raw),
history: Endpoint3_13(raw),
events: Endpoint3_14(raw),
interrupt: Endpoint3_15(raw),
message: Endpoint3_16(raw),
const adaptGroup0 = (raw: RawClient["server.session"]) => ({
list: Endpoint0_0(raw),
create: Endpoint0_1(raw),
active: Endpoint0_2(raw),
get: Endpoint0_3(raw),
switchAgent: Endpoint0_4(raw),
switchModel: Endpoint0_5(raw),
prompt: Endpoint0_6(raw),
compact: Endpoint0_7(raw),
wait: Endpoint0_8(raw),
stage: Endpoint0_9(raw),
clear: Endpoint0_10(raw),
commit: Endpoint0_11(raw),
context: Endpoint0_12(raw),
history: Endpoint0_13(raw),
events: Endpoint0_14(raw),
interrupt: Endpoint0_15(raw),
message: Endpoint0_16(raw),
})
type Endpoint4_0Request = Parameters<RawClient["server.message"]["session.messages"]>[0]
type Endpoint4_0Input = {
readonly sessionID: Endpoint4_0Request["params"]["sessionID"]
readonly limit?: Endpoint4_0Request["query"]["limit"]
readonly order?: Endpoint4_0Request["query"]["order"]
readonly cursor?: Endpoint4_0Request["query"]["cursor"]
}
const Endpoint4_0 = (raw: RawClient["server.message"]) => (input: Endpoint4_0Input) =>
raw["session.messages"]({
params: { sessionID: input["sessionID"] },
query: { limit: input["limit"], order: input["order"], cursor: input["cursor"] },
}).pipe(Effect.mapError(mapClientError))
const adaptGroup4 = (raw: RawClient["server.message"]) => ({ list: Endpoint4_0(raw) })
type Endpoint5_0Request = Parameters<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"]) => () =>
const Endpoint1_0 = (raw: RawClient["server.event"]) => () =>
Stream.unwrap(
raw["event.subscribe"]({}).pipe(
Effect.mapError(mapClientError),
@@ -516,190 +216,11 @@ const Endpoint13_0 = (raw: RawClient["server.event"]) => () =>
),
)
const adaptGroup13 = (raw: RawClient["server.event"]) => ({ subscribe: Endpoint13_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 adaptGroup1 = (raw: RawClient["server.event"]) => ({ subscribe: Endpoint1_0(raw) })
const adaptClient = (raw: RawClient) => ({
health: adaptGroup0(raw["server.health"]),
location: adaptGroup1(raw["server.location"]),
agents: adaptGroup2(raw["server.agent"]),
sessions: adaptGroup3(raw["server.session"]),
messages: adaptGroup4(raw["server.message"]),
models: adaptGroup5(raw["server.model"]),
providers: adaptGroup6(raw["server.provider"]),
integrations: adaptGroup7(raw["server.integration"]),
credentials: adaptGroup8(raw["server.credential"]),
permissions: adaptGroup9(raw["server.permission"]),
files: adaptGroup10(raw["server.fs"]),
commands: adaptGroup11(raw["server.command"]),
skills: adaptGroup12(raw["server.skill"]),
events: adaptGroup13(raw["server.event"]),
ptys: adaptGroup14(raw["server.pty"]),
questions: adaptGroup15(raw["server.question"]),
references: adaptGroup16(raw["server.reference"]),
projectCopies: adaptGroup17(raw["server.projectCopy"]),
sessions: adaptGroup0(raw["server.session"]),
events: adaptGroup1(raw["server.event"]),
})
export const make = (options?: { readonly baseUrl?: URL | string }) =>
+15 -621
View File
@@ -1,9 +1,4 @@
import type {
HealthGetOutput,
LocationGetInput,
LocationGetOutput,
AgentsListInput,
AgentsListOutput,
SessionsListInput,
SessionsListOutput,
SessionsCreateInput,
@@ -37,81 +32,7 @@ import type {
SessionsInterruptOutput,
SessionsMessageInput,
SessionsMessageOutput,
MessagesListInput,
MessagesListOutput,
ModelsListInput,
ModelsListOutput,
ProvidersListInput,
ProvidersListOutput,
ProvidersGetInput,
ProvidersGetOutput,
IntegrationsListInput,
IntegrationsListOutput,
IntegrationsGetInput,
IntegrationsGetOutput,
IntegrationsConnectKeyInput,
IntegrationsConnectKeyOutput,
IntegrationsConnectOauthInput,
IntegrationsConnectOauthOutput,
IntegrationsAttemptStatusInput,
IntegrationsAttemptStatusOutput,
IntegrationsAttemptCompleteInput,
IntegrationsAttemptCompleteOutput,
IntegrationsAttemptCancelInput,
IntegrationsAttemptCancelOutput,
CredentialsUpdateInput,
CredentialsUpdateOutput,
CredentialsRemoveInput,
CredentialsRemoveOutput,
PermissionsListRequestsInput,
PermissionsListRequestsOutput,
PermissionsListSavedInput,
PermissionsListSavedOutput,
PermissionsRemoveSavedInput,
PermissionsRemoveSavedOutput,
PermissionsCreateInput,
PermissionsCreateOutput,
PermissionsListInput,
PermissionsListOutput,
PermissionsGetInput,
PermissionsGetOutput,
PermissionsReplyInput,
PermissionsReplyOutput,
FilesListInput,
FilesListOutput,
FilesFindInput,
FilesFindOutput,
CommandsListInput,
CommandsListOutput,
SkillsListInput,
SkillsListOutput,
EventsSubscribeOutput,
PtysListInput,
PtysListOutput,
PtysCreateInput,
PtysCreateOutput,
PtysGetInput,
PtysGetOutput,
PtysUpdateInput,
PtysUpdateOutput,
PtysRemoveInput,
PtysRemoveOutput,
QuestionsListRequestsInput,
QuestionsListRequestsOutput,
QuestionsListInput,
QuestionsListOutput,
QuestionsReplyInput,
QuestionsReplyOutput,
QuestionsRejectInput,
QuestionsRejectOutput,
ReferencesListInput,
ReferencesListOutput,
ProjectCopiesCreateInput,
ProjectCopiesCreateOutput,
ProjectCopiesRemoveInput,
ProjectCopiesRemoveOutput,
ProjectCopiesRefreshInput,
ProjectCopiesRefreshOutput,
} from "./types"
import { ClientError } from "./client-error"
@@ -247,41 +168,6 @@ export function make(options: ClientOptions) {
})
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: {
list: (input?: SessionsListInput, requestOptions?: RequestOptions) =>
request<SessionsListOutput>(
@@ -289,14 +175,14 @@ export function make(options: ClientOptions) {
method: "GET",
path: `/api/session`,
query: {
workspace: input?.["workspace"],
limit: input?.["limit"],
order: input?.["order"],
search: input?.["search"],
directory: input?.["directory"],
project: input?.["project"],
subpath: input?.["subpath"],
cursor: input?.["cursor"],
workspace: input?.workspace,
limit: input?.limit,
order: input?.order,
search: input?.search,
directory: input?.directory,
project: input?.project,
subpath: input?.subpath,
cursor: input?.cursor,
},
successStatus: 200,
declaredStatuses: [400, 401],
@@ -309,12 +195,7 @@ export function make(options: ClientOptions) {
{
method: "POST",
path: `/api/session`,
body: {
id: input?.["id"],
agent: input?.["agent"],
model: input?.["model"],
location: input?.["location"],
},
body: { id: input?.id, agent: input?.agent, model: input?.model, location: input?.location },
successStatus: 200,
declaredStatuses: [401, 400],
empty: false,
@@ -348,7 +229,7 @@ export function make(options: ClientOptions) {
{
method: "POST",
path: `/api/session/${encodeURIComponent(input.sessionID)}/agent`,
body: { agent: input["agent"] },
body: { agent: input.agent },
successStatus: 204,
declaredStatuses: [404, 400, 401],
empty: true,
@@ -360,7 +241,7 @@ export function make(options: ClientOptions) {
{
method: "POST",
path: `/api/session/${encodeURIComponent(input.sessionID)}/model`,
body: { model: input["model"] },
body: { model: input.model },
successStatus: 204,
declaredStatuses: [404, 400, 401],
empty: true,
@@ -372,7 +253,7 @@ export function make(options: ClientOptions) {
{
method: "POST",
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,
declaredStatuses: [409, 404, 400, 401],
empty: false,
@@ -406,7 +287,7 @@ export function make(options: ClientOptions) {
{
method: "POST",
path: `/api/session/${encodeURIComponent(input.sessionID)}/revert/stage`,
body: { messageID: input["messageID"], files: input["files"] },
body: { messageID: input.messageID, files: input.files },
successStatus: 200,
declaredStatuses: [404, 500, 400, 401],
empty: false,
@@ -451,7 +332,7 @@ export function make(options: ClientOptions) {
{
method: "GET",
path: `/api/session/${encodeURIComponent(input.sessionID)}/history`,
query: { limit: input["limit"], after: input["after"] },
query: { limit: input.limit, after: input.after },
successStatus: 200,
declaredStatuses: [404, 400, 401],
empty: false,
@@ -463,7 +344,7 @@ export function make(options: ClientOptions) {
{
method: "GET",
path: `/api/session/${encodeURIComponent(input.sessionID)}/event`,
query: { after: input["after"] },
query: { after: input.after },
successStatus: 200,
declaredStatuses: [404, 400, 401],
empty: false,
@@ -493,321 +374,6 @@ export function make(options: ClientOptions) {
requestOptions,
).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: {
subscribe: (requestOptions?: RequestOptions): AsyncIterable<EventsSubscribeOutput> =>
sse<EventsSubscribeOutput>(
@@ -815,178 +381,6 @@ export function make(options: ClientOptions) {
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 { Api } from "@opencode-ai/server/api"
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", () => {
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(CorePrompt).toBe(Prompt)
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(Project.ID.global).toBe("global")
expect(Provider.ID.anthropic).toBe("anthropic")
expect(Workspace.ID.create()).toStartWith("wrk_")
})
test("client and Server contracts generate identically", () => {
const server = compile(Api, { groupNames, endpointNames, omitEndpoints })
const client = compile(ClientApi, { groupNames, endpointNames, omitEndpoints })
test("client and Server Session contracts generate identically", () => {
const options = { groupNames: { "server.session": "sessions" } }
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))
})
-37
View File
@@ -1,43 +1,6 @@
import { expect, test } from "bun:test"
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 () => {
const client = OpenCode.make({
baseUrl: "http://localhost:3000",
+2 -3
View File
@@ -16,8 +16,8 @@
"opencode": "./bin/opencode"
},
"exports": {
"./effect/layer-node": "./src/effect/layer-node.ts",
"./effect/app-node": "./src/effect/app-node.ts",
"./effect/layer-node": "./src/effect/layer-node/index.ts",
"./effect/node": "./src/effect/node.ts",
"./session/runner": "./src/session/runner/index.ts",
"./system-context": "./src/system-context/index.ts",
"./*": "./src/*.ts"
@@ -104,7 +104,6 @@
"ai-gateway-provider": "3.1.2",
"bun-pty": "0.4.8",
"cross-spawn": "catalog:",
"diff": "catalog:",
"drizzle-orm": "catalog:",
"effect": "catalog:",
"fuzzysort": "3.1.0",
+1 -1
View File
@@ -1,6 +1,6 @@
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 { Agent } from "@opencode-ai/schema/agent"
import { State } from "./state"
+1 -1
View File
@@ -1,6 +1,6 @@
export * as AISDK from "./aisdk"
import { makeLocationNode } from "./effect/app-node"
import { makeLocationNode } from "./effect/node"
import type { LanguageModelV3 } from "@ai-sdk/provider"
import { Cause, Context, Effect, Layer, Schema, Scope } from "effect"
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 { Identifier } from "./id/id"
import { makeGlobalNode } from "./effect/app-node"
import { makeGlobalNode } from "./effect/node"
export type Status = "running" | "completed" | "error" | "cancelled"
+1 -1
View File
@@ -1,6 +1,6 @@
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 { Catalog } from "@opencode-ai/schema/catalog"
import { ModelV2 } from "./model"
+1 -1
View File
@@ -1,6 +1,6 @@
export * as CommandV2 from "./command"
import { makeLocationNode } from "./effect/app-node"
import { makeLocationNode } from "./effect/node"
import { Context, Effect, Layer, Types } from "effect"
import { Command } from "@opencode-ai/schema/command"
import { State } from "./state"
+1 -1
View File
@@ -1,6 +1,6 @@
export * as Config from "./config"
import { makeLocationNode } from "./effect/app-node"
import { makeLocationNode } from "./effect/node"
import path from "path"
import { type ParseError, parse } from "jsonc-parser"
import { Context, Effect, Layer, Option, Schema } from "effect"
@@ -66,7 +66,6 @@ export const Plugin = define({
if (config.capabilities !== undefined) {
model.capabilities = {
tools: config.capabilities.tools,
reasoning: config.capabilities.reasoning,
input: [...config.capabilities.input],
output: [...config.capabilities.output],
}
+1 -1
View File
@@ -5,7 +5,7 @@ import { Context, Effect, Layer, Schema } from "effect"
import { Credential } from "@opencode-ai/schema/credential"
import { Integration } from "@opencode-ai/schema/integration"
import { Database } from "./database/database"
import { makeGlobalNode } from "./effect/app-node"
import { makeGlobalNode } from "./effect/node"
import { CredentialTable } from "./credential/sql"
export const ID = Credential.ID
+2 -2
View File
@@ -24,8 +24,8 @@ import {
import * as NodeChildProcess from "node:child_process"
import { PassThrough } from "node:stream"
import launch from "cross-spawn"
import { makeGlobalNode } from "./effect/app-node"
import { filesystem, path } from "./effect/app-node-platform"
import { makeGlobalNode } from "./effect/node"
import { filesystem, path } from "./effect/layer-node-platform"
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 { DatabaseMigration } from "./migration"
import { InstallationChannel } from "../installation/version"
import { makeGlobalNode } from "../effect/app-node"
import { makeGlobalNode } from "../effect/node"
const makeDatabase = EffectDrizzleSqlite.makeWithDefaults()
type DatabaseShape = Effect.Success<typeof makeDatabase>
@@ -3,7 +3,7 @@ import { LLMClient, RequestExecutor } from "@opencode-ai/llm/route"
import { FileSystem, Path } from "effect"
import { FetchHttpClient } 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 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 * 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"
type AnyNode = Node<unknown, unknown, any>
type RuntimeLayer = Layer.Layer<never, unknown, unknown>
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 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 }
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<
Implementation extends Layer.Any,
Items extends NodeList,
@@ -111,6 +77,37 @@ export function group<const Items extends readonly AnyNode[]>(
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 = {
readonly source: Layer.Any
readonly replacement: Layer.Any
@@ -127,147 +124,4 @@ export function replace<A, E, R, E2>(
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"
@@ -1,24 +1,18 @@
import { Layer } from "effect"
import { buildLocationServiceMap } from "../location-services"
import { LocationServiceMap } from "../location-service-map"
import { LayerNode } from "./layer-node"
import { makeGlobalNode } from "./app-node"
import { LayerNode, LayerNodeTree } from "./layer-node"
import { makeGlobalNode } from "./node"
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]))
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 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 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 { EventSequenceTable, EventTable } from "./event/sql"
import { Location } from "./location"
import { makeGlobalNode } from "./effect/app-node"
import { makeGlobalNode } from "./effect/node"
import { isDeepStrictEqual } from "node:util"
import { Durable } from "@opencode-ai/schema/durable-event-manifest"
+1 -1
View File
@@ -1,6 +1,6 @@
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 { dirname } from "path"
import { KeyedMutex } from "./effect/keyed-mutex"
+1 -1
View File
@@ -1,6 +1,6 @@
export * as FileSystem from "./filesystem"
import { makeLocationNode } from "./effect/app-node"
import { makeLocationNode } from "./effect/node"
import path from "path"
import { Context, Effect, Layer, Schema } from "effect"
import { FSUtil } from "./fs-util"
+1 -1
View File
@@ -1,6 +1,6 @@
export * as FileSystemSearch from "./search"
import { makeLocationNode } from "../effect/app-node"
import { makeLocationNode } from "../effect/node"
import path from "path"
import { Context, Effect, Layer, Scope } from "effect"
import { Fff } from "#fff"
+1 -1
View File
@@ -3,7 +3,7 @@ export * as Watcher from "./watcher"
// @ts-ignore
import { createWrapper } from "@parcel/watcher/wrapper"
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 { FileSystemWatcher } from "@opencode-ai/schema/filesystem-watcher"
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 { Glob } from "./util/glob"
import { serviceUse } from "./effect/service-use"
import { makeGlobalNode } from "./effect/app-node"
import { filesystem } from "./effect/app-node-platform"
import { makeGlobalNode } from "./effect/node"
import { filesystem } from "./effect/layer-node-platform"
export namespace FSUtil {
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 { FSUtil } from "./fs-util"
import { AppProcess } from "./process"
import { makeGlobalNode } from "./effect/app-node"
import { makeGlobalNode } from "./effect/node"
import { File } from "./file"
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 { Flock } from "./util/flock"
import { Flag } from "./flag/flag"
import { makeGlobalNode } from "./effect/app-node"
import { makeGlobalNode } from "./effect/node"
const app = "opencode"
const data = path.join(xdgData!, app)
+1 -1
View File
@@ -1,6 +1,6 @@
export * as Image from "./image"
import { makeLocationNode } from "./effect/app-node"
import { makeLocationNode } from "./effect/node"
import { Context, Effect, Layer, Schema } from "effect"
import { Config } from "./config"
import { FileSystem } from "./filesystem"
+1 -1
View File
@@ -9,7 +9,7 @@ import { Location } from "./location"
import { AbsolutePath } from "./schema"
import { SystemContext } from "./system-context/index"
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")({
path: AbsolutePath,
+1 -1
View File
@@ -1,6 +1,6 @@
export * as Integration from "./integration"
import { makeLocationNode } from "./effect/app-node"
import { makeLocationNode } from "./effect/node"
import {
Cause,
Clock,
+1 -1
View File
@@ -1,6 +1,6 @@
export * as LocationMutation from "./location-mutation"
import { makeLocationNode } from "./effect/app-node"
import { makeLocationNode } from "./effect/node"
import path from "path"
import { Context, Effect, Layer, Schema } from "effect"
import { FSUtil } from "./fs-util"
+1 -1
View File
@@ -1,6 +1,6 @@
import { Context, Effect, Layer, LayerMap } from "effect"
import { LayerNode } from "./effect/layer-node"
import { Node } from "./effect/app-node"
import { Node } from "./effect/node"
import { Location } from "./location"
import type { LocationError, LocationServices } from "./location-services"
+6 -6
View File
@@ -4,8 +4,8 @@ import { AISDK } from "./aisdk"
import { Catalog } from "./catalog"
import { CommandV2 } from "./command"
import { Config } from "./config"
import { LayerNode } from "./effect/layer-node"
import { Node } from "./effect/app-node"
import { LayerNode, LayerNodeTree } from "./effect/layer-node"
import { Node } from "./effect/node"
import { FileMutation } from "./file-mutation"
import { FileSystem } from "./filesystem"
import { FileSystemSearch } from "./filesystem/search"
@@ -88,11 +88,11 @@ export function buildLocationServiceMap(
LocationServiceMap.Service,
LayerMap.make(
(ref: Location.Ref) => {
const location = LayerNode.hoist(
LayerNode.bind(locationServices, Location.node, Location.boundNode(ref)),
const location = LayerNodeTree.hoist(
LayerNodeTree.bind(locationServices, Location.node, Location.boundNode(ref)),
Node.tags.values.global,
)
return LayerNode.compile(location.node, replacements).pipe(
return LayerNodeTree.compile(location.node, replacements).pipe(
Layer.fresh,
Layer.tap(() =>
Effect.logInfo("booting location services", {
@@ -100,7 +100,7 @@ export function buildLocationServiceMap(
workspaceID: ref.workspaceID,
}),
),
Layer.provide(LayerNode.compile(location.hoisted, replacements)),
Layer.provide(LayerNodeTree.compile(location.hoisted, replacements)),
)
},
{ 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 { Project } from "./project"
import { LayerNode } from "./effect/layer-node"
import { makeLocationNode, tags } from "./effect/app-node"
import { makeLocationNode, tags } from "./effect/node"
export * as Location from "./location"
+2 -2
View File
@@ -9,8 +9,8 @@ import { Hash } from "./util/hash"
import { FSUtil } from "./fs-util"
import { InstallationChannel, InstallationVersion } from "./installation/version"
import { EventV2 } from "./event"
import { makeGlobalNode } from "./effect/app-node"
import { httpClient } from "./effect/app-node-platform"
import { makeGlobalNode } from "./effect/node"
import { httpClient } from "./effect/layer-node-platform"
export const CatalogModelStatus = Schema.Literals(["alpha", "beta", "deprecated"])
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 { Global } from "./global"
import { EffectFlock } from "./util/effect-flock"
import { makeGlobalNode } from "./effect/app-node"
import { filesystem } from "./effect/app-node-platform"
import { makeGlobalNode } from "./effect/node"
import { filesystem } from "./effect/layer-node-platform"
import { makeRuntime } from "./effect/runtime"
import { NpmConfig } from "./npm-config"
+1 -1
View File
@@ -1,6 +1,6 @@
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 { Permission } from "@opencode-ai/schema/permission"
import { EventV2 } from "./event"
+1 -1
View File
@@ -3,7 +3,7 @@ export * as PermissionSaved from "./saved"
import { eq } from "drizzle-orm"
import { Context, Effect, Layer, Schema } from "effect"
import { Database } from "../database/database"
import { makeGlobalNode } from "../effect/app-node"
import { makeGlobalNode } from "../effect/node"
import { ProjectV2 } from "../project"
import { PermissionTable } from "./sql"
import { PermissionSaved } from "@opencode-ai/schema/permission-saved"
+1 -1
View File
@@ -1,6 +1,6 @@
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 type { Plugin as PluginRuntime } from "@opencode-ai/plugin/v2/effect"
import { Plugin } from "@opencode-ai/schema/plugin"
+16 -19
View File
@@ -1,7 +1,7 @@
export * as PluginInternal from "./internal"
import { makeLocationNode } from "../effect/app-node"
import { httpClient } from "../effect/app-node-platform"
import { makeLocationNode } from "../effect/node"
import { httpClient } from "../effect/layer-node-platform"
import type { PluginContext } from "@opencode-ai/plugin/v2/effect"
import { Effect, Layer, Scope } from "effect"
import { AgentV2 } from "../agent"
@@ -25,7 +25,6 @@ import { Npm } from "../npm"
import { PluginV2 } from "../plugin"
import { Reference } from "../reference"
import { SkillV2 } from "../skill"
import { State } from "../state"
import { FetchHttpClient, HttpClient } from "effect/unstable/http"
import { AgentPlugin } from "./agent"
import { CommandPlugin } from "./command"
@@ -105,22 +104,20 @@ const layer = Layer.effectDiscard(
return plugin.add(PluginV2.ID.make(loaded.id), loaded.effect)
}
yield* State.batch(
Effect.gen(function* () {
yield* add(ConfigReferencePlugin.Plugin)
yield* add(AgentPlugin.Plugin)
yield* add(CommandPlugin.Plugin)
yield* add(SkillPlugin.Plugin)
yield* add(ModelsDevPlugin)
yield* add(ConfigAgentPlugin.Plugin)
yield* add(ConfigCommandPlugin.Plugin)
yield* add(ConfigSkillPlugin.Plugin)
for (const item of ProviderPlugins) yield* add(item)
yield* add(ConfigExternalPlugin.Plugin)
yield* add(ConfigProviderPlugin.Plugin)
yield* add(VariantPlugin.Plugin)
}),
).pipe(Effect.withSpan("PluginInternal.boot"), Effect.forkScoped({ startImmediately: true }))
yield* Effect.gen(function* () {
yield* add(ConfigReferencePlugin.Plugin)
yield* add(AgentPlugin.Plugin)
yield* add(CommandPlugin.Plugin)
yield* add(SkillPlugin.Plugin)
yield* add(ModelsDevPlugin)
yield* add(ConfigAgentPlugin.Plugin)
yield* add(ConfigCommandPlugin.Plugin)
yield* add(ConfigSkillPlugin.Plugin)
for (const item of ProviderPlugins) yield* add(item)
yield* add(ConfigExternalPlugin.Plugin)
yield* add(ConfigProviderPlugin.Plugin)
yield* add(VariantPlugin.Plugin)
}).pipe(Effect.withSpan("PluginInternal.boot"), Effect.forkScoped({ startImmediately: true }))
}),
)
-1
View File
@@ -108,7 +108,6 @@ export const ModelsDevPlugin = define({
}
draft.capabilities = {
tools: model.tool_call,
reasoning: model.reasoning,
input: [...(model.modalities?.input ?? [])],
output: [...(model.modalities?.output ?? [])],
}
@@ -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 { IntegrationOAuthMethodRegistration } from "@opencode-ai/plugin/v2/effect/integration"
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) {
const events = yield* EventV2.Service
const http = yield* HttpClient.HttpClient
const loading = Semaphore.makeUnsafe(1)
let connected = false
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)" } })
})
connected = (yield* ctx.integration.connection.active("opencode")) !== undefined
yield* load()
yield* ctx.catalog.transform((catalog) => {
for (const [providerID, item] of Object.entries(providers ?? {})) {
catalog.provider.update(providerID, (provider) => {
@@ -135,7 +134,6 @@ export const OpencodePlugin = define<HttpClient.HttpClient | EventV2.Service | S
: { id: model.api.id, type: "native", url: config.provider.api, settings: {} }
}
if (config.tool_call !== undefined) model.capabilities.tools = config.tool_call
if (config.reasoning !== undefined) model.capabilities.reasoning = config.reasoning
if (config.modalities?.input !== undefined) model.capabilities.input = [...config.modalities.input]
if (config.modalities?.output !== undefined) model.capabilities.output = [...config.modalities.output]
const packageName = config.provider?.npm ?? item.npm
@@ -178,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(
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 }),
)
yield* refresh().pipe(Effect.forkScoped)
}),
})
+6 -30
View File
@@ -28,36 +28,12 @@ export const Plugin = define({
})
export function generate(model: ModelV2Info): ModelV2Info["variants"] {
if (model.api.type !== "aisdk") return []
if (model.api.package === "@ai-sdk/openai-compatible") {
const ids = `${model.id} ${model.api.id}`.toLowerCase()
if (["glm-5.2", "glm-5-2", "glm-5p2"].some((name) => ids.includes(name)))
return ["high", "max"].map((id) => ({
id,
headers: {},
body: { reasoning_effort: id },
}))
return []
}
if (model.api.package === "@ai-sdk/anthropic") return anthropicThinking(model)
return []
}
// Anthropic thinking levels lower to the Messages API `thinking` body, which the
// v2 Anthropic protocol only accepts as `{ type: "enabled", budget_tokens }`.
// Budgets scale with the model output limit so the larger tier stays under the cap.
function anthropicThinking(model: ModelV2Info): ModelV2Info["variants"] {
if (!model.capabilities.reasoning) return []
const budget = (target: number) => Math.max(1024, Math.min(target, model.limit.output - 1))
return [
{ id: "high", target: 16_000 },
{ id: "max", target: 31_999 },
].map((tier) => ({
id: tier.id,
if (model.api.type !== "aisdk" || model.api.package !== "@ai-sdk/openai-compatible") return []
const ids = `${model.id} ${model.api.id}`.toLowerCase()
if (!["glm-5.2", "glm-5-2", "glm-5p2"].some((name) => ids.includes(name))) return []
return ["high", "max"].map((id) => ({
id,
headers: {},
body: { thinking: { type: "enabled", budget_tokens: budget(tier.target) } },
body: { reasoning_effort: id },
}))
}
+1 -1
View File
@@ -1,6 +1,6 @@
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 { Wildcard } from "./util/wildcard"
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 { ChildProcessSpawner } from "effect/unstable/process/ChildProcessSpawner"
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", {
command: Schema.String,
@@ -20,7 +20,6 @@ export class AppProcessError extends Schema.TaggedErrorClass<AppProcessError>()(
}
export interface RunOptions {
readonly combineOutput?: boolean
readonly maxOutputBytes?: number
readonly maxErrorBytes?: number
readonly signal?: AbortSignal
@@ -38,10 +37,8 @@ export interface RunStreamOptions {
export interface RunResult {
readonly command: string
readonly exitCode: number
readonly output?: Buffer
readonly stdout: Buffer
readonly stderr: Buffer
readonly outputTruncated?: boolean
readonly stdoutTruncated: boolean
readonly stderrTruncated: boolean
}
@@ -146,22 +143,6 @@ export const layer = Layer.effect(
const collect = Effect.scoped(
Effect.gen(function* () {
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(
[
collectStream(handle.stdout, options?.maxOutputBytes),
+1 -1
View File
@@ -6,7 +6,7 @@ import path from "path"
import { AbsolutePath } from "./schema"
import { FSUtil } from "./fs-util"
import { Git } from "./git"
import { makeGlobalNode } from "./effect/app-node"
import { makeGlobalNode } from "./effect/node"
import { Hash } from "./util/hash"
import { ProjectDirectories } from "./project/directories"
import { ProjectSchema } from "./project/schema"
+1 -1
View File
@@ -5,7 +5,7 @@ import path from "path"
import { AbsolutePath } from "../schema"
import { FSUtil } from "../fs-util"
import { Git } from "../git"
import { makeLocationNode } from "../effect/app-node"
import { makeLocationNode } from "../effect/node"
import { Project } from "../project"
import { ProjectDirectories } from "./directories"
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 { Context, Effect, Layer, Schema } from "effect"
import { Database } from "../database/database"
import { makeGlobalNode } from "../effect/app-node"
import { makeGlobalNode } from "../effect/node"
import { AbsolutePath, optional } from "../schema"
import { ProjectSchema } from "./schema"
import { ProjectDirectoryTable } from "./sql"
+1 -1
View File
@@ -1,6 +1,6 @@
export * as Pty from "./pty"
import { makeLocationNode } from "./effect/app-node"
import { makeLocationNode } from "./effect/node"
import type { Disp, Proc } from "#pty"
import { Context, Effect, Layer, Schema, Types } from "effect"
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 { PtyID } from "./schema"
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 CAPACITY = 10_000
+1 -5
View File
@@ -1,7 +1,3 @@
export * as PublicEventManifest from "./public-event-manifest"
import { Event } from "@opencode-ai/schema/event"
import { EventManifest } from "@opencode-ai/schema/event-manifest"
export const Definitions = EventManifest.ServerDefinitions
export const Latest = Event.latest(Definitions)
export { ServerDefinitions as Definitions } from "@opencode-ai/schema/event-manifest"
+1 -1
View File
@@ -1,6 +1,6 @@
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 { Question } from "@opencode-ai/schema/question"
import { EventV2 } from "./event"
+1 -1
View File
@@ -1,6 +1,6 @@
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 { Reference } from "@opencode-ai/schema/reference"
import { Global } from "./global"
+1 -1
View File
@@ -1,6 +1,6 @@
export * as ReferenceGuidance from "./guidance"
import { makeLocationNode } from "../effect/app-node"
import { makeLocationNode } from "../effect/node"
import { Context, Effect, Layer, Schema } from "effect"
import { Reference } from "../reference"
import { SystemContext } from "../system-context/index"
+1 -1
View File
@@ -5,7 +5,7 @@ import { Git } from "./git"
import { Global } from "./global"
import { Repository } from "./repository"
import { AbsolutePath } from "./schema"
import { makeGlobalNode } from "./effect/app-node"
import { makeGlobalNode } from "./effect/node"
import { EffectFlock } from "./util/effect-flock"
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 { ChildProcess } from "effect/unstable/process"
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 { NonNegativeInt, PositiveInt, RelativePath } from "./schema"
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 { ChildProcessSpawner } from "effect/unstable/process/ChildProcessSpawner"
import { CrossSpawnSpawner } from "../cross-spawn-spawner"
import { makeGlobalNode } from "../effect/app-node"
import { httpClient } from "../effect/app-node-platform"
import { makeGlobalNode } from "../effect/node"
import { httpClient } from "../effect/layer-node-platform"
import { FSUtil } from "../fs-util"
import { Global } from "../global"
import { which } from "../util/which"
+2 -8
View File
@@ -27,7 +27,7 @@ import { fromRow } from "./session/info"
import { SessionRunner } from "./session/runner/index"
import { SessionStore } from "./session/store"
import { SessionExecution } from "./session/execution"
import { makeGlobalNode } from "./effect/app-node"
import { makeGlobalNode } from "./effect/node"
import { LocationServiceMap } from "./location-service-map"
import { MessageDecodeError } from "./session/error"
import { SessionEvent } from "./session/event"
@@ -400,13 +400,7 @@ export const layer = Layer.effect(
})
}),
switchModel: Effect.fn("V2Session.switchModel")(function* (input) {
const session = 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* result.get(input.sessionID)
yield* events.publish(SessionEvent.ModelSwitched, {
sessionID: input.sessionID,
messageID: SessionMessage.ID.create(),
+1 -1
View File
@@ -2,7 +2,7 @@ export * as SessionExecution from "./execution"
import { Context, Effect, Layer } from "effect"
import { LayerNode } from "../effect/layer-node"
import { Node } from "../effect/app-node"
import { Node } from "../effect/node"
import { SessionRunner } from "./runner/index"
import { SessionSchema } from "./schema"
+1 -1
View File
@@ -1,6 +1,6 @@
import { Cause, Effect, Layer } from "effect"
import { LocationServiceMap } from "../../location-service-map"
import { makeGlobalNode } from "../../effect/app-node"
import { makeGlobalNode } from "../../effect/node"
import { SessionRunCoordinator } from "../run-coordinator"
import { SessionRunner } from "../runner"
import { SessionSchema } from "../schema"
@@ -349,7 +349,6 @@ export function update(adapter: Adapter, event: SessionEvent.Event) {
id: event.data.reasoningID,
text: "",
providerMetadata: event.data.providerMetadata,
time: { created: event.data.timestamp },
}),
),
)
@@ -366,7 +365,6 @@ export function update(adapter: Adapter, event: SessionEvent.Event) {
const match = latestReasoning(draft, event.data.reasoningID)
if (match) {
match.text = event.data.text
match.time = { created: match.time?.created ?? event.data.timestamp, completed: event.data.timestamp }
if (event.data.providerMetadata !== undefined) match.providerMetadata = event.data.providerMetadata
}
})
+1 -1
View File
@@ -4,7 +4,7 @@ import { and, desc, eq, gt, or, sql } from "drizzle-orm"
import { DateTime, Effect, Layer, Schema } from "effect"
import { Database } from "../database/database"
import { EventV2 } from "../event"
import { makeGlobalNode } from "../effect/app-node"
import { makeGlobalNode } from "../effect/node"
import { SessionEvent } from "./event"
import { SessionV1 } from "../v1/session"
import { WorkspaceTable } from "../control-plane/workspace.sql"
+3 -4
View File
@@ -36,8 +36,8 @@ import { createLLMEventPublisher } from "./publish-llm-event"
import { toLLMMessages } from "./to-llm-message"
import { MAX_STEPS_PROMPT } from "./max-steps"
import { Snapshot } from "../../snapshot"
import { makeLocationNode } from "../../effect/app-node"
import { llmClient } from "../../effect/app-node-platform"
import { makeLocationNode } from "../../effect/node"
import { llmClient } from "../../effect/layer-node-platform"
/**
* Runs one durable coding-agent Session until it settles.
@@ -335,8 +335,7 @@ export const layer = Layer.effect(
if (stream._tag === "Success" && !publisher.hasProviderError())
yield* withPublication(publisher.failUnsettledTools("Provider did not return a tool result", true))
if (stream._tag === "Failure") return yield* Effect.failCause(stream.cause)
if (settled._tag === "Failure" && Cause.hasInterrupts(settled.cause))
return yield* Effect.failCause(settled.cause)
if (settled._tag === "Failure") return yield* Effect.failCause(settled.cause)
return { needsContinuation: !publisher.hasProviderError() && needsContinuation, step: currentStep }
}),
)
+2 -2
View File
@@ -1,6 +1,6 @@
export * as SessionRunnerModel from "./model"
import { makeLocationNode } from "../../effect/app-node"
import { makeLocationNode } from "../../effect/node"
import { type Model } from "@opencode-ai/llm"
import * as AnthropicMessages from "@opencode-ai/llm/protocols/anthropic-messages"
import * as OpenAICompatibleChat from "@opencode-ai/llm/protocols/openai-compatible-chat"
@@ -133,7 +133,7 @@ export const fromCatalogModel = (
credential?: Credential.Value,
): Effect.Effect<Model, UnsupportedApiError> => {
const resolved =
credential?.type !== "key" || credential.metadata === undefined
credential?.metadata === undefined
? model
: produce(model, (draft) => {
Object.assign(draft.request.body, credential.metadata)
+1 -1
View File
@@ -3,7 +3,7 @@ export * as SessionStore from "./store"
import { eq } from "drizzle-orm"
import { Context, Effect, Layer, Schema } from "effect"
import { Database } from "../database/database"
import { makeGlobalNode } from "../effect/app-node"
import { makeGlobalNode } from "../effect/node"
import { SessionHistory } from "./history"
import { MessageDecodeError } from "./error"
import { SessionMessage } from "./message"
+1 -1
View File
@@ -4,7 +4,7 @@ import { asc, eq } from "drizzle-orm"
import { Context, Effect, Layer } from "effect"
import { SessionTodo } from "@opencode-ai/schema/session-todo"
import { Database } from "../database/database"
import { makeLocationNode } from "../effect/app-node"
import { makeLocationNode } from "../effect/node"
import { EventV2 } from "../event"
import { SessionSchema } from "./schema"
import { TodoTable } from "./sql"
+1 -1
View File
@@ -1,6 +1,6 @@
export * as SkillV2 from "./skill"
import { makeLocationNode } from "./effect/app-node"
import { makeLocationNode } from "./effect/node"
import path from "path"
import { Context, Effect, Layer, Schema, Types } from "effect"
import { Skill } from "@opencode-ai/schema/skill"
+2 -2
View File
@@ -5,8 +5,8 @@ import { Context, Effect, Layer, Schedule, Schema } from "effect"
import { FetchHttpClient, HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http"
import { FSUtil } from "../fs-util"
import { Global } from "../global"
import { makeGlobalNode } from "../effect/app-node"
import { httpClient } from "../effect/app-node-platform"
import { makeGlobalNode } from "../effect/node"
import { httpClient } from "../effect/layer-node-platform"
import { AbsolutePath } from "../schema"
const skillConcurrency = 4
+1 -1
View File
@@ -1,6 +1,6 @@
export * as SkillGuidance from "./guidance"
import { makeLocationNode } from "../effect/app-node"
import { makeLocationNode } from "../effect/node"
import { Context, Effect, Layer, Schema } from "effect"
import { AgentV2 } from "../agent"
import { PermissionV2 } from "../permission"
+1 -1
View File
@@ -1,6 +1,6 @@
export * as Snapshot from "./snapshot"
import { makeLocationNode } from "./effect/app-node"
import { makeLocationNode } from "./effect/node"
import path from "path"
import { Context, Effect, Layer, Schema } from "effect"
import { Config } from "./config"
+1 -1
View File
@@ -1,6 +1,6 @@
export * as SystemContextBuiltIns from "./builtins"
import { makeLocationNode } from "../effect/app-node"
import { makeLocationNode } from "../effect/node"
import { DateTime, Effect, Layer, Schema } from "effect"
import { Location } from "../location"
import { SystemContext } from "./index"
+1 -1
View File
@@ -2,7 +2,7 @@ export * as SystemContextRegistry from "./registry"
import { Context, Effect, Layer, Ref, Scope } from "effect"
import { SystemContext } from "./index"
import { makeLocationNode } from "../effect/app-node"
import { makeLocationNode } from "../effect/node"
export interface Entry {
readonly key: SystemContext.Key
+1 -1
View File
@@ -5,7 +5,7 @@ import { Context, Duration, Effect, Layer, Option, Schedule, Schema } from "effe
import { Config } from "./config"
import { FSUtil } from "./fs-util"
import { Global } from "./global"
import { makeGlobalNode, makeLocationNode } from "./effect/app-node"
import { makeGlobalNode, makeLocationNode } from "./effect/node"
import { SessionSchema } from "./session/schema"
import { Identifier } from "./util/identifier"
import type { ToolOutput } from "@opencode-ai/llm"
+1 -1
View File
@@ -3,7 +3,7 @@ export * as ApplicationTools from "./application-tools"
import { Context, Effect, Layer, Scope } from "effect"
import { State } from "../state"
import { Tool } from "./tool"
import { makeGlobalNode } from "../effect/app-node"
import { makeGlobalNode } from "../effect/node"
type Data = {
readonly entries: Map<string, Entry>
+11 -45
View File
@@ -1,8 +1,6 @@
export * as ApplyPatchTool from "./apply-patch"
import { ToolFailure } from "@opencode-ai/llm"
import { FileDiff } from "@opencode-ai/schema/file-diff"
import { createTwoFilesPatch, diffLines } from "diff"
import { Effect, Layer, Schema } from "effect"
import { FileMutation } from "../file-mutation"
import { FSUtil } from "../fs-util"
@@ -26,10 +24,7 @@ export const Applied = Schema.Struct({
target: Schema.String,
})
export const Output = Schema.Struct({
applied: Schema.Array(Applied),
files: Schema.Array(FileDiff.Info),
})
export const Output = Schema.Struct({ applied: Schema.Array(Applied) })
export type Output = typeof Output.Type
export const toModelOutput = (output: Output) =>
@@ -41,17 +36,11 @@ export const toModelOutput = (output: Output) =>
].join("\n")
type Prepared =
| (Extract<Patch.Hunk, { readonly type: "add" | "delete" }> & {
readonly target: LocationMutation.Target
readonly before: string
readonly after: string
})
| (Extract<Patch.Hunk, { readonly type: "add" | "delete" }> & { readonly target: LocationMutation.Target })
| (Extract<Patch.Hunk, { readonly type: "update" }> & {
readonly target: LocationMutation.Target
readonly source: Uint8Array
readonly content: string
readonly before: string
readonly after: string
})
export const layer = Layer.effectDiscard(
@@ -124,36 +113,29 @@ export const layer = Layer.effectDiscard(
for (const { hunk, target } of targets) {
yield* Effect.gen(function* () {
if (hunk.type === "add") {
prepared.push({
...hunk,
target,
before: "",
after:
hunk.contents.endsWith("\n") || hunk.contents === "" ? hunk.contents : `${hunk.contents}\n`,
})
prepared.push({ ...hunk, target })
return
}
if ((yield* fs.stat(target.canonical)).type !== "File") yield* fail(hunk.path)
const source = yield* fs.readFile(target.canonical)
const original = new TextDecoder("utf-8", { ignoreBOM: true }).decode(source)
const before = original.replace(/^\uFEFF/, "")
if (hunk.type === "delete") {
prepared.push({ ...hunk, target, before, after: "" })
prepared.push({ ...hunk, target })
return
}
const update = Patch.derive(hunk.path, hunk.chunks, original)
const source = yield* fs.readFile(target.canonical)
const update = Patch.derive(
hunk.path,
hunk.chunks,
new TextDecoder("utf-8", { ignoreBOM: true }).decode(source),
)
prepared.push({
...hunk,
target,
source,
content: Patch.joinBom(update.content, update.bom),
before,
after: update.content,
})
}).pipe(Effect.mapError(() => fail(hunk.path)))
}
const patchFiles = prepared.map(patchFile)
yield* Effect.forEach(
prepared,
(change) =>
@@ -183,7 +165,7 @@ export const layer = Layer.effectDiscard(
}).pipe(Effect.mapError(() => fail(change.path))),
{ discard: true },
)
return { applied, files: patchFiles }
return { applied }
}).pipe(Effect.mapError((error) => (error instanceof ToolFailure ? error : fail("patch"))))
},
}),
@@ -193,19 +175,3 @@ export const layer = Layer.effectDiscard(
.pipe(Effect.orDie)
}),
)
function patchFile(change: Prepared): typeof FileDiff.Info.Type {
const counts = diffLines(change.before, change.after).reduce(
(result, item) => ({
additions: result.additions + (item.added ? (item.count ?? 0) : 0),
deletions: result.deletions + (item.removed ? (item.count ?? 0) : 0),
}),
{ additions: 0, deletions: 0 },
)
return {
file: change.target.resource,
patch: createTwoFilesPatch(change.target.resource, change.target.resource, change.before, change.after),
status: change.type === "add" ? "added" : change.type === "delete" ? "deleted" : "modified",
...counts,
}
}
+36 -28
View File
@@ -30,15 +30,16 @@ export const Input = Schema.Struct({
}),
})
const StructuredOutput = Schema.Struct({
exit: Schema.Number.pipe(Schema.optional),
truncated: Schema.Boolean,
timeout: Schema.Boolean.pipe(Schema.optional),
})
const Output = Schema.Struct({
...StructuredOutput.fields,
command: Schema.String,
cwd: Schema.String,
exitCode: Schema.Number.pipe(Schema.optional),
/** Bounded compact equivalent of stdout/stderr: stderr is labeled when present. */
output: Schema.String,
truncated: Schema.Boolean,
stdoutTruncated: Schema.Boolean.pipe(Schema.optional),
stderrTruncated: Schema.Boolean.pipe(Schema.optional),
timedOut: Schema.Boolean.pipe(Schema.optional),
warnings: Schema.Array(Schema.String).pipe(Schema.optional),
})
@@ -46,12 +47,24 @@ type Output = typeof Output.Type
const defaultShell = () => (process.platform === "win32" ? (process.env.COMSPEC ?? "cmd.exe") : "/bin/sh")
const compactOutput = (stdout: string, stderr: string) => {
const output = stdout && stderr ? `${stdout}\n\nstderr:\n${stderr}` : stderr ? `stderr:\n${stderr}` : stdout
return output || "(no output)"
}
const captureNotice = (stdoutTruncated: boolean, stderrTruncated: boolean) => {
if (stdoutTruncated && stderrTruncated) return "[stdout and stderr capture truncated at the in-memory safety limit]"
if (stdoutTruncated) return "[stdout capture truncated at the in-memory safety limit]"
if (stderrTruncated) return "[stderr capture truncated at the in-memory safety limit]"
return undefined
}
const modelOutput = (output: Output) => {
const warnings = output.warnings?.length
? `\n\nWarnings:\n${output.warnings.map((warning) => `- ${warning}`).join("\n")}`
: ""
if (output.timeout) return `${warnings.trimStart()}${warnings ? "\n\n" : ""}Command timed out before completion.`
return `${warnings.trimStart()}${warnings ? "\n\n" : ""}Command exited with code ${output.exit}.`
if (output.timedOut) return `${output.output}${warnings}\n\nCommand timed out before completion.`
return `${output.output}${warnings}\n\nCommand exited with code ${output.exitCode}.`
}
const isTimeout = (error: AppProcess.AppProcessError) =>
@@ -103,16 +116,7 @@ export const layer = Layer.effectDiscard(
description: `Execute one shell command string with the host user's filesystem, process, and network authority. The active Location is the default working directory. Relative workdir values resolve from that Location. External workdir values require external_directory approval; best-effort command-argument path warnings are advisory only. Timeout values are milliseconds (default: ${DEFAULT_TIMEOUT_MS}; maximum: ${MAX_TIMEOUT_MS}). Uses the configured shell when set; otherwise uses /bin/sh on POSIX and COMSPEC or cmd.exe on Windows.`,
input: Input,
output: Output,
structured: StructuredOutput,
toStructuredOutput: ({ output }) => ({
truncated: output.truncated,
...(output.exit === undefined ? {} : { exit: output.exit }),
...(output.timeout === undefined ? {} : { timeout: output.timeout }),
}),
toModelOutput: ({ output }) => [
{ type: "text", text: output.output },
{ type: "text", text: modelOutput(output) },
],
toModelOutput: ({ output }) => [{ type: "text", text: modelOutput(output) }],
execute: (input, context) =>
Effect.gen(function* () {
const source = {
@@ -159,9 +163,9 @@ export const layer = Layer.effectDiscard(
const timeout = input.timeout ?? DEFAULT_TIMEOUT_MS
const result = yield* appProcess
.run(command, {
combineOutput: true,
timeout: Duration.millis(timeout),
maxOutputBytes: MAX_CAPTURE_BYTES,
maxErrorBytes: MAX_CAPTURE_BYTES,
})
.pipe(
Effect.catchTag("AppProcessError", (error) =>
@@ -170,22 +174,26 @@ export const layer = Layer.effectDiscard(
)
if (!result) {
return {
command: input.command,
cwd: target.canonical,
output: `Command exceeded timeout of ${timeout} ms. Retry with a larger timeout if the command is expected to take longer.`,
truncated: false,
timeout: true,
timedOut: true,
...(warnings.length ? { warnings } : {}),
}
}
const output = result.output?.toString("utf8") || "(no output)"
const notice = result.outputTruncated
? "[output capture truncated at the in-memory safety limit]"
: undefined
const compact = compactOutput(result.stdout.toString("utf8"), result.stderr.toString("utf8"))
const notice = captureNotice(result.stdoutTruncated, result.stderrTruncated)
return {
exit: result.exitCode,
output: notice ? `${output}\n\n${notice}` : output,
truncated: result.outputTruncated === true,
command: input.command,
cwd: target.canonical,
exitCode: result.exitCode,
output: notice ? `${compact}\n\n${notice}` : compact,
truncated: result.stdoutTruncated || result.stderrTruncated,
...(warnings.length ? { warnings } : {}),
...(result.stdoutTruncated ? { stdoutTruncated: true } : {}),
...(result.stderrTruncated ? { stderrTruncated: true } : {}),
}
}).pipe(Effect.mapError(() => new ToolFailure({ message: `Unable to execute command: ${input.command}` }))),
}),
+2 -2
View File
@@ -1,6 +1,6 @@
export * as BuiltInTools from "./builtins"
import { makeLocationNode } from "../effect/app-node"
import { makeLocationNode } from "../effect/node"
import { Layer } from "effect"
import { BashTool } from "./bash"
import { ApplyPatchTool } from "./apply-patch"
@@ -28,7 +28,7 @@ import { QuestionV2 } from "../question"
import { SkillV2 } from "../skill"
import { SessionTodo } from "../session/todo"
import { ToolRegistry } from "./registry"
import { httpClient } from "../effect/app-node-platform"
import { httpClient } from "../effect/layer-node-platform"
/**
* Composes only the shipped Location-scoped built-in tool transforms.
+6 -22
View File
@@ -7,8 +7,6 @@
export * as EditTool from "./edit"
import { ToolFailure } from "@opencode-ai/llm"
import { FileDiff } from "@opencode-ai/schema/file-diff"
import { createTwoFilesPatch, diffLines } from "diff"
import { Effect, Layer, Schema } from "effect"
import { FileMutation } from "../file-mutation"
import { FSUtil } from "../fs-util"
@@ -32,7 +30,10 @@ export const Input = Schema.Struct({
})
export const Output = Schema.Struct({
files: Schema.Array(FileDiff.Info),
operation: Schema.Literal("write"),
target: Schema.String,
resource: Schema.String,
existed: Schema.Boolean,
replacements: Schema.Number,
})
export type Output = typeof Output.Type
@@ -70,7 +71,7 @@ const previewLines = (value: string, prefix: "+" | "-") => {
export const toModelOutput = (output: Output, oldString: string, newString: string) =>
[
`Edited file successfully: ${output.files[0]?.file}`,
`Edited file successfully: ${output.resource}`,
`Replacements: ${output.replacements}`,
"```diff",
...previewLines(oldString, "-"),
@@ -178,13 +179,6 @@ export const layer = Layer.effectDiscard(
input.replaceAll === true
? source.text.replaceAll(oldString, newString)
: source.text.replace(oldString, newString)
const counts = diffLines(source.text, replaced).reduce(
(result, item) => ({
additions: result.additions + (item.added ? (item.count ?? 0) : 0),
deletions: result.deletions + (item.removed ? (item.count ?? 0) : 0),
}),
{ additions: 0, deletions: 0 },
)
const next = splitBom(replaced)
const result = yield* unableToEdit(
files.writeIfUnchanged({
@@ -193,17 +187,7 @@ export const layer = Layer.effectDiscard(
content: joinBom(next.text, source.bom || next.bom),
}),
)
return {
files: [
{
file: result.resource,
patch: createTwoFilesPatch(result.resource, result.resource, source.text, replaced),
status: "modified" as const,
...counts,
},
],
replacements,
} satisfies Output
return { ...result, replacements } satisfies Output
})
},
}),
+1 -1
View File
@@ -5,7 +5,7 @@ import { pathToFileURL } from "url"
import { Context, Effect, Layer, Option, Schema } from "effect"
import { FileSystem } from "../filesystem"
import { FSUtil } from "../fs-util"
import { makeLocationNode } from "../effect/app-node"
import { makeLocationNode } from "../effect/node"
import { AbsolutePath, PositiveInt, RelativePath } from "../schema"
export const MAX_READ_LINES = 2_000

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