mirror of
https://github.com/sst/opencode.git
synced 2025-08-28 17:04:10 +00:00
sync
This commit is contained in:
parent
7993c2ebde
commit
a0293468de
17 changed files with 393 additions and 133 deletions
36
bun.lock
36
bun.lock
|
@ -26,7 +26,7 @@
|
|||
},
|
||||
"cloud/core": {
|
||||
"name": "@opencode/cloud-core",
|
||||
"version": "0.5.18",
|
||||
"version": "0.5.23",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-sts": "3.782.0",
|
||||
"drizzle-orm": "0.41.0",
|
||||
|
@ -40,7 +40,7 @@
|
|||
},
|
||||
"cloud/function": {
|
||||
"name": "@opencode/cloud-function",
|
||||
"version": "0.5.18",
|
||||
"version": "0.5.23",
|
||||
"dependencies": {
|
||||
"@ai-sdk/anthropic": "2.0.0",
|
||||
"@ai-sdk/openai": "2.0.2",
|
||||
|
@ -60,7 +60,7 @@
|
|||
},
|
||||
"cloud/web": {
|
||||
"name": "@opencode/cloud-web",
|
||||
"version": "0.5.18",
|
||||
"version": "0.5.23",
|
||||
"dependencies": {
|
||||
"@kobalte/core": "0.13.9",
|
||||
"@openauthjs/solid": "0.0.0-20250322224806",
|
||||
|
@ -79,7 +79,7 @@
|
|||
},
|
||||
"packages/function": {
|
||||
"name": "@opencode/function",
|
||||
"version": "0.5.18",
|
||||
"version": "0.5.23",
|
||||
"dependencies": {
|
||||
"@octokit/auth-app": "8.0.1",
|
||||
"@octokit/rest": "22.0.0",
|
||||
|
@ -94,7 +94,7 @@
|
|||
},
|
||||
"packages/opencode": {
|
||||
"name": "opencode",
|
||||
"version": "0.5.18",
|
||||
"version": "0.5.23",
|
||||
"bin": {
|
||||
"opencode": "./bin/opencode",
|
||||
},
|
||||
|
@ -105,8 +105,8 @@
|
|||
"@openauthjs/openauth": "0.4.3",
|
||||
"@opencode-ai/plugin": "workspace:*",
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
"@opentui/core": "0.1.9",
|
||||
"@opentui/solid": "0.1.9",
|
||||
"@opentui/core": "0.1.10",
|
||||
"@opentui/solid": "0.1.10",
|
||||
"@standard-schema/spec": "1.0.0",
|
||||
"@zip.js/zip.js": "2.7.62",
|
||||
"ai": "catalog:",
|
||||
|
@ -148,7 +148,7 @@
|
|||
},
|
||||
"packages/plugin": {
|
||||
"name": "@opencode-ai/plugin",
|
||||
"version": "0.5.18",
|
||||
"version": "0.5.23",
|
||||
"dependencies": {
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
},
|
||||
|
@ -160,7 +160,7 @@
|
|||
},
|
||||
"packages/sdk/js": {
|
||||
"name": "@opencode-ai/sdk",
|
||||
"version": "0.5.18",
|
||||
"version": "0.5.23",
|
||||
"devDependencies": {
|
||||
"@hey-api/openapi-ts": "0.81.0",
|
||||
"@tsconfig/node22": "catalog:",
|
||||
|
@ -169,7 +169,7 @@
|
|||
},
|
||||
"packages/web": {
|
||||
"name": "@opencode/web",
|
||||
"version": "0.5.18",
|
||||
"version": "0.5.23",
|
||||
"dependencies": {
|
||||
"@astrojs/cloudflare": "12.6.3",
|
||||
"@astrojs/markdown-remark": "6.3.1",
|
||||
|
@ -779,21 +779,21 @@
|
|||
|
||||
"@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.28.0", "", {}, "sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA=="],
|
||||
|
||||
"@opentui/core": ["@opentui/core@0.1.9", "", { "dependencies": { "jimp": "1.6.0", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@dimforge/rapier2d-simd-compat": "^0.17.3", "@opentui/core-darwin-arm64": "0.1.9", "@opentui/core-darwin-x64": "0.1.9", "@opentui/core-linux-arm64": "0.1.9", "@opentui/core-linux-x64": "0.1.9", "@opentui/core-win32-arm64": "0.1.9", "@opentui/core-win32-x64": "0.1.9", "bun-webgpu": "0.1.3", "planck": "^1.4.2", "three": "0.177.0" } }, "sha512-GX4PNUe07hbDXxD37kKJAf3tPyMEisVFC0XA493HRFsuOAtkspnbgQGBr/5YOn4WQsXI5U/vRrSjc8pv50xEmg=="],
|
||||
"@opentui/core": ["@opentui/core@0.1.10", "", { "dependencies": { "jimp": "1.6.0", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@dimforge/rapier2d-simd-compat": "^0.17.3", "@opentui/core-darwin-arm64": "0.1.10", "@opentui/core-darwin-x64": "0.1.10", "@opentui/core-linux-arm64": "0.1.10", "@opentui/core-linux-x64": "0.1.10", "@opentui/core-win32-arm64": "0.1.10", "@opentui/core-win32-x64": "0.1.10", "bun-webgpu": "0.1.3", "planck": "^1.4.2", "three": "0.177.0" } }, "sha512-+MrWNp0fX8VPAIMHK5tMHfal5VYrXWIM5D1PEdxEQlBFZgdUADRVxktrCYZVJtdU+WwSlqJ0bQY8QzA3aXWH0A=="],
|
||||
|
||||
"@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.1.9", "", { "os": "darwin", "cpu": "arm64" }, "sha512-JsMjA1T9UAecIC/XFkxhv2jVKI0OBPIJGzqdTP+7MCWz2WBNEEkLVre3L06wxL/iDYudeM44XMZBgDUM1F05Ug=="],
|
||||
"@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.1.10", "", { "os": "darwin", "cpu": "arm64" }, "sha512-O0e4gIlfsPUGUOI9eFi4mStWkiUGOChvwIcJNv8Ppt+8aPPXzEgku4ptZF9G4KURXt3pgfkvVXgde1K29EnsUg=="],
|
||||
|
||||
"@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.1.9", "", { "os": "darwin", "cpu": "x64" }, "sha512-TmpHcNySbKEBDTCcleN/06q8QZpUhSNP5YjkSfvz/qWnEL9RrzjcfZttZ2iotDomQ7ufywMFhUtHyRaFcN5tDg=="],
|
||||
"@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.1.10", "", { "os": "darwin", "cpu": "x64" }, "sha512-PmhB0Ij5Jz+5rnVq33QCuxSaiCkGBnLH3dy5MUwp11A4VohSFR8TrQz3L5k419wZdejUgedMaT83QTWME5Fbag=="],
|
||||
|
||||
"@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.1.9", "", { "os": "linux", "cpu": "arm64" }, "sha512-XDOBwX66jWhQVdGXu6BzNIALSpRS20119K7IO4SCK3NTPaTkKk+FX+gOwbV6noWxViHhYH2wXBRXY4b/6hmJNA=="],
|
||||
"@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.1.10", "", { "os": "linux", "cpu": "arm64" }, "sha512-2TNU9DrAW1+pnqxI+9/cvtvGyBeJcyRVuddipcRZXz2k3xmmAn8A2UJ+CPJPzxET1aDsjMWiHUnESJqgkX5rog=="],
|
||||
|
||||
"@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.1.9", "", { "os": "linux", "cpu": "x64" }, "sha512-eQaEfsKt2fzAX4inTVPyrOcgLkRv+c3ytSs5rRhukd3QkhuX7d3s16cq+aQ4WeAbINbohvGvx3yyneWHwtUN3w=="],
|
||||
"@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.1.10", "", { "os": "linux", "cpu": "x64" }, "sha512-7nCN+9llnRvwaaI+XAKoT+5RUvK5dykoDtOAYNIQwWB46gPwwwCXhrua8U9V3P1dPeh2E2Pvcl5qhatuBk7Gmw=="],
|
||||
|
||||
"@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.1.9", "", { "os": "win32", "cpu": "arm64" }, "sha512-1jnO8f1WqIDFkCVpa72KX1Sv2cCfaWsAwCB1oTs/ovREVVtqtvDAp9PuFtT7iXG8hZVPtTqsZtZy5DnQLO49/A=="],
|
||||
"@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.1.10", "", { "os": "win32", "cpu": "arm64" }, "sha512-CUGL+aPxLRYQrtrVUNBVwkMj8SzCZRaB2BVgiKDZSJODzcFewEFyKDNSmvon2TkXSrjBW8TDil+lb9E3gMiw1Q=="],
|
||||
|
||||
"@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.1.9", "", { "os": "win32", "cpu": "x64" }, "sha512-UqhOj2j6VYM+Gvp5raU+aK2gjes3X1cDQOmyTiwK2OS3GEZIqkmGbZ+FSdttF1J2XnQp1MyBR1Zvmok45DDPMw=="],
|
||||
"@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.1.10", "", { "os": "win32", "cpu": "x64" }, "sha512-+zPKXAA31mNGiqw0Wv+9i/NhemnbdoyKhxpw5p7vyM5mzmQbTzPJ5gLoULUNWcCEyJLK1qfp2d1yLpFjLq9tNw=="],
|
||||
|
||||
"@opentui/solid": ["@opentui/solid@0.1.9", "", { "dependencies": { "@opentui/core": "0.1.9" }, "peerDependencies": { "solid-js": "1.9.9", "typescript": "^5" } }, "sha512-yMI8C01QbEqFAGTXuLgfW4ZJHvegarD3K/HJeAIHqZ5oyLR+AgyOKg24Ih+vRaFlDtM8BBh9z4ZrSoZ1mr+v6g=="],
|
||||
"@opentui/solid": ["@opentui/solid@0.1.10", "", { "dependencies": { "@opentui/core": "0.1.10" }, "peerDependencies": { "solid-js": "1.9.9", "typescript": "^5" } }, "sha512-kp499X1n0whyCZeBcivzRzkClI+2m/tp/bjC9Yw3Bhr6kq59y48DR/ZFqNUFr0X7NOkYFvwo3ApLtslcXoJ6qQ=="],
|
||||
|
||||
"@oslojs/asn1": ["@oslojs/asn1@1.0.0", "", { "dependencies": { "@oslojs/binary": "1.0.0" } }, "sha512-zw/wn0sj0j0QKbIXfIlnEcTviaCzYOY3V5rAyjR6YtOByFtJiT574+8p9Wlach0lZH9fddD4yb9laEAIl4vXQA=="],
|
||||
|
||||
|
|
|
@ -33,8 +33,8 @@
|
|||
"@openauthjs/openauth": "0.4.3",
|
||||
"@opencode-ai/plugin": "workspace:*",
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
"@opentui/solid": "0.1.9",
|
||||
"@opentui/core": "0.1.9",
|
||||
"@opentui/solid": "0.1.10",
|
||||
"@opentui/core": "0.1.10",
|
||||
"@standard-schema/spec": "1.0.0",
|
||||
"@zip.js/zip.js": "2.7.62",
|
||||
"ai": "catalog:",
|
||||
|
|
15
packages/opencode/src/cli/cmd/opentui/component/border.tsx
Normal file
15
packages/opencode/src/cli/cmd/opentui/component/border.tsx
Normal file
|
@ -0,0 +1,15 @@
|
|||
import type { BorderCharacters } from "@opentui/core";
|
||||
|
||||
export const SplitBorder: BorderCharacters = {
|
||||
topLeft: "",
|
||||
bottomLeft: "",
|
||||
vertical: "┃",
|
||||
topRight: "",
|
||||
bottomRight: "",
|
||||
horizontal: "",
|
||||
bottomT: "",
|
||||
topT: "",
|
||||
cross: "",
|
||||
leftT: "",
|
||||
rightT: "",
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
import { useDialog } from "../ui/dialog"
|
||||
import { DialogModel } from "./dialog-model"
|
||||
import { DialogSelect } from "../ui/dialog-select"
|
||||
import { useRoute } from "../context/route"
|
||||
import { DialogSessionList } from "./dialog-session-list"
|
||||
|
||||
export function DialogCommand() {
|
||||
const dialog = useDialog()
|
||||
const route = useRoute()
|
||||
return (
|
||||
<DialogSelect
|
||||
title="Commands"
|
||||
options={[
|
||||
{
|
||||
title: "Switch model",
|
||||
value: "switch-model",
|
||||
category: "Agent",
|
||||
onSelect: () => {
|
||||
dialog.replace(() => <DialogModel />)
|
||||
}
|
||||
},
|
||||
{
|
||||
title: "Switch session",
|
||||
value: "switch-session",
|
||||
category: "Session",
|
||||
onSelect: () => {
|
||||
dialog.replace(() => <DialogSessionList />)
|
||||
}
|
||||
},
|
||||
{
|
||||
title: "New session",
|
||||
value: "new-session",
|
||||
category: "Session",
|
||||
onSelect: () => {
|
||||
route.navigate({
|
||||
type: "home",
|
||||
})
|
||||
dialog.clear()
|
||||
}
|
||||
},
|
||||
{
|
||||
title: "Share session",
|
||||
value: "share-session",
|
||||
category: "Session",
|
||||
onSelect: () => {
|
||||
console.log("share session")
|
||||
}
|
||||
}
|
||||
]}
|
||||
/>
|
||||
|
||||
)
|
||||
}
|
|
@ -1,9 +1,9 @@
|
|||
import { createMemo } from "solid-js";
|
||||
import { useLocal } from "../context/local";
|
||||
import { useSync } from "../context/sync";
|
||||
import { map, pipe, flatMap, entries, filter } from "remeda";
|
||||
import { DialogSelect } from "./dialog-select";
|
||||
import { useDialog } from "./dialog";
|
||||
import { map, pipe, flatMap, entries, filter, isDeepEqual } from "remeda";
|
||||
import { DialogSelect } from "../ui/dialog-select";
|
||||
import { useDialog } from "../ui/dialog";
|
||||
|
||||
export function DialogModel() {
|
||||
const local = useLocal()
|
||||
|
@ -11,14 +11,16 @@ export function DialogModel() {
|
|||
const dialog = useDialog()
|
||||
|
||||
const options = createMemo(() => [
|
||||
...local.model.recent().map(key => {
|
||||
const [providerID, ...rest] = key.split("/")
|
||||
const provider = sync.data.provider.find((x) => x.id === providerID)!
|
||||
const modelID = rest.join("/")
|
||||
const model = provider.models[modelID]
|
||||
...local.model.recent().map(item => {
|
||||
const provider = sync.data.provider.find((x) => x.id === item.providerID)!
|
||||
const model = provider.models[item.modelID]
|
||||
return {
|
||||
key,
|
||||
title: model.name ?? modelID,
|
||||
key: item,
|
||||
value: {
|
||||
providerID: provider.id,
|
||||
modelID: model.id,
|
||||
},
|
||||
title: model.name ?? item.modelID,
|
||||
description: provider.name,
|
||||
category: "Recent",
|
||||
}
|
||||
|
@ -29,12 +31,15 @@ export function DialogModel() {
|
|||
provider.models,
|
||||
entries(),
|
||||
map(([model, info]) => ({
|
||||
key: `${provider.id}/${model}`,
|
||||
value: {
|
||||
providerID: provider.id,
|
||||
modelID: model,
|
||||
},
|
||||
title: info.name ?? model,
|
||||
description: provider.name,
|
||||
category: provider.name,
|
||||
})),
|
||||
filter(x => !local.model.recent().includes(x.key)),
|
||||
filter(x => !local.model.recent().find(y => isDeepEqual(y, x.value))),
|
||||
)),
|
||||
)
|
||||
])
|
||||
|
@ -44,7 +49,7 @@ export function DialogModel() {
|
|||
current={local.model.current()}
|
||||
options={options()}
|
||||
onSelect={option => {
|
||||
local.model.set(option.key, { recent: true })
|
||||
local.model.set(option.value, { recent: true })
|
||||
dialog.clear()
|
||||
}} />
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
import { useDialog } from "../ui/dialog"
|
||||
import { DialogModel } from "./dialog-model"
|
||||
import { DialogSelect } from "../ui/dialog-select"
|
||||
import { useRoute } from "../context/route"
|
||||
import { useSync } from "../context/sync"
|
||||
import { createMemo } from "solid-js"
|
||||
|
||||
export function DialogSessionList() {
|
||||
const dialog = useDialog()
|
||||
const sync = useSync()
|
||||
const route = useRoute()
|
||||
|
||||
const options = createMemo(() => {
|
||||
const today = new Date().toDateString()
|
||||
return Object.values(sync.data.session).map((x) => {
|
||||
let category = new Date(x.time.created).toDateString()
|
||||
if (category === today) {
|
||||
category = "Today"
|
||||
}
|
||||
return {
|
||||
title: x.title,
|
||||
value: x.id,
|
||||
category,
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
return (
|
||||
<DialogSelect
|
||||
title="Sessions"
|
||||
options={options()}
|
||||
onSelect={(option) => {
|
||||
route.navigate({
|
||||
type: "session",
|
||||
sessionID: option.value,
|
||||
})
|
||||
dialog.clear()
|
||||
}}
|
||||
/>
|
||||
|
||||
)
|
||||
}
|
41
packages/opencode/src/cli/cmd/opentui/component/prompt.tsx
Normal file
41
packages/opencode/src/cli/cmd/opentui/component/prompt.tsx
Normal file
|
@ -0,0 +1,41 @@
|
|||
import { InputRenderable, TextAttributes, fg, bold } from "@opentui/core"
|
||||
import { createEffect } from "solid-js"
|
||||
import { useLocal } from "../context/local"
|
||||
import { Theme } from "../context/theme"
|
||||
import { useDialog } from "../ui/dialog"
|
||||
|
||||
|
||||
export type PromptProps = {
|
||||
onSubmit?: (value: string) => void
|
||||
}
|
||||
export function Prompt(props: PromptProps) {
|
||||
let input: InputRenderable
|
||||
const dialog = useDialog()
|
||||
const local = useLocal()
|
||||
|
||||
createEffect(() => {
|
||||
if (dialog.stack.length === 0 && input)
|
||||
input.focus()
|
||||
if (dialog.stack.length > 0)
|
||||
input.blur()
|
||||
})
|
||||
|
||||
return (
|
||||
<box>
|
||||
<box flexDirection="row">
|
||||
<box backgroundColor={Theme.backgroundElement} width={3} border={false} justifyContent="center" alignItems="center">
|
||||
<text attributes={TextAttributes.BOLD} fg={Theme.primary}>{">"}</text>
|
||||
</box>
|
||||
<box border={false} paddingTop={1} paddingBottom={2} backgroundColor={Theme.backgroundElement} flexGrow={1}>
|
||||
<input onSubmit={props.onSubmit} ref={r => input = r} onMouseDown={r => r.target?.focus()} focusedBackgroundColor={Theme.backgroundElement} cursorColor={Theme.primary} backgroundColor={Theme.backgroundElement} />
|
||||
</box>
|
||||
<box backgroundColor={Theme.backgroundElement} width={1} border={false} justifyContent="center" alignItems="center">
|
||||
</box>
|
||||
</box>
|
||||
<group paddingLeft={2} paddingRight={1} flexDirection="row" justifyContent="space-between">
|
||||
<text>enter {fg(Theme.textMuted)("send")}</text>
|
||||
<text>{fg(Theme.textMuted)(local.model.parsed().provider)} {bold(local.model.parsed().model)}</text>
|
||||
</group >
|
||||
</box>
|
||||
)
|
||||
}
|
|
@ -2,9 +2,10 @@ import { createStore } from "solid-js/store"
|
|||
import { batch, createContext, createEffect, createMemo, useContext, type ParentProps } from "solid-js"
|
||||
import { useSync } from "./sync"
|
||||
import { Theme } from "./theme"
|
||||
import { unique } from "remeda"
|
||||
import { unique, uniqueBy } from "remeda"
|
||||
import path from "path"
|
||||
import { Global } from "../../../../global"
|
||||
import type { Agent } from "@opencode-ai/sdk"
|
||||
|
||||
|
||||
function init() {
|
||||
|
@ -29,7 +30,10 @@ function init() {
|
|||
const value = agents()[next]
|
||||
setStore("current", value.name)
|
||||
if (value.model)
|
||||
model.set(`${value.model.providerID}/${value.model.modelID}`)
|
||||
model.set({
|
||||
providerID: value.model.providerID,
|
||||
modelID: value.model.modelID,
|
||||
})
|
||||
},
|
||||
color(name: string) {
|
||||
const index = agents().findIndex((x) => x.name === name)
|
||||
|
@ -41,8 +45,14 @@ function init() {
|
|||
|
||||
const model = (() => {
|
||||
const [store, setStore] = createStore<{
|
||||
model: Record<string, string>
|
||||
recent: string[]
|
||||
model: Record<string, {
|
||||
providerID: string
|
||||
modelID: string
|
||||
}>
|
||||
recent: {
|
||||
providerID: string
|
||||
modelID: string
|
||||
}[]
|
||||
}>({
|
||||
model: {},
|
||||
recent: []
|
||||
|
@ -63,12 +73,15 @@ function init() {
|
|||
const fallback = createMemo(() => {
|
||||
const provider = sync.data.provider[0]
|
||||
const model = Object.values(provider.models)[0]
|
||||
return `${provider.id}/${model.id}`
|
||||
return {
|
||||
providerID: provider.id,
|
||||
modelID: model.id,
|
||||
}
|
||||
})
|
||||
|
||||
const current = createMemo<string>(() => {
|
||||
const current = createMemo(() => {
|
||||
const a = agent.current()
|
||||
return store.model[agent.current().name] ?? (a.model ? `${a.model.providerID}/${a.model.modelID}` : fallback())
|
||||
return store.model[agent.current().name] ?? (a.model ? a.model : fallback())
|
||||
})
|
||||
|
||||
|
||||
|
@ -79,20 +92,18 @@ function init() {
|
|||
},
|
||||
parsed: createMemo(() => {
|
||||
const value = current()
|
||||
const [providerID, ...rest] = value.split("/")
|
||||
const provider = sync.data.provider.find((x) => x.id === providerID)!
|
||||
const modelID = rest.join("/")
|
||||
const model = provider.models[modelID]
|
||||
const provider = sync.data.provider.find((x) => x.id === value.providerID)!
|
||||
const model = provider.models[value.modelID]
|
||||
return {
|
||||
provider: provider.name ?? providerID,
|
||||
model: model.name ?? modelID,
|
||||
provider: provider.name ?? value.providerID,
|
||||
model: model.name ?? value.modelID,
|
||||
}
|
||||
}),
|
||||
set(model: string, options?: { recent?: boolean }) {
|
||||
set(model: { providerID: string, modelID: string }, options?: { recent?: boolean }) {
|
||||
batch(() => {
|
||||
setStore("model", agent.current().name, model)
|
||||
if (options?.recent) {
|
||||
const uniq = unique([model, ...store.recent])
|
||||
const uniq = uniqueBy([model, ...store.recent], (x) => x.providerID + x.modelID)
|
||||
if (uniq.length > 5) uniq.pop()
|
||||
setStore("recent", uniq)
|
||||
}
|
||||
|
|
|
@ -11,15 +11,16 @@ type Route =
|
|||
}
|
||||
|
||||
function init() {
|
||||
const [store, setStore] = createStore<Route>({
|
||||
type: "home",
|
||||
})
|
||||
const [store, setStore] = createStore<Route>(
|
||||
{ type: 'session', sessionID: 'ses_71b466c91ffelM4E0ltHr2sQr3' }
|
||||
)
|
||||
|
||||
return {
|
||||
get route() {
|
||||
get data() {
|
||||
return store
|
||||
},
|
||||
set navigate(route: Route) {
|
||||
navigate(route: Route) {
|
||||
console.log("navigate", route)
|
||||
setStore(route)
|
||||
},
|
||||
}
|
||||
|
@ -42,3 +43,8 @@ export function useRoute() {
|
|||
}
|
||||
return value
|
||||
}
|
||||
|
||||
export function useRouteData<T extends Route["type"]>(type: T) {
|
||||
const route = useRoute()
|
||||
return route.data as Extract<Route, { type: typeof type }>
|
||||
}
|
||||
|
|
|
@ -4,10 +4,12 @@ import { Server } from "../../../../server/server"
|
|||
|
||||
|
||||
function init() {
|
||||
const app = Server.app()
|
||||
const server = Server.listen({
|
||||
port: 0,
|
||||
hostname: "127.0.0.1",
|
||||
})
|
||||
const client = createOpencodeClient({
|
||||
baseUrl: "http://localhost:4096",
|
||||
fetch: async (...args) => app.fetch(...args),
|
||||
baseUrl: server.url.toString(),
|
||||
})
|
||||
return client
|
||||
}
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
import type { Agent, Provider, Session } from "@opencode-ai/sdk"
|
||||
import { createStore } from "solid-js/store"
|
||||
import type { Message, Agent, Provider, Session, Part } from "@opencode-ai/sdk"
|
||||
import { createStore, produce } from "solid-js/store"
|
||||
import { useSDK } from "./sdk"
|
||||
import { createContext, onMount, Show, useContext, type ParentProps } from "solid-js"
|
||||
import type { Message } from "vscode-jsonrpc"
|
||||
import { createContext, Show, useContext, type ParentProps } from "solid-js"
|
||||
|
||||
|
||||
function init() {
|
||||
|
@ -10,27 +9,51 @@ function init() {
|
|||
ready: boolean
|
||||
provider: Provider[]
|
||||
agent: Agent[]
|
||||
session: Record<string, {
|
||||
info: Session
|
||||
message: Record<string, {
|
||||
info: Message
|
||||
part: Record<string, Message>
|
||||
}>
|
||||
}>
|
||||
session: {
|
||||
[sessionID: string]: Session
|
||||
}
|
||||
message: {
|
||||
[sessionID: string]: {
|
||||
[messageID: string]: Message
|
||||
}
|
||||
}
|
||||
part: {
|
||||
[sessionID: string]: {
|
||||
[messageID: string]: {
|
||||
[partID: string]: Part
|
||||
}
|
||||
}
|
||||
}
|
||||
}>({
|
||||
ready: false,
|
||||
agent: [],
|
||||
provider: [],
|
||||
session: {},
|
||||
message: {},
|
||||
part: {},
|
||||
})
|
||||
|
||||
const sdk = useSDK()
|
||||
|
||||
onMount(async () => {
|
||||
const events = await sdk.event.subscribe()
|
||||
sdk.event.subscribe().then(async events => {
|
||||
for await (const event of events.stream) {
|
||||
switch (event.type) {
|
||||
case "storage.write":
|
||||
case "session.updated":
|
||||
setStore("session", event.properties.info.id, event.properties.info)
|
||||
break
|
||||
case "message.updated":
|
||||
setStore("message", produce((message) => {
|
||||
message[event.properties.info.sessionID] ??= {}
|
||||
message[event.properties.info.sessionID][event.properties.info.id] = event.properties.info
|
||||
}))
|
||||
break
|
||||
|
||||
case "message.part.updated":
|
||||
setStore("part", produce((part) => {
|
||||
part[event.properties.part.sessionID] ??= {}
|
||||
part[event.properties.part.sessionID][event.properties.part.messageID] ??= {}
|
||||
part[event.properties.part.sessionID][event.properties.part.messageID][event.properties.part.id] = event.properties.part
|
||||
}))
|
||||
break
|
||||
}
|
||||
}
|
||||
|
@ -39,6 +62,12 @@ function init() {
|
|||
Promise.all([
|
||||
sdk.config.providers().then((x) => setStore("provider", x.data!.providers)),
|
||||
sdk.app.agents().then((x) => setStore("agent", x.data ?? [])),
|
||||
sdk.session.list().then((x) =>
|
||||
setStore("session", x.data!.reduce((acc, item) => {
|
||||
acc[item.id] = item
|
||||
return acc
|
||||
}, {} as Record<string, Session>))
|
||||
),
|
||||
]).then(() => setStore("ready", true))
|
||||
|
||||
return {
|
||||
|
|
|
@ -1,11 +1,15 @@
|
|||
import { createEffect } from "solid-js";
|
||||
import { Installation } from "../../../installation";
|
||||
import { Theme } from "./context/theme";
|
||||
import { InputRenderable, TextAttributes, bold, fg } from "@opentui/core"
|
||||
import { useDialog } from "./ui/dialog";
|
||||
import { TextAttributes, bold, fg } from "@opentui/core"
|
||||
import { Prompt } from "./component/prompt";
|
||||
import { useSDK } from "./context/sdk";
|
||||
import { useRoute } from "./context/route";
|
||||
import { useLocal } from "./context/local";
|
||||
|
||||
export function Home() {
|
||||
const sdk = useSDK()
|
||||
const route = useRoute()
|
||||
const local = useLocal()
|
||||
return (
|
||||
<group flexGrow={1} justifyContent="center" alignItems="center">
|
||||
<group>
|
||||
|
@ -19,49 +23,31 @@ export function Home() {
|
|||
</group>
|
||||
</group>
|
||||
<group paddingTop={3} >
|
||||
<Prompt />
|
||||
</group >
|
||||
</group>
|
||||
)
|
||||
}
|
||||
|
||||
function Prompt() {
|
||||
let input: InputRenderable
|
||||
const dialog = useDialog()
|
||||
const local = useLocal()
|
||||
|
||||
createEffect(() => {
|
||||
if (dialog.stack.length === 0 && input)
|
||||
input.focus()
|
||||
if (dialog.stack.length > 0)
|
||||
input.blur()
|
||||
})
|
||||
|
||||
return (
|
||||
<group>
|
||||
<group flexDirection="row">
|
||||
<group>
|
||||
<text fg={Theme.textMuted}>┃</text>
|
||||
<text fg={Theme.textMuted}>┃</text>
|
||||
<text fg={Theme.textMuted}>┃</text>
|
||||
</group>
|
||||
<box backgroundColor={Theme.backgroundElement} width={3} border={false} justifyContent="center" alignItems="center">
|
||||
<text attributes={TextAttributes.BOLD} fg={Theme.primary}>{">"}</text>
|
||||
</box>
|
||||
<box border={false} paddingTop={1} paddingBottom={2} backgroundColor={Theme.backgroundElement}>
|
||||
<input ref={r => input = r} onMouseDown={r => r.target?.focus()} focusedBackgroundColor={Theme.backgroundElement} cursorColor={Theme.primary} backgroundColor={Theme.backgroundElement} width={70} />
|
||||
</box>
|
||||
<box backgroundColor={Theme.backgroundElement} width={1} border={false} justifyContent="center" alignItems="center">
|
||||
</box>
|
||||
<group>
|
||||
<text fg={Theme.textMuted}>┃</text>
|
||||
<text fg={Theme.textMuted}>┃</text>
|
||||
<text fg={Theme.textMuted}>┃</text>
|
||||
</group>
|
||||
</group>
|
||||
<group paddingLeft={2} paddingRight={1} flexDirection="row" justifyContent="space-between">
|
||||
<text>enter {fg(Theme.textMuted)("send")}</text>
|
||||
<text>{fg(Theme.textMuted)(local.model.parsed().provider)} {bold(local.model.parsed().model)}</text>
|
||||
<Prompt onSubmit={async (val) => {
|
||||
const session = await sdk.session.create({
|
||||
body: {
|
||||
},
|
||||
})
|
||||
route.navigate({
|
||||
type: "session",
|
||||
sessionID: session.data!.id,
|
||||
})
|
||||
await sdk.session.chat({
|
||||
path: {
|
||||
id: session.data!.id,
|
||||
},
|
||||
body: {
|
||||
...local.model.current(),
|
||||
agent: local.agent.current().name,
|
||||
parts: [
|
||||
{
|
||||
type: "text",
|
||||
text: val,
|
||||
}
|
||||
]
|
||||
},
|
||||
})
|
||||
}} />
|
||||
</group >
|
||||
</group>
|
||||
)
|
||||
|
|
|
@ -12,7 +12,9 @@ import { bootstrap } from "../../bootstrap"
|
|||
import { SDKProvider } from "./context/sdk"
|
||||
import { SyncProvider } from "./context/sync"
|
||||
import { LocalProvider, useLocal } from "./context/local"
|
||||
import { DialogModel } from "./ui/dialog-model"
|
||||
import { DialogModel } from "./component/dialog-model"
|
||||
import { DialogCommand } from "./component/dialog-command"
|
||||
import { Session } from "./session"
|
||||
|
||||
export const OpentuiCommand = cmd({
|
||||
command: "opentui",
|
||||
|
@ -53,6 +55,11 @@ function App() {
|
|||
return
|
||||
}
|
||||
|
||||
if (evt.ctrl && evt.name === "p") {
|
||||
dialog.replace(() => <DialogCommand />)
|
||||
return
|
||||
}
|
||||
|
||||
if (evt.meta && evt.name === "d") {
|
||||
renderer.console.toggle()
|
||||
return
|
||||
|
@ -67,9 +74,12 @@ function App() {
|
|||
<box border={false} width={dimensions().width} height={dimensions().height} backgroundColor={Theme.background}>
|
||||
<group flexDirection="column" flexGrow={1}>
|
||||
<Switch>
|
||||
<Match when={route.route.type === "home"}>
|
||||
<Match when={route.data.type === "home"}>
|
||||
<Home />
|
||||
</Match>
|
||||
<Match when={route.data.type === "session"}>
|
||||
<Session />
|
||||
</Match>
|
||||
</Switch>
|
||||
</group>
|
||||
<box border={false} height={1} backgroundColor={Theme.backgroundPanel} flexDirection="row" justifyContent="space-between">
|
||||
|
|
40
packages/opencode/src/cli/cmd/opentui/session.tsx
Normal file
40
packages/opencode/src/cli/cmd/opentui/session.tsx
Normal file
|
@ -0,0 +1,40 @@
|
|||
import { createEffect, createMemo, Match, Show, Switch } from "solid-js";
|
||||
import { useRoute, useRouteData } from "./context/route";
|
||||
import { useSync } from "./context/sync";
|
||||
import { SplitBorder } from "./component/border";
|
||||
import { Theme } from "./context/theme";
|
||||
import { bold, fg } from "@opentui/core";
|
||||
import { Prompt } from "./component/prompt";
|
||||
import { useTerminalDimensions } from "@opentui/solid";
|
||||
|
||||
export function Session() {
|
||||
const route = useRouteData("session")
|
||||
const sync = useSync()
|
||||
const session = createMemo(() => sync.data.session[route.sessionID])
|
||||
const dimensions = useTerminalDimensions()
|
||||
|
||||
return (
|
||||
<Show when={session()}>
|
||||
<box paddingTop={1} paddingBottom={1} paddingLeft={2} paddingRight={2} flexGrow={1}>
|
||||
<box customBorderChars={SplitBorder} border={["left", "right"]} borderColor={Theme.backgroundElement} paddingLeft={1} paddingRight={1}>
|
||||
<text>{bold(fg(Theme.accent)("#"))} {bold(session().title)}</text>
|
||||
<group flexDirection="row">
|
||||
<Switch>
|
||||
<Match when={session().share?.url}>
|
||||
<text fg={Theme.textMuted}>{session().share!.url}</text>
|
||||
</Match>
|
||||
<Match when={true}>
|
||||
<text>/share {fg(Theme.textMuted)("to create a shareable link")}</text>
|
||||
</Match>
|
||||
</Switch>
|
||||
</group>
|
||||
</box>
|
||||
<box flexGrow={1}>
|
||||
</box>
|
||||
<group>
|
||||
<Prompt />
|
||||
</group>
|
||||
</box >
|
||||
</Show >
|
||||
)
|
||||
}
|
|
@ -1,26 +1,28 @@
|
|||
import { InputRenderable, RGBA, TextAttributes } from "@opentui/core"
|
||||
import { Theme } from "../context/theme"
|
||||
import { entries, flatMap, groupBy, mapValues, pipe, take } from "remeda"
|
||||
import { entries, flatMap, groupBy, pipe, take } from "remeda"
|
||||
import { createEffect, createMemo, For, Show } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { useKeyHandler } from "@opentui/solid"
|
||||
import * as fuzzysort from "fuzzysort"
|
||||
import { isDeepEqual } from "remeda"
|
||||
|
||||
export interface DialogSelectProps {
|
||||
export interface DialogSelectProps<T> {
|
||||
title: string
|
||||
options: DialogSelectOption[]
|
||||
onSelect: (option: DialogSelectOption) => void
|
||||
current?: string
|
||||
options: DialogSelectOption<T>[]
|
||||
onSelect?: (option: DialogSelectOption<T>) => void
|
||||
current?: T
|
||||
}
|
||||
|
||||
export interface DialogSelectOption {
|
||||
key: string
|
||||
export interface DialogSelectOption<T> {
|
||||
value: T
|
||||
title: string
|
||||
description?: string
|
||||
category?: string
|
||||
onSelect?: () => void
|
||||
}
|
||||
|
||||
export function DialogSelect(props: DialogSelectProps) {
|
||||
export function DialogSelect<T>(props: DialogSelectProps<T>) {
|
||||
const [store, setStore] = createStore({
|
||||
selected: 0,
|
||||
filter: ""
|
||||
|
@ -64,14 +66,18 @@ export function DialogSelect(props: DialogSelectProps) {
|
|||
useKeyHandler((evt) => {
|
||||
if (evt.name === "up") move(-1)
|
||||
if (evt.name === "down") move(1)
|
||||
if (evt.name === "return") props.onSelect(flat()[store.selected])
|
||||
if (evt.name === "return") {
|
||||
const option = flat()[store.selected]
|
||||
if (option.onSelect) option.onSelect()
|
||||
props.onSelect?.(option)
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
return (
|
||||
<group>
|
||||
<group paddingLeft={2} paddingRight={2}>
|
||||
<group paddingLeft={1} paddingRight={1}>
|
||||
<group paddingLeft={1}>
|
||||
<group flexDirection="row" justifyContent="space-between">
|
||||
<text attributes={TextAttributes.BOLD}>{props.title}</text>
|
||||
<text fg={Theme.textMuted}>esc</text>
|
||||
|
@ -91,16 +97,19 @@ export function DialogSelect(props: DialogSelectProps) {
|
|||
<group paddingBottom={1} >
|
||||
<For each={grouped()}>
|
||||
{([category, options]) =>
|
||||
<group paddingTop={1} flexShrink={0} >
|
||||
<group flexShrink={0} >
|
||||
<Show when={category}>
|
||||
<group paddingLeft={1} >
|
||||
<group paddingTop={1} paddingLeft={1} >
|
||||
<text fg={Theme.accent} attributes={TextAttributes.BOLD}>{category}</text>
|
||||
</group>
|
||||
</Show>
|
||||
<For each={options}>
|
||||
{(option) =>
|
||||
<Option
|
||||
title={option.title} description={option.description !== category ? option.description : undefined} active={option.key === flat()[store.selected].key} current={option.key === props.current} />
|
||||
title={option.title}
|
||||
description={option.description !== category ? option.description : undefined}
|
||||
active={isDeepEqual(option.value, flat()[store.selected].value)}
|
||||
current={isDeepEqual(option.value, props.current)} />
|
||||
}
|
||||
</For>
|
||||
</group>
|
||||
|
@ -108,7 +117,7 @@ export function DialogSelect(props: DialogSelectProps) {
|
|||
</For>
|
||||
</group>
|
||||
</group>
|
||||
<box border={false} paddingRight={2} paddingLeft={3} paddingBottom={1} paddingTop={1} flexDirection="row" >
|
||||
<box border={false} paddingRight={2} paddingLeft={3} paddingBottom={1} flexDirection="row" >
|
||||
<text fg={Theme.text} attributes={TextAttributes.BOLD}>n</text>
|
||||
<text fg={Theme.textMuted}> new</text>
|
||||
<text fg={Theme.text} attributes={TextAttributes.BOLD}>{" "}r</text>
|
||||
|
|
|
@ -35,7 +35,7 @@ export function Dialog(props: ParentProps) {
|
|||
<box
|
||||
border={false}
|
||||
customBorderChars={Border}
|
||||
width={76}
|
||||
width={60}
|
||||
maxWidth={dimensions().width - 2}
|
||||
backgroundColor={Theme.backgroundPanel}
|
||||
borderColor={Theme.border}
|
||||
|
|
11
packages/sdk/js/example/test.ts
Normal file
11
packages/sdk/js/example/test.ts
Normal file
|
@ -0,0 +1,11 @@
|
|||
import { createOpencodeClient, createOpencodeServer } from "../src/index"
|
||||
|
||||
const client = createOpencodeClient({
|
||||
baseUrl: "http://localhost:4096",
|
||||
})
|
||||
|
||||
await client.event.subscribe().then(async (event) => {
|
||||
for await (const e of event.stream) {
|
||||
console.log(e)
|
||||
}
|
||||
})
|
Loading…
Add table
Add a link
Reference in a new issue