fix(app): stabilize virtual session timeline interactions (#28422)

This commit is contained in:
Luke Parker
2026-05-25 12:05:27 +10:00
committed by GitHub
parent 9495ecd536
commit f023c63a60
14 changed files with 1062 additions and 177 deletions
+46 -11
View File
@@ -175,7 +175,10 @@ export interface MessagePartProps {
message: MessageType
hideDetails?: boolean
defaultOpen?: boolean
toolOpen?: boolean
onToolOpenChange?: (open: boolean) => void
deferToolContent?: boolean
virtualizeDiff?: boolean
showAssistantCopyPartID?: string | null
turnDurationMs?: number
}
@@ -290,7 +293,7 @@ function getDirectory(path: string | undefined) {
}
import type { IconProps } from "./icon"
import { normalize } from "./session-diff"
import { normalize, resolveFileDiff } from "./session-diff"
export type ToolInfo = {
icon: IconProps["name"]
@@ -930,7 +933,7 @@ export function AssistantMessageDisplay(props: {
)
}
export function ContextToolGroup(props: { parts: ToolPart[]; busy?: boolean }) {
export function ContextToolGroup(props: { parts: ToolPart[]; busy?: boolean; onSizeChange?: () => void }) {
const i18n = useI18n()
const [open, setOpen] = createSignal(false)
const pending = createMemo(
@@ -938,11 +941,15 @@ export function ContextToolGroup(props: { parts: ToolPart[]; busy?: boolean }) {
!!props.busy || props.parts.some((part) => part.state.status === "pending" || part.state.status === "running"),
)
const summary = createMemo(() => contextToolSummary(props.parts))
const handleOpenChange = (value: boolean) => {
setOpen(value)
props.onSizeChange?.()
}
return (
<Collapsible
open={open()}
onOpenChange={setOpen}
onOpenChange={handleOpenChange}
variant="ghost"
class="tool-collapsible"
data-timeline-part-ids={props.parts.map((part) => part.id).join(",")}
@@ -1261,7 +1268,10 @@ export function Part(props: MessagePartProps) {
message={props.message}
hideDetails={props.hideDetails}
defaultOpen={props.defaultOpen}
toolOpen={props.toolOpen}
onToolOpenChange={props.onToolOpenChange}
deferToolContent={props.deferToolContent}
virtualizeDiff={props.virtualizeDiff}
showAssistantCopyPartID={props.showAssistantCopyPartID}
turnDurationMs={props.turnDurationMs}
/>
@@ -1278,7 +1288,10 @@ export interface ToolProps {
status?: string
hideDetails?: boolean
defaultOpen?: boolean
open?: boolean
onOpenChange?: (open: boolean) => void
deferContent?: boolean
virtualizeDiff?: boolean
forceOpen?: boolean
locked?: boolean
}
@@ -1376,6 +1389,8 @@ PART_MAPPING["tool"] = function ToolPartDisplay(props) {
})
const render = createMemo(() => ToolRegistry.render(part().tool) ?? GenericTool)
const controlledOpen = () => (props.onToolOpenChange ? (props.toolOpen ?? props.defaultOpen) : undefined)
const handleToolOpenChange = (open: boolean) => props.onToolOpenChange?.(open)
return (
<Show when={!hideQuestion()}>
@@ -1399,6 +1414,8 @@ PART_MAPPING["tool"] = function ToolPartDisplay(props) {
error={error()}
title={part().tool === "websearch" ? webSearchProviderLabel(partMetadata().provider) : undefined}
defaultOpen={props.defaultOpen}
open={controlledOpen()}
onOpenChange={props.onToolOpenChange ? handleToolOpenChange : undefined}
subtitle={taskSubtitle()}
href={taskHref()}
/>
@@ -1417,7 +1434,10 @@ PART_MAPPING["tool"] = function ToolPartDisplay(props) {
status={part().state.status}
hideDetails={props.hideDetails}
defaultOpen={props.defaultOpen}
open={controlledOpen()}
onOpenChange={props.onToolOpenChange ? handleToolOpenChange : undefined}
deferContent={props.deferToolContent}
virtualizeDiff={props.virtualizeDiff}
/>
</Match>
</Switch>
@@ -1921,15 +1941,29 @@ ToolRegistry.register({
const path = createMemo(() => props.metadata?.filediff?.file || props.input.filePath || "")
const filename = () => getFilename(props.input.filePath ?? "")
const pending = () => props.status === "pending" || props.status === "running"
const diffSource = createMemo(
() => {
const filediff = props.metadata?.filediff
if (!filediff) return
return {
file: filediff.file || props.input.filePath || "",
patch: typeof filediff.patch === "string" ? filediff.patch : undefined,
before: typeof filediff.before === "string" ? filediff.before : undefined,
after: typeof filediff.after === "string" ? filediff.after : undefined,
}
},
undefined,
{
equals: (a, b) =>
a?.file === b?.file && a?.patch === b?.patch && a?.before === b?.before && a?.after === b?.after,
},
)
const fileCompProps = createMemo(() => {
try {
if (props.metadata?.filediff) {
const diff = normalize({
...props.metadata?.filediff,
status: "modified",
})
const fileDiff = diff.fileDiff
const source = diffSource()
if (source) {
const fileDiff = resolveFileDiff(source)
if (fileDiff) return { fileDiff, hunkSeparators: fileDiff.isPartial ? "simple" : "line-info-basic" }
}
} catch {}
@@ -1987,7 +2021,7 @@ ToolRegistry.register({
}
>
<div data-component="edit-content">
<Dynamic component={fileComponent} mode="diff" {...fileCompProps()} />
<Dynamic component={fileComponent} mode="diff" virtualize={props.virtualizeDiff} {...fileCompProps()} />
</div>
</ToolFileAccordion>
</Show>
@@ -2171,6 +2205,7 @@ ToolRegistry.register({
<Dynamic
component={fileComponent}
mode="diff"
virtualize={props.virtualizeDiff}
fileDiff={file.view.fileDiff}
hunkSeparators={file.view.fileDiff.isPartial ? "simple" : "line-info-basic"}
/>
@@ -2243,7 +2278,7 @@ ToolRegistry.register({
}
>
<div data-component="apply-patch-file-diff">
<Dynamic component={fileComponent} mode="diff" fileDiff={single()!.view.fileDiff} />
<Dynamic component={fileComponent} mode="diff" virtualize={props.virtualizeDiff} fileDiff={single()!.view.fileDiff} />
</div>
</ToolFileAccordion>
</BasicTool>