This commit is contained in:
Dax Raad 2025-08-27 11:51:40 -04:00
parent 7993c2ebde
commit a0293468de
17 changed files with 393 additions and 133 deletions

View file

@ -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=="],

View file

@ -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:",

View 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: "",
}

View file

@ -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")
}
}
]}
/>
)
}

View file

@ -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()
}} />

View file

@ -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()
}}
/>
)
}

View 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>
)
}

View file

@ -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)
}

View file

@ -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 }>
}

View file

@ -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
}

View file

@ -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 {

View file

@ -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>
)

View file

@ -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">

View 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 >
)
}

View file

@ -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>

View file

@ -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}

View 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)
}
})