Skip to content

Commit 00d7ae5

Browse files
committed
fix: avoid fake subagent management entries
1 parent 0c881f2 commit 00d7ae5

2 files changed

Lines changed: 72 additions & 19 deletions

File tree

web/src/lib/chat/__tests__/subagent-management.test.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,4 +102,36 @@ describe('buildSubagentManagementModel', () => {
102102
lastActionLabel: 'Closed',
103103
});
104104
});
105+
106+
it('does not create a fake subagent entry for list operations', () => {
107+
const messages: ChatMessage[] = [
108+
new CodexSubagentToolUseMessage(TS, 'tool-subagent-list', 'list_agents'),
109+
];
110+
111+
const model = buildSubagentManagementModel(messages);
112+
113+
expect(model.subagents).toHaveLength(0);
114+
});
115+
116+
it('applies multi-target lifecycle events to each targeted subagent', () => {
117+
const messages: ChatMessage[] = [
118+
new CodexSubagentToolUseMessage(TS, 'tool-subagent-1', 'spawn_agent', {
119+
taskName: 'review-auth',
120+
}),
121+
new CodexSubagentToolUseMessage(TS, 'tool-subagent-2', 'spawn_agent', {
122+
taskName: 'ui-polish',
123+
}),
124+
new CodexSubagentToolUseMessage(TS, 'tool-subagent-3', 'wait_agent', {
125+
targets: ['/root/review-auth', '/root/ui-polish'],
126+
}),
127+
];
128+
129+
const model = buildSubagentManagementModel(messages);
130+
131+
expect(model.subagents).toHaveLength(2);
132+
expect(model.subagents.map((entry) => [entry.name, entry.status])).toEqual([
133+
['review-auth', 'waiting'],
134+
['ui-polish', 'waiting'],
135+
]);
136+
});
105137
});

web/src/lib/chat/subagent-management.ts

Lines changed: 40 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -68,16 +68,20 @@ export function buildSubagentManagementModel(
6868
if (!(message instanceof CodexSubagentToolUseMessage)) continue;
6969

7070
const result = resultsByToolId.get(message.toolId);
71-
const key = resolveEntryKey(message.details, message.toolId, aliasToKey);
72-
let entry = subagentsByKey.get(key);
73-
if (!entry) {
74-
entry = createSubagentEntry(key, message);
75-
subagentsByKey.set(key, entry);
76-
orderedSubagents.push(entry);
71+
for (const eventDetails of entryDetailsForMessage(message)) {
72+
const key = resolveEntryKey(eventDetails, message.toolId, aliasToKey, message.action);
73+
if (!key) continue;
74+
75+
let entry = subagentsByKey.get(key);
76+
if (!entry) {
77+
entry = createSubagentEntry(key, message, eventDetails);
78+
subagentsByKey.set(key, entry);
79+
orderedSubagents.push(entry);
80+
}
81+
82+
registerAliases(key, eventDetails, aliasToKey);
83+
applySubagentEvent(entry, message, result, eventDetails);
7784
}
78-
79-
registerAliases(key, message.details, aliasToKey);
80-
applySubagentEvent(entry, message, result);
8185
}
8286

8387
return {
@@ -89,14 +93,15 @@ export function buildSubagentManagementModel(
8993
function createSubagentEntry(
9094
key: string,
9195
message: CodexSubagentToolUseMessage,
96+
details: CodexSubagentDetails,
9297
): SubagentManagementEntry {
9398
return {
9499
id: key,
95100
kind: 'subagent',
96-
name: displayNameFor(message.details, message.toolId),
97-
path: message.details.target ?? message.details.pathPrefix ?? message.details.taskName,
98-
model: message.details.model,
99-
message: message.details.message,
101+
name: displayNameFor(details, message.toolId),
102+
path: details.target ?? details.pathPrefix ?? details.taskName,
103+
model: details.model,
104+
message: details.message,
100105
status: 'running',
101106
statusLabel: statusLabelFor('running'),
102107
lastActionLabel: actionLabelFor(message.action),
@@ -108,21 +113,34 @@ function applySubagentEvent(
108113
entry: SubagentManagementEntry,
109114
message: CodexSubagentToolUseMessage,
110115
result: ToolResultMessage | undefined,
116+
details: CodexSubagentDetails,
111117
): void {
112-
entry.name = displayNameFor(message.details, entry.name);
113-
entry.path = message.details.target ?? message.details.pathPrefix ?? entry.path;
114-
entry.model = message.details.model ?? entry.model;
115-
entry.message = message.details.message ?? entry.message;
118+
entry.name = displayNameFor(details, entry.name);
119+
entry.path = details.target ?? details.pathPrefix ?? entry.path;
120+
entry.model = details.model ?? entry.model;
121+
entry.message = details.message ?? entry.message;
116122
entry.lastActionLabel = actionLabelFor(message.action);
117123
entry.status = statusFor(message.action, result?.isError === true);
118124
entry.statusLabel = statusLabelFor(entry.status);
119125
}
120126

127+
function entryDetailsForMessage(message: CodexSubagentToolUseMessage): CodexSubagentDetails[] {
128+
if (message.action === 'list_agents') return [];
129+
const targets = message.details.targets?.filter((target) => target.trim().length > 0) ?? [];
130+
if (targets.length === 0) return [message.details];
131+
return targets.map((target) => ({
132+
...message.details,
133+
target,
134+
targets: undefined,
135+
}));
136+
}
137+
121138
function resolveEntryKey(
122139
details: CodexSubagentDetails,
123140
fallback: string,
124141
aliasToKey: Map<string, string>,
125-
): string {
142+
action: CodexSubagentAction,
143+
): string | null {
126144
const candidates = [
127145
details.target,
128146
details.pathPrefix,
@@ -136,7 +154,10 @@ function resolveEntryKey(
136154
if (existing) return existing;
137155
}
138156

139-
return normalizeAlias(details.target ?? details.pathPrefix ?? details.taskName ?? fallback);
157+
const identity = details.target ?? details.pathPrefix ?? details.taskName;
158+
if (identity) return normalizeAlias(identity);
159+
if (action === 'spawn_agent') return normalizeAlias(fallback);
160+
return null;
140161
}
141162

142163
function registerAliases(

0 commit comments

Comments
 (0)