Compare commits

...

2 Commits

Author SHA1 Message Date
Aiden ff7b40ed4a refactor(tui): handle reasoning starts uniformly 2026-08-20 03:31:18 +00:00
Aiden 4a94dbe8af fix(tui): show encrypted reasoning status 2026-08-20 03:21:22 +00:00
3 changed files with 90 additions and 29 deletions
+55 -28
View File
@@ -1629,10 +1629,11 @@ function SessionReasoningGroupView(props: {
const message = props.message(ref.messageID)
if (message?.type !== "assistant") return []
const part = resolvePart(message, ref.partID)
if (part?.type !== "reasoning" || !reasoningContent(part)) return []
if (part?.type !== "reasoning" || (!reasoningContent(part) && !part.state)) return []
return [{ message, part }]
}),
)
const opaque = createMemo(() => parts().length > 0 && parts().every((item) => !reasoningContent(item.part)))
const latest = createMemo((previous: string | null) => {
const item = parts().at(-1)
if (!item) return previous
@@ -1657,7 +1658,7 @@ function SessionReasoningGroupView(props: {
>
<box flexDirection="column" flexShrink={0}>
<InlineToolRow
icon={expanded() ? "-" : "+"}
icon={opaque() ? "" : expanded() ? "-" : "+"}
color={
!props.completed
? theme.text.default
@@ -1673,19 +1674,29 @@ function SessionReasoningGroupView(props: {
complete={props.completed}
pending={latest() ? `Thinking: ${latest()}` : "Thinking"}
spinner={!props.completed}
onMouseOver={() => setHover(true)}
onMouseOver={() => !opaque() && setHover(true)}
onMouseOut={() => setHover(false)}
onMouseUp={() => {
if (renderer.getSelection()?.getSelectedText()) return
if (renderer.getSelection()?.getSelectedText() || opaque()) return
setExpanded((value) => !value)
}}
>
{props.completed ? "Thought" : latest() ? `Thinking: ${latest()}` : "Thinking"}
<Show when={props.completed && !expanded() && latest()}>: {latest()}</Show>
<Show when={props.completed && parts().length > 1}> · {parts().length} steps</Show>
<Show when={props.completed && duration()}> · {Locale.duration(duration())}</Show>
<Show
when={opaque() && props.completed}
fallback={
<>
{props.completed ? "Thought" : latest() ? `Thinking: ${latest()}` : "Thinking"}
<Show when={props.completed && !expanded() && latest()}>: {latest()}</Show>
<Show when={props.completed && parts().length > 1}> · {parts().length} steps</Show>
<Show when={props.completed && duration()}> · {Locale.duration(duration())}</Show>
</>
}
>
Thought
<Show when={duration()}> · {Locale.duration(duration())}</Show> · encrypted
</Show>
</InlineToolRow>
<Show when={expanded()}>
<Show when={expanded() && !opaque()}>
<box paddingLeft={3}>
<For each={props.refs}>
{(ref) => {
@@ -2296,6 +2307,7 @@ function ReasoningPart(props: {
const [expanded, setExpanded] = createSignal(false)
const content = createMemo(() => reasoningContent(props.part))
const opaque = createMemo(() => !content() && Boolean(props.part.state))
const isDone = createMemo(
() => props.part.time?.completed !== undefined || props.message.time.completed !== undefined,
)
@@ -2307,12 +2319,12 @@ function ReasoningPart(props: {
})
const summary = createMemo(() => reasoningSummary(content()))
const toggle = () => {
if (!inMinimal()) return
if (!inMinimal() || opaque()) return
setExpanded((prev) => !prev)
}
return (
<Show when={content()}>
<Show when={content() || opaque()}>
<box paddingLeft={3} flexDirection="column" flexShrink={0}>
<box
border={!inMinimal() || expanded() ? ["left"] : undefined}
@@ -2322,15 +2334,16 @@ function ReasoningPart(props: {
>
<box onMouseUp={toggle}>
<ReasoningHeader
toggleable={inMinimal()}
toggleable={inMinimal() && !opaque()}
open={!inMinimal() || expanded()}
done={isDone()}
title={inMinimal() && !expanded() ? summary().title : null}
duration={isDone() ? Locale.duration(duration()) : undefined}
encrypted={opaque()}
/>
</box>
</box>
<Show when={!inMinimal() || expanded()}>
<Show when={!opaque() && (!inMinimal() || expanded())}>
<box marginTop={1}>
<box
border={["left"]}
@@ -2366,6 +2379,7 @@ function ReasoningHeader(props: {
done: boolean
title: string | null
duration?: string
encrypted?: boolean
}) {
const theme = useTheme()
const fg = () =>
@@ -2387,21 +2401,34 @@ function ReasoningHeader(props: {
</Match>
<Match when={true}>
<text fg={fg()} wrapMode="none">
<Show when={props.toggleable}>
<span>{props.open ? "- " : "+ "}</span>
</Show>
<span>Thought</span>
<Show when={props.title || props.duration}>
<span>: </span>
</Show>
<Show when={props.title}>
<span>{props.title}</span>
</Show>
<Show when={props.duration}>
<span>
{props.title ? " · " : ""}
{props.duration}
</span>
<Show
when={props.encrypted}
fallback={
<>
<Show when={props.toggleable}>
<span>{props.open ? "- " : "+ "}</span>
</Show>
<span>Thought</span>
<Show when={props.title || props.duration}>
<span>: </span>
</Show>
<Show when={props.title}>
<span>{props.title}</span>
</Show>
<Show when={props.duration}>
<span>
{props.title ? " · " : ""}
{props.duration}
</span>
</Show>
</>
}
>
<span>Thought</span>
<Show when={props.duration}>
<span> · {props.duration}</span>
</Show>
<span> · encrypted</span>
</Show>
</text>
</Match>
+13 -1
View File
@@ -239,6 +239,13 @@ export function createSessionRows(sessionID: Accessor<string>, onSynced?: (sessi
if (event.data.sessionID === sessionID() && event.data.text.trim())
appendPart({ messageID: event.data.assistantMessageID, partID: `text:${event.data.ordinal}` }, { type: "text" })
}),
data.on("session.reasoning.started", (event) => {
if (event.data.sessionID === sessionID())
appendPart(
{ messageID: event.data.assistantMessageID, partID: `reasoning:${event.data.ordinal}` },
{ type: "reasoning" },
)
}),
data.on("session.reasoning.delta", (event) => {
if (event.data.sessionID === sessionID() && event.data.delta.trim())
appendPart(
@@ -305,7 +312,12 @@ export function reduceSessionRows(messages: SessionMessageInfo[], inputs = new S
const ordinals = { text: 0, reasoning: 0 }
message.content.forEach((part) => {
const partID = part.type === "tool" ? part.id : `${part.type}:${ordinals[part.type]++}`
if ((part.type === "text" || part.type === "reasoning") && !part.text.trim()) return
if (
(part.type === "text" || part.type === "reasoning") &&
!part.text.trim() &&
!(part.type === "reasoning" && part.state)
)
return
append(rows, { messageID: message.id, partID }, part)
})
const terminal = (message.finish && !["tool-calls", "unknown"].includes(message.finish)) || message.error
@@ -266,6 +266,28 @@ test("groups across empty assistant reasoning parts", () => {
])
})
test("keeps empty reasoning with provider state", () => {
const message = assistant("assistant-1", [
{
type: "reasoning",
text: "",
state: { reasoningEncryptedContent: "opaque" },
time: { created: 1_000, completed: 4_200 },
},
])
message.finish = "stop"
expect(reduceSessionRows([message])).toEqual([
{
type: "group",
kind: "reasoning",
completed: true,
refs: [{ messageID: "assistant-1", partID: "reasoning:0" }],
},
{ type: "assistant-footer", messageID: "assistant-1" },
])
})
test("completes exploration groups when another row follows", () => {
const finished = assistant("assistant-2", [
{ type: "tool", id: "grep-1", name: "grep", state: pending(), time: { created: 3 } },