Skip to content

Commit d388d0b

Browse files
committed
🤖 fix: address review feedback for subagents implementation
- Fix processQueue parallel limit enforcement - re-check running count each iteration instead of once before the loop - Add args field to tool-call-end event schema so TaskService can receive agent_report arguments for report delivery - Fix config→metadata passing parentWorkspaceId/agentType/taskState fields so UI nesting works correctly - Deduplicate sortWithNesting function - export from workspaceFiltering.ts and import in useSortedWorkspacesByProject.ts (DRY violation fix) - Add error handling to TasksSection.tsx - properly catch API errors instead of leaving UI stuck in 'Loading...' state - Extract magic numbers for indentation to named constants (BASE_PADDING_PX, NESTING_INDENT_PX) - Update tests and mock types to include new args field in tool-call-end events Change-Id: I7fcc69e8616f758a68b5864ebe6afb318dafbb85 Signed-off-by: Thomas Kosiewski <tk@coder.com>
1 parent f630c09 commit d388d0b

File tree

13 files changed

+83
-73
lines changed

13 files changed

+83
-73
lines changed

src/browser/components/Settings/sections/TasksSection.tsx

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,19 +18,28 @@ export function TasksSection() {
1818
const { api } = useAPI();
1919
const [settings, setSettings] = useState<TaskSettings>(DEFAULT_TASK_SETTINGS);
2020
const [loaded, setLoaded] = useState(false);
21+
const [loadError, setLoadError] = useState<string | null>(null);
2122

2223
// Load settings on mount
2324
useEffect(() => {
2425
if (api) {
25-
void api.general.getTaskSettings().then((taskSettings) => {
26-
setSettings({
27-
maxParallelAgentTasks:
28-
taskSettings.maxParallelAgentTasks ?? DEFAULT_TASK_SETTINGS.maxParallelAgentTasks,
29-
maxTaskNestingDepth:
30-
taskSettings.maxTaskNestingDepth ?? DEFAULT_TASK_SETTINGS.maxTaskNestingDepth,
26+
api.general
27+
.getTaskSettings()
28+
.then((taskSettings) => {
29+
setSettings({
30+
maxParallelAgentTasks:
31+
taskSettings.maxParallelAgentTasks ?? DEFAULT_TASK_SETTINGS.maxParallelAgentTasks,
32+
maxTaskNestingDepth:
33+
taskSettings.maxTaskNestingDepth ?? DEFAULT_TASK_SETTINGS.maxTaskNestingDepth,
34+
});
35+
setLoaded(true);
36+
})
37+
.catch((error) => {
38+
console.error("Failed to load task settings:", error);
39+
setLoadError("Failed to load settings");
40+
// Still mark as loaded so user can interact with defaults
41+
setLoaded(true);
3142
});
32-
setLoaded(true);
33-
});
3443
}
3544
}, [api]);
3645

@@ -82,6 +91,9 @@ export function TasksSection() {
8291
<p className="text-muted mb-4 text-xs">
8392
Configure limits for agent subworkspaces spawned via the task tool.
8493
</p>
94+
{loadError && (
95+
<p className="mb-4 text-xs text-red-500">{loadError}. Using default values.</p>
96+
)}
8597

8698
<div className="space-y-4">
8799
<div className="flex items-center justify-between">

src/browser/components/WorkspaceListItem.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -105,8 +105,12 @@ const WorkspaceListItemInner: React.FC<WorkspaceListItemProps> = ({
105105
const { canInterrupt, awaitingUserQuestion } = useWorkspaceSidebarState(workspaceId);
106106
const isWorking = canInterrupt && !awaitingUserQuestion;
107107

108-
// Calculate left padding based on nesting depth (base 9px + 16px per level)
109-
const leftPadding = 9 + nestingDepth * 16;
108+
// Sidebar indentation constants
109+
const BASE_PADDING_PX = 9;
110+
const NESTING_INDENT_PX = 16;
111+
112+
// Calculate left padding based on nesting depth
113+
const leftPadding = BASE_PADDING_PX + nestingDepth * NESTING_INDENT_PX;
110114
const isAgentTask = Boolean(metadata.parentWorkspaceId);
111115

112116
return (

src/browser/hooks/useSortedWorkspacesByProject.ts

Lines changed: 3 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -3,65 +3,10 @@ import { useWorkspaceContext } from "@/browser/contexts/WorkspaceContext";
33
import type { FrontendWorkspaceMetadata } from "@/common/types/workspace";
44
import { useWorkspaceRecency } from "@/browser/stores/WorkspaceStore";
55
import { useStableReference, compareMaps } from "@/browser/hooks/useStableReference";
6+
import { sortWithNesting, type WorkspaceWithNesting } from "@/browser/utils/ui/workspaceFiltering";
67

7-
/** Workspace metadata extended with computed nesting depth */
8-
export interface WorkspaceWithNesting extends FrontendWorkspaceMetadata {
9-
/** Nesting depth (0 = top-level, 1 = direct child, etc.) */
10-
nestingDepth: number;
11-
}
12-
13-
/**
14-
* Sort workspaces so children appear immediately after their parent.
15-
* Maintains recency order within each level.
16-
*/
17-
function sortWithNesting(
18-
metadataList: FrontendWorkspaceMetadata[],
19-
workspaceRecency: Record<string, number>
20-
): WorkspaceWithNesting[] {
21-
// Build parent→children map
22-
const childrenByParent = new Map<string, FrontendWorkspaceMetadata[]>();
23-
const topLevel: FrontendWorkspaceMetadata[] = [];
24-
25-
for (const ws of metadataList) {
26-
const parentId = ws.parentWorkspaceId;
27-
if (parentId) {
28-
const siblings = childrenByParent.get(parentId) ?? [];
29-
siblings.push(ws);
30-
childrenByParent.set(parentId, siblings);
31-
} else {
32-
topLevel.push(ws);
33-
}
34-
}
35-
36-
// Sort by recency (most recent first)
37-
const sortByRecency = (a: FrontendWorkspaceMetadata, b: FrontendWorkspaceMetadata) => {
38-
const aTs = workspaceRecency[a.id] ?? 0;
39-
const bTs = workspaceRecency[b.id] ?? 0;
40-
return bTs - aTs;
41-
};
42-
43-
topLevel.sort(sortByRecency);
44-
for (const children of childrenByParent.values()) {
45-
children.sort(sortByRecency);
46-
}
47-
48-
// Flatten: parent, then children recursively
49-
const result: WorkspaceWithNesting[] = [];
50-
51-
const visit = (ws: FrontendWorkspaceMetadata, depth: number) => {
52-
result.push({ ...ws, nestingDepth: depth });
53-
const children = childrenByParent.get(ws.id) ?? [];
54-
for (const child of children) {
55-
visit(child, depth + 1);
56-
}
57-
};
58-
59-
for (const ws of topLevel) {
60-
visit(ws, 0);
61-
}
62-
63-
return result;
64-
}
8+
// Re-export for backward compatibility
9+
export type { WorkspaceWithNesting };
6510

6611
export function useSortedWorkspacesByProject() {
6712
const { projects } = useProjectContext();

src/browser/utils/messages/StreamingMessageAggregator.status.test.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@ describe("StreamingMessageAggregator - Agent Status", () => {
131131
// Complete the tool call
132132
aggregator.handleToolCallEnd({
133133
type: "tool-call-end",
134+
args: {},
134135
workspaceId: "workspace1",
135136
messageId,
136137
toolCallId,
@@ -173,6 +174,7 @@ describe("StreamingMessageAggregator - Agent Status", () => {
173174

174175
aggregator.handleToolCallEnd({
175176
type: "tool-call-end",
177+
args: {},
176178
workspaceId: "workspace1",
177179
messageId,
178180
toolCallId: "tool1",
@@ -197,6 +199,7 @@ describe("StreamingMessageAggregator - Agent Status", () => {
197199

198200
aggregator.handleToolCallEnd({
199201
type: "tool-call-end",
202+
args: {},
200203
workspaceId: "workspace1",
201204
messageId,
202205
toolCallId: "tool2",
@@ -237,6 +240,7 @@ describe("StreamingMessageAggregator - Agent Status", () => {
237240

238241
aggregator.handleToolCallEnd({
239242
type: "tool-call-end",
243+
args: {},
240244
workspaceId: "workspace1",
241245
messageId,
242246
toolCallId: "tool1",
@@ -290,6 +294,7 @@ describe("StreamingMessageAggregator - Agent Status", () => {
290294
// Complete with failure
291295
aggregator.handleToolCallEnd({
292296
type: "tool-call-end",
297+
args: {},
293298
workspaceId: "workspace1",
294299
messageId,
295300
toolCallId: "tool1",
@@ -328,6 +333,7 @@ describe("StreamingMessageAggregator - Agent Status", () => {
328333

329334
aggregator.handleToolCallEnd({
330335
type: "tool-call-end",
336+
args: {},
331337
workspaceId: "workspace1",
332338
messageId: "msg1",
333339
toolCallId: "tool1",
@@ -393,6 +399,7 @@ describe("StreamingMessageAggregator - Agent Status", () => {
393399
// Complete with validation failure
394400
aggregator.handleToolCallEnd({
395401
type: "tool-call-end",
402+
args: {},
396403
workspaceId: "workspace1",
397404
messageId,
398405
toolCallId: "tool1",
@@ -453,6 +460,7 @@ describe("StreamingMessageAggregator - Agent Status", () => {
453460
// Complete successfully
454461
aggregator.handleToolCallEnd({
455462
type: "tool-call-end",
463+
args: {},
456464
workspaceId: "workspace1",
457465
messageId,
458466
toolCallId: "tool1",
@@ -699,6 +707,7 @@ describe("StreamingMessageAggregator - Agent Status", () => {
699707

700708
aggregator.handleToolCallEnd({
701709
type: "tool-call-end",
710+
args: {},
702711
workspaceId: "workspace1",
703712
messageId,
704713
toolCallId,
@@ -744,6 +753,7 @@ describe("StreamingMessageAggregator - Agent Status", () => {
744753
// Complete the tool call
745754
aggregator.handleToolCallEnd({
746755
type: "tool-call-end",
756+
args: {},
747757
workspaceId: "workspace1",
748758
messageId,
749759
toolCallId,
@@ -788,6 +798,7 @@ describe("StreamingMessageAggregator - Agent Status", () => {
788798

789799
aggregator.handleToolCallEnd({
790800
type: "tool-call-end",
801+
args: {},
791802
workspaceId: "workspace1",
792803
messageId,
793804
toolCallId: "tool1",
@@ -812,6 +823,7 @@ describe("StreamingMessageAggregator - Agent Status", () => {
812823

813824
aggregator.handleToolCallEnd({
814825
type: "tool-call-end",
826+
args: {},
815827
workspaceId: "workspace1",
816828
messageId,
817829
toolCallId: "tool2",
@@ -840,6 +852,7 @@ describe("StreamingMessageAggregator - Agent Status", () => {
840852

841853
aggregator.handleToolCallEnd({
842854
type: "tool-call-end",
855+
args: {},
843856
workspaceId: "workspace1",
844857
messageId,
845858
toolCallId: "tool3",
@@ -883,6 +896,7 @@ describe("StreamingMessageAggregator - Agent Status", () => {
883896

884897
aggregator.handleToolCallEnd({
885898
type: "tool-call-end",
899+
args: {},
886900
workspaceId: "workspace1",
887901
messageId: messageId1,
888902
toolCallId: "tool1",
@@ -930,6 +944,7 @@ describe("StreamingMessageAggregator - Agent Status", () => {
930944

931945
aggregator.handleToolCallEnd({
932946
type: "tool-call-end",
947+
args: {},
933948
workspaceId: "workspace1",
934949
messageId: messageId2,
935950
toolCallId: "tool2",

src/browser/utils/messages/StreamingMessageAggregator.test.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,7 @@ describe("StreamingMessageAggregator", () => {
171171

172172
aggregator.handleToolCallEnd({
173173
type: "tool-call-end",
174+
args: {},
174175
workspaceId: "test-workspace",
175176
messageId: "msg1",
176177
toolCallId: "tool1",
@@ -228,6 +229,7 @@ describe("StreamingMessageAggregator", () => {
228229

229230
aggregator.handleToolCallEnd({
230231
type: "tool-call-end",
232+
args: {},
231233
workspaceId: "test-workspace",
232234
messageId: "msg1",
233235
toolCallId: "tool1",
@@ -363,6 +365,7 @@ describe("StreamingMessageAggregator", () => {
363365

364366
aggregator.handleToolCallEnd({
365367
type: "tool-call-end",
368+
args: {},
366369
workspaceId: "test-workspace",
367370
messageId: "msg1",
368371
toolCallId: "tool1",

src/browser/utils/ui/workspaceFiltering.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ export interface WorkspaceWithNesting extends FrontendWorkspaceMetadata {
2020
* Sort workspaces so children appear immediately after their parent.
2121
* Maintains recency order within each level.
2222
*/
23-
function sortWithNesting(
23+
export function sortWithNesting(
2424
metadataList: FrontendWorkspaceMetadata[],
2525
workspaceRecency: Record<string, number>
2626
): WorkspaceWithNesting[] {

src/common/orpc/schemas/stream.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,7 @@ export const ToolCallEndEventSchema = z.object({
174174
messageId: z.string(),
175175
toolCallId: z.string(),
176176
toolName: z.string(),
177+
args: z.unknown().meta({ description: "Tool input arguments (for listeners that need args)" }),
177178
result: z.unknown(),
178179
timestamp: z.number().meta({ description: "When tool call completed (Date.now())" }),
179180
});

src/node/config.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -345,6 +345,10 @@ export class Config {
345345
// GUARANTEE: All workspaces must have runtimeConfig (apply default if missing)
346346
runtimeConfig: workspace.runtimeConfig ?? DEFAULT_RUNTIME_CONFIG,
347347
aiSettings: workspace.aiSettings,
348+
// Agent task workspace fields (optional - only set for subagent workspaces)
349+
parentWorkspaceId: workspace.parentWorkspaceId,
350+
agentType: workspace.agentType,
351+
taskState: workspace.taskState,
348352
};
349353

350354
// Migrate missing createdAt to config for next load

src/node/services/mock/mockScenarioPlayer.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -316,6 +316,7 @@ export class MockScenarioPlayer {
316316
messageId,
317317
toolCallId: event.toolCallId,
318318
toolName: event.toolName,
319+
args: event.args,
319320
result: event.result,
320321
timestamp: Date.now(),
321322
};

src/node/services/mock/scenarioTypes.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ export interface MockToolEndEvent extends MockAssistantEventBase {
6161
kind: "tool-end";
6262
toolCallId: string;
6363
toolName: string;
64+
args?: unknown;
6465
result: unknown;
6566
}
6667

0 commit comments

Comments
 (0)