mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-08-28 11:05:31 +08:00
fix(vscode): preserve timeline highlight while streaming
This commit is contained in:
@@ -49,4 +49,9 @@ describe("TaskTimeline delegated tooltip contract", () => {
|
||||
expect(src).toMatch(/const select = \(idx: number\) => \{[\s\S]*showTip\(idx\)/)
|
||||
expect(src).toMatch(/select\(selected\(\)\)/)
|
||||
})
|
||||
|
||||
it("preserves a hovered part across streaming updates", () => {
|
||||
expect(src).toMatch(/if \(idx < 0 \|\| same\(previous\?\.\[idx\], next\[idx\]\)\) return/)
|
||||
expect(src).toMatch(/if \(same\(previous, next\)\) return previous/)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -12,7 +12,7 @@ const SCRIPT = `
|
||||
globalThis.window = window
|
||||
globalThis.CustomEvent = window.CustomEvent
|
||||
|
||||
const { dispatchTimelineHighlight, onTimelineHighlight } = await import("./src/utils/timeline/highlight.ts")
|
||||
const { dispatchTimelineHighlight, onTimelineHighlight, same } = await import("./src/utils/timeline/highlight.ts")
|
||||
const values = []
|
||||
const dispose = onTimelineHighlight((value) => values.push(value))
|
||||
const value = { msgId: "message-1", partId: "part-1" }
|
||||
@@ -28,6 +28,9 @@ const SCRIPT = `
|
||||
if (values[0]?.msgId !== value.msgId || values[0]?.partId !== value.partId) {
|
||||
fail("listener received the wrong highlight")
|
||||
}
|
||||
if (!same(value, { ...value }) || same(value, { ...value, partId: "part-2" })) {
|
||||
fail("highlight identity comparison is incorrect")
|
||||
}
|
||||
console.log("${PASS}")
|
||||
`
|
||||
|
||||
|
||||
@@ -21,6 +21,16 @@ const SCRIPT = `
|
||||
{ id: "empty-text", type: "text", text: " " },
|
||||
{ id: "synthetic-text", type: "text", text: "Synthetic", synthetic: true },
|
||||
{ id: "visible-text", type: "text", text: "Visible transcript text" },
|
||||
{ id: "redacted-reasoning", type: "reasoning", text: "[REDACTED]" },
|
||||
{ id: "visible-reasoning", type: "reasoning", text: "Inspect the implementation" },
|
||||
{ id: "todo-pending", type: "tool", tool: "todowrite", state: { status: "pending", input: {} } },
|
||||
{
|
||||
id: "todo-completed",
|
||||
type: "tool",
|
||||
tool: "todowrite",
|
||||
state: { status: "completed", input: {}, output: "done", title: "Updated todos" },
|
||||
},
|
||||
{ id: "read-running", type: "tool", tool: "read", state: { status: "running", input: {} } },
|
||||
]
|
||||
const visible = parts.filter((part) => isRenderable(part, message)).map((part) => part.id)
|
||||
|
||||
@@ -28,7 +38,8 @@ const SCRIPT = `
|
||||
console.log("${FAIL}" + reason)
|
||||
process.exit(2)
|
||||
}
|
||||
if (visible.length !== 1 || visible[0] !== "visible-text") {
|
||||
const expected = ["visible-text", "visible-reasoning", "todo-completed", "read-running"]
|
||||
if (visible.length !== expected.length || visible.some((id, index) => id !== expected[index])) {
|
||||
fail("did not exclude transcript-invisible parts")
|
||||
}
|
||||
console.log("${PASS}")
|
||||
|
||||
@@ -11,7 +11,7 @@ import { useSession } from "../../context/session"
|
||||
import { visibleParts } from "../../context/session-queue"
|
||||
import { color, label } from "../../utils/timeline/colors"
|
||||
import { geometry, hit, navigate } from "../../utils/timeline/geometry"
|
||||
import { dispatchTimelineHighlight } from "../../utils/timeline/highlight"
|
||||
import { dispatchTimelineHighlight, same, type TimelineHighlight } from "../../utils/timeline/highlight"
|
||||
import { sizes, pinned, MAX_HEIGHT } from "../../utils/timeline/sizes"
|
||||
import { isRenderable } from "../../utils/transcript-parts"
|
||||
import type { Part, Message } from "../../types/messages"
|
||||
@@ -129,14 +129,27 @@ export const TaskTimeline: Component = () => {
|
||||
setTip(undefined)
|
||||
}
|
||||
|
||||
createEffect(on(bars, hideTip, { defer: true }))
|
||||
createEffect(
|
||||
on(
|
||||
bars,
|
||||
(next, previous) => {
|
||||
const idx = hover()
|
||||
if (idx < 0 || same(previous?.[idx], next[idx])) return
|
||||
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(() => {
|
||||
createEffect<TimelineHighlight | undefined>((previous) => {
|
||||
const idx = hover()
|
||||
const bar = idx >= 0 ? bars()[idx] : undefined
|
||||
dispatchTimelineHighlight(bar ? { msgId: bar.msgId, partId: bar.partId } : undefined)
|
||||
const next = bar ? { msgId: bar.msgId, partId: bar.partId } : undefined
|
||||
if (same(previous, next)) return previous
|
||||
dispatchTimelineHighlight(next)
|
||||
return next
|
||||
})
|
||||
onCleanup(() => dispatchTimelineHighlight(undefined))
|
||||
|
||||
@@ -265,7 +278,7 @@ export const TaskTimeline: Component = () => {
|
||||
role="slider"
|
||||
tabIndex={0}
|
||||
aria-label="Session activity timeline"
|
||||
aria-description="Use arrow keys to choose activity, then press Enter to open it in the transcript."
|
||||
aria-keyshortcuts="ArrowLeft ArrowRight Home End Enter Space"
|
||||
aria-valuemin={bars().length > 0 ? 1 : 0}
|
||||
aria-valuemax={bars().length}
|
||||
aria-valuenow={value()}
|
||||
|
||||
@@ -13,6 +13,10 @@ export interface TimelineHighlight {
|
||||
partId: string
|
||||
}
|
||||
|
||||
export function same(a: TimelineHighlight | undefined, b: TimelineHighlight | undefined) {
|
||||
return a?.msgId === b?.msgId && a?.partId === b?.partId
|
||||
}
|
||||
|
||||
const EVENT = "timelineHighlight"
|
||||
|
||||
export function dispatchTimelineHighlight(value: TimelineHighlight | undefined) {
|
||||
|
||||
@@ -4,7 +4,7 @@ import { snapshotProgress } from "../context/session-utils"
|
||||
|
||||
export const UPSTREAM_SUPPRESSED_TOOLS = new Set(["todowrite", "todoread"])
|
||||
|
||||
export function isRenderable(part: Part, message?: AssistantMessage): boolean {
|
||||
export function isRenderable(part: Part, message: AssistantMessage): boolean {
|
||||
if (part.type === "tool") {
|
||||
if (UPSTREAM_SUPPRESSED_TOOLS.has(part.tool)) {
|
||||
return part.state.status === "completed" && !!ToolRegistry.render(part.tool)
|
||||
|
||||
Reference in New Issue
Block a user