Merge pull request #11037 from Kilo-Org/perf-agent-manager-timeline-renderer

perf(vscode): coalesce task timeline performance optimization
This commit is contained in:
Marius
2026-06-09 16:25:23 +02:00
committed by GitHub
4 changed files with 43 additions and 5 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"kilo-code": patch
---
Keep task timelines in place while reviewing earlier activity across Kilo chat surfaces, and resume following updates from the latest bar.
@@ -1,5 +1,5 @@
import { describe, it, expect } from "vitest"
import { sizes, MAX_HEIGHT } from "../../webview-ui/src/utils/timeline/sizes"
import { sizes, pinned, MAX_HEIGHT } from "../../webview-ui/src/utils/timeline/sizes"
import type { Part, TextPart, ToolPart, StepFinishPart } from "../../webview-ui/src/types/messages"
function mkText(text: string): TextPart {
@@ -89,4 +89,10 @@ describe("timeline sizes", () => {
expect(Number.isInteger(bar.height)).toBe(true)
}
})
it("detects whether scrolling should follow new bars", () => {
expect(pinned({ scrollLeft: 100, scrollWidth: 200, clientWidth: 100 })).toBe(true)
expect(pinned({ scrollLeft: 88, scrollWidth: 200, clientWidth: 100 })).toBe(true)
expect(pinned({ scrollLeft: 87, scrollWidth: 200, clientWidth: 100 })).toBe(false)
})
})
@@ -16,7 +16,7 @@ import { Component, Index, Show, createMemo, createEffect, createSignal, on, onC
import { Portal } from "solid-js/web"
import { useSession } from "../../context/session"
import { color, label } from "../../utils/timeline/colors"
import { sizes, MAX_HEIGHT } from "../../utils/timeline/sizes"
import { sizes, pinned, MAX_HEIGHT } from "../../utils/timeline/sizes"
import type { Part, Message } from "../../types/messages"
export interface TimelineBar {
@@ -73,19 +73,34 @@ export const TaskTimeline: Component = () => {
const bars = createMemo(() => collect(messages(), allParts()))
const busy = () => session.status() === "busy"
// Auto-scroll to the latest bar when new bars appear
// Reading scrollWidth and writing scrollLeft synchronously for every appended bar can
// force repeated layout during streamed part updates. Batch those appends behind one
// animation frame, after Solid has applied the current DOM updates. Only follow while
// pinned so inspecting earlier activity is not interrupted by incoming bars.
let prev = 0
let frame: number | undefined
let follow = true
const onScroll = () => {
if (ref) follow = pinned(ref)
}
createEffect(
on(
() => bars().length,
(len) => {
if (len > prev && ref) {
ref.scrollLeft = ref.scrollWidth
if (len > prev && ref && follow && frame === undefined) {
frame = requestAnimationFrame(() => {
frame = undefined
if (!ref || !follow) return
ref.scrollLeft = ref.scrollWidth
})
}
prev = len
},
),
)
onCleanup(() => {
if (frame !== undefined) cancelAnimationFrame(frame)
})
const hideTip = () => {
tipBar = undefined
@@ -161,6 +176,7 @@ export const TaskTimeline: Component = () => {
onPointerUp={onPointerUp}
onPointerCancel={onPointerUp}
onPointerLeave={hideTip}
onScroll={onScroll}
>
<Index each={bars()}>
{(bar) => {
@@ -20,6 +20,17 @@ export interface BarSize {
content: number
}
export interface TimelineScroll {
scrollLeft: number
scrollWidth: number
clientWidth: number
}
/** Keep following updates when the viewport is within one bar of the right edge. */
export function pinned(scroll: TimelineScroll, slack = BAR_W): boolean {
return scroll.scrollWidth - scroll.clientWidth - scroll.scrollLeft <= slack
}
// ── Content length ───────────────────────────────────────────────────
function content(part: Part): number {