Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 55 additions & 28 deletions packages/tui/src/routes/session/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -1657,7 +1658,7 @@ function SessionReasoningGroupView(props: {
>
<box flexDirection="column" flexShrink={0}>
<InlineToolRow
icon={expanded() ? "-" : "+"}
icon={opaque() ? "" : expanded() ? "-" : "+"}
color={
!props.completed
? theme.text.default
Expand All @@ -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) => {
Expand Down Expand Up @@ -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,
)
Expand All @@ -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}
Expand All @@ -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"]}
Expand Down Expand Up @@ -2366,6 +2379,7 @@ function ReasoningHeader(props: {
done: boolean
title: string | null
duration?: string
encrypted?: boolean
}) {
const theme = useTheme()
const fg = () =>
Expand All @@ -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>
Expand Down
14 changes: 13 additions & 1 deletion packages/tui/src/routes/session/rows.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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
Expand Down
22 changes: 22 additions & 0 deletions packages/tui/test/cli/tui/session-rows.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 } },
Expand Down
Loading