Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion desktop/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -6076,7 +6076,10 @@ type SkillView struct {
Model string `json:"model,omitempty"`
Effort string `json:"effort,omitempty"`
AllowedTools []string `json:"allowedTools,omitempty"`
Color string `json:"color,omitempty"`
// ReadOnly mirrors frontmatter read-only; omitted/false keeps the legacy
// writable default for older profiles.
ReadOnly bool `json:"readOnly,omitempty"`
Color string `json:"color,omitempty"`
// Invocation is the user-facing slash name; InvocationMode preserves the
// frontmatter policy used by the subagent profile editor.
Invocation string `json:"invocation,omitempty"`
Expand Down Expand Up @@ -6456,6 +6459,7 @@ func (a *App) SkillsSettings() SkillsSettingsView {
Model: s.Model,
Effort: s.Effort,
AllowedTools: append([]string{}, s.AllowedTools...),
ReadOnly: s.ReadOnly,
Color: s.Color,
Invocation: "/" + s.SlashName(),
InvocationMode: s.Invocation,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ function baseSettings(displayMode: "standard" | "compact" = "standard"): Setting
permissions: { mode: "ask", allow: [], ask: [], deny: [] },
sandbox: { bash: "enforce", network: false, workspaceRoot: "", allowWrite: [], effectiveWorkspaceRoot: "/work", effectiveWriteRoots: ["/work"], shell: "auto" },
network: { proxyMode: "auto", proxyUrl: "", noProxy: "", proxy: { type: "socks5", server: "", port: 0, username: "", password: "" } },
agent: { temperature: 0, maxSteps: 0, plannerMaxSteps: 0, maxSubagentDepth: 2, systemPrompt: "", coldResumePrune: true, reasoningLanguage: "auto" },
agent: { temperature: 0, maxSteps: 0, plannerMaxSteps: 0, maxSubagentDepth: 2, maxSubagentConcurrency: 6, maxParallelWriters: 3, systemPrompt: "", coldResumePrune: true, reasoningLanguage: "auto" },
bot: {
enabled: false,
model: "",
Expand Down
42 changes: 40 additions & 2 deletions desktop/frontend/src/components/SettingsPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1246,7 +1246,7 @@ function normalizeSettingsView(view: SettingsView | null | undefined): SettingsV
noProxy: "",
proxy: { type: "socks5", server: "", port: 0, username: "", password: "" },
};
const agent = view.agent ?? { temperature: 0, maxSteps: 0, plannerMaxSteps: 0, maxSubagentDepth: 2, systemPrompt: "", coldResumePrune: true, reasoningLanguage: "auto" };
const agent = view.agent ?? { temperature: 0, maxSteps: 0, plannerMaxSteps: 0, maxSubagentDepth: 2, maxSubagentConcurrency: 6, maxParallelWriters: 3, systemPrompt: "", coldResumePrune: true, reasoningLanguage: "auto" };
agent.plannerMaxSteps = Number.isFinite(agent.plannerMaxSteps) ? Math.max(0, Math.trunc(agent.plannerMaxSteps)) : 0;
agent.maxSteps = Number.isFinite(agent.maxSteps) ? Math.max(0, Math.trunc(agent.maxSteps)) : 0;
agent.maxSubagentDepth = Number.isFinite(agent.maxSubagentDepth) && agent.maxSubagentDepth <= 1 ? 1 : 2;
Expand Down Expand Up @@ -3950,8 +3950,14 @@ function ModelsSection({ s, busy, apply, backgroundApply }: ModelsSectionProps)
: !providerIsConfigured(defaultProviderView)
? t("settings.modelNeedsKey", { provider: modelProviderLabel(defaultProvider, defaultProviderView, t) })
: "";
const agent = s.agent ?? { temperature: 0, maxSteps: 0, plannerMaxSteps: 0, maxSubagentDepth: 2, systemPrompt: "", coldResumePrune: true, reasoningLanguage: "auto" };
const agent = s.agent ?? { temperature: 0, maxSteps: 0, plannerMaxSteps: 0, maxSubagentDepth: 2, maxSubagentConcurrency: 6, maxParallelWriters: 3, systemPrompt: "", coldResumePrune: true, reasoningLanguage: "auto" };
const subagentDepth = Number.isFinite(agent.maxSubagentDepth) && agent.maxSubagentDepth <= 1 ? 1 : 2;
const subagentConcurrency = Number.isFinite(agent.maxSubagentConcurrency) && agent.maxSubagentConcurrency > 0
? Math.max(1, Math.min(32, Math.floor(agent.maxSubagentConcurrency)))
: 6;
const parallelWriters = Number.isFinite(agent.maxParallelWriters) && agent.maxParallelWriters > 0
? Math.max(1, Math.min(subagentConcurrency, Math.floor(agent.maxParallelWriters)))
: Math.min(3, subagentConcurrency);

useEffect(() => {
if (subtab !== "usage") return;
Expand Down Expand Up @@ -4077,6 +4083,38 @@ function ModelsSection({ s, busy, apply, backgroundApply }: ModelsSectionProps)
</div>
</SettingsField>

<SettingsField label={t("settings.subagentConcurrency")} hint={t("settings.subagentConcurrencyHint")}>
<input
className="mem-input"
type="number"
min={1}
max={32}
value={subagentConcurrency}
disabled={busy}
onChange={(e) => {
const n = Number(e.target.value);
if (!Number.isFinite(n)) return;
void apply(() => app.SetMaxSubagentConcurrency(n));
}}
/>
</SettingsField>

<SettingsField label={t("settings.parallelWriters")} hint={t("settings.parallelWritersHint")}>
<input
className="mem-input"
type="number"
min={1}
max={subagentConcurrency}
value={parallelWriters}
disabled={busy}
onChange={(e) => {
const n = Number(e.target.value);
if (!Number.isFinite(n)) return;
void apply(() => app.SetMaxParallelWriters(n));
}}
/>
</SettingsField>

{modelIssue && <div className="provider-fetch-banner provider-fetch-banner--warn">{modelIssue}</div>}
</SettingsSection>
<SettingsSection title={t("settings.agentRuntime")} description={t("settings.agentRuntimeHint")}>
Expand Down
23 changes: 23 additions & 0 deletions desktop/frontend/src/components/SubagentsPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -587,6 +587,7 @@ function SubagentProfileForm({
const [selectedTools, setSelectedTools] = useState<Set<string>>(() => new Set(editingSkill?.allowedTools ?? []));
const hasUsedCustomMode = useRef(Boolean(editingSkill?.allowedTools?.length));
const [systemPrompt, setSystemPrompt] = useState(editingSkill?.body ?? "");
const [readOnly, setReadOnly] = useState(Boolean(editingSkill?.readOnly));
const [scope, setScope] = useState<"global" | "project">(editingSkill?.scope === "project" ? "project" : "global");
const [tryTask, setTryTask] = useState("");
const [tryRunning, setTryRunning] = useState(false);
Expand Down Expand Up @@ -614,6 +615,7 @@ function SubagentProfileForm({
model,
effort,
allowedTools: toolMode === "custom" ? Array.from(selectedTools) : [],
readOnly,
scope,
});

Expand Down Expand Up @@ -706,6 +708,27 @@ function SubagentProfileForm({
{toolMode === "custom" && <ToolMultiSelect tools={tools} selected={selectedTools} onChange={setSelectedTools} />}
{toolMode === "custom" && !toolsReady && <div className="subagents-field-error">{t("subagents.selectAtLeastOneTool")}</div>}

<label className="set-label">{t("subagents.readOnly")}</label>
<div className="set-seg" role="group" aria-label={t("subagents.readOnly")}>
<button
type="button"
className={`set-seg__btn${!readOnly ? " set-seg__btn--on" : ""}`}
disabled={busy}
onClick={() => setReadOnly(false)}
>
{t("subagents.readOnlyOff")}
</button>
<button
type="button"
className={`set-seg__btn${readOnly ? " set-seg__btn--on" : ""}`}
disabled={busy}
onClick={() => setReadOnly(true)}
>
{t("subagents.readOnlyOn")}
</button>
</div>
<div className="set-hint">{t("subagents.readOnlyHint")}</div>

<label className="set-label">{t("subagents.systemPrompt")}</label>
<textarea
className="mem-textarea"
Expand Down
16 changes: 14 additions & 2 deletions desktop/frontend/src/lib/bridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,8 @@ export interface AppBindings {
SetSubagentModel(ref: string): Promise<void>;
SetSubagentEffort(level: string): Promise<void>;
SetMaxSubagentDepth(depth: number): Promise<void>;
SetMaxSubagentConcurrency(n: number): Promise<void>;
SetMaxParallelWriters(n: number): Promise<void>;
SetAutoPlan(mode: string): Promise<void>;
SetDefaultToolApprovalMode(mode: string): Promise<void>;
SaveProvider(p: ProviderView): Promise<void>;
Expand Down Expand Up @@ -678,7 +680,7 @@ function bridgeBreadcrumb(method: string): string {
if (method === "ReportCrash") return "";
if (/^(Submit|SubmitDisplay|RunShell|Steer|Cancel|Approve|AnswerQuestion|ReplayPendingPrompts)/.test(method))
return `turn ${method}`;
if (/^(SetModel|SetEffort|SetTokenMode|SetDefaultModel|SetPlannerModel|SetSubagentModel|SetSubagentEffort|SetMaxSubagentDepth)/.test(method))
if (/^(SetModel|SetEffort|SetTokenMode|SetDefaultModel|SetPlannerModel|SetSubagentModel|SetSubagentEffort|SetMaxSubagentDepth|SetMaxSubagentConcurrency|SetMaxParallelWriters)/.test(method))
return `model ${method}`;
if (/^(SetDesktop|SetCloseBehavior|SetDisplayMode|SetStatusBar|SetExpandThinking|SetAutoPlan|SetDefaultToolApprovalMode|SetReasoningLanguage)/.test(method))
return `settings ${method}`;
Expand Down Expand Up @@ -1232,7 +1234,7 @@ function makeMockApp(): AppBindings {
noProxy: "",
proxy: { type: "socks5", server: "127.0.0.1", port: 7890, username: "", password: "" },
},
agent: { temperature: 0.2, maxSteps: 0, plannerMaxSteps: 0, maxSubagentDepth: 2, systemPrompt: "You are Reasonix, a coding agent.", coldResumePrune: true, reasoningLanguage: "auto" },
agent: { temperature: 0.2, maxSteps: 0, plannerMaxSteps: 0, maxSubagentDepth: 2, maxSubagentConcurrency: 6, maxParallelWriters: 3, systemPrompt: "You are Reasonix, a coding agent.", coldResumePrune: true, reasoningLanguage: "auto" },
bot: {
enabled: !freshMock,
model: "",
Expand Down Expand Up @@ -3501,6 +3503,16 @@ function makeMockApp(): AppBindings {
async SetMaxSubagentDepth(depth: number) {
settings.agent = { ...settings.agent, maxSubagentDepth: depth <= 1 ? 1 : 2 };
},
async SetMaxSubagentConcurrency(n: number) {
const total = Math.max(1, Math.min(32, Math.floor(n) || 6));
const writers = Math.min(total, Math.max(1, settings.agent.maxParallelWriters || 3));
settings.agent = { ...settings.agent, maxSubagentConcurrency: total, maxParallelWriters: writers };
},
async SetMaxParallelWriters(n: number) {
const total = Math.max(1, Math.min(32, settings.agent.maxSubagentConcurrency || 6));
const writers = Math.max(1, Math.min(total, Math.floor(n) || 3));
settings.agent = { ...settings.agent, maxParallelWriters: writers };
},
async SetAutoPlan(mode: string) {
settings.autoPlan = mode;
},
Expand Down
4 changes: 4 additions & 0 deletions desktop/frontend/src/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -785,6 +785,7 @@ export interface SkillView {
model?: string;
effort?: string;
allowedTools?: string[];
readOnly?: boolean;
color?: string;
invocation?: string;
invocationMode?: string;
Expand Down Expand Up @@ -833,6 +834,7 @@ export interface SubagentProfileInput {
model?: string;
effort?: string;
allowedTools?: string[];
readOnly?: boolean;
scope?: "project" | "global";
}
export interface PluginView {
Expand Down Expand Up @@ -1239,6 +1241,8 @@ export interface AgentView {
maxSteps: number;
plannerMaxSteps: number;
maxSubagentDepth: number;
maxSubagentConcurrency: number;
maxParallelWriters: number;
systemPrompt: string;
coldResumePrune: boolean;
reasoningLanguage: string; // "auto" | "zh" | "en"
Expand Down
8 changes: 8 additions & 0 deletions desktop/frontend/src/locales/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1319,6 +1319,14 @@ export const en = {
"settings.subagentDepthHint": "Controls whether a first-layer subagent may dispatch one more reviewer or worker subagent.",
"settings.subagentDepthOne": "1 layer",
"settings.subagentDepthTwo": "2 layers",
"settings.subagentConcurrency": "Subagent concurrency",
"settings.subagentConcurrencyHint": "Max concurrent sub-agents per session (task, fleet, profile skills). Default 6; range 1–32.",
"settings.parallelWriters": "Parallel writers",
"settings.parallelWritersHint": "Max concurrent writer sub-agents with non-overlapping write_paths. Default 3; cannot exceed total concurrency.",
"subagents.readOnly": "Read-only",
"subagents.readOnlyOn": "Read-only",
"subagents.readOnlyOff": "Writable",
"subagents.readOnlyHint": "Read-only profiles strip writer tools. Existing profiles default to writable.",
"settings.defaultToolApprovalMode": "New session approval",
"settings.defaultToolApprovalModeHint": "Defaults to Auto when unset. Saved globally and used only for new sessions; existing sessions keep their Ask / Auto / Yolo state.",
"settings.defaultToolApprovalMode.ask": "Ask",
Expand Down
8 changes: 8 additions & 0 deletions desktop/frontend/src/locales/zh-TW.ts
Original file line number Diff line number Diff line change
Expand Up @@ -993,6 +993,14 @@ export const zhTW: Record<DictKey, string> = {
"settings.subagentDepthHint": "控制第一層子代理能否再派發一層 reviewer 或 worker 子代理。",
"settings.subagentDepthOne": "1 層",
"settings.subagentDepthTwo": "2 層",
"settings.subagentConcurrency": "子代理並行數",
"settings.subagentConcurrencyHint": "每個工作階段最多同時執行的子代理數量(task / fleet / Profile Skill)。預設 6,範圍 1–32。",
"settings.parallelWriters": "並行寫入數",
"settings.parallelWritersHint": "宣告互不重疊 write_paths 時可並行的寫入子代理上限。預設 3,不得超過總並行。",
"subagents.readOnly": "唯讀",
"subagents.readOnlyOn": "唯讀",
"subagents.readOnlyOff": "可寫",
"subagents.readOnlyHint": "唯讀 Profile 會移除寫入工具。舊 Profile 預設為可寫。",
"settings.defaultToolApprovalMode": "新會話預設審批",
"settings.defaultToolApprovalModeHint": "未設定時預設為自動。保存在全域設定中,僅用於新建會話;已有會話會保留其詢問、自動或 Yolo 狀態。",
"settings.defaultToolApprovalMode.ask": "詢問",
Expand Down
8 changes: 8 additions & 0 deletions desktop/frontend/src/locales/zh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1321,6 +1321,14 @@ export const zh: Record<DictKey, string> = {
"settings.subagentDepthHint": "控制第一层子代理能否再派发一层 reviewer 或 worker 子代理。",
"settings.subagentDepthOne": "1 层",
"settings.subagentDepthTwo": "2 层",
"settings.subagentConcurrency": "子代理并发数",
"settings.subagentConcurrencyHint": "每个会话最多同时运行的子代理数量(task / fleet / Profile Skill)。默认 6,范围 1–32。",
"settings.parallelWriters": "并行写入数",
"settings.parallelWritersHint": "声明了互不重叠 write_paths 时可并行的写入子代理上限。默认 3,不得超过总并发。",
"subagents.readOnly": "只读",
"subagents.readOnlyOn": "只读",
"subagents.readOnlyOff": "可写",
"subagents.readOnlyHint": "只读 Profile 会剥离写入工具。旧 Profile 缺省为可写。",
"settings.defaultToolApprovalMode": "新会话默认审批",
"settings.defaultToolApprovalModeHint": "未设置时默认为自动。保存在全局设置中,仅用于新建会话;已有会话会保留其询问、自动或 Yolo 状态。",
"settings.defaultToolApprovalMode.ask": "询问",
Expand Down
69 changes: 59 additions & 10 deletions desktop/settings_app.go
Original file line number Diff line number Diff line change
Expand Up @@ -128,13 +128,15 @@ type NetworkView struct {
}

type AgentView struct {
Temperature float64 `json:"temperature"`
MaxSteps int `json:"maxSteps"`
PlannerMaxSteps int `json:"plannerMaxSteps"`
MaxSubagentDepth int `json:"maxSubagentDepth"`
SystemPrompt string `json:"systemPrompt"`
ColdResumePrune bool `json:"coldResumePrune"`
ReasoningLanguage string `json:"reasoningLanguage"`
Temperature float64 `json:"temperature"`
MaxSteps int `json:"maxSteps"`
PlannerMaxSteps int `json:"plannerMaxSteps"`
MaxSubagentDepth int `json:"maxSubagentDepth"`
MaxSubagentConcurrency int `json:"maxSubagentConcurrency"`
MaxParallelWriters int `json:"maxParallelWriters"`
SystemPrompt string `json:"systemPrompt"`
ColdResumePrune bool `json:"coldResumePrune"`
ReasoningLanguage string `json:"reasoningLanguage"`
}

type BotAllowlistView struct {
Expand Down Expand Up @@ -802,8 +804,15 @@ func (a *App) Settings() SettingsView {
Ask: []string{},
Deny: []string{},
},
Sandbox: SandboxView{Bash: config.Default().BashMode(), AllowWrite: []string{}, EffectiveWriteRoots: []string{}, Shell: "auto", EffectiveShell: sandboxEffectiveShellView(sandbox.ResolveShell("", "", nil))},
Agent: AgentView{PlannerMaxSteps: 0, MaxSubagentDepth: agent.DefaultMaxSubagentDepth, ColdResumePrune: true, ReasoningLanguage: "auto"},
Sandbox: SandboxView{Bash: config.Default().BashMode(), AllowWrite: []string{}, EffectiveWriteRoots: []string{}, Shell: "auto", EffectiveShell: sandboxEffectiveShellView(sandbox.ResolveShell("", "", nil))},
Agent: AgentView{
PlannerMaxSteps: 0,
MaxSubagentDepth: agent.DefaultMaxSubagentDepth,
MaxSubagentConcurrency: agent.DefaultMaxSubagentConcurrency,
MaxParallelWriters: agent.DefaultMaxParallelWriters,
ColdResumePrune: true,
ReasoningLanguage: "auto",
},
Bot: botSettingsView(config.BotConfig{}),
AutoPlan: "off",
DesktopLayoutStyle: "workbench",
Expand Down Expand Up @@ -866,7 +875,17 @@ func (a *App) Settings() SettingsView {
Password: cfg.Network.Proxy.Password,
},
},
Agent: AgentView{Temperature: cfg.Agent.Temperature, MaxSteps: cfg.Agent.MaxSteps, PlannerMaxSteps: cfg.Agent.PlannerMaxSteps, MaxSubagentDepth: desktopMaxSubagentDepth(cfg.Agent.MaxSubagentDepth), SystemPrompt: cfg.Agent.SystemPrompt, ColdResumePrune: cfg.ColdResumePruneEnabled(), ReasoningLanguage: cfg.ReasoningLanguage()},
Agent: AgentView{
Temperature: cfg.Agent.Temperature,
MaxSteps: cfg.Agent.MaxSteps,
PlannerMaxSteps: cfg.Agent.PlannerMaxSteps,
MaxSubagentDepth: desktopMaxSubagentDepth(cfg.Agent.MaxSubagentDepth),
MaxSubagentConcurrency: desktopSubagentConcurrency(cfg.Agent.MaxSubagentConcurrency),
MaxParallelWriters: desktopParallelWriters(cfg.Agent.MaxParallelWriters, cfg.Agent.MaxSubagentConcurrency),
SystemPrompt: cfg.Agent.SystemPrompt,
ColdResumePrune: cfg.ColdResumePruneEnabled(),
ReasoningLanguage: cfg.ReasoningLanguage(),
},
Bot: botSettingsView(cfg.Bot),
DesktopLanguage: cfg.DesktopLanguage(),
DesktopLayoutStyle: cfg.DesktopLayoutStyle(),
Expand Down Expand Up @@ -1837,6 +1856,36 @@ func (a *App) SetMaxSubagentDepth(depth int) error {
})
}

func desktopSubagentConcurrency(n int) int {
total, _ := agent.NormalizeConcurrencyLimits(n, 0)
return total
}

func desktopParallelWriters(writers, total int) int {
_, w := agent.NormalizeConcurrencyLimits(total, writers)
return w
}

// SetMaxSubagentConcurrency sets the session-wide sub-agent concurrency cap (1–32).
func (a *App) SetMaxSubagentConcurrency(n int) error {
return a.applyConfigChange(func(c *config.Config) error {
total, writers := agent.NormalizeConcurrencyLimits(n, c.Agent.MaxParallelWriters)
c.Agent.MaxSubagentConcurrency = total
c.Agent.MaxParallelWriters = writers
return nil
})
}

// SetMaxParallelWriters sets the concurrent writer cap (1–32, ≤ total concurrency).
func (a *App) SetMaxParallelWriters(n int) error {
return a.applyConfigChange(func(c *config.Config) error {
total, writers := agent.NormalizeConcurrencyLimits(c.Agent.MaxSubagentConcurrency, n)
c.Agent.MaxSubagentConcurrency = total
c.Agent.MaxParallelWriters = writers
return nil
})
}

// SetAutoPlan updates the automatic plan-first workflow setting (off|on).
func (a *App) SetAutoPlan(mode string) error {
if err := a.ensureLiveControllersRuntimeMutationAllowed("auto-plan"); err != nil {
Expand Down
Loading