Skip to content

Commit 2f9f477

Browse files
committed
🤖 fix: validate task settings + reschedule queue
- Clamp/validate Tasks settings inputs and surface save errors\n- Re-run TaskService scheduler after task limit updates\n\n---\n_Generated with `mux` • Model: `openai:gpt-5.2` • Thinking: `xhigh`_\n Change-Id: I5932a79b9d4df8a476e74d1b8a6e65a32a2ad0f9 Signed-off-by: Thomas Kosiewski <tk@coder.com>
1 parent 051d8f4 commit 2f9f477

File tree

9 files changed

+131
-12
lines changed

9 files changed

+131
-12
lines changed

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

Lines changed: 113 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,30 @@
11
import React from "react";
22
import { useAPI } from "@/browser/contexts/API";
3-
import { Button } from "@/browser/components/ui/button";
43
import { Input } from "@/browser/components/ui/input";
4+
import { Button } from "@/browser/components/ui/button";
5+
6+
const MAX_PARALLEL_AGENT_TASKS_MIN = 1;
7+
const MAX_PARALLEL_AGENT_TASKS_MAX = 10;
8+
const MAX_TASK_NESTING_DEPTH_MIN = 1;
9+
const MAX_TASK_NESTING_DEPTH_MAX = 5;
10+
11+
function clampNumber(value: number, min: number, max: number): number {
12+
return Math.min(max, Math.max(min, value));
13+
}
14+
15+
function parseIntOrNull(value: string): number | null {
16+
const parsed = Number.parseInt(value, 10);
17+
return Number.isNaN(parsed) ? null : parsed;
18+
}
519

620
export function TasksSection() {
721
const { api } = useAPI();
822

923
const [maxParallelAgentTasks, setMaxParallelAgentTasks] = React.useState<number>(3);
24+
const [maxParallelAgentTasksInput, setMaxParallelAgentTasksInput] = React.useState<string>("3");
1025
const [maxTaskNestingDepth, setMaxTaskNestingDepth] = React.useState<number>(3);
26+
const [maxTaskNestingDepthInput, setMaxTaskNestingDepthInput] = React.useState<string>("3");
27+
const [error, setError] = React.useState<string | null>(null);
1128
const [isSaving, setIsSaving] = React.useState(false);
1229
const [isLoading, setIsLoading] = React.useState(true);
1330

@@ -29,7 +46,10 @@ export function TasksSection() {
2946
}
3047

3148
setMaxParallelAgentTasks(settings.maxParallelAgentTasks);
49+
setMaxParallelAgentTasksInput(String(settings.maxParallelAgentTasks));
3250
setMaxTaskNestingDepth(settings.maxTaskNestingDepth);
51+
setMaxTaskNestingDepthInput(String(settings.maxTaskNestingDepth));
52+
setError(null);
3353
} finally {
3454
if (!cancelled) {
3555
setIsLoading(false);
@@ -47,16 +67,57 @@ export function TasksSection() {
4767
return;
4868
}
4969

70+
const parsedMaxParallelAgentTasks = parseIntOrNull(maxParallelAgentTasksInput);
71+
const parsedMaxTaskNestingDepth = parseIntOrNull(maxTaskNestingDepthInput);
72+
73+
if (parsedMaxParallelAgentTasks === null || parsedMaxTaskNestingDepth === null) {
74+
setError("Please enter valid numbers for task limits.");
75+
setMaxParallelAgentTasksInput(String(maxParallelAgentTasks));
76+
setMaxTaskNestingDepthInput(String(maxTaskNestingDepth));
77+
return;
78+
}
79+
80+
const nextMaxParallelAgentTasks = clampNumber(
81+
parsedMaxParallelAgentTasks,
82+
MAX_PARALLEL_AGENT_TASKS_MIN,
83+
MAX_PARALLEL_AGENT_TASKS_MAX
84+
);
85+
const nextMaxTaskNestingDepth = clampNumber(
86+
parsedMaxTaskNestingDepth,
87+
MAX_TASK_NESTING_DEPTH_MIN,
88+
MAX_TASK_NESTING_DEPTH_MAX
89+
);
90+
91+
setMaxParallelAgentTasks(nextMaxParallelAgentTasks);
92+
setMaxParallelAgentTasksInput(String(nextMaxParallelAgentTasks));
93+
setMaxTaskNestingDepth(nextMaxTaskNestingDepth);
94+
setMaxTaskNestingDepthInput(String(nextMaxTaskNestingDepth));
95+
5096
setIsSaving(true);
97+
setError(null);
5198
try {
5299
await api.tasks.setTaskSettings({
53-
maxParallelAgentTasks,
54-
maxTaskNestingDepth,
100+
maxParallelAgentTasks: nextMaxParallelAgentTasks,
101+
maxTaskNestingDepth: nextMaxTaskNestingDepth,
55102
});
103+
104+
const saved = await api.tasks.getTaskSettings();
105+
setMaxParallelAgentTasks(saved.maxParallelAgentTasks);
106+
setMaxParallelAgentTasksInput(String(saved.maxParallelAgentTasks));
107+
setMaxTaskNestingDepth(saved.maxTaskNestingDepth);
108+
setMaxTaskNestingDepthInput(String(saved.maxTaskNestingDepth));
109+
} catch (err) {
110+
setError(err instanceof Error ? err.message : "Failed to save task settings");
56111
} finally {
57112
setIsSaving(false);
58113
}
59-
}, [api, maxParallelAgentTasks, maxTaskNestingDepth]);
114+
}, [
115+
api,
116+
maxParallelAgentTasks,
117+
maxParallelAgentTasksInput,
118+
maxTaskNestingDepth,
119+
maxTaskNestingDepthInput,
120+
]);
60121

61122
return (
62123
<div className="space-y-4">
@@ -73,11 +134,30 @@ export function TasksSection() {
73134
<label className="text-sm">Max parallel subagents</label>
74135
<Input
75136
type="number"
76-
min={1}
77-
max={10}
78-
value={maxParallelAgentTasks}
137+
min={MAX_PARALLEL_AGENT_TASKS_MIN}
138+
max={MAX_PARALLEL_AGENT_TASKS_MAX}
139+
step={1}
140+
value={maxParallelAgentTasksInput}
79141
disabled={isLoading}
80-
onChange={(e) => setMaxParallelAgentTasks(Number(e.target.value))}
142+
onChange={(e) => {
143+
setMaxParallelAgentTasksInput(e.target.value);
144+
setError(null);
145+
}}
146+
onBlur={(e) => {
147+
const parsed = parseIntOrNull(e.target.value);
148+
if (parsed === null) {
149+
setMaxParallelAgentTasksInput(String(maxParallelAgentTasks));
150+
return;
151+
}
152+
153+
const clamped = clampNumber(
154+
parsed,
155+
MAX_PARALLEL_AGENT_TASKS_MIN,
156+
MAX_PARALLEL_AGENT_TASKS_MAX
157+
);
158+
setMaxParallelAgentTasks(clamped);
159+
setMaxParallelAgentTasksInput(String(clamped));
160+
}}
81161
/>
82162
</div>
83163
</div>
@@ -87,18 +167,39 @@ export function TasksSection() {
87167
<label className="text-sm">Max nesting depth</label>
88168
<Input
89169
type="number"
90-
min={1}
91-
max={5}
92-
value={maxTaskNestingDepth}
170+
min={MAX_TASK_NESTING_DEPTH_MIN}
171+
max={MAX_TASK_NESTING_DEPTH_MAX}
172+
step={1}
173+
value={maxTaskNestingDepthInput}
93174
disabled={isLoading}
94-
onChange={(e) => setMaxTaskNestingDepth(Number(e.target.value))}
175+
onChange={(e) => {
176+
setMaxTaskNestingDepthInput(e.target.value);
177+
setError(null);
178+
}}
179+
onBlur={(e) => {
180+
const parsed = parseIntOrNull(e.target.value);
181+
if (parsed === null) {
182+
setMaxTaskNestingDepthInput(String(maxTaskNestingDepth));
183+
return;
184+
}
185+
186+
const clamped = clampNumber(
187+
parsed,
188+
MAX_TASK_NESTING_DEPTH_MIN,
189+
MAX_TASK_NESTING_DEPTH_MAX
190+
);
191+
setMaxTaskNestingDepth(clamped);
192+
setMaxTaskNestingDepthInput(String(clamped));
193+
}}
95194
/>
96195
</div>
97196
</div>
98197

99198
<Button onClick={() => void onSave()} disabled={isLoading || isSaving}>
100199
{isSaving ? "Saving..." : "Save"}
101200
</Button>
201+
202+
{error && <div className="text-error text-sm">{error}</div>}
102203
</div>
103204
</div>
104205
);

‎src/cli/cli.test.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ async function createTestServer(authToken?: string): Promise<TestServerHandle> {
5757

5858
// Build context
5959
const context: ORPCContext = {
60+
taskService: services.taskService,
6061
config: services.config,
6162
aiService: services.aiService,
6263
projectService: services.projectService,

‎src/cli/server.test.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ async function createTestServer(): Promise<TestServerHandle> {
6060

6161
// Build context
6262
const context: ORPCContext = {
63+
taskService: services.taskService,
6364
config: services.config,
6465
aiService: services.aiService,
6566
projectService: services.projectService,

‎src/cli/server.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ const mockWindow: BrowserWindow = {
7676

7777
// Build oRPC context from services
7878
const context: ORPCContext = {
79+
taskService: serviceContainer.taskService,
7980
config: serviceContainer.config,
8081
aiService: serviceContainer.aiService,
8182
projectService: serviceContainer.projectService,

‎src/desktop/main.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -323,6 +323,7 @@ async function loadServices(): Promise<void> {
323323
// Build the oRPC context with all services
324324
const orpcContext = {
325325
config: services.config,
326+
taskService: services.taskService,
326327
aiService: services.aiService,
327328
projectService: services.projectService,
328329
workspaceService: services.workspaceService,

‎src/node/orpc/context.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { IncomingHttpHeaders } from "http";
22
import type { Config } from "@/node/config";
33
import type { AIService } from "@/node/services/aiService";
4+
import type { TaskService } from "@/node/services/taskService";
45
import type { ProjectService } from "@/node/services/projectService";
56
import type { WorkspaceService } from "@/node/services/workspaceService";
67
import type { ProviderService } from "@/node/services/providerService";
@@ -20,6 +21,7 @@ import type { SessionUsageService } from "@/node/services/sessionUsageService";
2021

2122
export interface ORPCContext {
2223
config: Config;
24+
taskService: TaskService;
2325
aiService: AIService;
2426
projectService: ProjectService;
2527
workspaceService: WorkspaceService;

‎src/node/orpc/router.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ export const router = (authToken?: string) => {
8080
.output(schemas.tasks.setTaskSettings.output)
8181
.handler(async ({ context, input }) => {
8282
await context.config.setTaskSettings(input);
83+
await context.taskService.rescheduleQueuedTasks();
8384
}),
8485
},
8586
server: {

‎src/node/services/taskService.ts‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,16 @@ export class TaskService {
120120
await this.queueScheduling();
121121
}
122122

123+
/**
124+
* Trigger a scheduler pass (e.g. after task settings change).
125+
*
126+
* Note: This does not block on any agent model calls; it only advances queued tasks into
127+
* "running" and kicks off their resume streams.
128+
*/
129+
async rescheduleQueuedTasks(): Promise<void> {
130+
await this.queueScheduling();
131+
}
132+
123133
private formatErrorForReport(error: unknown): string {
124134
if (error instanceof Error) {
125135
return error.message;

‎tests/ipc/setup.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ export async function createTestEnvironment(): Promise<TestEnvironment> {
6969
services.windowService.setMainWindow(mockWindow);
7070

7171
const orpcContext: ORPCContext = {
72+
taskService: services.taskService,
7273
config: services.config,
7374
aiService: services.aiService,
7475
projectService: services.projectService,

0 commit comments

Comments
 (0)