diff --git a/packages/web/src/__tests__/stepsToMessages.test.ts b/packages/web/src/__tests__/stepsToMessages.test.ts
index 92dd75f..404018d 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 eb0842b..4a3f792 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 ae0b5d7..8e55698 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 ff1ccb0..0387cd9 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 3430973..5b6a5a9 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 b25482d..7242953 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;