detached c35564a

Merge pull request #47 from bnema/feat/fast-mode-command

feat(agent): add fast mode toggle
commit
c35564a8242df5afdbabb2dbec7315c22453030a
author
bnema · · 2026-08-06 14:55 UTC
committer
GitHub · · 2026-08-06 14:55 UTC
merge of
83c2c60 + 83e4294 · diff against the first parent
signature
GPG UnverifiedB5690EEEBB952194 The key is unknown
refs
tag: v0.5.0
14 files · +144 -33
M README.md +3 -2
@@ -278,6 +278,7 @@ Slash command names are case-insensitive in the TUI and messaging backends; argu
278278 | `/logout [provider]` | Clear credentials for any logged-in provider, or all when omitted. `/logout openai-codex` clears ChatGPT/Codex subscription auth while preserving a public OpenAI API key; `/logout kimi` also disables fallback to the official Kimi Code CLI token until you log in to Kimi through zut again. |
279279 | `/model` | Pick a model from a list (or `/model <id>` to set directly). |
280280 | `/reasoning` | Set the reasoning level for subsequent model calls. |
281+| `/fast` | Toggle fast mode for subsequent model calls. |
281282 | `/llama` | Connect to the configured llama.cpp router, load, unload, or remove cached models, and search/download GGUF models from Hugging Face with live progress. Shown after llama.cpp login is configured. |
282283 | `/sessions` | Resume a previous session for this directory. The picker omits branches created only for tree navigation. |
283284 | `/fork` | Pick a previous user message and fork the current session after that turn. The selected turn becomes branch context and no provider turn starts during the checkout. |
@@ -292,7 +293,7 @@ Slash command names are case-insensitive in the TUI and messaging backends; argu
292293 | `/unjail` | Allow tools to touch paths outside again. |
293294 | `/reload-ext` | Hot-reload all extensions (re-read manifests, respawn subprocesses, rebuild tool registry). |
294295 | `/telegram` | Connect, disconnect, or show status of the Telegram bridge (takes `connect` / `disconnect` / `status` as an optional argument; opens a picker without one). When connected, DMs from the paired user become prompts in the running session and the assistant's replies are mirrored back to Telegram. Alias: `/tg`. |
295-| `/settings` | Change persistent settings, including inline images, terminal alerts, AI terminal titles, auto-subagents, Ponytail coding mode, fast mode, main/sub-agent LSP access, and the auto-compact threshold. Saved to `$ZUT_HOME/config.json`; setting changes apply without a restart, while AI title generation waits for the first real prompt. |
296+| `/settings` | Change persistent settings, including inline images, terminal alerts, AI terminal titles, auto-subagents, Ponytail coding mode, fast mode, main/sub-agent LSP access, and the auto-compact threshold. `/fast` is a shortcut for the fast-mode toggle. Saved to `$ZUT_HOME/config.json`; setting changes apply without a restart, while AI title generation waits for the first real prompt. |
296297 | `/clear` | Clear the chat transcript. |
297298 | `/exit` | Exit zut. |
298299
@@ -400,7 +401,7 @@ Opens a dialog with every persistent setting. `up`/`down` to navigate, `enter` o
400401 - **AI terminal titles** — after the first real prompt of a fresh interactive session, make one small hidden request to the active model and set the terminal title to `zut: <title>`. The title is limited to 40 Unicode characters, persisted with the session, restored on resume, and never added to the conversation. Enabled by default; disable it to avoid the extra model request. The toggle applies immediately, but title generation still waits for the first real prompt and never starts from startup context, resumed history, or slash commands. Persists as `terminal_title_enabled`.
401402 - **auto-subagents** — let the main agent spawn background sub-agents in parallel via `subagent_spawn` and query live state via `subagent_status`. Off by default. The tools accept named profiles, reasoning, timeout, turn limits, and shared/worktree isolation where applicable; lifecycle and result references are persisted. Completion updates still arrive through the auto-subagents watcher. See `/subagents` and [docs/subagents.md](docs/subagents.md) for details.
402403 - **Ponytail coding mode** — include compact engineering guidance in each resolved system prompt; the guidance tells the model to apply it to coding, debugging, and review work rather than ordinary conversation. It favors understanding the real flow, small validated changes, reuse, and preserving safety checks. Enabled by default; changes apply to the next model call and persist as `ponytail_enabled` in `$ZUT_HOME/config.json`. It is included by interactive, print, stream, JSON, RPC, subagent, bot, SDK, and Zutfile agents.
403-- **fast mode** — request OpenAI's Fast service tier for OpenAI, OpenAI Responses, and OpenAI Codex models. Off by default; changes apply on the next model call and persist as `fast_mode`. Other providers return an unsupported-provider error. Fast mode may cost more and depends on the selected OpenAI model/account.
404+- **fast mode** — request the provider's fast tier where supported. It currently uses OpenAI's Fast service tier for OpenAI, OpenAI Responses, and OpenAI Codex models. Off by default; toggle it with `/fast` or `/settings`; changes apply on the next model call and persist as `fast_mode`. Other providers currently return an unsupported-provider error. Fast mode may cost more and depends on the selected model/account.
404405 - **lsp in main session** — enable the built-in `lsp` tool and code diagnostics for the main agent. Enabled by default; changes persist as `lsp_enabled`.
405406 - **lsp in sub-agents** — allow newly spawned background sub-agents to use the built-in `lsp` tool. Enabled by default; changes persist as `subagent_lsp_enabled` and apply when a child starts.
406407 - **auto-compact threshold** — choose `off`, `70%`, `80%`, `85%` (default), or `90%` of the model's advertised context window. The selected percentage controls automatic compaction before and after interactive turns and persists as `auto_compact_threshold`. After a successful threshold compaction, zut automatically resumes an unfinished most-recent intent; a prompt queued during compaction takes priority. `off` disables percentage-based triggers but keeps manual `/compact` and automatic recovery from context-window and payload-too-large responses.
@@ -278,6 +278,7 @@ Slash command names are case-insensitive in the TUI and messaging backends; argu
278 | `/logout [provider]` | Clear credentials for any logged-in provider, or all when omitted. `/logout openai-codex` clears ChatGPT/Codex subscription auth while preserving a public OpenAI API key; `/logout kimi` also disables fallback to the official Kimi Code CLI token until you log in to Kimi through zut again. |278 | `/logout [provider]` | Clear credentials for any logged-in provider, or all when omitted. `/logout openai-codex` clears ChatGPT/Codex subscription auth while preserving a public OpenAI API key; `/logout kimi` also disables fallback to the official Kimi Code CLI token until you log in to Kimi through zut again. |
279 | `/model` | Pick a model from a list (or `/model <id>` to set directly). |279 | `/model` | Pick a model from a list (or `/model <id>` to set directly). |
280 | `/reasoning` | Set the reasoning level for subsequent model calls. |280 | `/reasoning` | Set the reasoning level for subsequent model calls. |
281+| `/fast` | Toggle fast mode for subsequent model calls. |
281 | `/llama` | Connect to the configured llama.cpp router, load, unload, or remove cached models, and search/download GGUF models from Hugging Face with live progress. Shown after llama.cpp login is configured. |282 | `/llama` | Connect to the configured llama.cpp router, load, unload, or remove cached models, and search/download GGUF models from Hugging Face with live progress. Shown after llama.cpp login is configured. |
282 | `/sessions` | Resume a previous session for this directory. The picker omits branches created only for tree navigation. |283 | `/sessions` | Resume a previous session for this directory. The picker omits branches created only for tree navigation. |
283 | `/fork` | Pick a previous user message and fork the current session after that turn. The selected turn becomes branch context and no provider turn starts during the checkout. |284 | `/fork` | Pick a previous user message and fork the current session after that turn. The selected turn becomes branch context and no provider turn starts during the checkout. |
@@ -292,7 +293,7 @@ Slash command names are case-insensitive in the TUI and messaging backends; argu
292 | `/unjail` | Allow tools to touch paths outside again. |293 | `/unjail` | Allow tools to touch paths outside again. |
293 | `/reload-ext` | Hot-reload all extensions (re-read manifests, respawn subprocesses, rebuild tool registry). |294 | `/reload-ext` | Hot-reload all extensions (re-read manifests, respawn subprocesses, rebuild tool registry). |
294 | `/telegram` | Connect, disconnect, or show status of the Telegram bridge (takes `connect` / `disconnect` / `status` as an optional argument; opens a picker without one). When connected, DMs from the paired user become prompts in the running session and the assistant's replies are mirrored back to Telegram. Alias: `/tg`. |295 | `/telegram` | Connect, disconnect, or show status of the Telegram bridge (takes `connect` / `disconnect` / `status` as an optional argument; opens a picker without one). When connected, DMs from the paired user become prompts in the running session and the assistant's replies are mirrored back to Telegram. Alias: `/tg`. |
295-| `/settings` | Change persistent settings, including inline images, terminal alerts, AI terminal titles, auto-subagents, Ponytail coding mode, fast mode, main/sub-agent LSP access, and the auto-compact threshold. Saved to `$ZUT_HOME/config.json`; setting changes apply without a restart, while AI title generation waits for the first real prompt. |296+| `/settings` | Change persistent settings, including inline images, terminal alerts, AI terminal titles, auto-subagents, Ponytail coding mode, fast mode, main/sub-agent LSP access, and the auto-compact threshold. `/fast` is a shortcut for the fast-mode toggle. Saved to `$ZUT_HOME/config.json`; setting changes apply without a restart, while AI title generation waits for the first real prompt. |
296 | `/clear` | Clear the chat transcript. |297 | `/clear` | Clear the chat transcript. |
297 | `/exit` | Exit zut. |298 | `/exit` | Exit zut. |
298 299
@@ -400,7 +401,7 @@ Opens a dialog with every persistent setting. `up`/`down` to navigate, `enter` o
400 - **AI terminal titles** — after the first real prompt of a fresh interactive session, make one small hidden request to the active model and set the terminal title to `zut: <title>`. The title is limited to 40 Unicode characters, persisted with the session, restored on resume, and never added to the conversation. Enabled by default; disable it to avoid the extra model request. The toggle applies immediately, but title generation still waits for the first real prompt and never starts from startup context, resumed history, or slash commands. Persists as `terminal_title_enabled`.401 - **AI terminal titles** — after the first real prompt of a fresh interactive session, make one small hidden request to the active model and set the terminal title to `zut: <title>`. The title is limited to 40 Unicode characters, persisted with the session, restored on resume, and never added to the conversation. Enabled by default; disable it to avoid the extra model request. The toggle applies immediately, but title generation still waits for the first real prompt and never starts from startup context, resumed history, or slash commands. Persists as `terminal_title_enabled`.
401 - **auto-subagents** — let the main agent spawn background sub-agents in parallel via `subagent_spawn` and query live state via `subagent_status`. Off by default. The tools accept named profiles, reasoning, timeout, turn limits, and shared/worktree isolation where applicable; lifecycle and result references are persisted. Completion updates still arrive through the auto-subagents watcher. See `/subagents` and [docs/subagents.md](docs/subagents.md) for details.402 - **auto-subagents** — let the main agent spawn background sub-agents in parallel via `subagent_spawn` and query live state via `subagent_status`. Off by default. The tools accept named profiles, reasoning, timeout, turn limits, and shared/worktree isolation where applicable; lifecycle and result references are persisted. Completion updates still arrive through the auto-subagents watcher. See `/subagents` and [docs/subagents.md](docs/subagents.md) for details.
402 - **Ponytail coding mode** — include compact engineering guidance in each resolved system prompt; the guidance tells the model to apply it to coding, debugging, and review work rather than ordinary conversation. It favors understanding the real flow, small validated changes, reuse, and preserving safety checks. Enabled by default; changes apply to the next model call and persist as `ponytail_enabled` in `$ZUT_HOME/config.json`. It is included by interactive, print, stream, JSON, RPC, subagent, bot, SDK, and Zutfile agents.403 - **Ponytail coding mode** — include compact engineering guidance in each resolved system prompt; the guidance tells the model to apply it to coding, debugging, and review work rather than ordinary conversation. It favors understanding the real flow, small validated changes, reuse, and preserving safety checks. Enabled by default; changes apply to the next model call and persist as `ponytail_enabled` in `$ZUT_HOME/config.json`. It is included by interactive, print, stream, JSON, RPC, subagent, bot, SDK, and Zutfile agents.
403-- **fast mode** — request OpenAI's Fast service tier for OpenAI, OpenAI Responses, and OpenAI Codex models. Off by default; changes apply on the next model call and persist as `fast_mode`. Other providers return an unsupported-provider error. Fast mode may cost more and depends on the selected OpenAI model/account.404+- **fast mode** — request the provider's fast tier where supported. It currently uses OpenAI's Fast service tier for OpenAI, OpenAI Responses, and OpenAI Codex models. Off by default; toggle it with `/fast` or `/settings`; changes apply on the next model call and persist as `fast_mode`. Other providers currently return an unsupported-provider error. Fast mode may cost more and depends on the selected model/account.
404 - **lsp in main session** — enable the built-in `lsp` tool and code diagnostics for the main agent. Enabled by default; changes persist as `lsp_enabled`.405 - **lsp in main session** — enable the built-in `lsp` tool and code diagnostics for the main agent. Enabled by default; changes persist as `lsp_enabled`.
405 - **lsp in sub-agents** — allow newly spawned background sub-agents to use the built-in `lsp` tool. Enabled by default; changes persist as `subagent_lsp_enabled` and apply when a child starts.406 - **lsp in sub-agents** — allow newly spawned background sub-agents to use the built-in `lsp` tool. Enabled by default; changes persist as `subagent_lsp_enabled` and apply when a child starts.
406 - **auto-compact threshold** — choose `off`, `70%`, `80%`, `85%` (default), or `90%` of the model's advertised context window. The selected percentage controls automatic compaction before and after interactive turns and persists as `auto_compact_threshold`. After a successful threshold compaction, zut automatically resumes an unfinished most-recent intent; a prompt queued during compaction takes priority. `off` disables percentage-based triggers but keeps manual `/compact` and automatic recovery from context-window and payload-too-large responses.407 - **auto-compact threshold** — choose `off`, `70%`, `80%`, `85%` (default), or `90%` of the model's advertised context window. The selected percentage controls automatic compaction before and after interactive turns and persists as `auto_compact_threshold`. After a successful threshold compaction, zut automatically resumes an unfinished most-recent intent; a prompt queued during compaction takes priority. `off` disables percentage-based triggers but keeps manual `/compact` and automatic recovery from context-window and payload-too-large responses.
M docs/providers.md +12 -10
@@ -115,16 +115,6 @@ show instructions and should be configured with environment variables.
115115 | Cloudflare AI Gateway | `CLOUDFLARE_API_KEY` | `cloudflare-ai-gateway` |
116116 | Azure OpenAI Responses | `AZURE_OPENAI_API_KEY` | `azure-openai-responses` |
117117
118-### OpenAI Fast mode
119-
120-OpenAI, OpenAI Responses, and OpenAI Codex requests can opt into OpenAI's
121-Fast service tier from `/settings` by enabling **fast mode**. The setting is
122-off by default and is stored as `fast_mode` in `$ZUT_HOME/config.json`.
123-
124-Fast mode is currently limited to those OpenAI provider IDs. Enabling it while
125-using another provider returns an error instead of silently changing that
126-provider's request. Subagent children inherit the setting from their parent.
127-
128118 Example:
129119
130120 ```bash
@@ -132,6 +122,18 @@ export OPENROUTER_API_KEY=...
132122 zut --provider openrouter
133123 ```
134124
125+## Fast mode
126+
127+Use `/fast` or `/settings` to enable **fast mode** for subsequent model calls.
128+The setting is off by default and is stored as `fast_mode` in
129+`$ZUT_HOME/config.json`.
130+
131+Fast mode currently requests OpenAI's Fast service tier for OpenAI, OpenAI
132+Responses, and OpenAI Codex requests. Enabling it while using another provider
133+currently returns an error instead of silently changing that provider's request.
134+Subagent children inherit the setting from their parent unless their profile
135+sets `fastMode` explicitly.
136+
135137 ## Local llama.cpp router
136138
137139 The `llama.cpp` provider connects to a multi-model router and is separate from the `ollama` provider. Ollama normally uses `http://localhost:11434`; entering that URL for llama.cpp management produces a 404 because Ollama does not implement the router endpoints.
@@ -115,16 +115,6 @@ show instructions and should be configured with environment variables.
115 | Cloudflare AI Gateway | `CLOUDFLARE_API_KEY` | `cloudflare-ai-gateway` |115 | Cloudflare AI Gateway | `CLOUDFLARE_API_KEY` | `cloudflare-ai-gateway` |
116 | Azure OpenAI Responses | `AZURE_OPENAI_API_KEY` | `azure-openai-responses` |116 | Azure OpenAI Responses | `AZURE_OPENAI_API_KEY` | `azure-openai-responses` |
117 117
118-### OpenAI Fast mode
119-
120-OpenAI, OpenAI Responses, and OpenAI Codex requests can opt into OpenAI's
121-Fast service tier from `/settings` by enabling **fast mode**. The setting is
122-off by default and is stored as `fast_mode` in `$ZUT_HOME/config.json`.
123-
124-Fast mode is currently limited to those OpenAI provider IDs. Enabling it while
125-using another provider returns an error instead of silently changing that
126-provider's request. Subagent children inherit the setting from their parent.
127-
128 Example:118 Example:
129 119
130 ```bash120 ```bash
@@ -132,6 +122,18 @@ export OPENROUTER_API_KEY=...
132 zut --provider openrouter122 zut --provider openrouter
133 ```123 ```
134 124
125+## Fast mode
126+
127+Use `/fast` or `/settings` to enable **fast mode** for subsequent model calls.
128+The setting is off by default and is stored as `fast_mode` in
129+`$ZUT_HOME/config.json`.
130+
131+Fast mode currently requests OpenAI's Fast service tier for OpenAI, OpenAI
132+Responses, and OpenAI Codex requests. Enabling it while using another provider
133+currently returns an error instead of silently changing that provider's request.
134+Subagent children inherit the setting from their parent unless their profile
135+sets `fastMode` explicitly.
136+
135 ## Local llama.cpp router137 ## Local llama.cpp router
136 138
137 The `llama.cpp` provider connects to a multi-model router and is separate from the `ollama` provider. Ollama normally uses `http://localhost:11434`; entering that URL for llama.cpp management produces a 404 because Ollama does not implement the router endpoints.139 The `llama.cpp` provider connects to a multi-model router and is separate from the `ollama` provider. Ollama normally uses `http://localhost:11434`; entering that URL for llama.cpp management produces a 404 because Ollama does not implement the router endpoints.
M docs/subagents.md +2 -2
@@ -48,7 +48,7 @@ Supported metadata:
4848 | `systemPromptMode` | `append` (default) or `replace`. |
4949 | `inheritProjectContext` | Set to `false` to omit `AGENTS.md` context from the child. |
5050 | `inheritSkills` | Set to `false` to omit skill discovery and the conditional `skill` loader from the child; `--no-skill` has the same effect for a run. |
51-| `fastMode` | Optional fast-mode restriction. Omit it to inherit the host setting; `false` disables fast mode for this profile; `true` only permits fast mode when the host setting is enabled. |
51+| `fastMode` | Optional fast-mode override. Omit it to inherit the host setting; `false` disables fast mode for this profile; `true` enables it for this profile even when the host setting is off. |
5252
5353 Other frontmatter from another agent host is ignored when zut does not have an equivalent setting. Recursive child spawning is disabled in v1; a worker cannot invoke `subagent_spawn` to create descendants.
5454
@@ -84,7 +84,7 @@ The interactive command also supports the same selection explicitly:
8484 /subagents new --agent implementer --reasoning high Implement the parser change
8585 ```
8686
87-Shared mode preserves the historical host working directory. For parallel coding, pass `isolation:"worktree"` to `subagent_spawn`; zut creates a detached Git worktree, captures changed files and a patch, and never merges automatically. Named profiles change the child's instructions and configuration; they are not a security sandbox. A profile's `systemPromptMode` controls its own body relative to the built-in identity; global append addenda, including enabled Ponytail coding guidance, remain present. Child credentials are transferred over stdin rather than argv or persisted metadata, and the active provider endpoint/TLS setting is inherited only when the child uses that provider. Fast mode is inherited by default. A profile with `fastMode: false` opts out, while `fastMode: true` cannot enable fast mode when the host setting is off. Non-OpenAI child providers return an unsupported-provider error instead of silently ignoring an enabled setting.
87+Shared mode preserves the historical host working directory. For parallel coding, pass `isolation:"worktree"` to `subagent_spawn`; zut creates a detached Git worktree, captures changed files and a patch, and never merges automatically. Named profiles change the child's instructions and configuration; they are not a security sandbox. A profile's `systemPromptMode` controls its own body relative to the built-in identity; globally appended instructions, including enabled Ponytail coding guidance, remain present. Child credentials are transferred over stdin rather than argv or persisted metadata, and the active provider endpoint/TLS setting is inherited only when the child uses that provider. Fast mode is inherited by default. A profile with `fastMode: false` opts out, while `fastMode: true` enables fast mode even when the host setting is off; the `subagent_spawn` result warns the parent session when this override occurs. Child providers that do not yet support fast mode return an unsupported-provider error instead of silently ignoring an enabled setting.
8888
8989 ## Lifecycle, results, and recovery
9090
@@ -48,7 +48,7 @@ Supported metadata:
48 | `systemPromptMode` | `append` (default) or `replace`. |48 | `systemPromptMode` | `append` (default) or `replace`. |
49 | `inheritProjectContext` | Set to `false` to omit `AGENTS.md` context from the child. |49 | `inheritProjectContext` | Set to `false` to omit `AGENTS.md` context from the child. |
50 | `inheritSkills` | Set to `false` to omit skill discovery and the conditional `skill` loader from the child; `--no-skill` has the same effect for a run. |50 | `inheritSkills` | Set to `false` to omit skill discovery and the conditional `skill` loader from the child; `--no-skill` has the same effect for a run. |
51-| `fastMode` | Optional fast-mode restriction. Omit it to inherit the host setting; `false` disables fast mode for this profile; `true` only permits fast mode when the host setting is enabled. |51+| `fastMode` | Optional fast-mode override. Omit it to inherit the host setting; `false` disables fast mode for this profile; `true` enables it for this profile even when the host setting is off. |
52 52
53 Other frontmatter from another agent host is ignored when zut does not have an equivalent setting. Recursive child spawning is disabled in v1; a worker cannot invoke `subagent_spawn` to create descendants.53 Other frontmatter from another agent host is ignored when zut does not have an equivalent setting. Recursive child spawning is disabled in v1; a worker cannot invoke `subagent_spawn` to create descendants.
54 54
@@ -84,7 +84,7 @@ The interactive command also supports the same selection explicitly:
84 /subagents new --agent implementer --reasoning high Implement the parser change84 /subagents new --agent implementer --reasoning high Implement the parser change
85 ```85 ```
86 86
87-Shared mode preserves the historical host working directory. For parallel coding, pass `isolation:"worktree"` to `subagent_spawn`; zut creates a detached Git worktree, captures changed files and a patch, and never merges automatically. Named profiles change the child's instructions and configuration; they are not a security sandbox. A profile's `systemPromptMode` controls its own body relative to the built-in identity; global append addenda, including enabled Ponytail coding guidance, remain present. Child credentials are transferred over stdin rather than argv or persisted metadata, and the active provider endpoint/TLS setting is inherited only when the child uses that provider. Fast mode is inherited by default. A profile with `fastMode: false` opts out, while `fastMode: true` cannot enable fast mode when the host setting is off. Non-OpenAI child providers return an unsupported-provider error instead of silently ignoring an enabled setting.87+Shared mode preserves the historical host working directory. For parallel coding, pass `isolation:"worktree"` to `subagent_spawn`; zut creates a detached Git worktree, captures changed files and a patch, and never merges automatically. Named profiles change the child's instructions and configuration; they are not a security sandbox. A profile's `systemPromptMode` controls its own body relative to the built-in identity; globally appended instructions, including enabled Ponytail coding guidance, remain present. Child credentials are transferred over stdin rather than argv or persisted metadata, and the active provider endpoint/TLS setting is inherited only when the child uses that provider. Fast mode is inherited by default. A profile with `fastMode: false` opts out, while `fastMode: true` enables fast mode even when the host setting is off; the `subagent_spawn` result warns the parent session when this override occurs. Child providers that do not yet support fast mode return an unsupported-provider error instead of silently ignoring an enabled setting.
88 88
89 ## Lifecycle, results, and recovery89 ## Lifecycle, results, and recovery
90 90
M packages/agent/build.go +4 -5
@@ -780,12 +780,11 @@ func Resolve(args Args, requireCred bool) (Resolved, error) {
780780 if args.FastModeSet {
781781 fastMode = args.FastMode
782782 }
783- // A profile can opt out of the effective host/child fast-mode setting,
784- // but it does not enable fast mode by itself. Keep this as a
785- // restriction on the effective value rather than an override so an
786- // explicit child --fast-mode flag cannot bypass fastMode: false.
783+ // A profile's explicit fastMode setting overrides the global setting
784+ // for this child. This preserves fastMode: false as an opt-out while
785+ // allowing fastMode: true to request the fast tier for a focused worker.
787786 if selectedProfile != nil && selectedProfile.FastMode != nil {
788- fastMode = fastMode && *selectedProfile.FastMode
787+ fastMode = *selectedProfile.FastMode
789788 }
790789
791790 max := args.MaxSteps // 0 = unlimited
@@ -780,12 +780,11 @@ func Resolve(args Args, requireCred bool) (Resolved, error) {
780 if args.FastModeSet {780 if args.FastModeSet {
781 fastMode = args.FastMode781 fastMode = args.FastMode
782 }782 }
783- // A profile can opt out of the effective host/child fast-mode setting,783+ // A profile's explicit fastMode setting overrides the global setting
784- // but it does not enable fast mode by itself. Keep this as a784+ // for this child. This preserves fastMode: false as an opt-out while
785- // restriction on the effective value rather than an override so an785+ // allowing fastMode: true to request the fast tier for a focused worker.
786- // explicit child --fast-mode flag cannot bypass fastMode: false.
787 if selectedProfile != nil && selectedProfile.FastMode != nil {786 if selectedProfile != nil && selectedProfile.FastMode != nil {
788- fastMode = fastMode && *selectedProfile.FastMode787+ fastMode = *selectedProfile.FastMode
789 }788 }
790 789
791 max := args.MaxSteps // 0 = unlimited790 max := args.MaxSteps // 0 = unlimited
M packages/agent/build_test.go +2 -2
@@ -300,7 +300,7 @@ You are a read-only reviewer.
300300 }
301301 }
302302
303-func TestResolveSubagentFastModeUsesHostSettingAsAnUpperBound(t *testing.T) {
303+func TestResolveSubagentFastModeProfileOverridesHostSetting(t *testing.T) {
304304 cases := []struct {
305305 name string
306306 hostFastMode bool
@@ -312,7 +312,7 @@ func TestResolveSubagentFastModeUsesHostSettingAsAnUpperBound(t *testing.T) {
312312 {name: "unset inherits enabled host", hostFastMode: true, wantFastMode: true},
313313 {name: "unset inherits disabled host", hostFastMode: false, wantFastMode: false},
314314 {name: "false disables enabled host", hostFastMode: true, profileFast: boolPtr(false), wantFastMode: false},
315- {name: "true cannot enable disabled host", hostFastMode: false, profileFast: boolPtr(true), wantFastMode: false},
315+ {name: "true enables disabled host", hostFastMode: false, profileFast: boolPtr(true), wantFastMode: true},
316316 {name: "false stays disabled with disabled host", hostFastMode: false, profileFast: boolPtr(false), wantFastMode: false},
317317 {name: "profile false cannot be bypassed", hostFastMode: true, profileFast: boolPtr(false), argFastMode: true, argFastSet: true, wantFastMode: false},
318318 }
@@ -300,7 +300,7 @@ You are a read-only reviewer.
300 }300 }
301 }301 }
302 302
303-func TestResolveSubagentFastModeUsesHostSettingAsAnUpperBound(t *testing.T) {303+func TestResolveSubagentFastModeProfileOverridesHostSetting(t *testing.T) {
304 cases := []struct {304 cases := []struct {
305 name string305 name string
306 hostFastMode bool306 hostFastMode bool
@@ -312,7 +312,7 @@ func TestResolveSubagentFastModeUsesHostSettingAsAnUpperBound(t *testing.T) {
312 {name: "unset inherits enabled host", hostFastMode: true, wantFastMode: true},312 {name: "unset inherits enabled host", hostFastMode: true, wantFastMode: true},
313 {name: "unset inherits disabled host", hostFastMode: false, wantFastMode: false},313 {name: "unset inherits disabled host", hostFastMode: false, wantFastMode: false},
314 {name: "false disables enabled host", hostFastMode: true, profileFast: boolPtr(false), wantFastMode: false},314 {name: "false disables enabled host", hostFastMode: true, profileFast: boolPtr(false), wantFastMode: false},
315- {name: "true cannot enable disabled host", hostFastMode: false, profileFast: boolPtr(true), wantFastMode: false},315+ {name: "true enables disabled host", hostFastMode: false, profileFast: boolPtr(true), wantFastMode: true},
316 {name: "false stays disabled with disabled host", hostFastMode: false, profileFast: boolPtr(false), wantFastMode: false},316 {name: "false stays disabled with disabled host", hostFastMode: false, profileFast: boolPtr(false), wantFastMode: false},
317 {name: "profile false cannot be bypassed", hostFastMode: true, profileFast: boolPtr(false), argFastMode: true, argFastSet: true, wantFastMode: false},317 {name: "profile false cannot be bypassed", hostFastMode: true, profileFast: boolPtr(false), argFastMode: true, argFastSet: true, wantFastMode: false},
318 }318 }
M packages/agent/modes/fast_mode_test.go +33 -0
@@ -6,6 +6,7 @@ import (
66 "strings"
77 "testing"
88
9+ "github.com/bnema/zut/packages/agent/subagents"
910 "github.com/bnema/zut/packages/core"
1011 "github.com/bnema/zut/packages/provider"
1112 )
@@ -76,6 +77,38 @@ func TestApplyFastModeUpdatesAgentAndStore(t *testing.T) {
7677 }
7778 }
7879
80+func TestFastSlashTogglesFastMode(t *testing.T) {
81+ store := &recordingFastModeSettingsStore{}
82+ agent := core.NewAgent(fastModeProviderClient{}, "gpt-5", "", nil)
83+ supervisor := subagents.New(subagents.Config{Root: t.TempDir(), RepoRoot: t.TempDir()})
84+ interactive := NewInteractive(InteractiveConfig{
85+ Provider: "openai",
86+ Model: "gpt-5",
87+ SettingsStore: store,
88+ Agent: agent,
89+ Supervisor: supervisor,
90+ })
91+ interactive.rend = nil
92+
93+ if interactive.runSlash(context.Background(), "/FAST") {
94+ t.Fatal("/fast requested exit")
95+ }
96+ if interactive.cfg.FastMode == nil || !*interactive.cfg.FastMode || !agent.FastMode || !supervisor.FastMode() {
97+ t.Fatalf("/fast did not enable every live fast-mode target: cfg=%v agent=%v supervisor=%v", interactive.cfg.FastMode, agent.FastMode, supervisor.FastMode())
98+ }
99+ if store.setCalls != 1 || !store.last || interactive.statusOK != "fast mode enabled" {
100+ t.Fatalf("enable persistence/status = calls=%d last=%v status=%q", store.setCalls, store.last, interactive.statusOK)
101+ }
102+
103+ interactive.runSlash(context.Background(), "/fast")
104+ if interactive.cfg.FastMode == nil || *interactive.cfg.FastMode || agent.FastMode || supervisor.FastMode() {
105+ t.Fatalf("/fast did not disable every live fast-mode target: cfg=%v agent=%v supervisor=%v", interactive.cfg.FastMode, agent.FastMode, supervisor.FastMode())
106+ }
107+ if store.setCalls != 2 || store.last || interactive.statusOK != "fast mode disabled" {
108+ t.Fatalf("disable persistence/status = calls=%d last=%v status=%q", store.setCalls, store.last, interactive.statusOK)
109+ }
110+}
111+
79112 func TestApplyFastModeRejectsUnsupportedProvider(t *testing.T) {
80113 store := &recordingFastModeSettingsStore{}
81114 interactive := NewInteractive(InteractiveConfig{
@@ -6,6 +6,7 @@ import (
6 "strings"6 "strings"
7 "testing"7 "testing"
8 8
9+ "github.com/bnema/zut/packages/agent/subagents"
9 "github.com/bnema/zut/packages/core"10 "github.com/bnema/zut/packages/core"
10 "github.com/bnema/zut/packages/provider"11 "github.com/bnema/zut/packages/provider"
11 )12 )
@@ -76,6 +77,38 @@ func TestApplyFastModeUpdatesAgentAndStore(t *testing.T) {
76 }77 }
77 }78 }
78 79
80+func TestFastSlashTogglesFastMode(t *testing.T) {
81+ store := &recordingFastModeSettingsStore{}
82+ agent := core.NewAgent(fastModeProviderClient{}, "gpt-5", "", nil)
83+ supervisor := subagents.New(subagents.Config{Root: t.TempDir(), RepoRoot: t.TempDir()})
84+ interactive := NewInteractive(InteractiveConfig{
85+ Provider: "openai",
86+ Model: "gpt-5",
87+ SettingsStore: store,
88+ Agent: agent,
89+ Supervisor: supervisor,
90+ })
91+ interactive.rend = nil
92+
93+ if interactive.runSlash(context.Background(), "/FAST") {
94+ t.Fatal("/fast requested exit")
95+ }
96+ if interactive.cfg.FastMode == nil || !*interactive.cfg.FastMode || !agent.FastMode || !supervisor.FastMode() {
97+ t.Fatalf("/fast did not enable every live fast-mode target: cfg=%v agent=%v supervisor=%v", interactive.cfg.FastMode, agent.FastMode, supervisor.FastMode())
98+ }
99+ if store.setCalls != 1 || !store.last || interactive.statusOK != "fast mode enabled" {
100+ t.Fatalf("enable persistence/status = calls=%d last=%v status=%q", store.setCalls, store.last, interactive.statusOK)
101+ }
102+
103+ interactive.runSlash(context.Background(), "/fast")
104+ if interactive.cfg.FastMode == nil || *interactive.cfg.FastMode || agent.FastMode || supervisor.FastMode() {
105+ t.Fatalf("/fast did not disable every live fast-mode target: cfg=%v agent=%v supervisor=%v", interactive.cfg.FastMode, agent.FastMode, supervisor.FastMode())
106+ }
107+ if store.setCalls != 2 || store.last || interactive.statusOK != "fast mode disabled" {
108+ t.Fatalf("disable persistence/status = calls=%d last=%v status=%q", store.setCalls, store.last, interactive.statusOK)
109+ }
110+}
111+
79 func TestApplyFastModeRejectsUnsupportedProvider(t *testing.T) {112 func TestApplyFastModeRejectsUnsupportedProvider(t *testing.T) {
80 store := &recordingFastModeSettingsStore{}113 store := &recordingFastModeSettingsStore{}
81 interactive := NewInteractive(InteractiveConfig{114 interactive := NewInteractive(InteractiveConfig{
M packages/agent/modes/interactive.go +4 -1
@@ -4317,7 +4317,7 @@ func (i *Interactive) openSettingsDialog() {
43174317 {
43184318 key: "fast_mode",
43194319 label: "fast mode",
4320- desc: "request OpenAI's fast service tier; unsupported providers return an error",
4320+ desc: "request the provider's fast tier where supported; unsupported providers return an error",
43214321 value: fastMode,
43224322 hint: fastModeHint,
43234323 },
@@ -5544,6 +5544,9 @@ func (i *Interactive) runSlash(ctx context.Context, cmd string) (done bool) {
55445544 }
55455545 case "/reasoning":
55465546 i.openReasoningDialog()
5547+ case "/fast":
5548+ enabled := i.cfg.FastMode == nil || !*i.cfg.FastMode
5549+ i.applySettingToggle("fast_mode", enabled)
55475550 case "/settings":
55485551 i.openSettingsDialog()
55495552 case "/sessions":
@@ -4317,7 +4317,7 @@ func (i *Interactive) openSettingsDialog() {
4317 {4317 {
4318 key: "fast_mode",4318 key: "fast_mode",
4319 label: "fast mode",4319 label: "fast mode",
4320- desc: "request OpenAI's fast service tier; unsupported providers return an error",4320+ desc: "request the provider's fast tier where supported; unsupported providers return an error",
4321 value: fastMode,4321 value: fastMode,
4322 hint: fastModeHint,4322 hint: fastModeHint,
4323 },4323 },
@@ -5544,6 +5544,9 @@ func (i *Interactive) runSlash(ctx context.Context, cmd string) (done bool) {
5544 }5544 }
5545 case "/reasoning":5545 case "/reasoning":
5546 i.openReasoningDialog()5546 i.openReasoningDialog()
5547+ case "/fast":
5548+ enabled := i.cfg.FastMode == nil || !*i.cfg.FastMode
5549+ i.applySettingToggle("fast_mode", enabled)
5547 case "/settings":5550 case "/settings":
5548 i.openSettingsDialog()5551 i.openSettingsDialog()
5549 case "/sessions":5552 case "/sessions":
M packages/agent/modes/slash_suggest.go +1 -0
@@ -51,6 +51,7 @@ var slashCatalog = []slashCommand{
5151 {Name: "/logout", Desc: "clear a provider's credentials"},
5252 {Name: "/model", Desc: "pick a model (or /model <id>)"},
5353 {Name: "/reasoning", Desc: "set the reasoning level"},
54+ {Name: "/fast", Desc: "toggle fast mode"},
5455 {Name: "/llama", Desc: "manage llama.cpp router models"},
5556 {Name: "/sessions", Desc: "resume a previous session for this directory"},
5657 {Name: "/fork", Desc: "pick a user message and fork the current session"},
@@ -51,6 +51,7 @@ var slashCatalog = []slashCommand{
51 {Name: "/logout", Desc: "clear a provider's credentials"},51 {Name: "/logout", Desc: "clear a provider's credentials"},
52 {Name: "/model", Desc: "pick a model (or /model <id>)"},52 {Name: "/model", Desc: "pick a model (or /model <id>)"},
53 {Name: "/reasoning", Desc: "set the reasoning level"},53 {Name: "/reasoning", Desc: "set the reasoning level"},
54+ {Name: "/fast", Desc: "toggle fast mode"},
54 {Name: "/llama", Desc: "manage llama.cpp router models"},55 {Name: "/llama", Desc: "manage llama.cpp router models"},
55 {Name: "/sessions", Desc: "resume a previous session for this directory"},56 {Name: "/sessions", Desc: "resume a previous session for this directory"},
56 {Name: "/fork", Desc: "pick a user message and fork the current session"},57 {Name: "/fork", Desc: "pick a user message and fork the current session"},
M packages/agent/modes/slash_suggest_test.go +10 -0
@@ -44,6 +44,16 @@ func TestSlashSuggesterHasSupervisor(t *testing.T) {
4444 }
4545 }
4646
47+func TestSlashSuggesterHasFastModeToggle(t *testing.T) {
48+ s := newSlashSuggester()
49+ if got := commandNames(s.matches("/fa")); !contains(got, "/fast") {
50+ t.Fatalf("/fast missing from suggestions, got %v", got)
51+ }
52+ if !isKnownSlashCommand("/FAST") {
53+ t.Fatal("/FAST was not recognized as a built-in command")
54+ }
55+}
56+
4757 func TestSlashCommandsAreCaseInsensitive(t *testing.T) {
4858 s := newSlashSuggester()
4959 if got := commandNames(s.matches("/EX")); !contains(got, "/exit") {
@@ -44,6 +44,16 @@ func TestSlashSuggesterHasSupervisor(t *testing.T) {
44 }44 }
45 }45 }
46 46
47+func TestSlashSuggesterHasFastModeToggle(t *testing.T) {
48+ s := newSlashSuggester()
49+ if got := commandNames(s.matches("/fa")); !contains(got, "/fast") {
50+ t.Fatalf("/fast missing from suggestions, got %v", got)
51+ }
52+ if !isKnownSlashCommand("/FAST") {
53+ t.Fatal("/FAST was not recognized as a built-in command")
54+ }
55+}
56+
47 func TestSlashCommandsAreCaseInsensitive(t *testing.T) {57 func TestSlashCommandsAreCaseInsensitive(t *testing.T) {
48 s := newSlashSuggester()58 s := newSlashSuggester()
49 if got := commandNames(s.matches("/EX")); !contains(got, "/exit") {59 if got := commandNames(s.matches("/EX")); !contains(got, "/exit") {
M packages/agent/subagents/agent.go +12 -0
@@ -57,6 +57,12 @@ type Agent struct {
5757 FastMode bool
5858 Subagent string
5959
60+ // fastModeOverridesHost records the spawn-time case where an explicit
61+ // profile fastMode: true enabled the child while the host was off. It is
62+ // intentionally transient: the parent only needs it to report the initial
63+ // spawn warning, while the effective FastMode remains durable state.
64+ fastModeOverridesHost bool
65+
6066 // SessionID, when non-empty, scopes the agent to a particular
6167 // host zut session: the dashboard only surfaces agents whose
6268 // SessionID matches the active session. Empty means "unscoped" and
@@ -167,6 +173,12 @@ type Agent struct {
167173 // doesn't speak the daemon protocol).
168174 func (a *Agent) Inbox() *Inbox { return a.inbox }
169175
176+// FastModeOverridesHost reports whether this child was created with an
177+// explicit fast-mode override while the host setting was disabled.
178+func (a *Agent) FastModeOverridesHost() bool {
179+ return a != nil && a.fastModeOverridesHost
180+}
181+
170182 // Status returns the current high-level status. Cheap; safe from any goroutine.
171183 func (a *Agent) Status() Status {
172184 a.mu.Lock()
@@ -57,6 +57,12 @@ type Agent struct {
57 FastMode bool57 FastMode bool
58 Subagent string58 Subagent string
59 59
60+ // fastModeOverridesHost records the spawn-time case where an explicit
61+ // profile fastMode: true enabled the child while the host was off. It is
62+ // intentionally transient: the parent only needs it to report the initial
63+ // spawn warning, while the effective FastMode remains durable state.
64+ fastModeOverridesHost bool
65+
60 // SessionID, when non-empty, scopes the agent to a particular66 // SessionID, when non-empty, scopes the agent to a particular
61 // host zut session: the dashboard only surfaces agents whose67 // host zut session: the dashboard only surfaces agents whose
62 // SessionID matches the active session. Empty means "unscoped" and68 // SessionID matches the active session. Empty means "unscoped" and
@@ -167,6 +173,12 @@ type Agent struct {
167 // doesn't speak the daemon protocol).173 // doesn't speak the daemon protocol).
168 func (a *Agent) Inbox() *Inbox { return a.inbox }174 func (a *Agent) Inbox() *Inbox { return a.inbox }
169 175
176+// FastModeOverridesHost reports whether this child was created with an
177+// explicit fast-mode override while the host setting was disabled.
178+func (a *Agent) FastModeOverridesHost() bool {
179+ return a != nil && a.fastModeOverridesHost
180+}
181+
170 // Status returns the current high-level status. Cheap; safe from any goroutine.182 // Status returns the current high-level status. Cheap; safe from any goroutine.
171 func (a *Agent) Status() Status {183 func (a *Agent) Status() Status {
172 a.mu.Lock()184 a.mu.Lock()
M packages/agent/subagents/fast_mode_test.go +10 -6
@@ -58,18 +58,19 @@ func TestSupervisorFastModePropagatesToChild(t *testing.T) {
5858 }
5959 }
6060
61-func TestSpawnRequestFastModeIsBoundByHostSetting(t *testing.T) {
61+func TestSpawnRequestFastModeProfileOverridesHostSetting(t *testing.T) {
6262 falseValue, trueValue := false, true
6363 cases := []struct {
64- name string
65- hostFast bool
66- profileFast *bool
67- want bool
64+ name string
65+ hostFast bool
66+ profileFast *bool
67+ want bool
68+ wantHostOverride bool
6869 }{
6970 {name: "unset inherits enabled host", hostFast: true, want: true},
7071 {name: "unset inherits disabled host", hostFast: false, want: false},
7172 {name: "false disables enabled host", hostFast: true, profileFast: &falseValue, want: false},
72- {name: "true cannot enable disabled host", hostFast: false, profileFast: &trueValue, want: false},
73+ {name: "true enables disabled host", hostFast: false, profileFast: &trueValue, want: true, wantHostOverride: true},
7374 {name: "true preserves enabled host", hostFast: true, profileFast: &trueValue, want: true},
7475 }
7576 for _, tc := range cases {
@@ -91,6 +92,9 @@ func TestSpawnRequestFastModeIsBoundByHostSetting(t *testing.T) {
9192 if a.FastMode != tc.want {
9293 t.Fatalf("FastMode = %v, want %v", a.FastMode, tc.want)
9394 }
95+ if a.FastModeOverridesHost() != tc.wantHostOverride {
96+ t.Fatalf("FastModeOverridesHost() = %v, want %v", a.FastModeOverridesHost(), tc.wantHostOverride)
97+ }
9498 args := defaultChildArgs("/zut", a, "/session", "/inbox")
9599 wantFlag := "--no-fast-mode"
96100 if tc.want {
@@ -58,18 +58,19 @@ func TestSupervisorFastModePropagatesToChild(t *testing.T) {
58 }58 }
59 }59 }
60 60
61-func TestSpawnRequestFastModeIsBoundByHostSetting(t *testing.T) {61+func TestSpawnRequestFastModeProfileOverridesHostSetting(t *testing.T) {
62 falseValue, trueValue := false, true62 falseValue, trueValue := false, true
63 cases := []struct {63 cases := []struct {
64- name string64+ name string
65- hostFast bool65+ hostFast bool
66- profileFast *bool66+ profileFast *bool
67- want bool67+ want bool
68+ wantHostOverride bool
68 }{69 }{
69 {name: "unset inherits enabled host", hostFast: true, want: true},70 {name: "unset inherits enabled host", hostFast: true, want: true},
70 {name: "unset inherits disabled host", hostFast: false, want: false},71 {name: "unset inherits disabled host", hostFast: false, want: false},
71 {name: "false disables enabled host", hostFast: true, profileFast: &falseValue, want: false},72 {name: "false disables enabled host", hostFast: true, profileFast: &falseValue, want: false},
72- {name: "true cannot enable disabled host", hostFast: false, profileFast: &trueValue, want: false},73+ {name: "true enables disabled host", hostFast: false, profileFast: &trueValue, want: true, wantHostOverride: true},
73 {name: "true preserves enabled host", hostFast: true, profileFast: &trueValue, want: true},74 {name: "true preserves enabled host", hostFast: true, profileFast: &trueValue, want: true},
74 }75 }
75 for _, tc := range cases {76 for _, tc := range cases {
@@ -91,6 +92,9 @@ func TestSpawnRequestFastModeIsBoundByHostSetting(t *testing.T) {
91 if a.FastMode != tc.want {92 if a.FastMode != tc.want {
92 t.Fatalf("FastMode = %v, want %v", a.FastMode, tc.want)93 t.Fatalf("FastMode = %v, want %v", a.FastMode, tc.want)
93 }94 }
95+ if a.FastModeOverridesHost() != tc.wantHostOverride {
96+ t.Fatalf("FastModeOverridesHost() = %v, want %v", a.FastModeOverridesHost(), tc.wantHostOverride)
97+ }
94 args := defaultChildArgs("/zut", a, "/session", "/inbox")98 args := defaultChildArgs("/zut", a, "/session", "/inbox")
95 wantFlag := "--no-fast-mode"99 wantFlag := "--no-fast-mode"
96 if tc.want {100 if tc.want {
M packages/agent/subagents/supervisor.go +7 -5
@@ -306,7 +306,7 @@ func (f *Supervisor) uniqueAgentIDLocked(base string) string {
306306 // chosen model regardless of the parent's current selection. A named
307307 // Subagent is passed to the child as --subagent, where it resolves the
308308 // markdown profile and applies its instructions. Reasoning is passed
309-// as --reasoning when set. FastMode restricts the host setting when a
309+// as --reasoning when set. FastMode overrides the host setting when a
310310 // selected profile declares a fastMode preference.
311311 type SpawnRequest struct {
312312 Task string
@@ -317,9 +317,8 @@ type SpawnRequest struct {
317317 // effective custom provider endpoint.
318318 InsecureTLS bool
319319 Reasoning string // optional reasoning/thinking level override
320- // FastMode is an optional profile restriction. A non-nil false value
321- // disables fast mode for this child; true still cannot enable fast mode
322- // when Config.FastMode is false. Nil inherits Config.FastMode.
320+ // FastMode is an optional profile override. A non-nil value enables or
321+ // disables fast mode for this child; nil inherits Config.FastMode.
323322 FastMode *bool
324323 Subagent string // optional named markdown profile
325324
@@ -386,8 +385,10 @@ func (f *Supervisor) SpawnReq(ctx context.Context, req SpawnRequest) (*Agent, er
386385 configInsecureTLS := f.cfg.InsecureTLS
387386 sessionID := f.activeSession
388387 f.mu.Unlock()
388+ fastModeOverridesHost := false
389389 if req.FastMode != nil {
390- fastMode = fastMode && *req.FastMode
390+ fastModeOverridesHost = !fastMode && *req.FastMode
391+ fastMode = *req.FastMode
391392 }
392393 childProvider := strings.TrimSpace(req.Provider)
393394 inheritProviderSettings := childProvider == "" || configProvider == "" || strings.EqualFold(childProvider, strings.TrimSpace(configProvider))
@@ -509,6 +510,7 @@ func (f *Supervisor) SpawnReq(ctx context.Context, req SpawnRequest) (*Agent, er
509510 done: make(chan struct{}),
510511 turnResults: make(chan *TurnResult, 16),
511512 }
513+ a.fastModeOverridesHost = fastModeOverridesHost
512514 if a.RootSessionID == "" {
513515 a.RootSessionID = sessionID
514516 }
@@ -306,7 +306,7 @@ func (f *Supervisor) uniqueAgentIDLocked(base string) string {
306 // chosen model regardless of the parent's current selection. A named306 // chosen model regardless of the parent's current selection. A named
307 // Subagent is passed to the child as --subagent, where it resolves the307 // Subagent is passed to the child as --subagent, where it resolves the
308 // markdown profile and applies its instructions. Reasoning is passed308 // markdown profile and applies its instructions. Reasoning is passed
309-// as --reasoning when set. FastMode restricts the host setting when a309+// as --reasoning when set. FastMode overrides the host setting when a
310 // selected profile declares a fastMode preference.310 // selected profile declares a fastMode preference.
311 type SpawnRequest struct {311 type SpawnRequest struct {
312 Task string312 Task string
@@ -317,9 +317,8 @@ type SpawnRequest struct {
317 // effective custom provider endpoint.317 // effective custom provider endpoint.
318 InsecureTLS bool318 InsecureTLS bool
319 Reasoning string // optional reasoning/thinking level override319 Reasoning string // optional reasoning/thinking level override
320- // FastMode is an optional profile restriction. A non-nil false value320+ // FastMode is an optional profile override. A non-nil value enables or
321- // disables fast mode for this child; true still cannot enable fast mode321+ // disables fast mode for this child; nil inherits Config.FastMode.
322- // when Config.FastMode is false. Nil inherits Config.FastMode.
323 FastMode *bool322 FastMode *bool
324 Subagent string // optional named markdown profile323 Subagent string // optional named markdown profile
325 324
@@ -386,8 +385,10 @@ func (f *Supervisor) SpawnReq(ctx context.Context, req SpawnRequest) (*Agent, er
386 configInsecureTLS := f.cfg.InsecureTLS385 configInsecureTLS := f.cfg.InsecureTLS
387 sessionID := f.activeSession386 sessionID := f.activeSession
388 f.mu.Unlock()387 f.mu.Unlock()
388+ fastModeOverridesHost := false
389 if req.FastMode != nil {389 if req.FastMode != nil {
390- fastMode = fastMode && *req.FastMode390+ fastModeOverridesHost = !fastMode && *req.FastMode
391+ fastMode = *req.FastMode
391 }392 }
392 childProvider := strings.TrimSpace(req.Provider)393 childProvider := strings.TrimSpace(req.Provider)
393 inheritProviderSettings := childProvider == "" || configProvider == "" || strings.EqualFold(childProvider, strings.TrimSpace(configProvider))394 inheritProviderSettings := childProvider == "" || configProvider == "" || strings.EqualFold(childProvider, strings.TrimSpace(configProvider))
@@ -509,6 +510,7 @@ func (f *Supervisor) SpawnReq(ctx context.Context, req SpawnRequest) (*Agent, er
509 done: make(chan struct{}),510 done: make(chan struct{}),
510 turnResults: make(chan *TurnResult, 16),511 turnResults: make(chan *TurnResult, 16),
511 }512 }
513+ a.fastModeOverridesHost = fastModeOverridesHost
512 if a.RootSessionID == "" {514 if a.RootSessionID == "" {
513 a.RootSessionID = sessionID515 a.RootSessionID = sessionID
514 }516 }
M packages/agent/tools/subagent_spawn.go +3 -0
@@ -274,6 +274,9 @@ func (t *SubagentSpawnTool) Execute(ctx context.Context, raw json.RawMessage, pr
274274 if agent.FastMode {
275275 sb.WriteString("fast mode: enabled\n")
276276 }
277+ if fastModeOverride != nil && *fastModeOverride && agent.FastModeOverridesHost() {
278+ sb.WriteString("warning: subagent profile has fast mode enabled, overriding global fast mode off\n")
279+ }
277280 sb.WriteString("\nThe sub-agent is running in the background. Use /subagents in the TUI to monitor it. ")
278281 sb.WriteString("This conversation continues immediately; do not wait for the sub-agent to finish before working on the next thing.")
279282 return core.ToolResult{
@@ -274,6 +274,9 @@ func (t *SubagentSpawnTool) Execute(ctx context.Context, raw json.RawMessage, pr
274 if agent.FastMode {274 if agent.FastMode {
275 sb.WriteString("fast mode: enabled\n")275 sb.WriteString("fast mode: enabled\n")
276 }276 }
277+ if fastModeOverride != nil && *fastModeOverride && agent.FastModeOverridesHost() {
278+ sb.WriteString("warning: subagent profile has fast mode enabled, overriding global fast mode off\n")
279+ }
277 sb.WriteString("\nThe sub-agent is running in the background. Use /subagents in the TUI to monitor it. ")280 sb.WriteString("\nThe sub-agent is running in the background. Use /subagents in the TUI to monitor it. ")
278 sb.WriteString("This conversation continues immediately; do not wait for the sub-agent to finish before working on the next thing.")281 sb.WriteString("This conversation continues immediately; do not wait for the sub-agent to finish before working on the next thing.")
279 return core.ToolResult{282 return core.ToolResult{
M packages/agent/tools/subagent_spawn_test.go +41 -0
@@ -299,6 +299,47 @@ func TestSubagentSpawnAppliesProfileFastModeRestriction(t *testing.T) {
299299 }
300300 }
301301
302+func TestSubagentSpawnProfileFastModeOverridesGlobalOffWithWarning(t *testing.T) {
303+ root := t.TempDir()
304+ profileFastMode := true
305+ f := subagents.New(subagents.Config{
306+ Root: filepath.Join(root, "subagents"),
307+ RepoRoot: root,
308+ NewRunner: func(*subagents.Agent) subagents.Runner {
309+ return noopSupervisorRunner{}
310+ },
311+ })
312+ t.Cleanup(f.StopAll)
313+ tool := &SubagentSpawnTool{
314+ Supervisor: f,
315+ Enabled: func() bool { return true },
316+ ResolveSubagent: func(name string) (*subagents.Profile, error) {
317+ if name != "fast-worker" {
318+ return nil, nil
319+ }
320+ return &subagents.Profile{Name: name, FastMode: &profileFastMode}, nil
321+ },
322+ }
323+
324+ res, err := tool.Execute(context.Background(), json.RawMessage(`{"task":"review auth","agent":"fast-worker"}`), nil)
325+ if err != nil {
326+ t.Fatal(err)
327+ }
328+ if res.IsError {
329+ t.Fatalf("unexpected tool error: %s", textResult(res.Content))
330+ }
331+ agents := f.List()
332+ if len(agents) != 1 || !agents[0].FastMode {
333+ t.Fatalf("agent fast mode = %#v, want enabled by profile", agents)
334+ }
335+ if got := res.Details.(map[string]any)["fast_mode"]; got != true {
336+ t.Fatalf("fast_mode detail = %v, want true", got)
337+ }
338+ if text := textResult(res.Content); !strings.Contains(text, "warning: subagent profile has fast mode enabled, overriding global fast mode off") {
339+ t.Fatalf("spawn result missing profile override warning:\n%s", text)
340+ }
341+}
342+
302343 func TestSubagentSpawnUsesProfileReasoningWhenOmitted(t *testing.T) {
303344 tool := &SubagentSpawnTool{
304345 Supervisor: newTestSupervisor(t),
@@ -299,6 +299,47 @@ func TestSubagentSpawnAppliesProfileFastModeRestriction(t *testing.T) {
299 }299 }
300 }300 }
301 301
302+func TestSubagentSpawnProfileFastModeOverridesGlobalOffWithWarning(t *testing.T) {
303+ root := t.TempDir()
304+ profileFastMode := true
305+ f := subagents.New(subagents.Config{
306+ Root: filepath.Join(root, "subagents"),
307+ RepoRoot: root,
308+ NewRunner: func(*subagents.Agent) subagents.Runner {
309+ return noopSupervisorRunner{}
310+ },
311+ })
312+ t.Cleanup(f.StopAll)
313+ tool := &SubagentSpawnTool{
314+ Supervisor: f,
315+ Enabled: func() bool { return true },
316+ ResolveSubagent: func(name string) (*subagents.Profile, error) {
317+ if name != "fast-worker" {
318+ return nil, nil
319+ }
320+ return &subagents.Profile{Name: name, FastMode: &profileFastMode}, nil
321+ },
322+ }
323+
324+ res, err := tool.Execute(context.Background(), json.RawMessage(`{"task":"review auth","agent":"fast-worker"}`), nil)
325+ if err != nil {
326+ t.Fatal(err)
327+ }
328+ if res.IsError {
329+ t.Fatalf("unexpected tool error: %s", textResult(res.Content))
330+ }
331+ agents := f.List()
332+ if len(agents) != 1 || !agents[0].FastMode {
333+ t.Fatalf("agent fast mode = %#v, want enabled by profile", agents)
334+ }
335+ if got := res.Details.(map[string]any)["fast_mode"]; got != true {
336+ t.Fatalf("fast_mode detail = %v, want true", got)
337+ }
338+ if text := textResult(res.Content); !strings.Contains(text, "warning: subagent profile has fast mode enabled, overriding global fast mode off") {
339+ t.Fatalf("spawn result missing profile override warning:\n%s", text)
340+ }
341+}
342+
302 func TestSubagentSpawnUsesProfileReasoningWhenOmitted(t *testing.T) {343 func TestSubagentSpawnUsesProfileReasoningWhenOmitted(t *testing.T) {
303 tool := &SubagentSpawnTool{344 tool := &SubagentSpawnTool{
304 Supervisor: newTestSupervisor(t),345 Supervisor: newTestSupervisor(t),

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 476

↑↓ moveenter openesc close