Improve OpenTUI session rendering and tool display (has typecheck errors)
This commit is contained in:
parent
173c5f1bd6
commit
0cc3d9229e
1 changed files with 71 additions and 38 deletions
|
|
@ -5,7 +5,7 @@ import { useRouteData } from "./context/route"
|
||||||
import { useSync } from "./context/sync"
|
import { useSync } from "./context/sync"
|
||||||
import { SplitBorder } from "./component/border"
|
import { SplitBorder } from "./component/border"
|
||||||
import { Theme } from "./context/theme"
|
import { Theme } from "./context/theme"
|
||||||
import { hastToStyledText, RGBA, ScrollBoxRenderable, SyntaxStyle } from "@opentui/core"
|
import { BoxRenderable, hastToStyledText, RGBA, ScrollBoxRenderable, SyntaxStyle } from "@opentui/core"
|
||||||
import { Prompt } from "./component/prompt"
|
import { Prompt } from "./component/prompt"
|
||||||
import type { AssistantMessage, Part, ToolPart, UserMessage } from "@opencode-ai/sdk"
|
import type { AssistantMessage, Part, ToolPart, UserMessage } from "@opencode-ai/sdk"
|
||||||
import type { TextPart } from "ai"
|
import type { TextPart } from "ai"
|
||||||
|
|
@ -25,7 +25,7 @@ import type { EditTool } from "../../../tool/edit"
|
||||||
import type { PatchTool } from "../../../tool/patch"
|
import type { PatchTool } from "../../../tool/patch"
|
||||||
import type { WebFetchTool } from "../../../tool/webfetch"
|
import type { WebFetchTool } from "../../../tool/webfetch"
|
||||||
import type { TaskTool } from "../../../tool/task"
|
import type { TaskTool } from "../../../tool/task"
|
||||||
import { useKeyboard, type JSX } from "@opentui/solid"
|
import { useKeyboard, type BoxProps, type JSX } from "@opentui/solid"
|
||||||
import { createStore } from "solid-js/store"
|
import { createStore } from "solid-js/store"
|
||||||
|
|
||||||
export function Session() {
|
export function Session() {
|
||||||
|
|
@ -39,8 +39,8 @@ export function Session() {
|
||||||
createEffect(() => sync.session.sync(route.sessionID))
|
createEffect(() => sync.session.sync(route.sessionID))
|
||||||
|
|
||||||
useKeyboard((evt) => {
|
useKeyboard((evt) => {
|
||||||
if (evt.name === "pageup") scroll.scrollBy(-scroll.height)
|
if (evt.name === "pageup") scroll.scrollBy(-scroll.height / 2)
|
||||||
if (evt.name === "pagedown") scroll.scrollBy(scroll.height)
|
if (evt.name === "pagedown") scroll.scrollBy(scroll.height / 2)
|
||||||
})
|
})
|
||||||
|
|
||||||
const [store, setStore] = createStore({
|
const [store, setStore] = createStore({
|
||||||
|
|
@ -136,23 +136,25 @@ function AssistantMessage(props: { message: AssistantMessage; parts: Part[] }) {
|
||||||
<For each={props.parts}>
|
<For each={props.parts}>
|
||||||
{(part) => {
|
{(part) => {
|
||||||
const component = createMemo(() => PART_MAPPING[part.type as keyof typeof PART_MAPPING])
|
const component = createMemo(() => PART_MAPPING[part.type as keyof typeof PART_MAPPING])
|
||||||
const [margin, setMargin] = createSignal(0)
|
function resize(el: BoxRenderable) {
|
||||||
|
const parent = el.parent
|
||||||
|
if (!parent) return
|
||||||
|
const children = parent.getChildren()
|
||||||
|
const index = children.indexOf(el)
|
||||||
|
const previous = children[index - 1]
|
||||||
|
if (!previous) return
|
||||||
|
console.log(previous.height, el.height)
|
||||||
|
if (el.height > 1 || previous.height > 1) {
|
||||||
|
el.marginTop = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<Show when={component()}>
|
<Show when={component()}>
|
||||||
<box
|
<box
|
||||||
marginTop={margin()}
|
|
||||||
onSizeChange={function () {
|
onSizeChange={function () {
|
||||||
const el = this
|
resize(this)
|
||||||
if (!el.parent) return
|
|
||||||
const children = el.parent!.getChildren()
|
|
||||||
const index = children.indexOf(el)
|
|
||||||
const previous = children[index - 1]
|
|
||||||
if (previous.height > 1 || el.height > 1) {
|
|
||||||
setMargin(1)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
setMargin(0)
|
|
||||||
}}
|
}}
|
||||||
|
ref={(el) => resize(el!)}
|
||||||
>
|
>
|
||||||
<Dynamic component={component()} part={part as any} message={props.message} />
|
<Dynamic component={component()} part={part as any} message={props.message} />
|
||||||
</box>
|
</box>
|
||||||
|
|
@ -194,8 +196,23 @@ function ToolPart(props: { part: ToolPart; message: AssistantMessage }) {
|
||||||
const metadata = props.part.state.status === "pending" ? {} : (props.part.state.metadata ?? {})
|
const metadata = props.part.state.status === "pending" ? {} : (props.part.state.metadata ?? {})
|
||||||
const input = props.part.state.input
|
const input = props.part.state.input
|
||||||
|
|
||||||
|
const container: BoxProps =
|
||||||
|
ToolRegistry.container(props.part.tool) === "block"
|
||||||
|
? {
|
||||||
|
border: ["left"] as const,
|
||||||
|
paddingTop: 1,
|
||||||
|
paddingBottom: 1,
|
||||||
|
paddingLeft: 2,
|
||||||
|
backgroundColor: Theme.backgroundPanel,
|
||||||
|
customBorderChars: SplitBorder.customBorderChars,
|
||||||
|
borderColor: Theme.background,
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
paddingLeft: 3,
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<box>
|
<box {...container}>
|
||||||
<Dynamic
|
<Dynamic
|
||||||
component={ready}
|
component={ready}
|
||||||
input={input}
|
input={input}
|
||||||
|
|
@ -203,7 +220,7 @@ function ToolPart(props: { part: ToolPart; message: AssistantMessage }) {
|
||||||
output={props.part.state.status === "completed" ? props.part.state.output : undefined}
|
output={props.part.state.status === "completed" ? props.part.state.output : undefined}
|
||||||
/>
|
/>
|
||||||
{props.part.state.status === "error" && (
|
{props.part.state.status === "error" && (
|
||||||
<box paddingLeft={2}>
|
<box paddingLeft={2} marginTop={1}>
|
||||||
<text fg={Theme.error}>{props.part.state.error.replace("Error: ", "")}</text>
|
<text fg={Theme.error}>{props.part.state.error.replace("Error: ", "")}</text>
|
||||||
</box>
|
</box>
|
||||||
)}
|
)}
|
||||||
|
|
@ -211,11 +228,7 @@ function ToolPart(props: { part: ToolPart; message: AssistantMessage }) {
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return <Show when={component()}>{component()}</Show>
|
||||||
<Show when={component()}>
|
|
||||||
<box paddingLeft={3}>{component()}</box>
|
|
||||||
</Show>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type ToolProps<T extends Tool.Info> = {
|
type ToolProps<T extends Tool.Info> = {
|
||||||
|
|
@ -225,13 +238,20 @@ type ToolProps<T extends Tool.Info> = {
|
||||||
}
|
}
|
||||||
|
|
||||||
const ToolRegistry = (() => {
|
const ToolRegistry = (() => {
|
||||||
const state: Record<string, { name: string; ready?: Component<ToolProps<any>> }> = {}
|
const state: Record<string, { name: string; container: "inline" | "block"; ready?: Component<ToolProps<any>> }> = {}
|
||||||
function register<T extends Tool.Info>(input: { name: string; ready?: Component<ToolProps<T>> }) {
|
function register<T extends Tool.Info>(input: {
|
||||||
|
name: string
|
||||||
|
container: "inline" | "block"
|
||||||
|
ready?: Component<ToolProps<T>>
|
||||||
|
}) {
|
||||||
state[input.name] = input
|
state[input.name] = input
|
||||||
return input
|
return input
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
register,
|
register,
|
||||||
|
container(name: string) {
|
||||||
|
return state[name]?.container
|
||||||
|
},
|
||||||
ready(name: string) {
|
ready(name: string) {
|
||||||
return state[name]?.ready
|
return state[name]?.ready
|
||||||
},
|
},
|
||||||
|
|
@ -240,7 +260,7 @@ const ToolRegistry = (() => {
|
||||||
|
|
||||||
function ToolTitle(props: { fallback: string; when: any; icon: string; children: JSX.Element }) {
|
function ToolTitle(props: { fallback: string; when: any; icon: string; children: JSX.Element }) {
|
||||||
return (
|
return (
|
||||||
<text fg={props.when ? Theme.textMuted : Theme.text}>
|
<text paddingLeft={3} fg={props.when ? Theme.textMuted : Theme.text}>
|
||||||
<Show fallback={<>~ {props.fallback}</>} when={props.when}>
|
<Show fallback={<>~ {props.fallback}</>} when={props.when}>
|
||||||
<span style={{ bold: true }}>{props.icon}</span> {props.children}
|
<span style={{ bold: true }}>{props.icon}</span> {props.children}
|
||||||
</Show>
|
</Show>
|
||||||
|
|
@ -250,21 +270,22 @@ function ToolTitle(props: { fallback: string; when: any; icon: string; children:
|
||||||
|
|
||||||
ToolRegistry.register<typeof BashTool>({
|
ToolRegistry.register<typeof BashTool>({
|
||||||
name: "bash",
|
name: "bash",
|
||||||
|
container: "block",
|
||||||
ready(props) {
|
ready(props) {
|
||||||
return (
|
return (
|
||||||
<>
|
<box>
|
||||||
<ToolTitle icon="#" fallback="Writing command..." when={props.input.command}>
|
<ToolTitle icon="#" fallback="Writing command..." when={props.input.command}>
|
||||||
{props.input.description}
|
{props.input.description}
|
||||||
</ToolTitle>
|
</ToolTitle>
|
||||||
<Show when={props.input.command}>
|
<Show when={props.input.command}>
|
||||||
<box>
|
<box paddingTop={1}>
|
||||||
<text fg={Theme.textMuted}>$ {props.input.command}</text>
|
<text fg={Theme.text}>$ {props.input.command}</text>
|
||||||
<box>
|
<box>
|
||||||
<text fg={Theme.textMuted}>{props.output?.trim()}</text>
|
<text fg={Theme.text}>{props.output?.trim()}</text>
|
||||||
</box>
|
</box>
|
||||||
</box>
|
</box>
|
||||||
</Show>
|
</Show>
|
||||||
</>
|
</box>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
@ -285,6 +306,7 @@ const syntax = new SyntaxStyle({
|
||||||
|
|
||||||
ToolRegistry.register<typeof ReadTool>({
|
ToolRegistry.register<typeof ReadTool>({
|
||||||
name: "read",
|
name: "read",
|
||||||
|
container: "inline",
|
||||||
ready(props) {
|
ready(props) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
@ -298,6 +320,7 @@ ToolRegistry.register<typeof ReadTool>({
|
||||||
|
|
||||||
ToolRegistry.register<typeof WriteTool>({
|
ToolRegistry.register<typeof WriteTool>({
|
||||||
name: "write",
|
name: "write",
|
||||||
|
container: "block",
|
||||||
ready(props) {
|
ready(props) {
|
||||||
const lines = createMemo(() => {
|
const lines = createMemo(() => {
|
||||||
return props.input.content?.split("\n") ?? []
|
return props.input.content?.split("\n") ?? []
|
||||||
|
|
@ -337,6 +360,7 @@ ToolRegistry.register<typeof WriteTool>({
|
||||||
|
|
||||||
ToolRegistry.register<typeof GlobTool>({
|
ToolRegistry.register<typeof GlobTool>({
|
||||||
name: "glob",
|
name: "glob",
|
||||||
|
container: "inline",
|
||||||
ready(props) {
|
ready(props) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
@ -350,9 +374,10 @@ ToolRegistry.register<typeof GlobTool>({
|
||||||
|
|
||||||
ToolRegistry.register<typeof GrepTool>({
|
ToolRegistry.register<typeof GrepTool>({
|
||||||
name: "grep",
|
name: "grep",
|
||||||
|
container: "inline",
|
||||||
ready(props) {
|
ready(props) {
|
||||||
return (
|
return (
|
||||||
<ToolTitle icon="%" fallback="Searching content..." when={props.input.pattern}>
|
<ToolTitle icon="✱" fallback="Searching content..." when={props.input.pattern}>
|
||||||
Grep "{props.input.pattern}" <Show when={props.metadata.matches}>({props.metadata.matches} matches)</Show>
|
Grep "{props.input.pattern}" <Show when={props.metadata.matches}>({props.metadata.matches} matches)</Show>
|
||||||
</ToolTitle>
|
</ToolTitle>
|
||||||
)
|
)
|
||||||
|
|
@ -361,6 +386,7 @@ ToolRegistry.register<typeof GrepTool>({
|
||||||
|
|
||||||
ToolRegistry.register<typeof ListTool>({
|
ToolRegistry.register<typeof ListTool>({
|
||||||
name: "list",
|
name: "list",
|
||||||
|
container: "inline",
|
||||||
ready(props) {
|
ready(props) {
|
||||||
const dir = createMemo(() => {
|
const dir = createMemo(() => {
|
||||||
if (props.input.path) {
|
if (props.input.path) {
|
||||||
|
|
@ -380,6 +406,7 @@ ToolRegistry.register<typeof ListTool>({
|
||||||
|
|
||||||
ToolRegistry.register<typeof TaskTool>({
|
ToolRegistry.register<typeof TaskTool>({
|
||||||
name: "task",
|
name: "task",
|
||||||
|
container: "block",
|
||||||
ready(props) {
|
ready(props) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
@ -387,7 +414,7 @@ ToolRegistry.register<typeof TaskTool>({
|
||||||
Task {props.input.description}
|
Task {props.input.description}
|
||||||
</ToolTitle>
|
</ToolTitle>
|
||||||
<Show when={props.metadata.summary?.length}>
|
<Show when={props.metadata.summary?.length}>
|
||||||
<box>
|
<box marginTop={1}>
|
||||||
<For each={props.metadata.summary ?? []}>
|
<For each={props.metadata.summary ?? []}>
|
||||||
{(task) => (
|
{(task) => (
|
||||||
<text style={{ fg: Theme.textMuted }}>
|
<text style={{ fg: Theme.textMuted }}>
|
||||||
|
|
@ -404,6 +431,7 @@ ToolRegistry.register<typeof TaskTool>({
|
||||||
|
|
||||||
ToolRegistry.register<typeof WebFetchTool>({
|
ToolRegistry.register<typeof WebFetchTool>({
|
||||||
name: "webfetch",
|
name: "webfetch",
|
||||||
|
container: "block",
|
||||||
ready(props) {
|
ready(props) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
@ -422,6 +450,7 @@ ToolRegistry.register<typeof WebFetchTool>({
|
||||||
|
|
||||||
ToolRegistry.register<typeof EditTool>({
|
ToolRegistry.register<typeof EditTool>({
|
||||||
name: "edit",
|
name: "edit",
|
||||||
|
container: "block",
|
||||||
ready(props) {
|
ready(props) {
|
||||||
const code = createMemo(() => {
|
const code = createMemo(() => {
|
||||||
if (!props.metadata.diff) return ""
|
if (!props.metadata.diff) return ""
|
||||||
|
|
@ -447,6 +476,7 @@ ToolRegistry.register<typeof EditTool>({
|
||||||
|
|
||||||
ToolRegistry.register<typeof PatchTool>({
|
ToolRegistry.register<typeof PatchTool>({
|
||||||
name: "patch",
|
name: "patch",
|
||||||
|
container: "block",
|
||||||
ready(props) {
|
ready(props) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
@ -465,13 +495,16 @@ ToolRegistry.register<typeof PatchTool>({
|
||||||
|
|
||||||
ToolRegistry.register<typeof TodoWriteTool>({
|
ToolRegistry.register<typeof TodoWriteTool>({
|
||||||
name: "todowrite",
|
name: "todowrite",
|
||||||
|
container: "block",
|
||||||
ready(props) {
|
ready(props) {
|
||||||
return (
|
return (
|
||||||
<>
|
<For each={props.input.todos ?? []}>
|
||||||
<ToolTitle icon="~" fallback="Planning..." when={true}>
|
{(todo) => (
|
||||||
Updated todos
|
<text style={{ fg: todo.status === "in_progress" ? Theme.success : Theme.textMuted }}>
|
||||||
</ToolTitle>
|
[{todo.status === "completed" ? "✓" : " "}] {todo.content}
|
||||||
</>
|
</text>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue