diff --git a/bun.lock b/bun.lock index 06dee3867..eb3eaebf3 100644 --- a/bun.lock +++ b/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=="], diff --git a/packages/opencode/package.json b/packages/opencode/package.json index f8157fbcf..205686782 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -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:", diff --git a/packages/opencode/src/cli/cmd/opentui/component/border.tsx b/packages/opencode/src/cli/cmd/opentui/component/border.tsx new file mode 100644 index 000000000..c06129c2d --- /dev/null +++ b/packages/opencode/src/cli/cmd/opentui/component/border.tsx @@ -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: "", +} diff --git a/packages/opencode/src/cli/cmd/opentui/component/dialog-command.tsx b/packages/opencode/src/cli/cmd/opentui/component/dialog-command.tsx new file mode 100644 index 000000000..44b2faa68 --- /dev/null +++ b/packages/opencode/src/cli/cmd/opentui/component/dialog-command.tsx @@ -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 ( + { + dialog.replace(() => ) + } + }, + { + title: "Switch session", + value: "switch-session", + category: "Session", + onSelect: () => { + dialog.replace(() => ) + } + }, + { + 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") + } + } + ]} + /> + + ) +} diff --git a/packages/opencode/src/cli/cmd/opentui/ui/dialog-model.tsx b/packages/opencode/src/cli/cmd/opentui/component/dialog-model.tsx similarity index 53% rename from packages/opencode/src/cli/cmd/opentui/ui/dialog-model.tsx rename to packages/opencode/src/cli/cmd/opentui/component/dialog-model.tsx index 0c2baddd8..e9b2eed80 100644 --- a/packages/opencode/src/cli/cmd/opentui/ui/dialog-model.tsx +++ b/packages/opencode/src/cli/cmd/opentui/component/dialog-model.tsx @@ -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() }} /> diff --git a/packages/opencode/src/cli/cmd/opentui/component/dialog-session-list.tsx b/packages/opencode/src/cli/cmd/opentui/component/dialog-session-list.tsx new file mode 100644 index 000000000..8ccffc53f --- /dev/null +++ b/packages/opencode/src/cli/cmd/opentui/component/dialog-session-list.tsx @@ -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 ( + { + route.navigate({ + type: "session", + sessionID: option.value, + }) + dialog.clear() + }} + /> + + ) +} diff --git a/packages/opencode/src/cli/cmd/opentui/component/prompt.tsx b/packages/opencode/src/cli/cmd/opentui/component/prompt.tsx new file mode 100644 index 000000000..d8dcc00a8 --- /dev/null +++ b/packages/opencode/src/cli/cmd/opentui/component/prompt.tsx @@ -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 ( + + + + {">"} + + + input = r} onMouseDown={r => r.target?.focus()} focusedBackgroundColor={Theme.backgroundElement} cursorColor={Theme.primary} backgroundColor={Theme.backgroundElement} /> + + + + + + enter {fg(Theme.textMuted)("send")} + {fg(Theme.textMuted)(local.model.parsed().provider)} {bold(local.model.parsed().model)} + + + ) +} diff --git a/packages/opencode/src/cli/cmd/opentui/context/local.tsx b/packages/opencode/src/cli/cmd/opentui/context/local.tsx index 73f1487c1..a0aff97b0 100644 --- a/packages/opencode/src/cli/cmd/opentui/context/local.tsx +++ b/packages/opencode/src/cli/cmd/opentui/context/local.tsx @@ -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 - recent: string[] + model: Record + 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(() => { + 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) } diff --git a/packages/opencode/src/cli/cmd/opentui/context/route.tsx b/packages/opencode/src/cli/cmd/opentui/context/route.tsx index 322c6cdda..942a71d3d 100644 --- a/packages/opencode/src/cli/cmd/opentui/context/route.tsx +++ b/packages/opencode/src/cli/cmd/opentui/context/route.tsx @@ -11,15 +11,16 @@ type Route = } function init() { - const [store, setStore] = createStore({ - type: "home", - }) + const [store, setStore] = createStore( + { 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(type: T) { + const route = useRoute() + return route.data as Extract +} diff --git a/packages/opencode/src/cli/cmd/opentui/context/sdk.tsx b/packages/opencode/src/cli/cmd/opentui/context/sdk.tsx index 64d135bb7..0f91469b9 100644 --- a/packages/opencode/src/cli/cmd/opentui/context/sdk.tsx +++ b/packages/opencode/src/cli/cmd/opentui/context/sdk.tsx @@ -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 } diff --git a/packages/opencode/src/cli/cmd/opentui/context/sync.tsx b/packages/opencode/src/cli/cmd/opentui/context/sync.tsx index ace61e69e..d085e3633 100644 --- a/packages/opencode/src/cli/cmd/opentui/context/sync.tsx +++ b/packages/opencode/src/cli/cmd/opentui/context/sync.tsx @@ -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 - }> - }> + 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)) + ), ]).then(() => setStore("ready", true)) return { diff --git a/packages/opencode/src/cli/cmd/opentui/home.tsx b/packages/opencode/src/cli/cmd/opentui/home.tsx index c86e7e564..4e085d0d3 100644 --- a/packages/opencode/src/cli/cmd/opentui/home.tsx +++ b/packages/opencode/src/cli/cmd/opentui/home.tsx @@ -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 ( @@ -19,49 +23,31 @@ export function Home() { - - - - ) -} - -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 ( - - - - - - - - - {">"} - - - input = r} onMouseDown={r => r.target?.focus()} focusedBackgroundColor={Theme.backgroundElement} cursorColor={Theme.primary} backgroundColor={Theme.backgroundElement} width={70} /> - - - - - - - - - - - enter {fg(Theme.textMuted)("send")} - {fg(Theme.textMuted)(local.model.parsed().provider)} {bold(local.model.parsed().model)} + { + 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, + } + ] + }, + }) + }} /> ) diff --git a/packages/opencode/src/cli/cmd/opentui/opentui.tsx b/packages/opencode/src/cli/cmd/opentui/opentui.tsx index 811cf4250..1c2dc5d4e 100644 --- a/packages/opencode/src/cli/cmd/opentui/opentui.tsx +++ b/packages/opencode/src/cli/cmd/opentui/opentui.tsx @@ -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(() => ) + return + } + if (evt.meta && evt.name === "d") { renderer.console.toggle() return @@ -67,9 +74,12 @@ function App() { - + + + + diff --git a/packages/opencode/src/cli/cmd/opentui/session.tsx b/packages/opencode/src/cli/cmd/opentui/session.tsx new file mode 100644 index 000000000..546a15cc3 --- /dev/null +++ b/packages/opencode/src/cli/cmd/opentui/session.tsx @@ -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 ( + + + + {bold(fg(Theme.accent)("#"))} {bold(session().title)} + + + + {session().share!.url} + + + /share {fg(Theme.textMuted)("to create a shareable link")} + + + + + + + + + + + + ) +} diff --git a/packages/opencode/src/cli/cmd/opentui/ui/dialog-select.tsx b/packages/opencode/src/cli/cmd/opentui/ui/dialog-select.tsx index 004e92fe4..56f36f887 100644 --- a/packages/opencode/src/cli/cmd/opentui/ui/dialog-select.tsx +++ b/packages/opencode/src/cli/cmd/opentui/ui/dialog-select.tsx @@ -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 { title: string - options: DialogSelectOption[] - onSelect: (option: DialogSelectOption) => void - current?: string + options: DialogSelectOption[] + onSelect?: (option: DialogSelectOption) => void + current?: T } -export interface DialogSelectOption { - key: string +export interface DialogSelectOption { + value: T title: string description?: string category?: string + onSelect?: () => void } -export function DialogSelect(props: DialogSelectProps) { +export function DialogSelect(props: DialogSelectProps) { 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 ( - + {props.title} esc @@ -91,16 +97,19 @@ export function DialogSelect(props: DialogSelectProps) { {([category, options]) => - + - + {category} {(option) => @@ -108,7 +117,7 @@ export function DialogSelect(props: DialogSelectProps) { - + n new {" "}r diff --git a/packages/opencode/src/cli/cmd/opentui/ui/dialog.tsx b/packages/opencode/src/cli/cmd/opentui/ui/dialog.tsx index 0dfa69383..cd23a9dec 100644 --- a/packages/opencode/src/cli/cmd/opentui/ui/dialog.tsx +++ b/packages/opencode/src/cli/cmd/opentui/ui/dialog.tsx @@ -35,7 +35,7 @@ export function Dialog(props: ParentProps) { { + for await (const e of event.stream) { + console.log(e) + } +})