-
Notifications
You must be signed in to change notification settings - Fork 3.1k
feat(vscode): reimplement task timeline graph header #8480
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
108 changes: 108 additions & 0 deletions
108
packages/kilo-vscode/tests/unit/timeline-colors.test.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,108 @@ | ||
| import { describe, it, expect } from "vitest" | ||
| import { color, palette, label } from "../../webview-ui/src/utils/timeline/colors" | ||
| import type { | ||
| Part, | ||
| ToolPart, | ||
| TextPart, | ||
| ReasoningPart, | ||
| StepStartPart, | ||
| StepFinishPart, | ||
| } from "../../webview-ui/src/types/messages" | ||
|
|
||
| function mkText(text = "hello"): TextPart { | ||
| return { id: "t1", type: "text", text } | ||
| } | ||
|
|
||
| function mkReasoning(text = "thinking..."): ReasoningPart { | ||
| return { id: "r1", type: "reasoning", text } | ||
| } | ||
|
|
||
| function mkTool(name: string, status: "pending" | "running" | "completed" | "error" = "completed"): ToolPart { | ||
| const base = { id: "tool1", type: "tool" as const, tool: name } | ||
| if (status === "pending") return { ...base, state: { status: "pending", input: {} } } | ||
| if (status === "running") return { ...base, state: { status: "running", input: {} } } | ||
| if (status === "error") return { ...base, state: { status: "error", input: {}, error: "fail" } } | ||
| return { ...base, state: { status: "completed", input: {}, output: "ok", title: name } } | ||
| } | ||
|
|
||
| function mkStepStart(): StepStartPart { | ||
| return { id: "ss1", type: "step-start" } | ||
| } | ||
|
|
||
| function mkStepFinish(): StepFinishPart { | ||
| return { id: "sf1", type: "step-finish", reason: "done" } | ||
| } | ||
|
|
||
| describe("timeline colors", () => { | ||
| it("classifies text parts as text color", () => { | ||
| expect(color(mkText())).toBe(palette.text) | ||
| }) | ||
|
|
||
| it("classifies reasoning parts as reasoning color", () => { | ||
| expect(color(mkReasoning())).toBe(palette.reasoning) | ||
| }) | ||
|
|
||
| it("classifies read tools as read color", () => { | ||
| expect(color(mkTool("read"))).toBe(palette.read) | ||
| expect(color(mkTool("glob"))).toBe(palette.read) | ||
| expect(color(mkTool("grep"))).toBe(palette.read) | ||
| expect(color(mkTool("ls"))).toBe(palette.read) | ||
| expect(color(mkTool("diagnostics"))).toBe(palette.read) | ||
| expect(color(mkTool("warpgrep"))).toBe(palette.read) | ||
| }) | ||
|
|
||
| it("classifies write tools as write color", () => { | ||
| expect(color(mkTool("edit"))).toBe(palette.write) | ||
| expect(color(mkTool("write"))).toBe(palette.write) | ||
| expect(color(mkTool("patch"))).toBe(palette.write) | ||
| expect(color(mkTool("multiedit"))).toBe(palette.write) | ||
| expect(color(mkTool("apply_patch"))).toBe(palette.write) | ||
| }) | ||
|
|
||
| it("classifies generic tools as tool color", () => { | ||
| expect(color(mkTool("bash"))).toBe(palette.tool) | ||
| expect(color(mkTool("task"))).toBe(palette.tool) | ||
| expect(color(mkTool("browser"))).toBe(palette.tool) | ||
| }) | ||
|
|
||
| it("classifies errored tools as error color", () => { | ||
| expect(color(mkTool("bash", "error"))).toBe(palette.error) | ||
| expect(color(mkTool("read", "error"))).toBe(palette.error) | ||
| }) | ||
|
|
||
| it("classifies step-start as step color", () => { | ||
| expect(color(mkStepStart())).toBe(palette.step) | ||
| }) | ||
|
|
||
| it("classifies step-finish as success color", () => { | ||
| expect(color(mkStepFinish())).toBe(palette.success) | ||
| }) | ||
|
|
||
| it("returns fallback for unknown part types", () => { | ||
| const weird = { id: "w1", type: "snapshot" } as unknown as Part | ||
| expect(color(weird)).toBe(palette.fallback) | ||
| }) | ||
| }) | ||
|
|
||
| describe("timeline labels", () => { | ||
| it("returns 'Text' for text parts", () => { | ||
| expect(label(mkText())).toBe("Text") | ||
| }) | ||
|
|
||
| it("returns 'Reasoning' for reasoning parts", () => { | ||
| expect(label(mkReasoning())).toBe("Reasoning") | ||
| }) | ||
|
|
||
| it("returns tool name for tool parts", () => { | ||
| expect(label(mkTool("bash"))).toBe("bash") | ||
| expect(label(mkTool("read"))).toBe("read") | ||
| }) | ||
|
|
||
| it("returns 'Step start' for step-start", () => { | ||
| expect(label(mkStepStart())).toBe("Step start") | ||
| }) | ||
|
|
||
| it("returns 'Step finish' for step-finish", () => { | ||
| expect(label(mkStepFinish())).toBe("Step finish") | ||
| }) | ||
| }) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,92 @@ | ||
| import { describe, it, expect } from "vitest" | ||
| import { sizes, MAX_HEIGHT } from "../../webview-ui/src/utils/timeline/sizes" | ||
| import type { Part, TextPart, ToolPart, StepFinishPart } from "../../webview-ui/src/types/messages" | ||
|
|
||
| function mkText(text: string): TextPart { | ||
| return { id: `t-${text.length}`, type: "text", text } | ||
| } | ||
|
|
||
| function mkTool(name: string, input: Record<string, unknown> = {}, output = ""): ToolPart { | ||
| return { | ||
| id: `tool-${name}`, | ||
| type: "tool", | ||
| tool: name, | ||
| state: { status: "completed", input, output, title: name }, | ||
| } | ||
| } | ||
|
|
||
| function mkStepFinish(input = 100, output = 50): StepFinishPart { | ||
| return { | ||
| id: "sf", | ||
| type: "step-finish", | ||
| reason: "done", | ||
| tokens: { input, output }, | ||
| } | ||
| } | ||
|
|
||
| describe("timeline sizes", () => { | ||
| it("returns empty array for empty input", () => { | ||
| expect(sizes([])).toEqual([]) | ||
| }) | ||
|
|
||
| it("returns one entry per part", () => { | ||
| const parts: Part[] = [mkText("a"), mkText("bb"), mkText("ccc")] | ||
| const result = sizes(parts) | ||
| expect(result).toHaveLength(3) | ||
| }) | ||
|
|
||
| it("all bars have uniform width", () => { | ||
| const parts: Part[] = [mkText("short"), mkText("a".repeat(500)), mkText("medium")] | ||
| const result = sizes(parts) | ||
| const w = result[0]!.width | ||
| for (const bar of result) { | ||
| expect(bar.width).toBe(w) | ||
| } | ||
| }) | ||
|
|
||
| it("height stays within bounds", () => { | ||
| const parts: Part[] = [mkText("short"), mkText("a".repeat(500)), mkText("medium length text")] | ||
| const result = sizes(parts) | ||
| for (const bar of result) { | ||
| expect(bar.height).toBeGreaterThanOrEqual(8) | ||
| expect(bar.height).toBeLessThanOrEqual(MAX_HEIGHT) | ||
| } | ||
| }) | ||
|
|
||
| it("larger content produces taller bars", () => { | ||
| const parts: Part[] = [mkText("x"), mkText("x".repeat(1000))] | ||
| const result = sizes(parts) | ||
| expect(result[1]!.height).toBeGreaterThan(result[0]!.height) | ||
| }) | ||
|
|
||
| it("handles tool parts with input/output content", () => { | ||
| const parts: Part[] = [ | ||
| mkTool("bash", { command: "ls" }, "file1\nfile2\nfile3"), | ||
| mkTool("read", { path: "README.md" }, "a".repeat(200)), | ||
| ] | ||
| const result = sizes(parts) | ||
| expect(result).toHaveLength(2) | ||
| expect(result[0]!.content).toBeGreaterThan(0) | ||
| expect(result[1]!.content).toBeGreaterThan(result[0]!.content) | ||
| }) | ||
|
|
||
| it("handles step-finish parts using token counts", () => { | ||
| const parts: Part[] = [mkStepFinish(1000, 500), mkStepFinish(100, 50)] | ||
| const result = sizes(parts) | ||
| expect(result).toHaveLength(2) | ||
| expect(result[0]!.content).toBeGreaterThan(result[1]!.content) | ||
| }) | ||
|
|
||
| it("handles single-part input without crashing", () => { | ||
| const result = sizes([mkText("only one")]) | ||
| expect(result).toHaveLength(1) | ||
| }) | ||
|
|
||
| it("returns integer values for height", () => { | ||
| const parts: Part[] = [mkText("a"), mkText("bb"), mkText("ccc"), mkText("dddd")] | ||
| const result = sizes(parts) | ||
| for (const bar of result) { | ||
| expect(Number.isInteger(bar.height)).toBe(true) | ||
| } | ||
| }) | ||
| }) |
81 changes: 81 additions & 0 deletions
81
packages/kilo-vscode/webview-ui/src/components/chat/ContextProgress.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,81 @@ | ||
| /** | ||
| * ContextProgress — three-segment progress bar showing context window usage. | ||
| * | ||
| * Segments: | ||
| * 1. Used tokens (foreground color, turns red when >= 50%) | ||
| * 2. Reserved for output (medium gray) | ||
| * 3. Available (transparent / background) | ||
| * | ||
| * Token counts flanking the bar: used on left, total on right. | ||
| */ | ||
|
|
||
| import { Component, createMemo, Show } from "solid-js" | ||
| import { Tooltip } from "@kilocode/kilo-ui/tooltip" | ||
| import { useSession } from "../../context/session" | ||
| import { useProvider } from "../../context/provider" | ||
|
|
||
| function fmt(n: number): string { | ||
| if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M` | ||
| if (n >= 1_000) return `${(n / 1_000).toFixed(1)}K` | ||
| return String(n) | ||
| } | ||
|
|
||
| export const ContextProgress: Component = () => { | ||
| const session = useSession() | ||
| const provider = useProvider() | ||
|
|
||
| const data = createMemo(() => { | ||
| const usage = session.contextUsage() | ||
| if (!usage || usage.tokens === 0) return undefined | ||
|
|
||
| const sel = session.selected() | ||
| const model = sel ? provider.findModel(sel) : undefined | ||
| const limit = model?.limit?.context ?? model?.contextLength ?? 0 | ||
| const output = model?.limit?.output ?? 0 | ||
|
|
||
| if (limit === 0) return undefined | ||
|
|
||
| const used = Math.min(usage.tokens, limit) | ||
| const reserved = Math.min(output, limit - used) | ||
| const available = Math.max(0, limit - used - reserved) | ||
|
|
||
| const pctUsed = (used / limit) * 100 | ||
| const pctReserved = (reserved / limit) * 100 | ||
| const pctAvail = (available / limit) * 100 | ||
|
|
||
| return { used, reserved, available, limit, pctUsed, pctReserved, pctAvail, output } | ||
| }) | ||
|
|
||
| const tip = createMemo(() => { | ||
| const d = data() | ||
| if (!d) return "" | ||
| const lines = [`${fmt(d.used)} / ${fmt(d.limit)} tokens used`] | ||
| if (d.output > 0) lines.push(`${fmt(d.output)} reserved for output`) | ||
| if (d.available > 0) lines.push(`${fmt(d.available)} available`) | ||
| return lines.join("\n") | ||
| }) | ||
|
|
||
| return ( | ||
| <Show when={data()}> | ||
| {(d) => ( | ||
| <div class="context-progress"> | ||
| <span class="context-progress-count">{fmt(d().used)}</span> | ||
| <Tooltip value={tip()} placement="top"> | ||
| <div class="context-progress-bar"> | ||
| <div | ||
| class="context-progress-used" | ||
| classList={{ "context-progress-used--hot": d().pctUsed >= 50 }} | ||
| style={{ width: `${d().pctUsed}%` }} | ||
| /> | ||
| <div class="context-progress-reserved" style={{ width: `${d().pctReserved}%` }} /> | ||
| <Show when={d().pctAvail > 0}> | ||
| <div class="context-progress-available" style={{ width: `${d().pctAvail}%` }} /> | ||
| </Show> | ||
| </div> | ||
| </Tooltip> | ||
| <span class="context-progress-count">{fmt(d().limit)}</span> | ||
| </div> | ||
| )} | ||
| </Show> | ||
| ) | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
WARNING: Timeline setting changes are not broadcast after initialization
showTaskTimelineis only sent here and when the webview explicitly requests it.handleUpdateSetting()and the reset-all-settings flow never callsendTimelineSetting(), so other open webviews — and the current webview after a reset — can keep showing a stale expanded/collapsed state even though the persisted VS Code setting changed.