feat(vscode): jump transcript to message on timeline bar click (#12025)

* feat(vscode): jump transcript to message on timeline bar click

* fix(vscode): jump to the exact chunk containing the clicked timeline part

---------

Co-authored-by: Sylwester Liljegren <sylwester.liljegren@softronic.se>
This commit is contained in:
sylwester-liljegren
2026-07-08 09:35:55 +02:00
committed by GitHub
co-authored by Sylwester Liljegren
parent 9845c67af6
commit 2d724f158b
3 changed files with 52 additions and 0 deletions
@@ -0,0 +1,5 @@
---
"kilo-code": minor
---
Click or press Enter/Space on a bar in the task timeline to jump the transcript to that message.
@@ -157,6 +157,32 @@ export const MessageList: Component<MessageListProps> = (props) => {
const lookup = createMemo(() => new Map(partition().direct.map((row) => [row.key, row])))
const keys = createMemo(() => partition().virtual.map((row) => row.key))
const fingerprint = createMemo(() => rowFingerprint(keys()))
// Clicking a bar in the task timeline scrolls the transcript to that message.
// Jumps land instantly (no smooth animation): while pinned at the bottom, a
// smooth scroll's initial frames sit within createAutoScroll's near-bottom
// threshold, which resumes auto-follow mid-animation and snaps back down.
const onScrollToMessage = (e: Event) => {
const detail = (e as CustomEvent<{ id: string; partId?: string }>).detail
if (!detail?.id) return
const matches = rows().filter((r) => r.type === "assistant" && r.message.id === detail.id)
// Long messages split into multiple rows (chunks); land on the chunk that
// actually contains the clicked part, not just the message's first chunk.
const row = matches.find((r) => r.type === "assistant" && r.parts.some((p) => p.id === detail.partId)) ?? matches[0]
if (!row) return
autoScroll.pause()
const index = keys().indexOf(row.key)
if (index >= 0) {
virtualizer()?.scrollToIndex(index, { align: "start" })
return
}
const el = scrollEl()
const target = el?.querySelector<HTMLElement>(`[data-row-key="${CSS.escape(row.key)}"]`)
target?.scrollIntoView({ block: "start" })
}
window.addEventListener("scrollToMessage", onScrollToMessage)
onCleanup(() => window.removeEventListener("scrollToMessage", onScrollToMessage))
const measurement = createMemo(() => {
const id = session.currentSessionID()
const token = layout()
@@ -17,6 +17,8 @@ export interface TimelineBar {
width: number
height: number
idx: number
msgId: string
partId: string
}
function collect(messages: Message[], parts: Record<string, Part[]>): TimelineBar[] {
@@ -39,6 +41,8 @@ function collect(messages: Message[], parts: Record<string, Part[]>): TimelineBa
width: sz[i]!.width,
height: sz[i]!.height,
idx: i,
msgId: item.msg.id,
partId: item.part.id,
}))
}
@@ -46,6 +50,7 @@ export const TaskTimeline: Component = () => {
const session = useSession()
let ref: HTMLDivElement | undefined
let dragging = false
let dragMoved = false
let startX = 0
let startScroll = 0
const [hover, setHover] = createSignal(-1)
@@ -135,6 +140,7 @@ export const TaskTimeline: Component = () => {
hideTip()
if (!ref) return
dragging = true
dragMoved = false
startX = e.clientX
startScroll = ref.scrollLeft
ref.setPointerCapture(e.pointerId)
@@ -142,6 +148,13 @@ export const TaskTimeline: Component = () => {
ref.style.userSelect = "none"
}
const jumpToMessage = (idx: number) => {
const bar = bars()[idx]
if (!bar) return
setActive(idx)
window.dispatchEvent(new CustomEvent("scrollToMessage", { detail: { id: bar.msgId, partId: bar.partId } }))
}
const onPointerMove = (e: PointerEvent) => {
if (!ref) return
if (!dragging) {
@@ -150,15 +163,18 @@ export const TaskTimeline: Component = () => {
if (idx < 0) return hideTip()
return showTip(idx)
}
if (Math.abs(e.clientX - startX) > 3) dragMoved = true
ref.scrollLeft = startScroll - (e.clientX - startX)
}
const onPointerUp = (e: PointerEvent) => {
if (!ref) return
const wasDragging = dragging
dragging = false
if (ref.hasPointerCapture(e.pointerId)) ref.releasePointerCapture(e.pointerId)
ref.style.cursor = "grab"
ref.style.userSelect = ""
if (wasDragging && !dragMoved) jumpToMessage(pointerIndex(e))
}
const onWheel = (e: WheelEvent) => {
@@ -169,6 +185,11 @@ export const TaskTimeline: Component = () => {
}
const onKeyDown = (e: KeyboardEvent) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault()
jumpToMessage(selected())
return
}
if (!ref || !["ArrowLeft", "ArrowRight", "Home", "End"].includes(e.key)) return
e.preventDefault()
const idx = navigate(selected(), bars().length, e.key)