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 ──────────────────────────────────────────────────