mirror of
https://github.com/sst/opencode.git
synced 2025-12-23 10:11:41 +00:00
tui: add event system for TUI command execution and improve SDK integration
This commit is contained in:
parent
e54746878f
commit
3e31ee0973
13 changed files with 439 additions and 197 deletions
|
|
@ -48,6 +48,7 @@
|
|||
"@opentui/core": "0.0.0-20251010-2eed09fd",
|
||||
"@opentui/solid": "0.0.0-20251010-2eed09fd",
|
||||
"@parcel/watcher": "2.5.1",
|
||||
"@solid-primitives/event-bus": "1.1.2",
|
||||
"@standard-schema/spec": "1.0.0",
|
||||
"@zip.js/zip.js": "2.7.62",
|
||||
"ai": "catalog:",
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import { Switch, Match, createEffect, untrack } from "solid-js"
|
|||
import { Installation } from "@/installation"
|
||||
import { Global } from "@/global"
|
||||
import { DialogProvider, useDialog } from "@tui/ui/dialog"
|
||||
import { SDKProvider } from "@tui/context/sdk"
|
||||
import { SDKProvider, useSDK } from "@tui/context/sdk"
|
||||
import { SyncProvider } from "@tui/context/sync"
|
||||
import { LocalProvider, useLocal } from "@tui/context/local"
|
||||
import { DialogModel } from "@tui/component/dialog-model"
|
||||
|
|
@ -64,17 +64,9 @@ function App() {
|
|||
const dialog = useDialog()
|
||||
const local = useLocal()
|
||||
const command = useCommandDialog()
|
||||
const keybind = useKeybind()
|
||||
const { event } = useSDK()
|
||||
|
||||
useKeyboard(async (evt) => {
|
||||
if (keybind.match("agent_cycle", evt)) {
|
||||
local.agent.move(1)
|
||||
return
|
||||
}
|
||||
if (keybind.match("agent_cycle_reverse", evt)) {
|
||||
local.agent.move(-1)
|
||||
}
|
||||
|
||||
if (evt.meta && evt.name === "t") {
|
||||
renderer.toggleDebugOverlay()
|
||||
return
|
||||
|
|
@ -130,6 +122,26 @@ function App() {
|
|||
dialog.replace(() => <DialogAgent />)
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Agent cycle",
|
||||
value: "agent.cycle",
|
||||
keybind: "agent_cycle",
|
||||
category: "Agent",
|
||||
disabled: true,
|
||||
onSelect: () => {
|
||||
local.agent.move(1)
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Agent cycle reverse",
|
||||
value: "agent.cycle.reverse",
|
||||
keybind: "agent_cycle_reverse",
|
||||
category: "Agent",
|
||||
disabled: true,
|
||||
onSelect: () => {
|
||||
local.agent.move(-1)
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "View status",
|
||||
keybind: "status_view",
|
||||
|
|
@ -154,6 +166,10 @@ function App() {
|
|||
}
|
||||
})
|
||||
|
||||
event.on("tui.command.execute", (evt) => {
|
||||
command.trigger(evt.properties.command)
|
||||
})
|
||||
|
||||
return (
|
||||
<box
|
||||
width={dimensions().width}
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ function init() {
|
|||
}
|
||||
})
|
||||
|
||||
return {
|
||||
const result = {
|
||||
trigger(name: string) {
|
||||
for (const option of options()) {
|
||||
if (option.value === name) {
|
||||
|
|
@ -58,6 +58,7 @@ function init() {
|
|||
return options()
|
||||
},
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
export function useCommandDialog() {
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@ export function DialogSessionList() {
|
|||
title: "delete",
|
||||
onTrigger: async (option) => {
|
||||
if (toDelete() === option.value) {
|
||||
sdk.session.delete({
|
||||
sdk.client.session.delete({
|
||||
path: {
|
||||
id: option.value,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ export function DialogTag(props: { onSelect?: (value: string) => void }) {
|
|||
const [files] = createResource(
|
||||
() => [store.filter],
|
||||
async () => {
|
||||
const result = await sdk.find.files({
|
||||
const result = await sdk.client.find.files({
|
||||
query: {
|
||||
query: store.filter,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ export function Autocomplete(props: {
|
|||
if (store.visible === "/") return []
|
||||
|
||||
// Get files from SDK
|
||||
const result = await sdk.find.files({
|
||||
const result = await sdk.client.find.files({
|
||||
query: {
|
||||
query: filter() ?? "",
|
||||
},
|
||||
|
|
|
|||
|
|
@ -73,9 +73,43 @@ export function Prompt(props: PromptProps) {
|
|||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Clear prompt",
|
||||
value: "prompt.clear",
|
||||
disabled: true,
|
||||
keybind: "input_clear",
|
||||
category: "Prompt",
|
||||
onSelect: (dialog) => {
|
||||
setStore("prompt", {
|
||||
input: "",
|
||||
parts: [],
|
||||
})
|
||||
dialog.clear()
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Submit prompt",
|
||||
value: "prompt.submit",
|
||||
disabled: true,
|
||||
keybind: "input_submit",
|
||||
category: "Prompt",
|
||||
onSelect: (dialog) => {
|
||||
submit()
|
||||
dialog.clear()
|
||||
},
|
||||
},
|
||||
]
|
||||
})
|
||||
|
||||
sdk.event.on("tui.prompt.append", (evt) => {
|
||||
setStore(
|
||||
"prompt",
|
||||
produce((draft) => {
|
||||
draft.input += evt.properties.text
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
if (props.disabled) input.cursorColor = Theme.backgroundElement
|
||||
if (!props.disabled) input.cursorColor = Theme.primary
|
||||
|
|
@ -125,13 +159,13 @@ export function Prompt(props: PromptProps) {
|
|||
const sessionID = props.sessionID
|
||||
? props.sessionID
|
||||
: await (async () => {
|
||||
const sessionID = await sdk.session.create({}).then((x) => x.data!.id)
|
||||
const sessionID = await sdk.client.session.create({}).then((x) => x.data!.id)
|
||||
return sessionID
|
||||
})()
|
||||
const messageID = Identifier.ascending("message")
|
||||
const input = store.prompt.input
|
||||
if (store.mode === "shell") {
|
||||
sdk.session.shell({
|
||||
sdk.client.session.shell({
|
||||
path: {
|
||||
id: sessionID,
|
||||
},
|
||||
|
|
@ -143,7 +177,7 @@ export function Prompt(props: PromptProps) {
|
|||
setStore("mode", "normal")
|
||||
} else if (input.startsWith("/")) {
|
||||
const [command, ...args] = input.split(" ")
|
||||
sdk.session.command({
|
||||
sdk.client.session.command({
|
||||
path: {
|
||||
id: sessionID,
|
||||
},
|
||||
|
|
@ -160,7 +194,7 @@ export function Prompt(props: PromptProps) {
|
|||
parts: [],
|
||||
})
|
||||
} else {
|
||||
sdk.session.prompt({
|
||||
sdk.client.session.prompt({
|
||||
path: {
|
||||
id: sessionID,
|
||||
},
|
||||
|
|
@ -296,7 +330,7 @@ export function Prompt(props: PromptProps) {
|
|||
return
|
||||
}
|
||||
if (e.name === "escape" && props.sessionID) {
|
||||
sdk.session.abort({
|
||||
sdk.client.session.abort({
|
||||
path: {
|
||||
id: props.sessionID,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,17 +1,37 @@
|
|||
import { createOpencodeClient } from "@opencode-ai/sdk"
|
||||
import { createOpencodeClient, type Event } from "@opencode-ai/sdk"
|
||||
import { createSimpleContext } from "./helper"
|
||||
import { createGlobalEmitter } from "@solid-primitives/event-bus"
|
||||
import { onCleanup } from "solid-js"
|
||||
|
||||
export const { use: useSDK, provider: SDKProvider } = createSimpleContext({
|
||||
name: "SDK",
|
||||
init: (props: { url: string }) => {
|
||||
const client = createOpencodeClient({
|
||||
const abort = new AbortController()
|
||||
const sdk = createOpencodeClient({
|
||||
baseUrl: props.url,
|
||||
signal: abort.signal,
|
||||
fetch: (req) => {
|
||||
// @ts-ignore
|
||||
req.timeout = false
|
||||
return fetch(req)
|
||||
},
|
||||
})
|
||||
return client
|
||||
|
||||
const emitter = createGlobalEmitter<{
|
||||
[key in Event["type"]]: Extract<Event, { type: key }>
|
||||
}>()
|
||||
|
||||
sdk.event.subscribe().then(async (events) => {
|
||||
for await (const event of events.stream) {
|
||||
console.log("event", event.type)
|
||||
emitter.emit(event.type, event)
|
||||
}
|
||||
})
|
||||
|
||||
onCleanup(() => {
|
||||
abort.abort()
|
||||
})
|
||||
|
||||
return { client: sdk, event: emitter }
|
||||
},
|
||||
})
|
||||
|
|
|
|||
|
|
@ -59,164 +59,162 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
|||
|
||||
const sdk = useSDK()
|
||||
|
||||
sdk.event.subscribe().then(async (events) => {
|
||||
for await (const event of events.stream) {
|
||||
console.log(event.type)
|
||||
switch (event.type) {
|
||||
case "permission.updated": {
|
||||
const permissions = store.permission[event.properties.sessionID]
|
||||
if (!permissions) {
|
||||
setStore("permission", event.properties.sessionID, [event.properties])
|
||||
break
|
||||
}
|
||||
const match = Binary.search(permissions, event.properties.id, (p) => p.id)
|
||||
setStore(
|
||||
"permission",
|
||||
event.properties.sessionID,
|
||||
produce((draft) => {
|
||||
if (match.found) {
|
||||
draft[match.index] = event.properties
|
||||
return
|
||||
}
|
||||
draft.push(event.properties)
|
||||
}),
|
||||
)
|
||||
sdk.event.listen((e) => {
|
||||
const event = e.details
|
||||
switch (event.type) {
|
||||
case "permission.updated": {
|
||||
const permissions = store.permission[event.properties.sessionID]
|
||||
if (!permissions) {
|
||||
setStore("permission", event.properties.sessionID, [event.properties])
|
||||
break
|
||||
}
|
||||
const match = Binary.search(permissions, event.properties.id, (p) => p.id)
|
||||
setStore(
|
||||
"permission",
|
||||
event.properties.sessionID,
|
||||
produce((draft) => {
|
||||
if (match.found) {
|
||||
draft[match.index] = event.properties
|
||||
return
|
||||
}
|
||||
draft.push(event.properties)
|
||||
}),
|
||||
)
|
||||
break
|
||||
}
|
||||
|
||||
case "permission.replied": {
|
||||
const permissions = store.permission[event.properties.sessionID]
|
||||
const match = Binary.search(permissions, event.properties.permissionID, (p) => p.id)
|
||||
if (!match.found) break
|
||||
setStore(
|
||||
"permission",
|
||||
event.properties.sessionID,
|
||||
produce((draft) => {
|
||||
draft.splice(match.index, 1)
|
||||
}),
|
||||
)
|
||||
break
|
||||
}
|
||||
case "permission.replied": {
|
||||
const permissions = store.permission[event.properties.sessionID]
|
||||
const match = Binary.search(permissions, event.properties.permissionID, (p) => p.id)
|
||||
if (!match.found) break
|
||||
setStore(
|
||||
"permission",
|
||||
event.properties.sessionID,
|
||||
produce((draft) => {
|
||||
draft.splice(match.index, 1)
|
||||
}),
|
||||
)
|
||||
break
|
||||
}
|
||||
|
||||
case "todo.updated":
|
||||
setStore("todo", event.properties.sessionID, event.properties.todos)
|
||||
break
|
||||
case "todo.updated":
|
||||
setStore("todo", event.properties.sessionID, event.properties.todos)
|
||||
break
|
||||
|
||||
case "session.deleted": {
|
||||
const result = Binary.search(store.session, event.properties.info.id, (s) => s.id)
|
||||
if (result.found) {
|
||||
setStore(
|
||||
"session",
|
||||
produce((draft) => {
|
||||
draft.splice(result.index, 1)
|
||||
}),
|
||||
)
|
||||
}
|
||||
break
|
||||
}
|
||||
case "session.updated":
|
||||
const result = Binary.search(store.session, event.properties.info.id, (s) => s.id)
|
||||
if (result.found) {
|
||||
setStore("session", result.index, reconcile(event.properties.info))
|
||||
break
|
||||
}
|
||||
case "session.deleted": {
|
||||
const result = Binary.search(store.session, event.properties.info.id, (s) => s.id)
|
||||
if (result.found) {
|
||||
setStore(
|
||||
"session",
|
||||
produce((draft) => {
|
||||
draft.splice(result.index, 0, event.properties.info)
|
||||
draft.splice(result.index, 1)
|
||||
}),
|
||||
)
|
||||
}
|
||||
break
|
||||
}
|
||||
case "session.updated":
|
||||
const result = Binary.search(store.session, event.properties.info.id, (s) => s.id)
|
||||
if (result.found) {
|
||||
setStore("session", result.index, reconcile(event.properties.info))
|
||||
break
|
||||
case "message.updated": {
|
||||
const messages = store.message[event.properties.info.sessionID]
|
||||
if (!messages) {
|
||||
setStore("message", event.properties.info.sessionID, [event.properties.info])
|
||||
break
|
||||
}
|
||||
const result = Binary.search(messages, event.properties.info.id, (m) => m.id)
|
||||
if (result.found) {
|
||||
setStore("message", event.properties.info.sessionID, result.index, reconcile(event.properties.info))
|
||||
break
|
||||
}
|
||||
}
|
||||
setStore(
|
||||
"session",
|
||||
produce((draft) => {
|
||||
draft.splice(result.index, 0, event.properties.info)
|
||||
}),
|
||||
)
|
||||
break
|
||||
case "message.updated": {
|
||||
const messages = store.message[event.properties.info.sessionID]
|
||||
if (!messages) {
|
||||
setStore("message", event.properties.info.sessionID, [event.properties.info])
|
||||
break
|
||||
}
|
||||
const result = Binary.search(messages, event.properties.info.id, (m) => m.id)
|
||||
if (result.found) {
|
||||
setStore("message", event.properties.info.sessionID, result.index, reconcile(event.properties.info))
|
||||
break
|
||||
}
|
||||
setStore(
|
||||
"message",
|
||||
event.properties.info.sessionID,
|
||||
produce((draft) => {
|
||||
draft.splice(result.index, 0, event.properties.info)
|
||||
}),
|
||||
)
|
||||
break
|
||||
}
|
||||
case "message.removed": {
|
||||
const messages = store.message[event.properties.sessionID]
|
||||
const result = Binary.search(messages, event.properties.messageID, (m) => m.id)
|
||||
if (result.found) {
|
||||
setStore(
|
||||
"message",
|
||||
event.properties.info.sessionID,
|
||||
event.properties.sessionID,
|
||||
produce((draft) => {
|
||||
draft.splice(result.index, 0, event.properties.info)
|
||||
draft.splice(result.index, 1)
|
||||
}),
|
||||
)
|
||||
}
|
||||
break
|
||||
}
|
||||
case "message.part.updated": {
|
||||
const parts = store.part[event.properties.part.messageID]
|
||||
if (!parts) {
|
||||
setStore("part", event.properties.part.messageID, [event.properties.part])
|
||||
break
|
||||
}
|
||||
case "message.removed": {
|
||||
const messages = store.message[event.properties.sessionID]
|
||||
const result = Binary.search(messages, event.properties.messageID, (m) => m.id)
|
||||
if (result.found) {
|
||||
setStore(
|
||||
"message",
|
||||
event.properties.sessionID,
|
||||
produce((draft) => {
|
||||
draft.splice(result.index, 1)
|
||||
}),
|
||||
)
|
||||
}
|
||||
const result = Binary.search(parts, event.properties.part.id, (p) => p.id)
|
||||
if (result.found) {
|
||||
setStore("part", event.properties.part.messageID, result.index, reconcile(event.properties.part))
|
||||
break
|
||||
}
|
||||
case "message.part.updated": {
|
||||
const parts = store.part[event.properties.part.messageID]
|
||||
if (!parts) {
|
||||
setStore("part", event.properties.part.messageID, [event.properties.part])
|
||||
break
|
||||
}
|
||||
const result = Binary.search(parts, event.properties.part.id, (p) => p.id)
|
||||
if (result.found) {
|
||||
setStore("part", event.properties.part.messageID, result.index, reconcile(event.properties.part))
|
||||
break
|
||||
}
|
||||
setStore(
|
||||
"part",
|
||||
event.properties.part.messageID,
|
||||
produce((draft) => {
|
||||
draft.splice(result.index, 0, event.properties.part)
|
||||
}),
|
||||
)
|
||||
break
|
||||
}
|
||||
|
||||
case "message.part.removed": {
|
||||
const parts = store.part[event.properties.messageID]
|
||||
const result = Binary.search(parts, event.properties.partID, (p) => p.id)
|
||||
if (result.found)
|
||||
setStore(
|
||||
"part",
|
||||
event.properties.part.messageID,
|
||||
event.properties.messageID,
|
||||
produce((draft) => {
|
||||
draft.splice(result.index, 0, event.properties.part)
|
||||
draft.splice(result.index, 1)
|
||||
}),
|
||||
)
|
||||
break
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case "message.part.removed": {
|
||||
const parts = store.part[event.properties.messageID]
|
||||
const result = Binary.search(parts, event.properties.partID, (p) => p.id)
|
||||
if (result.found)
|
||||
setStore(
|
||||
"part",
|
||||
event.properties.messageID,
|
||||
produce((draft) => {
|
||||
draft.splice(result.index, 1)
|
||||
}),
|
||||
)
|
||||
break
|
||||
}
|
||||
|
||||
case "lsp.updated": {
|
||||
sdk.lsp.status().then((x) => setStore("lsp", x.data!))
|
||||
break
|
||||
}
|
||||
case "lsp.updated": {
|
||||
sdk.client.lsp.status().then((x) => setStore("lsp", x.data!))
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// blocking
|
||||
Promise.all([
|
||||
sdk.config.providers().then((x) => setStore("provider", x.data!.providers)),
|
||||
sdk.app.agents().then((x) => setStore("agent", x.data ?? [])),
|
||||
sdk.config.get().then((x) => setStore("config", x.data!)),
|
||||
sdk.client.config.providers().then((x) => setStore("provider", x.data!.providers)),
|
||||
sdk.client.app.agents().then((x) => setStore("agent", x.data ?? [])),
|
||||
sdk.client.config.get().then((x) => setStore("config", x.data!)),
|
||||
]).then(() => setStore("ready", true))
|
||||
|
||||
// non-blocking
|
||||
Promise.all([
|
||||
sdk.session.list().then((x) => setStore("session", x.data ?? [])),
|
||||
sdk.command.list().then((x) => setStore("command", x.data ?? [])),
|
||||
sdk.lsp.status().then((x) => setStore("lsp", x.data!)),
|
||||
sdk.mcp.status().then((x) => setStore("mcp", x.data!)),
|
||||
sdk.client.session.list().then((x) => setStore("session", x.data ?? [])),
|
||||
sdk.client.command.list().then((x) => setStore("command", x.data ?? [])),
|
||||
sdk.client.lsp.status().then((x) => setStore("lsp", x.data!)),
|
||||
sdk.client.mcp.status().then((x) => setStore("mcp", x.data!)),
|
||||
])
|
||||
|
||||
const result = {
|
||||
|
|
@ -243,9 +241,9 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
|||
},
|
||||
async sync(sessionID: string) {
|
||||
const [session, messages, todo] = await Promise.all([
|
||||
sdk.session.get({ path: { id: sessionID } }),
|
||||
sdk.session.messages({ path: { id: sessionID } }),
|
||||
sdk.session.todo({ path: { id: sessionID } }),
|
||||
sdk.client.session.get({ path: { id: sessionID } }),
|
||||
sdk.client.session.messages({ path: { id: sessionID } }),
|
||||
sdk.client.session.todo({ path: { id: sessionID } }),
|
||||
])
|
||||
setStore(
|
||||
produce((draft) => {
|
||||
|
|
|
|||
38
packages/opencode/src/cli/cmd/tui/event.ts
Normal file
38
packages/opencode/src/cli/cmd/tui/event.ts
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
import { Bus } from "@/bus"
|
||||
import z from "zod"
|
||||
|
||||
export const TuiEvent = {
|
||||
PromptAppend: Bus.event("tui.prompt.append", z.object({ text: z.string() })),
|
||||
CommandExecute: Bus.event(
|
||||
"tui.command.execute",
|
||||
z.object({
|
||||
command: z.union([
|
||||
z.enum([
|
||||
"session.list",
|
||||
"session.new",
|
||||
"session.share",
|
||||
"session.interrupt",
|
||||
"session.compact",
|
||||
"session.page.up",
|
||||
"session.page.down",
|
||||
"session.half.page.up",
|
||||
"session.half.page.down",
|
||||
"session.first",
|
||||
"session.last",
|
||||
"prompt.clear",
|
||||
"prompt.submit",
|
||||
"agent.cycle",
|
||||
]),
|
||||
z.string(),
|
||||
]),
|
||||
}),
|
||||
),
|
||||
ToastShow: Bus.event(
|
||||
"tui.toast.show",
|
||||
z.object({
|
||||
title: z.string().optional(),
|
||||
message: z.string(),
|
||||
variant: z.enum(["info", "success", "warning", "error"]),
|
||||
}),
|
||||
),
|
||||
}
|
||||
|
|
@ -19,7 +19,7 @@ export function DialogMessage(props: { messageID: string; sessionID: string }) {
|
|||
value: "session.revert",
|
||||
description: "undo messages and file changes",
|
||||
onSelect: (dialog) => {
|
||||
sdk.session.revert({
|
||||
sdk.client.session.revert({
|
||||
path: {
|
||||
id: props.sessionID,
|
||||
},
|
||||
|
|
@ -35,7 +35,7 @@ export function DialogMessage(props: { messageID: string; sessionID: string }) {
|
|||
value: "session.fork",
|
||||
description: "create a new session",
|
||||
onSelect: async (dialog) => {
|
||||
const result = await sdk.session.fork({
|
||||
const result = await sdk.client.session.fork({
|
||||
path: {
|
||||
id: props.sessionID,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -82,10 +82,6 @@ export function Session() {
|
|||
|
||||
useKeyboard((evt) => {
|
||||
if (dialog.stack.length > 0) return
|
||||
if (keybind.match("messages_page_up", evt)) scroll.scrollBy(-scroll.height / 2)
|
||||
if (keybind.match("messages_page_down", evt)) scroll.scrollBy(scroll.height / 2)
|
||||
if (keybind.match("messages_first", evt)) scroll.scrollTo(0)
|
||||
if (keybind.match("messages_last", evt)) scroll.scrollTo(scroll.scrollHeight)
|
||||
|
||||
const first = permissions()[0]
|
||||
if (first) {
|
||||
|
|
@ -96,7 +92,7 @@ export function Session() {
|
|||
return
|
||||
})
|
||||
if (response) {
|
||||
sdk.postSessionIdPermissionsPermissionId({
|
||||
sdk.client.postSessionIdPermissionsPermissionId({
|
||||
path: {
|
||||
permissionID: first.id,
|
||||
id: route.sessionID,
|
||||
|
|
@ -150,7 +146,7 @@ export function Session() {
|
|||
keybind: "session_compact",
|
||||
category: "Session",
|
||||
onSelect: (dialog) => {
|
||||
sdk.session.summarize({
|
||||
sdk.client.session.summarize({
|
||||
path: {
|
||||
id: route.sessionID,
|
||||
},
|
||||
|
|
@ -169,7 +165,7 @@ export function Session() {
|
|||
disabled: !!session()?.share?.url,
|
||||
category: "Session",
|
||||
onSelect: (dialog) => {
|
||||
sdk.session.share({
|
||||
sdk.client.session.share({
|
||||
path: {
|
||||
id: route.sessionID,
|
||||
},
|
||||
|
|
@ -184,7 +180,7 @@ export function Session() {
|
|||
disabled: !session()?.share?.url,
|
||||
category: "Session",
|
||||
onSelect: (dialog) => {
|
||||
sdk.session.unshare({
|
||||
sdk.client.session.unshare({
|
||||
path: {
|
||||
id: route.sessionID,
|
||||
},
|
||||
|
|
@ -201,7 +197,7 @@ export function Session() {
|
|||
const revert = session().revert?.messageID
|
||||
const message = messages().findLast((x) => (!revert || x.id < revert) && x.role === "user")
|
||||
if (!message) return
|
||||
sdk.session.revert({
|
||||
sdk.client.session.revert({
|
||||
path: {
|
||||
id: route.sessionID,
|
||||
},
|
||||
|
|
@ -235,7 +231,7 @@ export function Session() {
|
|||
if (!messageID) return
|
||||
const message = messages().find((x) => x.role === "user" && x.id > messageID)
|
||||
if (!message) {
|
||||
sdk.session.unrevert({
|
||||
sdk.client.session.unrevert({
|
||||
path: {
|
||||
id: route.sessionID,
|
||||
},
|
||||
|
|
@ -243,7 +239,7 @@ export function Session() {
|
|||
prompt.set({ input: "", parts: [] })
|
||||
return
|
||||
}
|
||||
sdk.session.revert({
|
||||
sdk.client.session.revert({
|
||||
path: {
|
||||
id: route.sessionID,
|
||||
},
|
||||
|
|
@ -267,6 +263,72 @@ export function Session() {
|
|||
dialog.clear()
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Page up",
|
||||
value: "session.page.up",
|
||||
keybind: "messages_page_up",
|
||||
category: "Session",
|
||||
disabled: true,
|
||||
onSelect: (dialog) => {
|
||||
scroll.scrollBy(-scroll.height / 2)
|
||||
dialog.clear()
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Page down",
|
||||
value: "session.page.down",
|
||||
keybind: "messages_page_down",
|
||||
category: "Session",
|
||||
disabled: true,
|
||||
onSelect: (dialog) => {
|
||||
scroll.scrollBy(scroll.height / 2)
|
||||
dialog.clear()
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Half page up",
|
||||
value: "session.half.page.up",
|
||||
keybind: "messages_half_page_up",
|
||||
category: "Session",
|
||||
disabled: true,
|
||||
onSelect: (dialog) => {
|
||||
scroll.scrollBy(-scroll.height / 4)
|
||||
dialog.clear()
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Half page down",
|
||||
value: "session.half.page.down",
|
||||
keybind: "messages_half_page_down",
|
||||
category: "Session",
|
||||
disabled: true,
|
||||
onSelect: (dialog) => {
|
||||
scroll.scrollBy(scroll.height / 4)
|
||||
dialog.clear()
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "First message",
|
||||
value: "session.first",
|
||||
keybind: "messages_first",
|
||||
category: "Session",
|
||||
disabled: true,
|
||||
onSelect: (dialog) => {
|
||||
scroll.scrollTo(0)
|
||||
dialog.clear()
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Last message",
|
||||
value: "session.last",
|
||||
keybind: "messages_last",
|
||||
category: "Session",
|
||||
disabled: true,
|
||||
onSelect: (dialog) => {
|
||||
scroll.scrollTo(scroll.scrollHeight)
|
||||
dialog.clear()
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
const revert = createMemo(() => {
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ import { Config } from "../config/config"
|
|||
import { File } from "../file"
|
||||
import { LSP } from "../lsp"
|
||||
import { MessageV2 } from "../session/message-v2"
|
||||
import { callTui, TuiRoute } from "./tui"
|
||||
import { TuiRoute } from "./tui"
|
||||
import { Permission } from "../permission"
|
||||
import { Instance } from "../project/instance"
|
||||
import { Agent } from "../agent/agent"
|
||||
|
|
@ -35,6 +35,7 @@ import { InstanceBootstrap } from "../project/bootstrap"
|
|||
import { MCP } from "../mcp"
|
||||
import { Storage } from "../storage/storage"
|
||||
import type { ContentfulStatusCode } from "hono/utils/http-status"
|
||||
import { TuiEvent } from "@/cli/cmd/tui/event"
|
||||
|
||||
const ERRORS = {
|
||||
400: {
|
||||
|
|
@ -59,9 +60,7 @@ const ERRORS = {
|
|||
description: "Not found",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(
|
||||
Storage.NotFoundError.Schema
|
||||
)
|
||||
schema: resolver(Storage.NotFoundError.Schema),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -87,12 +86,9 @@ export namespace Server {
|
|||
})
|
||||
if (err instanceof NamedError) {
|
||||
let status: ContentfulStatusCode
|
||||
if (err instanceof Storage.NotFoundError)
|
||||
status = 404
|
||||
else if (err instanceof Provider.ModelNotFoundError)
|
||||
status = 400
|
||||
else
|
||||
status = 500
|
||||
if (err instanceof Storage.NotFoundError) status = 404
|
||||
else if (err instanceof Provider.ModelNotFoundError) status = 400
|
||||
else status = 500
|
||||
return c.json(err.toObject(), { status })
|
||||
}
|
||||
const message = err instanceof Error && err.stack ? err.stack : err.toString()
|
||||
|
|
@ -449,7 +445,9 @@ export namespace Server {
|
|||
}),
|
||||
),
|
||||
async (c) => {
|
||||
await Session.remove(c.req.valid("param").id)
|
||||
await Bus.publish(TuiEvent.CommandExecute, {
|
||||
command: "session.list",
|
||||
})
|
||||
return c.json(true)
|
||||
},
|
||||
)
|
||||
|
|
@ -1288,13 +1286,11 @@ export namespace Server {
|
|||
...errors(400),
|
||||
},
|
||||
}),
|
||||
validator(
|
||||
"json",
|
||||
z.object({
|
||||
text: z.string(),
|
||||
}),
|
||||
),
|
||||
async (c) => c.json(await callTui(c)),
|
||||
validator("json", TuiEvent.PromptAppend.properties),
|
||||
async (c) => {
|
||||
await Bus.publish(TuiEvent.PromptAppend, c.req.valid("json"))
|
||||
return c.json(true)
|
||||
},
|
||||
)
|
||||
.post(
|
||||
"/tui/open-help",
|
||||
|
|
@ -1312,7 +1308,10 @@ export namespace Server {
|
|||
},
|
||||
},
|
||||
}),
|
||||
async (c) => c.json(await callTui(c)),
|
||||
async (c) => {
|
||||
// TODO: open dialog
|
||||
return c.json(true)
|
||||
},
|
||||
)
|
||||
.post(
|
||||
"/tui/open-sessions",
|
||||
|
|
@ -1330,7 +1329,12 @@ export namespace Server {
|
|||
},
|
||||
},
|
||||
}),
|
||||
async (c) => c.json(await callTui(c)),
|
||||
async (c) => {
|
||||
await Bus.publish(TuiEvent.CommandExecute, {
|
||||
command: "session.list",
|
||||
})
|
||||
return c.json(true)
|
||||
},
|
||||
)
|
||||
.post(
|
||||
"/tui/open-themes",
|
||||
|
|
@ -1348,7 +1352,12 @@ export namespace Server {
|
|||
},
|
||||
},
|
||||
}),
|
||||
async (c) => c.json(await callTui(c)),
|
||||
async (c) => {
|
||||
await Bus.publish(TuiEvent.CommandExecute, {
|
||||
command: "session.list",
|
||||
})
|
||||
return c.json(true)
|
||||
},
|
||||
)
|
||||
.post(
|
||||
"/tui/open-models",
|
||||
|
|
@ -1366,7 +1375,12 @@ export namespace Server {
|
|||
},
|
||||
},
|
||||
}),
|
||||
async (c) => c.json(await callTui(c)),
|
||||
async (c) => {
|
||||
await Bus.publish(TuiEvent.CommandExecute, {
|
||||
command: "model.list",
|
||||
})
|
||||
return c.json(true)
|
||||
},
|
||||
)
|
||||
.post(
|
||||
"/tui/submit-prompt",
|
||||
|
|
@ -1384,7 +1398,12 @@ export namespace Server {
|
|||
},
|
||||
},
|
||||
}),
|
||||
async (c) => c.json(await callTui(c)),
|
||||
async (c) => {
|
||||
await Bus.publish(TuiEvent.CommandExecute, {
|
||||
command: "prompt.submit",
|
||||
})
|
||||
return c.json(true)
|
||||
},
|
||||
)
|
||||
.post(
|
||||
"/tui/clear-prompt",
|
||||
|
|
@ -1402,7 +1421,12 @@ export namespace Server {
|
|||
},
|
||||
},
|
||||
}),
|
||||
async (c) => c.json(await callTui(c)),
|
||||
async (c) => {
|
||||
await Bus.publish(TuiEvent.CommandExecute, {
|
||||
command: "prompt.clear",
|
||||
})
|
||||
return c.json(true)
|
||||
},
|
||||
)
|
||||
.post(
|
||||
"/tui/execute-command",
|
||||
|
|
@ -1421,13 +1445,27 @@ export namespace Server {
|
|||
...errors(400),
|
||||
},
|
||||
}),
|
||||
validator(
|
||||
"json",
|
||||
z.object({
|
||||
command: z.string(),
|
||||
}),
|
||||
),
|
||||
async (c) => c.json(await callTui(c)),
|
||||
validator("json", z.object({ command: z.string() })),
|
||||
async (c) => {
|
||||
const command = c.req.valid("json").command
|
||||
await Bus.publish(TuiEvent.CommandExecute, {
|
||||
// @ts-expect-error
|
||||
command: {
|
||||
session_new: "session.new",
|
||||
session_share: "session.share",
|
||||
session_interrupt: "session.interrupt",
|
||||
session_compact: "session.compact",
|
||||
messages_page_up: "session.page.up",
|
||||
messages_page_down: "session.page.down",
|
||||
messages_half_page_up: "session.half.page.up",
|
||||
messages_half_page_down: "session.half.page.down",
|
||||
messages_first: "session.first",
|
||||
messages_last: "session.last",
|
||||
agent_cycle: "agent.cycle",
|
||||
}[command],
|
||||
})
|
||||
return c.json(true)
|
||||
},
|
||||
)
|
||||
.post(
|
||||
"/tui/show-toast",
|
||||
|
|
@ -1445,15 +1483,49 @@ export namespace Server {
|
|||
},
|
||||
},
|
||||
}),
|
||||
validator("json", TuiEvent.ToastShow.properties),
|
||||
async (c) => {
|
||||
await Bus.publish(TuiEvent.ToastShow, c.req.valid("json"))
|
||||
return c.json(true)
|
||||
},
|
||||
)
|
||||
.post(
|
||||
"/tui/publish",
|
||||
describeRoute({
|
||||
description: "Publish a TUI event",
|
||||
operationId: "tui.publish",
|
||||
responses: {
|
||||
200: {
|
||||
description: "Event published successfully",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(z.boolean()),
|
||||
},
|
||||
},
|
||||
},
|
||||
...errors(400),
|
||||
},
|
||||
}),
|
||||
validator(
|
||||
"json",
|
||||
z.object({
|
||||
title: z.string().optional(),
|
||||
message: z.string(),
|
||||
variant: z.enum(["info", "success", "warning", "error"]),
|
||||
}),
|
||||
z.union(
|
||||
Object.values(TuiEvent).map((def) => {
|
||||
return z
|
||||
.object({
|
||||
type: z.literal(def.type),
|
||||
properties: def.properties,
|
||||
})
|
||||
.meta({
|
||||
ref: "Event" + "." + def.type,
|
||||
})
|
||||
}),
|
||||
),
|
||||
),
|
||||
async (c) => c.json(await callTui(c)),
|
||||
async (c) => {
|
||||
const evt = c.req.valid("json")
|
||||
await Bus.publish(Object.values(TuiEvent).find((def) => def.type === evt.type)!, evt.properties)
|
||||
return c.json(true)
|
||||
},
|
||||
)
|
||||
.route("/tui/control", TuiRoute)
|
||||
.put(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue