Compare commits

..

4 Commits

Author SHA1 Message Date
Aiden Cline 6be105f82b wip 2026-05-10 20:36:32 -05:00
Aiden Cline 7ac810c67e Merge branch 'dev' into styled-skill-errors 2026-05-09 13:37:38 -05:00
Aiden Cline 32935ef3cf wip 2026-05-09 13:37:34 -05:00
Aiden Cline 59795b7543 ignore: fix typerrs on dev 2026-05-09 12:06:14 -05:00
139 changed files with 743 additions and 1864 deletions
+17 -17
View File
@@ -29,7 +29,7 @@
},
"packages/app": {
"name": "@opencode-ai/app",
"version": "1.14.44",
"version": "1.14.42",
"dependencies": {
"@kobalte/core": "catalog:",
"@opencode-ai/core": "workspace:*",
@@ -85,7 +85,7 @@
},
"packages/console/app": {
"name": "@opencode-ai/console-app",
"version": "1.14.44",
"version": "1.14.42",
"dependencies": {
"@cloudflare/vite-plugin": "1.15.2",
"@ibm/plex": "6.4.1",
@@ -120,7 +120,7 @@
},
"packages/console/core": {
"name": "@opencode-ai/console-core",
"version": "1.14.44",
"version": "1.14.42",
"dependencies": {
"@aws-sdk/client-sts": "3.782.0",
"@jsx-email/render": "1.1.1",
@@ -147,7 +147,7 @@
},
"packages/console/function": {
"name": "@opencode-ai/console-function",
"version": "1.14.44",
"version": "1.14.42",
"dependencies": {
"@ai-sdk/anthropic": "3.0.64",
"@ai-sdk/openai": "3.0.48",
@@ -171,7 +171,7 @@
},
"packages/console/mail": {
"name": "@opencode-ai/console-mail",
"version": "1.14.44",
"version": "1.14.42",
"dependencies": {
"@jsx-email/all": "2.2.3",
"@jsx-email/cli": "1.4.3",
@@ -195,7 +195,7 @@
},
"packages/core": {
"name": "@opencode-ai/core",
"version": "1.14.44",
"version": "1.14.42",
"bin": {
"opencode": "./bin/opencode",
},
@@ -229,7 +229,7 @@
},
"packages/desktop": {
"name": "@opencode-ai/desktop",
"version": "1.14.44",
"version": "1.14.42",
"dependencies": {
"drizzle-orm": "catalog:",
"effect": "catalog:",
@@ -283,7 +283,7 @@
},
"packages/enterprise": {
"name": "@opencode-ai/enterprise",
"version": "1.14.44",
"version": "1.14.42",
"dependencies": {
"@opencode-ai/core": "workspace:*",
"@opencode-ai/ui": "workspace:*",
@@ -313,7 +313,7 @@
},
"packages/function": {
"name": "@opencode-ai/function",
"version": "1.14.44",
"version": "1.14.42",
"dependencies": {
"@octokit/auth-app": "8.0.1",
"@octokit/rest": "catalog:",
@@ -329,7 +329,7 @@
},
"packages/http-recorder": {
"name": "@opencode-ai/http-recorder",
"version": "1.14.44",
"version": "1.14.42",
"dependencies": {
"@effect/platform-node": "catalog:",
"effect": "catalog:",
@@ -342,7 +342,7 @@
},
"packages/llm": {
"name": "@opencode-ai/llm",
"version": "1.14.44",
"version": "1.14.42",
"dependencies": {
"@smithy/eventstream-codec": "4.2.14",
"@smithy/util-utf8": "4.2.2",
@@ -360,7 +360,7 @@
},
"packages/opencode": {
"name": "opencode",
"version": "1.14.44",
"version": "1.14.42",
"bin": {
"opencode": "./bin/opencode",
},
@@ -495,7 +495,7 @@
},
"packages/plugin": {
"name": "@opencode-ai/plugin",
"version": "1.14.44",
"version": "1.14.42",
"dependencies": {
"@opencode-ai/sdk": "workspace:*",
"effect": "catalog:",
@@ -533,7 +533,7 @@
},
"packages/sdk/js": {
"name": "@opencode-ai/sdk",
"version": "1.14.44",
"version": "1.14.42",
"dependencies": {
"cross-spawn": "catalog:",
},
@@ -548,7 +548,7 @@
},
"packages/slack": {
"name": "@opencode-ai/slack",
"version": "1.14.44",
"version": "1.14.42",
"dependencies": {
"@opencode-ai/sdk": "workspace:*",
"@slack/bolt": "^3.17.1",
@@ -583,7 +583,7 @@
},
"packages/ui": {
"name": "@opencode-ai/ui",
"version": "1.14.44",
"version": "1.14.42",
"dependencies": {
"@kobalte/core": "catalog:",
"@opencode-ai/core": "workspace:*",
@@ -632,7 +632,7 @@
},
"packages/web": {
"name": "@opencode-ai/web",
"version": "1.14.44",
"version": "1.14.42",
"dependencies": {
"@astrojs/cloudflare": "12.6.3",
"@astrojs/markdown-remark": "6.3.1",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/app",
"version": "1.14.44",
"version": "1.14.42",
"description": "",
"type": "module",
"exports": {
@@ -28,12 +28,6 @@ import { createOpenSessionFileTab, createSessionTabs, getTabReorderIndex, type S
import { setSessionHandoff } from "@/pages/session/handoff"
import { useSessionLayout } from "@/pages/session/session-layout"
type RenderDiff = (SnapshotFileDiff & { file: string }) | VcsFileDiff
function renderDiff(value: SnapshotFileDiff | VcsFileDiff): value is RenderDiff {
return typeof value.file === "string"
}
export function SessionSidePanel(props: {
canReview: () => boolean
diffs: () => (SnapshotFileDiff | VcsFileDiff)[]
@@ -76,8 +70,7 @@ export function SessionSidePanel(props: {
})
const treeWidth = createMemo(() => (fileOpen() ? `${layout.fileTree.width()}px` : "0px"))
const diffs = createMemo(() => props.diffs().filter(renderDiff))
const diffFiles = createMemo(() => diffs().map((d) => d.file))
const diffFiles = createMemo(() => props.diffs().map((d) => d.file))
const kinds = createMemo(() => {
const merge = (a: "add" | "del" | "mix" | undefined, b: "add" | "del" | "mix") => {
if (!a) return b
@@ -88,7 +81,7 @@ export function SessionSidePanel(props: {
const normalize = (p: string) => p.replaceAll("\\\\", "/").replace(/\/+$/, "")
const out = new Map<string, "add" | "del" | "mix">()
for (const diff of diffs()) {
for (const diff of props.diffs()) {
const file = normalize(diff.file)
const kind = diff.status === "added" ? "add" : diff.status === "deleted" ? "del" : "mix"
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-app",
"version": "1.14.44",
"version": "1.14.42",
"type": "module",
"license": "MIT",
"scripts": {
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/console-core",
"version": "1.14.44",
"version": "1.14.42",
"private": true,
"type": "module",
"license": "MIT",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-function",
"version": "1.14.44",
"version": "1.14.42",
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-mail",
"version": "1.14.44",
"version": "1.14.42",
"dependencies": {
"@jsx-email/all": "2.2.3",
"@jsx-email/cli": "1.4.3",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"$schema": "https://json.schemastore.org/package.json",
"version": "1.14.44",
"version": "1.14.42",
"name": "@opencode-ai/core",
"type": "module",
"license": "MIT",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@opencode-ai/desktop",
"private": true,
"version": "1.14.44",
"version": "1.14.42",
"type": "module",
"license": "MIT",
"homepage": "https://opencode.ai",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/enterprise",
"version": "1.14.44",
"version": "1.14.42",
"private": true,
"type": "module",
"license": "MIT",
+6 -6
View File
@@ -1,7 +1,7 @@
id = "opencode"
name = "OpenCode"
description = "The open source coding agent."
version = "1.14.44"
version = "1.14.42"
schema_version = 1
authors = ["Anomaly"]
repository = "https://github.com/anomalyco/opencode"
@@ -11,26 +11,26 @@ name = "OpenCode"
icon = "./icons/opencode.svg"
[agent_servers.opencode.targets.darwin-aarch64]
archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.44/opencode-darwin-arm64.zip"
archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.42/opencode-darwin-arm64.zip"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.darwin-x86_64]
archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.44/opencode-darwin-x64.zip"
archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.42/opencode-darwin-x64.zip"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.linux-aarch64]
archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.44/opencode-linux-arm64.tar.gz"
archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.42/opencode-linux-arm64.tar.gz"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.linux-x86_64]
archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.44/opencode-linux-x64.tar.gz"
archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.42/opencode-linux-x64.tar.gz"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.windows-x86_64]
archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.44/opencode-windows-x64.zip"
archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.42/opencode-windows-x64.zip"
cmd = "./opencode.exe"
args = ["acp"]
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/function",
"version": "1.14.44",
"version": "1.14.42",
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"$schema": "https://json.schemastore.org/package.json",
"version": "1.14.44",
"version": "1.14.42",
"name": "@opencode-ai/http-recorder",
"type": "module",
"license": "MIT",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"$schema": "https://json.schemastore.org/package.json",
"version": "1.14.44",
"version": "1.14.42",
"name": "@opencode-ai/llm",
"type": "module",
"license": "MIT",
@@ -1 +1 @@
ALTER TABLE `workspace` ADD `time_used` integer NOT NULL DEFAULT 0;
ALTER TABLE `workspace` ADD `time_used` integer NOT NULL;
+1 -1
View File
@@ -1,6 +1,6 @@
{
"$schema": "https://json.schemastore.org/package.json",
"version": "1.14.44",
"version": "1.14.42",
"name": "opencode",
"type": "module",
"license": "MIT",
+2 -2
View File
@@ -59,10 +59,10 @@ Rules:
## Schema → Zod interop
When a service uses Effect Schema internally but needs Zod schemas for the HTTP layer, derive Zod from Schema using the `zod()` helper from `@opencode-ai/core/effect-zod`:
When a service uses Effect Schema internally but needs Zod schemas for the HTTP layer, derive Zod from Schema using the `zod()` helper from `@/util/effect-zod`:
```ts
import { zod } from "@opencode-ai/core/effect-zod"
import { zod } from "@/util/effect-zod"
export const ZodInfo = zod(Info) // derives z.ZodType from Schema.Union
```
+3 -3
View File
@@ -8,7 +8,7 @@ Zod-first definitions to Effect Schema with Zod compatibility shims.
Use Effect Schema as the source of truth for domain models, IDs, inputs,
outputs, and typed errors. Keep Zod available at existing HTTP, tool, and
compatibility boundaries by exposing a `.zod` static derived from the Effect
schema via `@opencode-ai/core/effect-zod`.
schema via `@/util/effect-zod`.
The long-term driver is `specs/effect/http-api.md` — once the HTTP server
moves to `@effect/platform`, every Schema-first DTO can flow through
@@ -97,7 +97,7 @@ creating a parallel schema source of truth.
## Escape hatches
The walker in `@opencode-ai/core/effect-zod` exposes two explicit escape hatches for
The walker in `@/util/effect-zod` exposes two explicit escape hatches for
cases the pure-Schema path cannot express. Each one stays in the codebase
only as long as its upstream or local dependency requires it — inline
comments document when each can be deleted.
@@ -389,7 +389,7 @@ piecewise.
## Notes
- Use `@opencode-ai/core/effect-zod` for all Schema → Zod conversion.
- Use `@/util/effect-zod` for all Schema → Zod conversion.
- Prefer one canonical schema definition. Avoid maintaining parallel Zod and
Effect definitions for the same domain type.
- Keep the migration incremental. Converting the domain model first is more
@@ -1,67 +0,0 @@
# TUI Command Shim Removal
Problem:
- v1 keeps a deprecated `api.command` TUI plugin shim so older plugins do not fail during initialization
- v2 should expose only the keymap command API
- tests and fixtures should not encode legacy command behavior as expected behavior
## Remove Public Types
In `packages/plugin/src/tui.ts`, remove:
- `TuiCommand`
- `TuiCommandApi`
- `TuiPluginApi.command`
Keep `api.keymap` as the only TUI command registration and execution surface.
## Remove Runtime Shim
Delete `packages/opencode/src/cli/cmd/tui/plugin/command-shim.ts`.
In `packages/opencode/src/cli/cmd/tui/plugin/api.tsx`, remove:
- the `createCommandShim` import
- the `command: createCommandShim(...)` field from `createTuiApi(...)`
In `packages/opencode/src/cli/cmd/tui/plugin/runtime.ts`, remove:
- the `createCommandShim` import
- the `command: createCommandShim(...)` field from `pluginApi(...)`
## Migration Target
Plugin authors should replace old calls with keymap calls:
```ts
api.keymap.registerLayer({
commands: [
{
name: "plugin.command",
title: "Plugin Command",
namespace: "palette",
slashName: "plugin",
run() {
api.ui.dialog.clear()
},
},
],
bindings: [{ key: "ctrl+shift+p", cmd: "plugin.command" }],
})
```
Direct replacements:
- `api.command.register(cb)` -> `api.keymap.registerLayer({ commands, bindings })`
- `api.command.trigger(name)` -> `api.keymap.dispatchCommand(name)`
- `api.command.show()` -> `api.keymap.dispatchCommand("command.palette.show")`
- `onSelect(dialog)` -> use `api.ui.dialog` from the plugin API closure
## Verification
After removal, run from package directories:
- `bun typecheck` in `packages/plugin`
- `bun typecheck` in `packages/opencode`
- TUI plugin loader tests in `packages/opencode` if runtime plugin API wiring changed
+2 -2
View File
@@ -24,8 +24,8 @@ import { Effect, Context, Layer, Schema } from "effect"
import { InstanceState } from "@/effect/instance-state"
import * as Option from "effect/Option"
import * as OtelTracer from "@effect/opentelemetry/Tracer"
import { zod } from "@opencode-ai/core/effect-zod"
import { withStatics, type DeepMutable } from "@opencode-ai/core/schema"
import { zod } from "@/util/effect-zod"
import { withStatics, type DeepMutable } from "@/util/schema"
type ReferenceEntry = NonNullable<Config.Info["reference"]>[string]
type ResolvedReference = { kind: "git"; repository: string; branch?: string } | { kind: "local"; path: string }
+2 -2
View File
@@ -1,7 +1,7 @@
import path from "path"
import { Effect, Layer, Record, Result, Schema, Context } from "effect"
import { zod } from "@opencode-ai/core/effect-zod"
import { NonNegativeInt } from "@opencode-ai/core/schema"
import { zod } from "@/util/effect-zod"
import { NonNegativeInt } from "@/util/schema"
import { Global } from "@opencode-ai/core/global"
import { AppFileSystem } from "@opencode-ai/core/filesystem"
+3 -3
View File
@@ -23,11 +23,11 @@ function span(id: string, value: { value: string; start: number; end: number })
}
}
function diff(kind: string, diffs: { file?: string; patch?: string }[] | undefined) {
function diff(kind: string, diffs: { file: string; patch: string }[] | undefined) {
return diffs?.map((item, i) => ({
...item,
file: item.file === undefined ? undefined : redact(`${kind}-file`, String(i), item.file),
patch: item.patch === undefined ? undefined : redact(`${kind}-patch`, String(i), item.patch),
file: redact(`${kind}-file`, String(i), item.file),
patch: redact(`${kind}-patch`, String(i), item.patch),
}))
}
@@ -1,7 +1,14 @@
import { DialogSelect, type DialogSelectOption } from "@tui/ui/dialog-select"
import { createResource, createMemo } from "solid-js"
import { createResource, createMemo, For } from "solid-js"
import { useDialog } from "@tui/ui/dialog"
import { useSDK } from "@tui/context/sdk"
import { useTheme } from "@tui/context/theme"
import { TextAttributes } from "@opentui/core"
import { useKeyboard } from "@opentui/solid"
import { createStore } from "solid-js/store"
import { Locale } from "@/util/locale"
import { getScrollAcceleration } from "../util/scroll"
import { useTuiConfig } from "../context/tui-config"
import path from "path"
export type DialogSkillProps = {
onSelect: (skill: string) => void
@@ -10,27 +17,226 @@ export type DialogSkillProps = {
export function DialogSkill(props: DialogSkillProps) {
const dialog = useDialog()
const sdk = useSDK()
const { theme } = useTheme()
const tuiConfig = useTuiConfig()
const scrollAcceleration = createMemo(() => getScrollAcceleration(tuiConfig))
const [store, setStore] = createStore({ selected: 0 })
dialog.setSize("large")
const [skills] = createResource(async () => {
const result = await sdk.client.app.skills()
return result.data ?? []
const result = await sdk.client.app.skills({ include: "invalid" })
return Array.isArray(result.data) ? { skills: result.data, invalid: [] } : (result.data ?? { skills: [], invalid: [] })
})
const options = createMemo<DialogSelectOption<string>[]>(() => {
const list = skills() ?? []
const maxWidth = Math.max(0, ...list.map((s) => s.name.length))
return list.map((skill) => ({
title: skill.name.padEnd(maxWidth),
description: skill.description?.replace(/\s+/g, " ").trim(),
value: skill.name,
category: "Skills",
onSelect: () => {
props.onSelect(skill.name)
dialog.clear()
},
}))
const rows = createMemo(() => {
const list = skills() ?? { skills: [], invalid: [] }
const maxWidth = Math.max(
16,
...list.skills.map((s) => s.name.length),
...list.invalid.map((s) => path.basename(path.dirname(s.path)).length),
)
const groups = Map.groupBy(
list.skills.toSorted((a, b) => sourceSort(a.location).localeCompare(sourceSort(b.location)) || a.name.localeCompare(b.name)),
(skill) => sourceRoot(skill.location),
)
return [
...Array.from(groups).flatMap(([root, group]) =>
[
{ type: "header" as const, id: root, root, count: group.length },
...group.map((skill) => ({
type: "skill" as const,
id: skill.name,
name: skill.name.padEnd(maxWidth),
rawName: skill.name,
description: skill.description?.replace(/\s+/g, " ").trim() ?? "No description",
})),
],
),
...(list.invalid.length > 0
? [
{ type: "error-header" as const, id: "errors", count: list.invalid.length },
...list.invalid.map((skill) => ({
type: "error" as const,
id: skill.path,
name: path.basename(path.dirname(skill.path)).padEnd(maxWidth),
reason: skill.reason,
message: skill.message,
location: compactLocation(skill.path),
})),
]
: []),
]
})
return <DialogSelect title="Skills" placeholder="Search skills..." options={options()} />
const selectable = createMemo(() => rows().filter((row) => row.type === "skill" || row.type === "error"))
const height = createMemo(() =>
Math.min(
18,
rows().reduce((total, row) => total + (row.type === "error" ? 2 : 1), 0),
),
)
function move(offset: number) {
if (selectable().length === 0) return
const next = store.selected + offset
setStore("selected", next < 0 ? selectable().length - 1 : next >= selectable().length ? 0 : next)
}
function select() {
const row = selectable()[store.selected]
if (!row || row.type !== "skill") return
props.onSelect(row.rawName)
dialog.clear()
}
useKeyboard((evt) => {
if (evt.name === "up") {
evt.preventDefault()
evt.stopPropagation()
move(-1)
return
}
if (evt.name === "down") {
evt.preventDefault()
evt.stopPropagation()
move(1)
return
}
if (evt.name === "return") {
evt.preventDefault()
evt.stopPropagation()
select()
}
})
const title = createMemo(() => {
const list = skills() ?? { skills: [], invalid: [] }
return list.invalid.length > 0 ? `Skills (${list.skills.length}) ${list.invalid.length} skipped` : `Skills (${list.skills.length})`
})
return (
<box border={true} borderColor={theme.accent} paddingTop={1} paddingBottom={1} gap={1}>
<box flexDirection="row" justifyContent="space-between" paddingLeft={3} paddingRight={3}>
<text fg={theme.accent} attributes={TextAttributes.BOLD}>{title()}</text>
<text>
<span style={{ fg: theme.textMuted }}>(</span>
<span style={{ fg: theme.accent }}>enter</span>
<span style={{ fg: theme.textMuted }}>) invoke Escape to close</span>
</text>
</box>
<scrollbox
height={height()}
paddingLeft={2}
paddingRight={2}
scrollbarOptions={{ visible: false }}
scrollAcceleration={scrollAcceleration()}
>
<For each={rows()}>
{(row) => {
if (row.type === "header") return <SourceRow root={row.root} count={row.count} />
if (row.type === "error-header") return <ErrorHeader count={row.count} />
const selected = createMemo(() => selectable()[store.selected]?.id === row.id)
if (row.type === "error") {
return (
<box
flexDirection="column"
paddingLeft={selected() ? 2 : 3}
paddingRight={2}
backgroundColor={selected() ? theme.backgroundElement : undefined}
onMouseUp={() => setStore("selected", selectable().findIndex((item) => item.id === row.id))}
>
<text>
<span style={{ fg: theme.error }}>! </span>
<span style={{ fg: selected() ? theme.text : theme.accent, attributes: selected() ? TextAttributes.BOLD : undefined }}>
{row.name}
</span>
<span style={{ fg: theme.textMuted }}> {row.reason}: </span>
<span style={{ fg: theme.text }}>{Locale.truncate(row.message, 46)}</span>
</text>
<text fg={theme.textMuted}> {Locale.truncateLeft(row.location, 66)}</text>
</box>
)
}
return (
<box
flexDirection="row"
paddingLeft={3}
paddingRight={2}
backgroundColor={selected() ? theme.backgroundElement : undefined}
onMouseUp={() => {
setStore("selected", selectable().findIndex((item) => item.id === row.id))
props.onSelect(row.rawName)
dialog.clear()
}}
>
<text flexGrow={1} wrapMode="none">
<span style={{ fg: selected() ? theme.selectedListItemText : theme.warning, attributes: TextAttributes.BOLD }}>
{row.name}
</span>
<span style={{ fg: selected() ? theme.text : theme.textMuted }}>
{Locale.truncate(row.description, 46)}
</span>
</text>
</box>
)
}}
</For>
</scrollbox>
</box>
)
}
function compactLocation(location: string) {
const home = process.env.HOME
if (!home) return location
return location.startsWith(home) ? `~${location.slice(home.length)}` : location
}
function sourceSort(location: string) {
const label = sourceLabel(location)
const rank = label === "Global" ? 0 : label === "Project" ? 1 : label === "User" ? 2 : label === "Registry" ? 3 : 4
return `${rank}:${sourceRoot(location)}`
}
function sourceLabel(location: string) {
return sourceRootLabel(sourceRoot(location))
}
function sourceRootLabel(root: string) {
const compact = compactLocation(root)
if (compact.startsWith("~/.agents/") || compact.startsWith("~/.claude/")) return "Global"
if (compact.includes("/.opencode/cache/skills")) return "Registry"
if (root.startsWith(process.cwd())) return "Project"
if (compact.startsWith("~/")) return "User"
return "Project"
}
function sourceRoot(location: string) {
return path.dirname(path.dirname(location))
}
function SourceRow(props: { root: string; count: number }) {
const { theme } = useTheme()
return (
<box paddingLeft={2}>
<text>
<span style={{ fg: theme.accent, attributes: TextAttributes.BOLD }}>{sourceRootLabel(props.root)} </span>
<span style={{ fg: theme.textMuted }}>{compactLocation(props.root)}/</span>
<span style={{ fg: theme.textMuted }}> ({props.count})</span>
</text>
</box>
)
}
function ErrorHeader(props: { count: number }) {
const { theme } = useTheme()
return (
<box paddingLeft={2}>
<text>
<span style={{ fg: theme.error, attributes: TextAttributes.BOLD }}>Skipped skills with errors </span>
<span style={{ fg: theme.textMuted }}>({props.count})</span>
</text>
</box>
)
}
@@ -1,34 +0,0 @@
/**
* Aggregate Promise.allSettled results into a single Error that names every
* failed endpoint, or return null when all fulfilled. Used at TUI bootstrap
* boundaries so a single 4xx doesn't drown its parallel siblings as
* unhandled rejections — every failure surfaces in one labeled message.
*/
export type LabeledSettled = {
name: string
result: PromiseSettledResult<unknown>
}
export function aggregateFailures(labeled: LabeledSettled[]): Error | null {
const failed = labeled.filter(
(x): x is { name: string; result: PromiseRejectedResult } => x.result.status === "rejected",
)
if (failed.length === 0) return null
const reasons = failed.map((f) => `${f.name}: ${reasonMessage(f.result.reason)}`).join("; ")
const summary = `${failed.length} of ${labeled.length} requests failed: ${reasons}`
const err = new Error(summary)
err.cause = { failures: failed.map((f) => ({ name: f.name, reason: f.result.reason })) }
return err
}
function reasonMessage(reason: unknown): string {
if (reason instanceof Error) return reason.message
if (typeof reason === "string") return reason
if (reason && typeof reason === "object") {
const obj = reason as { message?: unknown; name?: unknown }
if (typeof obj.message === "string") return obj.message
if (typeof obj.name === "string") return obj.name
}
return String(reason)
}
@@ -32,7 +32,6 @@ import * as Log from "@opencode-ai/core/util/log"
import { emptyConsoleState, type ConsoleState } from "@/config/console-state"
import path from "path"
import { useKV } from "./kv"
import { aggregateFailures } from "./aggregate-failures"
export const { use: useSync, provider: SyncProvider } = createSimpleContext({
name: "Sync",
@@ -392,23 +391,16 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
.catch(() => emptyConsoleState)
const agentsPromise = sdk.client.app.agents({ workspace }, { throwOnError: true })
const configPromise = sdk.client.config.get({ workspace }, { throwOnError: true })
const blockingRequests: { name: string; promise: Promise<unknown> }[] = [
{ name: "config.providers", promise: providersPromise },
{ name: "provider.list", promise: providerListPromise },
{ name: "app.agents", promise: agentsPromise },
{ name: "config.get", promise: configPromise },
{ name: "project.sync", promise: projectPromise },
...(args.continue ? [{ name: "session.list", promise: sessionListPromise }] : []),
const blockingRequests: Promise<unknown>[] = [
providersPromise,
providerListPromise,
agentsPromise,
configPromise,
projectPromise,
...(args.continue ? [sessionListPromise] : []),
]
await Promise.allSettled(blockingRequests.map((r) => r.promise))
.then((settled) => {
// Surface every failed endpoint in one labeled message instead of
// letting the first rejection drown its siblings as unhandled
// rejections.
const failure = aggregateFailures(blockingRequests.map((r, i) => ({ name: r.name, result: settled[i] })))
if (failure) throw failure
})
await Promise.all(blockingRequests)
.then(async () => {
const providersResponse = providersPromise.then((x) => x.data!)
const providerListResponse = providerListPromise.then((x) => x.data!)
@@ -534,12 +526,10 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
if (match.found) draft.session[match.index] = session.data!
if (!match.found) draft.session.splice(match.index, 0, session.data!)
draft.todo[sessionID] = todo.data ?? []
const infos: (typeof draft.message)[string] = []
for (const message of messages.data ?? []) {
infos.push(message.info)
draft.message[sessionID] = messages.data!.map((x) => x.info)
for (const message of messages.data!) {
draft.part[message.info.id] = message.parts
}
draft.message[sessionID] = infos
draft.session_diff[sessionID] = diff.data ?? []
}),
)
+1 -1
View File
@@ -1,6 +1,6 @@
import { BusEvent } from "@/bus/bus-event"
import { SessionID } from "@/session/schema"
import { PositiveInt } from "@opencode-ai/core/schema"
import { PositiveInt } from "@/util/schema"
import { Effect, Schema } from "effect"
const DEFAULT_TOAST_DURATION = 5000
@@ -17,7 +17,6 @@ import { Slot as HostSlot } from "./slots"
import type { useToast } from "../ui/toast"
import { InstallationVersion } from "@opencode-ai/core/installation/version"
import * as Keymap from "../keymap"
import { createCommandShim } from "./command-shim"
type RouteEntry = {
key: symbol
@@ -148,9 +147,7 @@ function stateApi(sync: ReturnType<typeof useSync>): TuiPluginApi["state"] {
return sync.data.session.length
},
diff(sessionID) {
return (sync.data.session_diff[sessionID] ?? []).flatMap((item) =>
item.file === undefined ? [] : [{ ...item, file: item.file }],
)
return sync.data.session_diff[sessionID] ?? []
},
todo(sessionID) {
return sync.data.todo[sessionID] ?? []
@@ -203,8 +200,6 @@ export function createTuiApi(input: Input): TuiPluginApi {
}
return {
app: appApi(),
// Keep deprecated `api.command` working for v1 plugins; remove in v2.
command: createCommandShim(input.keymap, input.dialog, input.tuiConfig.keybinds),
keys: {
formatSequence(parts) {
return Keymap.formatKeySequence(parts, input.tuiConfig)
@@ -1,109 +0,0 @@
// Legacy `api.command` bridge for v1 plugins; remove in v2.
import type { TuiCommand, TuiPluginApi } from "@opencode-ai/plugin/tui"
import { TuiKeybind } from "../config/keybind"
import type { DialogContext } from "../ui/dialog"
const COMMAND_PALETTE_SHOW = "command.palette.show"
const warned = new Set<string>()
type Warn = (api: string, replacement: string) => void
type LegacyDialog = TuiPluginApi["ui"]["dialog"]
type CommandShimDialog = DialogContext | LegacyDialog
type LegacyKeybinds = TuiPluginApi["tuiConfig"]["keybinds"]
function warnCommandShim(api: string, replacement: string) {
// Warn v1 plugins about deprecated `api.command`; remove this shim path in v2.
console.warn("[tui.plugin] deprecated TUI plugin API", { api, replacement })
}
function createCommandShimDialog(dialog: CommandShimDialog): LegacyDialog {
if (!("stack" in dialog)) return dialog
return {
replace(render, onClose) {
dialog.replace(render, onClose)
},
clear() {
dialog.clear()
},
setSize(size) {
dialog.setSize(size)
},
get size() {
return dialog.size
},
get depth() {
return dialog.stack.length
},
get open() {
return dialog.stack.length > 0
},
}
}
function warnOnce(api: string, replacement: string, warn: Warn) {
if (warned.has(api)) return
warned.add(api)
warn(api, replacement)
}
function toCommand(item: TuiCommand, dialog: LegacyDialog) {
return {
namespace: "palette",
name: item.value,
title: item.title,
desc: item.description,
category: item.category,
suggested: item.suggested,
hidden: item.hidden,
enabled: item.enabled,
slashName: item.slash?.name,
slashAliases: item.slash?.aliases,
run() {
return item.onSelect?.(dialog)
},
}
}
function toBindings(commands: TuiCommand[], keybinds: LegacyKeybinds) {
return commands.flatMap((item) =>
item.keybind
? keybinds.has(TuiKeybind.CommandMap[item.keybind as keyof typeof TuiKeybind.CommandMap] ?? item.keybind)
? keybinds
.get(TuiKeybind.CommandMap[item.keybind as keyof typeof TuiKeybind.CommandMap] ?? item.keybind)
.map((binding) => ({ ...binding, cmd: item.value, desc: binding.desc ?? item.title }))
: [
{
key: item.keybind,
cmd: item.value,
desc: item.title,
},
]
: [],
)
}
export function createCommandShim(
keymap: TuiPluginApi["keymap"],
dialog: CommandShimDialog,
keybinds: LegacyKeybinds,
): TuiPluginApi["command"] {
const shimDialog = createCommandShimDialog(dialog)
return {
register(cb) {
warnOnce("api.command.register", "api.keymap.registerLayer({ commands, bindings })", warnCommandShim)
const commands = cb()
return keymap.registerLayer({
commands: commands.map((item) => toCommand(item, shimDialog)),
bindings: toBindings(commands, keybinds),
})
},
trigger(value) {
warnOnce("api.command.trigger", "api.keymap.dispatchCommand(name)", warnCommandShim)
keymap.dispatchCommand(value)
},
show() {
warnOnce("api.command.show", `api.keymap.dispatchCommand("${COMMAND_PALETTE_SHOW}")`, warnCommandShim)
keymap.dispatchCommand(COMMAND_PALETTE_SHOW)
},
}
}
@@ -39,7 +39,6 @@ import { INTERNAL_TUI_PLUGINS, type InternalTuiPlugin } from "./internal"
import { setupSlots, Slot as View } from "./slots"
import type { HostPluginApi, HostSlots } from "./slots"
import { ConfigPlugin } from "@/config/plugin"
import { createCommandShim } from "./command-shim"
ensureRuntimePluginSupport({ additional: keymapRuntimeModules })
@@ -577,8 +576,6 @@ function pluginApi(runtime: RuntimeState, plugin: PluginEntry, scope: PluginScop
return {
app: api.app,
// Keep deprecated `api.command` working for v1 plugins; remove in v2.
command: createCommandShim(keymap, api.ui.dialog, api.tuiConfig.keybinds),
keys: api.keys,
keymap,
route,
+2 -2
View File
@@ -5,8 +5,8 @@ import type { InstanceContext } from "@/project/instance"
import { SessionID, MessageID } from "@/session/schema"
import { Effect, Layer, Context, Schema } from "effect"
import z from "zod"
import { zod, ZodOverride } from "@opencode-ai/core/effect-zod"
import { withStatics } from "@opencode-ai/core/schema"
import { zod, ZodOverride } from "@/util/effect-zod"
import { withStatics } from "@/util/schema"
import { Config } from "@/config/config"
import { MCP } from "../mcp"
import { Skill } from "../skill"
+2 -2
View File
@@ -2,8 +2,8 @@ export * as ConfigAgent from "./agent"
import { Exit, Schema, SchemaGetter } from "effect"
import { Bus } from "@/bus"
import { zod } from "@opencode-ai/core/effect-zod"
import { PositiveInt, withStatics } from "@opencode-ai/core/schema"
import { zod } from "@/util/effect-zod"
import { PositiveInt, withStatics } from "@/util/schema"
import * as Log from "@opencode-ai/core/util/log"
import { NamedError } from "@opencode-ai/core/util/error"
import { Glob } from "@opencode-ai/core/util/glob"
+2 -2
View File
@@ -5,8 +5,8 @@ import { Schema } from "effect"
import { NamedError } from "@opencode-ai/core/util/error"
import { Glob } from "@opencode-ai/core/util/glob"
import { Bus } from "@/bus"
import { zod } from "@opencode-ai/core/effect-zod"
import { withStatics } from "@opencode-ai/core/schema"
import { zod } from "@/util/effect-zod"
import { withStatics } from "@/util/schema"
import { configEntryNameFromPath } from "./entry-name"
import { InvalidError } from "./error"
import * as ConfigMarkdown from "./markdown"
+3 -3
View File
@@ -22,8 +22,8 @@ import { InstanceState } from "@/effect/instance-state"
import { Context, Duration, Effect, Exit, Fiber, Layer, Option, Schema } from "effect"
import { EffectFlock } from "@opencode-ai/core/util/effect-flock"
import { containsPath } from "../project/instance-context"
import { zod } from "@opencode-ai/core/effect-zod"
import { NonNegativeInt, PositiveInt, withStatics, type DeepMutable } from "@opencode-ai/core/schema"
import { zod } from "@/util/effect-zod"
import { NonNegativeInt, PositiveInt, withStatics, type DeepMutable } from "@/util/schema"
import { ConfigAgent } from "./agent"
import { ConfigCommand } from "./command"
import { ConfigFormatter } from "./formatter"
@@ -307,7 +307,7 @@ export const Info = Schema.Struct({
})),
)
// Uses the shared `DeepMutable` from `@opencode-ai/core/schema`. See the definition
// Uses the shared `DeepMutable` from `@/util/schema`. See the definition
// there for why the local variant is needed over `Types.DeepMutable` from
// effect-smol (the upstream version collapses `unknown` to `{}`).
export type Info = DeepMutable<Schema.Schema.Type<typeof Info>> & {
@@ -1,6 +1,6 @@
import { Schema } from "effect"
import { zod } from "@opencode-ai/core/effect-zod"
import { NonNegativeInt } from "@opencode-ai/core/schema"
import { zod } from "@/util/effect-zod"
import { NonNegativeInt } from "@/util/schema"
export class ConsoleState extends Schema.Class<ConsoleState>("ConsoleState")({
consoleManagedProviders: Schema.mutable(Schema.Array(Schema.String)),
+2 -2
View File
@@ -1,8 +1,8 @@
export * as ConfigFormatter from "./formatter"
import { Schema } from "effect"
import { zod } from "@opencode-ai/core/effect-zod"
import { withStatics } from "@opencode-ai/core/schema"
import { zod } from "@/util/effect-zod"
import { withStatics } from "@/util/schema"
export const Entry = Schema.Struct({
disabled: Schema.optional(Schema.Boolean),
+2 -2
View File
@@ -1,6 +1,6 @@
import { Schema } from "effect"
import { zod } from "@opencode-ai/core/effect-zod"
import { withStatics } from "@opencode-ai/core/schema"
import { zod } from "@/util/effect-zod"
import { withStatics } from "@/util/schema"
export const Layout = Schema.Literals(["auto", "stretch"])
.annotate({ identifier: "LayoutConfig" })
+2 -2
View File
@@ -1,8 +1,8 @@
export * as ConfigLSP from "./lsp"
import { Schema } from "effect"
import { zod } from "@opencode-ai/core/effect-zod"
import { withStatics } from "@opencode-ai/core/schema"
import { zod } from "@/util/effect-zod"
import { withStatics } from "@/util/schema"
import * as LSPServer from "../lsp/server"
export const Disabled = Schema.Struct({
+2 -2
View File
@@ -1,6 +1,6 @@
import { Schema } from "effect"
import { zod } from "@opencode-ai/core/effect-zod"
import { PositiveInt, withStatics } from "@opencode-ai/core/schema"
import { zod } from "@/util/effect-zod"
import { PositiveInt, withStatics } from "@/util/schema"
export const Local = Schema.Struct({
type: Schema.Literal("local").annotate({ description: "Type of MCP server connection" }),
+2 -2
View File
@@ -1,7 +1,7 @@
import { Schema } from "effect"
import z from "zod"
import { zod, ZodOverride } from "@opencode-ai/core/effect-zod"
import { withStatics } from "@opencode-ai/core/schema"
import { zod, ZodOverride } from "@/util/effect-zod"
import { withStatics } from "@/util/schema"
// The original Zod schema carried an external $ref pointing at the models.dev
// JSON schema. That external reference is not a named SDK component — it is a
+1 -1
View File
@@ -3,7 +3,7 @@ export * as ConfigParse from "./parse"
import { type ParseError as JsoncParseError, parse as parseJsoncImpl, printParseErrorCode } from "jsonc-parser"
import { Cause, Exit, Schema as EffectSchema, SchemaIssue } from "effect"
import z from "zod"
import type { DeepMutable } from "@opencode-ai/core/schema"
import type { DeepMutable } from "@/util/schema"
import { InvalidError, JsonError } from "./error"
type ZodSchema<T> = z.ZodType<T>
+2 -2
View File
@@ -1,7 +1,7 @@
export * as ConfigPermission from "./permission"
import { Schema, SchemaGetter } from "effect"
import { zod } from "@opencode-ai/core/effect-zod"
import { withStatics } from "@opencode-ai/core/schema"
import { zod } from "@/util/effect-zod"
import { withStatics } from "@/util/schema"
export const Action = Schema.Literals(["ask", "allow", "deny"])
.annotate({ identifier: "PermissionActionConfig" })
+2 -2
View File
@@ -2,8 +2,8 @@ import { Glob } from "@opencode-ai/core/util/glob"
import { Schema } from "effect"
import { pathToFileURL } from "url"
import { isPathPluginSpec, parsePluginSpecifier, resolvePathPluginTarget } from "@/plugin/shared"
import { zod } from "@opencode-ai/core/effect-zod"
import { withStatics } from "@opencode-ai/core/schema"
import { zod } from "@/util/effect-zod"
import { withStatics } from "@/util/schema"
import path from "path"
export const Options = Schema.Record(Schema.String, Schema.Unknown).pipe(withStatics((s) => ({ zod: zod(s) })))
+2 -2
View File
@@ -1,6 +1,6 @@
import { Schema } from "effect"
import { zod } from "@opencode-ai/core/effect-zod"
import { PositiveInt, withStatics } from "@opencode-ai/core/schema"
import { zod } from "@/util/effect-zod"
import { PositiveInt, withStatics } from "@/util/schema"
export const Model = Schema.Struct({
id: Schema.optional(Schema.String),
+2 -2
View File
@@ -1,8 +1,8 @@
export * as ConfigReference from "./reference"
import { Schema } from "effect"
import { zod } from "@opencode-ai/core/effect-zod"
import { withStatics } from "@opencode-ai/core/schema"
import { zod } from "@/util/effect-zod"
import { withStatics } from "@/util/schema"
const Git = Schema.Struct({
repository: Schema.String.annotate({
+2 -2
View File
@@ -1,6 +1,6 @@
import { Schema } from "effect"
import { zod } from "@opencode-ai/core/effect-zod"
import { PositiveInt, withStatics } from "@opencode-ai/core/schema"
import { zod } from "@/util/effect-zod"
import { PositiveInt, withStatics } from "@/util/schema"
export const Server = Schema.Struct({
port: Schema.optional(PositiveInt).annotate({
+2 -2
View File
@@ -1,6 +1,6 @@
import { Schema } from "effect"
import { zod } from "@opencode-ai/core/effect-zod"
import { withStatics } from "@opencode-ai/core/schema"
import { zod } from "@/util/effect-zod"
import { withStatics } from "@/util/schema"
export const Info = Schema.Struct({
paths: Schema.optional(Schema.Array(Schema.String)).annotate({
@@ -1,8 +1,8 @@
import { Schema } from "effect"
import { Identifier } from "@/id/id"
import { zod, ZodOverride } from "@opencode-ai/core/effect-zod"
import { withStatics } from "@opencode-ai/core/schema"
import { zod, ZodOverride } from "@/util/effect-zod"
import { withStatics } from "@/util/schema"
const workspaceIdSchema = Schema.String.annotate({ [ZodOverride]: Identifier.schema("workspace") }).pipe(
Schema.brand("WorkspaceID"),
+1 -1
View File
@@ -1,7 +1,7 @@
import { Schema, Struct } from "effect"
import { ProjectID } from "@/project/schema"
import { WorkspaceID } from "./schema"
import type { DeepMutable } from "@opencode-ai/core/schema"
import type { DeepMutable } from "@/util/schema"
export const WorkspaceInfo = Schema.Struct({
id: WorkspaceID,
+2 -2
View File
@@ -14,8 +14,8 @@ import { containsPath } from "../project/instance-context"
import * as Log from "@opencode-ai/core/util/log"
import { Protected } from "./protected"
import { Ripgrep } from "./ripgrep"
import { zod } from "@opencode-ai/core/effect-zod"
import { NonNegativeInt, type DeepMutable, withStatics } from "@opencode-ai/core/schema"
import { zod } from "@/util/effect-zod"
import { NonNegativeInt, type DeepMutable, withStatics } from "@/util/schema"
export const Info = Schema.Struct({
path: Schema.String,
+2 -2
View File
@@ -11,8 +11,8 @@ import { Global } from "@opencode-ai/core/global"
import * as Log from "@opencode-ai/core/util/log"
import { sanitizedProcessEnv } from "@opencode-ai/core/util/opencode-process"
import { which } from "@/util/which"
import { zod } from "@opencode-ai/core/effect-zod"
import { NonNegativeInt, withStatics } from "@opencode-ai/core/schema"
import { zod } from "@/util/effect-zod"
import { NonNegativeInt, withStatics } from "@/util/schema"
const log = Log.create({ service: "ripgrep" })
const VERSION = "15.1.0"
+2 -2
View File
@@ -7,8 +7,8 @@ import { mergeDeep } from "remeda"
import { Config } from "@/config/config"
import * as Log from "@opencode-ai/core/util/log"
import * as Formatter from "./formatter"
import { zod } from "@opencode-ai/core/effect-zod"
import { withStatics } from "@opencode-ai/core/schema"
import { zod } from "@/util/effect-zod"
import { withStatics } from "@/util/schema"
const log = Log.create({ service: "format" })
+2 -2
View File
@@ -13,8 +13,8 @@ import { spawn as lspspawn } from "./launch"
import { Effect, Layer, Context, Schema } from "effect"
import { InstanceState } from "@/effect/instance-state"
import { containsPath } from "@/project/instance-context"
import { NonNegativeInt, withStatics } from "@opencode-ai/core/schema"
import { zod, ZodOverride } from "@opencode-ai/core/effect-zod"
import { NonNegativeInt, withStatics } from "@/util/schema"
import { zod, ZodOverride } from "@/util/effect-zod"
const log = Log.create({ service: "lsp" })
+7 -47
View File
@@ -30,30 +30,12 @@ import { EffectBridge } from "@/effect/bridge"
import { InstanceState } from "@/effect/instance-state"
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
import { zod as effectZod } from "@opencode-ai/core/effect-zod"
import { withStatics } from "@opencode-ai/core/schema"
import { zod as effectZod } from "@/util/effect-zod"
import { withStatics } from "@/util/schema"
const log = Log.create({ service: "mcp" })
const DEFAULT_TIMEOUT = 30_000
const TolerantToolSchema = z.looseObject({
name: z.string(),
description: z.string().optional(),
inputSchema: z
.object({
type: z.literal("object"),
properties: z.record(z.string(), z.unknown()).optional(),
required: z.array(z.string()).optional(),
})
.catchall(z.unknown()),
outputSchema: z.unknown().optional(),
})
const TolerantListToolsResultSchema = z.looseObject({
tools: z.array(TolerantToolSchema),
nextCursor: z.string().optional(),
})
export const Resource = Schema.Struct({
name: Schema.String,
uri: Schema.String,
@@ -137,32 +119,6 @@ function remoteURL(key: string, value: string) {
log.warn("invalid remote mcp url", { key })
}
function isSchemaReferenceError(err: unknown) {
return err instanceof Error && /can't resolve reference|schema.*reference|reference.*schema/i.test(err.message)
}
const wrapAsError = (err: unknown) => (err instanceof Error ? err : new Error(String(err)))
function listToolsTolerant(key: string, client: MCPClient, timeout: number) {
return Effect.tryPromise({
try: () => client.listTools(undefined, { timeout }),
catch: wrapAsError,
}).pipe(
Effect.map((result) => result.tools),
Effect.catch((err) => {
if (!isSchemaReferenceError(err)) return Effect.fail(err)
log.warn("failed to validate MCP tool output schemas, retrying without output schema validation", {
key,
error: err,
})
return Effect.tryPromise({
try: () => client.request({ method: "tools/list" }, TolerantListToolsResultSchema, { timeout }),
catch: wrapAsError,
}).pipe(Effect.map((result) => result.tools as MCPToolDef[]))
}),
)
}
// Convert MCP tool definition to AI SDK Tool type
function convertMcpTool(mcpTool: MCPToolDef, client: MCPClient, timeout?: number): Tool {
const inputSchema = mcpTool.inputSchema
@@ -195,7 +151,11 @@ function convertMcpTool(mcpTool: MCPToolDef, client: MCPClient, timeout?: number
}
function defs(key: string, client: MCPClient, timeout?: number) {
return listToolsTolerant(key, client, timeout ?? DEFAULT_TIMEOUT).pipe(
return Effect.tryPromise({
try: () => withTimeout(client.listTools(), timeout ?? DEFAULT_TIMEOUT),
catch: (err) => (err instanceof Error ? err : new Error(String(err))),
}).pipe(
Effect.map((result) => result.tools),
Effect.catch((err) => {
log.error("failed to get tools from client", { key, error: err })
return Effect.succeed(undefined)
+2 -2
View File
@@ -7,9 +7,9 @@ import { MessageID, SessionID } from "@/session/schema"
import { PermissionTable } from "@/session/session.sql"
import { Database } from "@/storage/db"
import { eq } from "drizzle-orm"
import { zod } from "@opencode-ai/core/effect-zod"
import { zod } from "@/util/effect-zod"
import * as Log from "@opencode-ai/core/util/log"
import { withStatics } from "@opencode-ai/core/schema"
import { withStatics } from "@/util/schema"
import { Wildcard } from "@/util/wildcard"
import { Deferred, Effect, Layer, Schema, Context } from "effect"
import os from "os"
+2 -2
View File
@@ -1,8 +1,8 @@
import { Schema } from "effect"
import { Identifier } from "@/id/id"
import { zod, ZodOverride } from "@opencode-ai/core/effect-zod"
import { Newtype } from "@opencode-ai/core/schema"
import { zod, ZodOverride } from "@/util/effect-zod"
import { Newtype } from "@/util/schema"
export class PermissionID extends Newtype<PermissionID>()(
"PermissionID",
+2 -2
View File
@@ -18,8 +18,8 @@ import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"
import { NodePath } from "@effect/platform-node"
import { AppFileSystem } from "@opencode-ai/core/filesystem"
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
import { zod } from "@opencode-ai/core/effect-zod"
import { NonNegativeInt, optionalOmitUndefined, withStatics } from "@opencode-ai/core/schema"
import { zod } from "@/util/effect-zod"
import { NonNegativeInt, optionalOmitUndefined, withStatics } from "@/util/schema"
import { serviceUse } from "@/effect/service-use"
const log = Log.create({ service: "project" })
+2 -2
View File
@@ -1,7 +1,7 @@
import { Schema } from "effect"
import { zod } from "@opencode-ai/core/effect-zod"
import { withStatics } from "@opencode-ai/core/schema"
import { zod } from "@/util/effect-zod"
import { withStatics } from "@/util/schema"
const projectIdSchema = Schema.String.pipe(Schema.brand("ProjectID"))
+3 -6
View File
@@ -6,8 +6,8 @@ import { InstanceState } from "@/effect/instance-state"
import { FileWatcher } from "@/file/watcher"
import { Git } from "@/git"
import * as Log from "@opencode-ai/core/util/log"
import { zod, zodObject } from "@opencode-ai/core/effect-zod"
import { NonNegativeInt, withStatics } from "@opencode-ai/core/schema"
import { zod, zodObject } from "@/util/effect-zod"
import { NonNegativeInt, withStatics } from "@/util/schema"
const log = Log.create({ service: "vcs" })
const PATCH_CONTEXT_LINES = 2_147_483_647
@@ -230,10 +230,7 @@ export type Info = Schema.Schema.Type<typeof Info>
export const FileDiff = Schema.Struct({
file: Schema.String,
// Mirrors Snapshot.FileDiff (see #26574). The current producer always
// populates patch, but loosening matches the sibling schema so a
// future code path that omits it can't crash /instance/vcs/diff.
patch: Schema.optional(Schema.String),
patch: Schema.String,
additions: NonNegativeInt,
deletions: NonNegativeInt,
status: Schema.optional(Schema.Literals(["added", "deleted", "modified"])),
+2 -2
View File
@@ -1,9 +1,9 @@
import type { AuthOAuthResult, Hooks } from "@opencode-ai/plugin"
import { Auth } from "@/auth"
import { InstanceState } from "@/effect/instance-state"
import { zod } from "@opencode-ai/core/effect-zod"
import { zod } from "@/util/effect-zod"
import { namedSchemaError } from "@/util/named-schema-error"
import { optionalOmitUndefined, withStatics } from "@opencode-ai/core/schema"
import { optionalOmitUndefined, withStatics } from "@/util/schema"
import { Plugin } from "../plugin"
import { ProviderID } from "./schema"
import { Array as Arr, Effect, Layer, Record, Result, Context, Schema } from "effect"
+3 -3
View File
@@ -13,7 +13,7 @@ import { Auth } from "../auth"
import { Env } from "../env"
import { InstallationVersion } from "@opencode-ai/core/installation/version"
import { Flag } from "@opencode-ai/core/flag/flag"
import { zod } from "@opencode-ai/core/effect-zod"
import { zod } from "@/util/effect-zod"
import { namedSchemaError } from "@/util/named-schema-error"
import { iife } from "@/util/iife"
import { Global } from "@opencode-ai/core/global"
@@ -24,7 +24,7 @@ import { EffectBridge } from "@/effect/bridge"
import { InstanceState } from "@/effect/instance-state"
import { AppFileSystem } from "@opencode-ai/core/filesystem"
import { isRecord } from "@/util/record"
import { optionalOmitUndefined, withStatics } from "@opencode-ai/core/schema"
import { optionalOmitUndefined, withStatics } from "@/util/schema"
import * as ProviderTransform from "./transform"
import { ModelID, ProviderID } from "./schema"
@@ -1162,7 +1162,7 @@ const layer: Layer.Layer<
const pluginAuth = yield* auth.get(providerID).pipe(Effect.orDie)
provider.models = yield* Effect.promise(async () => {
const next = await models(toPublicInfo(provider), { auth: pluginAuth })
const next = await models(provider, { auth: pluginAuth })
return Object.fromEntries(
Object.entries(next).map(([id, model]) => [
id,
+2 -2
View File
@@ -1,7 +1,7 @@
import { Schema } from "effect"
import { zod } from "@opencode-ai/core/effect-zod"
import { withStatics } from "@opencode-ai/core/schema"
import { zod } from "@/util/effect-zod"
import { withStatics } from "@/util/schema"
const providerIdSchema = Schema.String.pipe(Schema.brand("ProviderID"))
+2 -2
View File
@@ -10,8 +10,8 @@ import type { Proc } from "#pty"
import * as Log from "@opencode-ai/core/util/log"
import { PtyID } from "./schema"
import { Effect, Layer, Context, Schema, Types } from "effect"
import { zod } from "@opencode-ai/core/effect-zod"
import { NonNegativeInt, PositiveInt, withStatics } from "@opencode-ai/core/schema"
import { zod } from "@/util/effect-zod"
import { NonNegativeInt, PositiveInt, withStatics } from "@/util/schema"
const log = Log.create({ service: "pty" })
+2 -2
View File
@@ -1,8 +1,8 @@
import { Schema } from "effect"
import { Identifier } from "@/id/id"
import { zod, ZodOverride } from "@opencode-ai/core/effect-zod"
import { withStatics } from "@opencode-ai/core/schema"
import { zod, ZodOverride } from "@/util/effect-zod"
import { withStatics } from "@/util/schema"
const ptyIdSchema = Schema.String.annotate({ [ZodOverride]: Identifier.schema("pty") }).pipe(Schema.brand("PtyID"))
+1 -1
View File
@@ -3,7 +3,7 @@ export * as PtyTicket from "./ticket"
import { WorkspaceID } from "@/control-plane/schema"
import { InstanceRef, WorkspaceRef } from "@/effect/instance-ref"
import { PtyID } from "@/pty/schema"
import { PositiveInt } from "@opencode-ai/core/schema"
import { PositiveInt } from "@/util/schema"
import { Cache, Context, Duration, Effect, Layer, Schema } from "effect"
const DEFAULT_TTL = Duration.seconds(60)
+2 -2
View File
@@ -3,9 +3,9 @@ import { Bus } from "@/bus"
import { BusEvent } from "@/bus/bus-event"
import { InstanceState } from "@/effect/instance-state"
import { SessionID, MessageID } from "@/session/schema"
import { zod } from "@opencode-ai/core/effect-zod"
import { zod } from "@/util/effect-zod"
import * as Log from "@opencode-ai/core/util/log"
import { withStatics } from "@opencode-ai/core/schema"
import { withStatics } from "@/util/schema"
import { QuestionID } from "./schema"
const log = Log.create({ service: "question" })
+2 -2
View File
@@ -1,8 +1,8 @@
import { Schema } from "effect"
import { Identifier } from "@/id/id"
import { zod, ZodOverride } from "@opencode-ai/core/effect-zod"
import { Newtype } from "@opencode-ai/core/schema"
import { zod, ZodOverride } from "@/util/effect-zod"
import { Newtype } from "@/util/schema"
export class QuestionID extends Newtype<QuestionID>()(
"QuestionID",
@@ -3,12 +3,12 @@ import { MCP } from "@/mcp"
import { ProviderID, ModelID } from "@/provider/schema"
import { Session } from "@/session/session"
import { Worktree } from "@/worktree"
import { NonNegativeInt } from "@opencode-ai/core/schema"
import { NonNegativeInt } from "@/util/schema"
import { Schema, SchemaGetter } from "effect"
import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
import { Authorization } from "../middleware/authorization"
import { InstanceContextMiddleware } from "../middleware/instance-context"
import { WorkspaceRoutingMiddleware, WorkspaceRoutingQueryFields } from "../middleware/workspace-routing"
import { WorkspaceRoutingMiddleware } from "../middleware/workspace-routing"
import { described } from "./metadata"
const ConsoleStateResponse = Schema.Struct({
@@ -43,7 +43,6 @@ const ToolListItem = Schema.Struct({
}).annotate({ identifier: "ToolListItem" })
const ToolList = Schema.Array(ToolListItem).annotate({ identifier: "ToolList" })
export const ToolListQuery = Schema.Struct({
...WorkspaceRoutingQueryFields,
provider: ProviderID,
model: ModelID,
})
@@ -56,7 +55,7 @@ const QueryBoolean = Schema.Literals(["true", "false"]).pipe(
)
const WorktreeList = Schema.Array(Schema.String)
export const SessionListQuery = Schema.Struct({
...WorkspaceRoutingQueryFields,
directory: Schema.optional(Schema.String),
roots: Schema.optional(QueryBoolean),
start: Schema.optional(Schema.NumberFromString),
cursor: Schema.optional(Schema.NumberFromString),
@@ -5,21 +5,18 @@ import { Schema } from "effect"
import { HttpApi, HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
import { Authorization } from "../middleware/authorization"
import { InstanceContextMiddleware } from "../middleware/instance-context"
import { WorkspaceRoutingMiddleware, WorkspaceRoutingQueryFields } from "../middleware/workspace-routing"
import { WorkspaceRoutingMiddleware } from "../middleware/workspace-routing"
import { described } from "./metadata"
export const FileQuery = Schema.Struct({
...WorkspaceRoutingQueryFields,
path: Schema.String,
})
export const FindTextQuery = Schema.Struct({
...WorkspaceRoutingQueryFields,
pattern: Schema.String,
})
export const FindFileQuery = Schema.Struct({
...WorkspaceRoutingQueryFields,
query: Schema.String,
dirs: Schema.optional(Schema.Literals(["true", "false"])),
type: Schema.optional(Schema.Literals(["file", "directory"])),
@@ -29,7 +26,6 @@ export const FindFileQuery = Schema.Struct({
})
export const FindSymbolQuery = Schema.Struct({
...WorkspaceRoutingQueryFields,
query: Schema.String,
})
@@ -8,7 +8,7 @@ import { Schema } from "effect"
import { HttpApi, HttpApiEndpoint, HttpApiGroup, HttpApiSchema, OpenApi } from "effect/unstable/httpapi"
import { Authorization } from "../middleware/authorization"
import { InstanceContextMiddleware } from "../middleware/instance-context"
import { WorkspaceRoutingMiddleware, WorkspaceRoutingQueryFields } from "../middleware/workspace-routing"
import { WorkspaceRoutingMiddleware } from "../middleware/workspace-routing"
import { described } from "./metadata"
const PathInfo = Schema.Struct({
@@ -20,10 +20,13 @@ const PathInfo = Schema.Struct({
}).annotate({ identifier: "Path" })
export const VcsDiffQuery = Schema.Struct({
...WorkspaceRoutingQueryFields,
mode: Vcs.Mode,
})
export const SkillQuery = Schema.Struct({
include: Schema.optional(Schema.Literals(["invalid"])),
})
export class ApiVcsApplyError extends Schema.ErrorClass<ApiVcsApplyError>("VcsApplyError")(
{
name: Schema.Literal("VcsApplyError"),
@@ -144,7 +147,8 @@ export const InstanceApi = HttpApi.make("instance")
}),
),
HttpApiEndpoint.get("skill", InstancePaths.skill, {
success: described(Schema.Array(Skill.Info), "List of skills"),
query: SkillQuery,
success: described(Schema.Union([Schema.Array(Skill.Info), Skill.ListWithInvalid]), "List of skills"),
}).annotateMerge(
OpenApi.annotations({
identifier: "app.skills",
@@ -5,16 +5,13 @@ import { Schema } from "effect"
import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
import { Authorization } from "../middleware/authorization"
import { InstanceContextMiddleware } from "../middleware/instance-context"
import { WorkspaceRoutingMiddleware, WorkspaceRoutingQueryFields } from "../middleware/workspace-routing"
import { WorkspaceRoutingMiddleware } from "../middleware/workspace-routing"
import { ApiNotFoundError } from "../errors"
import { described } from "./metadata"
const root = "/pty"
export const Params = Schema.Struct({ ptyID: PtyID })
export const CursorQuery = Schema.Struct({
...WorkspaceRoutingQueryFields,
cursor: Schema.optional(Schema.String),
})
export const CursorQuery = Schema.Struct({ cursor: Schema.optional(Schema.String) })
export const ShellItem = Schema.Struct({
path: Schema.String,
name: Schema.String,
@@ -14,7 +14,7 @@ import { Schema, SchemaGetter, Struct } from "effect"
import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, HttpApiSchema, OpenApi } from "effect/unstable/httpapi"
import { Authorization } from "../middleware/authorization"
import { InstanceContextMiddleware } from "../middleware/instance-context"
import { WorkspaceRoutingMiddleware, WorkspaceRoutingQueryFields } from "../middleware/workspace-routing"
import { WorkspaceRoutingMiddleware } from "../middleware/workspace-routing"
import { ApiNotFoundError } from "../errors"
import { described } from "./metadata"
@@ -26,7 +26,7 @@ const QueryBoolean = Schema.Literals(["true", "false"]).pipe(
}),
)
export const ListQuery = Schema.Struct({
...WorkspaceRoutingQueryFields,
directory: Schema.optional(Schema.String),
scope: Schema.optional(Schema.Literals(["project"])),
path: Schema.optional(Schema.String),
roots: Schema.optional(QueryBoolean),
@@ -34,12 +34,8 @@ export const ListQuery = Schema.Struct({
search: Schema.optional(Schema.String),
limit: Schema.optional(Schema.NumberFromString),
})
export const DiffQuery = Schema.Struct({
...WorkspaceRoutingQueryFields,
...Struct.omit(SessionSummary.DiffInput.fields, ["sessionID"]),
})
export const DiffQuery = Schema.Struct(Struct.omit(SessionSummary.DiffInput.fields, ["sessionID"]))
export const MessagesQuery = Schema.Struct({
...WorkspaceRoutingQueryFields,
limit: Schema.optional(Schema.NumberFromString.check(Schema.isInt(), Schema.isGreaterThanOrEqualTo(0))),
before: Schema.optional(Schema.String),
})
@@ -1,4 +1,4 @@
import { NonNegativeInt } from "@opencode-ai/core/schema"
import { NonNegativeInt } from "@/util/schema"
import { SessionID } from "@/session/schema"
import { Schema } from "effect"
import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
@@ -6,7 +6,7 @@ import { MCP } from "@/mcp"
import { Project } from "@/project/project"
import { Session } from "@/session/session"
import { ToolRegistry } from "@/tool/registry"
import * as EffectZod from "@opencode-ai/core/effect-zod"
import * as EffectZod from "@/util/effect-zod"
import { Worktree } from "@/worktree"
import { Effect, Option } from "effect"
import * as HttpServerResponse from "effect/unstable/http/HttpServerResponse"
@@ -9,7 +9,7 @@ import { Skill } from "@/skill"
import { Effect } from "effect"
import { HttpApiBuilder } from "effect/unstable/httpapi"
import { InstanceHttpApi } from "../api"
import { ApiVcsApplyError } from "../groups/instance"
import { ApiVcsApplyError, SkillQuery } from "../groups/instance"
import { markInstanceForDisposal } from "../lifecycle"
export const instanceHandlers = HttpApiBuilder.group(InstanceHttpApi, "instance", (handlers) =>
@@ -77,8 +77,15 @@ export const instanceHandlers = HttpApiBuilder.group(InstanceHttpApi, "instance"
return yield* agent.list()
})
const getSkill = Effect.fn("InstanceHttpApi.skill")(function* () {
return yield* skill.all()
const getSkill = Effect.fn("InstanceHttpApi.skill")(function* (ctx: { query: typeof SkillQuery.Type }) {
const skills = yield* skill.all()
if (ctx.query.include === "invalid") {
return {
skills,
invalid: yield* skill.invalid(),
}
}
return skills
})
const getLsp = Effect.fn("InstanceHttpApi.lsp")(function* () {
@@ -9,21 +9,11 @@ import * as Fence from "@/server/shared/fence"
import { getWorkspaceRouteSessionID, isLocalWorkspaceRoute, workspaceProxyURL } from "@/server/shared/workspace-routing"
import { NotFoundError } from "@/storage/storage"
import { Flag } from "@opencode-ai/core/flag/flag"
import { Context, Data, Effect, Layer, Schema } from "effect"
import { Context, Data, Effect, Layer } from "effect"
import { HttpClient, HttpRouter, HttpServerRequest, HttpServerResponse } from "effect/unstable/http"
import { HttpApiMiddleware } from "effect/unstable/httpapi"
import * as Socket from "effect/unstable/socket/Socket"
// Query fields this middleware reads from the URL. Spread into every
// endpoint query schema in groups that apply WorkspaceRoutingMiddleware,
// otherwise HttpApi rejects requests carrying these params with 400.
// HttpApiMiddleware in effect-smol cannot declare query params today —
// remove this once upstream supports middleware-declared query schemas.
export const WorkspaceRoutingQueryFields = {
directory: Schema.optional(Schema.String),
workspace: Schema.optional(Schema.String),
}
type RemoteTarget = Extract<Target, { type: "remote" }>
type RequestPlan = Data.TaggedEnum<{
+2 -2
View File
@@ -23,8 +23,8 @@ import type { SystemError } from "bun"
import type { Provider } from "@/provider/provider"
import { ModelID, ProviderID } from "@/provider/schema"
import { Effect, Schema, Types } from "effect"
import { zod, ZodOverride } from "@opencode-ai/core/effect-zod"
import { NonNegativeInt, withStatics } from "@opencode-ai/core/schema"
import { zod, ZodOverride } from "@/util/effect-zod"
import { NonNegativeInt, withStatics } from "@/util/schema"
import { namedSchemaError } from "@/util/named-schema-error"
import * as EffectLogger from "@opencode-ai/core/effect/logger"
+2 -2
View File
@@ -1,8 +1,8 @@
import { Schema } from "effect"
import { SessionID } from "./schema"
import { ModelID, ProviderID } from "../provider/schema"
import { zod } from "@opencode-ai/core/effect-zod"
import { NonNegativeInt, withStatics } from "@opencode-ai/core/schema"
import { zod } from "@/util/effect-zod"
import { NonNegativeInt, withStatics } from "@/util/schema"
import { namedSchemaError } from "@/util/named-schema-error"
export const OutputLengthError = namedSchemaError("MessageOutputLengthError", {})
+3 -3
View File
@@ -1,6 +1,6 @@
import path from "path"
import os from "os"
import * as EffectZod from "@opencode-ai/core/effect-zod"
import * as EffectZod from "@/util/effect-zod"
import { SessionID, MessageID, PartID } from "./schema"
import { MessageV2 } from "./message-v2"
import * as Log from "@opencode-ai/core/util/log"
@@ -46,8 +46,8 @@ import { Truncate } from "@/tool/truncate"
import { decodeDataUrl } from "@/util/data-url"
import { Process } from "@/util/process"
import { Cause, Effect, Exit, Latch, Layer, Option, Scope, Context, Schema, Types } from "effect"
import { zod } from "@opencode-ai/core/effect-zod"
import { withStatics } from "@opencode-ai/core/schema"
import { zod } from "@/util/effect-zod"
import { withStatics } from "@/util/schema"
import * as EffectLogger from "@opencode-ai/core/effect/logger"
import { InstanceState } from "@/effect/instance-state"
import { TaskTool, type TaskPromptOps } from "@/tool/task"
+2 -2
View File
@@ -4,8 +4,8 @@ import { Snapshot } from "../snapshot"
import { Storage } from "@/storage/storage"
import { SyncEvent } from "../sync"
import * as Log from "@opencode-ai/core/util/log"
import { zod } from "@opencode-ai/core/effect-zod"
import { withStatics } from "@opencode-ai/core/schema"
import { zod } from "@/util/effect-zod"
import { withStatics } from "@/util/schema"
import * as Session from "./session"
import { MessageV2 } from "./message-v2"
import { SessionID, MessageID, PartID } from "./schema"
+2 -2
View File
@@ -1,8 +1,8 @@
import { Schema } from "effect"
import { Identifier } from "@/id/id"
import { zod, ZodOverride } from "@opencode-ai/core/effect-zod"
import { withStatics } from "@opencode-ai/core/schema"
import { zod, ZodOverride } from "@/util/effect-zod"
import { withStatics } from "@/util/schema"
export const SessionID = Schema.String.annotate({ [ZodOverride]: Identifier.schema("session") }).pipe(
Schema.brand("SessionID"),
+2 -2
View File
@@ -37,8 +37,8 @@ import type { Provider } from "@/provider/provider"
import { Permission } from "@/permission"
import { Global } from "@opencode-ai/core/global"
import { Effect, Layer, Option, Context, Schema, Types } from "effect"
import { zod } from "@opencode-ai/core/effect-zod"
import { NonNegativeInt, optionalOmitUndefined, withStatics } from "@opencode-ai/core/schema"
import { zod } from "@/util/effect-zod"
import { NonNegativeInt, optionalOmitUndefined, withStatics } from "@/util/schema"
const log = Log.create({ service: "session" })
+2 -2
View File
@@ -2,8 +2,8 @@ import { BusEvent } from "@/bus/bus-event"
import { Bus } from "@/bus"
import { InstanceState } from "@/effect/instance-state"
import { SessionID } from "./schema"
import { zod } from "@opencode-ai/core/effect-zod"
import { NonNegativeInt, withStatics } from "@opencode-ai/core/schema"
import { zod } from "@/util/effect-zod"
import { NonNegativeInt, withStatics } from "@/util/schema"
import { Effect, Layer, Context, Schema } from "effect"
import z from "zod"
+2 -3
View File
@@ -2,8 +2,8 @@ import { Effect, Layer, Context, Schema } from "effect"
import { Bus } from "@/bus"
import { Snapshot } from "@/snapshot"
import { Storage } from "@/storage/storage"
import { zod } from "@opencode-ai/core/effect-zod"
import { withStatics } from "@opencode-ai/core/schema"
import { zod } from "@/util/effect-zod"
import { withStatics } from "@/util/schema"
import * as Session from "./session"
import { MessageV2 } from "./message-v2"
import { SessionID, MessageID } from "./schema"
@@ -134,7 +134,6 @@ export const layer = Layer.effect(
.read<Snapshot.FileDiff[]>(["session_diff", input.sessionID])
.pipe(Effect.catch(() => Effect.succeed([] as Snapshot.FileDiff[])))
const next = diffs.map((item) => {
if (item.file === undefined) return item
const file = unquoteGitPath(item.file)
if (file === item.file) return item
return { ...item, file }
+2 -2
View File
@@ -1,8 +1,8 @@
import { BusEvent } from "@/bus/bus-event"
import { Bus } from "@/bus"
import { SessionID } from "./schema"
import { zod } from "@opencode-ai/core/effect-zod"
import { withStatics } from "@opencode-ai/core/schema"
import { zod } from "@/util/effect-zod"
import { withStatics } from "@/util/schema"
import { Effect, Layer, Context, Schema } from "effect"
import z from "zod"
import { Database } from "@/storage/db"
+67 -7
View File
@@ -2,8 +2,8 @@ import path from "path"
import { pathToFileURL } from "url"
import z from "zod"
import { Effect, Layer, Context, Schema } from "effect"
import { zod } from "@opencode-ai/core/effect-zod"
import { withStatics } from "@opencode-ai/core/schema"
import { zod } from "@/util/effect-zod"
import { withStatics } from "@/util/schema"
import { NamedError } from "@opencode-ai/core/util/error"
import type { Agent } from "@/agent/agent"
import { Bus } from "@/bus"
@@ -33,6 +33,19 @@ export const Info = Schema.Struct({
}).pipe(withStatics((s) => ({ zod: zod(s) })))
export type Info = Schema.Schema.Type<typeof Info>
export const Invalid = Schema.Struct({
path: Schema.String,
reason: Schema.Literals(["parse", "frontmatter", "schema", "duplicate"]),
message: Schema.String,
}).pipe(withStatics((s) => ({ zod: zod(s) })))
export type Invalid = Schema.Schema.Type<typeof Invalid>
export const ListWithInvalid = Schema.Struct({
skills: Schema.Array(Info),
invalid: Schema.Array(Invalid),
}).pipe(withStatics((s) => ({ zod: zod(s) })))
export type ListWithInvalid = Schema.Schema.Type<typeof ListWithInvalid>
export const InvalidError = NamedError.create(
"SkillInvalidError",
z.object({
@@ -53,6 +66,7 @@ export const NameMismatchError = NamedError.create(
type State = {
skills: Record<string, Info>
invalid: Invalid[]
dirs: Set<string>
}
@@ -69,6 +83,7 @@ type ScanState = {
export interface Interface {
readonly get: (name: string) => Effect.Effect<Info | undefined>
readonly all: () => Effect.Effect<Info[]>
readonly invalid: () => Effect.Effect<Invalid[]>
readonly dirs: () => Effect.Effect<string[]>
readonly available: (agent?: Agent.Info) => Effect.Effect<Info[]>
}
@@ -80,9 +95,15 @@ const add = Effect.fnUntraced(function* (state: State, match: string, bus: Bus.I
}).pipe(
Effect.catch(
Effect.fnUntraced(function* (err) {
const message = ConfigMarkdown.FrontmatterError.isInstance(err)
const isFrontmatter = ConfigMarkdown.FrontmatterError.isInstance(err)
const message = isFrontmatter
? err.data.message
: `Failed to parse skill ${match}`
state.invalid.push({
path: match,
reason: isFrontmatter ? "frontmatter" : "parse",
message,
})
const { Session } = yield* Effect.promise(() => import("@/session/session"))
yield* bus.publish(Session.Event.Error, { error: new NamedError.Unknown({ message }).toObject() })
log.error("failed to load skill", { skill: match, err })
@@ -93,15 +114,50 @@ const add = Effect.fnUntraced(function* (state: State, match: string, bus: Bus.I
if (!md) return
const parsed = z.object({ name: z.string(), description: z.string().optional() }).safeParse(md.data)
if (!parsed.success) return
const parsed = z
.object({
name: z
.string()
.min(1, "Skill name is required")
.max(64, "Skill name must be at most 64 characters")
.regex(/^[a-z0-9-]+$/, "Skill name must contain only lowercase letters, numbers, and hyphens")
.regex(/^[^-].*[^-]$|^[^-]$/, "Skill name must not start or end with a hyphen")
.refine((value) => !value.includes("--"), "Skill name must not contain consecutive hyphens")
.refine((value) => !/(<[^>]*>)/.test(value), "Skill name must not contain XML tags")
.refine(
(value) => !value.includes("anthropic") && !value.includes("claude"),
'Skill name must not contain reserved words "anthropic" or "claude"',
)
.refine((value) => value === path.basename(path.dirname(match)), "Skill name must match parent directory name"),
description: z
.string({ error: "Skill description is required" })
.trim()
.min(1, "Skill description is required")
.max(1024, "Skill description must be at most 1024 characters")
.refine((value) => !/(<[^>]*>)/.test(value), "Skill description must not contain XML tags"),
})
.safeParse(md.data)
if (!parsed.success) {
state.invalid.push({
path: match,
reason: "schema",
message: parsed.error.issues.map((issue) => issue.message).join(", "),
})
return
}
if (state.skills[parsed.data.name]) {
state.invalid.push({
path: match,
reason: "duplicate",
message: `Duplicate skill name "${parsed.data.name}" already loaded from ${state.skills[parsed.data.name].location}`,
})
log.warn("duplicate skill name", {
name: parsed.data.name,
existing: state.skills[parsed.data.name].location,
duplicate: match,
})
return
}
state.dirs.add(path.dirname(match))
@@ -229,7 +285,7 @@ export const layer = Layer.effect(
)
const state = yield* InstanceState.make(
Effect.fn("Skill.state")(function* () {
const s: State = { skills: {}, dirs: new Set() }
const s: State = { skills: {}, invalid: [], dirs: new Set() }
yield* loadSkills(s, yield* InstanceState.get(discovered), bus)
return s
}),
@@ -245,6 +301,10 @@ export const layer = Layer.effect(
return Object.values(s.skills)
})
const invalid = Effect.fn("Skill.invalid")(function* () {
return (yield* InstanceState.get(state)).invalid
})
const dirs = Effect.fn("Skill.dirs")(function* () {
return (yield* InstanceState.get(discovered)).dirs
})
@@ -256,7 +316,7 @@ export const layer = Layer.effect(
return list.filter((skill) => Permission.evaluate("skill", skill.name, agent.permission).action !== "deny")
})
return Service.of({ get, all, dirs, available })
return Service.of({ get, all, invalid, dirs, available })
}),
)
+4 -7
View File
@@ -10,8 +10,8 @@ import { Hash } from "@opencode-ai/core/util/hash"
import { Config } from "@/config/config"
import { Global } from "@opencode-ai/core/global"
import * as Log from "@opencode-ai/core/util/log"
import { NonNegativeInt, withStatics } from "@opencode-ai/core/schema"
import { zod } from "@opencode-ai/core/effect-zod"
import { NonNegativeInt, withStatics } from "@/util/schema"
import { zod } from "@/util/effect-zod"
export const Patch = Schema.Struct({
hash: Schema.String,
@@ -20,11 +20,8 @@ export const Patch = Schema.Struct({
export type Patch = typeof Patch.Type
export const FileDiff = Schema.Struct({
// Optional because legacy/imported `summary_diffs` on disk may omit
// file details and patch text. Required Schema rejected the whole
// session response and broke session loading on Desktop.
file: Schema.optional(Schema.String),
patch: Schema.optional(Schema.String),
file: Schema.String,
patch: Schema.String,
additions: NonNegativeInt,
deletions: NonNegativeInt,
status: Schema.optional(Schema.Literals(["added", "deleted", "modified"])),
+1 -1
View File
@@ -5,7 +5,7 @@ import { NamedError } from "@opencode-ai/core/util/error"
import z from "zod"
import { AppFileSystem } from "@opencode-ai/core/filesystem"
import { Effect, Exit, Layer, Option, RcMap, Schema, Context, TxReentrantLock } from "effect"
import { NonNegativeInt } from "@opencode-ai/core/schema"
import { NonNegativeInt } from "@/util/schema"
import { Git } from "@/git"
const log = Log.create({ service: "storage" })
+1 -1
View File
@@ -9,7 +9,7 @@ import type { WorkspaceID } from "@/control-plane/schema"
import { EventID } from "./schema"
import { Flag } from "@opencode-ai/core/flag/flag"
import { Context, Effect, Layer, Schema as EffectSchema } from "effect"
import type { DeepMutable } from "@opencode-ai/core/schema"
import type { DeepMutable } from "@/util/schema"
import { makeRuntime } from "@/effect/run-service"
import { serviceUse } from "@/effect/service-use"
import { InstanceState } from "@/effect/instance-state"
+2 -2
View File
@@ -1,8 +1,8 @@
import { Schema } from "effect"
import { Identifier } from "@/id/id"
import { zod, ZodOverride } from "@opencode-ai/core/effect-zod"
import { withStatics } from "@opencode-ai/core/schema"
import { zod, ZodOverride } from "@/util/effect-zod"
import { withStatics } from "@/util/schema"
export const EventID = Schema.String.annotate({ [ZodOverride]: Identifier.schema("event") }).pipe(
Schema.brand("EventID"),
+2 -2
View File
@@ -1,5 +1,5 @@
import { Effect, Option, Schema, Scope } from "effect"
import { NonNegativeInt } from "@opencode-ai/core/schema"
import { NonNegativeInt } from "@/util/schema"
import { createReadStream } from "fs"
import * as path from "path"
import { createInterface } from "readline"
@@ -178,7 +178,7 @@ export const ReadTool = Tool.define(
yield* ctx.ask({
permission: "read",
patterns: [path.relative(instance.worktree, filepath)],
patterns: [filepath],
always: ["*"],
metadata: {},
})
+1 -1
View File
@@ -17,7 +17,7 @@ import { Config } from "@/config/config"
import { type ToolContext as PluginToolContext, type ToolDefinition } from "@opencode-ai/plugin"
import { Schema } from "effect"
import z from "zod"
import { ZodOverride } from "@opencode-ai/core/effect-zod"
import { ZodOverride } from "@/util/effect-zod"
import { Plugin } from "../plugin"
import { Provider } from "@/provider/provider"
import { ProviderID, type ModelID } from "../provider/schema"
+2 -2
View File
@@ -1,8 +1,8 @@
import { Schema } from "effect"
import { Identifier } from "@/id/id"
import { zod, ZodOverride } from "@opencode-ai/core/effect-zod"
import { withStatics } from "@opencode-ai/core/schema"
import { zod, ZodOverride } from "@/util/effect-zod"
import { withStatics } from "@/util/schema"
const toolIdSchema = Schema.String.annotate({ [ZodOverride]: Identifier.schema("tool") }).pipe(Schema.brand("ToolID"))
+1 -1
View File
@@ -1,6 +1,6 @@
import { Schema } from "effect"
import DESCRIPTION from "./shell.txt"
import { PositiveInt } from "@opencode-ai/core/schema"
import { PositiveInt } from "@/util/schema"
import { Global } from "@opencode-ai/core/global"
import { ShellID } from "./id"
@@ -1,6 +1,6 @@
import { Schema } from "effect"
import z from "zod"
import { zod } from "@opencode-ai/core/effect-zod"
import { zod } from "@/util/effect-zod"
/**
* Create a Schema-backed NamedError-shaped class.
+1 -1
View File
@@ -1,7 +1,7 @@
import path from "path"
import { Effect, Layer, Option, Schema, Context, SynchronizedRef } from "effect"
import { Identifier } from "@opencode-ai/core/util/identifier"
import { NonNegativeInt, withStatics } from "@opencode-ai/core/schema"
import { NonNegativeInt, withStatics } from "@/util/schema"
import { Global } from "@opencode-ai/core/global"
import { AppFileSystem } from "@opencode-ai/core/filesystem"
+1 -1
View File
@@ -1,6 +1,6 @@
import { Identifier } from "@/id/id"
import { SyncEvent } from "@/sync"
import { withStatics } from "@opencode-ai/core/schema"
import { withStatics } from "@/util/schema"
import { Flag } from "@opencode-ai/core/flag/flag"
import * as Schema from "effect/Schema"
+1 -1
View File
@@ -1,4 +1,4 @@
import { withStatics } from "@opencode-ai/core/schema"
import { withStatics } from "@/util/schema"
import { Array, Context, Effect, HashMap, Layer, Option, Order, pipe, Schema } from "effect"
import { DateTimeUtcFromMillis } from "effect/Schema"
+1 -1
View File
@@ -1,5 +1,5 @@
import { SessionID } from "@/session/schema"
import { NonNegativeInt } from "@opencode-ai/core/schema"
import { NonNegativeInt } from "@/util/schema"
import { EventV2 } from "./event"
import { FileAttachment, Prompt } from "./session-prompt"
import { Schema } from "effect"

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