opentui: feat: Implement POST /tui/toast-show (#3604)

This commit is contained in:
Haris Gušić 2025-10-31 16:39:08 +01:00 committed by GitHub
parent 2d0bc37a65
commit 54e2b1f399
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 53 additions and 103 deletions

View file

@ -25,6 +25,7 @@ import { DialogAlert } from "./ui/dialog-alert"
import { ToastProvider, useToast } from "./ui/toast"
import { ExitProvider } from "./context/exit"
import type { SessionRoute } from "./context/route"
import { TuiEvent } from "./event"
export function tui(input: {
url: string
@ -119,7 +120,7 @@ function App() {
await sync.session.sync(data.sessionID).catch(() => {
toast.show({
message: `Session not found: ${data.sessionID}`,
type: "error",
variant: "error",
})
return route.navigate({ type: "home" })
})
@ -231,10 +232,19 @@ function App() {
}
})
event.on("tui.command.execute", (evt) => {
event.on(TuiEvent.CommandExecute.type, (evt) => {
command.trigger(evt.properties.command)
})
event.on(TuiEvent.ToastShow.type, (evt) => {
toast.show({
title: evt.properties.title,
message: evt.properties.message,
variant: evt.properties.variant,
duration: evt.properties.duration,
})
})
return (
<box
width={dimensions().width}

View file

@ -27,6 +27,7 @@ import { Editor } from "@tui/util/editor"
import { useExit } from "../../context/exit"
import { Clipboard } from "../../util/clipboard"
import type { FilePart } from "@opencode-ai/sdk"
import { TuiEvent } from "../../event"
export type PromptProps = {
sessionID?: string
@ -162,7 +163,7 @@ export function Prompt(props: PromptProps) {
]
})
sdk.event.on("tui.prompt.append", (evt) => {
sdk.event.on(TuiEvent.PromptAppend.type, (evt) => {
setStore(
"prompt",
produce((draft) => {

View file

@ -8,7 +8,6 @@ import { Global } from "@/global"
import { iife } from "@/util/iife"
import { createSimpleContext } from "./helper"
import { useToast } from "../ui/toast"
import type { Provider } from "@opencode-ai/sdk"
export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
name: "Local",
@ -40,7 +39,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
const [providerID, modelID] = props.initialModel.split("/")
if (!providerID || !modelID)
return toast.show({
type: "warning",
variant: "warning",
message: `Invalid model format: ${props.initialModel}`,
duration: 3000,
})
@ -60,7 +59,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
})
else
toast.show({
type: "warning",
variant: "warning",
message: `Agent ${value.name}'s configured model ${value.model.providerID}/${value.model.modelID} is not valid`,
duration: 3000,
})
@ -93,7 +92,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
set(name: string) {
if (!agents().some((x) => x.name === name))
return toast.show({
type: "warning",
variant: "warning",
message: `Agent not found: ${name}`,
duration: 3000,
})
@ -211,7 +210,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
if (!isModelValid(model)) {
toast.show({
message: `Model ${model.providerID}/${model.modelID} is not valid`,
type: "warning",
variant: "warning",
duration: 3000,
})
return
@ -274,4 +273,4 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
}
return result
},
})
})

View file

@ -1,5 +1,6 @@
import { Bus } from "@/bus"
import z from "zod"
import { Schema as ToastSchema } from "./ui/toast"
export const TuiEvent = {
PromptAppend: Bus.event("tui.prompt.append", z.object({ text: z.string() })),
@ -29,10 +30,6 @@ export const TuiEvent = {
),
ToastShow: Bus.event(
"tui.toast.show",
z.object({
title: z.string().optional(),
message: z.string(),
variant: z.enum(["info", "success", "warning", "error"]),
}),
ToastSchema,
),
}

View file

@ -2,12 +2,17 @@ import { createContext, useContext, type ParentProps, Show } from "solid-js"
import { createStore } from "solid-js/store"
import { useTheme } from "@tui/context/theme"
import { SplitBorder } from "../component/border"
import { TextAttributes } from "@opentui/core"
import z from "zod"
export interface ToastOptions {
message: string | null
duration?: number
type: "info" | "success" | "warning" | "error"
}
export const Schema = z.object({
title: z.string().optional(),
message: z.string(),
variant: z.enum(["info", "success", "warning", "error"]),
duration: z.number().default(5000).optional().describe("Duration in milliseconds"),
})
export type ToastOptions = z.infer<typeof Schema>
export function Toast() {
const toast = useToast()
@ -19,7 +24,7 @@ export function Toast() {
<box
position="absolute"
justifyContent="center"
alignItems="center"
alignItems="flex-start"
top={2}
right={2}
paddingLeft={2}
@ -27,10 +32,13 @@ export function Toast() {
paddingTop={1}
paddingBottom={1}
backgroundColor={theme.backgroundPanel}
borderColor={theme[current().type]}
borderColor={theme[current().variant]}
border={["left", "right"]}
customBorderChars={SplitBorder.customBorderChars}
>
<Show when={current().title}>
<text attributes={TextAttributes.BOLD} marginBottom={1}>{current().title}</text>
</Show>
<text>{current().message}</text>
</box>
)}
@ -47,12 +55,13 @@ function init() {
return {
show(options: ToastOptions) {
const { duration, ...currentToast } = options
const parsedOptions = Schema.parse(options)
const { duration, ...currentToast } = parsedOptions
setStore("currentToast", currentToast)
if (timeoutHandle) clearTimeout(timeoutHandle)
timeoutHandle = setTimeout(() => {
setStore("currentToast", null)
}, duration ?? 5000).unref()
}, duration).unref()
},
get currentToast(): ToastOptions | null {
return store.currentToast

View file

@ -18,10 +18,6 @@ export type KeybindsConfig = {
* Leader key for keybind combinations
*/
leader?: string
/**
* Show help dialog
*/
app_help?: string
/**
* Exit the application
*/
@ -34,18 +30,6 @@ export type KeybindsConfig = {
* List available themes
*/
theme_list?: string
/**
* Create/update AGENTS.md
*/
project_init?: string
/**
* Toggle tool details
*/
tool_details?: string
/**
* Toggle thinking blocks
*/
thinking_blocks?: string
/**
* Toggle sidebar
*/
@ -86,14 +70,6 @@ export type KeybindsConfig = {
* Compact the session
*/
session_compact?: string
/**
* Cycle to next child session
*/
session_child_cycle?: string
/**
* Cycle to previous child session
*/
session_child_cycle_reverse?: string
/**
* Scroll messages up by one page
*/
@ -138,14 +114,6 @@ export type KeybindsConfig = {
* List available commands
*/
command_list?: string
/**
* Next recent model
*/
model_cycle_recent?: string
/**
* Previous recent model
*/
model_cycle_recent_reverse?: string
/**
* List agents
*/
@ -174,54 +142,6 @@ export type KeybindsConfig = {
* Insert newline in input
*/
input_newline?: string
/**
* @deprecated use agent_cycle. Next mode
*/
switch_mode?: string
/**
* @deprecated use agent_cycle_reverse. Previous mode
*/
switch_mode_reverse?: string
/**
* @deprecated use agent_cycle. Next agent
*/
switch_agent?: string
/**
* @deprecated use agent_cycle_reverse. Previous agent
*/
switch_agent_reverse?: string
/**
* @deprecated Currently not available. List files
*/
file_list?: string
/**
* @deprecated Close file
*/
file_close?: string
/**
* @deprecated Search file
*/
file_search?: string
/**
* @deprecated Split/unified diff
*/
file_diff_toggle?: string
/**
* @deprecated Navigate to previous message
*/
messages_previous?: string
/**
* @deprecated Navigate to next message
*/
messages_next?: string
/**
* @deprecated Toggle layout
*/
messages_layout_toggle?: string
/**
* @deprecated use messages_undo. Revert message
*/
messages_revert?: string
}
export type AgentConfig = {
@ -442,6 +362,9 @@ export type Config = {
options?: {
[key: string]: unknown
}
headers?: {
[key: string]: string
}
provider?: {
npm: string
}
@ -1009,6 +932,9 @@ export type Model = {
options: {
[key: string]: unknown
}
headers?: {
[key: string]: string
}
provider?: {
npm: string
}
@ -1157,6 +1083,10 @@ export type EventTuiToastShow = {
title?: string
message: string
variant: "info" | "success" | "warning" | "error"
/**
* Duration in milliseconds
*/
duration?: number
}
}
@ -2728,6 +2658,10 @@ export type TuiShowToastData = {
title?: string
message: string
variant: "info" | "success" | "warning" | "error"
/**
* Duration in milliseconds
*/
duration?: number
}
path?: never
query?: {