feat(tui): group sequential thinking (#36901)
Co-authored-by: Kit Langton <7587245+kitlangton@users.noreply.github.com>
This commit is contained in:
parent
b67bed061a
commit
016545513f
8 changed files with 262 additions and 99 deletions
|
|
@ -968,7 +968,10 @@ function SessionRowView(props: { row: SessionRow; message: (messageID: string) =
|
|||
<Match when={props.row.type === "part" ? props.row : undefined}>
|
||||
{(row) => <SessionPartView partRef={row().ref} message={props.message} />}
|
||||
</Match>
|
||||
<Match when={props.row.type === "group" ? props.row : undefined}>
|
||||
<Match when={props.row.type === "group" && props.row.kind === "reasoning" ? props.row : undefined}>
|
||||
{(row) => <SessionReasoningGroupView refs={row().refs} completed={row().completed} message={props.message} />}
|
||||
</Match>
|
||||
<Match when={props.row.type === "group" && props.row.kind === "exploration" ? props.row : undefined}>
|
||||
{(row) => (
|
||||
<SessionGroupView
|
||||
refs={row().refs}
|
||||
|
|
@ -1078,6 +1081,114 @@ function SessionPartView(props: { partRef: PartRef; message: (messageID: string)
|
|||
)
|
||||
}
|
||||
|
||||
function SessionReasoningGroupView(props: {
|
||||
refs: PartRef[]
|
||||
completed: boolean
|
||||
message: (messageID: string) => SessionMessageInfo | undefined
|
||||
}) {
|
||||
const ctx = use()
|
||||
const { theme, syntax } = useTheme()
|
||||
const renderer = useRenderer()
|
||||
const [expanded, setExpanded] = createSignal(false)
|
||||
const parts = createMemo<{ message: SessionMessageAssistant; part: SessionMessageAssistantReasoning }[]>(
|
||||
(previous) => {
|
||||
const next = props.refs.flatMap((ref) => {
|
||||
const message = props.message(ref.messageID)
|
||||
if (message?.type !== "assistant") return []
|
||||
const part = resolvePart(message, ref.partID)
|
||||
if (part?.type !== "reasoning" || !part.text.replace("[REDACTED]", "").trim()) return []
|
||||
return [{ message, part }]
|
||||
})
|
||||
return next.length > 0 ? next : previous
|
||||
},
|
||||
[] as { message: SessionMessageAssistant; part: SessionMessageAssistantReasoning }[],
|
||||
)
|
||||
const latest = createMemo((previous: string | null) => {
|
||||
const item = parts().at(-1)
|
||||
if (!item) return previous
|
||||
const title = reasoningSummary(item.part.text.replace("[REDACTED]", "").trim()).title
|
||||
if (title) return title
|
||||
if (item.part.time?.completed !== undefined || item.message.time.completed !== undefined) return null
|
||||
return previous
|
||||
}, null)
|
||||
const duration = createMemo(() =>
|
||||
parts().reduce((total, item) => {
|
||||
const end = item.part.time?.completed ?? item.message.time.completed
|
||||
const start = item.part.time?.created ?? item.message.time.created
|
||||
return total + (end === undefined ? 0 : Math.max(0, end - start))
|
||||
}, 0),
|
||||
)
|
||||
|
||||
return (
|
||||
<Show when={parts().length > 0}>
|
||||
<Show
|
||||
when={ctx.thinkingMode() === "hide"}
|
||||
fallback={
|
||||
<For each={parts()}>{(item) => <ReasoningPart part={item.part} message={item.message} last={false} />}</For>
|
||||
}
|
||||
>
|
||||
<box paddingLeft={3} flexDirection="column" flexShrink={0}>
|
||||
<box
|
||||
onMouseUp={() => {
|
||||
if (renderer.getSelection()?.getSelectedText()) return
|
||||
setExpanded((value) => !value)
|
||||
}}
|
||||
>
|
||||
<Show
|
||||
when={props.completed}
|
||||
fallback={<Spinner color={theme.warning}>{latest() ? `Thinking: ${latest()}` : "Thinking"}</Spinner>}
|
||||
>
|
||||
<Show
|
||||
when={expanded()}
|
||||
fallback={
|
||||
<text fg={theme.warning} wrapMode="none">
|
||||
+ Thought
|
||||
<Show when={latest()}>: {latest()}</Show>
|
||||
<Show when={parts().length > 1}> · {parts().length} steps</Show>
|
||||
<Show when={duration()}> · {Locale.duration(duration())}</Show>
|
||||
</text>
|
||||
}
|
||||
>
|
||||
<text fg={theme.warning} wrapMode="none">
|
||||
- Thought
|
||||
<Show when={parts().length > 1}> · {parts().length} steps</Show>
|
||||
<Show when={duration()}> · {Locale.duration(duration())}</Show>
|
||||
</text>
|
||||
</Show>
|
||||
</Show>
|
||||
</box>
|
||||
<Show when={expanded()}>
|
||||
<For each={parts()}>
|
||||
{(item) => {
|
||||
const content = createMemo(() => item.part.text.replace("[REDACTED]", "").trim())
|
||||
const summary = createMemo(() => reasoningSummary(content()))
|
||||
const markdown = createMemo(() => {
|
||||
if (!summary().title) return content()
|
||||
if (!summary().body) return `**${summary().title}**`
|
||||
return `**${summary().title}** · ${summary().body}`
|
||||
})
|
||||
return (
|
||||
<box marginTop={1}>
|
||||
<code
|
||||
filetype="markdown"
|
||||
drawUnstyledText={false}
|
||||
streaming={false}
|
||||
syntaxStyle={syntax()}
|
||||
content={markdown()}
|
||||
conceal={ctx.markdownMode() === "rendered"}
|
||||
fg={theme.textMuted}
|
||||
/>
|
||||
</box>
|
||||
)
|
||||
}}
|
||||
</For>
|
||||
</Show>
|
||||
</box>
|
||||
</Show>
|
||||
</Show>
|
||||
)
|
||||
}
|
||||
|
||||
function SessionGroupView(props: {
|
||||
refs: PartRef[]
|
||||
pending: PartRef[]
|
||||
|
|
|
|||
|
|
@ -12,6 +12,12 @@ export type SessionRow =
|
|||
| { type: "message"; messageID: string }
|
||||
| { type: "compaction-queued"; inputID: string }
|
||||
| { type: "part"; ref: PartRef }
|
||||
| {
|
||||
type: "group"
|
||||
kind: "reasoning"
|
||||
refs: PartRef[]
|
||||
completed: boolean
|
||||
}
|
||||
| {
|
||||
type: "group"
|
||||
kind: "exploration"
|
||||
|
|
@ -131,6 +137,16 @@ export function createSessionRows(sessionID: Accessor<string>) {
|
|||
produce((draft) => {
|
||||
if (hasPart(draft, ref)) return
|
||||
const index = queuedStart(draft)
|
||||
if (ref.partID.startsWith("reasoning:")) {
|
||||
const previous = draft[index - 1]
|
||||
if (previous?.type === "group" && previous.kind === "reasoning") {
|
||||
previous.refs.push(ref)
|
||||
return
|
||||
}
|
||||
completePrevious(draft, index)
|
||||
draft.splice(index, 0, { type: "group", kind: "reasoning", refs: [ref], completed: false })
|
||||
return
|
||||
}
|
||||
if (name && exploration(name)) {
|
||||
const previous = draft[index - 1]
|
||||
if (previous?.type === "group" && previous.kind === "exploration") {
|
||||
|
|
@ -213,7 +229,7 @@ export function createSessionRows(sessionID: Accessor<string>) {
|
|||
data.on("session.agent.selected", message),
|
||||
data.on("session.model.selected", message),
|
||||
data.on("session.text.delta", (event) => {
|
||||
if (event.data.sessionID === sessionID())
|
||||
if (event.data.sessionID === sessionID() && event.data.delta.trim())
|
||||
appendPart({ messageID: event.data.assistantMessageID, partID: `text:${event.data.ordinal}` })
|
||||
}),
|
||||
data.on("session.text.ended", (event) => {
|
||||
|
|
@ -290,6 +306,16 @@ export function resolvePart(message: SessionMessageAssistant, partID: string) {
|
|||
}
|
||||
|
||||
function append(rows: SessionRow[], ref: PartRef, part: SessionMessageAssistant["content"][number]) {
|
||||
if (part.type === "reasoning") {
|
||||
const previous = rows.at(-1)
|
||||
if (previous?.type === "group" && previous.kind === "reasoning") {
|
||||
previous.refs.push(ref)
|
||||
return
|
||||
}
|
||||
completePrevious(rows)
|
||||
rows.push({ type: "group", kind: "reasoning", refs: [ref], completed: false })
|
||||
return
|
||||
}
|
||||
if (part.type === "tool") {
|
||||
if (exploration(part.name)) {
|
||||
const previous = rows.at(-1)
|
||||
|
|
@ -313,7 +339,7 @@ function completePrevious(rows: SessionRow[], index = rows.length) {
|
|||
|
||||
function partitionPending(rows: SessionRow[], pending: Set<string>) {
|
||||
rows.forEach((row) => {
|
||||
if (row.type !== "group") return
|
||||
if (row.type !== "group" || row.kind !== "exploration") return
|
||||
const refs = [...row.refs, ...row.pending]
|
||||
row.refs = refs.filter((ref) => !pending.has(ref.partID))
|
||||
row.pending = refs.filter((ref) => pending.has(ref.partID))
|
||||
|
|
@ -328,6 +354,7 @@ function hasPart(rows: SessionRow[], ref: PartRef) {
|
|||
return rows.some((row) => {
|
||||
if (row.type === "part") return row.ref.messageID === ref.messageID && row.ref.partID === ref.partID
|
||||
if (row.type !== "group") return false
|
||||
return [...row.refs, ...row.pending].some((item) => item.messageID === ref.messageID && item.partID === ref.partID)
|
||||
const refs = row.kind === "exploration" ? [...row.refs, ...row.pending] : row.refs
|
||||
return refs.some((item) => item.messageID === ref.messageID && item.partID === ref.partID)
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ export function DialogPrompt(props: DialogPromptProps) {
|
|||
{
|
||||
id: "dialog.prompt.submit",
|
||||
title: "Submit dialog prompt",
|
||||
bind: "return",
|
||||
group: "Dialog",
|
||||
run: confirm,
|
||||
},
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue