mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-08-31 01:37:28 +08:00
fix(vscode): compact todo update cards (#9687)
* fix(vscode): compact todo update cards * chore: update kilo-vscode visual regression baselines --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"kilo-code": patch
|
||||
"@kilocode/cli": patch
|
||||
---
|
||||
|
||||
Show compact todo update cards when checking off items in long todo lists.
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:9b47f6adfc72399cb1535be3a69661b027eec3068880aba35821d5240e0c3492
|
||||
size 16739
|
||||
@@ -221,6 +221,20 @@ html[data-theme="kilo-vscode"] [data-component="bash-output"] {
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
[data-component="todos"] {
|
||||
[data-slot="message-part-todo-hidden"] {
|
||||
padding: 2px 0 2px 28px;
|
||||
color: var(--text-weaker);
|
||||
font-size: var(--font-size-small);
|
||||
line-height: var(--line-height-normal);
|
||||
}
|
||||
|
||||
[data-slot="message-part-todo-content"][data-changed="changed"] {
|
||||
color: var(--text-base);
|
||||
font-weight: var(--font-weight-medium);
|
||||
}
|
||||
}
|
||||
|
||||
/* User message copy button: collapse wrapper when idle, show on hover.
|
||||
* The wrapper must be tall enough to fully contain the button (20px)
|
||||
* so that hovering the button keeps [data-component="user-message"]
|
||||
|
||||
@@ -85,6 +85,17 @@ interface Diagnostic {
|
||||
severity?: number
|
||||
}
|
||||
|
||||
type TodoView = {
|
||||
mode?: "full" | "compact"
|
||||
todos?: TodoItem[]
|
||||
hiddenBefore?: number
|
||||
hiddenAfter?: number
|
||||
}
|
||||
|
||||
type TodoItem = Todo & {
|
||||
changed?: boolean
|
||||
}
|
||||
|
||||
function getDiagnostics(
|
||||
diagnosticsByFile: Record<string, Diagnostic[]> | undefined,
|
||||
filePath: string | undefined,
|
||||
@@ -2403,6 +2414,7 @@ ToolRegistry.register({
|
||||
name: "todowrite",
|
||||
render(props) {
|
||||
const i18n = useI18n()
|
||||
const view = createMemo(() => (isTodoView(props.metadata?.view) ? props.metadata.view : undefined))
|
||||
const todos = createMemo(() => {
|
||||
const meta = props.metadata?.todos
|
||||
if (Array.isArray(meta)) return meta
|
||||
@@ -2412,6 +2424,7 @@ ToolRegistry.register({
|
||||
|
||||
return []
|
||||
})
|
||||
const shown = createMemo(() => view()?.todos ?? todos())
|
||||
const pending = createMemo(() => busy(props.status))
|
||||
|
||||
const subtitle = createMemo(() => {
|
||||
@@ -2434,20 +2447,27 @@ ToolRegistry.register({
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Show when={todos().length}>
|
||||
<Show when={shown().length}>
|
||||
<div data-component="todos">
|
||||
<For each={todos()}>
|
||||
{(todo: Todo) => (
|
||||
<Show when={view()?.mode === "compact" && (view()?.hiddenBefore ?? 0) > 0}>
|
||||
<div data-slot="message-part-todo-hidden">{hiddenText("earlier", view()?.hiddenBefore ?? 0)}</div>
|
||||
</Show>
|
||||
<For each={shown()}>
|
||||
{(todo: TodoItem) => (
|
||||
<Checkbox readOnly checked={todo.status === "completed"}>
|
||||
<span
|
||||
data-slot="message-part-todo-content"
|
||||
data-completed={todo.status === "completed" ? "completed" : undefined}
|
||||
data-changed={todo.changed ? "changed" : undefined}
|
||||
>
|
||||
{todo.content}
|
||||
</span>
|
||||
</Checkbox>
|
||||
)}
|
||||
</For>
|
||||
<Show when={view()?.mode === "compact" && (view()?.hiddenAfter ?? 0) > 0}>
|
||||
<div data-slot="message-part-todo-hidden">{hiddenText("later", view()?.hiddenAfter ?? 0)}</div>
|
||||
</Show>
|
||||
</div>
|
||||
</Show>
|
||||
</BasicTool>
|
||||
@@ -2455,6 +2475,17 @@ ToolRegistry.register({
|
||||
},
|
||||
})
|
||||
|
||||
function isTodoView(value: unknown): value is TodoView {
|
||||
if (!value || typeof value !== "object") return false
|
||||
const view = value as TodoView
|
||||
return Array.isArray(view.todos)
|
||||
}
|
||||
|
||||
function hiddenText(dir: "earlier" | "later", count: number) {
|
||||
const noun = count === 1 ? "to-do" : "to-dos"
|
||||
return `${count} ${dir} ${noun} hidden`
|
||||
}
|
||||
|
||||
ToolRegistry.register({
|
||||
name: "question",
|
||||
render(props) {
|
||||
|
||||
@@ -338,6 +338,37 @@ const todoWriteDocsOverview: ToolPart = {
|
||||
},
|
||||
}
|
||||
|
||||
const compactTodos = docsTodos.map((todo, index) =>
|
||||
index < 5 ? { ...todo, status: "completed" } : { ...todo, status: index === 5 ? "pending" : todo.status },
|
||||
)
|
||||
const compactViewTodos = compactTodos.slice(3, 6).map((todo, index) => ({ ...todo, changed: index === 1 }))
|
||||
|
||||
const todoWriteCompactUpdate: ToolPart = {
|
||||
id: "part-todo-compact-001",
|
||||
sessionID: SESSION_ID,
|
||||
messageID: ASST_MSG_ID,
|
||||
type: "tool",
|
||||
callID: "call-todo-compact-001",
|
||||
tool: "todowrite",
|
||||
state: {
|
||||
status: "completed",
|
||||
input: { todos: compactTodos },
|
||||
output: "Updated 10 todos",
|
||||
title: "Todo List Updated",
|
||||
metadata: {
|
||||
todos: compactTodos,
|
||||
view: {
|
||||
mode: "compact",
|
||||
todos: compactViewTodos,
|
||||
hiddenBefore: 3,
|
||||
hiddenAfter: 4,
|
||||
changed: 1,
|
||||
},
|
||||
},
|
||||
time: { start: now - 3000, end: now - 2800 },
|
||||
},
|
||||
}
|
||||
|
||||
const todoWritePermission: PermissionRequest = {
|
||||
id: "perm-todo-001",
|
||||
sessionID: SESSION_ID,
|
||||
@@ -667,6 +698,18 @@ export const TodoWriteDocsOverview: Story = {
|
||||
},
|
||||
}
|
||||
|
||||
export const TodoWriteCompactUpdate: Story = {
|
||||
name: "TodoWrite - Compact update",
|
||||
render: () => {
|
||||
const data = dataWith([todoWriteCompactUpdate])
|
||||
return (
|
||||
<StoryProviders data={data} sessionID={SESSION_ID}>
|
||||
<AssistantMessage message={baseAssistantMessage} />
|
||||
</StoryProviders>
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 12. Permission dock — edit tool with file patterns
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
export namespace TodoView {
|
||||
export type Todo = {
|
||||
content: string
|
||||
status: string
|
||||
priority: string
|
||||
}
|
||||
|
||||
export type Item = Todo & {
|
||||
changed?: boolean
|
||||
}
|
||||
|
||||
export type Info = {
|
||||
mode: "full" | "compact"
|
||||
todos: Item[]
|
||||
hiddenBefore: number
|
||||
hiddenAfter: number
|
||||
changed: number
|
||||
}
|
||||
|
||||
export function calculate(before: Todo[], after: Todo[]): Info {
|
||||
const diff = after
|
||||
.map((todo, index) => ({
|
||||
index,
|
||||
changed: !same(before[index], todo),
|
||||
}))
|
||||
.filter((item) => item.changed)
|
||||
|
||||
const wide =
|
||||
before.length === 0 ||
|
||||
after.length === 0 ||
|
||||
structural(before, after) ||
|
||||
terminal(after) ||
|
||||
diff.length === 0
|
||||
if (wide) return full(after, diff.length)
|
||||
|
||||
const first = Math.max(0, Math.min(...diff.map((item) => item.index)) - 1)
|
||||
const last = Math.min(after.length - 1, Math.max(...diff.map((item) => item.index)) + 1)
|
||||
const hidden = first + after.length - last - 1
|
||||
if (hidden === 0) return full(after, diff.length)
|
||||
|
||||
const set = new Set(diff.map((item) => item.index))
|
||||
return {
|
||||
mode: "compact",
|
||||
todos: after.slice(first, last + 1).map((todo, index) => ({
|
||||
...todo,
|
||||
changed: set.has(first + index),
|
||||
})),
|
||||
hiddenBefore: first,
|
||||
hiddenAfter: after.length - last - 1,
|
||||
changed: diff.length,
|
||||
}
|
||||
}
|
||||
|
||||
function full(todos: Todo[], changed: number): Info {
|
||||
return {
|
||||
mode: "full",
|
||||
todos,
|
||||
hiddenBefore: 0,
|
||||
hiddenAfter: 0,
|
||||
changed,
|
||||
}
|
||||
}
|
||||
|
||||
function same(before: Todo | undefined, after: Todo) {
|
||||
if (!before) return false
|
||||
return before.content === after.content && before.status === after.status && before.priority === after.priority
|
||||
}
|
||||
|
||||
function terminal(todos: Todo[]) {
|
||||
return todos.every((todo) => todo.status === "completed" || todo.status === "cancelled")
|
||||
}
|
||||
|
||||
function structural(before: Todo[], after: Todo[]) {
|
||||
if (before.length !== after.length) return true
|
||||
return after.some((todo, index) => before[index]?.content !== todo.content)
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,9 @@ import { Effect, Schema } from "effect"
|
||||
import * as Tool from "./tool"
|
||||
import DESCRIPTION_WRITE from "./todowrite.txt"
|
||||
import { Todo } from "../session/todo"
|
||||
// kilocode_change start
|
||||
import { TodoView } from "../kilocode/todo-view"
|
||||
// kilocode_change end
|
||||
|
||||
// Todo.Info is still a zod schema (session/todo.ts). Inline the field shape
|
||||
// here rather than referencing its `.shape` — the LLM-visible JSON Schema is
|
||||
@@ -20,6 +23,9 @@ export const Parameters = Schema.Struct({
|
||||
|
||||
type Metadata = {
|
||||
todos: Todo.Info[]
|
||||
// kilocode_change start
|
||||
view?: TodoView.Info
|
||||
// kilocode_change end
|
||||
}
|
||||
|
||||
export const TodoWriteTool = Tool.define<typeof Parameters, Metadata, Todo.Service>(
|
||||
@@ -39,6 +45,11 @@ export const TodoWriteTool = Tool.define<typeof Parameters, Metadata, Todo.Servi
|
||||
metadata: {},
|
||||
})
|
||||
|
||||
// kilocode_change start
|
||||
const before = yield* todo.get(ctx.sessionID)
|
||||
const view = TodoView.calculate(before, params.todos)
|
||||
// kilocode_change end
|
||||
|
||||
yield* todo.update({
|
||||
sessionID: ctx.sessionID,
|
||||
todos: params.todos,
|
||||
@@ -49,6 +60,9 @@ export const TodoWriteTool = Tool.define<typeof Parameters, Metadata, Todo.Servi
|
||||
output: JSON.stringify(params.todos, null, 2),
|
||||
metadata: {
|
||||
todos: params.todos,
|
||||
// kilocode_change start
|
||||
view,
|
||||
// kilocode_change end
|
||||
},
|
||||
}
|
||||
}),
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { TodoView } from "../../src/kilocode/todo-view"
|
||||
|
||||
function item(content: string, status = "pending"): TodoView.Todo {
|
||||
return { content, status, priority: "medium" }
|
||||
}
|
||||
|
||||
describe("TodoView.calculate", () => {
|
||||
test("shows the full list when todos are first created", () => {
|
||||
const after = [item("Inspect files"), item("Implement fix"), item("Run checks")]
|
||||
const view = TodoView.calculate([], after)
|
||||
|
||||
expect(view.mode).toBe("full")
|
||||
expect(view.todos).toEqual(after)
|
||||
expect(view.hiddenBefore).toBe(0)
|
||||
expect(view.hiddenAfter).toBe(0)
|
||||
})
|
||||
|
||||
test("shows a compact window around the changed item", () => {
|
||||
const before = Array.from({ length: 10 }, (_, index) => item(`Task ${index + 1}`))
|
||||
const after = before.map((todo, index) => (index === 4 ? { ...todo, status: "completed" } : todo))
|
||||
const view = TodoView.calculate(before, after)
|
||||
|
||||
expect(view.mode).toBe("compact")
|
||||
expect(view.hiddenBefore).toBe(3)
|
||||
expect(view.hiddenAfter).toBe(4)
|
||||
expect(view.todos.map((todo) => todo.content)).toEqual(["Task 4", "Task 5", "Task 6"])
|
||||
expect(view.todos.map((todo) => Boolean(todo.changed))).toEqual([false, true, false])
|
||||
})
|
||||
|
||||
test("shows the full list when all todos are terminal", () => {
|
||||
const before = [item("Inspect files", "completed"), item("Implement fix"), item("Run checks")]
|
||||
const after = before.map((todo) => ({ ...todo, status: "completed" }))
|
||||
const view = TodoView.calculate(before, after)
|
||||
|
||||
expect(view.mode).toBe("full")
|
||||
expect(view.todos).toEqual(after)
|
||||
})
|
||||
|
||||
test("shows the full list when todo content is rewritten", () => {
|
||||
const before = [item("One"), item("Two"), item("Three"), item("Four")]
|
||||
const after = [item("One"), item("Two changed"), item("Three"), item("Four")]
|
||||
const view = TodoView.calculate(before, after)
|
||||
|
||||
expect(view.mode).toBe("full")
|
||||
expect(view.todos).toEqual(after)
|
||||
})
|
||||
|
||||
test("shows the full list when todos are added", () => {
|
||||
const before = [item("One"), item("Two"), item("Three")]
|
||||
const after = [...before, item("Four")]
|
||||
const view = TodoView.calculate(before, after)
|
||||
|
||||
expect(view.mode).toBe("full")
|
||||
expect(view.todos).toEqual(after)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user