fix(tui): align wrapped inline tool rows (#28664)

This commit is contained in:
Kit Langton
2026-05-29 21:50:05 -04:00
committed by GitHub
parent b2a06351b5
commit 5fb85a6aa3
3 changed files with 284 additions and 35 deletions

View File

@@ -1524,6 +1524,8 @@ const PART_MAPPING = {
reasoning: ReasoningPart,
}
const INLINE_TOOL_ICON_WIDTH = 2
function ReasoningPart(props: { last: boolean; part: ReasoningPart; message: AssistantMessage }) {
const { theme } = useTheme()
const ctx = use()
@@ -1789,12 +1791,12 @@ function InlineTool(props: {
part: ToolPart
onClick?: () => void
}) {
const [margin, setMargin] = createSignal(0)
const { theme } = useTheme()
const ctx = use()
const sync = useSync()
const renderer = useRenderer()
const [hover, setHover] = createSignal(false)
const [errorExpanded, setErrorExpanded] = createSignal(false)
const permission = createMemo(() => {
const callID = sync.data.permission[ctx.sessionID]?.at(0)?.tool?.callID
@@ -1802,14 +1804,6 @@ function InlineTool(props: {
return callID === props.part.callID
})
const fg = createMemo(() => {
if (props.color) return props.color
if (permission()) return theme.warning
if (hover() && props.onClick) return theme.text
if (props.complete) return theme.textMuted
return theme.text
})
const error = createMemo(() => (props.part.state.status === "error" ? props.part.state.error : undefined))
const denied = createMemo(
@@ -1820,53 +1814,134 @@ function InlineTool(props: {
error()?.includes("user dismissed"),
)
const failed = createMemo(() => Boolean(error() && !denied()))
const clickable = createMemo(() => Boolean(props.onClick || failed()))
const fg = createMemo(() => {
if (props.color) return props.color
if (permission()) return theme.warning
if (failed()) return theme.error
if (hover() && props.onClick) return theme.text
if (props.complete) return theme.textMuted
return theme.text
})
return (
<InlineToolRow
icon={props.icon}
iconColor={props.iconColor}
color={fg()}
errorColor={theme.error}
failed={failed()}
denied={Boolean(denied())}
error={error()}
errorExpanded={errorExpanded()}
complete={props.complete}
pending={props.pending}
spinner={props.spinner}
separateAfter={(id) =>
sync.data.message[ctx.sessionID]?.some((message) => message.role === "user" && message.id === id) ?? false
}
onMouseOver={() => clickable() && setHover(true)}
onMouseOut={() => setHover(false)}
onMouseUp={() => {
if (renderer.getSelection()?.getSelectedText()) return
if (failed()) {
setErrorExpanded((value) => !value)
return
}
props.onClick?.()
}}
>
{props.children}
</InlineToolRow>
)
}
export function InlineToolRow(props: {
icon: string
iconColor?: RGBA
color?: RGBA
errorColor?: RGBA
failed?: boolean
denied?: boolean
error?: string
errorExpanded?: boolean
complete: any
pending: string
spinner?: boolean
children: JSX.Element
separateAfter?: (id: string | undefined) => boolean
onMouseOver?: () => void
onMouseOut?: () => void
onMouseUp?: () => void
}) {
const [margin, setMargin] = createSignal(0)
return (
<box
marginTop={margin()}
paddingLeft={3}
onMouseOver={() => props.onClick && setHover(true)}
onMouseOut={() => setHover(false)}
onMouseUp={() => {
if (renderer.getSelection()?.getSelectedText()) return
props.onClick?.()
}}
onMouseOver={props.onMouseOver}
onMouseOut={props.onMouseOut}
onMouseUp={props.onMouseUp}
renderBefore={function () {
const el = this as BoxRenderable
const parent = el.parent
if (!parent) {
return
}
if (el.height > 1) {
setMargin(1)
return
}
const children = parent.getChildren()
const index = children.indexOf(el)
const previous = children[index - 1]
if (!previous) {
setMargin(0)
return
}
if (previous.height > 1 || previous.id.startsWith("text-")) {
setMargin(1)
return
}
setMargin(
previous?.id.startsWith("text-") ||
previous?.id.startsWith("tool-block-") ||
props.separateAfter?.(previous?.id)
? 1
: 0,
)
}}
>
<Switch>
<Match when={props.spinner}>
<Spinner color={fg()} children={props.children} />
<Spinner color={props.color} children={props.children} />
</Match>
<Match when={true}>
<text paddingLeft={3} 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>
</text>
<Show
fallback={
<text
paddingLeft={3}
fg={props.color}
attributes={props.denied ? TextAttributes.STRIKETHROUGH : undefined}
>
~ {props.pending}
</text>
}
when={props.complete}
>
<box flexDirection="row">
<text
width={INLINE_TOOL_ICON_WIDTH}
fg={props.failed ? props.errorColor : (props.iconColor ?? props.color)}
attributes={props.denied ? TextAttributes.STRIKETHROUGH : undefined}
>
{props.icon}
</text>
<text
flexGrow={1}
fg={props.failed ? props.errorColor : props.color}
attributes={props.denied ? TextAttributes.STRIKETHROUGH : undefined}
>
{props.children}
</text>
</box>
</Show>
</Match>
</Switch>
<Show when={error() && !denied()}>
<text fg={theme.error}>{error()}</text>
<Show when={props.failed && props.errorExpanded}>
<box paddingLeft={INLINE_TOOL_ICON_WIDTH}>
<text fg={props.errorColor}>{props.error}</text>
</box>
</Show>
</box>
)
@@ -1885,6 +1960,7 @@ function BlockTool(props: {
const error = createMemo(() => (props.part?.state.status === "error" ? props.part.state.error : undefined))
return (
<box
id={props.part ? "tool-block-" + props.part.id : undefined}
border={["left"]}
paddingTop={1}
paddingBottom={1}