From 0aad88ec3034be8a62e0a2e9bad4d41162a1ef4e Mon Sep 17 00:00:00 2001 From: marius-kilocode Date: Wed, 9 Sep 2026 14:12:32 +0200 Subject: [PATCH 1/3] feat(agent-manager): animate pending board routes --- .changeset/board-post-sending-route.md | 5 ++ .../kilo-ui/src/components/board-message.css | 40 +++++++++++--- .../kilo-ui/src/components/board-message.tsx | 18 +++--- .../src/components/board-route.test.ts | 55 +++++++++++++++++++ .../kilo-ui/src/components/board-route.ts | 33 +++++++++++ .../kilo-ui/src/components/message-part.tsx | 21 +++++-- .../src/stories/composite.stories.tsx | 47 ++++++++++++++++ 7 files changed, 201 insertions(+), 18 deletions(-) create mode 100644 .changeset/board-post-sending-route.md create mode 100644 packages/kilo-ui/src/components/board-route.test.ts create mode 100644 packages/kilo-ui/src/components/board-route.ts diff --git a/.changeset/board-post-sending-route.md b/.changeset/board-post-sending-route.md new file mode 100644 index 000000000000..ea795a0404b4 --- /dev/null +++ b/.changeset/board-post-sending-route.md @@ -0,0 +1,5 @@ +--- +"kilo-code": patch +--- + +Show the real sender and recipient avatars and titles while an agent board message is still being written, with a sending animation on the route arrow. diff --git a/packages/kilo-ui/src/components/board-message.css b/packages/kilo-ui/src/components/board-message.css index 90ae47cda290..d674ddf41241 100644 --- a/packages/kilo-ui/src/components/board-message.css +++ b/packages/kilo-ui/src/components/board-message.css @@ -23,26 +23,52 @@ max-width: none; } - > [data-component="icon"], + [data-slot="board-route-arrow"], [data-slot="board-route-recipient-icon"] { flex: 0 0 auto; - } - - [data-slot="board-route-recipient-icon"] { display: inline-flex; } - /* Parent session marker: the spinner grid, static, in the avatar slot size. */ + /* Parent session marker, in the avatar slot size. */ .board-route-parent { width: 18px; color: var(--text-weak); + } - > rect { - animation: none !important; + /* While the post is still being written or stored, the sender glyph + pulses (see agent-avatar.css) and the arrow travels toward the + recipient in the same 1.4s rhythm, so the row reads as "sending". */ + &[data-pending="true"] [data-slot="board-route-arrow"] { + color: var(--text-base); + + > [data-component="icon"] { + animation: board-route-send 1.4s ease-in-out infinite both; } } } +@keyframes board-route-send { + 0% { + transform: translateX(-4px); + opacity: 0; + } + 35%, + 65% { + transform: translateX(0); + opacity: 1; + } + 100% { + transform: translateX(4px); + opacity: 0; + } +} + +@media (prefers-reduced-motion: reduce) { + [data-component="board-route"][data-pending="true"] [data-slot="board-route-arrow"] > [data-component="icon"] { + animation: none; + } +} + [data-component="board-messages"] { display: flex; flex-direction: column; diff --git a/packages/kilo-ui/src/components/board-message.tsx b/packages/kilo-ui/src/components/board-message.tsx index 862fa727fbef..cee6cd978d1e 100644 --- a/packages/kilo-ui/src/components/board-message.tsx +++ b/packages/kilo-ui/src/components/board-message.tsx @@ -5,18 +5,19 @@ import { AgentAvatar } from "./agent-avatar" import { Markdown } from "./markdown" import { Tooltip } from "./tooltip" -// The parent session keeps the plain spinner grid; only subagents get a glyph. -function Member(props: { id: string }) { +// The parent session keeps the plain task icon; only subagents get a glyph. +function Member(props: { id: string; active?: boolean }) { return ( }> - + ) } type Route = { from?: unknown; to?: unknown; fromLabel?: unknown; toLabel?: unknown } -export function BoardRoute(props: Route) { +/** `pending` animates the route while the post is still being written or stored. */ +export function BoardRoute(props: Route & { pending?: boolean }) { const i18n = useI18n() const text = (value: unknown) => (typeof value === "string" ? value : "") const from = () => text(props.from) @@ -42,10 +43,11 @@ export function BoardRoute(props: Route) { - + {sender()} - + + + - }> + }> diff --git a/packages/kilo-ui/src/components/board-route.test.ts b/packages/kilo-ui/src/components/board-route.test.ts new file mode 100644 index 000000000000..01ab2647396a --- /dev/null +++ b/packages/kilo-ui/src/components/board-route.test.ts @@ -0,0 +1,55 @@ +import { describe, expect, test } from "bun:test" +import { preview } from "./board-route" + +const sessions = [ + { id: "root", title: "Fix comment UI cutoff issue" }, + { id: "child", parentID: "root", title: "Find PR comment overflow (@explore subagent)" }, + { id: "sibling", parentID: "root", title: "Check serializer compatibility" }, + { id: "orphan", parentID: "missing", title: "Detached" }, +] + +describe("board route preview", () => { + test("maps the root session to main and resolves titles", () => { + expect(preview(sessions, "child", "main")).toEqual({ + from: "child", + fromLabel: "Find PR comment overflow (@explore subagent)", + to: "main", + toLabel: "Fix comment UI cutoff issue", + }) + expect(preview(sessions, "root", "child")).toEqual({ + from: "main", + fromLabel: "Fix comment UI cutoff issue", + to: "child", + toLabel: "Find PR comment overflow (@explore subagent)", + }) + }) + + test("aliases a recipient given by root ID to main", () => { + expect(preview(sessions, "child", "root")).toMatchObject({ to: "main", toLabel: "Fix comment UI cutoff issue" }) + }) + + test("keeps broadcasts and hides unknown or partial recipient IDs", () => { + expect(preview(sessions, "child", "ALL")).toMatchObject({ to: "ALL", toLabel: undefined }) + expect(preview(sessions, "child", "sib")).toMatchObject({ to: "", toLabel: undefined }) + expect(preview(sessions, "child", undefined)).toMatchObject({ to: "", toLabel: undefined }) + expect(preview(sessions, "child", "sibling")).toMatchObject({ + to: "sibling", + toLabel: "Check serializer compatibility", + }) + }) + + test("does not guess main when the lineage is incomplete", () => { + expect(preview(sessions, "orphan", "main")).toEqual({ + from: "orphan", + fromLabel: "Detached", + to: "", + toLabel: undefined, + }) + expect(preview([], "unknown", "main")).toEqual({ + from: "unknown", + fromLabel: undefined, + to: "", + toLabel: undefined, + }) + }) +}) diff --git a/packages/kilo-ui/src/components/board-route.ts b/packages/kilo-ui/src/components/board-route.ts new file mode 100644 index 000000000000..4bee46badb37 --- /dev/null +++ b/packages/kilo-ui/src/components/board-route.ts @@ -0,0 +1,33 @@ +type Node = { id: string; parentID?: string; title?: string } + +const LIMIT = 32 + +/** + * Optimistic route for a board_post that is still pending. The stored route + * (from, to, labels) only arrives with the tool result, but the sender and the + * usual recipients are already in the session store, so the trigger can show + * the real avatars and titles while the model still streams the message body. + * Unknown or partially streamed recipient IDs resolve to "" so the avatar does + * not flicker through hash colors. + */ +export function preview(sessions: readonly Node[], sessionID: string, to: unknown) { + const byID = new Map(sessions.map((node) => [node.id, node])) + const root = (id: string, depth = 0): string | undefined => { + const node = byID.get(id) + if (!node) return undefined + if (!node.parentID) return node.id + if (depth >= LIMIT) return undefined + return root(node.parentID, depth + 1) + } + const top = root(sessionID) + const alias = (id: string) => (top && id === top ? "main" : id) + const label = (id: string) => byID.get(id)?.title?.trim() || undefined + const value = typeof to === "string" ? to.trim() : "" + const target = (() => { + if (value === "ALL") return { id: "ALL", label: undefined } + if (value === "main") return { id: top ? "main" : "", label: top ? label(top) : undefined } + if (!byID.has(value)) return { id: "", label: undefined } + return { id: alias(value), label: label(value) } + })() + return { from: alias(sessionID), fromLabel: label(sessionID), to: target.id, toLabel: target.label } +} diff --git a/packages/kilo-ui/src/components/message-part.tsx b/packages/kilo-ui/src/components/message-part.tsx index e419b1115414..259df4180f41 100644 --- a/packages/kilo-ui/src/components/message-part.tsx +++ b/packages/kilo-ui/src/components/message-part.tsx @@ -36,6 +36,7 @@ import { useClipboard } from "../context/clipboard" import { type UiI18n, useI18n } from "../context/i18n" import { BasicTool, useToolApprovalLine } from "./basic-tool" import { BoardMessage, BoardRoute } from "./board-message" +import { preview } from "./board-route" import { AgentAvatar, taskStatus } from "./agent-avatar" import { Accordion } from "./accordion" import { StickyAccordionHeader } from "./sticky-accordion-header" @@ -1093,6 +1094,7 @@ export interface ToolProps { tool: string partID?: string callID?: string + sessionID?: string output?: string status?: string attachments?: FilePart[] @@ -1195,14 +1197,24 @@ function McpTool(props: ToolProps) { ) return items.length === rows.length ? items : undefined }) + // The stored route only arrives with the result. While the model still + // streams the post, derive the sender and recipient from the session store + // so the trigger shows the real avatars and titles from the first frame. + const data = props.tool === "board_post" ? useData() : undefined + const live = () => props.status === "pending" || props.status === "running" + const guess = createMemo(() => { + if (!data || !live() || !props.sessionID) return undefined + return preview(data.store.session, props.sessionID, props.input.to) + }) const trigger = () => { if (props.tool === "board_post") return ( ) if (props.tool === "board_read") { @@ -1423,6 +1435,7 @@ PART_MAPPING["tool"] = function ToolPartDisplay(props) { tool={part.tool} partID={part.id} callID={part.callID} + sessionID={part.sessionID} metadata={meta()} partMetadata={top()} // @ts-expect-error diff --git a/packages/kilo-vscode/webview-ui/src/stories/composite.stories.tsx b/packages/kilo-vscode/webview-ui/src/stories/composite.stories.tsx index 35d774322afd..fff1d568427e 100644 --- a/packages/kilo-vscode/webview-ui/src/stories/composite.stories.tsx +++ b/packages/kilo-vscode/webview-ui/src/stories/composite.stories.tsx @@ -1407,6 +1407,53 @@ export const AgentMessages200: Story = { name: "Agent messages with long titles (200px)", } +// A post that is still streaming: the route is derived from the session store +// (sender title, recipient resolved to main) and the arrow animates. +export const AgentMessagePending: Story = { + name: "Agent message, sending", + render: () => { + const parts: ToolPart[] = [ + { + id: "part_board_pending", + sessionID: SESSION_ID, + messageID: ASST_MSG_ID, + type: "tool", + callID: "call_board_pending", + tool: "board_post", + state: { + status: "running", + input: { to: "main", type: "RESULT", body: "Parser checks are complete." }, + title: "Post agent message", + metadata: {}, + time: { start: now - 1000 }, + }, + }, + { + id: "part_board_partial", + sessionID: SESSION_ID, + messageID: ASST_MSG_ID, + type: "tool", + callID: "call_board_partial", + tool: "board_post", + state: { status: "pending", input: { to: "ses_ser" }, raw: "" }, + }, + ] + const data = { + ...dataWith(parts), + session: [ + { id: "ses_root", title: "Fix comment UI cutoff issue" }, + { id: SESSION_ID, parentID: "ses_root", title: "Find PR comment overflow (@explore subagent)" }, + { id: "ses_serializer", parentID: "ses_root", title: "Check serializer compatibility" }, + ], + } + return ( + + {(part) => } + + ) + }, +} + export const McpToolCards: Story = { name: "MCP Tool Cards — collapsed", render: () => { From e9dc151fddf8ac0ba9d2ce6d7bd07623b43c8fd5 Mon Sep 17 00:00:00 2001 From: marius-kilocode Date: Wed, 9 Sep 2026 14:38:11 +0200 Subject: [PATCH 2/3] docs(agent-manager): clarify pending route optimization --- .changeset/board-post-sending-route.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/board-post-sending-route.md b/.changeset/board-post-sending-route.md index ea795a0404b4..ee7269f50f7a 100644 --- a/.changeset/board-post-sending-route.md +++ b/.changeset/board-post-sending-route.md @@ -2,4 +2,4 @@ "kilo-code": patch --- -Show the real sender and recipient avatars and titles while an agent board message is still being written, with a sending animation on the route arrow. +Resolve agent board route metadata from the session store while a message is still being written, so sender and recipient avatars and titles appear without waiting for the completed tool result. From fd4475e70320eb4f06d23f8cc79ebfe371c309a6 Mon Sep 17 00:00:00 2001 From: "kilo-maintainer[bot]" Date: Wed, 9 Sep 2026 12:41:38 +0000 Subject: [PATCH 3/3] chore: update kilo-vscode visual regression baselines --- .../composite-webview/agent-message-pending-chromium-linux.png | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/composite-webview/agent-message-pending-chromium-linux.png diff --git a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/composite-webview/agent-message-pending-chromium-linux.png b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/composite-webview/agent-message-pending-chromium-linux.png new file mode 100644 index 000000000000..6145eb763a5b --- /dev/null +++ b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/composite-webview/agent-message-pending-chromium-linux.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:13354cd75ccb15dea7149f1fac7b8f8ec34cf38489a0ec473365274a4ed2bbc0 +size 5469