Skip to content

Commit b27acfe

Browse files
committed
feat: implement configurable multi-agent runtime limits with a new UI section and settings store integration
1 parent 0e0286b commit b27acfe

6 files changed

Lines changed: 169 additions & 15 deletions

File tree

src-tauri/src/codex/config/mod.rs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,45 @@ use std::path::PathBuf;
77

88
use self::mcp::McpServerConfig;
99

10+
/// Multi-agent runtime limits, mirrors the upstream `[agents]` TOML section.
11+
/// Defaults align with CodexMonitor conventions:
12+
/// max_threads = 6 (upstream Codex default)
13+
/// max_depth = 1 (CodexMonitor product default)
14+
#[derive(Debug, Clone, Serialize, Deserialize)]
15+
pub struct AgentsConfig {
16+
/// Maximum number of concurrent sub-agent threads (UI cap: 12).
17+
#[serde(default = "AgentsConfig::default_max_threads")]
18+
pub max_threads: u32,
19+
/// Maximum agent spawning depth (UI cap: 4).
20+
#[serde(default = "AgentsConfig::default_max_depth")]
21+
pub max_depth: u32,
22+
}
23+
24+
impl AgentsConfig {
25+
fn default_max_threads() -> u32 {
26+
6
27+
}
28+
fn default_max_depth() -> u32 {
29+
1
30+
}
31+
}
32+
33+
impl Default for AgentsConfig {
34+
fn default() -> Self {
35+
Self {
36+
max_threads: Self::default_max_threads(),
37+
max_depth: Self::default_max_depth(),
38+
}
39+
}
40+
}
41+
1042
#[derive(Debug, Clone, Serialize, Deserialize)]
1143
pub struct CodexConfig {
1244
#[serde(default)]
1345
pub mcp_servers: HashMap<String, McpServerConfig>,
46+
/// Multi-agent runtime configuration parsed from `[agents]`.
47+
#[serde(default)]
48+
pub agents: AgentsConfig,
1449
}
1550

1651
pub fn get_config_path() -> Result<PathBuf, String> {

src/components/settings/SettingsView.tsx

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,12 @@ import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/component
2121
import { GeneralSettings } from './GeneralSettings';
2222
import { ExplorerSettings } from './ExplorerSettings';
2323
import { useLayoutStore } from '@/stores/settings';
24-
import { ConfigSettings, ArchivedThreadSettings, PersonalizationSettings } from './codex';
24+
import {
25+
ConfigSettings,
26+
ArchivedThreadSettings,
27+
PersonalizationSettings,
28+
SettingsAgentsSection,
29+
} from './codex';
2530
import { ClaudeSettings } from './ClaudeSettings';
2631
import { CodexAuth } from '../codex/CodexAuth';
2732
import { QuoteSettings } from './QuoteSettings';
@@ -41,10 +46,11 @@ type SettingsSection =
4146
| 'explorer'
4247
| 'quote'
4348
| 'task'
49+
| 'agents'
4450
| 'claude'
4551
| 'remote';
4652

47-
const codexSections = ['codexauth', 'task', 'config', 'personalization', 'archived'] as const;
53+
const codexSections = ['codexauth', 'task', 'agents', 'config', 'personalization', 'archived'] as const;
4854
const topLevelSections = ['general', 'projects', 'claude', 'explorer', 'quote', 'remote'] as const;
4955

5056
const sectionLabel: Record<SettingsSection, string> = {
@@ -57,6 +63,7 @@ const sectionLabel: Record<SettingsSection, string> = {
5763
explorer: 'Explorer',
5864
quote: 'Quote',
5965
task: 'Task',
66+
agents: 'Multi-agent',
6067
claude: 'Claude',
6168
remote: 'Remote',
6269
};
@@ -78,6 +85,7 @@ export function SettingsView() {
7885
{activeSection === 'explorer' && <ExplorerSettings />}
7986
{activeSection === 'quote' && <QuoteSettings />}
8087
{activeSection === 'task' && <TaskSettings />}
88+
{activeSection === 'agents' && <SettingsAgentsSection />}
8189
{activeSection === 'claude' && <ClaudeSettings />}
8290
{activeSection === 'remote' && <RemoteSettings />}
8391
</>
@@ -157,10 +165,11 @@ export function SettingsView() {
157165
<button
158166
type="button"
159167
onClick={() => setActiveSection(section)}
160-
className={`w-full rounded-lg px-3 py-2 text-left transition-colors ${activeSection === section
161-
? 'bg-accent text-foreground'
162-
: 'text-muted-foreground hover:bg-accent/50 hover:text-foreground'
163-
}`}
168+
className={`w-full rounded-lg px-3 py-2 text-left transition-colors ${
169+
activeSection === section
170+
? 'bg-accent text-foreground'
171+
: 'text-muted-foreground hover:bg-accent/50 hover:text-foreground'
172+
}`}
164173
>
165174
{sectionLabel[section]}
166175
</button>
@@ -185,10 +194,11 @@ export function SettingsView() {
185194
key={section}
186195
type="button"
187196
onClick={() => setActiveSection(section)}
188-
className={`w-full rounded-lg px-6 py-2 text-left transition-colors ${activeSection === section
189-
? 'bg-accent text-foreground'
190-
: 'text-muted-foreground hover:bg-accent/50 hover:text-foreground'
191-
}`}
197+
className={`w-full rounded-lg px-6 py-2 text-left transition-colors ${
198+
activeSection === section
199+
? 'bg-accent text-foreground'
200+
: 'text-muted-foreground hover:bg-accent/50 hover:text-foreground'
201+
}`}
192202
>
193203
{sectionLabel[section]}
194204
</button>
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import { Input } from '@/components/ui/input';
2+
import { Label } from '@/components/ui/label';
3+
import { useSettingsStore, AGENTS_MAX_THREADS_CAP, AGENTS_MAX_DEPTH_CAP } from '@/stores/settings';
4+
5+
/**
6+
* Settings panel for multi-agent runtime limits.
7+
* Mirrors the `[agents]` TOML section that Codex reads from ~/.codex/config.toml.
8+
* Product caps: max_threads ≤ 12, max_depth ≤ 4.
9+
*/
10+
export function SettingsAgentsSection() {
11+
const {
12+
agentsMaxThreads,
13+
agentsMaxDepth,
14+
setAgentsMaxThreads,
15+
setAgentsMaxDepth,
16+
} = useSettingsStore();
17+
18+
const handleThreadsChange = (e: React.ChangeEvent<HTMLInputElement>) => {
19+
const val = parseInt(e.target.value, 10);
20+
if (!isNaN(val)) setAgentsMaxThreads(val);
21+
};
22+
23+
const handleDepthChange = (e: React.ChangeEvent<HTMLInputElement>) => {
24+
const val = parseInt(e.target.value, 10);
25+
if (!isNaN(val)) setAgentsMaxDepth(val);
26+
};
27+
28+
return (
29+
<div className="w-full px-2 sm:px-4 space-y-6">
30+
<section className="space-y-3">
31+
<h3 className="text-sm sm:text-base font-medium">Multi-agent</h3>
32+
<div className="flex flex-col gap-3 sm:gap-4">
33+
34+
{/* max_threads */}
35+
<div className="flex items-center justify-between gap-3 rounded-md border p-3 sm:p-4">
36+
<div className="space-y-0.5 flex-1 min-w-0">
37+
<Label className="text-xs sm:text-sm font-medium">Thread limit</Label>
38+
<p className="text-xs text-muted-foreground">
39+
Maximum number of concurrent sub-agent threads (1–{AGENTS_MAX_THREADS_CAP}).
40+
Injected as <code className="text-xs">agents.max_threads</code> at thread start.
41+
</p>
42+
</div>
43+
<Input
44+
type="number"
45+
min={1}
46+
max={AGENTS_MAX_THREADS_CAP}
47+
value={agentsMaxThreads}
48+
onChange={handleThreadsChange}
49+
className="w-20 text-right"
50+
/>
51+
</div>
52+
53+
{/* max_depth */}
54+
<div className="flex items-center justify-between gap-3 rounded-md border p-3 sm:p-4">
55+
<div className="space-y-0.5 flex-1 min-w-0">
56+
<Label className="text-xs sm:text-sm font-medium">Spawn depth</Label>
57+
<p className="text-xs text-muted-foreground">
58+
How many levels deep agents may spawn child agents (1–{AGENTS_MAX_DEPTH_CAP}).
59+
Injected as <code className="text-xs">agents.max_depth</code> at thread start.
60+
</p>
61+
</div>
62+
<Input
63+
type="number"
64+
min={1}
65+
max={AGENTS_MAX_DEPTH_CAP}
66+
value={agentsMaxDepth}
67+
onChange={handleDepthChange}
68+
className="w-20 text-right"
69+
/>
70+
</div>
71+
72+
</div>
73+
</section>
74+
</div>
75+
);
76+
}

src/components/settings/codex/index.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,5 @@ export * from './ArchivedThreadSettings';
22
export * from './ConfigSettings';
33
export * from './PersonalizationSettings';
44
export * from './RateLimitSettings';
5-
export * from './TaskSettings';
5+
export * from './SettingsAgentsSection';
6+
export * from './TaskSettings';

src/services/codexService.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import type { CollaborationMode } from '@/bindings';
3030
import type { ThreadListItem } from '@/types/codex/ThreadListItem';
3131
import { useCodexStore, useConfigStore, type ModeKind } from '@/stores/codex';
3232
import { useWorkspaceStore } from '@/stores';
33+
import { useSettingsStore } from '@/stores/settings';
3334
import { convertThreadHistoryToEvents } from '@/utils/threadHistoryConverter';
3435
import { getErrorMessage } from '@/utils/errorUtils';
3536

@@ -72,6 +73,16 @@ const generateWorktreeKey = (): string => {
7273
const isExperimentalRawEventsCapabilityError = (error: unknown): boolean =>
7374
getErrorMessage(error).includes('experimentalRawEvents requires experimentalApi capability');
7475

76+
/** Build the agents config fragment to inject into thread config params. */
77+
const buildAgentsConfigFragment = (): Record<string, unknown> => {
78+
const { agentsMaxThreads, agentsMaxDepth } = useSettingsStore.getState();
79+
return {
80+
'features.multi_agents': true,
81+
'agents.max_threads': agentsMaxThreads,
82+
'agents.max_depth': agentsMaxDepth,
83+
};
84+
};
85+
7586
type ThreadLike = Thread & { updatedAt?: number };
7687
const getThreadPreviewFromInput = (userInputs: UserInput[]): string => {
7788
for (const item of userInputs) {
@@ -271,7 +282,8 @@ export const codexService = {
271282
model_reasoning_summary: 'auto',
272283
web_search_request: webSearchRequest,
273284
view_image_tool: true,
274-
'features.multi_agents': true,
285+
// Inject user-configured multi-agent limits.
286+
...buildAgentsConfigFragment(),
275287
},
276288
experimentalRawEvents: true,
277289
persistExtendedHistory: true,
@@ -378,7 +390,8 @@ export const codexService = {
378390
model_reasoning_summary: 'auto',
379391
web_search_request: webSearchRequest,
380392
view_image_tool: true,
381-
'features.multi_agents': true,
393+
// Inject user-configured multi-agent limits.
394+
...buildAgentsConfigFragment(),
382395
},
383396
baseInstructions: null,
384397
developerInstructions: null,

src/stores/settings/useSettingsStore.ts

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,19 @@ import { persist } from 'zustand/middleware';
44
export type TaskDetail = 'steps' | 'stepsWithCommand' | 'stepsWithOutput';
55
export type TaskCompleteBeepMode = 'never' | 'unfocused' | 'always';
66

7-
export interface SettingState {
7+
// --- Agents config ---
8+
// UI caps: max_threads ≤ 12, max_depth ≤ 4 (mirrors CodexMonitor product limits).
9+
export const AGENTS_MAX_THREADS_CAP = 12;
10+
export const AGENTS_MAX_DEPTH_CAP = 4;
11+
12+
export interface AgentsSettings {
13+
agentsMaxThreads: number;
14+
agentsMaxDepth: number;
15+
setAgentsMaxThreads: (value: number) => void;
16+
setAgentsMaxDepth: (value: number) => void;
17+
}
18+
19+
export interface SettingState extends AgentsSettings {
820
hiddenNames: string[];
921
taskDetail: TaskDetail;
1022
setTaskDetail: (taskDetail: TaskDetail) => void;
@@ -81,10 +93,17 @@ export const useSettingsStore = create<SettingState>()(
8193
setCustomStunServers: (servers: string[]) => set({ customStunServers: servers }),
8294
p2pAutoStart: false,
8395
setP2pAutoStart: (enabled: boolean) => set({ p2pAutoStart: enabled }),
96+
// Agents defaults: max_threads=6 (upstream Codex default), max_depth=1 (product default)
97+
agentsMaxThreads: 6,
98+
agentsMaxDepth: 1,
99+
setAgentsMaxThreads: (value: number) =>
100+
set({ agentsMaxThreads: Math.min(Math.max(1, value), AGENTS_MAX_THREADS_CAP) }),
101+
setAgentsMaxDepth: (value: number) =>
102+
set({ agentsMaxDepth: Math.min(Math.max(1, value), AGENTS_MAX_DEPTH_CAP) }),
84103
}),
85104
{
86105
name: 'settings-storage',
87-
version: 10
106+
version: 10,
88107
}
89108
)
90109
);

0 commit comments

Comments
 (0)