Compare commits
1 Commits
dev
...
tui-claude
| Author | SHA1 | Date |
|---|---|---|
|
|
8a323b6a8b |
|
|
@ -86,6 +86,21 @@ const setSessionHandoff = (key: string, patch: Partial<HandoffSession>) => {
|
||||||
touch(handoff.session, key, { ...prev, ...patch })
|
touch(handoff.session, key, { ...prev, ...patch })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const readPromptParam = () => {
|
||||||
|
if (typeof window === "undefined") return
|
||||||
|
const value = new URLSearchParams(window.location.search).get("prompt")?.trim()
|
||||||
|
if (!value) return
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
const clearPromptParam = () => {
|
||||||
|
if (typeof window === "undefined") return
|
||||||
|
const url = new URL(window.location.href)
|
||||||
|
if (!url.searchParams.has("prompt")) return
|
||||||
|
url.searchParams.delete("prompt")
|
||||||
|
window.history.replaceState({}, "", url)
|
||||||
|
}
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
const layout = useLayout()
|
const layout = useLayout()
|
||||||
const local = useLocal()
|
const local = useLocal()
|
||||||
|
|
@ -1491,6 +1506,20 @@ export default function Page() {
|
||||||
setSessionHandoff(sessionKey(), { prompt: previewPrompt() })
|
setSessionHandoff(sessionKey(), { prompt: previewPrompt() })
|
||||||
})
|
})
|
||||||
|
|
||||||
|
createEffect(
|
||||||
|
on(
|
||||||
|
() => [params.dir, params.id, prompt.ready()] as const,
|
||||||
|
() => {
|
||||||
|
if (!prompt.ready()) return
|
||||||
|
const value = readPromptParam()
|
||||||
|
if (!value) return
|
||||||
|
clearPromptParam()
|
||||||
|
if (prompt.dirty()) return
|
||||||
|
prompt.set([{ type: "text", content: value, start: 0, end: value.length }], value.length)
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
if (!terminal.ready()) return
|
if (!terminal.ready()) return
|
||||||
language.locale()
|
language.locale()
|
||||||
|
|
|
||||||
|
|
@ -691,7 +691,6 @@ function App() {
|
||||||
<box
|
<box
|
||||||
width={dimensions().width}
|
width={dimensions().width}
|
||||||
height={dimensions().height}
|
height={dimensions().height}
|
||||||
backgroundColor={theme.background}
|
|
||||||
onMouseUp={async () => {
|
onMouseUp={async () => {
|
||||||
if (Flag.OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT) {
|
if (Flag.OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT) {
|
||||||
renderer.clearSelection()
|
renderer.clearSelection()
|
||||||
|
|
|
||||||
|
|
@ -19,3 +19,17 @@ export const SplitBorder = {
|
||||||
vertical: "┃",
|
vertical: "┃",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const RoundedBorder = {
|
||||||
|
topLeft: "╭",
|
||||||
|
topRight: "╮",
|
||||||
|
bottomLeft: "╰",
|
||||||
|
bottomRight: "╯",
|
||||||
|
horizontal: "─",
|
||||||
|
vertical: "│",
|
||||||
|
bottomT: "┴",
|
||||||
|
topT: "┬",
|
||||||
|
cross: "┼",
|
||||||
|
leftT: "├",
|
||||||
|
rightT: "┤",
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import { createEffect, createMemo, type JSX, onMount, createSignal, onCleanup, S
|
||||||
import "opentui-spinner/solid"
|
import "opentui-spinner/solid"
|
||||||
import { useLocal } from "@tui/context/local"
|
import { useLocal } from "@tui/context/local"
|
||||||
import { useTheme } from "@tui/context/theme"
|
import { useTheme } from "@tui/context/theme"
|
||||||
import { EmptyBorder } from "@tui/component/border"
|
|
||||||
import { useSDK } from "@tui/context/sdk"
|
import { useSDK } from "@tui/context/sdk"
|
||||||
import { useRoute } from "@tui/context/route"
|
import { useRoute } from "@tui/context/route"
|
||||||
import { useSync } from "@tui/context/sync"
|
import { useSync } from "@tui/context/sync"
|
||||||
|
|
@ -779,23 +779,11 @@ export function Prompt(props: PromptProps) {
|
||||||
promptPartTypeId={() => promptPartTypeId}
|
promptPartTypeId={() => promptPartTypeId}
|
||||||
/>
|
/>
|
||||||
<box ref={(r) => (anchor = r)} visible={props.visible !== false}>
|
<box ref={(r) => (anchor = r)} visible={props.visible !== false}>
|
||||||
<box
|
<box flexDirection="row" alignItems="flex-start" paddingLeft={1}>
|
||||||
border={["left"]}
|
<text fg={highlight()} flexShrink={0}>
|
||||||
borderColor={highlight()}
|
{store.mode === "shell" ? "! " : "❯ "}
|
||||||
customBorderChars={{
|
</text>
|
||||||
...EmptyBorder,
|
<box flexGrow={1}>
|
||||||
vertical: "┃",
|
|
||||||
bottomLeft: "╹",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<box
|
|
||||||
paddingLeft={2}
|
|
||||||
paddingRight={2}
|
|
||||||
paddingTop={1}
|
|
||||||
flexShrink={0}
|
|
||||||
backgroundColor={theme.backgroundElement}
|
|
||||||
flexGrow={1}
|
|
||||||
>
|
|
||||||
<textarea
|
<textarea
|
||||||
placeholder={props.sessionID ? undefined : `Ask anything... "${PLACEHOLDERS[store.placeholder]}"`}
|
placeholder={props.sessionID ? undefined : `Ask anything... "${PLACEHOLDERS[store.placeholder]}"`}
|
||||||
textColor={keybind.leader ? theme.textMuted : theme.text}
|
textColor={keybind.leader ? theme.textMuted : theme.text}
|
||||||
|
|
@ -969,59 +957,45 @@ export function Prompt(props: PromptProps) {
|
||||||
}, 0)
|
}, 0)
|
||||||
}}
|
}}
|
||||||
onMouseDown={(r: MouseEvent) => r.target?.focus()}
|
onMouseDown={(r: MouseEvent) => r.target?.focus()}
|
||||||
focusedBackgroundColor={theme.backgroundElement}
|
|
||||||
cursorColor={theme.text}
|
cursorColor={theme.text}
|
||||||
syntaxStyle={syntax()}
|
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" gap={1}>
|
|
||||||
<text flexShrink={0} fg={keybind.leader ? theme.textMuted : theme.text}>
|
|
||||||
{local.model.parsed().model}
|
|
||||||
</text>
|
|
||||||
<text fg={theme.textMuted}>{local.model.parsed().provider}</text>
|
|
||||||
<Show when={showVariant()}>
|
|
||||||
<text fg={theme.textMuted}>·</text>
|
|
||||||
<text>
|
|
||||||
<span style={{ fg: theme.warning, bold: true }}>{local.model.variant.current()}</span>
|
|
||||||
</text>
|
|
||||||
</Show>
|
|
||||||
</box>
|
|
||||||
</Show>
|
|
||||||
</box>
|
|
||||||
</box>
|
</box>
|
||||||
</box>
|
</box>
|
||||||
<box
|
<box flexDirection="row" justifyContent="space-between" paddingLeft={3}>
|
||||||
height={1}
|
<Show
|
||||||
border={["left"]}
|
when={status().type !== "idle"}
|
||||||
borderColor={highlight()}
|
fallback={
|
||||||
customBorderChars={{
|
<box flexDirection="row" gap={2}>
|
||||||
...EmptyBorder,
|
<text fg={theme.textMuted}>
|
||||||
vertical: theme.backgroundElement.a !== 0 ? "╹" : " ",
|
{store.mode === "shell" ? "Shell" : Locale.titlecase(local.agent.current().name)}
|
||||||
}}
|
<Show when={store.mode === "normal"}>
|
||||||
>
|
{" · "}
|
||||||
<box
|
{local.model.parsed().model}
|
||||||
height={1}
|
<Show when={showVariant()}>
|
||||||
border={["bottom"]}
|
{" · "}
|
||||||
borderColor={theme.backgroundElement}
|
<span style={{ fg: theme.warning, bold: true }}>{local.model.variant.current()}</span>
|
||||||
customBorderChars={
|
</Show>
|
||||||
theme.backgroundElement.a !== 0
|
</Show>
|
||||||
? {
|
</text>
|
||||||
...EmptyBorder,
|
<box flexGrow={1} />
|
||||||
horizontal: "▀",
|
<box gap={2} flexDirection="row" flexShrink={0}>
|
||||||
}
|
<Switch>
|
||||||
: {
|
<Match when={store.mode === "normal"}>
|
||||||
...EmptyBorder,
|
<text fg={theme.textMuted}>
|
||||||
horizontal: " ",
|
{keybind.print("command_list")} <span style={{ fg: theme.textMuted }}>commands</span>
|
||||||
}
|
</text>
|
||||||
|
</Match>
|
||||||
|
<Match when={store.mode === "shell"}>
|
||||||
|
<text fg={theme.textMuted}>
|
||||||
|
esc <span style={{ fg: theme.textMuted }}>exit shell mode</span>
|
||||||
|
</text>
|
||||||
|
</Match>
|
||||||
|
</Switch>
|
||||||
|
</box>
|
||||||
|
</box>
|
||||||
}
|
}
|
||||||
/>
|
>
|
||||||
</box>
|
|
||||||
<box flexDirection="row" justifyContent="space-between">
|
|
||||||
<Show when={status().type !== "idle"} fallback={<text />}>
|
|
||||||
<box
|
<box
|
||||||
flexDirection="row"
|
flexDirection="row"
|
||||||
gap={1}
|
gap={1}
|
||||||
|
|
@ -1029,11 +1003,9 @@ export function Prompt(props: PromptProps) {
|
||||||
justifyContent={status().type === "retry" ? "space-between" : "flex-start"}
|
justifyContent={status().type === "retry" ? "space-between" : "flex-start"}
|
||||||
>
|
>
|
||||||
<box flexShrink={0} flexDirection="row" gap={1}>
|
<box flexShrink={0} flexDirection="row" gap={1}>
|
||||||
<box marginLeft={1}>
|
<Show when={kv.get("animations_enabled", true)} fallback={<text fg={theme.textMuted}>⋯</text>}>
|
||||||
<Show when={kv.get("animations_enabled", true)} fallback={<text fg={theme.textMuted}>[⋯]</text>}>
|
<spinner color={spinnerDef().color} frames={spinnerDef().frames} interval={40} />
|
||||||
<spinner color={spinnerDef().color} frames={spinnerDef().frames} interval={40} />
|
</Show>
|
||||||
</Show>
|
|
||||||
</box>
|
|
||||||
<box flexDirection="row" gap={1} flexShrink={0}>
|
<box flexDirection="row" gap={1} flexShrink={0}>
|
||||||
{(() => {
|
{(() => {
|
||||||
const retry = createMemo(() => {
|
const retry = createMemo(() => {
|
||||||
|
|
@ -1093,7 +1065,7 @@ export function Prompt(props: PromptProps) {
|
||||||
})()}
|
})()}
|
||||||
</box>
|
</box>
|
||||||
</box>
|
</box>
|
||||||
<text fg={store.interrupt > 0 ? theme.primary : theme.text}>
|
<text fg={store.interrupt > 0 ? theme.primary : theme.textMuted}>
|
||||||
esc{" "}
|
esc{" "}
|
||||||
<span style={{ fg: store.interrupt > 0 ? theme.primary : theme.textMuted }}>
|
<span style={{ fg: store.interrupt > 0 ? theme.primary : theme.textMuted }}>
|
||||||
{store.interrupt > 0 ? "again to interrupt" : "interrupt"}
|
{store.interrupt > 0 ? "again to interrupt" : "interrupt"}
|
||||||
|
|
@ -1101,30 +1073,6 @@ export function Prompt(props: PromptProps) {
|
||||||
</text>
|
</text>
|
||||||
</box>
|
</box>
|
||||||
</Show>
|
</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>
|
|
||||||
</text>
|
|
||||||
</Show>
|
|
||||||
<text fg={theme.text}>
|
|
||||||
{keybind.print("agent_cycle")} <span style={{ fg: theme.textMuted }}>agents</span>
|
|
||||||
</text>
|
|
||||||
<text fg={theme.text}>
|
|
||||||
{keybind.print("command_list")} <span style={{ fg: theme.textMuted }}>commands</span>
|
|
||||||
</text>
|
|
||||||
</Match>
|
|
||||||
<Match when={store.mode === "shell"}>
|
|
||||||
<text fg={theme.text}>
|
|
||||||
esc <span style={{ fg: theme.textMuted }}>exit shell mode</span>
|
|
||||||
</text>
|
|
||||||
</Match>
|
|
||||||
</Switch>
|
|
||||||
</box>
|
|
||||||
</Show>
|
|
||||||
</box>
|
</box>
|
||||||
</box>
|
</box>
|
||||||
</>
|
</>
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,31 @@
|
||||||
import { Prompt, type PromptRef } from "@tui/component/prompt"
|
import { Prompt, type PromptRef } from "@tui/component/prompt"
|
||||||
import { createMemo, Match, onMount, Show, Switch } from "solid-js"
|
import { createMemo, For, Match, onMount, Show, Switch } from "solid-js"
|
||||||
import { useTheme } from "@tui/context/theme"
|
import { useTheme } from "@tui/context/theme"
|
||||||
import { useKeybind } from "@tui/context/keybind"
|
import { useKeybind } from "@tui/context/keybind"
|
||||||
import { Logo } from "../component/logo"
|
|
||||||
import { Tips } from "../component/tips"
|
|
||||||
import { Locale } from "@/util/locale"
|
import { Locale } from "@/util/locale"
|
||||||
import { useSync } from "../context/sync"
|
import { useSync } from "../context/sync"
|
||||||
import { Toast } from "../ui/toast"
|
import { Toast } from "../ui/toast"
|
||||||
import { useArgs } from "../context/args"
|
import { useArgs } from "../context/args"
|
||||||
import { useDirectory } from "../context/directory"
|
import { useDirectory } from "../context/directory"
|
||||||
import { useRouteData } from "@tui/context/route"
|
import { useRoute, useRouteData } from "@tui/context/route"
|
||||||
import { usePromptRef } from "../context/prompt"
|
import { usePromptRef } from "../context/prompt"
|
||||||
import { Installation } from "@/installation"
|
import { Installation } from "@/installation"
|
||||||
import { useKV } from "../context/kv"
|
import { useKV } from "../context/kv"
|
||||||
import { useCommandDialog } from "../component/dialog-command"
|
import { useCommandDialog } from "../component/dialog-command"
|
||||||
|
import { useLocal } from "../context/local"
|
||||||
|
import { RoundedBorder } from "../component/border"
|
||||||
|
import { useTerminalDimensions } from "@opentui/solid"
|
||||||
|
|
||||||
// TODO: what is the best way to do this?
|
// TODO: what is the best way to do this?
|
||||||
let once = false
|
let once = false
|
||||||
|
|
||||||
|
const STARTER_TIPS = [
|
||||||
|
"Type @ followed by a filename to attach files",
|
||||||
|
"Start a message with ! to run shell commands",
|
||||||
|
"Press Tab to cycle between Build and Plan agents",
|
||||||
|
"Run /init to create an AGENTS.md with instructions",
|
||||||
|
]
|
||||||
|
|
||||||
export function Home() {
|
export function Home() {
|
||||||
const sync = useSync()
|
const sync = useSync()
|
||||||
const kv = useKV()
|
const kv = useKV()
|
||||||
|
|
@ -25,6 +33,9 @@ export function Home() {
|
||||||
const route = useRouteData("home")
|
const route = useRouteData("home")
|
||||||
const promptRef = usePromptRef()
|
const promptRef = usePromptRef()
|
||||||
const command = useCommandDialog()
|
const command = useCommandDialog()
|
||||||
|
const local = useLocal()
|
||||||
|
const { navigate } = useRoute()
|
||||||
|
const dimensions = useTerminalDimensions()
|
||||||
const mcp = createMemo(() => Object.keys(sync.data.mcp).length > 0)
|
const mcp = createMemo(() => Object.keys(sync.data.mcp).length > 0)
|
||||||
const mcpError = createMemo(() => {
|
const mcpError = createMemo(() => {
|
||||||
return Object.values(sync.data.mcp).some((x) => x.status === "failed")
|
return Object.values(sync.data.mcp).some((x) => x.status === "failed")
|
||||||
|
|
@ -37,7 +48,6 @@ export function Home() {
|
||||||
const isFirstTimeUser = createMemo(() => sync.data.session.length === 0)
|
const isFirstTimeUser = createMemo(() => sync.data.session.length === 0)
|
||||||
const tipsHidden = createMemo(() => kv.get("tips_hidden", false))
|
const tipsHidden = createMemo(() => kv.get("tips_hidden", false))
|
||||||
const showTips = createMemo(() => {
|
const showTips = createMemo(() => {
|
||||||
// Don't show tips for first-time users
|
|
||||||
if (isFirstTimeUser()) return false
|
if (isFirstTimeUser()) return false
|
||||||
return !tipsHidden()
|
return !tipsHidden()
|
||||||
})
|
})
|
||||||
|
|
@ -55,24 +65,14 @@ export function Home() {
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
|
|
||||||
const Hint = (
|
const recentSessions = createMemo(() => {
|
||||||
<Show when={connectedMcpCount() > 0}>
|
return sync.data.session
|
||||||
<box flexShrink={0} flexDirection="row" gap={1}>
|
.filter((x) => !x.parentID)
|
||||||
<text fg={theme.text}>
|
.toSorted((a, b) => b.time.updated - a.time.updated)
|
||||||
<Switch>
|
.slice(0, 5)
|
||||||
<Match when={mcpError()}>
|
})
|
||||||
<span style={{ fg: theme.error }}>•</span> mcp errors{" "}
|
|
||||||
<span style={{ fg: theme.textMuted }}>ctrl+x s</span>
|
const wide = createMemo(() => dimensions().width > 80)
|
||||||
</Match>
|
|
||||||
<Match when={true}>
|
|
||||||
<span style={{ fg: theme.success }}>•</span>{" "}
|
|
||||||
{Locale.pluralize(connectedMcpCount(), "{} mcp server", "{} mcp servers")}
|
|
||||||
</Match>
|
|
||||||
</Switch>
|
|
||||||
</text>
|
|
||||||
</box>
|
|
||||||
</Show>
|
|
||||||
)
|
|
||||||
|
|
||||||
let prompt: PromptRef
|
let prompt: PromptRef
|
||||||
const args = useArgs()
|
const args = useArgs()
|
||||||
|
|
@ -89,52 +89,95 @@ export function Home() {
|
||||||
})
|
})
|
||||||
const directory = useDirectory()
|
const directory = useDirectory()
|
||||||
|
|
||||||
const keybind = useKeybind()
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<box flexGrow={1} justifyContent="center" alignItems="center" paddingLeft={2} paddingRight={2} gap={1}>
|
<box flexGrow={1} justifyContent="center" alignItems="center" paddingLeft={2} paddingRight={2} gap={1}>
|
||||||
<box height={3} />
|
<box height={2} />
|
||||||
<Logo />
|
{/* Bordered welcome box */}
|
||||||
<box width="100%" maxWidth={75} zIndex={1000} paddingTop={1}>
|
<box
|
||||||
|
width="100%"
|
||||||
|
maxWidth={90}
|
||||||
|
border={["top", "bottom", "left", "right"]}
|
||||||
|
customBorderChars={RoundedBorder}
|
||||||
|
borderColor={theme.border}
|
||||||
|
title={` OpenCode v${Installation.VERSION} `}
|
||||||
|
titleAlignment="left"
|
||||||
|
>
|
||||||
|
<box
|
||||||
|
flexDirection={wide() ? "row" : "column"}
|
||||||
|
paddingTop={1}
|
||||||
|
paddingBottom={1}
|
||||||
|
paddingLeft={2}
|
||||||
|
paddingRight={2}
|
||||||
|
gap={wide() ? 4 : 2}
|
||||||
|
>
|
||||||
|
{/* Left column */}
|
||||||
|
<box flexGrow={1}>
|
||||||
|
<text fg={theme.text}>
|
||||||
|
<b>Welcome back!</b>
|
||||||
|
</text>
|
||||||
|
<box paddingTop={1}>
|
||||||
|
<text fg={theme.textMuted}>
|
||||||
|
{local.model.parsed().model} · {local.model.parsed().provider}
|
||||||
|
</text>
|
||||||
|
<Show when={mcp()}>
|
||||||
|
<text fg={theme.text}>
|
||||||
|
<Switch>
|
||||||
|
<Match when={mcpError()}>
|
||||||
|
<span style={{ fg: theme.error }}>●</span> {connectedMcpCount()} MCP · errors
|
||||||
|
</Match>
|
||||||
|
<Match when={true}>
|
||||||
|
<span style={{ fg: theme.success }}>●</span>{" "}
|
||||||
|
{Locale.pluralize(connectedMcpCount(), "{} MCP server", "{} MCP servers")}
|
||||||
|
</Match>
|
||||||
|
</Switch>
|
||||||
|
</text>
|
||||||
|
</Show>
|
||||||
|
<text fg={theme.textMuted}>{directory()}</text>
|
||||||
|
</box>
|
||||||
|
</box>
|
||||||
|
{/* Right column */}
|
||||||
|
<box flexGrow={1}>
|
||||||
|
<Show when={showTips()}>
|
||||||
|
<text fg={theme.primary}>
|
||||||
|
<b>Tips for getting started</b>
|
||||||
|
</text>
|
||||||
|
<For each={STARTER_TIPS}>{(tip) => <text fg={theme.textMuted}>{tip}</text>}</For>
|
||||||
|
</Show>
|
||||||
|
<box paddingTop={showTips() ? 1 : 0}>
|
||||||
|
<text fg={theme.primary}>
|
||||||
|
<b>Recent activity</b>
|
||||||
|
</text>
|
||||||
|
<Show when={recentSessions().length === 0}>
|
||||||
|
<text fg={theme.textMuted}>No recent activity</text>
|
||||||
|
</Show>
|
||||||
|
<For each={recentSessions()}>
|
||||||
|
{(session) => (
|
||||||
|
<box
|
||||||
|
flexDirection="row"
|
||||||
|
gap={1}
|
||||||
|
onMouseUp={() => navigate({ type: "session", sessionID: session.id })}
|
||||||
|
>
|
||||||
|
<text fg={theme.textMuted} wrapMode="none">
|
||||||
|
{Locale.truncate(session.title, wide() ? 35 : 50)}
|
||||||
|
</text>
|
||||||
|
</box>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</box>
|
||||||
|
</box>
|
||||||
|
</box>
|
||||||
|
</box>
|
||||||
|
<box width="100%" maxWidth={90} zIndex={1000} paddingTop={1}>
|
||||||
<Prompt
|
<Prompt
|
||||||
ref={(r) => {
|
ref={(r) => {
|
||||||
prompt = r
|
prompt = r
|
||||||
promptRef.set(r)
|
promptRef.set(r)
|
||||||
}}
|
}}
|
||||||
hint={Hint}
|
|
||||||
/>
|
/>
|
||||||
</box>
|
</box>
|
||||||
<box height={3} width="100%" maxWidth={75} alignItems="center" paddingTop={2}>
|
|
||||||
<Show when={showTips()}>
|
|
||||||
<Tips />
|
|
||||||
</Show>
|
|
||||||
</box>
|
|
||||||
<Toast />
|
<Toast />
|
||||||
</box>
|
</box>
|
||||||
<box paddingTop={1} paddingBottom={1} paddingLeft={2} paddingRight={2} flexDirection="row" flexShrink={0} gap={2}>
|
|
||||||
<text fg={theme.textMuted}>{directory()}</text>
|
|
||||||
<box gap={1} flexDirection="row" flexShrink={0}>
|
|
||||||
<Show when={mcp()}>
|
|
||||||
<text fg={theme.text}>
|
|
||||||
<Switch>
|
|
||||||
<Match when={mcpError()}>
|
|
||||||
<span style={{ fg: theme.error }}>⊙ </span>
|
|
||||||
</Match>
|
|
||||||
<Match when={true}>
|
|
||||||
<span style={{ fg: connectedMcpCount() > 0 ? theme.success : theme.textMuted }}>⊙ </span>
|
|
||||||
</Match>
|
|
||||||
</Switch>
|
|
||||||
{connectedMcpCount()} MCP
|
|
||||||
</text>
|
|
||||||
<text fg={theme.textMuted}>/status</text>
|
|
||||||
</Show>
|
|
||||||
</box>
|
|
||||||
<box flexGrow={1} />
|
|
||||||
<box flexShrink={0}>
|
|
||||||
<text fg={theme.textMuted}>{Installation.VERSION}</text>
|
|
||||||
</box>
|
|
||||||
</box>
|
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,24 +1,15 @@
|
||||||
import { type Accessor, createMemo, createSignal, Match, Show, Switch } from "solid-js"
|
import { type Accessor, createMemo, createSignal, Show } from "solid-js"
|
||||||
import { useRouteData } from "@tui/context/route"
|
import { useRouteData } from "@tui/context/route"
|
||||||
import { useSync } from "@tui/context/sync"
|
import { useSync } from "@tui/context/sync"
|
||||||
import { pipe, sumBy } from "remeda"
|
import { pipe, sumBy } from "remeda"
|
||||||
import { useTheme } from "@tui/context/theme"
|
import { useTheme } from "@tui/context/theme"
|
||||||
import { SplitBorder } from "@tui/component/border"
|
|
||||||
import type { AssistantMessage, Session } from "@opencode-ai/sdk/v2"
|
import type { AssistantMessage } from "@opencode-ai/sdk/v2"
|
||||||
import { useCommandDialog } from "@tui/component/dialog-command"
|
import { useCommandDialog } from "@tui/component/dialog-command"
|
||||||
import { useKeybind } from "../../context/keybind"
|
import { useKeybind } from "../../context/keybind"
|
||||||
import { Installation } from "@/installation"
|
import { Installation } from "@/installation"
|
||||||
import { useTerminalDimensions } from "@opentui/solid"
|
import { useTerminalDimensions } from "@opentui/solid"
|
||||||
|
|
||||||
const Title = (props: { session: Accessor<Session> }) => {
|
|
||||||
const { theme } = useTheme()
|
|
||||||
return (
|
|
||||||
<text fg={theme.text}>
|
|
||||||
<span style={{ bold: true }}>#</span> <span style={{ bold: true }}>{props.session().title}</span>
|
|
||||||
</text>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const ContextInfo = (props: { context: Accessor<string | undefined>; cost: Accessor<string> }) => {
|
const ContextInfo = (props: { context: Accessor<string | undefined>; cost: Accessor<string> }) => {
|
||||||
const { theme } = useTheme()
|
const { theme } = useTheme()
|
||||||
return (
|
return (
|
||||||
|
|
@ -67,76 +58,51 @@ export function Header() {
|
||||||
const dimensions = useTerminalDimensions()
|
const dimensions = useTerminalDimensions()
|
||||||
const narrow = createMemo(() => dimensions().width < 80)
|
const narrow = createMemo(() => dimensions().width < 80)
|
||||||
|
|
||||||
|
// For non-subagent sessions, render nothing (Claude Code style)
|
||||||
return (
|
return (
|
||||||
<box flexShrink={0}>
|
<Show when={session()?.parentID}>
|
||||||
<box
|
<box flexShrink={0} paddingTop={1} paddingBottom={1} paddingLeft={1}>
|
||||||
paddingTop={1}
|
<box flexDirection="column" gap={1}>
|
||||||
paddingBottom={1}
|
<box flexDirection={narrow() ? "column" : "row"} justifyContent="space-between" gap={narrow() ? 1 : 0}>
|
||||||
paddingLeft={2}
|
<text fg={theme.text}>
|
||||||
paddingRight={1}
|
<b>Subagent session</b>
|
||||||
{...SplitBorder}
|
</text>
|
||||||
border={["left"]}
|
<box flexDirection="row" gap={1} flexShrink={0}>
|
||||||
borderColor={theme.border}
|
<ContextInfo context={context} cost={cost} />
|
||||||
flexShrink={0}
|
<text fg={theme.textMuted}>v{Installation.VERSION}</text>
|
||||||
backgroundColor={theme.backgroundPanel}
|
|
||||||
>
|
|
||||||
<Switch>
|
|
||||||
<Match when={session()?.parentID}>
|
|
||||||
<box flexDirection="column" gap={1}>
|
|
||||||
<box flexDirection={narrow() ? "column" : "row"} justifyContent="space-between" gap={narrow() ? 1 : 0}>
|
|
||||||
<text fg={theme.text}>
|
|
||||||
<b>Subagent session</b>
|
|
||||||
</text>
|
|
||||||
<box flexDirection="row" gap={1} flexShrink={0}>
|
|
||||||
<ContextInfo context={context} cost={cost} />
|
|
||||||
<text fg={theme.textMuted}>v{Installation.VERSION}</text>
|
|
||||||
</box>
|
|
||||||
</box>
|
|
||||||
<box flexDirection="row" gap={2}>
|
|
||||||
<box
|
|
||||||
onMouseOver={() => setHover("parent")}
|
|
||||||
onMouseOut={() => setHover(null)}
|
|
||||||
onMouseUp={() => command.trigger("session.parent")}
|
|
||||||
backgroundColor={hover() === "parent" ? theme.backgroundElement : theme.backgroundPanel}
|
|
||||||
>
|
|
||||||
<text fg={theme.text}>
|
|
||||||
Parent <span style={{ fg: theme.textMuted }}>{keybind.print("session_parent")}</span>
|
|
||||||
</text>
|
|
||||||
</box>
|
|
||||||
<box
|
|
||||||
onMouseOver={() => setHover("prev")}
|
|
||||||
onMouseOut={() => setHover(null)}
|
|
||||||
onMouseUp={() => command.trigger("session.child.previous")}
|
|
||||||
backgroundColor={hover() === "prev" ? theme.backgroundElement : theme.backgroundPanel}
|
|
||||||
>
|
|
||||||
<text fg={theme.text}>
|
|
||||||
Prev <span style={{ fg: theme.textMuted }}>{keybind.print("session_child_cycle_reverse")}</span>
|
|
||||||
</text>
|
|
||||||
</box>
|
|
||||||
<box
|
|
||||||
onMouseOver={() => setHover("next")}
|
|
||||||
onMouseOut={() => setHover(null)}
|
|
||||||
onMouseUp={() => command.trigger("session.child.next")}
|
|
||||||
backgroundColor={hover() === "next" ? theme.backgroundElement : theme.backgroundPanel}
|
|
||||||
>
|
|
||||||
<text fg={theme.text}>
|
|
||||||
Next <span style={{ fg: theme.textMuted }}>{keybind.print("session_child_cycle")}</span>
|
|
||||||
</text>
|
|
||||||
</box>
|
|
||||||
</box>
|
|
||||||
</box>
|
</box>
|
||||||
</Match>
|
</box>
|
||||||
<Match when={true}>
|
<box flexDirection="row" gap={2}>
|
||||||
<box flexDirection={narrow() ? "column" : "row"} justifyContent="space-between" gap={1}>
|
<box
|
||||||
<Title session={session} />
|
onMouseOver={() => setHover("parent")}
|
||||||
<box flexDirection="row" gap={1} flexShrink={0}>
|
onMouseOut={() => setHover(null)}
|
||||||
<ContextInfo context={context} cost={cost} />
|
onMouseUp={() => command.trigger("session.parent")}
|
||||||
<text fg={theme.textMuted}>v{Installation.VERSION}</text>
|
>
|
||||||
</box>
|
<text fg={hover() === "parent" ? theme.text : theme.textMuted}>
|
||||||
|
Parent <span style={{ fg: theme.textMuted }}>{keybind.print("session_parent")}</span>
|
||||||
|
</text>
|
||||||
</box>
|
</box>
|
||||||
</Match>
|
<box
|
||||||
</Switch>
|
onMouseOver={() => setHover("prev")}
|
||||||
|
onMouseOut={() => setHover(null)}
|
||||||
|
onMouseUp={() => command.trigger("session.child.previous")}
|
||||||
|
>
|
||||||
|
<text fg={hover() === "prev" ? theme.text : theme.textMuted}>
|
||||||
|
Prev <span style={{ fg: theme.textMuted }}>{keybind.print("session_child_cycle_reverse")}</span>
|
||||||
|
</text>
|
||||||
|
</box>
|
||||||
|
<box
|
||||||
|
onMouseOver={() => setHover("next")}
|
||||||
|
onMouseOut={() => setHover(null)}
|
||||||
|
onMouseUp={() => command.trigger("session.child.next")}
|
||||||
|
>
|
||||||
|
<text fg={hover() === "next" ? theme.text : theme.textMuted}>
|
||||||
|
Next <span style={{ fg: theme.textMuted }}>{keybind.print("session_child_cycle")}</span>
|
||||||
|
</text>
|
||||||
|
</box>
|
||||||
|
</box>
|
||||||
|
</box>
|
||||||
</box>
|
</box>
|
||||||
</box>
|
</Show>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ import { Dynamic } from "solid-js/web"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import { useRoute, useRouteData } from "@tui/context/route"
|
import { useRoute, useRouteData } from "@tui/context/route"
|
||||||
import { useSync } from "@tui/context/sync"
|
import { useSync } from "@tui/context/sync"
|
||||||
import { SplitBorder } from "@tui/component/border"
|
|
||||||
import { Spinner } from "@tui/component/spinner"
|
import { Spinner } from "@tui/component/spinner"
|
||||||
import { useTheme } from "@tui/context/theme"
|
import { useTheme } from "@tui/context/theme"
|
||||||
import {
|
import {
|
||||||
|
|
@ -68,7 +68,7 @@ import { Toast, useToast } from "../../ui/toast"
|
||||||
import { useKV } from "../../context/kv.tsx"
|
import { useKV } from "../../context/kv.tsx"
|
||||||
import { Editor } from "../../util/editor"
|
import { Editor } from "../../util/editor"
|
||||||
import stripAnsi from "strip-ansi"
|
import stripAnsi from "strip-ansi"
|
||||||
import { Footer } from "./footer.tsx"
|
|
||||||
import { usePromptRef } from "../../context/prompt"
|
import { usePromptRef } from "../../context/prompt"
|
||||||
import { useExit } from "../../context/exit"
|
import { useExit } from "../../context/exit"
|
||||||
import { Filesystem } from "@/util/filesystem"
|
import { Filesystem } from "@/util/filesystem"
|
||||||
|
|
@ -156,7 +156,6 @@ export function Session() {
|
||||||
const sidebarVisible = createMemo(() => {
|
const sidebarVisible = createMemo(() => {
|
||||||
if (session()?.parentID) return false
|
if (session()?.parentID) return false
|
||||||
if (sidebarOpen()) return true
|
if (sidebarOpen()) return true
|
||||||
if (sidebar() === "auto" && wide()) return true
|
|
||||||
return false
|
return false
|
||||||
})
|
})
|
||||||
const showTimestamps = createMemo(() => timestamps() === "show")
|
const showTimestamps = createMemo(() => timestamps() === "show")
|
||||||
|
|
@ -956,11 +955,9 @@ export function Session() {
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<box flexDirection="row">
|
<box flexDirection="row">
|
||||||
<box flexGrow={1} paddingBottom={1} paddingTop={1} paddingLeft={2} paddingRight={2} gap={1}>
|
<box flexGrow={1} paddingBottom={0} paddingTop={1} paddingLeft={2} paddingRight={2}>
|
||||||
<Show when={session()}>
|
<Show when={session()}>
|
||||||
<Show when={!sidebarVisible() || !wide()}>
|
<Header />
|
||||||
<Header />
|
|
||||||
</Show>
|
|
||||||
<scrollbox
|
<scrollbox
|
||||||
ref={(r) => (scroll = r)}
|
ref={(r) => (scroll = r)}
|
||||||
viewportOptions={{
|
viewportOptions={{
|
||||||
|
|
@ -1006,16 +1003,9 @@ export function Session() {
|
||||||
onMouseUp={handleUnrevert}
|
onMouseUp={handleUnrevert}
|
||||||
marginTop={1}
|
marginTop={1}
|
||||||
flexShrink={0}
|
flexShrink={0}
|
||||||
border={["left"]}
|
paddingLeft={1}
|
||||||
customBorderChars={SplitBorder.customBorderChars}
|
|
||||||
borderColor={theme.backgroundPanel}
|
|
||||||
>
|
>
|
||||||
<box
|
<box paddingTop={1} paddingBottom={1} paddingLeft={2}>
|
||||||
paddingTop={1}
|
|
||||||
paddingBottom={1}
|
|
||||||
paddingLeft={2}
|
|
||||||
backgroundColor={hover() ? theme.backgroundElement : theme.backgroundPanel}
|
|
||||||
>
|
|
||||||
<text fg={theme.textMuted}>{revert()!.reverted.length} message reverted</text>
|
<text fg={theme.textMuted}>{revert()!.reverted.length} message reverted</text>
|
||||||
<text fg={theme.textMuted}>
|
<text fg={theme.textMuted}>
|
||||||
<span style={{ fg: theme.text }}>{keybind.print("messages_redo")}</span> or /redo to
|
<span style={{ fg: theme.text }}>{keybind.print("messages_redo")}</span> or /redo to
|
||||||
|
|
@ -1075,7 +1065,25 @@ export function Session() {
|
||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
</scrollbox>
|
</scrollbox>
|
||||||
<box flexShrink={0}>
|
<box flexShrink={0} paddingTop={1}>
|
||||||
|
<box
|
||||||
|
height={1}
|
||||||
|
border={["top"]}
|
||||||
|
borderColor={theme.border}
|
||||||
|
customBorderChars={{
|
||||||
|
topLeft: "",
|
||||||
|
topRight: "",
|
||||||
|
bottomLeft: "",
|
||||||
|
bottomRight: "",
|
||||||
|
vertical: "",
|
||||||
|
horizontal: "─",
|
||||||
|
bottomT: "",
|
||||||
|
topT: "",
|
||||||
|
cross: "",
|
||||||
|
leftT: "",
|
||||||
|
rightT: "",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
<Show when={permissions().length > 0}>
|
<Show when={permissions().length > 0}>
|
||||||
<PermissionPrompt request={permissions()[0]} />
|
<PermissionPrompt request={permissions()[0]} />
|
||||||
</Show>
|
</Show>
|
||||||
|
|
@ -1162,62 +1170,53 @@ function UserMessage(props: {
|
||||||
<Show when={text()}>
|
<Show when={text()}>
|
||||||
<box
|
<box
|
||||||
id={props.message.id}
|
id={props.message.id}
|
||||||
border={["left"]}
|
|
||||||
borderColor={color()}
|
|
||||||
customBorderChars={SplitBorder.customBorderChars}
|
|
||||||
marginTop={props.index === 0 ? 0 : 1}
|
marginTop={props.index === 0 ? 0 : 1}
|
||||||
|
flexShrink={0}
|
||||||
|
onMouseOver={() => setHover(true)}
|
||||||
|
onMouseOut={() => setHover(false)}
|
||||||
|
onMouseUp={props.onMouseUp}
|
||||||
>
|
>
|
||||||
<box
|
<box flexDirection="row" paddingLeft={1}>
|
||||||
onMouseOver={() => {
|
<text fg={theme.textMuted} flexShrink={0}>
|
||||||
setHover(true)
|
{queued() ? ") " : "❯ "}
|
||||||
}}
|
</text>
|
||||||
onMouseOut={() => {
|
<box flexGrow={1}>
|
||||||
setHover(false)
|
<text fg={theme.text}>{text()?.text}</text>
|
||||||
}}
|
</box>
|
||||||
onMouseUp={props.onMouseUp}
|
|
||||||
paddingTop={1}
|
|
||||||
paddingBottom={1}
|
|
||||||
paddingLeft={2}
|
|
||||||
backgroundColor={hover() ? theme.backgroundElement : theme.backgroundPanel}
|
|
||||||
flexShrink={0}
|
|
||||||
>
|
|
||||||
<text fg={theme.text}>{text()?.text}</text>
|
|
||||||
<Show when={files().length}>
|
|
||||||
<box flexDirection="row" paddingBottom={metadataVisible() ? 1 : 0} paddingTop={1} gap={1} flexWrap="wrap">
|
|
||||||
<For each={files()}>
|
|
||||||
{(file) => {
|
|
||||||
const bg = createMemo(() => {
|
|
||||||
if (file.mime.startsWith("image/")) return theme.accent
|
|
||||||
if (file.mime === "application/pdf") return theme.primary
|
|
||||||
return theme.secondary
|
|
||||||
})
|
|
||||||
return (
|
|
||||||
<text fg={theme.text}>
|
|
||||||
<span style={{ bg: bg(), fg: theme.background }}> {MIME_BADGE[file.mime] ?? file.mime} </span>
|
|
||||||
<span style={{ bg: theme.backgroundElement, fg: theme.textMuted }}> {file.filename} </span>
|
|
||||||
</text>
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
</For>
|
|
||||||
</box>
|
|
||||||
</Show>
|
|
||||||
<Show
|
|
||||||
when={queued()}
|
|
||||||
fallback={
|
|
||||||
<Show when={ctx.showTimestamps()}>
|
|
||||||
<text fg={theme.textMuted}>
|
|
||||||
<span style={{ fg: theme.textMuted }}>
|
|
||||||
{Locale.todayTimeOrDateTime(props.message.time.created)}
|
|
||||||
</span>
|
|
||||||
</text>
|
|
||||||
</Show>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<text fg={theme.textMuted}>
|
|
||||||
<span style={{ bg: theme.accent, fg: theme.backgroundPanel, bold: true }}> QUEUED </span>
|
|
||||||
</text>
|
|
||||||
</Show>
|
|
||||||
</box>
|
</box>
|
||||||
|
<Show when={files().length}>
|
||||||
|
<box flexDirection="row" paddingLeft={3} paddingTop={1} gap={1} flexWrap="wrap">
|
||||||
|
<For each={files()}>
|
||||||
|
{(file) => {
|
||||||
|
const bg = createMemo(() => {
|
||||||
|
if (file.mime.startsWith("image/")) return theme.accent
|
||||||
|
if (file.mime === "application/pdf") return theme.primary
|
||||||
|
return theme.secondary
|
||||||
|
})
|
||||||
|
return (
|
||||||
|
<text fg={theme.text}>
|
||||||
|
<span style={{ bg: bg(), fg: theme.background }}> {MIME_BADGE[file.mime] ?? file.mime} </span>
|
||||||
|
<span style={{ bg: theme.backgroundElement, fg: theme.textMuted }}> {file.filename} </span>
|
||||||
|
</text>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</For>
|
||||||
|
</box>
|
||||||
|
</Show>
|
||||||
|
<Show
|
||||||
|
when={queued()}
|
||||||
|
fallback={
|
||||||
|
<Show when={ctx.showTimestamps()}>
|
||||||
|
<text paddingLeft={3} fg={theme.textMuted}>
|
||||||
|
{Locale.todayTimeOrDateTime(props.message.time.created)}
|
||||||
|
</text>
|
||||||
|
</Show>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<text paddingLeft={3} fg={theme.textMuted}>
|
||||||
|
<span style={{ bg: theme.accent, fg: theme.background, bold: true }}> QUEUED </span>
|
||||||
|
</text>
|
||||||
|
</Show>
|
||||||
</box>
|
</box>
|
||||||
</Show>
|
</Show>
|
||||||
<Show when={compaction()}>
|
<Show when={compaction()}>
|
||||||
|
|
@ -1269,17 +1268,8 @@ function AssistantMessage(props: { message: AssistantMessage; parts: Part[]; las
|
||||||
}}
|
}}
|
||||||
</For>
|
</For>
|
||||||
<Show when={props.message.error && props.message.error.name !== "MessageAbortedError"}>
|
<Show when={props.message.error && props.message.error.name !== "MessageAbortedError"}>
|
||||||
<box
|
<box paddingTop={1} paddingBottom={1} paddingLeft={3} marginTop={1}>
|
||||||
border={["left"]}
|
<text fg={theme.error}>⎿ {props.message.error?.data.message}</text>
|
||||||
paddingTop={1}
|
|
||||||
paddingBottom={1}
|
|
||||||
paddingLeft={2}
|
|
||||||
marginTop={1}
|
|
||||||
backgroundColor={theme.backgroundPanel}
|
|
||||||
customBorderChars={SplitBorder.customBorderChars}
|
|
||||||
borderColor={theme.error}
|
|
||||||
>
|
|
||||||
<text fg={theme.textMuted}>{props.message.error?.data.message}</text>
|
|
||||||
</box>
|
</box>
|
||||||
</Show>
|
</Show>
|
||||||
<Switch>
|
<Switch>
|
||||||
|
|
@ -1294,7 +1284,7 @@ function AssistantMessage(props: { message: AssistantMessage; parts: Part[]; las
|
||||||
: local.agent.color(props.message.agent),
|
: local.agent.color(props.message.agent),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
▣{" "}
|
⏺{" "}
|
||||||
</span>{" "}
|
</span>{" "}
|
||||||
<span style={{ fg: theme.text }}>{Locale.titlecase(props.message.mode)}</span>
|
<span style={{ fg: theme.text }}>{Locale.titlecase(props.message.mode)}</span>
|
||||||
<span style={{ fg: theme.textMuted }}> · {props.message.modelID}</span>
|
<span style={{ fg: theme.textMuted }}> · {props.message.modelID}</span>
|
||||||
|
|
@ -1328,15 +1318,7 @@ function ReasoningPart(props: { last: boolean; part: ReasoningPart; message: Ass
|
||||||
})
|
})
|
||||||
return (
|
return (
|
||||||
<Show when={content() && ctx.showThinking()}>
|
<Show when={content() && ctx.showThinking()}>
|
||||||
<box
|
<box id={"text-" + props.part.id} paddingLeft={3} marginTop={1} flexDirection="column">
|
||||||
id={"text-" + props.part.id}
|
|
||||||
paddingLeft={2}
|
|
||||||
marginTop={1}
|
|
||||||
flexDirection="column"
|
|
||||||
border={["left"]}
|
|
||||||
customBorderChars={SplitBorder.customBorderChars}
|
|
||||||
borderColor={theme.backgroundElement}
|
|
||||||
>
|
|
||||||
<code
|
<code
|
||||||
filetype="markdown"
|
filetype="markdown"
|
||||||
drawUnstyledText={false}
|
drawUnstyledText={false}
|
||||||
|
|
@ -1486,7 +1468,7 @@ type ToolProps<T extends Tool.Info> = {
|
||||||
function GenericTool(props: ToolProps<any>) {
|
function GenericTool(props: ToolProps<any>) {
|
||||||
return (
|
return (
|
||||||
<InlineTool icon="⚙" pending="Writing command..." complete={true} part={props.part}>
|
<InlineTool icon="⚙" pending="Writing command..." complete={true} part={props.part}>
|
||||||
{props.tool} {input(props.input)}
|
{props.tool}({input(props.input)})
|
||||||
</InlineTool>
|
</InlineTool>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -1509,6 +1491,7 @@ function InlineTool(props: {
|
||||||
pending: string
|
pending: string
|
||||||
children: JSX.Element
|
children: JSX.Element
|
||||||
part: ToolPart
|
part: ToolPart
|
||||||
|
result?: JSX.Element
|
||||||
}) {
|
}) {
|
||||||
const [margin, setMargin] = createSignal(0)
|
const [margin, setMargin] = createSignal(0)
|
||||||
const { theme } = useTheme()
|
const { theme } = useTheme()
|
||||||
|
|
@ -1521,6 +1504,13 @@ function InlineTool(props: {
|
||||||
return callID === props.part.callID
|
return callID === props.part.callID
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const iconColor = createMemo(() => {
|
||||||
|
if (permission()) return theme.warning
|
||||||
|
if (props.part.state.status === "completed") return theme.success
|
||||||
|
if (props.part.state.status === "error") return theme.error
|
||||||
|
return theme.textMuted
|
||||||
|
})
|
||||||
|
|
||||||
const fg = createMemo(() => {
|
const fg = createMemo(() => {
|
||||||
if (permission()) return theme.warning
|
if (permission()) return theme.warning
|
||||||
if (props.complete) return theme.textMuted
|
if (props.complete) return theme.textMuted
|
||||||
|
|
@ -1563,13 +1553,20 @@ function InlineTool(props: {
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<text paddingLeft={3} fg={fg()} attributes={denied() ? TextAttributes.STRIKETHROUGH : undefined}>
|
<text fg={fg()} attributes={denied() ? TextAttributes.STRIKETHROUGH : undefined}>
|
||||||
<Show fallback={<>~ {props.pending}</>} when={props.complete}>
|
<Show fallback={<>~ {props.pending}</>} when={props.complete}>
|
||||||
<span style={{ fg: props.iconColor }}>{props.icon}</span> {props.children}
|
<span style={{ fg: props.iconColor ?? iconColor() }}>⏺</span> {props.children}
|
||||||
</Show>
|
</Show>
|
||||||
</text>
|
</text>
|
||||||
|
<Show when={props.result}>
|
||||||
|
<text paddingLeft={2} fg={theme.textMuted}>
|
||||||
|
⎿ {props.result}
|
||||||
|
</text>
|
||||||
|
</Show>
|
||||||
<Show when={error() && !denied()}>
|
<Show when={error() && !denied()}>
|
||||||
<text fg={theme.error}>{error()}</text>
|
<text paddingLeft={2} fg={theme.error}>
|
||||||
|
⎿ {error()}
|
||||||
|
</text>
|
||||||
</Show>
|
</Show>
|
||||||
</box>
|
</box>
|
||||||
)
|
)
|
||||||
|
|
@ -1586,17 +1583,17 @@ function BlockTool(props: {
|
||||||
const renderer = useRenderer()
|
const renderer = useRenderer()
|
||||||
const [hover, setHover] = createSignal(false)
|
const [hover, setHover] = createSignal(false)
|
||||||
const error = createMemo(() => (props.part?.state.status === "error" ? props.part.state.error : undefined))
|
const error = createMemo(() => (props.part?.state.status === "error" ? props.part.state.error : undefined))
|
||||||
|
|
||||||
|
const iconColor = createMemo(() => {
|
||||||
|
if (props.part?.state.status === "completed") return theme.success
|
||||||
|
if (props.part?.state.status === "error") return theme.error
|
||||||
|
return theme.textMuted
|
||||||
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<box
|
<box
|
||||||
border={["left"]}
|
paddingLeft={3}
|
||||||
paddingTop={1}
|
|
||||||
paddingBottom={1}
|
|
||||||
paddingLeft={2}
|
|
||||||
marginTop={1}
|
marginTop={1}
|
||||||
gap={1}
|
|
||||||
backgroundColor={hover() ? theme.backgroundMenu : theme.backgroundPanel}
|
|
||||||
customBorderChars={SplitBorder.customBorderChars}
|
|
||||||
borderColor={theme.background}
|
|
||||||
onMouseOver={() => props.onClick && setHover(true)}
|
onMouseOver={() => props.onClick && setHover(true)}
|
||||||
onMouseOut={() => setHover(false)}
|
onMouseOut={() => setHover(false)}
|
||||||
onMouseUp={() => {
|
onMouseUp={() => {
|
||||||
|
|
@ -1607,16 +1604,20 @@ function BlockTool(props: {
|
||||||
<Show
|
<Show
|
||||||
when={props.spinner}
|
when={props.spinner}
|
||||||
fallback={
|
fallback={
|
||||||
<text paddingLeft={3} fg={theme.textMuted}>
|
<text fg={theme.textMuted}>
|
||||||
{props.title}
|
<span style={{ fg: iconColor() }}>⏺</span> {props.title.replace(/^# /, "").replace(/^← /, "")}
|
||||||
</text>
|
</text>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Spinner color={theme.textMuted}>{props.title.replace(/^# /, "")}</Spinner>
|
<box flexDirection="row" gap={1}>
|
||||||
|
<Spinner color={theme.textMuted}>{props.title.replace(/^# /, "").replace(/^← /, "")}</Spinner>
|
||||||
|
</box>
|
||||||
</Show>
|
</Show>
|
||||||
{props.children}
|
<box paddingLeft={2}>{props.children}</box>
|
||||||
<Show when={error()}>
|
<Show when={error()}>
|
||||||
<text fg={theme.error}>{error()}</text>
|
<text paddingLeft={2} fg={theme.error}>
|
||||||
|
⎿ {error()}
|
||||||
|
</text>
|
||||||
</Show>
|
</Show>
|
||||||
</box>
|
</box>
|
||||||
)
|
)
|
||||||
|
|
@ -1669,20 +1670,22 @@ function Bash(props: ToolProps<typeof BashTool>) {
|
||||||
spinner={isRunning()}
|
spinner={isRunning()}
|
||||||
onClick={overflow() ? () => setExpanded((prev) => !prev) : undefined}
|
onClick={overflow() ? () => setExpanded((prev) => !prev) : undefined}
|
||||||
>
|
>
|
||||||
<box gap={1}>
|
<text fg={theme.textMuted}>⎿ $ {props.input.command}</text>
|
||||||
<text fg={theme.text}>$ {props.input.command}</text>
|
<Show when={output()}>
|
||||||
<Show when={output()}>
|
<text paddingLeft={2} fg={theme.text}>
|
||||||
<text fg={theme.text}>{limited()}</text>
|
{limited()}
|
||||||
</Show>
|
</text>
|
||||||
<Show when={overflow()}>
|
</Show>
|
||||||
<text fg={theme.textMuted}>{expanded() ? "Click to collapse" : "Click to expand"}</text>
|
<Show when={overflow()}>
|
||||||
</Show>
|
<text paddingLeft={2} fg={theme.textMuted}>
|
||||||
</box>
|
{expanded() ? "Click to collapse" : "Click to expand"}
|
||||||
|
</text>
|
||||||
|
</Show>
|
||||||
</BlockTool>
|
</BlockTool>
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={true}>
|
<Match when={true}>
|
||||||
<InlineTool icon="$" pending="Writing command..." complete={props.input.command} part={props.part}>
|
<InlineTool icon="$" pending="Writing command..." complete={props.input.command} part={props.part}>
|
||||||
{props.input.command}
|
Bash({props.input.command})
|
||||||
</InlineTool>
|
</InlineTool>
|
||||||
</Match>
|
</Match>
|
||||||
</Switch>
|
</Switch>
|
||||||
|
|
@ -1701,23 +1704,19 @@ function Write(props: ToolProps<typeof WriteTool>) {
|
||||||
return props.metadata.diagnostics?.[filePath] ?? []
|
return props.metadata.diagnostics?.[filePath] ?? []
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const lineCount = createMemo(() => code().split("\n").length)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Switch>
|
<Switch>
|
||||||
<Match when={props.metadata.diagnostics !== undefined}>
|
<Match when={props.metadata.diagnostics !== undefined}>
|
||||||
<BlockTool title={"# Wrote " + normalizePath(props.input.filePath!)} part={props.part}>
|
<BlockTool title={"Wrote " + normalizePath(props.input.filePath!)} part={props.part}>
|
||||||
<line_number fg={theme.textMuted} minWidth={3} paddingRight={1}>
|
<text fg={theme.textMuted}>
|
||||||
<code
|
⎿ Wrote {lineCount()} lines to {normalizePath(props.input.filePath!)}
|
||||||
conceal={false}
|
</text>
|
||||||
fg={theme.text}
|
|
||||||
filetype={filetype(props.input.filePath!)}
|
|
||||||
syntaxStyle={syntax()}
|
|
||||||
content={code()}
|
|
||||||
/>
|
|
||||||
</line_number>
|
|
||||||
<Show when={diagnostics().length}>
|
<Show when={diagnostics().length}>
|
||||||
<For each={diagnostics()}>
|
<For each={diagnostics()}>
|
||||||
{(diagnostic) => (
|
{(diagnostic) => (
|
||||||
<text fg={theme.error}>
|
<text paddingLeft={2} fg={theme.error}>
|
||||||
Error [{diagnostic.range.start.line}:{diagnostic.range.start.character}]: {diagnostic.message}
|
Error [{diagnostic.range.start.line}:{diagnostic.range.start.character}]: {diagnostic.message}
|
||||||
</text>
|
</text>
|
||||||
)}
|
)}
|
||||||
|
|
@ -1727,7 +1726,7 @@ function Write(props: ToolProps<typeof WriteTool>) {
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={true}>
|
<Match when={true}>
|
||||||
<InlineTool icon="←" pending="Preparing write..." complete={props.input.filePath} part={props.part}>
|
<InlineTool icon="←" pending="Preparing write..." complete={props.input.filePath} part={props.part}>
|
||||||
Write {normalizePath(props.input.filePath!)}
|
Write({normalizePath(props.input.filePath!)})
|
||||||
</InlineTool>
|
</InlineTool>
|
||||||
</Match>
|
</Match>
|
||||||
</Switch>
|
</Switch>
|
||||||
|
|
@ -1735,12 +1734,13 @@ function Write(props: ToolProps<typeof WriteTool>) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function Glob(props: ToolProps<typeof GlobTool>) {
|
function Glob(props: ToolProps<typeof GlobTool>) {
|
||||||
|
const result = createMemo(() => {
|
||||||
|
if (!props.metadata.count) return undefined
|
||||||
|
return `${props.metadata.count} ${props.metadata.count === 1 ? "match" : "matches"}`
|
||||||
|
})
|
||||||
return (
|
return (
|
||||||
<InlineTool icon="✱" pending="Finding files..." complete={props.input.pattern} part={props.part}>
|
<InlineTool icon="✱" pending="Finding files..." complete={props.input.pattern} part={props.part} result={result()}>
|
||||||
Glob "{props.input.pattern}" <Show when={props.input.path}>in {normalizePath(props.input.path)} </Show>
|
Glob("{props.input.pattern}"<Show when={props.input.path}>, {normalizePath(props.input.path)}</Show>)
|
||||||
<Show when={props.metadata.count}>
|
|
||||||
({props.metadata.count} {props.metadata.count === 1 ? "match" : "matches"})
|
|
||||||
</Show>
|
|
||||||
</InlineTool>
|
</InlineTool>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -1754,31 +1754,40 @@ function Read(props: ToolProps<typeof ReadTool>) {
|
||||||
if (!value || !Array.isArray(value)) return []
|
if (!value || !Array.isArray(value)) return []
|
||||||
return value.filter((p): p is string => typeof p === "string")
|
return value.filter((p): p is string => typeof p === "string")
|
||||||
})
|
})
|
||||||
|
const result = createMemo(() => {
|
||||||
|
const l = loaded()
|
||||||
|
if (l.length === 0) return undefined
|
||||||
|
return `Loaded ${l.length + 1} file${l.length > 0 ? "s" : ""}`
|
||||||
|
})
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<InlineTool icon="→" pending="Reading file..." complete={props.input.filePath} part={props.part}>
|
<InlineTool
|
||||||
Read {normalizePath(props.input.filePath!)} {input(props.input, ["filePath"])}
|
icon="→"
|
||||||
|
pending="Reading file..."
|
||||||
|
complete={props.input.filePath}
|
||||||
|
part={props.part}
|
||||||
|
result={result()}
|
||||||
|
>
|
||||||
|
Read({normalizePath(props.input.filePath!)})
|
||||||
</InlineTool>
|
</InlineTool>
|
||||||
<For each={loaded()}>
|
|
||||||
{(filepath) => (
|
|
||||||
<box paddingLeft={3}>
|
|
||||||
<text paddingLeft={3} fg={theme.textMuted}>
|
|
||||||
↳ Loaded {normalizePath(filepath)}
|
|
||||||
</text>
|
|
||||||
</box>
|
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function Grep(props: ToolProps<typeof GrepTool>) {
|
function Grep(props: ToolProps<typeof GrepTool>) {
|
||||||
|
const result = createMemo(() => {
|
||||||
|
if (!props.metadata.matches) return undefined
|
||||||
|
return `${props.metadata.matches} ${props.metadata.matches === 1 ? "match" : "matches"}`
|
||||||
|
})
|
||||||
return (
|
return (
|
||||||
<InlineTool icon="✱" pending="Searching content..." complete={props.input.pattern} part={props.part}>
|
<InlineTool
|
||||||
Grep "{props.input.pattern}" <Show when={props.input.path}>in {normalizePath(props.input.path)} </Show>
|
icon="✱"
|
||||||
<Show when={props.metadata.matches}>
|
pending="Searching content..."
|
||||||
({props.metadata.matches} {props.metadata.matches === 1 ? "match" : "matches"})
|
complete={props.input.pattern}
|
||||||
</Show>
|
part={props.part}
|
||||||
|
result={result()}
|
||||||
|
>
|
||||||
|
Grep("{props.input.pattern}"<Show when={props.input.path}>, {normalizePath(props.input.path)}</Show>)
|
||||||
</InlineTool>
|
</InlineTool>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -1792,7 +1801,7 @@ function List(props: ToolProps<typeof ListTool>) {
|
||||||
})
|
})
|
||||||
return (
|
return (
|
||||||
<InlineTool icon="→" pending="Listing directory..." complete={props.input.path !== undefined} part={props.part}>
|
<InlineTool icon="→" pending="Listing directory..." complete={props.input.path !== undefined} part={props.part}>
|
||||||
List {dir()}
|
List({dir()})
|
||||||
</InlineTool>
|
</InlineTool>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -1800,7 +1809,7 @@ function List(props: ToolProps<typeof ListTool>) {
|
||||||
function WebFetch(props: ToolProps<typeof WebFetchTool>) {
|
function WebFetch(props: ToolProps<typeof WebFetchTool>) {
|
||||||
return (
|
return (
|
||||||
<InlineTool icon="%" pending="Fetching from the web..." complete={(props.input as any).url} part={props.part}>
|
<InlineTool icon="%" pending="Fetching from the web..." complete={(props.input as any).url} part={props.part}>
|
||||||
WebFetch {(props.input as any).url}
|
Fetch({(props.input as any).url})
|
||||||
</InlineTool>
|
</InlineTool>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -1808,9 +1817,10 @@ function WebFetch(props: ToolProps<typeof WebFetchTool>) {
|
||||||
function CodeSearch(props: ToolProps<any>) {
|
function CodeSearch(props: ToolProps<any>) {
|
||||||
const input = props.input as any
|
const input = props.input as any
|
||||||
const metadata = props.metadata as any
|
const metadata = props.metadata as any
|
||||||
|
const result = createMemo(() => (metadata.results ? `${metadata.results} results` : undefined))
|
||||||
return (
|
return (
|
||||||
<InlineTool icon="◇" pending="Searching code..." complete={input.query} part={props.part}>
|
<InlineTool icon="◇" pending="Searching code..." complete={input.query} part={props.part} result={result()}>
|
||||||
Exa Code Search "{input.query}" <Show when={metadata.results}>({metadata.results} results)</Show>
|
Code Search("{input.query}")
|
||||||
</InlineTool>
|
</InlineTool>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -1818,9 +1828,10 @@ function CodeSearch(props: ToolProps<any>) {
|
||||||
function WebSearch(props: ToolProps<any>) {
|
function WebSearch(props: ToolProps<any>) {
|
||||||
const input = props.input as any
|
const input = props.input as any
|
||||||
const metadata = props.metadata as any
|
const metadata = props.metadata as any
|
||||||
|
const result = createMemo(() => (metadata.numResults ? `${metadata.numResults} results` : undefined))
|
||||||
return (
|
return (
|
||||||
<InlineTool icon="◈" pending="Searching web..." complete={input.query} part={props.part}>
|
<InlineTool icon="◈" pending="Searching web..." complete={input.query} part={props.part} result={result()}>
|
||||||
Exa Web Search "{input.query}" <Show when={metadata.numResults}>({metadata.numResults} results)</Show>
|
Web Search("{input.query}")
|
||||||
</InlineTool>
|
</InlineTool>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -1850,7 +1861,7 @@ function Task(props: ToolProps<typeof TaskTool>) {
|
||||||
<Switch>
|
<Switch>
|
||||||
<Match when={props.input.description || props.input.subagent_type}>
|
<Match when={props.input.description || props.input.subagent_type}>
|
||||||
<BlockTool
|
<BlockTool
|
||||||
title={"# " + Locale.titlecase(props.input.subagent_type ?? "unknown") + " Task"}
|
title={Locale.titlecase(props.input.subagent_type ?? "unknown") + "(" + (props.input.description ?? "") + ")"}
|
||||||
onClick={
|
onClick={
|
||||||
props.metadata.sessionId
|
props.metadata.sessionId
|
||||||
? () => navigate({ type: "session", sessionID: props.metadata.sessionId! })
|
? () => navigate({ type: "session", sessionID: props.metadata.sessionId! })
|
||||||
|
|
@ -1859,32 +1870,27 @@ function Task(props: ToolProps<typeof TaskTool>) {
|
||||||
part={props.part}
|
part={props.part}
|
||||||
spinner={isRunning()}
|
spinner={isRunning()}
|
||||||
>
|
>
|
||||||
<box>
|
<text fg={theme.textMuted}>
|
||||||
<text style={{ fg: theme.textMuted }}>
|
⎿{" "}
|
||||||
{props.input.description} ({tools().length} toolcalls)
|
{isRunning()
|
||||||
</text>
|
? `${tools().length} tool use${tools().length !== 1 ? "s" : ""}`
|
||||||
<Show when={current()}>
|
: `Done (${tools().length} tool use${tools().length !== 1 ? "s" : ""})`}
|
||||||
{(item) => {
|
</text>
|
||||||
const title = item().state.status === "completed" ? (item().state as any).title : ""
|
<Show when={isRunning() ? current() : undefined}>
|
||||||
return (
|
{(item) => {
|
||||||
<text style={{ fg: item().state.status === "error" ? theme.error : theme.textMuted }}>
|
const title = item().state.status === "completed" ? (item().state as any).title : ""
|
||||||
└ {Locale.titlecase(item().tool)} {title}
|
return (
|
||||||
</text>
|
<text paddingLeft={2} style={{ fg: item().state.status === "error" ? theme.error : theme.textMuted }}>
|
||||||
)
|
└ {Locale.titlecase(item().tool)} {title}
|
||||||
}}
|
</text>
|
||||||
</Show>
|
)
|
||||||
</box>
|
}}
|
||||||
<Show when={props.metadata.sessionId}>
|
|
||||||
<text fg={theme.text}>
|
|
||||||
{keybind.print("session_child_cycle")}
|
|
||||||
<span style={{ fg: theme.textMuted }}> view subagents</span>
|
|
||||||
</text>
|
|
||||||
</Show>
|
</Show>
|
||||||
</BlockTool>
|
</BlockTool>
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={true}>
|
<Match when={true}>
|
||||||
<InlineTool icon="#" pending="Delegating..." complete={props.input.subagent_type} part={props.part}>
|
<InlineTool icon="#" pending="Delegating..." complete={props.input.subagent_type} part={props.part}>
|
||||||
{props.input.subagent_type} Task {props.input.description}
|
{Locale.titlecase(props.input.subagent_type ?? "unknown")}({props.input.description})
|
||||||
</InlineTool>
|
</InlineTool>
|
||||||
</Match>
|
</Match>
|
||||||
</Switch>
|
</Switch>
|
||||||
|
|
@ -1912,11 +1918,21 @@ function Edit(props: ToolProps<typeof EditTool>) {
|
||||||
return arr.filter((x) => x.severity === 1).slice(0, 3)
|
return arr.filter((x) => x.severity === 1).slice(0, 3)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const [expanded, setExpanded] = createSignal(false)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Switch>
|
<Switch>
|
||||||
<Match when={props.metadata.diff !== undefined}>
|
<Match when={props.metadata.diff !== undefined}>
|
||||||
<BlockTool title={"← Edit " + normalizePath(props.input.filePath!)} part={props.part}>
|
<BlockTool
|
||||||
<box paddingLeft={1}>
|
title={"Edit(" + normalizePath(props.input.filePath!) + ")"}
|
||||||
|
part={props.part}
|
||||||
|
onClick={() => setExpanded((prev) => !prev)}
|
||||||
|
>
|
||||||
|
<Show
|
||||||
|
when={expanded()}
|
||||||
|
fallback={<text fg={theme.textMuted}>⎿ {normalizePath(props.input.filePath!)} (ctrl+o to expand)</text>}
|
||||||
|
>
|
||||||
|
<text fg={theme.textMuted}>⎿ {normalizePath(props.input.filePath!)}</text>
|
||||||
<diff
|
<diff
|
||||||
diff={diffContent()}
|
diff={diffContent()}
|
||||||
view={view()}
|
view={view()}
|
||||||
|
|
@ -1936,24 +1952,21 @@ function Edit(props: ToolProps<typeof EditTool>) {
|
||||||
addedLineNumberBg={theme.diffAddedLineNumberBg}
|
addedLineNumberBg={theme.diffAddedLineNumberBg}
|
||||||
removedLineNumberBg={theme.diffRemovedLineNumberBg}
|
removedLineNumberBg={theme.diffRemovedLineNumberBg}
|
||||||
/>
|
/>
|
||||||
</box>
|
</Show>
|
||||||
<Show when={diagnostics().length}>
|
<Show when={diagnostics().length}>
|
||||||
<box>
|
<For each={diagnostics()}>
|
||||||
<For each={diagnostics()}>
|
{(diagnostic) => (
|
||||||
{(diagnostic) => (
|
<text paddingLeft={2} fg={theme.error}>
|
||||||
<text fg={theme.error}>
|
Error [{diagnostic.range.start.line + 1}:{diagnostic.range.start.character + 1}] {diagnostic.message}
|
||||||
Error [{diagnostic.range.start.line + 1}:{diagnostic.range.start.character + 1}]{" "}
|
</text>
|
||||||
{diagnostic.message}
|
)}
|
||||||
</text>
|
</For>
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
</box>
|
|
||||||
</Show>
|
</Show>
|
||||||
</BlockTool>
|
</BlockTool>
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={true}>
|
<Match when={true}>
|
||||||
<InlineTool icon="←" pending="Preparing edit..." complete={props.input.filePath} part={props.part}>
|
<InlineTool icon="←" pending="Preparing edit..." complete={props.input.filePath} part={props.part}>
|
||||||
Edit {normalizePath(props.input.filePath!)} {input({ replaceAll: props.input.replaceAll })}
|
Edit({normalizePath(props.input.filePath!)})
|
||||||
</InlineTool>
|
</InlineTool>
|
||||||
</Match>
|
</Match>
|
||||||
</Switch>
|
</Switch>
|
||||||
|
|
@ -1999,10 +2012,10 @@ function ApplyPatch(props: ToolProps<typeof ApplyPatchTool>) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function title(file: { type: string; relativePath: string; filePath: string; deletions: number }) {
|
function title(file: { type: string; relativePath: string; filePath: string; deletions: number }) {
|
||||||
if (file.type === "delete") return "# Deleted " + file.relativePath
|
if (file.type === "delete") return "Deleted " + file.relativePath
|
||||||
if (file.type === "add") return "# Created " + file.relativePath
|
if (file.type === "add") return "Created " + file.relativePath
|
||||||
if (file.type === "move") return "# Moved " + normalizePath(file.filePath) + " → " + file.relativePath
|
if (file.type === "move") return "Moved " + normalizePath(file.filePath) + " → " + file.relativePath
|
||||||
return "← Patched " + file.relativePath
|
return "Patched " + file.relativePath
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -2038,7 +2051,7 @@ function TodoWrite(props: ToolProps<typeof TodoWriteTool>) {
|
||||||
return (
|
return (
|
||||||
<Switch>
|
<Switch>
|
||||||
<Match when={props.metadata.todos?.length}>
|
<Match when={props.metadata.todos?.length}>
|
||||||
<BlockTool title="# Todos" part={props.part}>
|
<BlockTool title="Todos" part={props.part}>
|
||||||
<box>
|
<box>
|
||||||
<For each={props.input.todos ?? []}>
|
<For each={props.input.todos ?? []}>
|
||||||
{(todo) => <TodoItem status={todo.status} content={todo.content} />}
|
{(todo) => <TodoItem status={todo.status} content={todo.content} />}
|
||||||
|
|
@ -2067,7 +2080,7 @@ function Question(props: ToolProps<typeof QuestionTool>) {
|
||||||
return (
|
return (
|
||||||
<Switch>
|
<Switch>
|
||||||
<Match when={props.metadata.answers}>
|
<Match when={props.metadata.answers}>
|
||||||
<BlockTool title="# Questions" part={props.part}>
|
<BlockTool title="Questions" part={props.part}>
|
||||||
<box gap={1}>
|
<box gap={1}>
|
||||||
<For each={props.input.questions ?? []}>
|
<For each={props.input.questions ?? []}>
|
||||||
{(q, i) => (
|
{(q, i) => (
|
||||||
|
|
@ -2092,7 +2105,7 @@ function Question(props: ToolProps<typeof QuestionTool>) {
|
||||||
function Skill(props: ToolProps<typeof SkillTool>) {
|
function Skill(props: ToolProps<typeof SkillTool>) {
|
||||||
return (
|
return (
|
||||||
<InlineTool icon="→" pending="Loading skill..." complete={props.input.name} part={props.part}>
|
<InlineTool icon="→" pending="Loading skill..." complete={props.input.name} part={props.part}>
|
||||||
Skill "{props.input.name}"
|
Skill({props.input.name})
|
||||||
</InlineTool>
|
</InlineTool>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,10 +3,10 @@ import { createMemo, For, Match, Show, Switch } from "solid-js"
|
||||||
import { Portal, useKeyboard, useRenderer, useTerminalDimensions, type JSX } from "@opentui/solid"
|
import { Portal, useKeyboard, useRenderer, useTerminalDimensions, type JSX } from "@opentui/solid"
|
||||||
import type { TextareaRenderable } from "@opentui/core"
|
import type { TextareaRenderable } from "@opentui/core"
|
||||||
import { useKeybind } from "../../context/keybind"
|
import { useKeybind } from "../../context/keybind"
|
||||||
import { useTheme, selectedForeground } from "../../context/theme"
|
import { useTheme } from "../../context/theme"
|
||||||
import type { PermissionRequest } from "@opencode-ai/sdk/v2"
|
import type { PermissionRequest } from "@opencode-ai/sdk/v2"
|
||||||
import { useSDK } from "../../context/sdk"
|
import { useSDK } from "../../context/sdk"
|
||||||
import { SplitBorder } from "../../component/border"
|
|
||||||
import { useSync } from "../../context/sync"
|
import { useSync } from "../../context/sync"
|
||||||
import { useTextareaKeybindings } from "../../component/textarea-keybindings"
|
import { useTextareaKeybindings } from "../../component/textarea-keybindings"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
|
|
@ -322,18 +322,13 @@ function RejectPrompt(props: { onConfirm: (message: string) => void; onCancel: (
|
||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<box
|
<box paddingLeft={1}>
|
||||||
backgroundColor={theme.backgroundPanel}
|
|
||||||
border={["left"]}
|
|
||||||
borderColor={theme.error}
|
|
||||||
customBorderChars={SplitBorder.customBorderChars}
|
|
||||||
>
|
|
||||||
<box gap={1} paddingLeft={1} paddingRight={3} paddingTop={1} paddingBottom={1}>
|
<box gap={1} paddingLeft={1} paddingRight={3} paddingTop={1} paddingBottom={1}>
|
||||||
<box flexDirection="row" gap={1} paddingLeft={1}>
|
<box flexDirection="row" gap={1}>
|
||||||
<text fg={theme.error}>{"△"}</text>
|
<text fg={theme.error}>⏺</text>
|
||||||
<text fg={theme.text}>Reject permission</text>
|
<text fg={theme.text}>Reject permission</text>
|
||||||
</box>
|
</box>
|
||||||
<box paddingLeft={1}>
|
<box paddingLeft={2}>
|
||||||
<text fg={theme.textMuted}>Tell OpenCode what to do differently</text>
|
<text fg={theme.textMuted}>Tell OpenCode what to do differently</text>
|
||||||
</box>
|
</box>
|
||||||
</box>
|
</box>
|
||||||
|
|
@ -341,10 +336,9 @@ function RejectPrompt(props: { onConfirm: (message: string) => void; onCancel: (
|
||||||
flexDirection={narrow() ? "column" : "row"}
|
flexDirection={narrow() ? "column" : "row"}
|
||||||
flexShrink={0}
|
flexShrink={0}
|
||||||
paddingTop={1}
|
paddingTop={1}
|
||||||
paddingLeft={2}
|
paddingLeft={3}
|
||||||
paddingRight={3}
|
paddingRight={3}
|
||||||
paddingBottom={1}
|
paddingBottom={1}
|
||||||
backgroundColor={theme.backgroundElement}
|
|
||||||
justifyContent={narrow() ? "flex-start" : "space-between"}
|
justifyContent={narrow() ? "flex-start" : "space-between"}
|
||||||
alignItems={narrow() ? "flex-start" : "center"}
|
alignItems={narrow() ? "flex-start" : "center"}
|
||||||
gap={1}
|
gap={1}
|
||||||
|
|
@ -358,10 +352,10 @@ function RejectPrompt(props: { onConfirm: (message: string) => void; onCancel: (
|
||||||
keyBindings={textareaKeybindings()}
|
keyBindings={textareaKeybindings()}
|
||||||
/>
|
/>
|
||||||
<box flexDirection="row" gap={2} flexShrink={0}>
|
<box flexDirection="row" gap={2} flexShrink={0}>
|
||||||
<text fg={theme.text}>
|
<text fg={theme.textMuted}>
|
||||||
enter <span style={{ fg: theme.textMuted }}>confirm</span>
|
enter <span style={{ fg: theme.textMuted }}>confirm</span>
|
||||||
</text>
|
</text>
|
||||||
<text fg={theme.text}>
|
<text fg={theme.textMuted}>
|
||||||
esc <span style={{ fg: theme.textMuted }}>cancel</span>
|
esc <span style={{ fg: theme.textMuted }}>cancel</span>
|
||||||
</text>
|
</text>
|
||||||
</box>
|
</box>
|
||||||
|
|
@ -429,12 +423,16 @@ function Prompt<const T extends Record<string, string>>(props: {
|
||||||
|
|
||||||
const content = () => (
|
const content = () => (
|
||||||
<box
|
<box
|
||||||
backgroundColor={theme.backgroundPanel}
|
paddingLeft={1}
|
||||||
border={["left"]}
|
|
||||||
borderColor={theme.warning}
|
|
||||||
customBorderChars={SplitBorder.customBorderChars}
|
|
||||||
{...(store.expanded
|
{...(store.expanded
|
||||||
? { top: dimensions().height * -1 + 1, bottom: 1, left: 2, right: 2, position: "absolute" }
|
? {
|
||||||
|
top: dimensions().height * -1 + 1,
|
||||||
|
bottom: 1,
|
||||||
|
left: 2,
|
||||||
|
right: 2,
|
||||||
|
position: "absolute",
|
||||||
|
backgroundColor: theme.background,
|
||||||
|
}
|
||||||
: {
|
: {
|
||||||
top: 0,
|
top: 0,
|
||||||
maxHeight: 15,
|
maxHeight: 15,
|
||||||
|
|
@ -445,8 +443,8 @@ function Prompt<const T extends Record<string, string>>(props: {
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<box gap={1} paddingLeft={1} paddingRight={3} paddingTop={1} paddingBottom={1} flexGrow={1}>
|
<box gap={1} paddingLeft={1} paddingRight={3} paddingTop={1} paddingBottom={1} flexGrow={1}>
|
||||||
<box flexDirection="row" gap={1} paddingLeft={1} flexShrink={0}>
|
<box flexDirection="row" gap={1} flexShrink={0}>
|
||||||
<text fg={theme.warning}>{"△"}</text>
|
<text fg={theme.warning}>⏺</text>
|
||||||
<text fg={theme.text}>{props.title}</text>
|
<text fg={theme.text}>{props.title}</text>
|
||||||
</box>
|
</box>
|
||||||
{props.body}
|
{props.body}
|
||||||
|
|
@ -456,28 +454,26 @@ function Prompt<const T extends Record<string, string>>(props: {
|
||||||
flexShrink={0}
|
flexShrink={0}
|
||||||
gap={1}
|
gap={1}
|
||||||
paddingTop={1}
|
paddingTop={1}
|
||||||
paddingLeft={2}
|
paddingLeft={3}
|
||||||
paddingRight={3}
|
paddingRight={3}
|
||||||
paddingBottom={1}
|
paddingBottom={1}
|
||||||
backgroundColor={theme.backgroundElement}
|
|
||||||
justifyContent={narrow() ? "flex-start" : "space-between"}
|
justifyContent={narrow() ? "flex-start" : "space-between"}
|
||||||
alignItems={narrow() ? "flex-start" : "center"}
|
alignItems={narrow() ? "flex-start" : "center"}
|
||||||
>
|
>
|
||||||
<box flexDirection="row" gap={1} flexShrink={0}>
|
<box gap={0}>
|
||||||
<For each={keys}>
|
<For each={keys}>
|
||||||
{(option) => (
|
{(option, i) => (
|
||||||
<box
|
<box
|
||||||
paddingLeft={1}
|
flexDirection="row"
|
||||||
paddingRight={1}
|
gap={1}
|
||||||
backgroundColor={option === store.selected ? theme.warning : theme.backgroundMenu}
|
|
||||||
onMouseOver={() => setStore("selected", option)}
|
onMouseOver={() => setStore("selected", option)}
|
||||||
onMouseUp={() => {
|
onMouseUp={() => {
|
||||||
setStore("selected", option)
|
setStore("selected", option)
|
||||||
props.onSelect(option)
|
props.onSelect(option)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<text fg={option === store.selected ? selectedForeground(theme, theme.warning) : theme.textMuted}>
|
<text fg={option === store.selected ? theme.text : theme.textMuted}>
|
||||||
{props.options[option]}
|
{option === store.selected ? "❯" : " "} {i() + 1}. {props.options[option]}
|
||||||
</text>
|
</text>
|
||||||
</box>
|
</box>
|
||||||
)}
|
)}
|
||||||
|
|
@ -485,15 +481,12 @@ function Prompt<const T extends Record<string, string>>(props: {
|
||||||
</box>
|
</box>
|
||||||
<box flexDirection="row" gap={2} flexShrink={0}>
|
<box flexDirection="row" gap={2} flexShrink={0}>
|
||||||
<Show when={props.fullscreen}>
|
<Show when={props.fullscreen}>
|
||||||
<text fg={theme.text}>
|
<text fg={theme.textMuted}>
|
||||||
{"ctrl+f"} <span style={{ fg: theme.textMuted }}>{hint()}</span>
|
{"ctrl+f"} <span style={{ fg: theme.textMuted }}>{hint()}</span>
|
||||||
</text>
|
</text>
|
||||||
</Show>
|
</Show>
|
||||||
<text fg={theme.text}>
|
<text fg={theme.textMuted}>
|
||||||
{"⇆"} <span style={{ fg: theme.textMuted }}>select</span>
|
Esc to cancel <span style={{ fg: theme.textMuted }}>· Tab to amend</span>
|
||||||
</text>
|
|
||||||
<text fg={theme.text}>
|
|
||||||
enter <span style={{ fg: theme.textMuted }}>confirm</span>
|
|
||||||
</text>
|
</text>
|
||||||
</box>
|
</box>
|
||||||
</box>
|
</box>
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import { useKeybind } from "../../context/keybind"
|
||||||
import { selectedForeground, tint, useTheme } from "../../context/theme"
|
import { selectedForeground, tint, useTheme } from "../../context/theme"
|
||||||
import type { QuestionAnswer, QuestionRequest } from "@opencode-ai/sdk/v2"
|
import type { QuestionAnswer, QuestionRequest } from "@opencode-ai/sdk/v2"
|
||||||
import { useSDK } from "../../context/sdk"
|
import { useSDK } from "../../context/sdk"
|
||||||
import { SplitBorder } from "../../component/border"
|
|
||||||
import { useTextareaKeybindings } from "../../component/textarea-keybindings"
|
import { useTextareaKeybindings } from "../../component/textarea-keybindings"
|
||||||
import { useDialog } from "../../ui/dialog"
|
import { useDialog } from "../../ui/dialog"
|
||||||
|
|
||||||
|
|
@ -251,12 +251,7 @@ export function QuestionPrompt(props: { request: QuestionRequest }) {
|
||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<box
|
<box paddingLeft={1}>
|
||||||
backgroundColor={theme.backgroundPanel}
|
|
||||||
border={["left"]}
|
|
||||||
borderColor={theme.accent}
|
|
||||||
customBorderChars={SplitBorder.customBorderChars}
|
|
||||||
>
|
|
||||||
<box gap={1} paddingLeft={1} paddingRight={3} paddingTop={1} paddingBottom={1}>
|
<box gap={1} paddingLeft={1} paddingRight={3} paddingTop={1} paddingBottom={1}>
|
||||||
<Show when={!single()}>
|
<Show when={!single()}>
|
||||||
<box flexDirection="row" gap={1} paddingLeft={1}>
|
<box flexDirection="row" gap={1} paddingLeft={1}>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue