mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-09-24 16:02:55 +08:00
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:
co-authored by
Sylwester Liljegren
parent
9845c67af6
commit
2d724f158b
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user