From b8fe0095d14a08c36b5648fe3c957c81e402911c Mon Sep 17 00:00:00 2001 From: marius-kilocode Date: Tue, 8 Sep 2026 20:49:49 +0200 Subject: [PATCH 1/5] tmp --- .changeset/subagent-avatars.md | 5 ++ packages/kilo-ui/package.json | 1 + .../components/agent-avatar-identity.test.ts | 28 ++++++++++ .../src/components/agent-avatar-identity.ts | 14 +++++ .../kilo-ui/src/components/agent-avatar.css | 54 +++++++++++++++++++ .../kilo-ui/src/components/agent-avatar.tsx | 29 ++++++++++ .../kilo-ui/src/components/basic-tool.css | 7 +++ .../kilo-ui/src/components/board-message.css | 2 +- .../kilo-ui/src/components/board-message.tsx | 7 +-- .../kilo-ui/src/components/message-part.tsx | 4 ++ packages/kilo-ui/src/styles/index.css | 1 + .../agent-manager/SubagentPanel.tsx | 12 +++-- .../agent-manager/agent-manager.css | 6 +++ .../src/components/chat/BackgroundAgents.tsx | 17 ++++-- .../src/components/chat/TaskToolExpanded.tsx | 10 ++-- .../webview-ui/src/styles/tool-overrides.css | 14 +++++ packages/ui/src/components/basic-tool.tsx | 3 +- 17 files changed, 197 insertions(+), 17 deletions(-) create mode 100644 .changeset/subagent-avatars.md create mode 100644 packages/kilo-ui/src/components/agent-avatar-identity.test.ts create mode 100644 packages/kilo-ui/src/components/agent-avatar-identity.ts create mode 100644 packages/kilo-ui/src/components/agent-avatar.css create mode 100644 packages/kilo-ui/src/components/agent-avatar.tsx diff --git a/.changeset/subagent-avatars.md b/.changeset/subagent-avatars.md new file mode 100644 index 000000000000..f495b8f425df --- /dev/null +++ b/.changeset/subagent-avatars.md @@ -0,0 +1,5 @@ +--- +"kilo-code": minor +--- + +Identify subagents with consistent theme-colored avatars in Task cards, background agents, subagent tabs, and swarm messages. Animate running avatars instead of showing a separate loading indicator. diff --git a/packages/kilo-ui/package.json b/packages/kilo-ui/package.json index 81d67922830c..771bd1e17078 100644 --- a/packages/kilo-ui/package.json +++ b/packages/kilo-ui/package.json @@ -26,6 +26,7 @@ "./tabs": "./src/components/tabs.tsx", "./card": "./src/components/card.tsx", "./avatar": "./src/components/avatar.tsx", + "./agent-avatar": "./src/components/agent-avatar.tsx", "./logo": "./src/components/logo.tsx", "./favicon": "./src/components/favicon.tsx", "./file-icon": "./src/components/file-icon.tsx", diff --git a/packages/kilo-ui/src/components/agent-avatar-identity.test.ts b/packages/kilo-ui/src/components/agent-avatar-identity.test.ts new file mode 100644 index 000000000000..c512d92a8048 --- /dev/null +++ b/packages/kilo-ui/src/components/agent-avatar-identity.test.ts @@ -0,0 +1,28 @@ +import { describe, expect, test } from "bun:test" +import { identity } from "./agent-avatar-identity" + +describe("agent avatar identity", () => { + test("uses the same identity for the same participant", () => { + expect(identity("ses_agent-one")).toEqual(identity("ses_agent-one")) + expect(identity("ses_agent-one").cells).not.toEqual(identity("ses_agent-two").cells) + }) + + test("keeps unknown participants neutral", () => { + expect(identity("").color).toBeUndefined() + expect(identity("unknown")).toEqual(identity("")) + expect(identity(" ")).toEqual(identity("")) + expect(identity("main").color).toBeNumber() + }) + + test("produces visible symmetric patterns within the avatar", () => { + for (const id of ["main", "ses_agent-one", "ses_agent-two", "participant", ""]) { + const avatar = identity(id) + expect(avatar.cells.length).toBeGreaterThan(0) + for (const cell of avatar.cells) { + expect(cell).toBeGreaterThanOrEqual(0) + expect(cell).toBeLessThan(25) + expect(avatar.cells).toContain(Math.floor(cell / 5) * 5 + 4 - (cell % 5)) + } + } + }) +}) diff --git a/packages/kilo-ui/src/components/agent-avatar-identity.ts b/packages/kilo-ui/src/components/agent-avatar-identity.ts new file mode 100644 index 000000000000..8727da1088b7 --- /dev/null +++ b/packages/kilo-ui/src/components/agent-avatar-identity.ts @@ -0,0 +1,14 @@ +export function identity(id: string) { + const known = id.trim() !== "" && id !== "unknown" + let hash = 2166136261 + for (let index = 0; index < id.length; index++) { + hash = Math.imul(hash ^ id.charCodeAt(index), 16777619) >>> 0 + } + // Mirror three columns into a five-column pattern. Keep the center visible. + const mask = known ? (hash >>> 8) | (1 << 8) : (1 << 5) | (1 << 7) | (1 << 8) | (1 << 11) + const cells = Array.from({ length: 25 }, (_, index) => index).filter((index) => { + const x = index % 5 + return mask & (1 << (Math.floor(index / 5) * 3 + Math.min(x, 4 - x))) + }) + return { color: known ? hash % 6 : undefined, cells } +} diff --git a/packages/kilo-ui/src/components/agent-avatar.css b/packages/kilo-ui/src/components/agent-avatar.css new file mode 100644 index 000000000000..19d092dfa3a5 --- /dev/null +++ b/packages/kilo-ui/src/components/agent-avatar.css @@ -0,0 +1,54 @@ +[data-component="agent-avatar"] { + display: inline-block; + flex: 0 0 20px; + width: 20px; + height: 20px; + vertical-align: middle; + color: var(--icon-weak-base); + fill: currentColor; + shape-rendering: crispEdges; + + &[data-running] rect { + animation: agent-avatar-shimmer 1.6s ease-in-out infinite; + } + + &[data-color="0"] { + color: var(--vscode-charts-blue, var(--icon-info-base)); + } + + &[data-color="1"] { + color: var(--vscode-charts-green, var(--icon-success-base)); + } + + &[data-color="2"] { + color: var(--vscode-charts-yellow, var(--icon-warning-base)); + } + + &[data-color="3"] { + color: var(--vscode-charts-orange, var(--icon-warning-base)); + } + + &[data-color="4"] { + color: var(--vscode-charts-purple, var(--icon-info-base)); + } + + &[data-color="5"] { + color: var(--vscode-charts-red, var(--icon-critical-base)); + } +} + +@keyframes agent-avatar-shimmer { + 0%, + 100% { + opacity: 0.35; + } + 50% { + opacity: 1; + } +} + +@media (prefers-reduced-motion: reduce) { + [data-component="agent-avatar"][data-running] rect { + animation: none; + } +} diff --git a/packages/kilo-ui/src/components/agent-avatar.tsx b/packages/kilo-ui/src/components/agent-avatar.tsx new file mode 100644 index 000000000000..b28f26cf4b05 --- /dev/null +++ b/packages/kilo-ui/src/components/agent-avatar.tsx @@ -0,0 +1,29 @@ +import { createMemo, For } from "solid-js" +import { identity } from "./agent-avatar-identity" + +export function AgentAvatar(props: { id: string; running?: boolean }) { + const avatar = createMemo(() => identity(props.id)) + return ( + + ) +} diff --git a/packages/kilo-ui/src/components/basic-tool.css b/packages/kilo-ui/src/components/basic-tool.css index 23224ffb4b15..593ae36e596a 100644 --- a/packages/kilo-ui/src/components/basic-tool.css +++ b/packages/kilo-ui/src/components/basic-tool.css @@ -36,6 +36,13 @@ [data-slot="basic-tool-icon"] { display: none; + + &:has([data-component="agent-avatar"]) { + display: inline-flex; + flex: 0 0 20px; + width: 20px; + height: 20px; + } } [data-slot="basic-tool-tool-subtitle"] { diff --git a/packages/kilo-ui/src/components/board-message.css b/packages/kilo-ui/src/components/board-message.css index 6e97fefade52..85d5beae9838 100644 --- a/packages/kilo-ui/src/components/board-message.css +++ b/packages/kilo-ui/src/components/board-message.css @@ -66,7 +66,7 @@ @container (max-width: 260px) { [data-component="board-route"] { display: grid; - grid-template-columns: 16px auto minmax(0, 1fr); + grid-template-columns: 20px auto minmax(0, 1fr); gap: 4px 6px; .board-route-member { diff --git a/packages/kilo-ui/src/components/board-message.tsx b/packages/kilo-ui/src/components/board-message.tsx index a5575ddeca76..c1e1aca471f7 100644 --- a/packages/kilo-ui/src/components/board-message.tsx +++ b/packages/kilo-ui/src/components/board-message.tsx @@ -1,6 +1,7 @@ import { Show } from "solid-js" import { useI18n } from "../context/i18n" import { Icon } from "./icon" +import { AgentAvatar } from "./agent-avatar" import { Markdown } from "./markdown" import { Tooltip } from "./tooltip" @@ -35,7 +36,7 @@ export function BoardRoute(props: Route) { role="group" aria-label={i18n.t("ui.messagePart.board.route", { from: sender(), to: recipient() })} > - + - - + }> + diff --git a/packages/kilo-ui/src/components/message-part.tsx b/packages/kilo-ui/src/components/message-part.tsx index ed539bb61e02..9a360689ae9d 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 { AgentAvatar } from "./agent-avatar" import { Accordion } from "./accordion" import { StickyAccordionHeader } from "./sticky-accordion-header" import { Card } from "./card" @@ -2457,6 +2458,9 @@ ToolRegistry.register({ hideDetails approvalPlacement="hidden" icon="task" + iconNode={ + + } status={props.status} trigger={trigger()} animated diff --git a/packages/kilo-ui/src/styles/index.css b/packages/kilo-ui/src/styles/index.css index 2b3687ac3406..4814bb1feecc 100644 --- a/packages/kilo-ui/src/styles/index.css +++ b/packages/kilo-ui/src/styles/index.css @@ -6,6 +6,7 @@ /* Per-component Kilo overrides */ @import "../components/accordion.css"; +@import "../components/agent-avatar.css"; @import "../components/basic-tool.css"; @import "../components/board-message.css"; @import "../components/auto-approve-bar.css"; diff --git a/packages/kilo-vscode/webview-ui/agent-manager/SubagentPanel.tsx b/packages/kilo-vscode/webview-ui/agent-manager/SubagentPanel.tsx index 1502ed9d34ad..fa15a7e55448 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/SubagentPanel.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/SubagentPanel.tsx @@ -6,14 +6,15 @@ */ import { Icon } from "@kilocode/kilo-ui/icon" +import { AgentAvatar } from "@kilocode/kilo-ui/agent-avatar" import { IconButton } from "@kilocode/kilo-ui/icon-button" -import { createEffect, createMemo, on, type Accessor, type Component } from "solid-js" +import { createEffect, createMemo, on, Show, type Accessor, type Component } from "solid-js" import { DataBridge } from "../src/App" import { ChatView } from "../src/components/chat" import { ActivityIcon } from "../src/components/shared/ActivityIcon" import { useLanguage } from "../src/context/language" import { SessionProvider, useSession, useSessionVisibility } from "../src/context/session" -import { description, label, type Activity } from "../src/utils/session-activity" +import { description, label, running, type Activity } from "../src/utils/session-activity" import { SortableClosableTab } from "./ClosableTab" import { InspectorTabStrip } from "./InspectorTabStrip" import type { SubagentTab } from "./subagent-tabs" @@ -104,7 +105,12 @@ const SubagentContent: Component Activity }> tooltip={() => (state() === "idle" ? name : `${name}: ${language.t(description(state()))}`)} icon="task" iconNode={ - } spinner="am-tab-spinner" /> + <> + + + + + } state={state()} stateLabel={state() === "idle" ? undefined : language.t(label(state()))} diff --git a/packages/kilo-vscode/webview-ui/agent-manager/agent-manager.css b/packages/kilo-vscode/webview-ui/agent-manager/agent-manager.css index 7978f988827f..1e9939b690b6 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/agent-manager.css +++ b/packages/kilo-vscode/webview-ui/agent-manager/agent-manager.css @@ -1520,6 +1520,12 @@ html[data-theme="kilo-vscode"] color: currentColor; } +.am-subagent-panel .am-tab-icon:has([data-component="agent-avatar"]) { + width: auto; + height: 20px; + gap: 4px; +} + .am-tab-icon[data-run-status="success"] { color: var(--vscode-testing-iconPassed, #34d399); } diff --git a/packages/kilo-vscode/webview-ui/src/components/chat/BackgroundAgents.tsx b/packages/kilo-vscode/webview-ui/src/components/chat/BackgroundAgents.tsx index fded16433704..9b6775fbf716 100644 --- a/packages/kilo-vscode/webview-ui/src/components/chat/BackgroundAgents.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/chat/BackgroundAgents.tsx @@ -12,6 +12,7 @@ import { Component, For, Show, createMemo, createSignal, onCleanup, onMount, createEffect, on } from "solid-js" import { Button } from "@kilocode/kilo-ui/button" +import { AgentAvatar } from "@kilocode/kilo-ui/agent-avatar" import { Icon } from "@kilocode/kilo-ui/icon" import { IconButton } from "@kilocode/kilo-ui/icon-button" import { Tooltip } from "@kilocode/kilo-ui/tooltip" @@ -264,17 +265,19 @@ export const BackgroundAgents: Component<{ readonly?: boolean }> = (props) => { aria-label={tooltip(agent())} onClick={() => openAgent(agent())} > + + {label(agent())} }> - {(name) => } - + {(name) => } } > - {label(agent())} )} @@ -360,7 +363,11 @@ export const BackgroundAgents: Component<{ readonly?: boolean }> = (props) => { agent.jobID === id)}> {(agent) => (
- }> + + {(name) => } @@ -265,19 +258,8 @@ export const BackgroundAgents: Component<{ readonly?: boolean }> = (props) => { aria-label={tooltip(agent())} onClick={() => openAgent(agent())} > - + {label(agent())} - {(name) => } - } - > - - )} @@ -308,7 +290,6 @@ export const BackgroundAgents: Component<{ readonly?: boolean }> = (props) => { ref={toggle} variant="ghost" size="small" - icon={waiting() > 0 ? "warning" : undefined} aria-label={accessible()} title={accessible()} aria-expanded={open()} @@ -354,7 +335,6 @@ export const BackgroundAgents: Component<{ readonly?: boolean }> = (props) => {
agent.permission || agent.question)}>
- {language.t("task.backgroundAgents.waiting")}
@@ -363,13 +343,7 @@ export const BackgroundAgents: Component<{ readonly?: boolean }> = (props) => { agent.jobID === id)}> {(agent) => (
- - - {(name) => } - +