Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
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-highlight.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"kilo-code": minor
---

Hovering or focusing a bar in the task timeline now highlights the matching tool call in the transcript, making it easier to see which bar belongs to which tool.
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ import { useServer } from "../../context/server"
import { snapshotProgress } from "../../context/session-utils"
import { planDisplayPath } from "../../utils/plan-path"
import { MemoryMarkerMeta } from "@kilocode/kilo-memory/marker-meta"
import { color as timelineColor } from "../../utils/timeline/colors"
import type { Part as TimelinePart } from "../../types/messages"
import type { TimelineHighlight } from "../../utils/timeline/highlight"
import { QuestionDock } from "./QuestionDock"
import { SuggestBar } from "./SuggestBar"

Expand Down Expand Up @@ -125,6 +128,8 @@ interface AssistantMessageProps {
parts?: SDKPart[]
showAssistantCopyPartID?: string | null
feedback?: MessageFeedbackControls
/** Part behind the currently hovered/focused task-timeline bar, if any. */
highlight?: () => TimelineHighlight | undefined
}

type ToolStateProps = {
Expand Down Expand Up @@ -280,6 +285,13 @@ export const AssistantMessage: Component<AssistantMessageProps> = (props) => {
return part as unknown as ToolPart
})

// Lights up when this part is behind the hovered/focused task-timeline
// bar, using that bar's own color so the two stay easy to correlate.
const highlighted = createMemo(() => {
const h = props.highlight?.()
return h?.msgId === props.message.id && h?.partId === part.id
})

return (
<Show
when={
Expand All @@ -291,7 +303,14 @@ export const AssistantMessage: Component<AssistantMessageProps> = (props) => {
PART_MAPPING[part.type]
}
>
<div data-component="tool-part-wrapper" data-part-type={part.type}>
<div
data-component="tool-part-wrapper"
data-part-type={part.type}
data-timeline-highlight={highlighted() ? "" : undefined}
style={
highlighted() ? { "--timeline-color": timelineColor(part as unknown as TimelinePart) } : undefined
}
>
<Show
when={activeQuestion()}
fallback={
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ import {
type TranscriptHold,
type TranscriptRow,
} from "../../context/transcript-rows"
import { onTimelineHighlight, type TimelineHighlight } from "../../utils/timeline/highlight"
import type { QuestionRequest, SuggestionRequest } from "../../types/messages"

interface MessageListProps {
Expand Down Expand Up @@ -183,6 +184,11 @@ export const MessageList: Component<MessageListProps> = (props) => {
window.addEventListener("scrollToMessage", onScrollToMessage)
onCleanup(() => window.removeEventListener("scrollToMessage", onScrollToMessage))

// Highlights the part behind the currently hovered/focused timeline bar
// (dispatched by TaskTimeline) so the two stay visually correlated.
const [highlight, setHighlight] = createSignal<TimelineHighlight>()
onCleanup(onTimelineHighlight(setHighlight))

const measurement = createMemo(() => {
const id = session.currentSessionID()
const token = layout()
Expand Down Expand Up @@ -371,12 +377,23 @@ export const MessageList: Component<MessageListProps> = (props) => {
itemSize={260}
>
{(row, index) => (
<TranscriptRowView row={row} index={index()} onForkMessage={props.onForkMessage} />
<TranscriptRowView
row={row}
index={index()}
onForkMessage={props.onForkMessage}
highlight={highlight}
/>
)}
</Virtualizer>
</Show>
<For each={tail()}>
{(key) => <TranscriptRowView row={lookup().get(key)!} onForkMessage={props.onForkMessage} />}
{(key) => (
<TranscriptRowView
row={lookup().get(key)!}
onForkMessage={props.onForkMessage}
highlight={highlight}
/>
)}
</For>
</div>
</Show>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { Portal } from "solid-js/web"
import { useSession } from "../../context/session"
import { color, label } from "../../utils/timeline/colors"
import { geometry, hit, navigate } from "../../utils/timeline/geometry"
import { dispatchTimelineHighlight } from "../../utils/timeline/highlight"
import { sizes, pinned, MAX_HEIGHT } from "../../utils/timeline/sizes"
import type { Part, Message } from "../../types/messages"

Expand Down Expand Up @@ -116,6 +117,15 @@ export const TaskTimeline: Component = () => {

createEffect(on(bars, hideTip, { defer: true }))

// Highlight the chat part behind the hovered/focused bar, using its own
// color, so it's easy to follow which bar belongs to which tool call.
createEffect(() => {
const idx = hover()
const bar = idx >= 0 ? bars()[idx] : undefined
dispatchTimelineHighlight(bar ? { msgId: bar.msgId, partId: bar.partId } : undefined)
})
onCleanup(() => dispatchTimelineHighlight(undefined))

const showTip = (idx: number) => {
const item = layout().items[idx]
const bar = bars()[idx]
Expand Down Expand Up @@ -174,7 +184,12 @@ export const TaskTimeline: Component = () => {
if (ref.hasPointerCapture(e.pointerId)) ref.releasePointerCapture(e.pointerId)
ref.style.cursor = "grab"
ref.style.userSelect = ""
if (wasDragging && !dragMoved) jumpToMessage(pointerIndex(e))
if (!wasDragging || dragMoved) return
const idx = pointerIndex(e)
jumpToMessage(idx)
// onPointerDown hid the tip pre-emptively in case this turned into a
// drag; restore it for the clicked bar since the pointer is still on it.
showTip(idx)
}

const onWheel = (e: WheelEvent) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Icon } from "@kilocode/kilo-ui/icon"
import { useI18n } from "@kilocode/kilo-ui/context/i18n"
import type { AssistantMessage as SDKAssistantMessage, Part as SDKPart, SnapshotFileDiff } from "@kilocode/sdk/v2"
import type { TranscriptRow } from "../../context/transcript-rows"
import type { TimelineHighlight } from "../../utils/timeline/highlight"
import { useSession } from "../../context/session"
import { useServer } from "../../context/server"
import { useLanguage } from "../../context/language"
Expand All @@ -17,6 +18,8 @@ interface TranscriptRowViewProps {
row: TranscriptRow
index?: number
onForkMessage?: (sessionId: string, messageId: string) => void
/** Part behind the currently hovered/focused task-timeline bar, if any. */
highlight?: () => TimelineHighlight | undefined
}

export const TranscriptRowView: Component<TranscriptRowViewProps> = (props) => {
Expand Down Expand Up @@ -76,6 +79,7 @@ export const TranscriptRowView: Component<TranscriptRowViewProps> = (props) => {
message={row().message as unknown as SDKAssistantMessage}
parts={row().parts as unknown as SDKPart[]}
showAssistantCopyPartID={row().copy}
highlight={props.highlight}
feedback={{
enabled: feedback.telemetryEnabled(),
rating: feedback.getRating(row().message.id),
Expand Down
45 changes: 45 additions & 0 deletions packages/kilo-vscode/webview-ui/src/styles/chat-layout.css
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,11 @@
flex-direction: column;
gap: 6px;
width: 100%;
/* Reserves extra room, on top of the turn's own 4px padding, for the
task-timeline highlight strip (see [data-component="tool-part-wrapper"]
below) to sit further from the card edge with a visible gap. Scoped to
assistant content only so user bubbles/diffs stay unaffected. */
padding-left: 4px;
}

[data-component="assistant-memory-badge"] {
Expand All @@ -251,6 +256,46 @@
margin-inline: auto;
}

/* Lights up the part behind a hovered/focused task-timeline bar, using the
bar's own color, so it's easy to follow which bar belongs to which tool
call (mirrors the legacy extension's task-timeline row gutter highlight).
Drawn as an absolutely positioned strip flush with the card's left edge,
not an inset box-shadow: a box-shadow paints as part of the wrapper's own
background layer, underneath every child, so tool cards with a negative-
margin background (bleeding past this edge) would otherwise cover it.
An absolutely positioned element always paints above normal-flow
(position: static) children, so it stays visible without needing to
extend past the wrapper's own box — virtua's row virtualizer clips its
content to exactly that box (`overflow: clip`), so anything drawn outside
it (e.g. a negative left offset) gets clipped entirely once rows are
virtualized. */
[data-component="tool-part-wrapper"] {
position: relative;
}

[data-component="tool-part-wrapper"]::before {
content: "";
position: absolute;
top: 2px;
bottom: 2px;
/* .vscode-session-turn-assistant's extra 4px padding-left (see above) plus
the turn's own 4px gives 8px of gutter here before virtua's clip edge.
Sit 1px inside that edge for safety, leaving a clear gap before the card
(which starts at 0). */
left: -7px;
width: 3px;
border-radius: 2px 0 0 2px;
background: var(--timeline-color, transparent);
opacity: 0;
z-index: 1;
transition: opacity 0.15s ease;
pointer-events: none;
}

[data-component="tool-part-wrapper"][data-timeline-highlight]::before {
opacity: 0.9;
}

.chat-view .message-list-content > .revert-banner,
.chat-view .message-list-content > [data-component="question-dock"],
.chat-view .message-list-content > .working-indicator-slot,
Expand Down
27 changes: 27 additions & 0 deletions packages/kilo-vscode/webview-ui/src/utils/timeline/highlight.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/**
* Cross-component signal correlating a hovered/selected task-timeline bar with
* the chat part it represents. TaskTimeline dispatches on hover/keyboard-nav
* change (no direct props/context link to the transcript, same convention as
* the `scrollToMessage` and `resumeAutoScroll` window events); AssistantMessage
* listens and highlights the matching part using the bar's own color, so users
* can visually follow which bar belongs to which tool call — mirroring the
* legacy extension's task-timeline row gutter highlight.
*/

export interface TimelineHighlight {
msgId: string
partId: string
}

const EVENT = "timelineHighlight"

export function dispatchTimelineHighlight(value: TimelineHighlight | undefined) {
window.dispatchEvent(new CustomEvent<TimelineHighlight | undefined>(EVENT, { detail: value }))
}

/** Registers a listener and returns an unregister function for onCleanup. */
export function onTimelineHighlight(handler: (value: TimelineHighlight | undefined) => void) {
const listener = (e: Event) => handler((e as CustomEvent<TimelineHighlight | undefined>).detail)
window.addEventListener(EVENT, listener)
return () => window.removeEventListener(EVENT, listener)
}
Loading