feat(app): show/hide reasoning summaries
This commit is contained in:
@@ -250,6 +250,18 @@ export const SettingsGeneral: Component = () => {
|
|||||||
)}
|
)}
|
||||||
</Select>
|
</Select>
|
||||||
</SettingsRow>
|
</SettingsRow>
|
||||||
|
|
||||||
|
<SettingsRow
|
||||||
|
title={language.t("settings.general.row.reasoningSummaries.title")}
|
||||||
|
description={language.t("settings.general.row.reasoningSummaries.description")}
|
||||||
|
>
|
||||||
|
<div data-action="settings-reasoning-summaries">
|
||||||
|
<Switch
|
||||||
|
checked={settings.general.showReasoningSummaries()}
|
||||||
|
onChange={(checked) => settings.general.setShowReasoningSummaries(checked)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</SettingsRow>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ export interface Settings {
|
|||||||
general: {
|
general: {
|
||||||
autoSave: boolean
|
autoSave: boolean
|
||||||
releaseNotes: boolean
|
releaseNotes: boolean
|
||||||
|
showReasoningSummaries: boolean
|
||||||
}
|
}
|
||||||
updates: {
|
updates: {
|
||||||
startup: boolean
|
startup: boolean
|
||||||
@@ -42,6 +43,7 @@ const defaultSettings: Settings = {
|
|||||||
general: {
|
general: {
|
||||||
autoSave: true,
|
autoSave: true,
|
||||||
releaseNotes: true,
|
releaseNotes: true,
|
||||||
|
showReasoningSummaries: false,
|
||||||
},
|
},
|
||||||
updates: {
|
updates: {
|
||||||
startup: true,
|
startup: true,
|
||||||
@@ -120,6 +122,13 @@ export const { use: useSettings, provider: SettingsProvider } = createSimpleCont
|
|||||||
setReleaseNotes(value: boolean) {
|
setReleaseNotes(value: boolean) {
|
||||||
setStore("general", "releaseNotes", value)
|
setStore("general", "releaseNotes", value)
|
||||||
},
|
},
|
||||||
|
showReasoningSummaries: withFallback(
|
||||||
|
() => store.general?.showReasoningSummaries,
|
||||||
|
defaultSettings.general.showReasoningSummaries,
|
||||||
|
),
|
||||||
|
setShowReasoningSummaries(value: boolean) {
|
||||||
|
setStore("general", "showReasoningSummaries", value)
|
||||||
|
},
|
||||||
},
|
},
|
||||||
updates: {
|
updates: {
|
||||||
startup: withFallback(() => store.updates?.startup, defaultSettings.updates.startup),
|
startup: withFallback(() => store.updates?.startup, defaultSettings.updates.startup),
|
||||||
|
|||||||
@@ -610,6 +610,8 @@ export const dict = {
|
|||||||
"settings.general.row.theme.description": "Customise how OpenCode is themed.",
|
"settings.general.row.theme.description": "Customise how OpenCode is themed.",
|
||||||
"settings.general.row.font.title": "Font",
|
"settings.general.row.font.title": "Font",
|
||||||
"settings.general.row.font.description": "Customise the mono font used in code blocks",
|
"settings.general.row.font.description": "Customise the mono font used in code blocks",
|
||||||
|
"settings.general.row.reasoningSummaries.title": "Show reasoning summaries",
|
||||||
|
"settings.general.row.reasoningSummaries.description": "Display model reasoning summaries in the timeline",
|
||||||
|
|
||||||
"settings.general.row.wayland.title": "Use native Wayland",
|
"settings.general.row.wayland.title": "Use native Wayland",
|
||||||
"settings.general.row.wayland.description": "Disable X11 fallback on Wayland. Requires restart.",
|
"settings.general.row.wayland.description": "Disable X11 fallback on Wayland. Requires restart.",
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { shouldMarkBoundaryGesture, normalizeWheelDelta } from "@/pages/session/
|
|||||||
import { SessionContextUsage } from "@/components/session-context-usage"
|
import { SessionContextUsage } from "@/components/session-context-usage"
|
||||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
|
import { useSettings } from "@/context/settings"
|
||||||
import { useSDK } from "@/context/sdk"
|
import { useSDK } from "@/context/sdk"
|
||||||
import { useSync } from "@/context/sync"
|
import { useSync } from "@/context/sync"
|
||||||
|
|
||||||
@@ -80,6 +81,7 @@ export function MessageTimeline(props: {
|
|||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const sdk = useSDK()
|
const sdk = useSDK()
|
||||||
const sync = useSync()
|
const sync = useSync()
|
||||||
|
const settings = useSettings()
|
||||||
const dialog = useDialog()
|
const dialog = useDialog()
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
|
|
||||||
@@ -535,6 +537,7 @@ export function MessageTimeline(props: {
|
|||||||
sessionID={sessionID() ?? ""}
|
sessionID={sessionID() ?? ""}
|
||||||
messageID={message.id}
|
messageID={message.id}
|
||||||
lastUserMessageID={props.lastUserMessageID}
|
lastUserMessageID={props.lastUserMessageID}
|
||||||
|
showReasoningSummaries={settings.general.showReasoningSummaries()}
|
||||||
classes={{
|
classes={{
|
||||||
root: "min-w-0 w-full relative",
|
root: "min-w-0 w-full relative",
|
||||||
content: "flex flex-col justify-between !overflow-visible",
|
content: "flex flex-col justify-between !overflow-visible",
|
||||||
|
|||||||
@@ -96,6 +96,7 @@ export interface MessageProps {
|
|||||||
parts: PartType[]
|
parts: PartType[]
|
||||||
showAssistantCopyPartID?: string | null
|
showAssistantCopyPartID?: string | null
|
||||||
interrupted?: boolean
|
interrupted?: boolean
|
||||||
|
showReasoningSummaries?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MessagePartProps {
|
export interface MessagePartProps {
|
||||||
@@ -264,14 +265,14 @@ function list<T>(value: T[] | undefined | null, fallback: T[]) {
|
|||||||
return fallback
|
return fallback
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderable(part: PartType) {
|
function renderable(part: PartType, showReasoningSummaries = true) {
|
||||||
if (part.type === "tool") {
|
if (part.type === "tool") {
|
||||||
if (HIDDEN_TOOLS.has(part.tool)) return false
|
if (HIDDEN_TOOLS.has(part.tool)) return false
|
||||||
if (part.tool === "question") return part.state.status !== "pending" && part.state.status !== "running"
|
if (part.tool === "question") return part.state.status !== "pending" && part.state.status !== "running"
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
if (part.type === "text") return !!part.text?.trim()
|
if (part.type === "text") return !!part.text?.trim()
|
||||||
if (part.type === "reasoning") return !!part.text?.trim()
|
if (part.type === "reasoning") return showReasoningSummaries && !!part.text?.trim()
|
||||||
return !!PART_MAPPING[part.type]
|
return !!PART_MAPPING[part.type]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -280,6 +281,7 @@ export function AssistantParts(props: {
|
|||||||
showAssistantCopyPartID?: string | null
|
showAssistantCopyPartID?: string | null
|
||||||
turnDurationMs?: number
|
turnDurationMs?: number
|
||||||
working?: boolean
|
working?: boolean
|
||||||
|
showReasoningSummaries?: boolean
|
||||||
}) {
|
}) {
|
||||||
const data = useData()
|
const data = useData()
|
||||||
const emptyParts: PartType[] = []
|
const emptyParts: PartType[] = []
|
||||||
@@ -300,7 +302,7 @@ export function AssistantParts(props: {
|
|||||||
|
|
||||||
const parts = props.messages.flatMap((message) =>
|
const parts = props.messages.flatMap((message) =>
|
||||||
list(data.store.part?.[message.id], emptyParts)
|
list(data.store.part?.[message.id], emptyParts)
|
||||||
.filter(renderable)
|
.filter((part) => renderable(part, props.showReasoningSummaries ?? true))
|
||||||
.map((part) => ({ message, part })),
|
.map((part) => ({ message, part })),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -480,6 +482,7 @@ export function Message(props: MessageProps) {
|
|||||||
message={assistantMessage() as AssistantMessage}
|
message={assistantMessage() as AssistantMessage}
|
||||||
parts={props.parts}
|
parts={props.parts}
|
||||||
showAssistantCopyPartID={props.showAssistantCopyPartID}
|
showAssistantCopyPartID={props.showAssistantCopyPartID}
|
||||||
|
showReasoningSummaries={props.showReasoningSummaries}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Match>
|
</Match>
|
||||||
@@ -491,6 +494,7 @@ export function AssistantMessageDisplay(props: {
|
|||||||
message: AssistantMessage
|
message: AssistantMessage
|
||||||
parts: PartType[]
|
parts: PartType[]
|
||||||
showAssistantCopyPartID?: string | null
|
showAssistantCopyPartID?: string | null
|
||||||
|
showReasoningSummaries?: boolean
|
||||||
}) {
|
}) {
|
||||||
const grouped = createMemo(() => {
|
const grouped = createMemo(() => {
|
||||||
const keys: string[] = []
|
const keys: string[] = []
|
||||||
@@ -519,7 +523,7 @@ export function AssistantMessageDisplay(props: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
parts.forEach((part, index) => {
|
parts.forEach((part, index) => {
|
||||||
if (!renderable(part)) return
|
if (!renderable(part, props.showReasoningSummaries ?? true)) return
|
||||||
|
|
||||||
if (isContextGroupTool(part)) {
|
if (isContextGroupTool(part)) {
|
||||||
if (start < 0) start = index
|
if (start < 0) start = index
|
||||||
|
|||||||
@@ -41,6 +41,8 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
|
width: 100%;
|
||||||
|
min-width: 0;
|
||||||
color: var(--text-weak);
|
color: var(--text-weak);
|
||||||
font-family: var(--font-family-sans);
|
font-family: var(--font-family-sans);
|
||||||
font-size: var(--font-size-base);
|
font-size: var(--font-size-base);
|
||||||
@@ -52,6 +54,16 @@
|
|||||||
width: 16px;
|
width: 16px;
|
||||||
height: 16px;
|
height: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[data-slot="session-turn-thinking-heading"] {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
color: var(--text-weaker);
|
||||||
|
font-weight: var(--font-weight-regular);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.error-card {
|
.error-card {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { Binary } from "@opencode-ai/util/binary"
|
|||||||
import { getDirectory, getFilename } from "@opencode-ai/util/path"
|
import { getDirectory, getFilename } from "@opencode-ai/util/path"
|
||||||
import { createEffect, createMemo, createSignal, For, on, ParentProps, Show } from "solid-js"
|
import { createEffect, createMemo, createSignal, For, on, ParentProps, Show } from "solid-js"
|
||||||
import { Dynamic } from "solid-js/web"
|
import { Dynamic } from "solid-js/web"
|
||||||
import { AssistantParts, Message } from "./message-part"
|
import { AssistantParts, Message, PART_MAPPING } from "./message-part"
|
||||||
import { Card } from "./card"
|
import { Card } from "./card"
|
||||||
import { Accordion } from "./accordion"
|
import { Accordion } from "./accordion"
|
||||||
import { StickyAccordionHeader } from "./sticky-accordion-header"
|
import { StickyAccordionHeader } from "./sticky-accordion-header"
|
||||||
@@ -83,15 +83,55 @@ function list<T>(value: T[] | undefined | null, fallback: T[]) {
|
|||||||
|
|
||||||
const hidden = new Set(["todowrite", "todoread"])
|
const hidden = new Set(["todowrite", "todoread"])
|
||||||
|
|
||||||
function visible(part: PartType) {
|
function partState(part: PartType, showReasoningSummaries: boolean) {
|
||||||
if (part.type === "tool") {
|
if (part.type === "tool") {
|
||||||
if (hidden.has(part.tool)) return false
|
if (hidden.has(part.tool)) return
|
||||||
if (part.tool === "question") return part.state.status !== "pending" && part.state.status !== "running"
|
if (part.tool === "question" && (part.state.status === "pending" || part.state.status === "running")) return
|
||||||
return true
|
return "visible" as const
|
||||||
|
}
|
||||||
|
if (part.type === "text") return part.text?.trim() ? ("visible" as const) : undefined
|
||||||
|
if (part.type === "reasoning") {
|
||||||
|
if (showReasoningSummaries) return "visible" as const
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (PART_MAPPING[part.type]) return "visible" as const
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
function clean(value: string) {
|
||||||
|
return value
|
||||||
|
.replace(/`([^`]+)`/g, "$1")
|
||||||
|
.replace(/\[([^\]]+)\]\([^\)]+\)/g, "$1")
|
||||||
|
.replace(/[*_~]+/g, "")
|
||||||
|
.trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
function heading(text: string) {
|
||||||
|
const markdown = text.replace(/\r\n?/g, "\n")
|
||||||
|
|
||||||
|
const html = markdown.match(/<h[1-6][^>]*>([\s\S]*?)<\/h[1-6]>/i)
|
||||||
|
if (html?.[1]) {
|
||||||
|
const value = clean(html[1].replace(/<[^>]+>/g, " "))
|
||||||
|
if (value) return value
|
||||||
|
}
|
||||||
|
|
||||||
|
const atx = markdown.match(/^\s{0,3}#{1,6}[ \t]+(.+?)(?:[ \t]+#+[ \t]*)?$/m)
|
||||||
|
if (atx?.[1]) {
|
||||||
|
const value = clean(atx[1])
|
||||||
|
if (value) return value
|
||||||
|
}
|
||||||
|
|
||||||
|
const setext = markdown.match(/^([^\n]+)\n(?:=+|-+)\s*$/m)
|
||||||
|
if (setext?.[1]) {
|
||||||
|
const value = clean(setext[1])
|
||||||
|
if (value) return value
|
||||||
|
}
|
||||||
|
|
||||||
|
const strong = markdown.match(/^\s*(?:\*\*|__)(.+?)(?:\*\*|__)\s*$/m)
|
||||||
|
if (strong?.[1]) {
|
||||||
|
const value = clean(strong[1])
|
||||||
|
if (value) return value
|
||||||
}
|
}
|
||||||
if (part.type === "text") return !!part.text?.trim()
|
|
||||||
if (part.type === "reasoning") return !!part.text?.trim()
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SessionTurn(
|
export function SessionTurn(
|
||||||
@@ -99,6 +139,7 @@ export function SessionTurn(
|
|||||||
sessionID: string
|
sessionID: string
|
||||||
messageID: string
|
messageID: string
|
||||||
lastUserMessageID?: string
|
lastUserMessageID?: string
|
||||||
|
showReasoningSummaries?: boolean
|
||||||
onUserInteracted?: () => void
|
onUserInteracted?: () => void
|
||||||
classes?: {
|
classes?: {
|
||||||
root?: string
|
root?: string
|
||||||
@@ -242,6 +283,7 @@ export function SessionTurn(
|
|||||||
|
|
||||||
const status = createMemo(() => data.store.session_status[props.sessionID] ?? idle)
|
const status = createMemo(() => data.store.session_status[props.sessionID] ?? idle)
|
||||||
const working = createMemo(() => status().type !== "idle" && isLastUserMessage())
|
const working = createMemo(() => status().type !== "idle" && isLastUserMessage())
|
||||||
|
const showReasoningSummaries = createMemo(() => props.showReasoningSummaries ?? true)
|
||||||
|
|
||||||
const assistantCopyPartID = createMemo(() => {
|
const assistantCopyPartID = createMemo(() => {
|
||||||
if (working()) return null
|
if (working()) return null
|
||||||
@@ -265,9 +307,33 @@ export function SessionTurn(
|
|||||||
const assistantVisible = createMemo(() =>
|
const assistantVisible = createMemo(() =>
|
||||||
assistantMessages().reduce((count, message) => {
|
assistantMessages().reduce((count, message) => {
|
||||||
const parts = list(data.store.part?.[message.id], emptyParts)
|
const parts = list(data.store.part?.[message.id], emptyParts)
|
||||||
return count + parts.filter(visible).length
|
return count + parts.filter((part) => partState(part, showReasoningSummaries()) === "visible").length
|
||||||
}, 0),
|
}, 0),
|
||||||
)
|
)
|
||||||
|
const assistantTailVisible = createMemo(() =>
|
||||||
|
assistantMessages()
|
||||||
|
.flatMap((message) => list(data.store.part?.[message.id], emptyParts))
|
||||||
|
.flatMap((part) => {
|
||||||
|
if (partState(part, showReasoningSummaries()) !== "visible") return []
|
||||||
|
if (part.type === "text") return ["text" as const]
|
||||||
|
return ["other" as const]
|
||||||
|
})
|
||||||
|
.at(-1),
|
||||||
|
)
|
||||||
|
const reasoningHeading = createMemo(() =>
|
||||||
|
assistantMessages()
|
||||||
|
.flatMap((message) => list(data.store.part?.[message.id], emptyParts))
|
||||||
|
.filter((part): part is PartType & { type: "reasoning"; text: string } => part.type === "reasoning")
|
||||||
|
.map((part) => heading(part.text))
|
||||||
|
.filter((text): text is string => !!text)
|
||||||
|
.at(-1),
|
||||||
|
)
|
||||||
|
const showThinking = createMemo(() => {
|
||||||
|
if (!working() || !!error()) return false
|
||||||
|
if (showReasoningSummaries()) return assistantVisible() === 0
|
||||||
|
if (assistantTailVisible() === "text") return false
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
const autoScroll = createAutoScroll({
|
const autoScroll = createAutoScroll({
|
||||||
working,
|
working,
|
||||||
@@ -295,11 +361,6 @@ export function SessionTurn(
|
|||||||
<div data-slot="session-turn-message-content" aria-live="off">
|
<div data-slot="session-turn-message-content" aria-live="off">
|
||||||
<Message message={msg()} parts={parts()} interrupted={interrupted()} />
|
<Message message={msg()} parts={parts()} interrupted={interrupted()} />
|
||||||
</div>
|
</div>
|
||||||
<Show when={working() && assistantVisible() === 0 && !error()}>
|
|
||||||
<div data-slot="session-turn-thinking">
|
|
||||||
<TextShimmer text={i18n.t("ui.sessionTurn.status.thinking")} />
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
<Show when={assistantMessages().length > 0}>
|
<Show when={assistantMessages().length > 0}>
|
||||||
<div data-slot="session-turn-assistant-content" aria-hidden={working()}>
|
<div data-slot="session-turn-assistant-content" aria-hidden={working()}>
|
||||||
<AssistantParts
|
<AssistantParts
|
||||||
@@ -307,9 +368,18 @@ export function SessionTurn(
|
|||||||
showAssistantCopyPartID={assistantCopyPartID()}
|
showAssistantCopyPartID={assistantCopyPartID()}
|
||||||
turnDurationMs={turnDurationMs()}
|
turnDurationMs={turnDurationMs()}
|
||||||
working={working()}
|
working={working()}
|
||||||
|
showReasoningSummaries={showReasoningSummaries()}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
|
<Show when={showThinking()}>
|
||||||
|
<div data-slot="session-turn-thinking">
|
||||||
|
<TextShimmer text={i18n.t("ui.sessionTurn.status.thinking")} />
|
||||||
|
<Show when={!showReasoningSummaries() && reasoningHeading()}>
|
||||||
|
{(text) => <span data-slot="session-turn-thinking-heading">{text()}</span>}
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
<Show when={edited() > 0 && !working()}>
|
<Show when={edited() > 0 && !working()}>
|
||||||
<div data-slot="session-turn-diffs">
|
<div data-slot="session-turn-diffs">
|
||||||
<Collapsible open={open()} onOpenChange={setOpen} variant="ghost">
|
<Collapsible open={open()} onOpenChange={setOpen} variant="ghost">
|
||||||
|
|||||||
Reference in New Issue
Block a user