diff --git a/apps/x/apps/renderer/src/components/settings-dialog.tsx b/apps/x/apps/renderer/src/components/settings-dialog.tsx
index ca46c909..3efa566f 100644
--- a/apps/x/apps/renderer/src/components/settings-dialog.tsx
+++ b/apps/x/apps/renderer/src/components/settings-dialog.tsx
@@ -2,7 +2,7 @@
import * as React from "react"
import { useState, useEffect, useCallback, useMemo } from "react"
-import { Server, Key, Shield, Palette, Monitor, Sun, Moon, Loader2, CheckCircle2, Plus, X, Wrench, Search, ChevronRight, Link2, Tags, Mail, BookOpen, User, Plug, HelpCircle, MessageCircle, Terminal, AlertTriangle, RefreshCw, PanelRight, Bell, Smartphone } from "lucide-react"
+import { Server, Key, Shield, Palette, Monitor, Sun, Moon, Loader2, CheckCircle2, Plus, Minus, X, Wrench, Search, ChevronRight, Link2, Tags, Mail, BookOpen, User, Plug, HelpCircle, MessageCircle, Terminal, AlertTriangle, RefreshCw, PanelRight, Bell, Smartphone } from "lucide-react"
import {
Dialog,
@@ -2641,11 +2641,86 @@ function NotificationSettings({ dialogOpen }: { dialogOpen: boolean }) {
// --- Advanced (runtime/cost controls) tab ---
const MODEL_CALL_LIMIT_MIN = 1
-const MODEL_CALL_LIMIT_MAX = 100
+const MODEL_CALL_LIMIT_MAX = 500
+
+function parseLimit(value: string): number | null {
+ const n = Number(value.trim())
+ if (!Number.isInteger(n) || n < MODEL_CALL_LIMIT_MIN || n > MODEL_CALL_LIMIT_MAX) return null
+ return n
+}
+
+/**
+ * Compact segmented − / value / + stepper. The native number-input spinners
+ * are replaced entirely: typing is free-form digits, stepping clamps to the
+ * range and commits immediately. An empty value steps from `fallback` (the
+ * chat field starts from the global limit).
+ */
+function LimitStepper({
+ value,
+ fallback,
+ placeholder,
+ onInput,
+ onCommit,
+}: {
+ value: string
+ fallback: number
+ placeholder?: string
+ /** Every keystroke (no save). */
+ onInput: (next: string) => void
+ /** A settled change: step click or blur. */
+ onCommit: (next: string) => void
+}) {
+ const current = parseLimit(value)
+
+ const step = (delta: number) => {
+ // From an empty/invalid field, the first step lands on the fallback so
+ // the override starts where the effective value already is.
+ const next = current === null
+ ? Math.min(MODEL_CALL_LIMIT_MAX, Math.max(MODEL_CALL_LIMIT_MIN, fallback))
+ : Math.min(MODEL_CALL_LIMIT_MAX, Math.max(MODEL_CALL_LIMIT_MIN, current + delta))
+ onCommit(String(next))
+ }
+
+ return (
+
+
+
onInput(e.target.value.replace(/[^0-9]/g, ""))}
+ onBlur={() => onCommit(value)}
+ className={cn(
+ "h-full border-x border-input bg-transparent text-center text-sm tabular-nums outline-none",
+ "placeholder:text-[11px] placeholder:text-muted-foreground/70",
+ placeholder ? "w-24" : "w-16",
+ )}
+ />
+
+
+ )
+}
function AdvancedSettings({ dialogOpen }: { dialogOpen: boolean }) {
// Inputs are kept as strings so the user can clear a field while typing;
- // validation happens on save (blur).
+ // validation happens on commit (step click or blur).
const [globalLimit, setGlobalLimit] = useState("")
const [chatLimit, setChatLimit] = useState("")
const [loaded, setLoaded] = useState(false)
@@ -2666,21 +2741,17 @@ function AdvancedSettings({ dialogOpen }: { dialogOpen: boolean }) {
return () => { cancelled = true }
}, [dialogOpen])
- const parseLimit = (value: string): number | null => {
- const n = Number(value.trim())
- if (!Number.isInteger(n) || n < MODEL_CALL_LIMIT_MIN || n > MODEL_CALL_LIMIT_MAX) return null
- return n
- }
-
- const handleSave = useCallback(async () => {
- const global = parseLimit(globalLimit)
+ // Saves silently on success (a toast per stepper click would be noisy,
+ // matching the notification toggles); errors still surface.
+ const saveLimits = useCallback(async (globalStr: string, chatStr: string) => {
+ const global = parseLimit(globalStr)
if (global === null) {
toast.error(`Model-call limit must be a whole number between ${MODEL_CALL_LIMIT_MIN} and ${MODEL_CALL_LIMIT_MAX}`)
return
}
let chat: number | undefined
- if (chatLimit.trim() !== "") {
- const parsed = parseLimit(chatLimit)
+ if (chatStr.trim() !== "") {
+ const parsed = parseLimit(chatStr)
if (parsed === null) {
toast.error(`Chat limit must be empty or a whole number between ${MODEL_CALL_LIMIT_MIN} and ${MODEL_CALL_LIMIT_MAX}`)
return
@@ -2692,11 +2763,10 @@ function AdvancedSettings({ dialogOpen }: { dialogOpen: boolean }) {
maxModelCalls: global,
...(chat === undefined ? {} : { chatMaxModelCalls: chat }),
})
- toast.success("Model-call limits saved. Applies to new turns.")
} catch {
toast.error("Failed to save model-call limits")
}
- }, [globalLimit, chatLimit])
+ }, [])
if (!loaded) {
return (
@@ -2723,14 +2793,14 @@ function AdvancedSettings({ dialogOpen }: { dialogOpen: boolean }) {
knowledge work, scheduled agents, and sub-agents (it also caps sub-agent budgets).
- setGlobalLimit(e.target.value)}
- onBlur={handleSave}
- className="w-24 shrink-0"
+ fallback={20}
+ onInput={setGlobalLimit}
+ onCommit={(next) => {
+ setGlobalLimit(next)
+ void saveLimits(next, chatLimit)
+ }}
/>
@@ -2742,16 +2812,34 @@ function AdvancedSettings({ dialogOpen }: { dialogOpen: boolean }) {
model-call limit above.
- setChatLimit(e.target.value)}
- onBlur={handleSave}
- placeholder="Same as above"
- className="w-32 shrink-0"
- />
+
+ {chatLimit.trim() !== "" && (
+
+ )}
+ {
+ setChatLimit(next)
+ // An emptied chat field on blur means "use the global
+ // limit" — persist the override removal.
+ void saveLimits(globalLimit, next)
+ }}
+ />
+
diff --git a/apps/x/packages/core/src/config/turn_limits.test.ts b/apps/x/packages/core/src/config/turn_limits.test.ts
index b9748615..bb7c3576 100644
--- a/apps/x/packages/core/src/config/turn_limits.test.ts
+++ b/apps/x/packages/core/src/config/turn_limits.test.ts
@@ -92,9 +92,9 @@ describe("saveTurnLimitsSettings", () => {
it("rejects out-of-range values", async () => {
const { saveTurnLimitsSettings } = await loadTurnLimits();
expect(() => saveTurnLimitsSettings({ maxModelCalls: 0 })).toThrow();
- expect(() => saveTurnLimitsSettings({ maxModelCalls: 101 })).toThrow();
+ expect(() => saveTurnLimitsSettings({ maxModelCalls: 501 })).toThrow();
expect(() =>
- saveTurnLimitsSettings({ maxModelCalls: 20, chatMaxModelCalls: 500 }),
+ saveTurnLimitsSettings({ maxModelCalls: 20, chatMaxModelCalls: 501 }),
).toThrow();
});
});
diff --git a/apps/x/packages/shared/src/turn-limits.ts b/apps/x/packages/shared/src/turn-limits.ts
index 6d390c1e..347a1a76 100644
--- a/apps/x/packages/shared/src/turn-limits.ts
+++ b/apps/x/packages/shared/src/turn-limits.ts
@@ -14,7 +14,7 @@ import { DEFAULT_MAX_MODEL_CALLS } from './turns.js';
* resolved limit in turn_created.config.maxModelCalls.
*/
export const MIN_MODEL_CALL_LIMIT = 1;
-export const MAX_MODEL_CALL_LIMIT = 100;
+export const MAX_MODEL_CALL_LIMIT = 500;
const limit = z
.number()