From 15d771e8fa2bb40294d0ea892ee8fcd73c66f215 Mon Sep 17 00:00:00 2001 From: marius-kilocode Date: Tue, 9 Jun 2026 11:59:17 +0200 Subject: [PATCH 1/3] perf(agent-manager): coalesce timeline auto-scroll --- .changeset/calm-timeline-follow.md | 5 +++++ .../tests/unit/timeline-sizes.test.ts | 8 ++++++- .../src/components/chat/TaskTimeline.tsx | 21 +++++++++++++++---- .../webview-ui/src/utils/timeline/sizes.ts | 10 +++++++++ 4 files changed, 39 insertions(+), 5 deletions(-) create mode 100644 .changeset/calm-timeline-follow.md diff --git a/.changeset/calm-timeline-follow.md b/.changeset/calm-timeline-follow.md new file mode 100644 index 00000000000..af52979da72 --- /dev/null +++ b/.changeset/calm-timeline-follow.md @@ -0,0 +1,5 @@ +--- +"kilo-code": patch +--- + +Keep the Agent Manager task timeline in place while reviewing earlier activity, and resume following updates from the latest bar. diff --git a/packages/kilo-vscode/tests/unit/timeline-sizes.test.ts b/packages/kilo-vscode/tests/unit/timeline-sizes.test.ts index 4194e7a81ca..5ff2006c0c6 100644 --- a/packages/kilo-vscode/tests/unit/timeline-sizes.test.ts +++ b/packages/kilo-vscode/tests/unit/timeline-sizes.test.ts @@ -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) + }) }) diff --git a/packages/kilo-vscode/webview-ui/src/components/chat/TaskTimeline.tsx b/packages/kilo-vscode/webview-ui/src/components/chat/TaskTimeline.tsx index 1c791cbb6fb..0a498ea2670 100644 --- a/packages/kilo-vscode/webview-ui/src/components/chat/TaskTimeline.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/chat/TaskTimeline.tsx @@ -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,31 @@ 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 + // Auto-scroll to the latest bar when new bars appear while preserving manual scroll position. 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 +173,7 @@ export const TaskTimeline: Component = () => { onPointerUp={onPointerUp} onPointerCancel={onPointerUp} onPointerLeave={hideTip} + onScroll={onScroll} > {(bar) => { diff --git a/packages/kilo-vscode/webview-ui/src/utils/timeline/sizes.ts b/packages/kilo-vscode/webview-ui/src/utils/timeline/sizes.ts index 8448b2dc738..995b1065065 100644 --- a/packages/kilo-vscode/webview-ui/src/utils/timeline/sizes.ts +++ b/packages/kilo-vscode/webview-ui/src/utils/timeline/sizes.ts @@ -20,6 +20,16 @@ export interface BarSize { content: number } +export interface TimelineScroll { + scrollLeft: number + scrollWidth: number + clientWidth: number +} + +export function pinned(scroll: TimelineScroll, slack = BAR_W): boolean { + return scroll.scrollWidth - scroll.clientWidth - scroll.scrollLeft <= slack +} + // ── Content length ─────────────────────────────────────────────────── function content(part: Part): number { From 246ba995a71766adf9d110ba72cd54560847a226 Mon Sep 17 00:00:00 2001 From: marius-kilocode Date: Tue, 9 Jun 2026 12:00:33 +0200 Subject: [PATCH 2/3] docs(agent-manager): explain timeline scroll batching --- .../webview-ui/src/components/chat/TaskTimeline.tsx | 5 ++++- packages/kilo-vscode/webview-ui/src/utils/timeline/sizes.ts | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/kilo-vscode/webview-ui/src/components/chat/TaskTimeline.tsx b/packages/kilo-vscode/webview-ui/src/components/chat/TaskTimeline.tsx index 0a498ea2670..3f4efb1db5b 100644 --- a/packages/kilo-vscode/webview-ui/src/components/chat/TaskTimeline.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/chat/TaskTimeline.tsx @@ -73,7 +73,10 @@ 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 while preserving manual scroll position. + // 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 diff --git a/packages/kilo-vscode/webview-ui/src/utils/timeline/sizes.ts b/packages/kilo-vscode/webview-ui/src/utils/timeline/sizes.ts index 995b1065065..78bb05248c2 100644 --- a/packages/kilo-vscode/webview-ui/src/utils/timeline/sizes.ts +++ b/packages/kilo-vscode/webview-ui/src/utils/timeline/sizes.ts @@ -26,6 +26,7 @@ export interface TimelineScroll { 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 } From 1aabc41b13a8b6589b0c0e8bf9a335abe533bac5 Mon Sep 17 00:00:00 2001 From: marius-kilocode Date: Tue, 9 Jun 2026 12:02:04 +0200 Subject: [PATCH 3/3] docs(vscode): clarify shared timeline behavior --- .changeset/calm-timeline-follow.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/calm-timeline-follow.md b/.changeset/calm-timeline-follow.md index af52979da72..77c4d6ca811 100644 --- a/.changeset/calm-timeline-follow.md +++ b/.changeset/calm-timeline-follow.md @@ -2,4 +2,4 @@ "kilo-code": patch --- -Keep the Agent Manager task timeline in place while reviewing earlier activity, and resume following updates from the latest bar. +Keep task timelines in place while reviewing earlier activity across Kilo chat surfaces, and resume following updates from the latest bar.