diff --git a/.changeset/subagent-avatars.md b/.changeset/subagent-avatars.md
index f495b8f425d..eb49f20aa42 100644
--- a/.changeset/subagent-avatars.md
+++ b/.changeset/subagent-avatars.md
@@ -2,4 +2,4 @@
"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.
+Identify subagents with consistent theme-colored avatars in Task cards, background agents, subagent tabs, and swarm messages. Show all participants in board message headers, and animate running avatars instead of showing a separate loading indicator.
diff --git a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/composite-webview/agent-messages-200-chromium-linux.png b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/composite-webview/agent-messages-200-chromium-linux.png
index b392a38b65f..ea799ec7151 100644
--- a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/composite-webview/agent-messages-200-chromium-linux.png
+++ b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/composite-webview/agent-messages-200-chromium-linux.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:9df99cbf6043b0444aac6ef3bde54d60649d99b30a14a147ab4f512cfc9dd0ff
-size 40030
+oid sha256:fea711f03eb18f2df4749a208a73b850178851bc131fe347b2616f84752c0cd7
+size 42704
diff --git a/packages/kilo-ui/src/components/agent-avatar.css b/packages/kilo-ui/src/components/agent-avatar.css
index 0173758adf7..3dc6358ddf3 100644
--- a/packages/kilo-ui/src/components/agent-avatar.css
+++ b/packages/kilo-ui/src/components/agent-avatar.css
@@ -69,6 +69,23 @@
}
}
+[data-component="board-participant-stack"] {
+ display: inline-flex;
+ align-items: center;
+ flex: 0 0 auto;
+ width: max-content;
+ height: 18px;
+ overflow: visible;
+ gap: 3px;
+
+ > * {
+ position: relative;
+ flex: 0 0 18px;
+ width: 18px;
+ height: 18px;
+ }
+}
+
@keyframes agent-avatar-pulse {
0%,
100% {
diff --git a/packages/kilo-ui/src/components/agent-avatar.tsx b/packages/kilo-ui/src/components/agent-avatar.tsx
index 628a5b26cda..eb8d21a1732 100644
--- a/packages/kilo-ui/src/components/agent-avatar.tsx
+++ b/packages/kilo-ui/src/components/agent-avatar.tsx
@@ -21,6 +21,11 @@ export function AgentAvatarPalette(props: { ids: string[]; children: JSX.Element
return {props.children}
}
+export function useAgentAvatarIds() {
+ const shared = useContext(Palette)
+ return createMemo(() => (shared ? [...shared().keys()] : []))
+}
+
// Corner cells are dropped so the dot grid reads as a circle.
const GRID = Array.from({ length: 25 }, (_, index) => index).filter((index) => ![0, 4, 20, 24].includes(index))
diff --git a/packages/kilo-ui/src/components/basic-tool.css b/packages/kilo-ui/src/components/basic-tool.css
index 49714f6bb4c..06ff361834b 100644
--- a/packages/kilo-ui/src/components/basic-tool.css
+++ b/packages/kilo-ui/src/components/basic-tool.css
@@ -49,6 +49,15 @@
height: 18px;
overflow: visible;
}
+
+ &:has([data-component="board-participant-stack"]) {
+ display: inline-flex;
+ flex: 0 0 auto;
+ width: auto;
+ min-width: 18px;
+ height: 18px;
+ overflow: visible;
+ }
}
[data-slot="basic-tool-tool-subtitle"] {
diff --git a/packages/kilo-ui/src/components/board-message.tsx b/packages/kilo-ui/src/components/board-message.tsx
index 862fa727fbe..00b8e7e1ff3 100644
--- a/packages/kilo-ui/src/components/board-message.tsx
+++ b/packages/kilo-ui/src/components/board-message.tsx
@@ -1,7 +1,7 @@
-import { Show } from "solid-js"
+import { createMemo, For, Show } from "solid-js"
import { useI18n } from "../context/i18n"
import { Icon } from "./icon"
-import { AgentAvatar } from "./agent-avatar"
+import { AgentAvatar, useAgentAvatarIds } from "./agent-avatar"
import { Markdown } from "./markdown"
import { Tooltip } from "./tooltip"
@@ -14,13 +14,29 @@ function Member(props: { id: string }) {
)
}
+export function BoardParticipantStack(props: { ids: string[] }) {
+ return (
+
+ 0} fallback={}>
+ {(id) => }
+
+
+ )
+}
+
type Route = { from?: unknown; to?: unknown; fromLabel?: unknown; toLabel?: unknown }
export function BoardRoute(props: Route) {
const i18n = useI18n()
+ const ids = useAgentAvatarIds()
const text = (value: unknown) => (typeof value === "string" ? value : "")
const from = () => text(props.from)
const to = () => text(props.to)
+ const broadcast = createMemo(() => {
+ const values = ids().filter((id) => id !== "main" && id !== from())
+ if (from() !== "main" && values.length > 0) values.unshift("main")
+ return values
+ })
const label = (id: string, value: unknown) => {
if (id === "ALL") return i18n.t("ui.messagePart.board.all")
const title = text(value)
@@ -56,8 +72,17 @@ export function BoardRoute(props: Route) {
}>
-
-
+ 0}
+ fallback={
+ <>
+
+
+ >
+ }
+ >
+
+
{
+ const seen = new Set()
+ const ids: string[] = []
+ for (const item of messages() ?? []) {
+ for (const id of [item.from, item.to]) {
+ if (!id || id === "ALL" || seen.has(id)) continue
+ seen.add(id)
+ ids.push(id)
+ }
+ }
+ const main = ids.indexOf("main")
+ if (main > 0) {
+ ids.splice(main, 1)
+ ids.unshift("main")
+ }
+ return ids
+ })
const trigger = () => {
if (props.tool === "board_post")
return (
@@ -1248,10 +1265,19 @@ function McpTool(props: ToolProps) {
return (
}
+ fallback={
+ : undefined}
+ status={props.status}
+ trigger={trigger()}
+ />
+ }
>
: undefined}
defer={board()}
status={props.status}
tool={props.tool}
diff --git a/packages/kilo-ui/src/stories/message-part.stories.tsx b/packages/kilo-ui/src/stories/message-part.stories.tsx
index cea172ffe6f..3c1bf353b52 100644
--- a/packages/kilo-ui/src/stories/message-part.stories.tsx
+++ b/packages/kilo-ui/src/stories/message-part.stories.tsx
@@ -1,6 +1,7 @@
/** @jsxImportSource solid-js */
import type { Meta, StoryObj } from "storybook-solidjs-vite"
import { UserMessageDisplay, AssistantParts } from "../components/message-part"
+import { AgentAvatarPalette } from "../components/agent-avatar"
import { DataProvider } from "@opencode-ai/ui/context/data"
import { DiffComponentProvider } from "@kilocode/kilo-ui/context/diff"
import { CodeComponentProvider } from "@kilocode/kilo-ui/context/code"
@@ -274,6 +275,68 @@ const mockDataContextGroup = createMockData([completedToolPart, grepCompleted, g
const mockDataEdit = createMockData([editCompletedPart])
const mockDataWrite = createMockData([writeCompletedPart])
+const boardReadPart: ToolPart = {
+ id: "part-board-read-001",
+ sessionID: SESSION_ID,
+ messageID: ASST_MSG_ID,
+ type: "tool",
+ callID: "call-board-read-001",
+ tool: "board_read",
+ state: {
+ status: "completed",
+ input: {},
+ output: JSON.stringify({
+ messages: [
+ {
+ from: "main",
+ to: "worker",
+ fromLabel: "Coordinator",
+ toLabel: "Worker",
+ body: "**First message**",
+ },
+ {
+ from: "worker",
+ to: "reviewer",
+ fromLabel: "Worker",
+ toLabel: "Reviewer",
+ body: "**Second message**",
+ },
+ ],
+ hasMore: false,
+ }),
+ title: "Read agent messages 2",
+ metadata: {},
+ time: { start: now - 4000, end: now - 3500 },
+ },
+}
+
+const mockDataBoardRead = createMockData([boardReadPart])
+
+const boardBroadcastPart: ToolPart = {
+ id: "part-board-broadcast-001",
+ sessionID: SESSION_ID,
+ messageID: ASST_MSG_ID,
+ type: "tool",
+ callID: "call-board-broadcast-001",
+ tool: "board_post",
+ state: {
+ status: "completed",
+ input: { to: "ALL", type: "INFO", body: "Broadcast update" },
+ output: JSON.stringify({
+ from: "main",
+ to: "ALL",
+ fromLabel: "Coordinator",
+ type: "INFO",
+ body: "Broadcast update",
+ }),
+ title: "INFO to ALL",
+ metadata: { from: "main", to: "ALL", fromLabel: "Coordinator" },
+ time: { start: now - 3000, end: now - 2500 },
+ },
+}
+
+const mockDataBoardBroadcast = createMockData([boardBroadcastPart])
+
function AllProviders(props: { children: any; data?: MockData; onOpenDiff?: () => void }) {
return (
@@ -409,6 +472,26 @@ export const WithBashToolExpanded: Story = {
},
}
+export const WithBoardRead: Story = {
+ render: () => (
+
+
+
+
+
+ ),
+}
+
+export const WithBoardBroadcast: Story = {
+ render: () => (
+
+
+
+
+
+ ),
+}
+
// --- Three context-group tools + text — exercises ContextToolGroupHeader collapse ---
export const WithContextGroup: Story = {
diff --git a/packages/kilo-vscode/src/agent-manager/AgentManagerProvider.ts b/packages/kilo-vscode/src/agent-manager/AgentManagerProvider.ts
index eab12f0d860..518b968e8f4 100644
--- a/packages/kilo-vscode/src/agent-manager/AgentManagerProvider.ts
+++ b/packages/kilo-vscode/src/agent-manager/AgentManagerProvider.ts
@@ -352,7 +352,6 @@ export class AgentManagerProvider implements Disposable {
}
this.log("Opening Agent Manager panel")
this.host.capture("Agent Manager Opened", { source: PLATFORM })
-
const panel = this.host.openPanel({
onBeforeMessage: (msg) => this.onMessage(msg),
worktreeDirectories: () => this.getWorktreeDirectories(),
diff --git a/packages/kilo-vscode/tests/fixtures/board-tool-render.tsx b/packages/kilo-vscode/tests/fixtures/board-tool-render.tsx
index 2f017b65c8f..88ea95b317b 100644
--- a/packages/kilo-vscode/tests/fixtures/board-tool-render.tsx
+++ b/packages/kilo-vscode/tests/fixtures/board-tool-render.tsx
@@ -29,6 +29,8 @@ const { createSignal } = await import("solid-js")
const { createStore } = await import("solid-js/store")
const { render } = await import("solid-js/web")
const { Part } = await import("@kilocode/kilo-ui/message-part")
+const { AgentAvatarPalette } = await import("@kilocode/kilo-ui/agent-avatar")
+const { BoardRoute } = await import("@kilocode/kilo-ui/board-message")
const { MarkedProvider, createMarkedParser } = await import("@kilocode/kilo-ui/context/marked")
const labels = ["initial", "hidden", "latest", "reopened", "search", "search-updated"]
@@ -36,6 +38,7 @@ const outputs = labels.map((label) =>
JSON.stringify({
messages: [
{ from: "worker", to: "main", fromLabel: `Worker ${label}`, toLabel: "Coordinator", body: `**${label}** body` },
+ { from: "reviewer", to: "worker", fromLabel: "Reviewer", toLabel: `Worker ${label}`, body: "Secondary body" },
],
hasMore: false,
}),
@@ -81,6 +84,10 @@ JSON.parse = (text, reviver) => {
}
const root = document.createElement("div")
document.body.append(root)
+const broadcastRoot = document.createElement("div")
+document.body.append(broadcastRoot)
+const workerBroadcastRoot = document.createElement("div")
+document.body.append(workerBroadcastRoot)
const dispose = render(
() => (
(
+
+
+
+ ),
+ broadcastRoot,
+)
+const disposeWorkerBroadcast = render(
+ () => (
+
+
+
+ ),
+ workerBroadcastRoot,
+)
const settle = async () => {
await Promise.resolve()
await window.happyDOM.waitUntilComplete()
@@ -109,6 +132,10 @@ const update = async (index: number) => {
}
const visible = (label: string) => {
assert.equal(trigger().getAttribute("aria-expanded"), "true")
+ const stack = root.querySelector('[data-component="board-participant-stack"]')
+ assert(stack)
+ assert.equal(stack.querySelectorAll('[data-component="icon"]').length, 1)
+ assert.equal(stack.querySelectorAll('[data-component="agent-avatar"]').length, 2)
assert.equal(root.querySelector('[data-slot="board-message-body"] strong')?.textContent, label)
assert.equal(root.querySelector(".board-route-sender")?.textContent, `Worker ${label}`)
assert.equal(root.querySelector(".board-route-recipient")?.textContent, "Coordinator")
@@ -117,6 +144,14 @@ const visible = (label: string) => {
try {
await settle()
+ const recipient = broadcastRoot.querySelector('[data-slot="board-route-recipient-icon"]')
+ assert(recipient)
+ assert.equal(recipient.querySelectorAll('[data-component="board-participant-stack"]').length, 1)
+ assert.equal(recipient.querySelectorAll('[data-component="agent-avatar"]').length, 2)
+ const workerRecipient = workerBroadcastRoot.querySelector('[data-slot="board-route-recipient-icon"]')
+ assert(workerRecipient)
+ assert.equal(workerRecipient.querySelectorAll('[data-component="board-participant-stack"]').length, 0)
+ assert.equal(workerRecipient.querySelectorAll('[data-component="icon"]').length, 2)
for (const index of [0, 1, 2]) {
if (index) await update(index)
assert.equal(trigger().getAttribute("aria-expanded"), "false")
@@ -129,7 +164,7 @@ try {
trigger().click()
await settle()
visible("latest")
- assert.deepEqual(parsed, ["**latest** body"])
+ assert.deepEqual(parsed, ["**latest** body", "Secondary body"])
trigger().click()
await settle()
@@ -149,6 +184,8 @@ try {
assert.deepEqual(decoded, outputs)
} finally {
dispose()
+ disposeBroadcast()
+ disposeWorkerBroadcast()
JSON.parse = decode
await window.happyDOM.cancelAsync()
await window.happyDOM.close()
diff --git a/packages/kilo-vscode/tests/swarm-board.spec.ts b/packages/kilo-vscode/tests/swarm-board.spec.ts
index 0a046306288..f316f9692c2 100644
--- a/packages/kilo-vscode/tests/swarm-board.spec.ts
+++ b/packages/kilo-vscode/tests/swarm-board.spec.ts
@@ -192,8 +192,14 @@ for (const width of [420, 200]) {
await expect(
routes.first().locator('[data-slot="board-route-recipient-icon"] [data-component="agent-avatar"]'),
).toHaveCount(1)
+ await expect(
+ routes.last().locator('[data-slot="board-route-recipient-icon"] [data-component="board-participant-stack"]'),
+ ).toHaveCount(1)
+ await expect(
+ routes.last().locator('[data-slot="board-route-recipient-icon"] [data-component="agent-avatar"]'),
+ ).toHaveCount(1)
await expect(routes.last().locator('[data-slot="board-route-recipient-icon"] [data-component="icon"]')).toHaveCount(
- 2,
+ 1,
)
})
}
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 35d774322af..b811a51ba7e 100644
--- a/packages/kilo-vscode/webview-ui/src/stories/composite.stories.tsx
+++ b/packages/kilo-vscode/webview-ui/src/stories/composite.stories.tsx
@@ -17,6 +17,7 @@ import { messageTurns } from "../context/session-queue"
import { transcriptRows } from "../context/transcript-rows"
import { ChatView } from "../components/chat/ChatView"
import { Part } from "@kilocode/kilo-ui/message-part"
+import { AgentAvatarPalette } from "@kilocode/kilo-ui/agent-avatar"
import { registerVscodeToolOverrides } from "../components/chat/VscodeToolOverrides"
import { SessionContext } from "../context/session"
import { ServerContext } from "../context/server"
@@ -1396,7 +1397,9 @@ export const AgentMessages: Story = {
const parts = board()
return (
- {(part) => }
+
+ {(part) => }
+
)
},