From 6a15159bdc00e2b6ad5fd8806599cdc135c9e237 Mon Sep 17 00:00:00 2001 From: johnjheejin Date: Wed, 29 Jul 2026 14:41:00 +0900 Subject: [PATCH] feat(web): add subagent status card UI and message step transform --- .../web/src/__tests__/stepsToMessages.test.ts | 28 ++++++++ packages/web/src/components/ChatPanel.tsx | 8 +++ packages/web/src/components/Icons.tsx | 8 +++ packages/web/src/components/StepCards.tsx | 64 +++++++++++++++++++ packages/web/src/styles/step-cards.css | 48 ++++++++++++++ .../web/src/transforms/stepsToMessages.ts | 20 ++++++ 6 files changed, 176 insertions(+) diff --git a/packages/web/src/__tests__/stepsToMessages.test.ts b/packages/web/src/__tests__/stepsToMessages.test.ts index 92dd75f6..404018da 100644 --- a/packages/web/src/__tests__/stepsToMessages.test.ts +++ b/packages/web/src/__tests__/stepsToMessages.test.ts @@ -428,4 +428,32 @@ describe("stepsToMessages", () => { expect(msgs[3].role).toBe("assistant"); expect(msgs).toHaveLength(4); }); + + // ── Subagent steps ── + + it("converts invoke_subagent tool call to subagent system message", () => { + const step: TrajectoryStep = { + type: "CORTEX_STEP_TYPE_TOOL_CALL", + metadata: { + toolCall: { + name: "invoke_subagent", + argumentsJson: JSON.stringify({ + Subagents: [ + { + Role: "Config Auditor", + TypeName: "research", + Prompt: "Audit all config files", + }, + ], + toolAction: "Invoking research subagent", + }), + }, + }, + }; + + const msgs = stepsToMessages([step]); + expect(msgs).toHaveLength(1); + expect(msgs[0].type).toBe("CORTEX_STEP_TYPE_SUBAGENT"); + expect(msgs[0].step).toBe(step); + }); }); diff --git a/packages/web/src/components/ChatPanel.tsx b/packages/web/src/components/ChatPanel.tsx index eb0842b1..4a3f7923 100644 --- a/packages/web/src/components/ChatPanel.tsx +++ b/packages/web/src/components/ChatPanel.tsx @@ -24,6 +24,7 @@ import { CommandCard, CodeActionCard, FilePermissionCard, + SubagentCard, } from "./StepCards"; import { getAskQuestionRequest, getFilePermissionRequest } from "../utils/stepCards"; import { @@ -313,6 +314,13 @@ function SystemMessage({ ); } + if (msg.type === "CORTEX_STEP_TYPE_SUBAGENT") { + return ( +
+ +
+ ); + } } return ( diff --git a/packages/web/src/components/Icons.tsx b/packages/web/src/components/Icons.tsx index ae0b5d74..8e556980 100644 --- a/packages/web/src/components/Icons.tsx +++ b/packages/web/src/components/Icons.tsx @@ -247,3 +247,11 @@ export const IconGear = ({ size = 16, className }: IconProps) => export const IconChevronLeft = ({ size = 16, className }: IconProps) => d(size, className, "m15 18-6-6 6-6"); + +export const IconUsers = ({ size = 16, className }: IconProps) => + m(size, className, [ + "M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2", + "M9 11a4 4 0 1 0 0-8 4 4 0 0 0 0 8z", + "M22 21v-2a4 4 0 0 0-3-3.87", + "M16 3.13a4 4 0 0 1 0 7.75", + ]); diff --git a/packages/web/src/components/StepCards.tsx b/packages/web/src/components/StepCards.tsx index ff1ccb01..0387cd95 100644 --- a/packages/web/src/components/StepCards.tsx +++ b/packages/web/src/components/StepCards.tsx @@ -8,6 +8,7 @@ import { IconFileText, IconLock, IconMessageCircle, + IconUsers, } from "./Icons"; import type { AskQuestionEntry, @@ -592,3 +593,66 @@ export function CodeActionCard({ step }: CodeActionCardProps) { ); } + +// ── Subagent Card ── + +export function SubagentCard({ step }: { step: TrajectoryStep }) { + const [expanded, setExpanded] = useState(false); + const toolCall = step.metadata?.toolCall; + const toolName = toolCall?.name ?? "invoke_subagent"; + + let subagents: Array<{ Role?: string; TypeName?: string; Prompt?: string; Model?: string }> = []; + let toolAction = (step as any).toolAction ?? ""; + let toolSummary = (step as any).toolSummary ?? ""; + + if (toolCall?.argumentsJson) { + try { + const parsed = JSON.parse(toolCall.argumentsJson); + if (Array.isArray(parsed.Subagents)) { + subagents = parsed.Subagents; + } + if (parsed.toolAction) toolAction = parsed.toolAction; + if (parsed.toolSummary) toolSummary = parsed.toolSummary; + } catch { + // ignore + } + } + + const primaryRole = + subagents[0]?.Role || + toolSummary || + (toolName === "define_subagent" ? "Subagent Defined" : "Subagent Invoked"); + const primaryTypeName = subagents[0]?.TypeName || "subagent"; + const promptText = subagents[0]?.Prompt || ""; + const isRunning = + step.status === "CORTEX_STEP_STATUS_RUNNING" || + step.status === "CORTEX_STEP_STATUS_WAITING"; + + return ( +
+ + {expanded && promptText && ( +
+
Instructions:
+
{promptText}
+
+ )} +
+ ); +} diff --git a/packages/web/src/styles/step-cards.css b/packages/web/src/styles/step-cards.css index 3430973c..5b6a5a94 100644 --- a/packages/web/src/styles/step-cards.css +++ b/packages/web/src/styles/step-cards.css @@ -688,3 +688,51 @@ border-color: var(--accent-hover); box-shadow: 0 2px 12px rgba(99, 102, 241, 0.3); } + +/* ── Subagent Card ── */ + +.subagent-card .subagent-role { + font-weight: 600; + color: var(--text-primary); + margin-right: 6px; +} + +.subagent-type-badge { + font-size: 10px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.04em; + padding: 2px 6px; + border-radius: 4px; + background: var(--bg-hover); + color: var(--accent); + border: 1px solid var(--border-subtle); + margin-right: 8px; +} + +.step-card-subagent-prompt { + border-top: 1px solid var(--border-subtle); + padding: 10px 12px; + font-size: 12px; + background: var(--bg-surface); + border-bottom-left-radius: var(--radius-md); + border-bottom-right-radius: var(--radius-md); +} + +.subagent-prompt-label { + font-weight: 600; + color: var(--text-muted); + margin-bottom: 4px; + font-size: 11px; + text-transform: uppercase; + letter-spacing: 0.03em; +} + +.subagent-prompt-text { + white-space: pre-wrap; + word-break: break-word; + font-family: var(--font-mono); + color: var(--text-secondary); + margin: 0; + line-height: 1.4; +} diff --git a/packages/web/src/transforms/stepsToMessages.ts b/packages/web/src/transforms/stepsToMessages.ts index b25482d4..72429532 100644 --- a/packages/web/src/transforms/stepsToMessages.ts +++ b/packages/web/src/transforms/stepsToMessages.ts @@ -46,6 +46,26 @@ export function stepsToMessages(steps: TrajectoryStep[]): ChatMessage[] { continue; } + const toolName = step.metadata?.toolCall?.name ?? ""; + const isSubagent = + toolName === "invoke_subagent" || + toolName === "define_subagent" || + toolName === "manage_subagents" || + toolName === "send_message" || + type === "CORTEX_STEP_TYPE_INVOKE_SUBAGENT" || + type === "CORTEX_STEP_TYPE_SUBAGENT"; + + if (isSubagent) { + messages.push({ + role: "system", + content: "", + stepIndex: i, + type: "CORTEX_STEP_TYPE_SUBAGENT", + step, + }); + continue; + } + if (type === "CORTEX_STEP_TYPE_USER_INPUT" && step.userInput?.items) { const text = textFromItems(step.userInput.items); const media = step.userInput.media;