diff --git a/extension/src/codex-rollout-parser.ts b/extension/src/codex-rollout-parser.ts
index 60c7ab7..9748952 100644
--- a/extension/src/codex-rollout-parser.ts
+++ b/extension/src/codex-rollout-parser.ts
@@ -386,6 +386,7 @@ export class CodexRolloutParser {
agent: ORCHESTRATOR_NAME,
role,
content: text.slice(0, MESSAGE_MAX),
+ runtime: 'codex',
},
})
this.emitContextUpdate(state)
@@ -575,6 +576,7 @@ export class CodexRolloutParser {
agent: ORCHESTRATOR_NAME,
role: 'thinking',
content: text.slice(0, MESSAGE_MAX),
+ runtime: 'codex',
},
})
this.emitContextUpdate(state)
diff --git a/extension/test/codex-rollout-parser.test.ts b/extension/test/codex-rollout-parser.test.ts
index aac38e4..f8c695d 100644
--- a/extension/test/codex-rollout-parser.test.ts
+++ b/extension/test/codex-rollout-parser.test.ts
@@ -93,6 +93,15 @@ describe('CodexRolloutParser', () => {
assert.equal(thinking[0].payload.content, '**Planning directory listing**')
})
+ it('tags Codex transcript messages with the codex runtime', () => {
+ const { events } = runFixture()
+ const messages = events.filter(e => e.type === 'message')
+ assert.ok(messages.length > 0)
+ for (const event of messages) {
+ assert.equal(event.payload.runtime, 'codex')
+ }
+ })
+
it('pairs function_call with function_call_output by call_id', () => {
const { events, state } = runFixture()
const starts = events.filter(e => e.type === 'tool_call_start' && e.payload.tool === 'exec_command')
diff --git a/web/components/agent-visualizer/canvas/draw-bubbles.ts b/web/components/agent-visualizer/canvas/draw-bubbles.ts
index dc952df..5716b29 100644
--- a/web/components/agent-visualizer/canvas/draw-bubbles.ts
+++ b/web/components/agent-visualizer/canvas/draw-bubbles.ts
@@ -1,4 +1,5 @@
import { Agent, NODE } from '@/lib/agent-types'
+import { getMessageSenderLabel } from '@/lib/agent-runtime'
import { COLORS, withAlpha } from '@/lib/colors'
import { BUBBLE_MAX_W, BUBBLE_GAP, BUBBLE_MAX_LINES, AGENT_DRAW, BUBBLE_DRAW } from '@/lib/canvas-constants'
import { bubbleAlpha } from './bubble-utils'
@@ -29,7 +30,7 @@ export function drawMessageBubblesWorld(
const isThinking = role === 'thinking'
const bgColor = isThinking ? COLORS.bubbleThinkingBase : role === 'user' ? COLORS.bubbleUserBase : COLORS.bubbleAssistantBase
const textColor = isThinking ? COLORS.roleThinkingText : role === 'user' ? COLORS.roleUserText : COLORS.roleAssistantText
- const label = isThinking ? '\uD83D\uDCAD THINKING' : role === 'user' ? 'USER' : 'CLAUDE'
+ const label = isThinking ? `\uD83D\uDCAD ${getMessageSenderLabel(role, agent.runtime)}` : getMessageSenderLabel(role, agent.runtime)
// Thinking bubbles: smaller font, tighter spacing, more translucent
const style = isThinking ? BUBBLE_DRAW.thinking : BUBBLE_DRAW.normal
diff --git a/web/components/agent-visualizer/chat-panel.tsx b/web/components/agent-visualizer/chat-panel.tsx
index f3d2dcf..24e656a 100644
--- a/web/components/agent-visualizer/chat-panel.tsx
+++ b/web/components/agent-visualizer/chat-panel.tsx
@@ -1,6 +1,6 @@
'use client'
-import { CARD, Z, type AgentState } from '@/lib/agent-types'
+import { CARD, Z, type Agent, type AgentState } from '@/lib/agent-types'
import { COLORS, getStateColor } from '@/lib/colors'
import { TranscriptMessage } from './transcript-message'
import type { ConversationMessage } from '@/hooks/simulation/types'
@@ -11,6 +11,7 @@ interface ChatPanelProps {
visible: boolean
agentName: string
agentState: AgentState
+ agentRuntime?: Agent['runtime']
conversation: ConversationMessage[]
onClose: () => void
}
@@ -19,6 +20,7 @@ export function AgentChatPanel({
visible,
agentName,
agentState,
+ agentRuntime,
conversation,
onClose,
}: ChatPanelProps) {
@@ -62,7 +64,7 @@ export function AgentChatPanel({
) : (
conversation.map((msg) => (
-
+
))
)}
diff --git a/web/components/agent-visualizer/index.tsx b/web/components/agent-visualizer/index.tsx
index 4fae266..43dc2fb 100644
--- a/web/components/agent-visualizer/index.tsx
+++ b/web/components/agent-visualizer/index.tsx
@@ -214,9 +214,12 @@ export function AgentVisualizer() {
// Only compute when the transcript panel is visible to avoid O(n log n) sort every frame
const sessionConversation = useMemo(() => {
if (!showTranscript) return []
- const all = Array.from(conversations.values()).flat()
+ const all = Array.from(conversations.entries()).flatMap(([agentId, msgs]) => {
+ const runtime = agents.get(agentId)?.runtime
+ return msgs.map(msg => msg.runtime ? msg : { ...msg, runtime })
+ })
return all.sort((a, b) => a.timestamp - b.timestamp)
- }, [conversations, showTranscript])
+ }, [agents, conversations, showTranscript])
// Context menu items
const contextMenuItems = selection.contextMenu ? (
@@ -326,6 +329,7 @@ export function AgentVisualizer() {
visible={!!selectedAgent}
agentName={selectedAgent?.name ?? ''}
agentState={selectedAgent?.state ?? 'idle'}
+ agentRuntime={selectedAgent?.runtime}
conversation={selectedConversation}
onClose={selection.clearAgent}
/>
diff --git a/web/components/agent-visualizer/message-feed-panel.tsx b/web/components/agent-visualizer/message-feed-panel.tsx
index 3eb8d3c..bf5ae74 100644
--- a/web/components/agent-visualizer/message-feed-panel.tsx
+++ b/web/components/agent-visualizer/message-feed-panel.tsx
@@ -2,6 +2,7 @@
import { useState, useEffect, useRef, useMemo, useCallback } from 'react'
import { Agent, Z, type AgentState } from '@/lib/agent-types'
+import { getMessageSenderLabel } from '@/lib/agent-runtime'
import { COLORS, ROLE_COLORS, getStateColor } from '@/lib/colors'
import type { ConversationMessage } from '@/hooks/simulation/types'
import { useClickOutside } from '@/hooks/use-click-outside'
@@ -285,6 +286,7 @@ export function MessageFeedPanel({
message={msg}
agentId={msg.agentId}
agentName={agents.get(msg.agentId)?.name ?? msg.agentId}
+ agentRuntime={msg.runtime ?? agents.get(msg.agentId)?.runtime}
showAgent={activeTab === 'all'}
isSelected={selectedAgentId === msg.agentId}
onClick={() => { onAgentClick(msg.agentId); setExpanded(false) }}
@@ -332,10 +334,11 @@ function TabButton({ label, active, onClick, color, hasUnread }: {
// ── Message Row ──
-function MessageRow({ message, agentId, agentName, showAgent, isSelected, onClick }: {
+function MessageRow({ message, agentId, agentName, agentRuntime, showAgent, isSelected, onClick }: {
message: ConversationMessage
agentId: string
agentName: string
+ agentRuntime?: Agent['runtime']
showAgent: boolean
isSelected: boolean
onClick: () => void
@@ -357,7 +360,7 @@ function MessageRow({ message, agentId, agentName, showAgent, isSelected, onClic
{/* Header row */}
- {role.label}
+ {getMessageSenderLabel(message.type, message.runtime ?? agentRuntime)}
{showAgent && (
diff --git a/web/components/agent-visualizer/transcript-message.tsx b/web/components/agent-visualizer/transcript-message.tsx
index 9b28829..620719b 100644
--- a/web/components/agent-visualizer/transcript-message.tsx
+++ b/web/components/agent-visualizer/transcript-message.tsx
@@ -1,6 +1,8 @@
'use client'
import { useState } from 'react'
+import type { Agent } from '@/lib/agent-types'
+import { getMessageSenderLabel } from '@/lib/agent-runtime'
import { COLORS } from '@/lib/colors'
import { ToolContentRenderer } from './tool-content-renderer'
import type { ConversationMessage } from '@/hooks/simulation/types'
@@ -21,8 +23,19 @@ export function HighlightText({ text, query }: { text: string; query?: string })
)
}
-export function TranscriptMessage({ message, compact = false, searchQuery }: { message: ConversationMessage; compact?: boolean; searchQuery?: string }) {
+export function TranscriptMessage({
+ message,
+ compact = false,
+ searchQuery,
+ agentRuntime,
+}: {
+ message: ConversationMessage
+ compact?: boolean
+ searchQuery?: string
+ agentRuntime?: Agent['runtime']
+}) {
const [expanded, setExpanded] = useState(false)
+ const runtime = message.runtime ?? agentRuntime
switch (message.type) {
case 'user':
@@ -34,7 +47,9 @@ export function TranscriptMessage({ message, compact = false, searchQuery }: { m
border: `1px solid ${COLORS.userMsgBorder}`,
}}
>
- USER
+
+ {getMessageSenderLabel(message.type, runtime)}
+
@@ -50,7 +65,9 @@ export function TranscriptMessage({ message, compact = false, searchQuery }: { m
border: `1px solid ${COLORS.holoBorder08}`,
}}
>
- CLAUDE
+
+ {getMessageSenderLabel(message.type, runtime)}
+
200 ? '...' : '') : message.content} query={searchQuery} />
@@ -68,7 +85,9 @@ export function TranscriptMessage({ message, compact = false, searchQuery }: { m
onClick={() => setExpanded(!expanded)}
>
- THINKING
+
+ {getMessageSenderLabel(message.type, runtime)}
+
{expanded ? '▾' : '▸'}
{!expanded && (
diff --git a/web/hooks/simulation/handle-agent-events.ts b/web/hooks/simulation/handle-agent-events.ts
index fb134fe..d8db79a 100644
--- a/web/hooks/simulation/handle-agent-events.ts
+++ b/web/hooks/simulation/handle-agent-events.ts
@@ -30,6 +30,7 @@ export function handleAgentSpawn(
state: 'idle',
...(task ? { task } : {}),
...(model ? { tokensMax: ctx.getContextWindowSize(model) } : {}),
+ ...(runtime ? { runtime } : {}),
})
return
}
diff --git a/web/hooks/simulation/handle-message-events.ts b/web/hooks/simulation/handle-message-events.ts
index 6a89e90..31b72b3 100644
--- a/web/hooks/simulation/handle-message-events.ts
+++ b/web/hooks/simulation/handle-message-events.ts
@@ -11,6 +11,11 @@ export function handleMessage(
const agentName = asString(payload.agent)
const content = asString(payload.content)
const role = typeof payload.role === 'string' ? payload.role : undefined
+ const payloadRuntime = payload.runtime === 'codex'
+ ? 'codex' as const
+ : payload.runtime === 'claude'
+ ? 'claude' as const
+ : undefined
// Map role to conversation message type
const msgType: ConversationMessage['type'] =
@@ -49,12 +54,15 @@ export function handleMessage(
updates.state = 'thinking'
}
}
+ if (payloadRuntime && msgAgent.runtime !== payloadRuntime) {
+ updates.runtime = payloadRuntime
+ }
if (Object.keys(updates).length > 0) {
state.agents.set(agentName, { ...msgAgent, ...updates })
}
}
- appendConversation(state.conversations, agentName, { type: msgType, content, timestamp: currentTime })
+ appendConversation(state.conversations, agentName, { type: msgType, content, timestamp: currentTime, runtime: payloadRuntime ?? msgAgent?.runtime })
}
export function handleContextUpdate(
diff --git a/web/hooks/simulation/handle-tool-events.ts b/web/hooks/simulation/handle-tool-events.ts
index fe22292..9d939cd 100644
--- a/web/hooks/simulation/handle-tool-events.ts
+++ b/web/hooks/simulation/handle-tool-events.ts
@@ -89,6 +89,7 @@ export function handleToolCallStart(
appendConversation(state.conversations, agentName, {
type: 'tool_call', content: `> ${toolName} ${args}`, timestamp: currentTime,
+ runtime: agent.runtime,
toolName, inputData,
})
}
@@ -166,6 +167,7 @@ export function handleToolCallEnd(
type: 'tool_result',
content: `< ${result}${tokenCost ? ` (${tokenCost} tokens)` : ''}`,
timestamp: currentTime,
+ runtime: agent.runtime,
toolName,
})
}
diff --git a/web/hooks/simulation/types.ts b/web/hooks/simulation/types.ts
index eda15b7..68d17f7 100644
--- a/web/hooks/simulation/types.ts
+++ b/web/hooks/simulation/types.ts
@@ -57,6 +57,7 @@ export interface ConversationMessage {
type: 'tool_call' | 'tool_result' | 'assistant' | 'user' | 'thinking'
content: string
timestamp: number
+ runtime?: Agent['runtime']
toolName?: string
inputData?: Record
}
diff --git a/web/lib/agent-runtime.ts b/web/lib/agent-runtime.ts
new file mode 100644
index 0000000..43988b9
--- /dev/null
+++ b/web/lib/agent-runtime.ts
@@ -0,0 +1,12 @@
+import type { Agent } from './agent-types'
+
+export function getAgentRuntimeLabel(runtime?: Agent['runtime']): string {
+ return runtime === 'codex' ? 'CODEX' : 'CLAUDE'
+}
+
+export function getMessageSenderLabel(type: string, runtime?: Agent['runtime']): string {
+ if (type === 'user') return 'USER'
+ if (type === 'thinking') return 'THINKING'
+ if (type === 'assistant') return getAgentRuntimeLabel(runtime)
+ return 'TOOL'
+}
diff --git a/web/lib/colors.ts b/web/lib/colors.ts
index 3f7c0b9..e9dadc5 100644
--- a/web/lib/colors.ts
+++ b/web/lib/colors.ts
@@ -219,10 +219,10 @@ export const COLORS = {
// ─── Role Colors (message feed & bubbles) ───────────────────────────────────
-export const ROLE_COLORS: Record = {
- assistant: { bg: COLORS.roleAssistantBg, bgSelected: COLORS.roleAssistantBgSelected, text: COLORS.roleAssistantText, label: 'CLAUDE' },
- thinking: { bg: COLORS.roleThinkingBg, bgSelected: COLORS.roleThinkingBgSelected, text: COLORS.roleThinkingText, label: 'THINKING' },
- user: { bg: COLORS.roleUserBg, bgSelected: COLORS.roleUserBgSelected, text: COLORS.roleUserText, label: 'USER' },
+export const ROLE_COLORS: Record = {
+ assistant: { bg: COLORS.roleAssistantBg, bgSelected: COLORS.roleAssistantBgSelected, text: COLORS.roleAssistantText },
+ thinking: { bg: COLORS.roleThinkingBg, bgSelected: COLORS.roleThinkingBgSelected, text: COLORS.roleThinkingText },
+ user: { bg: COLORS.roleUserBg, bgSelected: COLORS.roleUserBgSelected, text: COLORS.roleUserText },
} as const
// ─── Color Helper Functions ──────────────────────────────────────────────────