mirror of
https://github.com/anomalyco/opencode.git
synced 2026-11-13 19:46:16 +03:00
fix(session-ui): show latest thinking summary heading while streaming (#53815)
This commit is contained in:
committed by
GitHub
Unverified
parent
be19c7612f
commit
46c69bc35c
@@ -6,7 +6,14 @@ import type {
|
||||
SessionMessageInfo,
|
||||
} from "@opencode/client/promise"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { createTimelineProjection, reuseTimelineRows, Timeline, TimelineRow, type PartGroup } from "./projection"
|
||||
import {
|
||||
createTimelineProjection,
|
||||
reasoningHeading,
|
||||
reuseTimelineRows,
|
||||
Timeline,
|
||||
TimelineRow,
|
||||
type PartGroup,
|
||||
} from "./projection"
|
||||
|
||||
const context = (key: string, partIDs: string[], identity: { userMessageID?: string; messageID?: string } = {}) =>
|
||||
new TimelineRow.AssistantPart({
|
||||
@@ -325,3 +332,35 @@ describe("createTimelineProjection", () => {
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe("reasoningHeading", () => {
|
||||
test("returns the most recent thinking summary heading as sections stream in", () => {
|
||||
const sections = [
|
||||
"**Designing Interactive Experience**\nI'm focusing on crafting a fun, tactile, and interactive visual toy.",
|
||||
"**Analyzing Reading Recommendations**\nI'm currently dissecting whether Luke Parker should read the *Throne of Glass* series.",
|
||||
"**Constructing Reader Tools**\nI'm now building out interactive tools.",
|
||||
]
|
||||
|
||||
expect(reasoningHeading(sections[0])).toBe("Designing Interactive Experience")
|
||||
expect(reasoningHeading(sections.slice(0, 2).join("\n\n"))).toBe("Analyzing Reading Recommendations")
|
||||
expect(reasoningHeading(sections.join("\n\n"))).toBe("Constructing Reader Tools")
|
||||
})
|
||||
|
||||
test("keeps the previous heading while the next bold heading is still streaming", () => {
|
||||
const text = [
|
||||
"**Designing Interactive Experience**\nI'm focusing on crafting a fun, tactile, and interactive visual toy.",
|
||||
"**Analyzing Reading",
|
||||
].join("\n\n")
|
||||
|
||||
expect(reasoningHeading(text)).toBe("Designing Interactive Experience")
|
||||
})
|
||||
|
||||
test("returns the latest heading across markdown heading styles and ignores inline bold spans", () => {
|
||||
expect(reasoningHeading("## First step\n\nDetails.\n\n## Second step\n\nMore details.")).toBe("Second step")
|
||||
expect(reasoningHeading("<h2>First step</h2>\n<p>Body</p>\n<h3>Second step</h3>")).toBe("Second step")
|
||||
expect(reasoningHeading("First step\n===\n\nSecond step\n---")).toBe("Second step")
|
||||
expect(
|
||||
reasoningHeading("**Designing Interactive Experience**\n\n**Note** that this line ends with **bold**"),
|
||||
).toBe("Designing Interactive Experience")
|
||||
})
|
||||
})
|
||||
|
||||
@@ -869,39 +869,32 @@ function toolGroupType(
|
||||
|
||||
export function reasoningHeading(text: string): string | undefined {
|
||||
const markdown = text.replace(/\r\n?/g, "\n")
|
||||
const html = markdown.match(/<h[1-6][^>]*>([\s\S]*?)<\/h[1-6]>/i)
|
||||
const html = lastHeading(markdown, /<h[1-6][^>]*>([\s\S]*?)<\/h[1-6]>/gi, (value) => value.replace(/<[^>]+>/g, " "))
|
||||
const atx = lastHeading(markdown, /^\s{0,3}#{1,6}[ \t]+(.+?)(?:[ \t]+#+[ \t]*)?$/gm)
|
||||
const setext = lastHeading(markdown, /^([^\n]+)\n(?:=+|-+)\s*$/gm)
|
||||
const strong = lastHeading(markdown, /^\s*(?:\*\*((?:(?!\*\*).)+)\*\*|__((?:(?!__).)+)__)\s*$/gm)
|
||||
|
||||
if (html?.[1]) {
|
||||
const value = cleanHeading(html[1].replace(/<[^>]+>/g, " "))
|
||||
const latest = [html, atx, strong].reduce<HeadingMatch | undefined>(
|
||||
(best, current) => (!best || (current && current.index >= best.index) ? current : best),
|
||||
undefined,
|
||||
)
|
||||
|
||||
if (value) return value
|
||||
}
|
||||
return (latest ?? setext)?.value
|
||||
}
|
||||
|
||||
const atx = markdown.match(/^\s{0,3}#{1,6}[ \t]+(.+?)(?:[ \t]+#+[ \t]*)?$/m)
|
||||
type HeadingMatch = { index: number; value: string }
|
||||
|
||||
if (atx?.[1]) {
|
||||
const value = cleanHeading(atx[1])
|
||||
function lastHeading(
|
||||
markdown: string,
|
||||
pattern: RegExp,
|
||||
transform?: (value: string) => string,
|
||||
): HeadingMatch | undefined {
|
||||
return Array.from(markdown.matchAll(pattern)).reduce<HeadingMatch | undefined>((best, match) => {
|
||||
const raw = match[1] ?? match[2] ?? ""
|
||||
const value = cleanHeading(transform ? transform(raw) : raw)
|
||||
|
||||
if (value) return value
|
||||
}
|
||||
|
||||
const setext = markdown.match(/^([^\n]+)\n(?:=+|-+)\s*$/m)
|
||||
|
||||
if (setext?.[1]) {
|
||||
const value = cleanHeading(setext[1])
|
||||
|
||||
if (value) return value
|
||||
}
|
||||
|
||||
const strong = markdown.match(/^\s*(?:\*\*|__)(.+?)(?:\*\*|__)\s*$/m)
|
||||
|
||||
if (strong?.[1]) {
|
||||
const value = cleanHeading(strong[1])
|
||||
|
||||
if (value) return value
|
||||
}
|
||||
|
||||
return undefined
|
||||
return value ? { index: match.index, value } : best
|
||||
}, undefined)
|
||||
}
|
||||
|
||||
function cleanHeading(value: string) {
|
||||
|
||||
Reference in New Issue
Block a user