Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/timeline-bar-jump-to-message.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"kilo-code": minor
---

Click or press Enter on a bar in the task timeline to jump the transcript to that message.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SUGGESTION: Changeset text omits the Space key

onKeyDown in TaskTimeline.tsx triggers the jump on both Enter and Space (e.key === " "), but this user-facing changeset only mentions Enter.

Suggested change
Click or press Enter on a bar in the task timeline to jump the transcript to that message.
Click or press Enter/Space on a bar in the task timeline to jump the transcript to that message.

Reply with @kilocode-bot fix it to have Kilo Code address this issue.

Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,29 @@ 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 id = (e as CustomEvent<{ id: string }>).detail?.id
if (!id) return
const row = rows().find((r) => r.type === "assistant" && r.message.id === id)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: Jump target loses granularity for long assistant messages

TaskTimeline.collect() creates one TimelineBar per non-step-start part, all sharing the same msgId for a given message. transcriptRows() (default chunk size of 8, no size override is passed here) splits a message's parts into multiple rows once it has more than 8 visible parts, each row keeping the same message.id but a different key. Since rows().find(...) returns only the first matching row, clicking any bar for a part beyond the first chunk of a long message (e.g. a turn with many tool calls) always scrolls to the top of that message instead of near the bar's actual part. This isn't wrong per the PR's stated "jump to that message" intent, but the per-part bars visually imply finer-grained navigation than what's delivered for longer turns. Worth considering matching against the containing chunk (e.g. by part id) rather than only the first row for that message id.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export interface TimelineBar {
width: number
height: number
idx: number
msgId: string
}

function collect(messages: Message[], parts: Record<string, Part[]>): TimelineBar[] {
Expand All @@ -39,13 +40,15 @@ function collect(messages: Message[], parts: Record<string, Part[]>): TimelineBa
width: sz[i]!.width,
height: sz[i]!.height,
idx: i,
msgId: item.msg.id,
}))
}

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)
Expand Down Expand Up @@ -135,13 +138,21 @@ export const TaskTimeline: Component = () => {
hideTip()
if (!ref) return
dragging = true
dragMoved = false
startX = e.clientX
startScroll = ref.scrollLeft
ref.setPointerCapture(e.pointerId)
ref.style.cursor = "grabbing"
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 } }))
}

const onPointerMove = (e: PointerEvent) => {
if (!ref) return
if (!dragging) {
Expand All @@ -150,15 +161,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) => {
Expand All @@ -169,6 +183,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)
Expand Down
Loading