mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-04 01:06:16 -04:00
Compare commits
38 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3d31ae28b0 | |||
| f8e939d96f | |||
| 923af96d26 | |||
| a882e958b3 | |||
| 2cda629c8d | |||
| f033d2d8fb | |||
| a4bd88ab97 | |||
| f4616c8268 | |||
| 4712f0f3c1 | |||
| 6c1268f3b1 | |||
| 2e156b8990 | |||
| 3bfe6a1ef6 | |||
| 5c5069b622 | |||
| f8c6ddd4cb | |||
| e50a688ca3 | |||
| 334ab4707c | |||
| 87321942fe | |||
| a771859362 | |||
| 31d01d404a | |||
| 814e83ffec | |||
| 4c3e65c877 | |||
| 98ea5b6e7e | |||
| 3f8c659056 | |||
| 3910a6e527 | |||
| 24892559ae | |||
| cd93533b1f | |||
| 0590452456 | |||
| 93940a1859 | |||
| 1e439b8226 | |||
| 8b2f8355b2 | |||
| aed03078f8 | |||
| c50d65b4d6 | |||
| 353532b1c1 | |||
| 9df7c78ebe | |||
| eb7555d3c6 | |||
| 2cd89d64e9 | |||
| 0517ab4695 | |||
| bbf67d0fff |
@@ -1,5 +1,10 @@
|
||||
name: "Setup Bun"
|
||||
description: "Setup Bun with caching and install dependencies"
|
||||
inputs:
|
||||
install-flags:
|
||||
description: "Additional flags to pass to 'bun install'"
|
||||
required: false
|
||||
default: ""
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
@@ -46,8 +51,8 @@ runs:
|
||||
# e.g. ./patches/ for standard-openapi
|
||||
# https://github.com/oven-sh/bun/issues/28147
|
||||
if [ "$RUNNER_OS" = "Windows" ]; then
|
||||
bun install --linker hoisted
|
||||
bun install --linker hoisted ${{ inputs.install-flags }}
|
||||
else
|
||||
bun install
|
||||
bun install ${{ inputs.install-flags }}
|
||||
fi
|
||||
shell: bash
|
||||
|
||||
@@ -402,12 +402,14 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
settings:
|
||||
- host: macos-latest
|
||||
- host: macos-26-intel
|
||||
target: x86_64-apple-darwin
|
||||
platform_flag: --mac --x64
|
||||
- host: macos-latest
|
||||
bun_install_flags: --os=darwin --cpu=x64
|
||||
- host: macos-26
|
||||
target: aarch64-apple-darwin
|
||||
platform_flag: --mac --arm64
|
||||
bun_install_flags: --os=darwin --cpu=arm64
|
||||
# github-hosted: blacksmith lacks ARM64 MSVC cross-compilation toolchain
|
||||
- host: "windows-2025"
|
||||
target: aarch64-pc-windows-msvc
|
||||
@@ -437,6 +439,8 @@ jobs:
|
||||
run: echo "${{ secrets.APPLE_API_KEY_PATH }}" > $RUNNER_TEMP/apple-api-key.p8
|
||||
|
||||
- uses: ./.github/actions/setup-bun
|
||||
with:
|
||||
install-flags: ${{ matrix.settings.bun_install_flags }}
|
||||
|
||||
- name: Azure login
|
||||
if: runner.os == 'Windows'
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
},
|
||||
"packages/app": {
|
||||
"name": "@opencode-ai/app",
|
||||
"version": "1.14.21",
|
||||
"version": "1.14.23",
|
||||
"dependencies": {
|
||||
"@kobalte/core": "catalog:",
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
@@ -83,7 +83,7 @@
|
||||
},
|
||||
"packages/console/app": {
|
||||
"name": "@opencode-ai/console-app",
|
||||
"version": "1.14.21",
|
||||
"version": "1.14.23",
|
||||
"dependencies": {
|
||||
"@cloudflare/vite-plugin": "1.15.2",
|
||||
"@ibm/plex": "6.4.1",
|
||||
@@ -117,7 +117,7 @@
|
||||
},
|
||||
"packages/console/core": {
|
||||
"name": "@opencode-ai/console-core",
|
||||
"version": "1.14.21",
|
||||
"version": "1.14.23",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-sts": "3.782.0",
|
||||
"@jsx-email/render": "1.1.1",
|
||||
@@ -144,7 +144,7 @@
|
||||
},
|
||||
"packages/console/function": {
|
||||
"name": "@opencode-ai/console-function",
|
||||
"version": "1.14.21",
|
||||
"version": "1.14.23",
|
||||
"dependencies": {
|
||||
"@ai-sdk/anthropic": "3.0.64",
|
||||
"@ai-sdk/openai": "3.0.48",
|
||||
@@ -168,7 +168,7 @@
|
||||
},
|
||||
"packages/console/mail": {
|
||||
"name": "@opencode-ai/console-mail",
|
||||
"version": "1.14.21",
|
||||
"version": "1.14.23",
|
||||
"dependencies": {
|
||||
"@jsx-email/all": "2.2.3",
|
||||
"@jsx-email/cli": "1.4.3",
|
||||
@@ -192,7 +192,7 @@
|
||||
},
|
||||
"packages/desktop": {
|
||||
"name": "@opencode-ai/desktop",
|
||||
"version": "1.14.21",
|
||||
"version": "1.14.23",
|
||||
"dependencies": {
|
||||
"@opencode-ai/app": "workspace:*",
|
||||
"@opencode-ai/ui": "workspace:*",
|
||||
@@ -225,7 +225,7 @@
|
||||
},
|
||||
"packages/desktop-electron": {
|
||||
"name": "@opencode-ai/desktop-electron",
|
||||
"version": "1.14.21",
|
||||
"version": "1.14.23",
|
||||
"dependencies": {
|
||||
"drizzle-orm": "catalog:",
|
||||
"effect": "catalog:",
|
||||
@@ -269,7 +269,7 @@
|
||||
},
|
||||
"packages/enterprise": {
|
||||
"name": "@opencode-ai/enterprise",
|
||||
"version": "1.14.21",
|
||||
"version": "1.14.23",
|
||||
"dependencies": {
|
||||
"@opencode-ai/shared": "workspace:*",
|
||||
"@opencode-ai/ui": "workspace:*",
|
||||
@@ -298,7 +298,7 @@
|
||||
},
|
||||
"packages/function": {
|
||||
"name": "@opencode-ai/function",
|
||||
"version": "1.14.21",
|
||||
"version": "1.14.23",
|
||||
"dependencies": {
|
||||
"@octokit/auth-app": "8.0.1",
|
||||
"@octokit/rest": "catalog:",
|
||||
@@ -314,7 +314,7 @@
|
||||
},
|
||||
"packages/opencode": {
|
||||
"name": "opencode",
|
||||
"version": "1.14.21",
|
||||
"version": "1.14.23",
|
||||
"bin": {
|
||||
"opencode": "./bin/opencode",
|
||||
},
|
||||
@@ -459,7 +459,7 @@
|
||||
},
|
||||
"packages/plugin": {
|
||||
"name": "@opencode-ai/plugin",
|
||||
"version": "1.14.21",
|
||||
"version": "1.14.23",
|
||||
"dependencies": {
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
"effect": "catalog:",
|
||||
@@ -494,7 +494,7 @@
|
||||
},
|
||||
"packages/sdk/js": {
|
||||
"name": "@opencode-ai/sdk",
|
||||
"version": "1.14.21",
|
||||
"version": "1.14.23",
|
||||
"dependencies": {
|
||||
"cross-spawn": "catalog:",
|
||||
},
|
||||
@@ -509,7 +509,7 @@
|
||||
},
|
||||
"packages/shared": {
|
||||
"name": "@opencode-ai/shared",
|
||||
"version": "1.14.21",
|
||||
"version": "1.14.23",
|
||||
"bin": {
|
||||
"opencode": "./bin/opencode",
|
||||
},
|
||||
@@ -533,7 +533,7 @@
|
||||
},
|
||||
"packages/slack": {
|
||||
"name": "@opencode-ai/slack",
|
||||
"version": "1.14.21",
|
||||
"version": "1.14.23",
|
||||
"dependencies": {
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
"@slack/bolt": "^3.17.1",
|
||||
@@ -568,7 +568,7 @@
|
||||
},
|
||||
"packages/ui": {
|
||||
"name": "@opencode-ai/ui",
|
||||
"version": "1.14.21",
|
||||
"version": "1.14.23",
|
||||
"dependencies": {
|
||||
"@kobalte/core": "catalog:",
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
@@ -617,7 +617,7 @@
|
||||
},
|
||||
"packages/web": {
|
||||
"name": "@opencode-ai/web",
|
||||
"version": "1.14.21",
|
||||
"version": "1.14.23",
|
||||
"dependencies": {
|
||||
"@astrojs/cloudflare": "12.6.3",
|
||||
"@astrojs/markdown-remark": "6.3.1",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/app",
|
||||
"version": "1.14.21",
|
||||
"version": "1.14.23",
|
||||
"description": "",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
|
||||
@@ -270,7 +270,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
const buttonsSpring = useSpring(() => (store.mode === "normal" ? 1 : 0), { visualDuration: 0.2, bounce: 0 })
|
||||
const motion = (value: number) => ({
|
||||
opacity: value,
|
||||
transform: `scale(${0.95 + value * 0.05})`,
|
||||
transform: `scale(${0.98 + value * 0.02})`,
|
||||
filter: `blur(${(1 - value) * 2}px)`,
|
||||
"pointer-events": value > 0.5 ? ("auto" as const) : ("none" as const),
|
||||
})
|
||||
@@ -345,7 +345,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
promptPlaceholder({
|
||||
mode: store.mode,
|
||||
commentCount: commentCount(),
|
||||
example: suggest() ? language.t(EXAMPLES[store.placeholder]) : "",
|
||||
example: suggest() ? (store.mode === "shell" ? "git status" : language.t(EXAMPLES[store.placeholder])) : "",
|
||||
suggest: suggest(),
|
||||
t: (key, params) => language.t(key as Parameters<typeof language.t>[0], params as never),
|
||||
}),
|
||||
@@ -1403,12 +1403,11 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
<IconButton
|
||||
data-action="prompt-submit"
|
||||
type="submit"
|
||||
disabled={store.mode !== "normal" || (!working() && blank())}
|
||||
disabled={!working() && blank()}
|
||||
tabIndex={store.mode === "normal" ? undefined : -1}
|
||||
icon={stopping() ? "stop" : "arrow-up"}
|
||||
icon={stopping() ? "stop" : store.mode === "shell" ? "arrow-undo-down" : "arrow-up"}
|
||||
variant="primary"
|
||||
class="size-8"
|
||||
style={buttons()}
|
||||
aria-label={stopping() ? language.t("prompt.action.stop") : language.t("prompt.action.send")}
|
||||
/>
|
||||
</Tooltip>
|
||||
@@ -1451,14 +1450,24 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
<div class="px-1.75 pt-5.5 pb-2 flex items-center gap-2 min-w-0">
|
||||
<div class="flex items-center gap-1.5 min-w-0 flex-1 relative">
|
||||
<div
|
||||
class="h-7 flex items-center gap-1.5 max-w-[160px] min-w-0 absolute inset-y-0 left-0"
|
||||
class="h-7 flex items-center gap-1.5 min-w-0 absolute inset-0"
|
||||
style={{
|
||||
padding: "0 4px 0 8px",
|
||||
padding: "0 0px 0 8px",
|
||||
...shell(),
|
||||
}}
|
||||
>
|
||||
<span class="truncate text-13-medium text-text-strong">{language.t("prompt.mode.shell")}</span>
|
||||
<div class="size-4 shrink-0" />
|
||||
<Icon name="console" />
|
||||
<span class="truncate text-13-medium text-text-base">{language.t("prompt.mode.shell")}</span>
|
||||
<div class="flex-1" />
|
||||
<Button
|
||||
variant="ghost"
|
||||
class="text-text-base"
|
||||
onClick={() => {
|
||||
setStore("mode", "normal")
|
||||
}}
|
||||
>
|
||||
{language.t("common.cancel")}
|
||||
</Button>
|
||||
</div>
|
||||
<div class="flex items-center gap-1.5 min-w-0 flex-1 h-7">
|
||||
<Show when={!agentsLoading()}>
|
||||
@@ -1565,33 +1574,35 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
</TooltipKeybind>
|
||||
</Show>
|
||||
</div>
|
||||
<div
|
||||
data-component="prompt-variant-control"
|
||||
style={providersShouldFadeIn() ? { animation: "fade-in 0.3s" } : undefined}
|
||||
>
|
||||
<TooltipKeybind
|
||||
placement="top"
|
||||
gutter={4}
|
||||
title={language.t("command.model.variant.cycle")}
|
||||
keybind={command.keybind("model.variant.cycle")}
|
||||
<Show when={variants().length > 2}>
|
||||
<div
|
||||
data-component="prompt-variant-control"
|
||||
style={providersShouldFadeIn() ? { animation: "fade-in 0.3s" } : undefined}
|
||||
>
|
||||
<Select
|
||||
size="normal"
|
||||
options={variants()}
|
||||
current={local.model.variant.current() ?? "default"}
|
||||
label={(x) => (x === "default" ? language.t("common.default") : x)}
|
||||
onSelect={(value) => {
|
||||
local.model.variant.set(value === "default" ? undefined : value)
|
||||
restoreFocus()
|
||||
}}
|
||||
class="capitalize max-w-[160px] text-text-base"
|
||||
valueClass="truncate text-13-regular text-text-base"
|
||||
triggerStyle={control()}
|
||||
triggerProps={{ "data-action": "prompt-model-variant" }}
|
||||
variant="ghost"
|
||||
/>
|
||||
</TooltipKeybind>
|
||||
</div>
|
||||
<TooltipKeybind
|
||||
placement="top"
|
||||
gutter={4}
|
||||
title={language.t("command.model.variant.cycle")}
|
||||
keybind={command.keybind("model.variant.cycle")}
|
||||
>
|
||||
<Select
|
||||
size="normal"
|
||||
options={variants()}
|
||||
current={local.model.variant.current() ?? "default"}
|
||||
label={(x) => (x === "default" ? language.t("common.default") : x)}
|
||||
onSelect={(value) => {
|
||||
local.model.variant.set(value === "default" ? undefined : value)
|
||||
restoreFocus()
|
||||
}}
|
||||
class="capitalize max-w-[160px] text-text-base"
|
||||
valueClass="truncate text-13-regular text-text-base"
|
||||
triggerStyle={control()}
|
||||
triggerProps={{ "data-action": "prompt-model-variant" }}
|
||||
variant="ghost"
|
||||
/>
|
||||
</TooltipKeybind>
|
||||
</div>
|
||||
</Show>
|
||||
</Show>
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
@@ -12,7 +12,7 @@ describe("promptPlaceholder", () => {
|
||||
suggest: true,
|
||||
t,
|
||||
})
|
||||
expect(value).toBe("prompt.placeholder.shell")
|
||||
expect(value).toBe("prompt.placeholder.shell:example")
|
||||
})
|
||||
|
||||
test("returns summarize placeholders for comment context", () => {
|
||||
|
||||
@@ -7,7 +7,7 @@ type PromptPlaceholderInput = {
|
||||
}
|
||||
|
||||
export function promptPlaceholder(input: PromptPlaceholderInput) {
|
||||
if (input.mode === "shell") return input.t("prompt.placeholder.shell")
|
||||
if (input.mode === "shell") return input.t("prompt.placeholder.shell", { example: input.example })
|
||||
if (input.commentCount > 1) return input.t("prompt.placeholder.summarizeComments")
|
||||
if (input.commentCount === 1) return input.t("prompt.placeholder.summarizeComment")
|
||||
if (!input.suggest) return input.t("prompt.placeholder.simple")
|
||||
|
||||
@@ -128,27 +128,25 @@ export const SettingsGeneral: Component = () => {
|
||||
return
|
||||
}
|
||||
|
||||
const actions =
|
||||
platform.update && platform.restart
|
||||
? [
|
||||
{
|
||||
label: language.t("toast.update.action.installRestart"),
|
||||
onClick: async () => {
|
||||
await platform.update!()
|
||||
await platform.restart!()
|
||||
},
|
||||
const actions = platform.updateAndRestart
|
||||
? [
|
||||
{
|
||||
label: language.t("toast.update.action.installRestart"),
|
||||
onClick: async () => {
|
||||
await platform.updateAndRestart!()
|
||||
},
|
||||
{
|
||||
label: language.t("toast.update.action.notYet"),
|
||||
onClick: "dismiss" as const,
|
||||
},
|
||||
]
|
||||
: [
|
||||
{
|
||||
label: language.t("toast.update.action.notYet"),
|
||||
onClick: "dismiss" as const,
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
label: language.t("toast.update.action.notYet"),
|
||||
onClick: "dismiss" as const,
|
||||
},
|
||||
]
|
||||
: [
|
||||
{
|
||||
label: language.t("toast.update.action.notYet"),
|
||||
onClick: "dismiss" as const,
|
||||
},
|
||||
]
|
||||
|
||||
showToast({
|
||||
persistent: true,
|
||||
|
||||
@@ -49,11 +49,11 @@ export type Platform = {
|
||||
/** Storage mechanism, defaults to localStorage */
|
||||
storage?: (name?: string) => SyncStorage | AsyncStorage
|
||||
|
||||
/** Check for updates (Tauri only) */
|
||||
/** Check for a downloadable desktop update */
|
||||
checkUpdate?(): Promise<UpdateInfo>
|
||||
|
||||
/** Install updates (Tauri only) */
|
||||
update?(): Promise<void>
|
||||
/** Install the downloaded update using the platform restart flow */
|
||||
updateAndRestart?(): Promise<void>
|
||||
|
||||
/** Fetch override */
|
||||
fetch?: typeof fetch
|
||||
|
||||
@@ -210,7 +210,7 @@ export const dict = {
|
||||
"common.saving": "جارٍ الحفظ...",
|
||||
"common.default": "افتراضي",
|
||||
"common.attachment": "مرفق",
|
||||
"prompt.placeholder.shell": "أدخل أمر shell...",
|
||||
"prompt.placeholder.shell": "أدخل أمر shell... {{example}}",
|
||||
"prompt.placeholder.normal": 'اسأل أي شيء... "{{example}}"',
|
||||
"prompt.placeholder.simple": "اسأل أي شيء...",
|
||||
"prompt.placeholder.summarizeComments": "لخّص التعليقات…",
|
||||
|
||||
@@ -210,7 +210,7 @@ export const dict = {
|
||||
"common.saving": "Salvando...",
|
||||
"common.default": "Padrão",
|
||||
"common.attachment": "anexo",
|
||||
"prompt.placeholder.shell": "Digite comando do shell...",
|
||||
"prompt.placeholder.shell": "Digite comando do shell... {{example}}",
|
||||
"prompt.placeholder.normal": 'Pergunte qualquer coisa... "{{example}}"',
|
||||
"prompt.placeholder.simple": "Pergunte qualquer coisa...",
|
||||
"prompt.placeholder.summarizeComments": "Resumir comentários…",
|
||||
|
||||
@@ -228,7 +228,7 @@ export const dict = {
|
||||
"common.default": "Podrazumijevano",
|
||||
"common.attachment": "prilog",
|
||||
|
||||
"prompt.placeholder.shell": "Unesi shell naredbu...",
|
||||
"prompt.placeholder.shell": "Unesi shell naredbu... {{example}}",
|
||||
"prompt.placeholder.normal": 'Pitaj bilo šta... "{{example}}"',
|
||||
"prompt.placeholder.simple": "Pitaj bilo šta...",
|
||||
"prompt.placeholder.summarizeComments": "Sažmi komentare…",
|
||||
|
||||
@@ -226,7 +226,7 @@ export const dict = {
|
||||
"common.default": "Standard",
|
||||
"common.attachment": "vedhæftning",
|
||||
|
||||
"prompt.placeholder.shell": "Indtast shell-kommando...",
|
||||
"prompt.placeholder.shell": "Indtast shell-kommando... {{example}}",
|
||||
"prompt.placeholder.normal": 'Spørg om hvad som helst... "{{example}}"',
|
||||
"prompt.placeholder.simple": "Spørg om hvad som helst...",
|
||||
"prompt.placeholder.summarizeComments": "Opsummér kommentarer…",
|
||||
|
||||
@@ -215,7 +215,7 @@ export const dict = {
|
||||
"common.saving": "Speichert...",
|
||||
"common.default": "Standard",
|
||||
"common.attachment": "Anhang",
|
||||
"prompt.placeholder.shell": "Shell-Befehl eingeben...",
|
||||
"prompt.placeholder.shell": "Shell-Befehl eingeben... {{example}}",
|
||||
"prompt.placeholder.normal": 'Fragen Sie alles... "{{example}}"',
|
||||
"prompt.placeholder.simple": "Fragen Sie alles...",
|
||||
"prompt.placeholder.summarizeComments": "Kommentare zusammenfassen…",
|
||||
|
||||
@@ -230,7 +230,7 @@ export const dict = {
|
||||
"common.default": "Default",
|
||||
"common.attachment": "attachment",
|
||||
|
||||
"prompt.placeholder.shell": "Enter shell command...",
|
||||
"prompt.placeholder.shell": "Enter shell command... {{example}}",
|
||||
"prompt.placeholder.normal": 'Ask anything... "{{example}}"',
|
||||
"prompt.placeholder.simple": "Ask anything...",
|
||||
"prompt.placeholder.summarizeComments": "Summarize comments…",
|
||||
|
||||
@@ -227,7 +227,7 @@ export const dict = {
|
||||
"common.default": "Predeterminado",
|
||||
"common.attachment": "adjunto",
|
||||
|
||||
"prompt.placeholder.shell": "Introduce comando de shell...",
|
||||
"prompt.placeholder.shell": "Introduce comando de shell... {{example}}",
|
||||
"prompt.placeholder.normal": 'Pregunta cualquier cosa... "{{example}}"',
|
||||
"prompt.placeholder.simple": "Pregunta cualquier cosa...",
|
||||
"prompt.placeholder.summarizeComments": "Resumir comentarios…",
|
||||
|
||||
@@ -210,7 +210,7 @@ export const dict = {
|
||||
"common.saving": "Enregistrement...",
|
||||
"common.default": "Défaut",
|
||||
"common.attachment": "pièce jointe",
|
||||
"prompt.placeholder.shell": "Entrez une commande shell...",
|
||||
"prompt.placeholder.shell": "Entrez une commande shell... {{example}}",
|
||||
"prompt.placeholder.normal": 'Demandez n\'importe quoi... "{{example}}"',
|
||||
"prompt.placeholder.simple": "Demandez n'importe quoi...",
|
||||
"prompt.placeholder.summarizeComments": "Résumer les commentaires…",
|
||||
|
||||
@@ -209,7 +209,7 @@ export const dict = {
|
||||
"common.saving": "保存中...",
|
||||
"common.default": "デフォルト",
|
||||
"common.attachment": "添付ファイル",
|
||||
"prompt.placeholder.shell": "シェルコマンドを入力...",
|
||||
"prompt.placeholder.shell": "シェルコマンドを入力... {{example}}",
|
||||
"prompt.placeholder.normal": '何でも聞いてください... "{{example}}"',
|
||||
"prompt.placeholder.simple": "何でも聞いてください...",
|
||||
"prompt.placeholder.summarizeComments": "コメントを要約…",
|
||||
|
||||
@@ -209,7 +209,7 @@ export const dict = {
|
||||
"common.saving": "저장 중...",
|
||||
"common.default": "기본값",
|
||||
"common.attachment": "첨부 파일",
|
||||
"prompt.placeholder.shell": "셸 명령어 입력...",
|
||||
"prompt.placeholder.shell": "셸 명령어 입력... {{example}}",
|
||||
"prompt.placeholder.normal": '무엇이든 물어보세요... "{{example}}"',
|
||||
"prompt.placeholder.simple": "무엇이든 물어보세요...",
|
||||
"prompt.placeholder.summarizeComments": "댓글 요약…",
|
||||
|
||||
@@ -230,7 +230,7 @@ export const dict = {
|
||||
"common.default": "Standard",
|
||||
"common.attachment": "vedlegg",
|
||||
|
||||
"prompt.placeholder.shell": "Skriv inn shell-kommando...",
|
||||
"prompt.placeholder.shell": "Skriv inn shell-kommando... {{example}}",
|
||||
"prompt.placeholder.normal": 'Spør om hva som helst... "{{example}}"',
|
||||
"prompt.placeholder.simple": "Spør om hva som helst...",
|
||||
"prompt.placeholder.summarizeComments": "Oppsummer kommentarer…",
|
||||
|
||||
@@ -211,7 +211,7 @@ export const dict = {
|
||||
"common.saving": "Zapisywanie...",
|
||||
"common.default": "Domyślny",
|
||||
"common.attachment": "załącznik",
|
||||
"prompt.placeholder.shell": "Wpisz polecenie terminala...",
|
||||
"prompt.placeholder.shell": "Wpisz polecenie terminala... {{example}}",
|
||||
"prompt.placeholder.normal": 'Zapytaj o cokolwiek... "{{example}}"',
|
||||
"prompt.placeholder.simple": "Zapytaj o cokolwiek...",
|
||||
"prompt.placeholder.summarizeComments": "Podsumuj komentarze…",
|
||||
|
||||
@@ -227,7 +227,7 @@ export const dict = {
|
||||
"common.default": "По умолчанию",
|
||||
"common.attachment": "вложение",
|
||||
|
||||
"prompt.placeholder.shell": "Введите команду оболочки...",
|
||||
"prompt.placeholder.shell": "Введите команду оболочки... {{example}}",
|
||||
"prompt.placeholder.normal": 'Спросите что угодно... "{{example}}"',
|
||||
"prompt.placeholder.simple": "Спросите что угодно...",
|
||||
"prompt.placeholder.summarizeComments": "Суммировать комментарии…",
|
||||
|
||||
@@ -227,7 +227,7 @@ export const dict = {
|
||||
"common.default": "ค่าเริ่มต้น",
|
||||
"common.attachment": "ไฟล์แนบ",
|
||||
|
||||
"prompt.placeholder.shell": "ป้อนคำสั่งเชลล์...",
|
||||
"prompt.placeholder.shell": "ป้อนคำสั่งเชลล์... {{example}}",
|
||||
"prompt.placeholder.normal": 'ถามอะไรก็ได้... "{{example}}"',
|
||||
"prompt.placeholder.simple": "ถามอะไรก็ได้...",
|
||||
"prompt.placeholder.summarizeComments": "สรุปความคิดเห็น…",
|
||||
|
||||
@@ -232,7 +232,7 @@ export const dict = {
|
||||
"common.default": "Varsayılan",
|
||||
"common.attachment": "ek",
|
||||
|
||||
"prompt.placeholder.shell": "Kabuk komutu girin...",
|
||||
"prompt.placeholder.shell": "Kabuk komutu girin... {{example}}",
|
||||
"prompt.placeholder.normal": 'Bir şeyler sorun... "{{example}}"',
|
||||
"prompt.placeholder.simple": "Bir şeyler sorun...",
|
||||
"prompt.placeholder.summarizeComments": "Yorumları özetle…",
|
||||
|
||||
@@ -249,7 +249,7 @@ export const dict = {
|
||||
"common.default": "默认",
|
||||
"common.attachment": "附件",
|
||||
|
||||
"prompt.placeholder.shell": "输入 shell 命令...",
|
||||
"prompt.placeholder.shell": "输入 shell 命令... {{example}}",
|
||||
"prompt.placeholder.normal": '随便问点什么... "{{example}}"',
|
||||
"prompt.placeholder.simple": "随便问点什么...",
|
||||
"prompt.placeholder.summarizeComments": "总结评论…",
|
||||
|
||||
@@ -227,7 +227,7 @@ export const dict = {
|
||||
"common.default": "預設",
|
||||
"common.attachment": "附件",
|
||||
|
||||
"prompt.placeholder.shell": "輸入 shell 命令...",
|
||||
"prompt.placeholder.shell": "輸入 shell 命令... {{example}}",
|
||||
"prompt.placeholder.normal": '隨便問點什麼... "{{example}}"',
|
||||
"prompt.placeholder.simple": "隨便問點什麼...",
|
||||
"prompt.placeholder.summarizeComments": "摘要評論…",
|
||||
|
||||
@@ -244,10 +244,9 @@ export const ErrorPage: Component<ErrorPageProps> = (props) => {
|
||||
}
|
||||
|
||||
async function installUpdate() {
|
||||
if (!platform.update || !platform.restart) return
|
||||
if (!platform.updateAndRestart) return
|
||||
await platform
|
||||
.update()
|
||||
.then(() => platform.restart!())
|
||||
.updateAndRestart()
|
||||
.then(() => setStore("actionError", undefined))
|
||||
.catch((err) => {
|
||||
setStore("actionError", formatError(err, language.t))
|
||||
|
||||
@@ -366,7 +366,7 @@ export default function Layout(props: ParentProps) {
|
||||
|
||||
const useUpdatePolling = () =>
|
||||
onMount(() => {
|
||||
if (!platform.checkUpdate || !platform.update || !platform.restart) return
|
||||
if (!platform.checkUpdate || !platform.updateAndRestart) return
|
||||
|
||||
let toastId: number | undefined
|
||||
let interval: ReturnType<typeof setInterval> | undefined
|
||||
@@ -384,8 +384,7 @@ export default function Layout(props: ParentProps) {
|
||||
{
|
||||
label: language.t("toast.update.action.installRestart"),
|
||||
onClick: async () => {
|
||||
await platform.update!()
|
||||
await platform.restart!()
|
||||
await platform.updateAndRestart!()
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/console-app",
|
||||
"version": "1.14.21",
|
||||
"version": "1.14.23",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
|
||||
@@ -187,6 +187,8 @@ export async function handler(
|
||||
// Try another provider => stop retrying if using fallback provider
|
||||
if (
|
||||
res.status !== 200 &&
|
||||
// ie. 400 error is usually provider error like malformed request
|
||||
res.status !== 400 &&
|
||||
// ie. openai 404 error: Item with id 'msg_0ead8b004a3b165d0069436a6b6834819896da85b63b196a3f' not found.
|
||||
res.status !== 404 &&
|
||||
// ie. cannot change codex model providers mid-session
|
||||
@@ -226,7 +228,7 @@ export async function handler(
|
||||
logger.debug("STATUS: " + res.status + " " + res.statusText)
|
||||
|
||||
// Handle non-streaming response
|
||||
if (!isStream || res.status === 429) {
|
||||
if (!isStream || [400, 404, 429].includes(res.status)) {
|
||||
const json = await res.json()
|
||||
await rateLimiter?.track()
|
||||
if (json.usage) {
|
||||
@@ -238,6 +240,9 @@ export async function handler(
|
||||
await reload(billingSource, authInfo, costInfo)
|
||||
json.cost = calculateOccurredCost(billingSource, costInfo)
|
||||
}
|
||||
if (res.status === 400) {
|
||||
logger.metric({ "error.response": JSON.stringify(json) })
|
||||
}
|
||||
if (json.error?.message) {
|
||||
json.error.message = `Error from provider${providerInfo.displayName ? ` (${providerInfo.displayName})` : ""}: ${json.error.message}`
|
||||
}
|
||||
@@ -393,7 +398,7 @@ export async function handler(
|
||||
type: "error",
|
||||
error: {
|
||||
type: "error",
|
||||
message: error.message,
|
||||
message: "Internal server error",
|
||||
},
|
||||
}),
|
||||
{ status: 500 },
|
||||
|
||||
@@ -53,9 +53,7 @@ export const anthropicHelper: ProviderHelper = ({ reqModel, providerModel }) =>
|
||||
anthropic_version: "bedrock-2023-05-31",
|
||||
anthropic_beta: supports1m ? ["context-1m-2025-08-07"] : undefined,
|
||||
}
|
||||
: {
|
||||
service_tier: "standard_only",
|
||||
}),
|
||||
: {}),
|
||||
}),
|
||||
createBinaryStreamDecoder: () => {
|
||||
if (!isBedrock) return undefined
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/console-core",
|
||||
"version": "1.14.21",
|
||||
"version": "1.14.23",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/console-function",
|
||||
"version": "1.14.21",
|
||||
"version": "1.14.23",
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/console-mail",
|
||||
"version": "1.14.21",
|
||||
"version": "1.14.23",
|
||||
"dependencies": {
|
||||
"@jsx-email/all": "2.2.3",
|
||||
"@jsx-email/cli": "1.4.3",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@opencode-ai/desktop-electron",
|
||||
"private": true,
|
||||
"version": "1.14.21",
|
||||
"version": "1.14.23",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"homepage": "https://opencode.ai",
|
||||
|
||||
@@ -337,11 +337,16 @@ function setupAutoUpdater() {
|
||||
})
|
||||
}
|
||||
|
||||
let updateReady = false
|
||||
let downloadedUpdateVersion: string | undefined
|
||||
|
||||
async function checkUpdate() {
|
||||
if (!UPDATER_ENABLED) return { updateAvailable: false }
|
||||
updateReady = false
|
||||
if (downloadedUpdateVersion) {
|
||||
logger.log("returning cached downloaded update", {
|
||||
version: downloadedUpdateVersion,
|
||||
})
|
||||
return { updateAvailable: true, version: downloadedUpdateVersion }
|
||||
}
|
||||
logger.log("checking for updates", {
|
||||
currentVersion: app.getVersion(),
|
||||
channel: autoUpdater.channel,
|
||||
@@ -367,7 +372,7 @@ async function checkUpdate() {
|
||||
logger.log("update available", { version })
|
||||
await autoUpdater.downloadUpdate()
|
||||
logger.log("update download completed", { version })
|
||||
updateReady = true
|
||||
downloadedUpdateVersion = version
|
||||
return { updateAvailable: true, version }
|
||||
} catch (error) {
|
||||
logger.error("update check failed", error)
|
||||
@@ -376,7 +381,15 @@ async function checkUpdate() {
|
||||
}
|
||||
|
||||
async function installUpdate() {
|
||||
if (!updateReady) return
|
||||
if (!downloadedUpdateVersion) {
|
||||
logger.log("install update skipped", {
|
||||
reason: "no downloaded update ready",
|
||||
})
|
||||
return
|
||||
}
|
||||
logger.log("installing downloaded update", {
|
||||
version: downloadedUpdateVersion,
|
||||
})
|
||||
killSidecar()
|
||||
autoUpdater.quitAndInstall()
|
||||
}
|
||||
|
||||
@@ -170,7 +170,7 @@ const createPlatform = (): Platform => {
|
||||
return window.api.checkUpdate()
|
||||
},
|
||||
|
||||
update: async () => {
|
||||
updateAndRestart: async () => {
|
||||
const config = await window.api.getWindowConfig().catch(() => ({ updaterEnabled: false }))
|
||||
if (!config.updaterEnabled) return
|
||||
await window.api.installUpdate()
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@opencode-ai/desktop",
|
||||
"private": true,
|
||||
"version": "1.14.21",
|
||||
"version": "1.14.23",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
|
||||
@@ -297,10 +297,15 @@ const createPlatform = (): Platform => {
|
||||
return { updateAvailable: true, version: next.version }
|
||||
},
|
||||
|
||||
update: async () => {
|
||||
updateAndRestart: async () => {
|
||||
if (!UPDATER_ENABLED || !update) return
|
||||
if (ostype() === "windows") await commands.killSidecar().catch(() => undefined)
|
||||
await update.install().catch(() => undefined)
|
||||
const installed = await update
|
||||
.install()
|
||||
.then(() => true)
|
||||
.catch(() => false)
|
||||
if (!installed) return
|
||||
await relaunch()
|
||||
},
|
||||
|
||||
restart: async () => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/enterprise",
|
||||
"version": "1.14.21",
|
||||
"version": "1.14.23",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
id = "opencode"
|
||||
name = "OpenCode"
|
||||
description = "The open source coding agent."
|
||||
version = "1.14.21"
|
||||
version = "1.14.23"
|
||||
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.21/opencode-darwin-arm64.zip"
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.23/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.21/opencode-darwin-x64.zip"
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.23/opencode-darwin-x64.zip"
|
||||
cmd = "./opencode"
|
||||
args = ["acp"]
|
||||
|
||||
[agent_servers.opencode.targets.linux-aarch64]
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.21/opencode-linux-arm64.tar.gz"
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.23/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.21/opencode-linux-x64.tar.gz"
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.23/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.21/opencode-windows-x64.zip"
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.23/opencode-windows-x64.zip"
|
||||
cmd = "./opencode.exe"
|
||||
args = ["acp"]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/function",
|
||||
"version": "1.14.21",
|
||||
"version": "1.14.23",
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "1.14.21",
|
||||
"version": "1.14.23",
|
||||
"name": "opencode",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -409,7 +409,7 @@ Current instance route inventory:
|
||||
- `project` - `bridged` (partial)
|
||||
bridged endpoints: `GET /project`, `GET /project/current`
|
||||
defer git-init mutation first
|
||||
- `workspace` - `next`
|
||||
- `workspace` - `bridged`
|
||||
best small reads: `GET /experimental/workspace/adaptor`, `GET /experimental/workspace`, `GET /experimental/workspace/status`
|
||||
defer create/remove mutations first
|
||||
- `file` - `later`
|
||||
@@ -448,7 +448,7 @@ Recommended near-term sequence:
|
||||
- [x] port `config` providers read endpoint
|
||||
- [x] port `project` read endpoints (`GET /project`, `GET /project/current`)
|
||||
- [x] port `GET /config` full read endpoint
|
||||
- [ ] port `workspace` read endpoints
|
||||
- [x] port `workspace` read endpoints
|
||||
- [ ] port `file` JSON read endpoints
|
||||
- [ ] decide when to remove the flag and make Effect routes the default
|
||||
|
||||
|
||||
@@ -147,6 +147,17 @@ import `z` do so only for local `ZodOverride` bridges or for `z.ZodType`
|
||||
type annotations — the `export const <Info|Spec>` values are all Effect
|
||||
Schema at source.
|
||||
|
||||
A file is considered "done" when:
|
||||
|
||||
- its exported schema values (`Info`, `Input`, `Event`, `Definition`, etc.)
|
||||
are authored as Effect Schema
|
||||
- any remaining zod is either a derived compat bridge (via `zod()` /
|
||||
`zodObject()`), a `z.ZodType` type annotation, or a documented
|
||||
`ZodOverride` escape hatch — never a hand-written parallel source of truth
|
||||
|
||||
Files that meet this bar but still carry a compat bridge are checked off
|
||||
with an inline note describing the bridge and what unblocks its removal.
|
||||
|
||||
- [x] skills, formatter, console-state, mcp, lsp, permission (leaves), model-id, command, plugin, provider
|
||||
- [x] server, layout
|
||||
- [x] keybinds
|
||||
@@ -159,6 +170,7 @@ Schema at source.
|
||||
These are the highest-priority next targets. Each is a small, self-contained
|
||||
schema module with a clear domain.
|
||||
|
||||
- [x] `src/account/schema.ts`
|
||||
- [x] `src/control-plane/schema.ts`
|
||||
- [x] `src/permission/schema.ts`
|
||||
- [x] `src/project/schema.ts`
|
||||
@@ -166,8 +178,10 @@ schema module with a clear domain.
|
||||
- [x] `src/pty/schema.ts`
|
||||
- [x] `src/question/schema.ts`
|
||||
- [x] `src/session/schema.ts`
|
||||
- [x] `src/storage/schema.ts`
|
||||
- [x] `src/sync/schema.ts`
|
||||
- [x] `src/tool/schema.ts`
|
||||
- [x] `src/util/schema.ts`
|
||||
|
||||
### Session domain
|
||||
|
||||
@@ -240,29 +254,29 @@ Working rule for this cluster:
|
||||
5. Errors and event payloads last
|
||||
- `NamedError.create(...)` shapes can stay temporarily if converting them to
|
||||
`Schema.TaggedErrorClass` would force unrelated churn
|
||||
- `SyncEvent.define(...)` and `BusEvent.define(...)` payloads can keep using
|
||||
derived `.zod` until the sync/bus layers are migrated
|
||||
- `SyncEvent.define(...)` and `BusEvent.define(...)` payloads can use
|
||||
derived `.zod` at remaining zod-based HTTP/OpenAPI boundaries
|
||||
|
||||
Possible later tightening after the Schema-first migration is stable:
|
||||
|
||||
- promote repeated opaque strings and timestamp numbers into branded/newtype
|
||||
leaf schemas where that adds domain value without changing the wire format
|
||||
|
||||
- [ ] `src/session/compaction.ts`
|
||||
- [ ] `src/session/message-v2.ts`
|
||||
- [ ] `src/session/message.ts`
|
||||
- [ ] `src/session/prompt.ts`
|
||||
- [ ] `src/session/revert.ts`
|
||||
- [ ] `src/session/session.ts`
|
||||
- [ ] `src/session/status.ts`
|
||||
- [ ] `src/session/summary.ts`
|
||||
- [ ] `src/session/todo.ts`
|
||||
- [x] `src/session/compaction.ts`
|
||||
- [x] `src/session/message-v2.ts`
|
||||
- [x] `src/session/message.ts`
|
||||
- [x] `src/session/prompt.ts`
|
||||
- [x] `src/session/revert.ts`
|
||||
- [x] `src/session/session.ts`
|
||||
- [x] `src/session/status.ts`
|
||||
- [x] `src/session/summary.ts`
|
||||
- [x] `src/session/todo.ts`
|
||||
|
||||
### Provider domain
|
||||
|
||||
- [ ] `src/provider/auth.ts`
|
||||
- [ ] `src/provider/models.ts`
|
||||
- [ ] `src/provider/provider.ts`
|
||||
- [x] `src/provider/auth.ts`
|
||||
- [x] `src/provider/models.ts`
|
||||
- [x] `src/provider/provider.ts`
|
||||
|
||||
### Tool schemas
|
||||
|
||||
@@ -270,25 +284,25 @@ Each tool declares its parameters via a zod schema. Tools are consumed by
|
||||
both the in-process runtime and the AI SDK's tool-calling layer, so the
|
||||
emitted JSON Schema must stay byte-identical.
|
||||
|
||||
- [ ] `src/tool/apply_patch.ts`
|
||||
- [ ] `src/tool/bash.ts`
|
||||
- [ ] `src/tool/codesearch.ts`
|
||||
- [ ] `src/tool/edit.ts`
|
||||
- [ ] `src/tool/glob.ts`
|
||||
- [ ] `src/tool/grep.ts`
|
||||
- [ ] `src/tool/invalid.ts`
|
||||
- [ ] `src/tool/lsp.ts`
|
||||
- [ ] `src/tool/plan.ts`
|
||||
- [ ] `src/tool/question.ts`
|
||||
- [ ] `src/tool/read.ts`
|
||||
- [ ] `src/tool/registry.ts`
|
||||
- [ ] `src/tool/skill.ts`
|
||||
- [ ] `src/tool/task.ts`
|
||||
- [ ] `src/tool/todo.ts`
|
||||
- [ ] `src/tool/tool.ts`
|
||||
- [ ] `src/tool/webfetch.ts`
|
||||
- [ ] `src/tool/websearch.ts`
|
||||
- [ ] `src/tool/write.ts`
|
||||
- [x] `src/tool/apply_patch.ts`
|
||||
- [x] `src/tool/bash.ts`
|
||||
- [x] `src/tool/codesearch.ts`
|
||||
- [x] `src/tool/edit.ts`
|
||||
- [x] `src/tool/glob.ts`
|
||||
- [x] `src/tool/grep.ts`
|
||||
- [x] `src/tool/invalid.ts`
|
||||
- [x] `src/tool/lsp.ts`
|
||||
- [x] `src/tool/plan.ts`
|
||||
- [x] `src/tool/question.ts`
|
||||
- [x] `src/tool/read.ts`
|
||||
- [x] `src/tool/registry.ts`
|
||||
- [x] `src/tool/skill.ts`
|
||||
- [x] `src/tool/task.ts`
|
||||
- [x] `src/tool/todo.ts`
|
||||
- [x] `src/tool/tool.ts`
|
||||
- [x] `src/tool/webfetch.ts`
|
||||
- [x] `src/tool/websearch.ts`
|
||||
- [x] `src/tool/write.ts`
|
||||
|
||||
### HTTP route boundaries
|
||||
|
||||
@@ -299,8 +313,8 @@ which means touching them is largely mechanical once the domain side is
|
||||
done.
|
||||
|
||||
- [ ] `src/server/error.ts`
|
||||
- [ ] `src/server/event.ts`
|
||||
- [ ] `src/server/projectors.ts`
|
||||
- [x] `src/server/event.ts`
|
||||
- [x] `src/server/projectors.ts`
|
||||
- [ ] `src/server/routes/control/index.ts`
|
||||
- [ ] `src/server/routes/control/workspace.ts`
|
||||
- [ ] `src/server/routes/global.ts`
|
||||
@@ -332,7 +346,7 @@ piecewise.
|
||||
|
||||
- [ ] `src/acp/agent.ts`
|
||||
- [ ] `src/agent/agent.ts`
|
||||
- [ ] `src/bus/bus-event.ts`
|
||||
- [x] `src/bus/bus-event.ts`
|
||||
- [ ] `src/bus/index.ts`
|
||||
- [ ] `src/cli/cmd/tui/config/tui-migrate.ts`
|
||||
- [ ] `src/cli/cmd/tui/config/tui-schema.ts`
|
||||
@@ -340,9 +354,9 @@ piecewise.
|
||||
- [ ] `src/cli/cmd/tui/event.ts`
|
||||
- [ ] `src/cli/ui.ts`
|
||||
- [ ] `src/command/index.ts`
|
||||
- [ ] `src/control-plane/adaptors/worktree.ts`
|
||||
- [ ] `src/control-plane/types.ts`
|
||||
- [ ] `src/control-plane/workspace.ts`
|
||||
- [x] `src/control-plane/adaptors/worktree.ts`
|
||||
- [x] `src/control-plane/types.ts`
|
||||
- [x] `src/control-plane/workspace.ts`
|
||||
- [ ] `src/file/index.ts`
|
||||
- [ ] `src/file/ripgrep.ts`
|
||||
- [ ] `src/file/watcher.ts`
|
||||
@@ -362,7 +376,7 @@ piecewise.
|
||||
- [ ] `src/snapshot/index.ts`
|
||||
- [ ] `src/storage/db.ts`
|
||||
- [ ] `src/storage/storage.ts`
|
||||
- [ ] `src/sync/index.ts`
|
||||
- [x] `src/sync/index.ts` — public API (`SyncEvent.define`) is Schema-first; `payloads()` still derives zod for the remaining HTTP/OpenAPI boundary
|
||||
- [ ] `src/util/fn.ts`
|
||||
- [ ] `src/util/log.ts`
|
||||
- [ ] `src/util/update-schema.ts`
|
||||
|
||||
@@ -372,7 +372,7 @@ export class Agent implements ACPAgent {
|
||||
}
|
||||
|
||||
if (part.tool === "todowrite") {
|
||||
const parsedTodos = z.array(Todo.Info).safeParse(JSON.parse(part.state.output))
|
||||
const parsedTodos = z.array(Todo.Info.zod).safeParse(JSON.parse(part.state.output))
|
||||
if (parsedTodos.success) {
|
||||
await this.connection
|
||||
.sessionUpdate({
|
||||
@@ -901,7 +901,7 @@ export class Agent implements ACPAgent {
|
||||
}
|
||||
|
||||
if (part.tool === "todowrite") {
|
||||
const parsedTodos = z.array(Todo.Info).safeParse(JSON.parse(part.state.output))
|
||||
const parsedTodos = z.array(Todo.Info.zod).safeParse(JSON.parse(part.state.output))
|
||||
if (parsedTodos.success) {
|
||||
await this.connection
|
||||
.sessionUpdate({
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
import z from "zod"
|
||||
import type { ZodType } from "zod"
|
||||
import { Schema } from "effect"
|
||||
import { zodObject } from "@/util/effect-zod"
|
||||
|
||||
export type Definition = ReturnType<typeof define>
|
||||
export type Definition<Type extends string = string, Properties extends Schema.Top = Schema.Top> = {
|
||||
type: Type
|
||||
properties: Properties
|
||||
}
|
||||
|
||||
const registry = new Map<string, Definition>()
|
||||
|
||||
export function define<Type extends string, Properties extends ZodType>(type: Type, properties: Properties) {
|
||||
const result = {
|
||||
type,
|
||||
properties,
|
||||
}
|
||||
export function define<Type extends string, Properties extends Schema.Top>(
|
||||
type: Type,
|
||||
properties: Properties,
|
||||
): Definition<Type, Properties> {
|
||||
const result = { type, properties }
|
||||
registry.set(type, result)
|
||||
return result
|
||||
}
|
||||
@@ -21,7 +25,7 @@ export function payloads() {
|
||||
return z
|
||||
.object({
|
||||
type: z.literal(type),
|
||||
properties: def.properties,
|
||||
properties: zodObject(def.properties),
|
||||
})
|
||||
.meta({
|
||||
ref: `Event.${def.type}`,
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import z from "zod"
|
||||
import { Effect, Exit, Layer, PubSub, Scope, Context, Stream } from "effect"
|
||||
import { Effect, Exit, Layer, PubSub, Scope, Context, Stream, Schema } from "effect"
|
||||
import { EffectBridge } from "@/effect"
|
||||
import { Log } from "../util"
|
||||
import { BusEvent } from "./bus-event"
|
||||
@@ -9,16 +8,18 @@ import { makeRuntime } from "@/effect/run-service"
|
||||
|
||||
const log = Log.create({ service: "bus" })
|
||||
|
||||
type BusProperties<D extends BusEvent.Definition<string, Schema.Top>> = Schema.Schema.Type<D["properties"]>
|
||||
|
||||
export const InstanceDisposed = BusEvent.define(
|
||||
"server.instance.disposed",
|
||||
z.object({
|
||||
directory: z.string(),
|
||||
Schema.Struct({
|
||||
directory: Schema.String,
|
||||
}),
|
||||
)
|
||||
|
||||
type Payload<D extends BusEvent.Definition = BusEvent.Definition> = {
|
||||
type: D["type"]
|
||||
properties: z.infer<D["properties"]>
|
||||
properties: BusProperties<D>
|
||||
}
|
||||
|
||||
type State = {
|
||||
@@ -27,10 +28,7 @@ type State = {
|
||||
}
|
||||
|
||||
export interface Interface {
|
||||
readonly publish: <D extends BusEvent.Definition>(
|
||||
def: D,
|
||||
properties: z.output<D["properties"]>,
|
||||
) => Effect.Effect<void>
|
||||
readonly publish: <D extends BusEvent.Definition>(def: D, properties: BusProperties<D>) => Effect.Effect<void>
|
||||
readonly subscribe: <D extends BusEvent.Definition>(def: D) => Stream.Stream<Payload<D>>
|
||||
readonly subscribeAll: () => Stream.Stream<Payload>
|
||||
readonly subscribeCallback: <D extends BusEvent.Definition>(
|
||||
@@ -79,7 +77,7 @@ export const layer = Layer.effect(
|
||||
})
|
||||
}
|
||||
|
||||
function publish<D extends BusEvent.Definition>(def: D, properties: z.output<D["properties"]>) {
|
||||
function publish<D extends BusEvent.Definition>(def: D, properties: BusProperties<D>) {
|
||||
return Effect.gen(function* () {
|
||||
const s = yield* InstanceState.get(state)
|
||||
const payload: Payload = { type: def.type, properties }
|
||||
@@ -175,14 +173,11 @@ const { runPromise, runSync } = makeRuntime(Service, layer)
|
||||
|
||||
// runSync is safe here because the subscribe chain (InstanceState.get, PubSub.subscribe,
|
||||
// Scope.make, Effect.forkScoped) is entirely synchronous. If any step becomes async, this will throw.
|
||||
export async function publish<D extends BusEvent.Definition>(def: D, properties: z.output<D["properties"]>) {
|
||||
export async function publish<D extends BusEvent.Definition>(def: D, properties: BusProperties<D>) {
|
||||
return runPromise((svc) => svc.publish(def, properties))
|
||||
}
|
||||
|
||||
export function subscribe<D extends BusEvent.Definition>(
|
||||
def: D,
|
||||
callback: (event: { type: D["type"]; properties: z.infer<D["properties"]> }) => unknown,
|
||||
) {
|
||||
export function subscribe<D extends BusEvent.Definition>(def: D, callback: (event: Payload<D>) => unknown) {
|
||||
return runSync((svc) => svc.subscribeCallback(def, callback))
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import { ShareNext } from "../../share"
|
||||
import { EOL } from "os"
|
||||
import { Filesystem } from "../../util"
|
||||
import { AppRuntime } from "@/effect/app-runtime"
|
||||
import { Schema } from "effect"
|
||||
|
||||
/** Discriminated union returned by the ShareNext API (GET /api/shares/:id/data) */
|
||||
export type ShareData =
|
||||
@@ -154,10 +155,10 @@ export const ImportCommand = cmd({
|
||||
return
|
||||
}
|
||||
|
||||
const info = Session.Info.parse({
|
||||
const info = Schema.decodeUnknownSync(Session.Info)({
|
||||
...exportData.info,
|
||||
projectID: Instance.project.id,
|
||||
})
|
||||
}) as Session.Info
|
||||
const row = Session.toRow(info)
|
||||
Database.use((db) =>
|
||||
db
|
||||
|
||||
@@ -24,6 +24,7 @@ import { DialogProvider as DialogProviderList } from "@tui/component/dialog-prov
|
||||
import { ErrorComponent } from "@tui/component/error-component"
|
||||
import { PluginRouteMissing } from "@tui/component/plugin-route-missing"
|
||||
import { ProjectProvider } from "@tui/context/project"
|
||||
import { EditorContextProvider } from "@tui/context/editor"
|
||||
import { useEvent } from "@tui/context/event"
|
||||
import { SDKProvider, useSDK } from "@tui/context/sdk"
|
||||
import { StartupLoading } from "@tui/component/startup-loading"
|
||||
@@ -177,7 +178,9 @@ export function tui(input: {
|
||||
<FrecencyProvider>
|
||||
<PromptHistoryProvider>
|
||||
<PromptRefProvider>
|
||||
<App onSnapshot={input.onSnapshot} />
|
||||
<EditorContextProvider>
|
||||
<App onSnapshot={input.onSnapshot} />
|
||||
</EditorContextProvider>
|
||||
</PromptRefProvider>
|
||||
</PromptHistoryProvider>
|
||||
</FrecencyProvider>
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import type { BoxRenderable, TextareaRenderable, KeyEvent, ScrollBoxRenderable } from "@opentui/core"
|
||||
import { pathToFileURL } from "bun"
|
||||
import fuzzysort from "fuzzysort"
|
||||
import path from "path"
|
||||
import { firstBy } from "remeda"
|
||||
import { createMemo, createResource, createEffect, onMount, onCleanup, Index, Show, createSignal } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { useEditorContext } from "@tui/context/editor"
|
||||
import { useSDK } from "@tui/context/sdk"
|
||||
import { useSync } from "@tui/context/sync"
|
||||
import { getScrollAcceleration } from "../../util/scroll"
|
||||
@@ -77,6 +79,7 @@ export function Autocomplete(props: {
|
||||
agentStyleId: number
|
||||
promptPartTypeId: () => number
|
||||
}) {
|
||||
const editor = useEditorContext()
|
||||
const sdk = useSDK()
|
||||
const sync = useSync()
|
||||
const command = useCommandDialog()
|
||||
@@ -221,6 +224,70 @@ export function Autocomplete(props: {
|
||||
}
|
||||
}
|
||||
|
||||
function createFilePart(item: string, lineRange?: { startLine: number; endLine?: number }) {
|
||||
const baseDir = (sync.path.directory || process.cwd()).replace(/\/+$/, "")
|
||||
const fullPath = path.isAbsolute(item) ? item : path.join(baseDir, item)
|
||||
const urlObj = pathToFileURL(fullPath)
|
||||
const filename =
|
||||
lineRange && !item.endsWith("/")
|
||||
? `${item}#${lineRange.startLine}${lineRange.endLine ? `-${lineRange.endLine}` : ""}`
|
||||
: item
|
||||
|
||||
if (lineRange && !item.endsWith("/")) {
|
||||
urlObj.searchParams.set("start", String(lineRange.startLine))
|
||||
if (lineRange.endLine !== undefined) {
|
||||
urlObj.searchParams.set("end", String(lineRange.endLine))
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
filename,
|
||||
url: urlObj.href,
|
||||
part: {
|
||||
type: "file" as const,
|
||||
mime: "text/plain",
|
||||
filename,
|
||||
url: urlObj.href,
|
||||
source: {
|
||||
type: "file" as const,
|
||||
text: {
|
||||
start: 0,
|
||||
end: 0,
|
||||
value: "",
|
||||
},
|
||||
path: item,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeMentionPath(filePath: string) {
|
||||
const baseDir = sync.path.directory || process.cwd()
|
||||
const absolute = path.resolve(filePath)
|
||||
const relative = path.relative(baseDir, absolute)
|
||||
|
||||
if (relative && !relative.startsWith("..") && !path.isAbsolute(relative)) {
|
||||
return relative.split(path.sep).join("/")
|
||||
}
|
||||
|
||||
return absolute.split(path.sep).join("/")
|
||||
}
|
||||
|
||||
function insertFileMention(input: { filePath: string; lineStart: number; lineEnd: number }) {
|
||||
const item = normalizeMentionPath(input.filePath)
|
||||
const lineRange = {
|
||||
startLine: input.lineStart,
|
||||
endLine: input.lineEnd > input.lineStart ? input.lineEnd : undefined,
|
||||
}
|
||||
const { filename, part } = createFilePart(item, lineRange)
|
||||
const index = store.visible === "@" ? store.index : props.input().cursorOffset
|
||||
|
||||
command.keybinds(true)
|
||||
setStore("visible", false)
|
||||
setStore("index", index)
|
||||
insertPart(filename, part)
|
||||
}
|
||||
|
||||
const [files] = createResource(
|
||||
() => search(),
|
||||
async (query) => {
|
||||
@@ -250,18 +317,7 @@ export function Autocomplete(props: {
|
||||
const width = props.anchor().width - 4
|
||||
options.push(
|
||||
...sortedFiles.map((item): AutocompleteOption => {
|
||||
const baseDir = (sync.path.directory || process.cwd()).replace(/\/+$/, "")
|
||||
const fullPath = `${baseDir}/${item}`
|
||||
const urlObj = pathToFileURL(fullPath)
|
||||
let filename = item
|
||||
if (lineRange && !item.endsWith("/")) {
|
||||
filename = `${item}#${lineRange.startLine}${lineRange.endLine ? `-${lineRange.endLine}` : ""}`
|
||||
urlObj.searchParams.set("start", String(lineRange.startLine))
|
||||
if (lineRange.endLine !== undefined) {
|
||||
urlObj.searchParams.set("end", String(lineRange.endLine))
|
||||
}
|
||||
}
|
||||
const url = urlObj.href
|
||||
const { filename, url, part } = createFilePart(item, lineRange)
|
||||
|
||||
const isDir = item.endsWith("/")
|
||||
return {
|
||||
@@ -270,21 +326,7 @@ export function Autocomplete(props: {
|
||||
isDirectory: isDir,
|
||||
path: item,
|
||||
onSelect: () => {
|
||||
insertPart(filename, {
|
||||
type: "file",
|
||||
mime: "text/plain",
|
||||
filename,
|
||||
url,
|
||||
source: {
|
||||
type: "file",
|
||||
text: {
|
||||
start: 0,
|
||||
end: 0,
|
||||
value: "",
|
||||
},
|
||||
path: item,
|
||||
},
|
||||
})
|
||||
insertPart(filename, part)
|
||||
},
|
||||
}
|
||||
}),
|
||||
@@ -501,6 +543,14 @@ export function Autocomplete(props: {
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
const unsubscribeMention = editor.onMention((mention) => {
|
||||
insertFileMention(mention)
|
||||
})
|
||||
|
||||
onCleanup(() => {
|
||||
unsubscribeMention()
|
||||
})
|
||||
|
||||
props.ref({
|
||||
get visible() {
|
||||
return store.visible
|
||||
|
||||
@@ -12,6 +12,7 @@ import { useRoute } from "@tui/context/route"
|
||||
import { useProject } from "@tui/context/project"
|
||||
import { useSync } from "@tui/context/sync"
|
||||
import { useEvent } from "@tui/context/event"
|
||||
import { useEditorContext } from "@tui/context/editor"
|
||||
import { MessageID, PartID } from "@/session/schema"
|
||||
import { createStore, produce, unwrap } from "solid-js/store"
|
||||
import { useKeybind } from "@tui/context/keybind"
|
||||
@@ -21,7 +22,7 @@ import { usePromptStash } from "./stash"
|
||||
import { DialogStash } from "../dialog-stash"
|
||||
import { type AutocompleteRef, Autocomplete } from "./autocomplete"
|
||||
import { useCommandDialog } from "../dialog-command"
|
||||
import { useRenderer, type JSX } from "@opentui/solid"
|
||||
import { useRenderer, useTerminalDimensions, type JSX } from "@opentui/solid"
|
||||
import * as Editor from "@tui/util/editor"
|
||||
import { useExit } from "../../context/exit"
|
||||
import * as Clipboard from "../../util/clipboard"
|
||||
@@ -94,6 +95,7 @@ export function Prompt(props: PromptProps) {
|
||||
const local = useLocal()
|
||||
const args = useArgs()
|
||||
const sdk = useSDK()
|
||||
const editor = useEditorContext()
|
||||
const route = useRoute()
|
||||
const project = useProject()
|
||||
const sync = useSync()
|
||||
@@ -104,11 +106,34 @@ export function Prompt(props: PromptProps) {
|
||||
const stash = usePromptStash()
|
||||
const command = useCommandDialog()
|
||||
const renderer = useRenderer()
|
||||
const dimensions = useTerminalDimensions()
|
||||
const { theme, syntax } = useTheme()
|
||||
const kv = useKV()
|
||||
const animationsEnabled = createMemo(() => kv.get("animations_enabled", true))
|
||||
const list = createMemo(() => props.placeholders?.normal ?? [])
|
||||
const shell = createMemo(() => props.placeholders?.shell ?? [])
|
||||
const editorPath = createMemo(() => editor.selection()?.filePath)
|
||||
const editorSelectionLabel = createMemo(() => {
|
||||
const selection = editor.selection()?.selection
|
||||
if (!selection) return
|
||||
if (selection.start.line === selection.end.line && selection.start.character === selection.end.character) return
|
||||
if (selection.start.line === selection.end.line) return `#${selection.start.line}`
|
||||
return `#${selection.start.line}-${selection.end.line}`
|
||||
})
|
||||
const editorFileLabel = createMemo(() => {
|
||||
const value = editorPath()
|
||||
if (!value) return
|
||||
const filename = path.basename(value)
|
||||
const file = /^index\.[^./]+$/.test(filename)
|
||||
? [path.basename(path.dirname(value)), filename].filter(Boolean).join("/")
|
||||
: filename
|
||||
return `${file.split(path.sep).join("/")}${editorSelectionLabel() ?? ""}`
|
||||
})
|
||||
const editorFileLabelDisplay = createMemo(() => {
|
||||
const file = editorFileLabel()
|
||||
if (!file) return
|
||||
return Locale.truncateMiddle(file, Math.max(12, Math.min(48, Math.floor(dimensions().width / 3))))
|
||||
})
|
||||
const [auto, setAuto] = createSignal<AutocompleteRef>()
|
||||
const currentProviderLabel = createMemo(() => local.model.parsed().provider)
|
||||
const hasRightContent = createMemo(() => Boolean(props.right))
|
||||
@@ -721,6 +746,27 @@ export function Prompt(props: PromptProps) {
|
||||
// Capture mode before it gets reset
|
||||
const currentMode = store.mode
|
||||
const variant = local.model.variant.current()
|
||||
const editorSelection = editor.selection()
|
||||
const editorParts = editorSelection
|
||||
? [
|
||||
{
|
||||
id: PartID.ascending(),
|
||||
type: "text" as const,
|
||||
text: (() => {
|
||||
const start = editorSelection.selection.start
|
||||
const end = editorSelection.selection.end
|
||||
if (start.line === end.line && start.character === end.character) {
|
||||
return `Note: The user opened the file "${editorSelection.filePath}".`
|
||||
}
|
||||
if (start.line === end.line) {
|
||||
return `Note: The user selected line ${start.line} from "${editorSelection.filePath}": ${editorSelection.text}`
|
||||
}
|
||||
return `Note: The user selected lines ${start.line} to ${end.line} from "${editorSelection.filePath}": ${editorSelection.text}`
|
||||
})(),
|
||||
synthetic: true,
|
||||
},
|
||||
]
|
||||
: []
|
||||
|
||||
if (store.mode === "shell") {
|
||||
void sdk.client.session.shell({
|
||||
@@ -773,6 +819,7 @@ export function Prompt(props: PromptProps) {
|
||||
model: selectedModel,
|
||||
variant,
|
||||
parts: [
|
||||
...editorParts,
|
||||
{
|
||||
id: PartID.ascending(),
|
||||
type: "text",
|
||||
@@ -1332,6 +1379,7 @@ export function Prompt(props: PromptProps) {
|
||||
</Show>
|
||||
<Show when={status().type !== "retry"}>
|
||||
<box gap={2} flexDirection="row">
|
||||
<Show when={editorFileLabelDisplay()}>{(file) => <text fg={theme.secondary}>{file()}</text>}</Show>
|
||||
<Switch>
|
||||
<Match when={store.mode === "normal"}>
|
||||
<Switch>
|
||||
|
||||
@@ -0,0 +1,319 @@
|
||||
import { readdirSync, readFileSync, statSync } from "node:fs"
|
||||
import os from "node:os"
|
||||
import path from "node:path"
|
||||
import { onCleanup, onMount } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import z from "zod"
|
||||
import { createSimpleContext } from "./helper"
|
||||
|
||||
const MCP_PROTOCOL_VERSION = "2025-11-25"
|
||||
|
||||
const JsonRpcMessageSchema = z.object({
|
||||
id: z.union([z.number(), z.string(), z.null()]).optional(),
|
||||
method: z.string().optional(),
|
||||
params: z.unknown().optional(),
|
||||
result: z.unknown().optional(),
|
||||
error: z
|
||||
.object({
|
||||
code: z.number().optional(),
|
||||
message: z.string().optional(),
|
||||
})
|
||||
.optional(),
|
||||
})
|
||||
|
||||
const PositionSchema = z.object({
|
||||
line: z.number(),
|
||||
character: z.number(),
|
||||
})
|
||||
|
||||
const EditorSelectionSchema = z.object({
|
||||
text: z.string(),
|
||||
filePath: z.string(),
|
||||
selection: z.object({
|
||||
start: PositionSchema,
|
||||
end: PositionSchema,
|
||||
}),
|
||||
})
|
||||
|
||||
const EditorMentionSchema = z.object({
|
||||
filePath: z.string(),
|
||||
lineStart: z.number(),
|
||||
lineEnd: z.number(),
|
||||
})
|
||||
|
||||
const EditorServerInfoSchema = z.object({
|
||||
protocolVersion: z.string().optional(),
|
||||
serverInfo: z
|
||||
.object({
|
||||
name: z.string().optional(),
|
||||
version: z.string().optional(),
|
||||
})
|
||||
.optional(),
|
||||
})
|
||||
|
||||
type JsonRpcMessage = z.infer<typeof JsonRpcMessageSchema>
|
||||
export type EditorSelection = z.infer<typeof EditorSelectionSchema>
|
||||
export type EditorMention = z.infer<typeof EditorMentionSchema>
|
||||
type EditorServerInfo = z.infer<typeof EditorServerInfoSchema>
|
||||
|
||||
type EditorConnection = {
|
||||
url: string
|
||||
authToken?: string
|
||||
source: string
|
||||
}
|
||||
|
||||
type EditorLockFile = {
|
||||
port: number
|
||||
authToken?: string
|
||||
transport?: string
|
||||
workspaceFolders: string[]
|
||||
mtimeMs: number
|
||||
}
|
||||
|
||||
export const { use: useEditorContext, provider: EditorContextProvider } = createSimpleContext({
|
||||
name: "EditorContext",
|
||||
init: () => {
|
||||
const mentionListeners = new Set<(mention: EditorMention) => void>()
|
||||
const [store, setStore] = createStore<{
|
||||
status: "disabled" | "connecting" | "connected"
|
||||
selection: EditorSelection | undefined
|
||||
server: EditorServerInfo | undefined
|
||||
}>({
|
||||
status: "disabled",
|
||||
selection: undefined,
|
||||
server: undefined,
|
||||
})
|
||||
|
||||
onMount(() => {
|
||||
let socket: WebSocket | undefined
|
||||
let closed = false
|
||||
let reconnect: ReturnType<typeof setTimeout> | undefined
|
||||
let attempt = 0
|
||||
let requestID = 0
|
||||
const pending = new Map<number, string>()
|
||||
|
||||
const send = (payload: JsonRpcMessage) => {
|
||||
if (!socket || socket.readyState !== WebSocket.OPEN) return
|
||||
socket.send(JSON.stringify({ jsonrpc: "2.0", ...payload }))
|
||||
}
|
||||
|
||||
const request = (method: string, params?: unknown) => {
|
||||
requestID += 1
|
||||
pending.set(requestID, method)
|
||||
send({ id: requestID, method, params })
|
||||
}
|
||||
|
||||
const scheduleReconnect = (delay: number) => {
|
||||
if (closed) return
|
||||
if (reconnect) clearTimeout(reconnect)
|
||||
reconnect = setTimeout(connect, delay)
|
||||
}
|
||||
|
||||
const connect = () => {
|
||||
if (closed) return
|
||||
|
||||
const connection = resolveEditorConnection()
|
||||
if (!connection) {
|
||||
setStore("status", "disabled")
|
||||
scheduleReconnect(1000)
|
||||
return
|
||||
}
|
||||
|
||||
setStore("status", "connecting")
|
||||
const current = openEditorSocket(connection)
|
||||
socket = current
|
||||
|
||||
current.addEventListener("open", () => {
|
||||
if (socket !== current) {
|
||||
current.close()
|
||||
return
|
||||
}
|
||||
|
||||
attempt = 0
|
||||
setStore("status", "connected")
|
||||
request("initialize", {
|
||||
protocolVersion: MCP_PROTOCOL_VERSION,
|
||||
capabilities: {},
|
||||
clientInfo: { name: "opencode", version: "0.0.0" },
|
||||
})
|
||||
})
|
||||
|
||||
current.addEventListener("message", (event) => {
|
||||
const message = parseMessage(event.data)
|
||||
if (!message) return
|
||||
|
||||
const selection =
|
||||
message.method === "selection_changed" ? EditorSelectionSchema.safeParse(message.params) : undefined
|
||||
if (selection?.success) {
|
||||
setStore("selection", selection.data)
|
||||
return
|
||||
}
|
||||
|
||||
const mention = message.method === "at_mentioned" ? EditorMentionSchema.safeParse(message.params) : undefined
|
||||
if (mention?.success) {
|
||||
mentionListeners.forEach((listener) => listener(mention.data))
|
||||
return
|
||||
}
|
||||
|
||||
if (typeof message.id !== "number") return
|
||||
|
||||
const method = pending.get(message.id)
|
||||
if (!method) return
|
||||
|
||||
pending.delete(message.id)
|
||||
if (message.error) return
|
||||
|
||||
const initialize = method === "initialize" ? EditorServerInfoSchema.safeParse(message.result) : undefined
|
||||
if (initialize?.success) {
|
||||
setStore("server", initialize.data)
|
||||
send({ method: "notifications/initialized" })
|
||||
return
|
||||
}
|
||||
})
|
||||
|
||||
current.addEventListener("close", () => {
|
||||
if (socket !== current) return
|
||||
|
||||
socket = undefined
|
||||
pending.clear()
|
||||
if (closed) return
|
||||
|
||||
setStore("status", "connecting")
|
||||
attempt += 1
|
||||
const delay = Math.min(1000 * 2 ** (attempt - 1), 30000)
|
||||
scheduleReconnect(delay)
|
||||
})
|
||||
}
|
||||
|
||||
scheduleReconnect(0)
|
||||
|
||||
onCleanup(() => {
|
||||
closed = true
|
||||
if (reconnect) clearTimeout(reconnect)
|
||||
socket?.close()
|
||||
})
|
||||
})
|
||||
|
||||
return {
|
||||
enabled() {
|
||||
return Boolean(resolveEditorConnection())
|
||||
},
|
||||
connected() {
|
||||
return store.status === "connected"
|
||||
},
|
||||
selection() {
|
||||
return store.selection
|
||||
},
|
||||
onMention(listener: (mention: EditorMention) => void) {
|
||||
mentionListeners.add(listener)
|
||||
return () => mentionListeners.delete(listener)
|
||||
},
|
||||
server() {
|
||||
return store.server
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
function parsePort(value: string | undefined) {
|
||||
if (!value) return
|
||||
|
||||
const parsed = Number.parseInt(value, 10)
|
||||
if (!Number.isInteger(parsed) || parsed <= 0 || parsed > 65535) return
|
||||
return parsed
|
||||
}
|
||||
|
||||
function resolveEditorConnection(): EditorConnection | undefined {
|
||||
const lock = resolveEditorLockFile()
|
||||
if (lock) {
|
||||
return {
|
||||
url: `ws://127.0.0.1:${lock.port}`,
|
||||
authToken: lock.authToken,
|
||||
source: `lock:${lock.port}`,
|
||||
}
|
||||
}
|
||||
|
||||
const port = parsePort(process.env.CLAUDE_CODE_SSE_PORT || process.env.OPENCODE_EDITOR_SSE_PORT)
|
||||
if (!port) return
|
||||
return {
|
||||
url: `ws://127.0.0.1:${port}`,
|
||||
source: `env:${port}`,
|
||||
}
|
||||
}
|
||||
|
||||
function resolveEditorLockFile() {
|
||||
const directory = path.join(os.homedir(), ".claude", "ide")
|
||||
let entries: string[]
|
||||
|
||||
try {
|
||||
entries = readdirSync(directory)
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
|
||||
const cwd = process.cwd()
|
||||
const locks = entries
|
||||
.filter((entry) => entry.endsWith(".lock"))
|
||||
.map((entry) => readEditorLockFile(path.join(directory, entry)))
|
||||
.filter((entry): entry is EditorLockFile => Boolean(entry))
|
||||
.sort((left, right) => scoreEditorLock(right, cwd) - scoreEditorLock(left, cwd))
|
||||
|
||||
return locks[0]
|
||||
}
|
||||
|
||||
function readEditorLockFile(filePath: string): EditorLockFile | undefined {
|
||||
const port = parsePort(path.basename(filePath, ".lock"))
|
||||
if (!port) return
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(readFileSync(filePath, "utf-8")) as unknown
|
||||
if (!isRecord(parsed)) return
|
||||
if (parsed.transport !== undefined && parsed.transport !== "ws") return
|
||||
|
||||
return {
|
||||
port,
|
||||
authToken: typeof parsed.authToken === "string" ? parsed.authToken : undefined,
|
||||
transport: typeof parsed.transport === "string" ? parsed.transport : undefined,
|
||||
workspaceFolders: Array.isArray(parsed.workspaceFolders)
|
||||
? parsed.workspaceFolders.filter((value): value is string => typeof value === "string")
|
||||
: [],
|
||||
mtimeMs: statSync(filePath).mtimeMs,
|
||||
}
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
function scoreEditorLock(lock: EditorLockFile, cwd: string) {
|
||||
const workspaceMatch = lock.workspaceFolders.some((folder) => pathContains(folder, cwd)) ? 1 : 0
|
||||
return workspaceMatch * 1_000_000_000_000 + lock.mtimeMs
|
||||
}
|
||||
|
||||
function pathContains(parent: string, child: string) {
|
||||
const relative = path.relative(path.resolve(parent), path.resolve(child))
|
||||
return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative))
|
||||
}
|
||||
|
||||
function openEditorSocket(connection: EditorConnection) {
|
||||
if (!connection.authToken) return new WebSocket(connection.url)
|
||||
|
||||
return new WebSocket(connection.url, {
|
||||
headers: {
|
||||
"x-claude-code-ide-authorization": connection.authToken,
|
||||
},
|
||||
} as any)
|
||||
}
|
||||
|
||||
function parseMessage(value: unknown) {
|
||||
if (typeof value !== "string") return
|
||||
|
||||
try {
|
||||
return JsonRpcMessageSchema.parse(JSON.parse(value))
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value)
|
||||
}
|
||||
@@ -1,14 +1,14 @@
|
||||
import { BusEvent } from "@/bus/bus-event"
|
||||
import { SessionID } from "@/session/schema"
|
||||
import z from "zod"
|
||||
import { Schema } from "effect"
|
||||
|
||||
export const TuiEvent = {
|
||||
PromptAppend: BusEvent.define("tui.prompt.append", z.object({ text: z.string() })),
|
||||
PromptAppend: BusEvent.define("tui.prompt.append", Schema.Struct({ text: Schema.String })),
|
||||
CommandExecute: BusEvent.define(
|
||||
"tui.command.execute",
|
||||
z.object({
|
||||
command: z.union([
|
||||
z.enum([
|
||||
Schema.Struct({
|
||||
command: Schema.Union([
|
||||
Schema.Literals([
|
||||
"session.list",
|
||||
"session.new",
|
||||
"session.share",
|
||||
@@ -26,23 +26,23 @@ export const TuiEvent = {
|
||||
"prompt.submit",
|
||||
"agent.cycle",
|
||||
]),
|
||||
z.string(),
|
||||
Schema.String,
|
||||
]),
|
||||
}),
|
||||
),
|
||||
ToastShow: BusEvent.define(
|
||||
"tui.toast.show",
|
||||
z.object({
|
||||
title: z.string().optional(),
|
||||
message: z.string(),
|
||||
variant: z.enum(["info", "success", "warning", "error"]),
|
||||
duration: z.number().default(5000).optional().describe("Duration in milliseconds"),
|
||||
Schema.Struct({
|
||||
title: Schema.optional(Schema.String),
|
||||
message: Schema.String,
|
||||
variant: Schema.Literals(["info", "success", "warning", "error"]),
|
||||
duration: Schema.optional(Schema.Number).annotate({ description: "Duration in milliseconds" }),
|
||||
}),
|
||||
),
|
||||
SessionSelect: BusEvent.define(
|
||||
"tui.session.select",
|
||||
z.object({
|
||||
sessionID: SessionID.zod.describe("Session ID to navigate to"),
|
||||
Schema.Struct({
|
||||
sessionID: SessionID.annotate({ description: "Session ID to navigate to" }),
|
||||
}),
|
||||
),
|
||||
}
|
||||
|
||||
@@ -1250,7 +1250,17 @@ function UserMessage(props: {
|
||||
}) {
|
||||
const ctx = use()
|
||||
const local = useLocal()
|
||||
const text = createMemo(() => props.parts.flatMap((x) => (x.type === "text" && !x.synthetic ? [x] : []))[0])
|
||||
const text = createMemo(() => {
|
||||
const texts = props.parts
|
||||
.map((x) => {
|
||||
if (x.type === "text" && !x.synthetic) {
|
||||
return x.text
|
||||
}
|
||||
return null
|
||||
})
|
||||
.filter(Boolean)
|
||||
return texts.join("\n\n")
|
||||
})
|
||||
const files = createMemo(() => props.parts.flatMap((x) => (x.type === "file" ? [x] : [])))
|
||||
const { theme } = useTheme()
|
||||
const [hover, setHover] = createSignal(false)
|
||||
@@ -1285,7 +1295,7 @@ function UserMessage(props: {
|
||||
backgroundColor={hover() ? theme.backgroundElement : theme.backgroundPanel}
|
||||
flexShrink={0}
|
||||
>
|
||||
<text fg={theme.text}>{text()?.text}</text>
|
||||
<text fg={theme.text}>{text()}</text>
|
||||
<Show when={files().length}>
|
||||
<box flexDirection="row" paddingBottom={metadataVisible() ? 1 : 0} paddingTop={1} gap={1} flexWrap="wrap">
|
||||
<For each={files()}>
|
||||
|
||||
@@ -4,10 +4,10 @@ import { useTheme } from "@tui/context/theme"
|
||||
import { useTerminalDimensions } from "@opentui/solid"
|
||||
import { SplitBorder } from "../component/border"
|
||||
import { TextAttributes } from "@opentui/core"
|
||||
import z from "zod"
|
||||
import { Schema } from "effect"
|
||||
import { type TuiEvent } from "../event"
|
||||
|
||||
export type ToastOptions = z.infer<typeof TuiEvent.ToastShow.properties>
|
||||
export type ToastOptions = Schema.Schema.Type<typeof TuiEvent.ToastShow.properties>
|
||||
|
||||
export function Toast() {
|
||||
const toast = useToast()
|
||||
|
||||
@@ -3,7 +3,7 @@ import { InstanceState } from "@/effect"
|
||||
import { EffectBridge } from "@/effect"
|
||||
import type { InstanceContext } from "@/project/instance"
|
||||
import { SessionID, MessageID } from "@/session/schema"
|
||||
import { Effect, Layer, Context } from "effect"
|
||||
import { Effect, Layer, Context, Schema } from "effect"
|
||||
import z from "zod"
|
||||
import { Config } from "../config"
|
||||
import { MCP } from "../mcp"
|
||||
@@ -18,11 +18,11 @@ type State = {
|
||||
export const Event = {
|
||||
Executed: BusEvent.define(
|
||||
"command.executed",
|
||||
z.object({
|
||||
name: z.string(),
|
||||
sessionID: SessionID.zod,
|
||||
arguments: z.string(),
|
||||
messageID: MessageID.zod,
|
||||
Schema.Struct({
|
||||
name: Schema.String,
|
||||
sessionID: SessionID,
|
||||
arguments: Schema.String,
|
||||
messageID: MessageID,
|
||||
}),
|
||||
),
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Schema } from "effect"
|
||||
import z from "zod"
|
||||
import { Bus } from "@/bus"
|
||||
import { zod } from "@/util/effect-zod"
|
||||
import { PositiveInt } from "@/util/schema"
|
||||
import { Log } from "../util"
|
||||
import { NamedError } from "@opencode-ai/shared/util/error"
|
||||
import { Glob } from "@opencode-ai/shared/util/glob"
|
||||
@@ -15,8 +16,6 @@ import { ConfigPermission } from "./permission"
|
||||
|
||||
const log = Log.create({ service: "config" })
|
||||
|
||||
const PositiveInt = Schema.Number.check(Schema.isInt()).check(Schema.isGreaterThan(0))
|
||||
|
||||
const Color = Schema.Union([
|
||||
Schema.String.check(Schema.isPattern(/^#[0-9a-fA-F]{6}$/)),
|
||||
Schema.Literals(["primary", "secondary", "accent", "success", "warning", "error", "info"]),
|
||||
|
||||
@@ -25,7 +25,7 @@ import { Context, Duration, Effect, Exit, Fiber, Layer, Option, Schema } from "e
|
||||
import { EffectFlock } from "@opencode-ai/shared/util/effect-flock"
|
||||
import { InstanceRef } from "@/effect/instance-ref"
|
||||
import { zod, ZodOverride } from "@/util/effect-zod"
|
||||
import { withStatics } from "@/util/schema"
|
||||
import { NonNegativeInt, PositiveInt, withStatics, type DeepMutable } from "@/util/schema"
|
||||
import { ConfigAgent } from "./agent"
|
||||
import { ConfigCommand } from "./command"
|
||||
import { ConfigFormatter } from "./formatter"
|
||||
@@ -88,9 +88,6 @@ export type Layout = ConfigLayout.Layout
|
||||
const AgentRef = Schema.Any.annotate({ [ZodOverride]: ConfigAgent.Info })
|
||||
const LogLevelRef = Schema.Any.annotate({ [ZodOverride]: Log.Level })
|
||||
|
||||
const PositiveInt = Schema.Number.check(Schema.isInt()).check(Schema.isGreaterThan(0))
|
||||
const NonNegativeInt = Schema.Number.check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0))
|
||||
|
||||
// The Effect Schema is the canonical source of truth. The `.zod` compatibility
|
||||
// surface is derived so existing Hono validators keep working without a parallel
|
||||
// Zod definition.
|
||||
@@ -204,6 +201,19 @@ export const Info = Schema.Struct({
|
||||
url: Schema.optional(Schema.String).annotate({ description: "Enterprise URL" }),
|
||||
}),
|
||||
),
|
||||
tool_output: Schema.optional(
|
||||
Schema.Struct({
|
||||
max_lines: Schema.optional(PositiveInt).annotate({
|
||||
description: "Maximum lines of tool output before it is truncated and saved to disk (default: 2000)",
|
||||
}),
|
||||
max_bytes: Schema.optional(PositiveInt).annotate({
|
||||
description: "Maximum bytes of tool output before it is truncated and saved to disk (default: 51200)",
|
||||
}),
|
||||
}),
|
||||
).annotate({
|
||||
description:
|
||||
"Thresholds for truncating tool output. When output exceeds either limit, the full text is written to the truncation directory and a preview is returned.",
|
||||
}),
|
||||
compaction: Schema.optional(
|
||||
Schema.Struct({
|
||||
auto: Schema.optional(Schema.Boolean).annotate({
|
||||
@@ -252,26 +262,9 @@ export const Info = Schema.Struct({
|
||||
})),
|
||||
)
|
||||
|
||||
// Schema.Struct produces readonly types by default, but the service code
|
||||
// below mutates Info objects directly (e.g. `config.mode = ...`). Strip the
|
||||
// readonly recursively so callers get the same mutable shape zod inferred.
|
||||
//
|
||||
// `Types.DeepMutable` from effect-smol would be a drop-in, but its fallback
|
||||
// branch `{ -readonly [K in keyof T]: ... }` collapses `unknown` to `{}`
|
||||
// (since `keyof unknown = never`), which widens `Record<string, unknown>`
|
||||
// fields like `ConfigPlugin.Options`. The local version gates on
|
||||
// `extends object` so `unknown` passes through.
|
||||
//
|
||||
// Tuple branch preserves `ConfigPlugin.Spec`'s `readonly [string, Options]`
|
||||
// shape (otherwise the general array branch widens it to an array).
|
||||
type DeepMutable<T> = T extends readonly [unknown, ...unknown[]]
|
||||
? { -readonly [K in keyof T]: DeepMutable<T[K]> }
|
||||
: T extends readonly (infer U)[]
|
||||
? DeepMutable<U>[]
|
||||
: T extends object
|
||||
? { -readonly [K in keyof T]: DeepMutable<T[K]> }
|
||||
: T
|
||||
|
||||
// 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>> & {
|
||||
// plugin_origins is derived state, not a persisted config field. It keeps each winning plugin spec together
|
||||
// with the file and scope it came from so later runtime code can make location-sensitive decisions.
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { Schema } from "effect"
|
||||
import { zod } from "@/util/effect-zod"
|
||||
import { withStatics } from "@/util/schema"
|
||||
|
||||
const PositiveInt = Schema.Number.check(Schema.isInt()).check(Schema.isGreaterThan(0))
|
||||
import { PositiveInt, withStatics } from "@/util/schema"
|
||||
|
||||
export const Model = Schema.Struct({
|
||||
id: Schema.optional(Schema.String),
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Schema } from "effect"
|
||||
import { zod } from "@/util/effect-zod"
|
||||
import { withStatics } from "@/util/schema"
|
||||
import { PositiveInt, withStatics } from "@/util/schema"
|
||||
|
||||
export const Server = Schema.Struct({
|
||||
port: Schema.optional(Schema.Number.check(Schema.isInt()).check(Schema.isGreaterThan(0))).annotate({
|
||||
port: Schema.optional(PositiveInt).annotate({
|
||||
description: "Port to listen on",
|
||||
}),
|
||||
hostname: Schema.optional(Schema.String).annotate({ description: "Hostname to listen on" }),
|
||||
|
||||
@@ -1,12 +1,6 @@
|
||||
import { lazy } from "@/util/lazy"
|
||||
import type { ProjectID } from "@/project/schema"
|
||||
import type { WorkspaceAdaptor } from "../types"
|
||||
|
||||
export type WorkspaceAdaptorEntry = {
|
||||
type: string
|
||||
name: string
|
||||
description: string
|
||||
}
|
||||
import type { WorkspaceAdaptor, WorkspaceAdaptorEntry } from "../types"
|
||||
|
||||
const BUILTIN: Record<string, () => Promise<WorkspaceAdaptor>> = {
|
||||
worktree: lazy(async () => (await import("./worktree")).WorktreeAdaptor),
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import z from "zod"
|
||||
import { Schema } from "effect"
|
||||
import { AppRuntime } from "@/effect/app-runtime"
|
||||
import { Worktree } from "@/worktree"
|
||||
import { type WorkspaceAdaptor, WorkspaceInfo } from "../types"
|
||||
import { zod } from "@/util/effect-zod"
|
||||
import { withStatics } from "@/util/schema"
|
||||
|
||||
const WorktreeConfig = z.object({
|
||||
name: WorkspaceInfo.shape.name,
|
||||
branch: WorkspaceInfo.shape.branch.unwrap(),
|
||||
directory: WorkspaceInfo.shape.directory.unwrap(),
|
||||
})
|
||||
const WorktreeConfig = Schema.Struct({
|
||||
name: WorkspaceInfo.fields.name,
|
||||
branch: Schema.String,
|
||||
directory: Schema.String,
|
||||
}).pipe(withStatics((s) => ({ zod: zod(s) })))
|
||||
|
||||
export const WorktreeAdaptor: WorkspaceAdaptor = {
|
||||
name: "Worktree",
|
||||
@@ -22,7 +24,7 @@ export const WorktreeAdaptor: WorkspaceAdaptor = {
|
||||
}
|
||||
},
|
||||
async create(info) {
|
||||
const config = WorktreeConfig.parse(info)
|
||||
const config = WorktreeConfig.zod.parse(info)
|
||||
await AppRuntime.runPromise(
|
||||
Worktree.Service.use((svc) =>
|
||||
svc.createFromInfo({
|
||||
@@ -34,11 +36,11 @@ export const WorktreeAdaptor: WorkspaceAdaptor = {
|
||||
)
|
||||
},
|
||||
async remove(info) {
|
||||
const config = WorktreeConfig.parse(info)
|
||||
const config = WorktreeConfig.zod.parse(info)
|
||||
await AppRuntime.runPromise(Worktree.Service.use((svc) => svc.remove({ directory: config.directory })))
|
||||
},
|
||||
target(info) {
|
||||
const config = WorktreeConfig.parse(info)
|
||||
const config = WorktreeConfig.zod.parse(info)
|
||||
return {
|
||||
type: "local",
|
||||
directory: config.directory,
|
||||
|
||||
@@ -1,17 +1,28 @@
|
||||
import z from "zod"
|
||||
import { Schema } from "effect"
|
||||
import { ProjectID } from "@/project/schema"
|
||||
import { WorkspaceID } from "./schema"
|
||||
import { zod } from "@/util/effect-zod"
|
||||
import { type DeepMutable, withStatics } from "@/util/schema"
|
||||
|
||||
export const WorkspaceInfo = z.object({
|
||||
id: WorkspaceID.zod,
|
||||
type: z.string(),
|
||||
name: z.string(),
|
||||
branch: z.string().nullable(),
|
||||
directory: z.string().nullable(),
|
||||
extra: z.unknown().nullable(),
|
||||
projectID: ProjectID.zod,
|
||||
export const WorkspaceInfo = Schema.Struct({
|
||||
id: WorkspaceID,
|
||||
type: Schema.String,
|
||||
name: Schema.String,
|
||||
branch: Schema.NullOr(Schema.String),
|
||||
directory: Schema.NullOr(Schema.String),
|
||||
extra: Schema.NullOr(Schema.Unknown),
|
||||
projectID: ProjectID,
|
||||
})
|
||||
export type WorkspaceInfo = z.infer<typeof WorkspaceInfo>
|
||||
.annotate({ identifier: "Workspace" })
|
||||
.pipe(withStatics((s) => ({ zod: zod(s) })))
|
||||
export type WorkspaceInfo = DeepMutable<Schema.Schema.Type<typeof WorkspaceInfo>>
|
||||
|
||||
export const WorkspaceAdaptorEntry = Schema.Struct({
|
||||
type: Schema.String,
|
||||
name: Schema.String,
|
||||
description: Schema.String,
|
||||
}).pipe(withStatics((s) => ({ zod: zod(s) })))
|
||||
export type WorkspaceAdaptorEntry = Schema.Schema.Type<typeof WorkspaceAdaptorEntry>
|
||||
|
||||
export type Target =
|
||||
| {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import z from "zod"
|
||||
import { Schema } from "effect"
|
||||
import { setTimeout as sleep } from "node:timers/promises"
|
||||
import { fn } from "@/util/fn"
|
||||
import { Database, asc, eq, inArray } from "@/storage"
|
||||
@@ -15,7 +15,7 @@ import { ProjectID } from "@/project/schema"
|
||||
import { Slug } from "@opencode-ai/shared/util/slug"
|
||||
import { WorkspaceTable } from "./workspace.sql"
|
||||
import { getAdaptor } from "./adaptors"
|
||||
import { WorkspaceInfo } from "./types"
|
||||
import { type WorkspaceInfo, WorkspaceInfo as WorkspaceInfoSchema } from "./types"
|
||||
import { WorkspaceID } from "./schema"
|
||||
import { parseSSE } from "./sse"
|
||||
import { Session } from "@/session"
|
||||
@@ -25,36 +25,36 @@ import { errorData } from "@/util/error"
|
||||
import { AppRuntime } from "@/effect/app-runtime"
|
||||
import { waitEvent } from "./util"
|
||||
import { WorkspaceContext } from "./workspace-context"
|
||||
import { NonNegativeInt, withStatics } from "@/util/schema"
|
||||
import { zod as effectZod, zodObject } from "@/util/effect-zod"
|
||||
|
||||
export const Info = WorkspaceInfo.meta({
|
||||
ref: "Workspace",
|
||||
export const Info = WorkspaceInfoSchema
|
||||
export type Info = WorkspaceInfo
|
||||
|
||||
export const ConnectionStatus = Schema.Struct({
|
||||
workspaceID: WorkspaceID,
|
||||
status: Schema.Literals(["connected", "connecting", "disconnected", "error"]),
|
||||
})
|
||||
export type Info = z.infer<typeof Info>
|
||||
export type ConnectionStatus = Schema.Schema.Type<typeof ConnectionStatus>
|
||||
|
||||
export const ConnectionStatus = z.object({
|
||||
workspaceID: WorkspaceID.zod,
|
||||
status: z.enum(["connected", "connecting", "disconnected", "error"]),
|
||||
})
|
||||
export type ConnectionStatus = z.infer<typeof ConnectionStatus>
|
||||
|
||||
const Restore = z.object({
|
||||
workspaceID: WorkspaceID.zod,
|
||||
sessionID: SessionID.zod,
|
||||
total: z.number().int().min(0),
|
||||
step: z.number().int().min(0),
|
||||
const Restore = Schema.Struct({
|
||||
workspaceID: WorkspaceID,
|
||||
sessionID: SessionID,
|
||||
total: NonNegativeInt,
|
||||
step: NonNegativeInt,
|
||||
})
|
||||
|
||||
export const Event = {
|
||||
Ready: BusEvent.define(
|
||||
"workspace.ready",
|
||||
z.object({
|
||||
name: z.string(),
|
||||
Schema.Struct({
|
||||
name: Schema.String,
|
||||
}),
|
||||
),
|
||||
Failed: BusEvent.define(
|
||||
"workspace.failed",
|
||||
z.object({
|
||||
message: z.string(),
|
||||
Schema.Struct({
|
||||
message: Schema.String,
|
||||
}),
|
||||
),
|
||||
Restore: BusEvent.define("workspace.restore", Restore),
|
||||
@@ -73,15 +73,16 @@ function fromRow(row: typeof WorkspaceTable.$inferSelect): Info {
|
||||
}
|
||||
}
|
||||
|
||||
const CreateInput = z.object({
|
||||
id: WorkspaceID.zod.optional(),
|
||||
type: Info.shape.type,
|
||||
branch: Info.shape.branch,
|
||||
projectID: ProjectID.zod,
|
||||
extra: Info.shape.extra,
|
||||
})
|
||||
export const CreateInput = Schema.Struct({
|
||||
id: Schema.optional(WorkspaceID),
|
||||
type: Info.fields.type,
|
||||
branch: Info.fields.branch,
|
||||
projectID: ProjectID,
|
||||
extra: Info.fields.extra,
|
||||
}).pipe(withStatics((s) => ({ zod: effectZod(s), zodObject: zodObject(s) })))
|
||||
export type CreateInput = Schema.Schema.Type<typeof CreateInput>
|
||||
|
||||
export const create = fn(CreateInput, async (input) => {
|
||||
export const create = fn(CreateInput.zod, async (input) => {
|
||||
const id = WorkspaceID.ascending(input.id)
|
||||
const adaptor = await getAdaptor(input.projectID, input.type)
|
||||
|
||||
@@ -137,12 +138,13 @@ export const create = fn(CreateInput, async (input) => {
|
||||
return info
|
||||
})
|
||||
|
||||
const SessionRestoreInput = z.object({
|
||||
workspaceID: WorkspaceID.zod,
|
||||
sessionID: SessionID.zod,
|
||||
})
|
||||
export const SessionRestoreInput = Schema.Struct({
|
||||
workspaceID: WorkspaceID,
|
||||
sessionID: SessionID,
|
||||
}).pipe(withStatics((s) => ({ zod: effectZod(s), zodObject: zodObject(s) })))
|
||||
export type SessionRestoreInput = Schema.Schema.Type<typeof SessionRestoreInput>
|
||||
|
||||
export const sessionRestore = fn(SessionRestoreInput, async (input) => {
|
||||
export const sessionRestore = fn(SessionRestoreInput.zod, async (input) => {
|
||||
log.info("session restore requested", {
|
||||
workspaceID: input.workspaceID,
|
||||
sessionID: input.sessionID,
|
||||
|
||||
@@ -3,7 +3,7 @@ import { InstanceState } from "@/effect"
|
||||
|
||||
import { AppFileSystem } from "@opencode-ai/shared/filesystem"
|
||||
import { Git } from "@/git"
|
||||
import { Effect, Layer, Context, Scope } from "effect"
|
||||
import { Effect, Layer, Context, Schema, Scope } from "effect"
|
||||
import * as Stream from "effect/Stream"
|
||||
import { formatPatch, structuredPatch } from "diff"
|
||||
import fuzzysort from "fuzzysort"
|
||||
@@ -76,8 +76,8 @@ export type Content = z.infer<typeof Content>
|
||||
export const Event = {
|
||||
Edited: BusEvent.define(
|
||||
"file.edited",
|
||||
z.object({
|
||||
file: z.string(),
|
||||
Schema.Struct({
|
||||
file: Schema.String,
|
||||
}),
|
||||
),
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Cause, Effect, Layer, Context } from "effect"
|
||||
import { Cause, Effect, Layer, Context, Schema } from "effect"
|
||||
// @ts-ignore
|
||||
import { createWrapper } from "@parcel/watcher/wrapper"
|
||||
import type ParcelWatcher from "@parcel/watcher"
|
||||
@@ -25,9 +25,9 @@ const SUBSCRIBE_TIMEOUT_MS = 10_000
|
||||
export const Event = {
|
||||
Updated: BusEvent.define(
|
||||
"file.watcher.updated",
|
||||
z.object({
|
||||
file: z.string(),
|
||||
event: z.union([z.literal("add"), z.literal("change"), z.literal("unlink")]),
|
||||
Schema.Struct({
|
||||
file: Schema.String,
|
||||
event: Schema.Literals(["add", "change", "unlink"]),
|
||||
}),
|
||||
),
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { BusEvent } from "@/bus/bus-event"
|
||||
import z from "zod"
|
||||
import { Schema } from "effect"
|
||||
import { NamedError } from "@opencode-ai/shared/util/error"
|
||||
import { Log } from "../util"
|
||||
import { Process } from "@/util"
|
||||
@@ -17,8 +18,8 @@ const log = Log.create({ service: "ide" })
|
||||
export const Event = {
|
||||
Installed: BusEvent.define(
|
||||
"ide.installed",
|
||||
z.object({
|
||||
ide: z.string(),
|
||||
Schema.Struct({
|
||||
ide: Schema.String,
|
||||
}),
|
||||
),
|
||||
}
|
||||
|
||||
@@ -21,14 +21,14 @@ export type ReleaseType = "patch" | "minor" | "major"
|
||||
export const Event = {
|
||||
Updated: BusEvent.define(
|
||||
"installation.updated",
|
||||
z.object({
|
||||
version: z.string(),
|
||||
Schema.Struct({
|
||||
version: Schema.String,
|
||||
}),
|
||||
),
|
||||
UpdateAvailable: BusEvent.define(
|
||||
"installation.update-available",
|
||||
z.object({
|
||||
version: z.string(),
|
||||
Schema.Struct({
|
||||
version: Schema.String,
|
||||
}),
|
||||
),
|
||||
}
|
||||
@@ -132,6 +132,17 @@ export const layer: Layer.Layer<Service, never, HttpClient.HttpClient | ChildPro
|
||||
Effect.catch(() => Effect.succeed({ code: ChildProcessSpawner.ExitCode(1), stdout: "", stderr: "" })),
|
||||
)
|
||||
|
||||
const viewVersion = Effect.fnUntraced(function* (method: "npm" | "pnpm" | "bun", spec: string) {
|
||||
const args = method === "bun" ? ["pm", "view", spec, "version", "--json"] : ["view", spec, "version", "--json"]
|
||||
const result = yield* run([method, ...args])
|
||||
if (result.code !== 0 || !result.stdout.trim()) {
|
||||
return yield* new UpgradeFailedError({
|
||||
stderr: result.stderr || result.stdout || `Failed to resolve ${spec}`,
|
||||
})
|
||||
}
|
||||
return yield* Schema.decodeUnknownEffect(Schema.fromJsonString(Schema.String))(result.stdout)
|
||||
})
|
||||
|
||||
const getBrewFormula = Effect.fnUntraced(function* () {
|
||||
const tapFormula = yield* text(["brew", "list", "--formula", "anomalyco/tap/opencode"])
|
||||
if (tapFormula.includes("opencode")) return "anomalyco/tap/opencode"
|
||||
@@ -217,15 +228,7 @@ export const layer: Layer.Layer<Service, never, HttpClient.HttpClient | ChildPro
|
||||
}
|
||||
|
||||
if (detectedMethod === "npm" || detectedMethod === "bun" || detectedMethod === "pnpm") {
|
||||
const r = (yield* text(["npm", "config", "get", "registry"])).trim()
|
||||
const reg = r || "https://registry.npmjs.org"
|
||||
const registry = reg.endsWith("/") ? reg.slice(0, -1) : reg
|
||||
const channel = InstallationChannel
|
||||
const response = yield* httpOk.execute(
|
||||
HttpClientRequest.get(`${registry}/opencode-ai/${channel}`).pipe(HttpClientRequest.acceptJson),
|
||||
)
|
||||
const data = yield* HttpClientResponse.schemaBodyJson(NpmPackage)(response)
|
||||
return data.version
|
||||
return yield* viewVersion(detectedMethod, `opencode-ai@${InstallationChannel}`)
|
||||
}
|
||||
|
||||
if (detectedMethod === "choco") {
|
||||
|
||||
@@ -8,6 +8,7 @@ import { Log } from "../util"
|
||||
import { Process } from "../util"
|
||||
import { LANGUAGE_EXTENSIONS } from "./language"
|
||||
import z from "zod"
|
||||
import { Schema } from "effect"
|
||||
import type * as LSPServer from "./server"
|
||||
import { NamedError } from "@opencode-ai/shared/util/error"
|
||||
import { withTimeout } from "../util/timeout"
|
||||
@@ -41,9 +42,9 @@ export const InitializeError = NamedError.create(
|
||||
export const Event = {
|
||||
Diagnostics: BusEvent.define(
|
||||
"lsp.client.diagnostics",
|
||||
z.object({
|
||||
serverID: z.string(),
|
||||
path: z.string(),
|
||||
Schema.Struct({
|
||||
serverID: Schema.String,
|
||||
path: Schema.String,
|
||||
}),
|
||||
),
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ import { zod, ZodOverride } from "@/util/effect-zod"
|
||||
const log = Log.create({ service: "lsp" })
|
||||
|
||||
export const Event = {
|
||||
Updated: BusEvent.define("lsp.updated", z.object({})),
|
||||
Updated: BusEvent.define("lsp.updated", Schema.Struct({})),
|
||||
}
|
||||
|
||||
const Position = Schema.Struct({
|
||||
|
||||
@@ -25,7 +25,7 @@ import { BusEvent } from "../bus/bus-event"
|
||||
import { Bus } from "@/bus"
|
||||
import { TuiEvent } from "@/cli/cmd/tui/event"
|
||||
import open from "open"
|
||||
import { Effect, Exit, Layer, Option, Context, Stream } from "effect"
|
||||
import { Effect, Exit, Layer, Option, Context, Schema, Stream } from "effect"
|
||||
import { EffectBridge } from "@/effect"
|
||||
import { InstanceState } from "@/effect"
|
||||
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"
|
||||
@@ -47,16 +47,16 @@ export type Resource = z.infer<typeof Resource>
|
||||
|
||||
export const ToolsChanged = BusEvent.define(
|
||||
"mcp.tools.changed",
|
||||
z.object({
|
||||
server: z.string(),
|
||||
Schema.Struct({
|
||||
server: Schema.String,
|
||||
}),
|
||||
)
|
||||
|
||||
export const BrowserOpenFailed = BusEvent.define(
|
||||
"mcp.browser.open.failed",
|
||||
z.object({
|
||||
mcpName: z.string(),
|
||||
url: z.string(),
|
||||
Schema.Struct({
|
||||
mcpName: Schema.String,
|
||||
url: Schema.String,
|
||||
}),
|
||||
)
|
||||
|
||||
|
||||
@@ -6,12 +6,14 @@ import npa from "npm-package-arg"
|
||||
import semver from "semver"
|
||||
import Config from "@npmcli/config"
|
||||
import { definitions, flatten, nerfDarts, shorthands } from "@npmcli/config/lib/definitions/index.js"
|
||||
import { Effect, Schema, Context, Layer, Option, FileSystem } from "effect"
|
||||
import { Effect, Schema, Context, Layer, Option, FileSystem, Stream } from "effect"
|
||||
import { NodeFileSystem } from "@effect/platform-node"
|
||||
import { AppFileSystem } from "@opencode-ai/shared/filesystem"
|
||||
import { Global } from "@opencode-ai/shared/global"
|
||||
import { EffectFlock } from "@opencode-ai/shared/util/effect-flock"
|
||||
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"
|
||||
|
||||
import * as CrossSpawnSpawner from "../effect/cross-spawn-spawner"
|
||||
import { makeRuntime } from "../effect/runtime"
|
||||
|
||||
export class InstallFailedError extends Schema.TaggedErrorClass<InstallFailedError>()("NpmInstallFailedError", {
|
||||
@@ -106,7 +108,33 @@ export const layer = Layer.effect(
|
||||
const global = yield* Global.Service
|
||||
const fs = yield* FileSystem.FileSystem
|
||||
const flock = yield* EffectFlock.Service
|
||||
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner
|
||||
const directory = (pkg: string) => path.join(global.cache, "packages", sanitize(pkg))
|
||||
const runView = Effect.fnUntraced(function* (cmd: string[]) {
|
||||
const handle = yield* spawner.spawn(
|
||||
ChildProcess.make(cmd[0], cmd.slice(1), {
|
||||
extendEnv: true,
|
||||
}),
|
||||
)
|
||||
const [stdout, stderr] = yield* Effect.all(
|
||||
[Stream.mkString(Stream.decodeText(handle.stdout)), Stream.mkString(Stream.decodeText(handle.stderr))],
|
||||
{ concurrency: 2 },
|
||||
)
|
||||
const code = yield* handle.exitCode
|
||||
if (code !== 0 || !stdout.trim()) {
|
||||
return yield* Effect.fail(stderr || stdout || `Failed to run ${cmd.join(" ")}`)
|
||||
}
|
||||
return yield* Schema.decodeUnknownEffect(Schema.fromJsonString(Schema.String))(stdout)
|
||||
}, Effect.scoped)
|
||||
const viewLatestVersion = Effect.fnUntraced(function* (pkg: string) {
|
||||
return yield* runView(["npm", "view", pkg, "dist-tags.latest", "--json"]).pipe(
|
||||
Effect.catch(() =>
|
||||
runView(["pnpm", "view", pkg, "dist-tags.latest", "--json"]).pipe(
|
||||
Effect.catch(() => runView(["bun", "pm", "view", pkg, "dist-tags.latest", "--json"])),
|
||||
),
|
||||
),
|
||||
)
|
||||
})
|
||||
const reify = (input: { dir: string; add?: string[] }) =>
|
||||
Effect.gen(function* () {
|
||||
yield* flock.acquire(`npm-install:${input.dir}`)
|
||||
@@ -143,29 +171,15 @@ export const layer = Layer.effect(
|
||||
)
|
||||
|
||||
const outdated = Effect.fn("Npm.outdated")(function* (pkg: string, cachedVersion: string) {
|
||||
const response = yield* Effect.tryPromise({
|
||||
try: () => fetch(`https://registry.npmjs.org/${pkg}`),
|
||||
catch: () => undefined,
|
||||
}).pipe(Effect.orElseSucceed(() => undefined))
|
||||
|
||||
if (!response || !response.ok) {
|
||||
return false
|
||||
}
|
||||
|
||||
const data = yield* Effect.tryPromise({
|
||||
try: () => response.json() as Promise<{ "dist-tags"?: { latest?: string } }>,
|
||||
catch: () => undefined,
|
||||
}).pipe(Effect.orElseSucceed(() => undefined))
|
||||
|
||||
const latestVersion = data?.["dist-tags"]?.latest
|
||||
if (!latestVersion) {
|
||||
const latestVersion = yield* viewLatestVersion(pkg).pipe(Effect.option)
|
||||
if (Option.isNone(latestVersion)) {
|
||||
return false
|
||||
}
|
||||
|
||||
const range = /[\s^~*xX<>|=]/.test(cachedVersion)
|
||||
if (range) return !semver.satisfies(latestVersion, cachedVersion)
|
||||
if (range) return !semver.satisfies(latestVersion.value, cachedVersion)
|
||||
|
||||
return semver.lt(cachedVersion, latestVersion)
|
||||
return semver.lt(cachedVersion, latestVersion.value)
|
||||
})
|
||||
|
||||
const add = Effect.fn("Npm.add")(function* (pkg: string) {
|
||||
@@ -304,6 +318,7 @@ export const defaultLayer = layer.pipe(
|
||||
Layer.provide(AppFileSystem.layer),
|
||||
Layer.provide(Global.layer),
|
||||
Layer.provide(NodeFileSystem.layer),
|
||||
Layer.provide(CrossSpawnSpawner.defaultLayer),
|
||||
)
|
||||
|
||||
const { runPromise } = makeRuntime(Service, defaultLayer)
|
||||
|
||||
@@ -73,16 +73,14 @@ export class Approval extends Schema.Class<Approval>("PermissionApproval")({
|
||||
}
|
||||
|
||||
export const Event = {
|
||||
Asked: BusEvent.define("permission.asked", Request.zod),
|
||||
Asked: BusEvent.define("permission.asked", Request),
|
||||
Replied: BusEvent.define(
|
||||
"permission.replied",
|
||||
zod(
|
||||
Schema.Struct({
|
||||
sessionID: SessionID,
|
||||
requestID: PermissionID,
|
||||
reply: Reply,
|
||||
}),
|
||||
),
|
||||
Schema.Struct({
|
||||
sessionID: SessionID,
|
||||
requestID: PermissionID,
|
||||
reply: Reply,
|
||||
}),
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
@@ -53,7 +53,7 @@ export const Info = Schema.Struct({
|
||||
export type Info = Types.DeepMutable<Schema.Schema.Type<typeof Info>>
|
||||
|
||||
export const Event = {
|
||||
Updated: BusEvent.define("project.updated", Info.zod),
|
||||
Updated: BusEvent.define("project.updated", Info),
|
||||
}
|
||||
|
||||
type Row = typeof ProjectTable.$inferSelect
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Effect, Layer, Context, Stream, Scope } from "effect"
|
||||
import { Effect, Layer, Context, Schema, Stream, Scope } from "effect"
|
||||
import { formatPatch, structuredPatch } from "diff"
|
||||
import path from "path"
|
||||
import { Bus } from "@/bus"
|
||||
@@ -107,8 +107,8 @@ export type Mode = z.infer<typeof Mode>
|
||||
export const Event = {
|
||||
BranchUpdated: BusEvent.define(
|
||||
"vcs.branch.updated",
|
||||
z.object({
|
||||
branch: z.string().optional(),
|
||||
Schema.Struct({
|
||||
branch: Schema.optional(Schema.String),
|
||||
}),
|
||||
),
|
||||
}
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import type { AuthOAuthResult, Hooks } from "@opencode-ai/plugin"
|
||||
import { NamedError } from "@opencode-ai/shared/util/error"
|
||||
import { Auth } from "@/auth"
|
||||
import { InstanceState } from "@/effect"
|
||||
import { zod } from "@/util/effect-zod"
|
||||
import { namedSchemaError } from "@/util/named-schema-error"
|
||||
import { withStatics } from "@/util/schema"
|
||||
import { Plugin } from "../plugin"
|
||||
import { ProviderID } from "./schema"
|
||||
import { Array as Arr, Effect, Layer, Record, Result, Context, Schema } from "effect"
|
||||
import z from "zod"
|
||||
|
||||
const When = Schema.Struct({
|
||||
key: Schema.String,
|
||||
@@ -70,22 +69,16 @@ export const CallbackInput = Schema.Struct({
|
||||
}).pipe(withStatics((s) => ({ zod: zod(s) })))
|
||||
export type CallbackInput = Schema.Schema.Type<typeof CallbackInput>
|
||||
|
||||
export const OauthMissing = NamedError.create("ProviderAuthOauthMissing", z.object({ providerID: ProviderID.zod }))
|
||||
export const OauthMissing = namedSchemaError("ProviderAuthOauthMissing", { providerID: ProviderID })
|
||||
|
||||
export const OauthCodeMissing = NamedError.create(
|
||||
"ProviderAuthOauthCodeMissing",
|
||||
z.object({ providerID: ProviderID.zod }),
|
||||
)
|
||||
export const OauthCodeMissing = namedSchemaError("ProviderAuthOauthCodeMissing", { providerID: ProviderID })
|
||||
|
||||
export const OauthCallbackFailed = NamedError.create("ProviderAuthOauthCallbackFailed", z.object({}))
|
||||
export const OauthCallbackFailed = namedSchemaError("ProviderAuthOauthCallbackFailed", {})
|
||||
|
||||
export const ValidationFailed = NamedError.create(
|
||||
"ProviderAuthValidationFailed",
|
||||
z.object({
|
||||
field: z.string(),
|
||||
message: z.string(),
|
||||
}),
|
||||
)
|
||||
export const ValidationFailed = namedSchemaError("ProviderAuthValidationFailed", {
|
||||
field: Schema.String,
|
||||
message: Schema.String,
|
||||
})
|
||||
|
||||
export type Error =
|
||||
| Auth.AuthError
|
||||
|
||||
@@ -111,12 +111,13 @@ export type ParsedStreamError =
|
||||
| {
|
||||
type: "api_error"
|
||||
message: string
|
||||
isRetryable: false
|
||||
isRetryable: boolean
|
||||
responseBody: string
|
||||
}
|
||||
|
||||
export function parseStreamError(input: unknown): ParsedStreamError | undefined {
|
||||
const body = json(input)
|
||||
const raw = json(input)
|
||||
const body = typeof raw?.message === "string" ? (json(raw.message) ?? raw) : raw
|
||||
if (!body) return
|
||||
|
||||
const responseBody = JSON.stringify(body)
|
||||
@@ -150,6 +151,13 @@ export function parseStreamError(input: unknown): ParsedStreamError | undefined
|
||||
isRetryable: false,
|
||||
responseBody,
|
||||
}
|
||||
case "server_error":
|
||||
return {
|
||||
type: "api_error",
|
||||
message: typeof body?.error?.message === "string" ? body?.error?.message : "Server error.",
|
||||
isRetryable: true,
|
||||
responseBody,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Global } from "../global"
|
||||
import { Log } from "../util"
|
||||
import path from "path"
|
||||
import z from "zod"
|
||||
import { Schema } from "effect"
|
||||
import { Installation } from "../installation"
|
||||
import { Flag } from "../flag/flag"
|
||||
import { lazy } from "@/util/lazy"
|
||||
@@ -21,91 +21,85 @@ const filepath = path.join(
|
||||
)
|
||||
const ttl = 5 * 60 * 1000
|
||||
|
||||
type JsonValue = string | number | boolean | null | { [key: string]: JsonValue } | JsonValue[]
|
||||
|
||||
const JsonValue: z.ZodType<JsonValue> = z.lazy(() =>
|
||||
z.union([z.string(), z.number(), z.boolean(), z.null(), z.array(JsonValue), z.record(z.string(), JsonValue)]),
|
||||
)
|
||||
|
||||
const Cost = z.object({
|
||||
input: z.number(),
|
||||
output: z.number(),
|
||||
cache_read: z.number().optional(),
|
||||
cache_write: z.number().optional(),
|
||||
context_over_200k: z
|
||||
.object({
|
||||
input: z.number(),
|
||||
output: z.number(),
|
||||
cache_read: z.number().optional(),
|
||||
cache_write: z.number().optional(),
|
||||
})
|
||||
.optional(),
|
||||
const Cost = Schema.Struct({
|
||||
input: Schema.Number,
|
||||
output: Schema.Number,
|
||||
cache_read: Schema.optional(Schema.Number),
|
||||
cache_write: Schema.optional(Schema.Number),
|
||||
context_over_200k: Schema.optional(
|
||||
Schema.Struct({
|
||||
input: Schema.Number,
|
||||
output: Schema.Number,
|
||||
cache_read: Schema.optional(Schema.Number),
|
||||
cache_write: Schema.optional(Schema.Number),
|
||||
}),
|
||||
),
|
||||
})
|
||||
|
||||
export const Model = z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
family: z.string().optional(),
|
||||
release_date: z.string(),
|
||||
attachment: z.boolean(),
|
||||
reasoning: z.boolean(),
|
||||
temperature: z.boolean(),
|
||||
tool_call: z.boolean(),
|
||||
interleaved: z
|
||||
.union([
|
||||
z.literal(true),
|
||||
z
|
||||
.object({
|
||||
field: z.enum(["reasoning_content", "reasoning_details"]),
|
||||
})
|
||||
.strict(),
|
||||
])
|
||||
.optional(),
|
||||
cost: Cost.optional(),
|
||||
limit: z.object({
|
||||
context: z.number(),
|
||||
input: z.number().optional(),
|
||||
output: z.number(),
|
||||
export const Model = Schema.Struct({
|
||||
id: Schema.String,
|
||||
name: Schema.String,
|
||||
family: Schema.optional(Schema.String),
|
||||
release_date: Schema.String,
|
||||
attachment: Schema.Boolean,
|
||||
reasoning: Schema.Boolean,
|
||||
temperature: Schema.Boolean,
|
||||
tool_call: Schema.Boolean,
|
||||
interleaved: Schema.optional(
|
||||
Schema.Union([
|
||||
Schema.Literal(true),
|
||||
Schema.Struct({
|
||||
field: Schema.Literals(["reasoning_content", "reasoning_details"]),
|
||||
}),
|
||||
]),
|
||||
),
|
||||
cost: Schema.optional(Cost),
|
||||
limit: Schema.Struct({
|
||||
context: Schema.Number,
|
||||
input: Schema.optional(Schema.Number),
|
||||
output: Schema.Number,
|
||||
}),
|
||||
modalities: z
|
||||
.object({
|
||||
input: z.array(z.enum(["text", "audio", "image", "video", "pdf"])),
|
||||
output: z.array(z.enum(["text", "audio", "image", "video", "pdf"])),
|
||||
})
|
||||
.optional(),
|
||||
experimental: z
|
||||
.object({
|
||||
modes: z
|
||||
.record(
|
||||
z.string(),
|
||||
z.object({
|
||||
cost: Cost.optional(),
|
||||
provider: z
|
||||
.object({
|
||||
body: z.record(z.string(), JsonValue).optional(),
|
||||
headers: z.record(z.string(), z.string()).optional(),
|
||||
})
|
||||
.optional(),
|
||||
modalities: Schema.optional(
|
||||
Schema.Struct({
|
||||
input: Schema.Array(Schema.Literals(["text", "audio", "image", "video", "pdf"])),
|
||||
output: Schema.Array(Schema.Literals(["text", "audio", "image", "video", "pdf"])),
|
||||
}),
|
||||
),
|
||||
experimental: Schema.optional(
|
||||
Schema.Struct({
|
||||
modes: Schema.optional(
|
||||
Schema.Record(
|
||||
Schema.String,
|
||||
Schema.Struct({
|
||||
cost: Schema.optional(Cost),
|
||||
provider: Schema.optional(
|
||||
Schema.Struct({
|
||||
body: Schema.optional(Schema.Record(Schema.String, Schema.MutableJson)),
|
||||
headers: Schema.optional(Schema.Record(Schema.String, Schema.String)),
|
||||
}),
|
||||
),
|
||||
}),
|
||||
)
|
||||
.optional(),
|
||||
})
|
||||
.optional(),
|
||||
status: z.enum(["alpha", "beta", "deprecated"]).optional(),
|
||||
provider: z.object({ npm: z.string().optional(), api: z.string().optional() }).optional(),
|
||||
),
|
||||
),
|
||||
}),
|
||||
),
|
||||
status: Schema.optional(Schema.Literals(["alpha", "beta", "deprecated"])),
|
||||
provider: Schema.optional(
|
||||
Schema.Struct({ npm: Schema.optional(Schema.String), api: Schema.optional(Schema.String) }),
|
||||
),
|
||||
})
|
||||
export type Model = z.infer<typeof Model>
|
||||
export type Model = Schema.Schema.Type<typeof Model>
|
||||
|
||||
export const Provider = z.object({
|
||||
api: z.string().optional(),
|
||||
name: z.string(),
|
||||
env: z.array(z.string()),
|
||||
id: z.string(),
|
||||
npm: z.string().optional(),
|
||||
models: z.record(z.string(), Model),
|
||||
export const Provider = Schema.Struct({
|
||||
api: Schema.optional(Schema.String),
|
||||
name: Schema.String,
|
||||
env: Schema.Array(Schema.String),
|
||||
id: Schema.String,
|
||||
npm: Schema.optional(Schema.String),
|
||||
models: Schema.Record(Schema.String, Model),
|
||||
})
|
||||
|
||||
export type Provider = z.infer<typeof Provider>
|
||||
export type Provider = Schema.Schema.Type<typeof Provider>
|
||||
|
||||
function url() {
|
||||
return Flag.OPENCODE_MODELS_URL || "https://models.dev"
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import z from "zod"
|
||||
import os from "os"
|
||||
import fuzzysort from "fuzzysort"
|
||||
import { Config } from "../config"
|
||||
@@ -8,7 +7,6 @@ import { Log } from "../util"
|
||||
import { Npm } from "../npm"
|
||||
import { Hash } from "@opencode-ai/shared/util/hash"
|
||||
import { Plugin } from "../plugin"
|
||||
import { NamedError } from "@opencode-ai/shared/util/error"
|
||||
import { type LanguageModelV3 } from "@ai-sdk/provider"
|
||||
import * as ModelsDev from "./models"
|
||||
import { Auth } from "../auth"
|
||||
@@ -16,6 +14,7 @@ import { Env } from "../env"
|
||||
import { InstallationVersion } from "../installation/version"
|
||||
import { Flag } from "../flag/flag"
|
||||
import { zod } from "@/util/effect-zod"
|
||||
import { namedSchemaError } from "@/util/named-schema-error"
|
||||
import { iife } from "@/util/iife"
|
||||
import { Global } from "../global"
|
||||
import path from "path"
|
||||
@@ -1047,7 +1046,7 @@ export function fromModelsDevProvider(provider: ModelsDev.Provider): Info {
|
||||
id: ProviderID.make(provider.id),
|
||||
source: "custom",
|
||||
name: provider.name,
|
||||
env: provider.env ?? [],
|
||||
env: [...(provider.env ?? [])],
|
||||
options: {},
|
||||
models,
|
||||
}
|
||||
@@ -1713,18 +1712,12 @@ export function parseModel(model: string) {
|
||||
}
|
||||
}
|
||||
|
||||
export const ModelNotFoundError = NamedError.create(
|
||||
"ProviderModelNotFoundError",
|
||||
z.object({
|
||||
providerID: ProviderID.zod,
|
||||
modelID: ModelID.zod,
|
||||
suggestions: z.array(z.string()).optional(),
|
||||
}),
|
||||
)
|
||||
export const ModelNotFoundError = namedSchemaError("ProviderModelNotFoundError", {
|
||||
providerID: ProviderID,
|
||||
modelID: ModelID,
|
||||
suggestions: Schema.optional(Schema.Array(Schema.String)),
|
||||
})
|
||||
|
||||
export const InitError = NamedError.create(
|
||||
"ProviderInitError",
|
||||
z.object({
|
||||
providerID: ProviderID.zod,
|
||||
}),
|
||||
)
|
||||
export const InitError = namedSchemaError("ProviderInitError", {
|
||||
providerID: ProviderID,
|
||||
})
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
README.md
|
||||
@@ -1,5 +1,5 @@
|
||||
This is a temporary package used primarily for GitHub Copilot compatibility.
|
||||
|
||||
Avoid making changes to these files unless you only want to affect the Copilot provider.
|
||||
These DO NOT apply for openai-compatible providers or majority of providers supporting completions/responses apis. THIS IS ONLY FOR GITHUB COPILOT!!!
|
||||
|
||||
Also, this should ONLY be used for the Copilot provider.
|
||||
Avoid making edits to these files
|
||||
|
||||
@@ -185,24 +185,19 @@ function normalizeMessages(
|
||||
// Filter out reasoning parts from content
|
||||
const filteredContent = msg.content.filter((part: any) => part.type !== "reasoning")
|
||||
|
||||
// Include reasoning_content | reasoning_details directly on the message for all assistant messages
|
||||
if (reasoningText) {
|
||||
return {
|
||||
...msg,
|
||||
content: filteredContent,
|
||||
providerOptions: {
|
||||
...msg.providerOptions,
|
||||
openaiCompatible: {
|
||||
...msg.providerOptions?.openaiCompatible,
|
||||
[field]: reasoningText,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Include reasoning_content | reasoning_details directly on the message for all assistant messages.
|
||||
// Always set the field even when empty — some providers (e.g. DeepSeek) may return empty
|
||||
// reasoning_content which still needs to be sent back in subsequent requests.
|
||||
return {
|
||||
...msg,
|
||||
content: filteredContent,
|
||||
providerOptions: {
|
||||
...msg.providerOptions,
|
||||
openaiCompatible: {
|
||||
...msg.providerOptions?.openaiCompatible,
|
||||
[field]: reasoningText,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -405,7 +400,10 @@ export function variants(model: Provider.Model): Record<string, Record<string, a
|
||||
const id = model.id.toLowerCase()
|
||||
const adaptiveEfforts = anthropicAdaptiveEfforts(model.api.id)
|
||||
if (
|
||||
id.includes("deepseek") ||
|
||||
id.includes("deepseek-chat") ||
|
||||
id.includes("deepseek-reasoner") ||
|
||||
id.includes("deepseek-r1") ||
|
||||
id.includes("deepseek-v3") ||
|
||||
id.includes("minimax") ||
|
||||
id.includes("glm") ||
|
||||
id.includes("kimi") ||
|
||||
@@ -531,7 +529,11 @@ export function variants(model: Provider.Model): Record<string, Record<string, a
|
||||
case "venice-ai-sdk-provider":
|
||||
// https://docs.venice.ai/overview/guides/reasoning-models#reasoning-effort
|
||||
case "@ai-sdk/openai-compatible":
|
||||
return Object.fromEntries(WIDELY_SUPPORTED_EFFORTS.map((effort) => [effort, { reasoningEffort: effort }]))
|
||||
const efforts = [...WIDELY_SUPPORTED_EFFORTS]
|
||||
if (model.api.id.includes("deepseek-v4")) {
|
||||
efforts.push("max")
|
||||
}
|
||||
return Object.fromEntries(efforts.map((effort) => [effort, { reasoningEffort: effort }]))
|
||||
|
||||
case "@ai-sdk/azure":
|
||||
// https://v5.ai-sdk.dev/providers/ai-sdk-providers/azure
|
||||
|
||||
@@ -3,13 +3,14 @@ import { Bus } from "@/bus"
|
||||
import { InstanceState } from "@/effect"
|
||||
import { Instance } from "@/project/instance"
|
||||
import type { Proc } from "#pty"
|
||||
import z from "zod"
|
||||
import { Log } from "../util"
|
||||
import { lazy } from "@opencode-ai/shared/util/lazy"
|
||||
import { Shell } from "@/shell/shell"
|
||||
import { Plugin } from "@/plugin"
|
||||
import { PtyID } from "./schema"
|
||||
import { Effect, Layer, Context } from "effect"
|
||||
import { Effect, Layer, Context, Schema, Types } from "effect"
|
||||
import { zod } from "@/util/effect-zod"
|
||||
import { withStatics } from "@/util/schema"
|
||||
import { EffectBridge } from "@/effect"
|
||||
|
||||
const log = Log.create({ service: "pty" })
|
||||
@@ -53,47 +54,47 @@ const meta = (cursor: number) => {
|
||||
|
||||
const pty = lazy(() => import("#pty"))
|
||||
|
||||
export const Info = z
|
||||
.object({
|
||||
id: PtyID.zod,
|
||||
title: z.string(),
|
||||
command: z.string(),
|
||||
args: z.array(z.string()),
|
||||
cwd: z.string(),
|
||||
status: z.enum(["running", "exited"]),
|
||||
pid: z.number(),
|
||||
})
|
||||
.meta({ ref: "Pty" })
|
||||
|
||||
export type Info = z.infer<typeof Info>
|
||||
|
||||
export const CreateInput = z.object({
|
||||
command: z.string().optional(),
|
||||
args: z.array(z.string()).optional(),
|
||||
cwd: z.string().optional(),
|
||||
title: z.string().optional(),
|
||||
env: z.record(z.string(), z.string()).optional(),
|
||||
export const Info = Schema.Struct({
|
||||
id: PtyID,
|
||||
title: Schema.String,
|
||||
command: Schema.String,
|
||||
args: Schema.Array(Schema.String),
|
||||
cwd: Schema.String,
|
||||
status: Schema.Literals(["running", "exited"]),
|
||||
pid: Schema.Number,
|
||||
})
|
||||
.annotate({ identifier: "Pty" })
|
||||
.pipe(withStatics((s) => ({ zod: zod(s) })))
|
||||
|
||||
export type CreateInput = z.infer<typeof CreateInput>
|
||||
export type Info = Types.DeepMutable<Schema.Schema.Type<typeof Info>>
|
||||
|
||||
export const UpdateInput = z.object({
|
||||
title: z.string().optional(),
|
||||
size: z
|
||||
.object({
|
||||
rows: z.number(),
|
||||
cols: z.number(),
|
||||
})
|
||||
.optional(),
|
||||
})
|
||||
export const CreateInput = Schema.Struct({
|
||||
command: Schema.optional(Schema.String),
|
||||
args: Schema.optional(Schema.Array(Schema.String)),
|
||||
cwd: Schema.optional(Schema.String),
|
||||
title: Schema.optional(Schema.String),
|
||||
env: Schema.optional(Schema.Record(Schema.String, Schema.String)),
|
||||
}).pipe(withStatics((s) => ({ zod: zod(s) })))
|
||||
|
||||
export type UpdateInput = z.infer<typeof UpdateInput>
|
||||
export type CreateInput = Types.DeepMutable<Schema.Schema.Type<typeof CreateInput>>
|
||||
|
||||
export const UpdateInput = Schema.Struct({
|
||||
title: Schema.optional(Schema.String),
|
||||
size: Schema.optional(
|
||||
Schema.Struct({
|
||||
rows: Schema.Number,
|
||||
cols: Schema.Number,
|
||||
}),
|
||||
),
|
||||
}).pipe(withStatics((s) => ({ zod: zod(s) })))
|
||||
|
||||
export type UpdateInput = Types.DeepMutable<Schema.Schema.Type<typeof UpdateInput>>
|
||||
|
||||
export const Event = {
|
||||
Created: BusEvent.define("pty.created", z.object({ info: Info })),
|
||||
Updated: BusEvent.define("pty.updated", z.object({ info: Info })),
|
||||
Exited: BusEvent.define("pty.exited", z.object({ id: PtyID.zod, exitCode: z.number() })),
|
||||
Deleted: BusEvent.define("pty.deleted", z.object({ id: PtyID.zod })),
|
||||
Created: BusEvent.define("pty.created", Schema.Struct({ info: Info })),
|
||||
Updated: BusEvent.define("pty.updated", Schema.Struct({ info: Info })),
|
||||
Exited: BusEvent.define("pty.exited", Schema.Struct({ id: PtyID, exitCode: Schema.Number })),
|
||||
Deleted: BusEvent.define("pty.deleted", Schema.Struct({ id: PtyID })),
|
||||
}
|
||||
|
||||
export interface Interface {
|
||||
|
||||
@@ -94,9 +94,9 @@ class Rejected extends Schema.Class<Rejected>("QuestionRejected")({
|
||||
}) {}
|
||||
|
||||
export const Event = {
|
||||
Asked: BusEvent.define("question.asked", Request.zod),
|
||||
Replied: BusEvent.define("question.replied", zod(Replied)),
|
||||
Rejected: BusEvent.define("question.rejected", zod(Rejected)),
|
||||
Asked: BusEvent.define("question.asked", Request),
|
||||
Replied: BusEvent.define("question.replied", Replied),
|
||||
Rejected: BusEvent.define("question.rejected", Rejected),
|
||||
}
|
||||
|
||||
export class RejectedError extends Schema.TaggedErrorClass<RejectedError>()("QuestionRejectedError", {}) {
|
||||
@@ -194,7 +194,7 @@ export const layer = Layer.effect(
|
||||
yield* bus.publish(Event.Replied, {
|
||||
sessionID: existing.info.sessionID,
|
||||
requestID: existing.info.id,
|
||||
answers: input.answers,
|
||||
answers: input.answers.map((a) => [...a]),
|
||||
})
|
||||
yield* Deferred.succeed(existing.deferred, input.answers)
|
||||
})
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { BusEvent } from "@/bus/bus-event"
|
||||
import z from "zod"
|
||||
import { Schema } from "effect"
|
||||
|
||||
export const Event = {
|
||||
Connected: BusEvent.define("server.connected", z.object({})),
|
||||
Disposed: BusEvent.define("global.disposed", z.object({})),
|
||||
Connected: BusEvent.define("server.connected", Schema.Struct({})),
|
||||
Disposed: BusEvent.define("global.disposed", Schema.Struct({})),
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import z from "zod"
|
||||
import sessionProjectors from "../session/projectors"
|
||||
import { SyncEvent } from "@/sync"
|
||||
import { Session } from "@/session"
|
||||
@@ -10,7 +9,7 @@ export function initProjectors() {
|
||||
projectors: sessionProjectors,
|
||||
convertEvent: (type, data) => {
|
||||
if (type === "session.updated") {
|
||||
const id = (data as z.infer<typeof Session.Event.Updated.schema>).sessionID
|
||||
const id = (data as SyncEvent.Event<typeof Session.Event.Updated>["data"]).sessionID
|
||||
const row = Database.use((db) => db.select().from(SessionTable).where(eq(SessionTable.id, id)).get())
|
||||
|
||||
if (!row) return data
|
||||
|
||||
@@ -3,6 +3,8 @@ import { describeRoute, resolver, validator } from "hono-openapi"
|
||||
import z from "zod"
|
||||
import { listAdaptors } from "@/control-plane/adaptors"
|
||||
import { Workspace } from "@/control-plane/workspace"
|
||||
import { WorkspaceAdaptorEntry } from "@/control-plane/types"
|
||||
import { zodObject } from "@/util/effect-zod"
|
||||
import { Instance } from "@/project/instance"
|
||||
import { errors } from "../../error"
|
||||
import { lazy } from "@/util/lazy"
|
||||
@@ -24,15 +26,7 @@ export const WorkspaceRoutes = lazy(() =>
|
||||
description: "Workspace adaptors",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(
|
||||
z.array(
|
||||
z.object({
|
||||
type: z.string(),
|
||||
name: z.string(),
|
||||
description: z.string(),
|
||||
}),
|
||||
),
|
||||
),
|
||||
schema: resolver(z.array(zodObject(WorkspaceAdaptorEntry))),
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -53,7 +47,7 @@ export const WorkspaceRoutes = lazy(() =>
|
||||
description: "Workspace created",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(Workspace.Info),
|
||||
schema: resolver(Workspace.Info.zod),
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -62,12 +56,12 @@ export const WorkspaceRoutes = lazy(() =>
|
||||
}),
|
||||
validator(
|
||||
"json",
|
||||
Workspace.create.schema.omit({
|
||||
Workspace.CreateInput.zodObject.omit({
|
||||
projectID: true,
|
||||
}),
|
||||
),
|
||||
async (c) => {
|
||||
const body = c.req.valid("json")
|
||||
const body = c.req.valid("json") as Omit<Workspace.CreateInput, "projectID">
|
||||
const workspace = await Workspace.create({
|
||||
projectID: Instance.project.id,
|
||||
...body,
|
||||
@@ -86,7 +80,7 @@ export const WorkspaceRoutes = lazy(() =>
|
||||
description: "Workspaces",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(z.array(Workspace.Info)),
|
||||
schema: resolver(z.array(Workspace.Info.zod)),
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -107,7 +101,7 @@ export const WorkspaceRoutes = lazy(() =>
|
||||
description: "Workspace status",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(z.array(Workspace.ConnectionStatus)),
|
||||
schema: resolver(z.array(zodObject(Workspace.ConnectionStatus))),
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -129,7 +123,7 @@ export const WorkspaceRoutes = lazy(() =>
|
||||
description: "Workspace removed",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(Workspace.Info.optional()),
|
||||
schema: resolver(Workspace.Info.zod.optional()),
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -139,7 +133,7 @@ export const WorkspaceRoutes = lazy(() =>
|
||||
validator(
|
||||
"param",
|
||||
z.object({
|
||||
id: Workspace.Info.shape.id,
|
||||
id: zodObject(Workspace.Info).shape.id,
|
||||
}),
|
||||
),
|
||||
async (c) => {
|
||||
@@ -169,11 +163,11 @@ export const WorkspaceRoutes = lazy(() =>
|
||||
...errors(400),
|
||||
},
|
||||
}),
|
||||
validator("param", z.object({ id: Workspace.Info.shape.id })),
|
||||
validator("json", Workspace.sessionRestore.schema.omit({ workspaceID: true })),
|
||||
validator("param", z.object({ id: zodObject(Workspace.Info).shape.id })),
|
||||
validator("json", Workspace.SessionRestoreInput.zodObject.omit({ workspaceID: true })),
|
||||
async (c) => {
|
||||
const { id } = c.req.valid("param")
|
||||
const body = c.req.valid("json")
|
||||
const body = c.req.valid("json") as Omit<Workspace.SessionRestoreInput, "workspaceID">
|
||||
log.info("session restore route requested", {
|
||||
workspaceID: id,
|
||||
sessionID: body.sessionID,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Hono, type Context } from "hono"
|
||||
import { describeRoute, resolver, validator } from "hono-openapi"
|
||||
import { streamSSE } from "hono/streaming"
|
||||
import { Effect } from "effect"
|
||||
import { Effect, Schema } from "effect"
|
||||
import z from "zod"
|
||||
import { BusEvent } from "@/bus/bus-event"
|
||||
import { SyncEvent } from "@/sync"
|
||||
@@ -18,7 +18,7 @@ import { errors } from "../error"
|
||||
|
||||
const log = Log.create({ service: "server" })
|
||||
|
||||
export const GlobalDisposedEvent = BusEvent.define("global.disposed", z.object({}))
|
||||
export const GlobalDisposedEvent = BusEvent.define("global.disposed", Schema.Struct({}))
|
||||
|
||||
async function streamEvents(c: Context, subscribe: (q: AsyncQueue<string | null>) => () => void) {
|
||||
return streamSSE(c, async (stream) => {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Hono } from "hono"
|
||||
import { describeRoute, validator, resolver } from "hono-openapi"
|
||||
import z from "zod"
|
||||
import * as EffectZod from "@/util/effect-zod"
|
||||
import { ProviderID, ModelID } from "@/provider/schema"
|
||||
import { ToolRegistry } from "@/tool"
|
||||
import { Worktree } from "@/worktree"
|
||||
@@ -213,7 +214,7 @@ export const ExperimentalRoutes = lazy(() =>
|
||||
tools.map((t) => ({
|
||||
id: t.id,
|
||||
description: t.description,
|
||||
parameters: z.toJSONSchema(t.parameters),
|
||||
parameters: EffectZod.toJsonSchema(t.parameters),
|
||||
})),
|
||||
)
|
||||
},
|
||||
@@ -335,7 +336,7 @@ export const ExperimentalRoutes = lazy(() =>
|
||||
description: "List of sessions",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(Session.GlobalInfo.array()),
|
||||
schema: resolver(Session.GlobalInfo.zod.array()),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -14,6 +14,7 @@ import { PermissionApi, permissionHandlers } from "./permission"
|
||||
import { ProjectApi, projectHandlers } from "./project"
|
||||
import { ProviderApi, providerHandlers } from "./provider"
|
||||
import { QuestionApi, questionHandlers } from "./question"
|
||||
import { WorkspaceApi, workspaceHandlers } from "./workspace"
|
||||
import { memoMap } from "@/effect/memo-map"
|
||||
|
||||
const Query = Schema.Struct({
|
||||
@@ -112,6 +113,7 @@ const PermissionSecured = PermissionApi.middleware(Authorization)
|
||||
const ProjectSecured = ProjectApi.middleware(Authorization)
|
||||
const ProviderSecured = ProviderApi.middleware(Authorization)
|
||||
const ConfigSecured = ConfigApi.middleware(Authorization)
|
||||
const WorkspaceSecured = WorkspaceApi.middleware(Authorization)
|
||||
|
||||
export const routes = Layer.mergeAll(
|
||||
HttpApiBuilder.layer(ConfigSecured).pipe(Layer.provide(configHandlers)),
|
||||
@@ -119,6 +121,7 @@ export const routes = Layer.mergeAll(
|
||||
HttpApiBuilder.layer(QuestionSecured).pipe(Layer.provide(questionHandlers)),
|
||||
HttpApiBuilder.layer(PermissionSecured).pipe(Layer.provide(permissionHandlers)),
|
||||
HttpApiBuilder.layer(ProviderSecured).pipe(Layer.provide(providerHandlers)),
|
||||
HttpApiBuilder.layer(WorkspaceSecured).pipe(Layer.provide(workspaceHandlers)),
|
||||
).pipe(
|
||||
Layer.provide(auth),
|
||||
Layer.provide(normalize),
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
import { listAdaptors } from "@/control-plane/adaptors"
|
||||
import { Workspace } from "@/control-plane/workspace"
|
||||
import { WorkspaceAdaptorEntry } from "@/control-plane/types"
|
||||
import * as InstanceState from "@/effect/instance-state"
|
||||
import { Effect, Layer, Schema } from "effect"
|
||||
import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
|
||||
|
||||
const root = "/experimental/workspace"
|
||||
export const WorkspacePaths = {
|
||||
adaptors: `${root}/adaptor`,
|
||||
list: root,
|
||||
status: `${root}/status`,
|
||||
} as const
|
||||
|
||||
export const WorkspaceApi = HttpApi.make("workspace")
|
||||
.add(
|
||||
HttpApiGroup.make("workspace")
|
||||
.add(
|
||||
HttpApiEndpoint.get("adaptors", WorkspacePaths.adaptors, {
|
||||
success: Schema.Array(WorkspaceAdaptorEntry),
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "experimental.workspace.adaptor.list",
|
||||
summary: "List workspace adaptors",
|
||||
description: "List all available workspace adaptors for the current project.",
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.get("list", WorkspacePaths.list, {
|
||||
success: Schema.Array(Workspace.Info),
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "experimental.workspace.list",
|
||||
summary: "List workspaces",
|
||||
description: "List all workspaces.",
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.get("status", WorkspacePaths.status, {
|
||||
success: Schema.Array(Workspace.ConnectionStatus),
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "experimental.workspace.status",
|
||||
summary: "Workspace status",
|
||||
description: "Get connection status for workspaces in the current project.",
|
||||
}),
|
||||
),
|
||||
)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
title: "workspace",
|
||||
description: "Experimental HttpApi workspace routes.",
|
||||
}),
|
||||
),
|
||||
)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
title: "opencode experimental HttpApi",
|
||||
version: "0.0.1",
|
||||
description: "Experimental HttpApi surface for selected instance routes.",
|
||||
}),
|
||||
)
|
||||
|
||||
export const workspaceHandlers = Layer.unwrap(
|
||||
Effect.gen(function* () {
|
||||
const adaptors = Effect.fn("WorkspaceHttpApi.adaptors")(function* () {
|
||||
const ctx = yield* InstanceState.context
|
||||
return yield* Effect.promise(() => listAdaptors(ctx.project.id))
|
||||
})
|
||||
|
||||
const list = Effect.fn("WorkspaceHttpApi.list")(function* () {
|
||||
return Workspace.list((yield* InstanceState.context).project)
|
||||
})
|
||||
|
||||
const status = Effect.fn("WorkspaceHttpApi.status")(function* () {
|
||||
const ids = new Set(Workspace.list((yield* InstanceState.context).project).map((item) => item.id))
|
||||
return Workspace.status().filter((item) => ids.has(item.workspaceID))
|
||||
})
|
||||
|
||||
return HttpApiBuilder.group(WorkspaceApi, "workspace", (handlers) =>
|
||||
handlers.handle("adaptors", adaptors).handle("list", list).handle("status", status),
|
||||
)
|
||||
}),
|
||||
)
|
||||
@@ -23,7 +23,7 @@ export function PtyRoutes(upgradeWebSocket: UpgradeWebSocket) {
|
||||
description: "List of sessions",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(Pty.Info.array()),
|
||||
schema: resolver(Pty.Info.zod.array()),
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -46,18 +46,18 @@ export function PtyRoutes(upgradeWebSocket: UpgradeWebSocket) {
|
||||
description: "Created session",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(Pty.Info),
|
||||
schema: resolver(Pty.Info.zod),
|
||||
},
|
||||
},
|
||||
},
|
||||
...errors(400),
|
||||
},
|
||||
}),
|
||||
validator("json", Pty.CreateInput),
|
||||
validator("json", Pty.CreateInput.zod),
|
||||
async (c) =>
|
||||
jsonRequest("PtyRoutes.create", c, function* () {
|
||||
const pty = yield* Pty.Service
|
||||
return yield* pty.create(c.req.valid("json"))
|
||||
return yield* pty.create(c.req.valid("json") as Pty.CreateInput)
|
||||
}),
|
||||
)
|
||||
.get(
|
||||
@@ -71,7 +71,7 @@ export function PtyRoutes(upgradeWebSocket: UpgradeWebSocket) {
|
||||
description: "Session info",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(Pty.Info),
|
||||
schema: resolver(Pty.Info.zod),
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -105,7 +105,7 @@ export function PtyRoutes(upgradeWebSocket: UpgradeWebSocket) {
|
||||
description: "Updated session",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(Pty.Info),
|
||||
schema: resolver(Pty.Info.zod),
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -113,11 +113,11 @@ export function PtyRoutes(upgradeWebSocket: UpgradeWebSocket) {
|
||||
},
|
||||
}),
|
||||
validator("param", z.object({ ptyID: PtyID.zod })),
|
||||
validator("json", Pty.UpdateInput),
|
||||
validator("json", Pty.UpdateInput.zod),
|
||||
async (c) =>
|
||||
jsonRequest("PtyRoutes.update", c, function* () {
|
||||
const pty = yield* Pty.Service
|
||||
return yield* pty.update(c.req.valid("param").ptyID, c.req.valid("json"))
|
||||
return yield* pty.update(c.req.valid("param").ptyID, c.req.valid("json") as Pty.UpdateInput)
|
||||
}),
|
||||
)
|
||||
.delete(
|
||||
|
||||
@@ -23,6 +23,7 @@ import { PermissionID } from "@/permission/schema"
|
||||
import { ModelID, ProviderID } from "@/provider/schema"
|
||||
import { errors } from "../../error"
|
||||
import { lazy } from "@/util/lazy"
|
||||
import { zodObject } from "@/util/effect-zod"
|
||||
import { Bus } from "@/bus"
|
||||
import { NamedError } from "@opencode-ai/shared/util/error"
|
||||
import { jsonRequest, runRequest } from "./trace"
|
||||
@@ -42,7 +43,7 @@ export const SessionRoutes = lazy(() =>
|
||||
description: "List of sessions",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(Session.Info.array()),
|
||||
schema: resolver(Session.Info.zod.array()),
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -87,7 +88,7 @@ export const SessionRoutes = lazy(() =>
|
||||
description: "Get session status",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(z.record(z.string(), SessionStatus.Info)),
|
||||
schema: resolver(z.record(z.string(), SessionStatus.Info.zod)),
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -112,7 +113,7 @@ export const SessionRoutes = lazy(() =>
|
||||
description: "Get session",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(Session.Info),
|
||||
schema: resolver(Session.Info.zod),
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -122,7 +123,7 @@ export const SessionRoutes = lazy(() =>
|
||||
validator(
|
||||
"param",
|
||||
z.object({
|
||||
sessionID: Session.GetInput,
|
||||
sessionID: Session.GetInput.zod,
|
||||
}),
|
||||
),
|
||||
async (c) => {
|
||||
@@ -145,7 +146,7 @@ export const SessionRoutes = lazy(() =>
|
||||
description: "List of children",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(Session.Info.array()),
|
||||
schema: resolver(Session.Info.zod.array()),
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -155,7 +156,7 @@ export const SessionRoutes = lazy(() =>
|
||||
validator(
|
||||
"param",
|
||||
z.object({
|
||||
sessionID: Session.ChildrenInput,
|
||||
sessionID: Session.ChildrenInput.zod,
|
||||
}),
|
||||
),
|
||||
async (c) => {
|
||||
@@ -177,7 +178,7 @@ export const SessionRoutes = lazy(() =>
|
||||
description: "Todo list",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(Todo.Info.array()),
|
||||
schema: resolver(Todo.Info.zod.array()),
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -210,13 +211,13 @@ export const SessionRoutes = lazy(() =>
|
||||
description: "Successfully created session",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(Session.Info),
|
||||
schema: resolver(Session.Info.zod),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
validator("json", Session.CreateInput),
|
||||
validator("json", Session.CreateInput.zod),
|
||||
async (c) =>
|
||||
jsonRequest("SessionRoutes.create", c, function* () {
|
||||
const body = c.req.valid("json") ?? {}
|
||||
@@ -245,7 +246,7 @@ export const SessionRoutes = lazy(() =>
|
||||
validator(
|
||||
"param",
|
||||
z.object({
|
||||
sessionID: Session.RemoveInput,
|
||||
sessionID: Session.RemoveInput.zod,
|
||||
}),
|
||||
),
|
||||
async (c) =>
|
||||
@@ -267,7 +268,7 @@ export const SessionRoutes = lazy(() =>
|
||||
description: "Successfully updated session",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(Session.Info),
|
||||
schema: resolver(Session.Info.zod),
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -375,7 +376,7 @@ export const SessionRoutes = lazy(() =>
|
||||
description: "200",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(Session.Info),
|
||||
schema: resolver(Session.Info.zod),
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -384,14 +385,14 @@ export const SessionRoutes = lazy(() =>
|
||||
validator(
|
||||
"param",
|
||||
z.object({
|
||||
sessionID: Session.ForkInput.shape.sessionID,
|
||||
sessionID: SessionID.zod,
|
||||
}),
|
||||
),
|
||||
validator("json", Session.ForkInput.omit({ sessionID: true })),
|
||||
validator("json", zodObject(Session.ForkInput).omit({ sessionID: true })),
|
||||
async (c) =>
|
||||
jsonRequest("SessionRoutes.fork", c, function* () {
|
||||
const sessionID = c.req.valid("param").sessionID
|
||||
const body = c.req.valid("json")
|
||||
const body = c.req.valid("json") as { messageID?: MessageID }
|
||||
const svc = yield* Session.Service
|
||||
return yield* svc.fork({ ...body, sessionID })
|
||||
}),
|
||||
@@ -438,7 +439,7 @@ export const SessionRoutes = lazy(() =>
|
||||
description: "Successfully shared session",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(Session.Info),
|
||||
schema: resolver(Session.Info.zod),
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -480,18 +481,13 @@ export const SessionRoutes = lazy(() =>
|
||||
validator(
|
||||
"param",
|
||||
z.object({
|
||||
sessionID: SessionSummary.DiffInput.shape.sessionID,
|
||||
}),
|
||||
),
|
||||
validator(
|
||||
"query",
|
||||
z.object({
|
||||
messageID: SessionSummary.DiffInput.shape.messageID,
|
||||
sessionID: SessionID.zod,
|
||||
}),
|
||||
),
|
||||
validator("query", zodObject(SessionSummary.DiffInput).omit({ sessionID: true })),
|
||||
async (c) =>
|
||||
jsonRequest("SessionRoutes.diff", c, function* () {
|
||||
const query = c.req.valid("query")
|
||||
const query = c.req.valid("query") as Omit<SessionSummary.DiffInput, "sessionID">
|
||||
const params = c.req.valid("param")
|
||||
const summary = yield* SessionSummary.Service
|
||||
return yield* summary.diff({
|
||||
@@ -511,7 +507,7 @@ export const SessionRoutes = lazy(() =>
|
||||
description: "Successfully unshared session",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(Session.Info),
|
||||
schema: resolver(Session.Info.zod),
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -872,7 +868,7 @@ export const SessionRoutes = lazy(() =>
|
||||
sessionID: SessionID.zod,
|
||||
}),
|
||||
),
|
||||
validator("json", SessionPrompt.PromptInput.omit({ sessionID: true })),
|
||||
validator("json", zodObject(SessionPrompt.PromptInput).omit({ sessionID: true })),
|
||||
async (c) => {
|
||||
c.status(200)
|
||||
c.header("Content-Type", "application/json")
|
||||
@@ -910,7 +906,7 @@ export const SessionRoutes = lazy(() =>
|
||||
sessionID: SessionID.zod,
|
||||
}),
|
||||
),
|
||||
validator("json", SessionPrompt.PromptInput.omit({ sessionID: true })),
|
||||
validator("json", zodObject(SessionPrompt.PromptInput).omit({ sessionID: true })),
|
||||
async (c) => {
|
||||
const sessionID = c.req.valid("param").sessionID
|
||||
const body = c.req.valid("json")
|
||||
@@ -960,11 +956,11 @@ export const SessionRoutes = lazy(() =>
|
||||
sessionID: SessionID.zod,
|
||||
}),
|
||||
),
|
||||
validator("json", SessionPrompt.CommandInput.omit({ sessionID: true })),
|
||||
validator("json", zodObject(SessionPrompt.CommandInput).omit({ sessionID: true })),
|
||||
async (c) =>
|
||||
jsonRequest("SessionRoutes.command", c, function* () {
|
||||
const sessionID = c.req.valid("param").sessionID
|
||||
const body = c.req.valid("json")
|
||||
const body = c.req.valid("json") as Omit<SessionPrompt.CommandInput, "sessionID">
|
||||
const svc = yield* SessionPrompt.Service
|
||||
return yield* svc.command({ ...body, sessionID })
|
||||
}),
|
||||
@@ -993,11 +989,11 @@ export const SessionRoutes = lazy(() =>
|
||||
sessionID: SessionID.zod,
|
||||
}),
|
||||
),
|
||||
validator("json", SessionPrompt.ShellInput.omit({ sessionID: true })),
|
||||
validator("json", zodObject(SessionPrompt.ShellInput).omit({ sessionID: true })),
|
||||
async (c) =>
|
||||
jsonRequest("SessionRoutes.shell", c, function* () {
|
||||
const sessionID = c.req.valid("param").sessionID
|
||||
const body = c.req.valid("json")
|
||||
const body = c.req.valid("json") as Omit<SessionPrompt.ShellInput, "sessionID">
|
||||
const svc = yield* SessionPrompt.Service
|
||||
return yield* svc.shell({ ...body, sessionID })
|
||||
}),
|
||||
@@ -1013,7 +1009,7 @@ export const SessionRoutes = lazy(() =>
|
||||
description: "Updated session",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(Session.Info),
|
||||
schema: resolver(Session.Info.zod),
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -1026,16 +1022,14 @@ export const SessionRoutes = lazy(() =>
|
||||
sessionID: SessionID.zod,
|
||||
}),
|
||||
),
|
||||
validator("json", SessionRevert.RevertInput.omit({ sessionID: true })),
|
||||
validator("json", zodObject(SessionRevert.RevertInput).omit({ sessionID: true })),
|
||||
async (c) => {
|
||||
const sessionID = c.req.valid("param").sessionID
|
||||
log.info("revert", c.req.valid("json"))
|
||||
const body = c.req.valid("json") as Omit<SessionRevert.RevertInput, "sessionID">
|
||||
log.info("revert", body)
|
||||
return jsonRequest("SessionRoutes.revert", c, function* () {
|
||||
const svc = yield* SessionRevert.Service
|
||||
return yield* svc.revert({
|
||||
sessionID,
|
||||
...c.req.valid("json"),
|
||||
})
|
||||
return yield* svc.revert({ sessionID, ...body })
|
||||
})
|
||||
},
|
||||
)
|
||||
@@ -1050,7 +1044,7 @@ export const SessionRoutes = lazy(() =>
|
||||
description: "Updated session",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(Session.Info),
|
||||
schema: resolver(Session.Info.zod),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import { Hono, type Context } from "hono"
|
||||
import { describeRoute, validator, resolver } from "hono-openapi"
|
||||
import { Schema } from "effect"
|
||||
import z from "zod"
|
||||
import { Bus } from "@/bus"
|
||||
import { Session } from "@/session"
|
||||
import type { SessionID } from "@/session/schema"
|
||||
import { TuiEvent } from "@/cli/cmd/tui/event"
|
||||
import { zodObject } from "@/util/effect-zod"
|
||||
import { AsyncQueue } from "@/util/queue"
|
||||
import { errors } from "../../error"
|
||||
import { lazy } from "@/util/lazy"
|
||||
@@ -96,9 +99,9 @@ export const TuiRoutes = lazy(() =>
|
||||
...errors(400),
|
||||
},
|
||||
}),
|
||||
validator("json", TuiEvent.PromptAppend.properties),
|
||||
validator("json", zodObject(TuiEvent.PromptAppend.properties)),
|
||||
async (c) => {
|
||||
await Bus.publish(TuiEvent.PromptAppend, c.req.valid("json"))
|
||||
await Bus.publish(TuiEvent.PromptAppend, c.req.valid("json") as { text: string })
|
||||
return c.json(true)
|
||||
},
|
||||
)
|
||||
@@ -305,9 +308,12 @@ export const TuiRoutes = lazy(() =>
|
||||
},
|
||||
},
|
||||
}),
|
||||
validator("json", TuiEvent.ToastShow.properties),
|
||||
validator("json", zodObject(TuiEvent.ToastShow.properties)),
|
||||
async (c) => {
|
||||
await Bus.publish(TuiEvent.ToastShow, c.req.valid("json"))
|
||||
await Bus.publish(
|
||||
TuiEvent.ToastShow,
|
||||
c.req.valid("json") as Schema.Schema.Type<typeof TuiEvent.ToastShow.properties>,
|
||||
)
|
||||
return c.json(true)
|
||||
},
|
||||
)
|
||||
@@ -336,7 +342,7 @@ export const TuiRoutes = lazy(() =>
|
||||
return z
|
||||
.object({
|
||||
type: z.literal(def.type),
|
||||
properties: def.properties,
|
||||
properties: zodObject(def.properties),
|
||||
})
|
||||
.meta({
|
||||
ref: `Event.${def.type}`,
|
||||
@@ -345,8 +351,9 @@ export const TuiRoutes = lazy(() =>
|
||||
),
|
||||
),
|
||||
async (c) => {
|
||||
const evt = c.req.valid("json")
|
||||
await Bus.publish(Object.values(TuiEvent).find((def) => def.type === evt.type)!, evt.properties)
|
||||
const evt = c.req.valid("json") as { type: string; properties: Record<string, unknown> }
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
await Bus.publish(Object.values(TuiEvent).find((def) => def.type === evt.type)! as any, evt.properties as any)
|
||||
return c.json(true)
|
||||
},
|
||||
)
|
||||
@@ -368,9 +375,9 @@ export const TuiRoutes = lazy(() =>
|
||||
...errors(400, 404),
|
||||
},
|
||||
}),
|
||||
validator("json", TuiEvent.SessionSelect.properties),
|
||||
validator("json", zodObject(TuiEvent.SessionSelect.properties)),
|
||||
async (c) => {
|
||||
const { sessionID } = c.req.valid("json")
|
||||
const { sessionID } = c.req.valid("json") as { sessionID: SessionID }
|
||||
await runRequest(
|
||||
"TuiRoutes.sessionSelect",
|
||||
c,
|
||||
|
||||
@@ -16,6 +16,9 @@ import { GlobalRoutes } from "./routes/global"
|
||||
import { WorkspaceRouterMiddleware } from "./workspace"
|
||||
import { InstanceMiddleware } from "./routes/instance/middleware"
|
||||
import { WorkspaceRoutes } from "./routes/control/workspace"
|
||||
import { ExperimentalHttpApiServer } from "./routes/instance/httpapi/server"
|
||||
import { WorkspacePaths } from "./routes/instance/httpapi/workspace"
|
||||
import { Context } from "effect"
|
||||
|
||||
// @ts-ignore This global is needed to prevent ai-sdk from logging warnings to stdout https://github.com/vercel/ai/blob/2dc67e0ef538307f21368db32d5a12345d98831b/packages/ai/src/logger/log-warnings.ts#L85
|
||||
globalThis.AI_SDK_LOG_WARNINGS = false
|
||||
@@ -54,16 +57,24 @@ function create(opts: { cors?: string[] }) {
|
||||
}
|
||||
}
|
||||
|
||||
const workspaceApp = new Hono()
|
||||
const workspaceLegacyApp = new Hono()
|
||||
.use(InstanceMiddleware())
|
||||
.route("/experimental/workspace", WorkspaceRoutes())
|
||||
.use(WorkspaceRouterMiddleware(runtime.upgradeWebSocket))
|
||||
if (Flag.OPENCODE_EXPERIMENTAL_HTTPAPI) {
|
||||
const handler = ExperimentalHttpApiServer.webHandler().handler
|
||||
const context = Context.empty() as Context.Context<unknown>
|
||||
workspaceApp.get(WorkspacePaths.adaptors, (c) => handler(c.req.raw, context))
|
||||
workspaceApp.get(WorkspacePaths.list, (c) => handler(c.req.raw, context))
|
||||
workspaceApp.get(WorkspacePaths.status, (c) => handler(c.req.raw, context))
|
||||
}
|
||||
workspaceApp.route("/", workspaceLegacyApp)
|
||||
|
||||
return {
|
||||
app: app
|
||||
.route("/", ControlPlaneRoutes())
|
||||
.route(
|
||||
"/",
|
||||
new Hono()
|
||||
.use(InstanceMiddleware())
|
||||
.route("/experimental/workspace", WorkspaceRoutes())
|
||||
.use(WorkspaceRouterMiddleware(runtime.upgradeWebSocket)),
|
||||
)
|
||||
.route("/", workspaceApp)
|
||||
.route("/", InstanceRoutes(runtime.upgradeWebSocket))
|
||||
.route("/", UIRoutes()),
|
||||
runtime,
|
||||
|
||||
@@ -13,7 +13,7 @@ import { Plugin } from "@/plugin"
|
||||
import { Config } from "@/config"
|
||||
import { NotFoundError } from "@/storage"
|
||||
import { ModelID, ProviderID } from "@/provider/schema"
|
||||
import { Effect, Layer, Context } from "effect"
|
||||
import { Effect, Layer, Context, Schema } from "effect"
|
||||
import { InstanceState } from "@/effect"
|
||||
import { isOverflow as overflow, usable } from "./overflow"
|
||||
import { makeRuntime } from "@/effect/run-service"
|
||||
@@ -24,8 +24,8 @@ const log = Log.create({ service: "session.compaction" })
|
||||
export const Event = {
|
||||
Compacted: BusEvent.define(
|
||||
"session.compacted",
|
||||
z.object({
|
||||
sessionID: SessionID.zod,
|
||||
Schema.Struct({
|
||||
sessionID: SessionID,
|
||||
}),
|
||||
),
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ import type { Provider } from "@/provider"
|
||||
import { ModelID, ProviderID } from "@/provider/schema"
|
||||
import { Effect, Schema, Types } from "effect"
|
||||
import { zod, ZodOverride } from "@/util/effect-zod"
|
||||
import { withStatics } from "@/util/schema"
|
||||
import { NonNegativeInt, withStatics } from "@/util/schema"
|
||||
import { namedSchemaError } from "@/util/named-schema-error"
|
||||
import { EffectLogger } from "@/effect"
|
||||
|
||||
@@ -64,9 +64,7 @@ export class OutputFormatText extends Schema.Class<OutputFormatText>("OutputForm
|
||||
export class OutputFormatJsonSchema extends Schema.Class<OutputFormatJsonSchema>("OutputFormatJsonSchema")({
|
||||
type: Schema.Literal("json_schema"),
|
||||
schema: Schema.Record(Schema.String, Schema.Any).annotate({ identifier: "JSONSchema" }),
|
||||
retryCount: Schema.Number.check(Schema.isInt())
|
||||
.check(Schema.isGreaterThanOrEqualTo(0))
|
||||
.pipe(Schema.optional, Schema.withDecodingDefault(Effect.succeed(2))),
|
||||
retryCount: NonNegativeInt.pipe(Schema.optional, Schema.withDecodingDefault(Effect.succeed(2))),
|
||||
}) {
|
||||
static readonly zod = zod(this)
|
||||
}
|
||||
@@ -138,8 +136,8 @@ export type ReasoningPart = Types.DeepMutable<Schema.Schema.Type<typeof Reasonin
|
||||
const filePartSourceBase = {
|
||||
text: Schema.Struct({
|
||||
value: Schema.String,
|
||||
start: Schema.Number.check(Schema.isInt()),
|
||||
end: Schema.Number.check(Schema.isInt()),
|
||||
start: Schema.Int,
|
||||
end: Schema.Int,
|
||||
}).annotate({ identifier: "FilePartSourceText" }),
|
||||
}
|
||||
|
||||
@@ -157,7 +155,7 @@ export const SymbolSource = Schema.Struct({
|
||||
path: Schema.String,
|
||||
range: LSP.Range,
|
||||
name: Schema.String,
|
||||
kind: Schema.Number.check(Schema.isInt()),
|
||||
kind: Schema.Int,
|
||||
})
|
||||
.annotate({ identifier: "SymbolSource" })
|
||||
.pipe(withStatics((s) => ({ zod: zod(s) })))
|
||||
@@ -196,8 +194,8 @@ export const AgentPart = Schema.Struct({
|
||||
source: Schema.optional(
|
||||
Schema.Struct({
|
||||
value: Schema.String,
|
||||
start: Schema.Number.check(Schema.isInt()),
|
||||
end: Schema.Number.check(Schema.isInt()),
|
||||
start: Schema.Int,
|
||||
end: Schema.Int,
|
||||
}),
|
||||
),
|
||||
})
|
||||
@@ -501,8 +499,8 @@ export const AgentPartInput = Schema.Struct({
|
||||
source: Schema.optional(
|
||||
Schema.Struct({
|
||||
value: Schema.String,
|
||||
start: Schema.Number.check(Schema.isInt()),
|
||||
end: Schema.Number.check(Schema.isInt()),
|
||||
start: Schema.Int,
|
||||
end: Schema.Int,
|
||||
}),
|
||||
),
|
||||
})
|
||||
@@ -576,54 +574,62 @@ export const Info = Object.assign(_Info, {
|
||||
})
|
||||
export type Info = User | Assistant
|
||||
|
||||
const UpdatedEventSchema = Schema.Struct({
|
||||
sessionID: SessionID,
|
||||
info: _Info,
|
||||
})
|
||||
|
||||
const RemovedEventSchema = Schema.Struct({
|
||||
sessionID: SessionID,
|
||||
messageID: MessageID,
|
||||
})
|
||||
|
||||
const PartUpdatedEventSchema = Schema.Struct({
|
||||
sessionID: SessionID,
|
||||
part: _Part,
|
||||
time: Schema.Number,
|
||||
})
|
||||
|
||||
const PartRemovedEventSchema = Schema.Struct({
|
||||
sessionID: SessionID,
|
||||
messageID: MessageID,
|
||||
partID: PartID,
|
||||
})
|
||||
|
||||
export const Event = {
|
||||
Updated: SyncEvent.define({
|
||||
type: "message.updated",
|
||||
version: 1,
|
||||
aggregate: "sessionID",
|
||||
schema: z.object({
|
||||
sessionID: SessionID.zod,
|
||||
info: Info.zod,
|
||||
}),
|
||||
schema: UpdatedEventSchema,
|
||||
}),
|
||||
Removed: SyncEvent.define({
|
||||
type: "message.removed",
|
||||
version: 1,
|
||||
aggregate: "sessionID",
|
||||
schema: z.object({
|
||||
sessionID: SessionID.zod,
|
||||
messageID: MessageID.zod,
|
||||
}),
|
||||
schema: RemovedEventSchema,
|
||||
}),
|
||||
PartUpdated: SyncEvent.define({
|
||||
type: "message.part.updated",
|
||||
version: 1,
|
||||
aggregate: "sessionID",
|
||||
schema: z.object({
|
||||
sessionID: SessionID.zod,
|
||||
part: Part.zod,
|
||||
time: z.number(),
|
||||
}),
|
||||
schema: PartUpdatedEventSchema,
|
||||
}),
|
||||
PartDelta: BusEvent.define(
|
||||
"message.part.delta",
|
||||
z.object({
|
||||
sessionID: SessionID.zod,
|
||||
messageID: MessageID.zod,
|
||||
partID: PartID.zod,
|
||||
field: z.string(),
|
||||
delta: z.string(),
|
||||
Schema.Struct({
|
||||
sessionID: SessionID,
|
||||
messageID: MessageID,
|
||||
partID: PartID,
|
||||
field: Schema.String,
|
||||
delta: Schema.String,
|
||||
}),
|
||||
),
|
||||
PartRemoved: SyncEvent.define({
|
||||
type: "message.part.removed",
|
||||
version: 1,
|
||||
aggregate: "sessionID",
|
||||
schema: z.object({
|
||||
sessionID: SessionID.zod,
|
||||
messageID: MessageID.zod,
|
||||
partID: PartID.zod,
|
||||
}),
|
||||
schema: PartRemovedEventSchema,
|
||||
}),
|
||||
}
|
||||
|
||||
|
||||
@@ -1,191 +1,192 @@
|
||||
import z from "zod"
|
||||
import { Schema } from "effect"
|
||||
import { SessionID } from "./schema"
|
||||
import { ModelID, ProviderID } from "../provider/schema"
|
||||
import { NamedError } from "@opencode-ai/shared/util/error"
|
||||
import { zod } from "@/util/effect-zod"
|
||||
import { withStatics } from "@/util/schema"
|
||||
import { namedSchemaError } from "@/util/named-schema-error"
|
||||
|
||||
export const OutputLengthError = NamedError.create("MessageOutputLengthError", z.object({}))
|
||||
export const AuthError = NamedError.create(
|
||||
"ProviderAuthError",
|
||||
z.object({
|
||||
providerID: z.string(),
|
||||
message: z.string(),
|
||||
}),
|
||||
)
|
||||
|
||||
export const ToolCall = z
|
||||
.object({
|
||||
state: z.literal("call"),
|
||||
step: z.number().optional(),
|
||||
toolCallId: z.string(),
|
||||
toolName: z.string(),
|
||||
args: z.custom<Required<unknown>>(),
|
||||
})
|
||||
.meta({
|
||||
ref: "ToolCall",
|
||||
})
|
||||
export type ToolCall = z.infer<typeof ToolCall>
|
||||
|
||||
export const ToolPartialCall = z
|
||||
.object({
|
||||
state: z.literal("partial-call"),
|
||||
step: z.number().optional(),
|
||||
toolCallId: z.string(),
|
||||
toolName: z.string(),
|
||||
args: z.custom<Required<unknown>>(),
|
||||
})
|
||||
.meta({
|
||||
ref: "ToolPartialCall",
|
||||
})
|
||||
export type ToolPartialCall = z.infer<typeof ToolPartialCall>
|
||||
|
||||
export const ToolResult = z
|
||||
.object({
|
||||
state: z.literal("result"),
|
||||
step: z.number().optional(),
|
||||
toolCallId: z.string(),
|
||||
toolName: z.string(),
|
||||
args: z.custom<Required<unknown>>(),
|
||||
result: z.string(),
|
||||
})
|
||||
.meta({
|
||||
ref: "ToolResult",
|
||||
})
|
||||
export type ToolResult = z.infer<typeof ToolResult>
|
||||
|
||||
export const ToolInvocation = z.discriminatedUnion("state", [ToolCall, ToolPartialCall, ToolResult]).meta({
|
||||
ref: "ToolInvocation",
|
||||
export const OutputLengthError = namedSchemaError("MessageOutputLengthError", {})
|
||||
export const AuthError = namedSchemaError("ProviderAuthError", {
|
||||
providerID: Schema.String,
|
||||
message: Schema.String,
|
||||
})
|
||||
export type ToolInvocation = z.infer<typeof ToolInvocation>
|
||||
|
||||
export const TextPart = z
|
||||
.object({
|
||||
type: z.literal("text"),
|
||||
text: z.string(),
|
||||
})
|
||||
.meta({
|
||||
ref: "TextPart",
|
||||
})
|
||||
export type TextPart = z.infer<typeof TextPart>
|
||||
const AuthErrorEffect = Schema.Struct({
|
||||
name: Schema.Literal("ProviderAuthError"),
|
||||
data: Schema.Struct({
|
||||
providerID: Schema.String,
|
||||
message: Schema.String,
|
||||
}),
|
||||
})
|
||||
|
||||
export const ReasoningPart = z
|
||||
.object({
|
||||
type: z.literal("reasoning"),
|
||||
text: z.string(),
|
||||
providerMetadata: z.record(z.string(), z.any()).optional(),
|
||||
})
|
||||
.meta({
|
||||
ref: "ReasoningPart",
|
||||
})
|
||||
export type ReasoningPart = z.infer<typeof ReasoningPart>
|
||||
const OutputLengthErrorEffect = Schema.Struct({
|
||||
name: Schema.Literal("MessageOutputLengthError"),
|
||||
data: Schema.Struct({}),
|
||||
})
|
||||
|
||||
export const ToolInvocationPart = z
|
||||
.object({
|
||||
type: z.literal("tool-invocation"),
|
||||
toolInvocation: ToolInvocation,
|
||||
})
|
||||
.meta({
|
||||
ref: "ToolInvocationPart",
|
||||
})
|
||||
export type ToolInvocationPart = z.infer<typeof ToolInvocationPart>
|
||||
const UnknownErrorEffect = Schema.Struct({
|
||||
name: Schema.Literal("UnknownError"),
|
||||
data: Schema.Struct({
|
||||
message: Schema.String,
|
||||
}),
|
||||
})
|
||||
|
||||
export const SourceUrlPart = z
|
||||
.object({
|
||||
type: z.literal("source-url"),
|
||||
sourceId: z.string(),
|
||||
url: z.string(),
|
||||
title: z.string().optional(),
|
||||
providerMetadata: z.record(z.string(), z.any()).optional(),
|
||||
})
|
||||
.meta({
|
||||
ref: "SourceUrlPart",
|
||||
})
|
||||
export type SourceUrlPart = z.infer<typeof SourceUrlPart>
|
||||
export const ToolCall = Schema.Struct({
|
||||
state: Schema.Literal("call"),
|
||||
step: Schema.optional(Schema.Number),
|
||||
toolCallId: Schema.String,
|
||||
toolName: Schema.String,
|
||||
args: Schema.Unknown,
|
||||
})
|
||||
.annotate({ identifier: "ToolCall" })
|
||||
.pipe(withStatics((s) => ({ zod: zod(s) })))
|
||||
export type ToolCall = Schema.Schema.Type<typeof ToolCall>
|
||||
|
||||
export const FilePart = z
|
||||
.object({
|
||||
type: z.literal("file"),
|
||||
mediaType: z.string(),
|
||||
filename: z.string().optional(),
|
||||
url: z.string(),
|
||||
})
|
||||
.meta({
|
||||
ref: "FilePart",
|
||||
})
|
||||
export type FilePart = z.infer<typeof FilePart>
|
||||
export const ToolPartialCall = Schema.Struct({
|
||||
state: Schema.Literal("partial-call"),
|
||||
step: Schema.optional(Schema.Number),
|
||||
toolCallId: Schema.String,
|
||||
toolName: Schema.String,
|
||||
args: Schema.Unknown,
|
||||
})
|
||||
.annotate({ identifier: "ToolPartialCall" })
|
||||
.pipe(withStatics((s) => ({ zod: zod(s) })))
|
||||
export type ToolPartialCall = Schema.Schema.Type<typeof ToolPartialCall>
|
||||
|
||||
export const StepStartPart = z
|
||||
.object({
|
||||
type: z.literal("step-start"),
|
||||
})
|
||||
.meta({
|
||||
ref: "StepStartPart",
|
||||
})
|
||||
export type StepStartPart = z.infer<typeof StepStartPart>
|
||||
export const ToolResult = Schema.Struct({
|
||||
state: Schema.Literal("result"),
|
||||
step: Schema.optional(Schema.Number),
|
||||
toolCallId: Schema.String,
|
||||
toolName: Schema.String,
|
||||
args: Schema.Unknown,
|
||||
result: Schema.String,
|
||||
})
|
||||
.annotate({ identifier: "ToolResult" })
|
||||
.pipe(withStatics((s) => ({ zod: zod(s) })))
|
||||
export type ToolResult = Schema.Schema.Type<typeof ToolResult>
|
||||
|
||||
export const MessagePart = z
|
||||
.discriminatedUnion("type", [TextPart, ReasoningPart, ToolInvocationPart, SourceUrlPart, FilePart, StepStartPart])
|
||||
.meta({
|
||||
ref: "MessagePart",
|
||||
})
|
||||
export type MessagePart = z.infer<typeof MessagePart>
|
||||
export const ToolInvocation = Schema.Union([ToolCall, ToolPartialCall, ToolResult])
|
||||
.annotate({ identifier: "ToolInvocation", discriminator: "state" })
|
||||
.pipe(withStatics((s) => ({ zod: zod(s) })))
|
||||
export type ToolInvocation = Schema.Schema.Type<typeof ToolInvocation>
|
||||
|
||||
export const Info = z
|
||||
.object({
|
||||
id: z.string(),
|
||||
role: z.enum(["user", "assistant"]),
|
||||
parts: z.array(MessagePart),
|
||||
metadata: z
|
||||
.object({
|
||||
time: z.object({
|
||||
created: z.number(),
|
||||
completed: z.number().optional(),
|
||||
export const TextPart = Schema.Struct({
|
||||
type: Schema.Literal("text"),
|
||||
text: Schema.String,
|
||||
})
|
||||
.annotate({ identifier: "TextPart" })
|
||||
.pipe(withStatics((s) => ({ zod: zod(s) })))
|
||||
export type TextPart = Schema.Schema.Type<typeof TextPart>
|
||||
|
||||
export const ReasoningPart = Schema.Struct({
|
||||
type: Schema.Literal("reasoning"),
|
||||
text: Schema.String,
|
||||
providerMetadata: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)),
|
||||
})
|
||||
.annotate({ identifier: "ReasoningPart" })
|
||||
.pipe(withStatics((s) => ({ zod: zod(s) })))
|
||||
export type ReasoningPart = Schema.Schema.Type<typeof ReasoningPart>
|
||||
|
||||
export const ToolInvocationPart = Schema.Struct({
|
||||
type: Schema.Literal("tool-invocation"),
|
||||
toolInvocation: ToolInvocation,
|
||||
})
|
||||
.annotate({ identifier: "ToolInvocationPart" })
|
||||
.pipe(withStatics((s) => ({ zod: zod(s) })))
|
||||
export type ToolInvocationPart = Schema.Schema.Type<typeof ToolInvocationPart>
|
||||
|
||||
export const SourceUrlPart = Schema.Struct({
|
||||
type: Schema.Literal("source-url"),
|
||||
sourceId: Schema.String,
|
||||
url: Schema.String,
|
||||
title: Schema.optional(Schema.String),
|
||||
providerMetadata: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)),
|
||||
})
|
||||
.annotate({ identifier: "SourceUrlPart" })
|
||||
.pipe(withStatics((s) => ({ zod: zod(s) })))
|
||||
export type SourceUrlPart = Schema.Schema.Type<typeof SourceUrlPart>
|
||||
|
||||
export const FilePart = Schema.Struct({
|
||||
type: Schema.Literal("file"),
|
||||
mediaType: Schema.String,
|
||||
filename: Schema.optional(Schema.String),
|
||||
url: Schema.String,
|
||||
})
|
||||
.annotate({ identifier: "FilePart" })
|
||||
.pipe(withStatics((s) => ({ zod: zod(s) })))
|
||||
export type FilePart = Schema.Schema.Type<typeof FilePart>
|
||||
|
||||
export const StepStartPart = Schema.Struct({
|
||||
type: Schema.Literal("step-start"),
|
||||
})
|
||||
.annotate({ identifier: "StepStartPart" })
|
||||
.pipe(withStatics((s) => ({ zod: zod(s) })))
|
||||
export type StepStartPart = Schema.Schema.Type<typeof StepStartPart>
|
||||
|
||||
export const MessagePart = Schema.Union([
|
||||
TextPart,
|
||||
ReasoningPart,
|
||||
ToolInvocationPart,
|
||||
SourceUrlPart,
|
||||
FilePart,
|
||||
StepStartPart,
|
||||
])
|
||||
.annotate({ identifier: "MessagePart", discriminator: "type" })
|
||||
.pipe(withStatics((s) => ({ zod: zod(s) })))
|
||||
export type MessagePart = Schema.Schema.Type<typeof MessagePart>
|
||||
|
||||
export const Info = Schema.Struct({
|
||||
id: Schema.String,
|
||||
role: Schema.Literals(["user", "assistant"]),
|
||||
parts: Schema.Array(MessagePart),
|
||||
metadata: Schema.Struct({
|
||||
time: Schema.Struct({
|
||||
created: Schema.Number,
|
||||
completed: Schema.optional(Schema.Number),
|
||||
}),
|
||||
error: Schema.optional(Schema.Union([AuthErrorEffect, UnknownErrorEffect, OutputLengthErrorEffect])),
|
||||
sessionID: SessionID,
|
||||
tool: Schema.Record(
|
||||
Schema.String,
|
||||
Schema.StructWithRest(
|
||||
Schema.Struct({
|
||||
title: Schema.String,
|
||||
snapshot: Schema.optional(Schema.String),
|
||||
time: Schema.Struct({
|
||||
start: Schema.Number,
|
||||
end: Schema.Number,
|
||||
}),
|
||||
}),
|
||||
error: z
|
||||
.discriminatedUnion("name", [AuthError.Schema, NamedError.Unknown.Schema, OutputLengthError.Schema])
|
||||
.optional(),
|
||||
sessionID: SessionID.zod,
|
||||
tool: z.record(
|
||||
z.string(),
|
||||
z
|
||||
.object({
|
||||
title: z.string(),
|
||||
snapshot: z.string().optional(),
|
||||
time: z.object({
|
||||
start: z.number(),
|
||||
end: z.number(),
|
||||
}),
|
||||
})
|
||||
.catchall(z.any()),
|
||||
),
|
||||
assistant: z
|
||||
.object({
|
||||
system: z.string().array(),
|
||||
modelID: ModelID.zod,
|
||||
providerID: ProviderID.zod,
|
||||
path: z.object({
|
||||
cwd: z.string(),
|
||||
root: z.string(),
|
||||
}),
|
||||
cost: z.number(),
|
||||
summary: z.boolean().optional(),
|
||||
tokens: z.object({
|
||||
input: z.number(),
|
||||
output: z.number(),
|
||||
reasoning: z.number(),
|
||||
cache: z.object({
|
||||
read: z.number(),
|
||||
write: z.number(),
|
||||
}),
|
||||
}),
|
||||
})
|
||||
.optional(),
|
||||
snapshot: z.string().optional(),
|
||||
})
|
||||
.meta({ ref: "MessageMetadata" }),
|
||||
})
|
||||
.meta({
|
||||
ref: "Message",
|
||||
})
|
||||
export type Info = z.infer<typeof Info>
|
||||
[Schema.Record(Schema.String, Schema.Unknown)],
|
||||
),
|
||||
),
|
||||
assistant: Schema.optional(
|
||||
Schema.Struct({
|
||||
system: Schema.Array(Schema.String),
|
||||
modelID: ModelID,
|
||||
providerID: ProviderID,
|
||||
path: Schema.Struct({
|
||||
cwd: Schema.String,
|
||||
root: Schema.String,
|
||||
}),
|
||||
cost: Schema.Number,
|
||||
summary: Schema.optional(Schema.Boolean),
|
||||
tokens: Schema.Struct({
|
||||
input: Schema.Number,
|
||||
output: Schema.Number,
|
||||
reasoning: Schema.Number,
|
||||
cache: Schema.Struct({
|
||||
read: Schema.Number,
|
||||
write: Schema.Number,
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
),
|
||||
snapshot: Schema.optional(Schema.String),
|
||||
}).annotate({ identifier: "MessageMetadata" }),
|
||||
})
|
||||
.annotate({ identifier: "Message" })
|
||||
.pipe(withStatics((s) => ({ zod: zod(s) })))
|
||||
export type Info = Schema.Schema.Type<typeof Info>
|
||||
|
||||
export * as Message from "./message"
|
||||
|
||||
@@ -62,14 +62,16 @@ export function toPartialRow(info: DeepPartial<Session.Info>) {
|
||||
|
||||
export default [
|
||||
SyncEvent.project(Session.Event.Created, (db, data) => {
|
||||
db.insert(SessionTable).values(Session.toRow(data.info)).run()
|
||||
db.insert(SessionTable)
|
||||
.values(Session.toRow(data.info as Session.Info))
|
||||
.run()
|
||||
}),
|
||||
|
||||
SyncEvent.project(Session.Event.Updated, (db, data) => {
|
||||
const info = data.info
|
||||
const row = db
|
||||
.update(SessionTable)
|
||||
.set(toPartialRow(info))
|
||||
.set(toPartialRow(info as Session.Patch))
|
||||
.where(eq(SessionTable.id, data.sessionID))
|
||||
.returning()
|
||||
.get()
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user