mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-09-19 01:51:21 +08:00
perf(vscode): share timeline tooltip and lazy-mount historical bash output
This commit is contained in:
@@ -2,4 +2,4 @@
|
||||
"kilo-code": patch
|
||||
---
|
||||
|
||||
Speed up Agent Manager switching for edit-heavy sessions by rendering collapsed historical tool details only when expanded.
|
||||
Speed up Agent Manager switching for long sessions by lazily mounting collapsed historical tool details and sharing timeline hover infrastructure across activity bars.
|
||||
|
||||
@@ -2189,6 +2189,11 @@ ToolRegistry.register({
|
||||
const subtitle = () => props.input.description ?? props.metadata.description
|
||||
const key = () => toolOpenKey(props)
|
||||
const [open, setOpen] = createSignal(readToolOpen(key(), props.defaultOpen ?? true) ?? true)
|
||||
const [mounted, setMounted] = createSignal(open())
|
||||
|
||||
createEffect(() => {
|
||||
if (open() || pending()) setMounted(true)
|
||||
})
|
||||
|
||||
// also apply processCarriageReturns for Windows CLI tools
|
||||
const cmd = createMemo(() => {
|
||||
@@ -2208,6 +2213,7 @@ ToolRegistry.register({
|
||||
{...props}
|
||||
icon="console"
|
||||
animated
|
||||
hasDetails
|
||||
defaultOpen={props.defaultOpen ?? true}
|
||||
onOpenChange={setOpen}
|
||||
allowPendingToggle
|
||||
@@ -2222,7 +2228,9 @@ ToolRegistry.register({
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<BashHighlightedOutput cmd={cmd()} output={out()} outputPath={props.metadata.outputPath} active={open()} />
|
||||
<Show when={mounted()}>
|
||||
<BashHighlightedOutput cmd={cmd()} output={out()} outputPath={props.metadata.outputPath} active={open()} />
|
||||
</Show>
|
||||
</BasicTool>
|
||||
)
|
||||
},
|
||||
|
||||
@@ -301,4 +301,13 @@ describe("Collapsed deferred tool details contract (source)", () => {
|
||||
expect(block).toContain("hasDetails")
|
||||
}
|
||||
})
|
||||
|
||||
it("lazy-mounts completed bash output and retains it after first expansion", () => {
|
||||
const block =
|
||||
message.match(/ToolRegistry\.register\(\{\s*name:\s*"bash"[\s\S]*?(?=ToolRegistry\.register\(|$)/)?.[0] ?? ""
|
||||
expect(block).toContain("const [mounted, setMounted] = createSignal(open())")
|
||||
expect(block).toMatch(/if \(open\(\) \|\| pending\(\)\) setMounted\(true\)/)
|
||||
expect(block).toContain("hasDetails")
|
||||
expect(block).toMatch(/<Show when=\{mounted\(\)\}>[\s\S]*?<BashHighlightedOutput/)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import { describe, expect, it } from "bun:test"
|
||||
import { readFileSync } from "node:fs"
|
||||
import { join } from "node:path"
|
||||
|
||||
/**
|
||||
* Regression guard for timeline tooltip mount cost.
|
||||
*
|
||||
* A long session can render hundreds of timeline bars. Wrapping every bar in
|
||||
* the shared Tooltip component creates a Kobalte tooltip instance and
|
||||
* MutationObserver per bar during session activation. The timeline keeps all
|
||||
* bars but delegates hover handling to one portal tooltip instead.
|
||||
*/
|
||||
describe("TaskTimeline delegated tooltip contract", () => {
|
||||
const path = join(__dirname, "..", "..", "webview-ui", "src", "components", "chat", "TaskTimeline.tsx")
|
||||
const src = readFileSync(path, "utf8")
|
||||
|
||||
it("does not mount one shared Tooltip component per timeline bar", () => {
|
||||
expect(src).not.toMatch(/@kilocode\/kilo-ui\/tooltip/)
|
||||
expect(src).not.toMatch(/<Tooltip\b/)
|
||||
})
|
||||
|
||||
it("keeps bar labels and renders one delegated portal tooltip", () => {
|
||||
expect(src).toMatch(/data-tip=\{bar\(\)\.tip\}/)
|
||||
expect(src).toMatch(/role="img"/)
|
||||
expect(src).toMatch(/aria-label=\{bar\(\)\.tip\}/)
|
||||
expect(src).toMatch(/if \(!bar \|\| !ref\?\.contains\(bar\)\) return hideTip\(\)/)
|
||||
expect(src).toMatch(/<Portal>/)
|
||||
expect(src).toMatch(/class="task-timeline-tooltip"/)
|
||||
})
|
||||
})
|
||||
@@ -12,8 +12,8 @@
|
||||
* updates bindings in place (unlike React). Even 1000+ bars are fine.
|
||||
*/
|
||||
|
||||
import { Component, Index, createMemo, createEffect, on, onCleanup } from "solid-js"
|
||||
import { Tooltip } from "@kilocode/kilo-ui/tooltip"
|
||||
import { Component, Index, Show, createMemo, createEffect, createSignal, on, onCleanup } from "solid-js"
|
||||
import { Portal } from "solid-js/web"
|
||||
import { useSession } from "../../context/session"
|
||||
import { color, label } from "../../utils/timeline/colors"
|
||||
import { sizes, MAX_HEIGHT } from "../../utils/timeline/sizes"
|
||||
@@ -56,6 +56,8 @@ export const TaskTimeline: Component = () => {
|
||||
let dragging = false
|
||||
let startX = 0
|
||||
let startScroll = 0
|
||||
let tipBar: HTMLElement | undefined
|
||||
const [tip, setTip] = createSignal<{ text: string; x: number; y: number }>()
|
||||
|
||||
const messages = () => session.visibleMessages()
|
||||
const allParts = () => {
|
||||
@@ -85,8 +87,31 @@ export const TaskTimeline: Component = () => {
|
||||
),
|
||||
)
|
||||
|
||||
const hideTip = () => {
|
||||
tipBar = undefined
|
||||
setTip(undefined)
|
||||
}
|
||||
|
||||
createEffect(on(bars, hideTip, { defer: true }))
|
||||
|
||||
const showTip = (e: PointerEvent) => {
|
||||
if (dragging || !(e.target instanceof Element)) return
|
||||
const bar = e.target.closest<HTMLElement>(".task-timeline-bar")
|
||||
if (!bar || !ref?.contains(bar)) return hideTip()
|
||||
if (bar === tipBar) return
|
||||
const rect = bar.getBoundingClientRect()
|
||||
tipBar = bar
|
||||
const margin = Math.min(160, window.innerWidth / 2)
|
||||
setTip({
|
||||
text: bar.dataset.tip ?? "",
|
||||
x: Math.max(margin, Math.min(window.innerWidth - margin, rect.left + rect.width / 2)),
|
||||
y: rect.top,
|
||||
})
|
||||
}
|
||||
|
||||
// ── Drag scroll ──────────────────────────────────────────────────
|
||||
const onPointerDown = (e: PointerEvent) => {
|
||||
hideTip()
|
||||
if (!ref) return
|
||||
dragging = true
|
||||
startX = e.clientX
|
||||
@@ -97,7 +122,7 @@ export const TaskTimeline: Component = () => {
|
||||
}
|
||||
|
||||
const onPointerMove = (e: PointerEvent) => {
|
||||
if (!dragging || !ref) return
|
||||
if (!dragging || !ref) return showTip(e)
|
||||
ref.scrollLeft = startScroll - (e.clientX - startX)
|
||||
}
|
||||
|
||||
@@ -111,6 +136,7 @@ export const TaskTimeline: Component = () => {
|
||||
|
||||
// ── Wheel → horizontal scroll ────────────────────────────────────
|
||||
const onWheel = (e: WheelEvent) => {
|
||||
hideTip()
|
||||
if (!ref) return
|
||||
e.preventDefault()
|
||||
ref.scrollLeft += e.deltaY || e.deltaX
|
||||
@@ -124,23 +150,27 @@ export const TaskTimeline: Component = () => {
|
||||
})
|
||||
|
||||
return (
|
||||
<div class="task-timeline-outer">
|
||||
<div
|
||||
ref={ref}
|
||||
class="task-timeline"
|
||||
style={{ height: `${MAX_HEIGHT}px` }}
|
||||
onPointerDown={onPointerDown}
|
||||
onPointerMove={onPointerMove}
|
||||
onPointerUp={onPointerUp}
|
||||
onPointerCancel={onPointerUp}
|
||||
>
|
||||
<Index each={bars()}>
|
||||
{(bar) => {
|
||||
const active = () => busy() && bar().idx === bars().length - 1
|
||||
return (
|
||||
<Tooltip value={bar().tip} placement="top">
|
||||
<>
|
||||
<div class="task-timeline-outer">
|
||||
<div
|
||||
ref={ref}
|
||||
class="task-timeline"
|
||||
style={{ height: `${MAX_HEIGHT}px` }}
|
||||
onPointerDown={onPointerDown}
|
||||
onPointerMove={onPointerMove}
|
||||
onPointerUp={onPointerUp}
|
||||
onPointerCancel={onPointerUp}
|
||||
onPointerLeave={hideTip}
|
||||
>
|
||||
<Index each={bars()}>
|
||||
{(bar) => {
|
||||
const active = () => busy() && bar().idx === bars().length - 1
|
||||
return (
|
||||
<div
|
||||
class="task-timeline-bar"
|
||||
data-tip={bar().tip}
|
||||
role="img"
|
||||
aria-label={bar().tip}
|
||||
style={{
|
||||
width: `${bar().width}px`,
|
||||
height: `${MAX_HEIGHT}px`,
|
||||
@@ -157,11 +187,25 @@ export const TaskTimeline: Component = () => {
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Tooltip>
|
||||
)
|
||||
}}
|
||||
</Index>
|
||||
)
|
||||
}}
|
||||
</Index>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Show when={tip()}>
|
||||
{(current) => (
|
||||
<Portal>
|
||||
<div
|
||||
data-component="tooltip"
|
||||
class="task-timeline-tooltip"
|
||||
role="tooltip"
|
||||
style={{ left: `${current().x}px`, top: `${current().y}px` }}
|
||||
>
|
||||
{current().text}
|
||||
</div>
|
||||
</Portal>
|
||||
)}
|
||||
</Show>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -164,6 +164,13 @@
|
||||
animation: timeline-pulse 2s ease-in-out infinite 0.5s;
|
||||
}
|
||||
|
||||
.task-timeline-tooltip {
|
||||
position: fixed;
|
||||
max-width: min(320px, calc(100vw - 16px));
|
||||
transform: translate(-50%, calc(-100% - 4px));
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
@keyframes timeline-fade-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
|
||||
Reference in New Issue
Block a user