Compare commits
3 commits
dev
...
tui-experi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b892d91bae | ||
|
|
ad110878c9 | ||
|
|
572fab3743 |
4 changed files with 255 additions and 369 deletions
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "opencode",
|
||||
"description": "AI-powered development tool",
|
||||
"description": "AI-powered coding assistant that generates perfect code most of the time",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"packageManager": "bun@1.3.10",
|
||||
|
|
|
|||
|
|
@ -814,24 +814,21 @@ export function Prompt(props: PromptProps) {
|
|||
agentStyleId={agentStyleId}
|
||||
promptPartTypeId={() => promptPartTypeId}
|
||||
/>
|
||||
<box ref={(r) => (anchor = r)} visible={props.visible !== false}>
|
||||
<box
|
||||
ref={(r) => (anchor = r)}
|
||||
visible={props.visible !== false}
|
||||
backgroundColor={theme.backgroundElement}
|
||||
paddingBottom={1}
|
||||
paddingLeft={1}
|
||||
paddingRight={2}
|
||||
border={["left"]}
|
||||
borderColor={highlight()}
|
||||
customBorderChars={{
|
||||
...EmptyBorder,
|
||||
vertical: "┃",
|
||||
bottomLeft: "╹",
|
||||
vertical: "▎",
|
||||
}}
|
||||
>
|
||||
<box
|
||||
paddingLeft={2}
|
||||
paddingRight={2}
|
||||
paddingTop={1}
|
||||
flexShrink={0}
|
||||
backgroundColor={theme.backgroundElement}
|
||||
flexGrow={1}
|
||||
>
|
||||
<box paddingTop={1} flexShrink={0} flexGrow={1}>
|
||||
<textarea
|
||||
placeholder={placeholderText()}
|
||||
textColor={keybind.leader ? theme.textMuted : theme.text}
|
||||
|
|
@ -1010,12 +1007,11 @@ export function Prompt(props: PromptProps) {
|
|||
cursorColor={theme.text}
|
||||
syntaxStyle={syntax()}
|
||||
/>
|
||||
<box flexDirection="row" flexShrink={0} paddingTop={1} gap={1}>
|
||||
<text fg={highlight()}>
|
||||
{store.mode === "shell" ? "Shell" : Locale.titlecase(local.agent.current().name)}{" "}
|
||||
</text>
|
||||
<Show when={store.mode === "normal"}>
|
||||
<box flexDirection="row" flexShrink={0} paddingTop={1} gap={1} justifyContent="space-between">
|
||||
<Switch>
|
||||
<Match when={store.mode === "normal"}>
|
||||
<box flexDirection="row" gap={1}>
|
||||
<text fg={highlight()}>{Locale.titlecase(local.agent.current().name)} </text>
|
||||
<text flexShrink={0} fg={keybind.leader ? theme.textMuted : theme.text}>
|
||||
{local.model.parsed().model}
|
||||
</text>
|
||||
|
|
@ -1027,121 +1023,8 @@ export function Prompt(props: PromptProps) {
|
|||
</text>
|
||||
</Show>
|
||||
</box>
|
||||
</Show>
|
||||
</box>
|
||||
</box>
|
||||
</box>
|
||||
<box
|
||||
height={1}
|
||||
border={["left"]}
|
||||
borderColor={highlight()}
|
||||
customBorderChars={{
|
||||
...EmptyBorder,
|
||||
vertical: theme.backgroundElement.a !== 0 ? "╹" : " ",
|
||||
}}
|
||||
>
|
||||
<box
|
||||
height={1}
|
||||
border={["bottom"]}
|
||||
borderColor={theme.backgroundElement}
|
||||
customBorderChars={
|
||||
theme.backgroundElement.a !== 0
|
||||
? {
|
||||
...EmptyBorder,
|
||||
horizontal: "▀",
|
||||
}
|
||||
: {
|
||||
...EmptyBorder,
|
||||
horizontal: " ",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</box>
|
||||
<box flexDirection="row" justifyContent="space-between">
|
||||
<Show when={status().type !== "idle"} fallback={<text />}>
|
||||
<box
|
||||
flexDirection="row"
|
||||
gap={1}
|
||||
flexGrow={1}
|
||||
justifyContent={status().type === "retry" ? "space-between" : "flex-start"}
|
||||
>
|
||||
<box flexShrink={0} flexDirection="row" gap={1}>
|
||||
<box marginLeft={1}>
|
||||
<Show when={kv.get("animations_enabled", true)} fallback={<text fg={theme.textMuted}>[⋯]</text>}>
|
||||
<spinner color={spinnerDef().color} frames={spinnerDef().frames} interval={40} />
|
||||
</Show>
|
||||
</box>
|
||||
<box flexDirection="row" gap={1} flexShrink={0}>
|
||||
{(() => {
|
||||
const retry = createMemo(() => {
|
||||
const s = status()
|
||||
if (s.type !== "retry") return
|
||||
return s
|
||||
})
|
||||
const message = createMemo(() => {
|
||||
const r = retry()
|
||||
if (!r) return
|
||||
if (r.message.includes("exceeded your current quota") && r.message.includes("gemini"))
|
||||
return "gemini is way too hot right now"
|
||||
if (r.message.length > 80) return r.message.slice(0, 80) + "..."
|
||||
return r.message
|
||||
})
|
||||
const isTruncated = createMemo(() => {
|
||||
const r = retry()
|
||||
if (!r) return false
|
||||
return r.message.length > 120
|
||||
})
|
||||
const [seconds, setSeconds] = createSignal(0)
|
||||
onMount(() => {
|
||||
const timer = setInterval(() => {
|
||||
const next = retry()?.next
|
||||
if (next) setSeconds(Math.round((next - Date.now()) / 1000))
|
||||
}, 1000)
|
||||
|
||||
onCleanup(() => {
|
||||
clearInterval(timer)
|
||||
})
|
||||
})
|
||||
const handleMessageClick = () => {
|
||||
const r = retry()
|
||||
if (!r) return
|
||||
if (isTruncated()) {
|
||||
DialogAlert.show(dialog, "Retry Error", r.message)
|
||||
}
|
||||
}
|
||||
|
||||
const retryText = () => {
|
||||
const r = retry()
|
||||
if (!r) return ""
|
||||
const baseMessage = message()
|
||||
const truncatedHint = isTruncated() ? " (click to expand)" : ""
|
||||
const duration = formatDuration(seconds())
|
||||
const retryInfo = ` [retrying ${duration ? `in ${duration} ` : ""}attempt #${r.attempt}]`
|
||||
return baseMessage + truncatedHint + retryInfo
|
||||
}
|
||||
|
||||
return (
|
||||
<Show when={retry()}>
|
||||
<box onMouseUp={handleMessageClick}>
|
||||
<text fg={theme.error}>{retryText()}</text>
|
||||
</box>
|
||||
</Show>
|
||||
)
|
||||
})()}
|
||||
</box>
|
||||
</box>
|
||||
<text fg={store.interrupt > 0 ? theme.primary : theme.text}>
|
||||
esc{" "}
|
||||
<span style={{ fg: store.interrupt > 0 ? theme.primary : theme.textMuted }}>
|
||||
{store.interrupt > 0 ? "again to interrupt" : "interrupt"}
|
||||
</span>
|
||||
</text>
|
||||
</box>
|
||||
</Show>
|
||||
<Show when={status().type !== "retry"}>
|
||||
<box gap={2} flexDirection="row">
|
||||
<Switch>
|
||||
<Match when={store.mode === "normal"}>
|
||||
<Show when={local.model.variant.list().length > 0}>
|
||||
<text fg={theme.text}>
|
||||
{keybind.print("variant_cycle")} <span style={{ fg: theme.textMuted }}>variants</span>
|
||||
|
|
@ -1153,15 +1036,16 @@ export function Prompt(props: PromptProps) {
|
|||
<text fg={theme.text}>
|
||||
{keybind.print("command_list")} <span style={{ fg: theme.textMuted }}>commands</span>
|
||||
</text>
|
||||
</box>
|
||||
</Show>
|
||||
</Match>
|
||||
<Match when={store.mode === "shell"}>
|
||||
<text fg={theme.text}>
|
||||
esc <span style={{ fg: theme.textMuted }}>exit shell mode</span>
|
||||
<text fg={highlight()}>
|
||||
{store.mode === "shell" ? "Shell" : Locale.titlecase(local.agent.current().name)}{" "}
|
||||
</text>
|
||||
</Match>
|
||||
</Switch>
|
||||
</box>
|
||||
</Show>
|
||||
</box>
|
||||
</box>
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -93,9 +93,6 @@ export function Header() {
|
|||
paddingBottom={1}
|
||||
paddingLeft={2}
|
||||
paddingRight={1}
|
||||
{...SplitBorder}
|
||||
border={["left"]}
|
||||
borderColor={theme.border}
|
||||
flexShrink={0}
|
||||
backgroundColor={theme.backgroundPanel}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ import { Dynamic } from "solid-js/web"
|
|||
import path from "path"
|
||||
import { useRoute, useRouteData } from "@tui/context/route"
|
||||
import { useSync } from "@tui/context/sync"
|
||||
import { SplitBorder } from "@tui/component/border"
|
||||
import { EmptyBorder, SplitBorder } from "@tui/component/border"
|
||||
import { Spinner } from "@tui/component/spinner"
|
||||
import { selectedForeground, useTheme } from "@tui/context/theme"
|
||||
import {
|
||||
|
|
@ -1046,7 +1046,12 @@ export function Session() {
|
|||
}}
|
||||
>
|
||||
<box flexDirection="row">
|
||||
<box flexGrow={1} paddingBottom={1} paddingTop={1} paddingLeft={2} paddingRight={2} gap={1}>
|
||||
<box
|
||||
flexGrow={1}
|
||||
gap={1}
|
||||
paddingX={sidebarVisible() && wide() ? 2 : 0}
|
||||
paddingY={sidebarVisible() && wide() ? 1 : 0}
|
||||
>
|
||||
<Show when={session()}>
|
||||
<Show when={showHeader() && (!sidebarVisible() || !wide())}>
|
||||
<Header />
|
||||
|
|
@ -1057,7 +1062,7 @@ export function Session() {
|
|||
paddingRight: showScrollbar() ? 1 : 0,
|
||||
}}
|
||||
verticalScrollbarOptions={{
|
||||
paddingLeft: 1,
|
||||
paddingLeft: 0,
|
||||
visible: showScrollbar(),
|
||||
trackOptions: {
|
||||
backgroundColor: theme.backgroundElement,
|
||||
|
|
@ -1251,13 +1256,7 @@ function UserMessage(props: {
|
|||
return (
|
||||
<>
|
||||
<Show when={text()}>
|
||||
<box
|
||||
id={props.message.id}
|
||||
border={["left"]}
|
||||
borderColor={color()}
|
||||
customBorderChars={SplitBorder.customBorderChars}
|
||||
marginTop={props.index === 0 ? 0 : 1}
|
||||
>
|
||||
<box id={props.message.id} marginTop={props.index === 0 ? 0 : 1}>
|
||||
<box
|
||||
onMouseOver={() => {
|
||||
setHover(true)
|
||||
|
|
@ -1268,9 +1267,15 @@ function UserMessage(props: {
|
|||
onMouseUp={props.onMouseUp}
|
||||
paddingTop={1}
|
||||
paddingBottom={1}
|
||||
paddingLeft={2}
|
||||
backgroundColor={hover() ? theme.backgroundElement : theme.backgroundPanel}
|
||||
backgroundColor={hover() ? theme.backgroundMenu : theme.backgroundMenu}
|
||||
paddingLeft={1}
|
||||
flexShrink={0}
|
||||
border={["left"]}
|
||||
borderColor={color()}
|
||||
customBorderChars={{
|
||||
...EmptyBorder,
|
||||
vertical: "▎",
|
||||
}}
|
||||
>
|
||||
<text fg={theme.text}>{text()?.text}</text>
|
||||
<Show when={files().length}>
|
||||
|
|
@ -1331,6 +1336,7 @@ function AssistantMessage(props: { message: AssistantMessage; parts: Part[]; las
|
|||
const messages = createMemo(() => sync.data.message[props.message.sessionID] ?? [])
|
||||
|
||||
const final = createMemo(() => {
|
||||
if (props.message.error) return true
|
||||
return props.message.finish && !["tool-calls", "unknown"].includes(props.message.finish)
|
||||
})
|
||||
|
||||
|
|
@ -1342,6 +1348,8 @@ function AssistantMessage(props: { message: AssistantMessage; parts: Part[]; las
|
|||
return props.message.time.completed - user.time.created
|
||||
})
|
||||
|
||||
const interrupted = createMemo(() => props.message.error?.name === "MessageAbortedError")
|
||||
|
||||
const keybind = useKeybind()
|
||||
|
||||
return (
|
||||
|
|
@ -1362,14 +1370,14 @@ function AssistantMessage(props: { message: AssistantMessage; parts: Part[]; las
|
|||
}}
|
||||
</For>
|
||||
<Show when={props.parts.some((x) => x.type === "tool" && x.tool === "task")}>
|
||||
<box paddingTop={1} paddingLeft={3}>
|
||||
<box paddingTop={1} paddingLeft={2}>
|
||||
<text fg={theme.text}>
|
||||
{keybind.print("session_child_first")}
|
||||
<span style={{ fg: theme.textMuted }}> view subagents</span>
|
||||
</text>
|
||||
</box>
|
||||
</Show>
|
||||
<Show when={props.message.error && props.message.error.name !== "MessageAbortedError"}>
|
||||
<Show when={props.message.error && !interrupted()}>
|
||||
<box
|
||||
border={["left"]}
|
||||
paddingTop={1}
|
||||
|
|
@ -1384,28 +1392,26 @@ function AssistantMessage(props: { message: AssistantMessage; parts: Part[]; las
|
|||
</box>
|
||||
</Show>
|
||||
<Switch>
|
||||
<Match when={props.last || final() || props.message.error?.name === "MessageAbortedError"}>
|
||||
<box paddingLeft={3}>
|
||||
<text marginTop={1}>
|
||||
<span
|
||||
style={{
|
||||
fg:
|
||||
props.message.error?.name === "MessageAbortedError"
|
||||
? theme.textMuted
|
||||
: local.agent.color(props.message.agent),
|
||||
}}
|
||||
>
|
||||
▣{" "}
|
||||
</span>{" "}
|
||||
<Match when={props.last || final()}>
|
||||
<box paddingLeft={2} marginTop={1}>
|
||||
<Show when={!duration()}>
|
||||
<Spinner color={local.agent.color(props.message.agent)}>
|
||||
<span style={{ fg: theme.text }}>{Locale.titlecase(props.message.mode)}</span>
|
||||
<span style={{ fg: theme.textMuted }}> · {props.message.modelID}</span>
|
||||
</Spinner>
|
||||
</Show>
|
||||
<Show when={duration()}>
|
||||
<text fg={interrupted() ? theme.textMuted : theme.text}>
|
||||
<span style={{ fg: theme.text }}>{Locale.titlecase(props.message.mode)}</span>
|
||||
<span style={{ fg: theme.textMuted }}> · {props.message.modelID}</span>
|
||||
<Show when={!interrupted()}>
|
||||
<span style={{ fg: theme.textMuted }}> · {Locale.duration(duration())}</span>
|
||||
</Show>
|
||||
<Show when={props.message.error?.name === "MessageAbortedError"}>
|
||||
<Show when={interrupted()}>
|
||||
<span style={{ fg: theme.textMuted }}> · interrupted</span>
|
||||
</Show>
|
||||
</text>
|
||||
</Show>
|
||||
</box>
|
||||
</Match>
|
||||
</Switch>
|
||||
|
|
@ -1457,7 +1463,7 @@ function TextPart(props: { last: boolean; part: TextPart; message: AssistantMess
|
|||
const { theme, syntax } = useTheme()
|
||||
return (
|
||||
<Show when={props.part.text.trim()}>
|
||||
<box id={"text-" + props.part.id} paddingLeft={3} marginTop={1} flexShrink={0}>
|
||||
<box id={"text-" + props.part.id} paddingLeft={2} marginTop={1} flexShrink={0}>
|
||||
<Switch>
|
||||
<Match when={Flag.OPENCODE_EXPERIMENTAL_MARKDOWN}>
|
||||
<markdown
|
||||
|
|
@ -1625,7 +1631,7 @@ function GenericTool(props: ToolProps<any>) {
|
|||
function ToolTitle(props: { fallback: string; when: any; icon: string; children: JSX.Element }) {
|
||||
const { theme } = useTheme()
|
||||
return (
|
||||
<text paddingLeft={3} fg={props.when ? theme.textMuted : theme.text}>
|
||||
<text paddingLeft={2} fg={props.when ? theme.textMuted : theme.text}>
|
||||
<Show fallback={<>~ {props.fallback}</>} when={props.when}>
|
||||
<span style={{ bold: true }}>{props.icon}</span> {props.children}
|
||||
</Show>
|
||||
|
|
@ -1676,7 +1682,7 @@ function InlineTool(props: {
|
|||
return (
|
||||
<box
|
||||
marginTop={margin()}
|
||||
paddingLeft={3}
|
||||
paddingLeft={2}
|
||||
onMouseOver={() => props.onClick && setHover(true)}
|
||||
onMouseOut={() => setHover(false)}
|
||||
onMouseUp={() => {
|
||||
|
|
@ -1711,7 +1717,7 @@ function InlineTool(props: {
|
|||
<Spinner color={fg()} children={props.children} />
|
||||
</Match>
|
||||
<Match when={true}>
|
||||
<text paddingLeft={3} fg={fg()} attributes={denied() ? TextAttributes.STRIKETHROUGH : undefined}>
|
||||
<text paddingLeft={2} fg={fg()} attributes={denied() ? TextAttributes.STRIKETHROUGH : undefined}>
|
||||
<Show fallback={<>~ {props.pending}</>} when={props.complete}>
|
||||
<span style={{ fg: props.iconColor }}>{props.icon}</span> {props.children}
|
||||
</Show>
|
||||
|
|
@ -1738,15 +1744,13 @@ function BlockTool(props: {
|
|||
const error = createMemo(() => (props.part?.state.status === "error" ? props.part.state.error : undefined))
|
||||
return (
|
||||
<box
|
||||
border={["left"]}
|
||||
paddingTop={1}
|
||||
marginTop={1}
|
||||
paddingBottom={1}
|
||||
paddingLeft={2}
|
||||
marginTop={1}
|
||||
paddingRight={2}
|
||||
gap={1}
|
||||
backgroundColor={hover() ? theme.backgroundMenu : theme.backgroundPanel}
|
||||
customBorderChars={SplitBorder.customBorderChars}
|
||||
borderColor={theme.background}
|
||||
backgroundColor={hover() ? theme.backgroundElement : theme.backgroundPanel}
|
||||
onMouseOver={() => props.onClick && setHover(true)}
|
||||
onMouseOut={() => setHover(false)}
|
||||
onMouseUp={() => {
|
||||
|
|
@ -1757,7 +1761,7 @@ function BlockTool(props: {
|
|||
<Show
|
||||
when={props.spinner}
|
||||
fallback={
|
||||
<text paddingLeft={3} fg={theme.textMuted}>
|
||||
<text paddingLeft={2} fg={theme.textMuted}>
|
||||
{props.title}
|
||||
</text>
|
||||
}
|
||||
|
|
@ -1905,8 +1909,8 @@ function Read(props: ToolProps<typeof ReadTool>) {
|
|||
</InlineTool>
|
||||
<For each={loaded()}>
|
||||
{(filepath) => (
|
||||
<box paddingLeft={3}>
|
||||
<text paddingLeft={3} fg={theme.textMuted}>
|
||||
<box paddingLeft={2}>
|
||||
<text paddingLeft={2} fg={theme.textMuted}>
|
||||
↳ Loaded {normalizePath(filepath)}
|
||||
</text>
|
||||
</box>
|
||||
|
|
@ -2056,7 +2060,7 @@ function Edit(props: ToolProps<typeof EditTool>) {
|
|||
<Switch>
|
||||
<Match when={props.metadata.diff !== undefined}>
|
||||
<BlockTool title={"← Edit " + normalizePath(props.input.filePath!)} part={props.part}>
|
||||
<box paddingLeft={1}>
|
||||
<box>
|
||||
<diff
|
||||
diff={diffContent()}
|
||||
view={view()}
|
||||
|
|
@ -2066,6 +2070,7 @@ function Edit(props: ToolProps<typeof EditTool>) {
|
|||
width="100%"
|
||||
wrapMode={ctx.diffWrapMode()}
|
||||
fg={theme.text}
|
||||
bg={theme.background}
|
||||
addedBg={theme.diffAddedBg}
|
||||
removedBg={theme.diffRemovedBg}
|
||||
contextBg={theme.diffContextBg}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue