detached 9467429

fix(profiles): persist fast mode per model profile (#187)

* fix(profiles): persist fast mode per model profile

* docs(settings): describe profile fast mode recall
commit
94674294104587ccca01563fd96dcce7e374e8bf
author
bnema · · 2026-09-06 19:33 UTC
committer
GitHub · · 2026-09-06 19:33 UTC
parent
6990358
signature
GPG UnverifiedB5690EEEBB952194 The key is unknown
refs
tag: v0.31.0
14 files · +235 -42
M README.md +11 -11
@@ -326,9 +326,9 @@ Slash command names are case-insensitive in the TUI and messaging backends; argu
326326 | `/login` | Log in via API key or subscription (opens a dialog). |
327327 | `/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. |
328328 | `/model` | Pick a model from a list (or `/model <id>` to set directly). |
329-| `/profile <1-9>` | Activate a saved model + reasoning profile, like `Ctrl+1` through `Ctrl+9`; empty slots open the model picker. See [Model profiles](#model-profiles). |
329+| `/profile <1-9>` | Activate a saved model + reasoning + fast-mode profile, like `Ctrl+1` through `Ctrl+9`; empty slots open the model picker. See [Model profiles](#model-profiles). |
330330 | `/reasoning` | Set the reasoning level for subsequent model calls. |
331-| `/fast` | Toggle fast mode for subsequent model calls. |
331+| `/fast` | Toggle fast mode for subsequent model calls; when a model profile is active, save the preference to that profile. |
332332 | `/orchestrator` | Toggle proactive subagent delegation. When disabled, subagent tools remain available for explicit delegation. |
333333 | `/goal <objective>` | Start an autonomous interactive-session mission. Use `/goal` for status, `/goal history` for prior goals, and `/goal pause`, `resume`, or `clear` for control. |
334334 | `/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. |
@@ -459,7 +459,7 @@ Opens a dialog with every persistent setting. `up`/`down` to navigate, `enter` o
459459 - **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`.
460460 - **proactive delegation** — let the interactive primary delegate concrete, bounded sidecars while it keeps ownership of the critical path. Before spawning, it identifies useful local work and gives the child a non-overlapping question, responsibility, package, or file set. Immediate blockers and tightly coupled work stay local; after spawning, the worker owns its scope until completion and the primary continues only its preselected independent task. If a user request or active workflow explicitly delegates blocking work, the primary yields rather than reproducing it. Off by default. When off, the permitted canonical subagent tools and profile manifest remain available for user-requested delegation or when an active skill workflow requires it. The tools accept named profiles, reasoning, per-spawn fast-mode overrides, and shared/worktree isolation where applicable; lifecycle and result references are persisted. Completion and required-work state are host-driven through `[auto-subagents update]` and `[required-subagents update]` messages; required work stays asynchronous, persists its outcome, and prevents the terminal parent response until a successful completion. If launch-time restrictions prevent spawning, the interactive primary continues locally. Explicit headless `--orchestrate` remains a separate strict manager-only mode and reports unavailable delegation rather than implementing directly. Do not poll or inspect dashboards, metadata, event logs, or files solely to wait. See `/subagents` and [docs/subagents.md](docs/subagents.md) for details.
461461 - **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.
462-- **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.
462+- **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`. When a model profile is active, the value is saved in that profile and restored when switching profiles. Other providers currently return an unsupported-provider error. Fast mode may cost more and depends on the selected model/account.
463463 - **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`.
464464 - **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.
465465 - **auto-compact threshold** — choose `off`, `70%`, `80%`, `85%` (default), `90%`, or `95%` of the model's advertised context window. The selected percentage controls automatic compaction before and after interactive turns and before sub-agent follow-up turns, and persists as `auto_compact_threshold`. Context accounting includes uncached input plus provider-reported cache reads and writes, so cached turns still trigger compaction. Structural unfinished tails and truncated output continue as before. After a successful after-turn threshold compaction, zut may make at most two hidden, bounded recovery continuations when the assistant's visible text appears to promise future work; clear completed text settles normally, and a newer queued user request wins. An interrupted interactive session retains an outstanding handoff when resumed, including after portable export and import. Manual `/compact` does not silently start work. `off` disables percentage-based triggers but keeps manual `/compact` and automatic recovery from context-window and payload-too-large responses. Truncated-output continuation remains a separate priority rule.
@@ -469,15 +469,15 @@ Opens a dialog with every persistent setting. `up`/`down` to navigate, `enter` o
469469 - **TUI settings**: opens a sub-view for input layout in display order: status, working spinner, input, then subagent operations. **Input style** can be `plain` (default prompt line), `lines` (separator lines above and below the input), or `block` (a user-bubble-style input block). **Status position** places model, usage, and working-directory information above or below the input. **Working spinner position** places the busy spinner above or below the input. Changes apply immediately and persist to `config.json` as `tui_input_style`, `tui_status_position`, and `tui_working_position`.
470470 - **reasoning level**: choose reasoning for supported models: off, minimum, low, medium, high, xhigh, or max. The `max` tier is opt-in and sent natively to GPT-5.6 and adaptive-thinking Claude models; unsupported backends clamp it to their highest accepted effort. The change is persisted to `config.json` and applied to the next model call. Use `/reasoning` to open this selector directly, or press `Ctrl+R` to cycle through the active model's supported levels. The selector only shows distinct levels supported by the active model; models without reasoning support only offer `off`.
471471 - **color theme** — `auto` follows the terminal foreground, background, ANSI palette, color depth, and supported live appearance changes. Choose fixed dark/light palettes or JSON overlays discovered under `$ZUT_HOME/themes` and loaded extensions. Active custom files reload safely; invalid edits retain the last valid appearance and persistent deletion resets to auto. See [docs/themes.md](docs/themes.md).
472-- **model profiles** — nine favorites containing a provider, model, and reasoning level. `enter` on a slot opens the `/model` picker; `left`/`right` chooses that favorite's reasoning without changing the live model. `enter` saves the assignment; `backspace` clears it. Editing the active slot immediately applies and saves its new model and reasoning. Assigning other slots does not change the live model. Clearing the active slot detaches it for this run without changing the live model. See [Model profiles](#model-profiles).
472+- **model profiles** — nine favorites containing a provider, model, reasoning level, and fast-mode preference. `enter` on a slot opens the `/model` picker; `left`/`right` chooses that favorite's reasoning without changing the live model. `enter` saves the assignment; `backspace` clears it. Editing the active slot immediately applies and saves its new model and reasoning. `/fast` while a profile is active applies and saves that profile's fast-mode preference. Assigning other slots does not change the live model. Clearing the active slot detaches it for this run without changing the live model. See [Model profiles](#model-profiles).
473473
474474 ### Model profiles
475475
476476 Press `Ctrl+1` ... `Ctrl+9` from the editor, or run `/profile 1` ... `/profile 9`:
477477
478-- An empty slot opens the `/model` picker. Choose a model and use `left`/`right` to adjust reasoning; `enter` saves and activates the slot. `esc` cancels without changing the current selection.
479-- An assigned slot restores its saved selection, carrying the transcript and accumulated cost across provider changes. Reasoning is clamped to the selected model's supported levels.
480-- While a slot is active, `/model`, `/reasoning`, and `Ctrl+R` changes automatically save back to it. Choose another slot first if you want to keep the old favorite.
478+- An empty slot opens the `/model` picker. Choose a model and use `left`/`right` to adjust reasoning; `enter` saves and activates the slot. It starts with the current fast-mode preference. `esc` cancels without changing the current selection.
479+- An assigned slot restores its saved selection, reasoning, and fast-mode preference, carrying the transcript and accumulated cost across provider changes. Reasoning is clamped to the selected model's supported levels.
480+- While a slot is active, `/model`, `/reasoning`, `Ctrl+R`, and `/fast` changes automatically save back to it. Choose another slot first if you want to keep the old favorite.
481481 - The status bar shows the active slot as `[1]` ... `[9]`. Status messages confirm creation, activation, and updates, or report switching/saving errors. Switching is rejected while a turn is running; dialogs retain their normal key handling.
482482
483483 If a model switch succeeds but saving its profile fails, zut reports **model switched; profile not saved** and detaches the active indicator. The live model and any model/session metadata already saved are retained; the favorite and startup selection are not guaranteed to reflect that change. Fix the save error and select the profile again. Switching and saving are not a transaction across config and session files.
@@ -488,13 +488,13 @@ Profiles and the active slot persist in `$ZUT_HOME/config.json`, using the exist
488488 {
489489 "active_model_profile": 1,
490490 "quick_model_shortcuts": [
491- {"provider": "openai-codex", "model": "gpt-6-astra", "reasoning": "medium"},
492- {"provider": "openai", "model": "gpt-5.6-sol", "reasoning": "max"}
491+ {"provider": "openai-codex", "model": "gpt-6-astra", "reasoning": "medium", "fast_mode": true},
492+ {"provider": "openai", "model": "gpt-5.6-sol", "reasoning": "max", "fast_mode": false}
493493 ]
494494 }
495495 ```
496496
497-Use `{}` for an empty position. If `active_model_profile` is missing, `0`, or outside 1–9, **slot 1 is the default**. Once another slot is activated, that slot is remembered across restarts. On a fresh config, model/reasoning edits save automatically into the default slot; pressing its shortcut before assigning a model still opens the picker. Old model-only slots still load; missing or empty `reasoning` means off. The active profile supplies startup defaults, with provider aliases normalized and reasoning clamped to the model's supported levels. If its provider or model is no longer available, startup repairs that profile to the resolved fallback. Explicit CLI options and session restoration keep their usual precedence; a different model/reasoning selection detaches the profile for that run rather than overwriting a favorite. These favorites are separate from named Zutfile or subagent profiles.
497+Use `{}` for an empty position. If `active_model_profile` is missing, `0`, or outside 1–9, **slot 1 is the default**. Once another slot is activated, that slot is remembered across restarts. On a fresh config, model/reasoning/fast-mode edits save automatically into the default slot; pressing its shortcut before assigning a model still opens the picker. Old model-only slots still load; missing or empty `reasoning` means off and missing `fast_mode` means off. The active profile supplies startup defaults, with provider aliases normalized and reasoning clamped to the model's supported levels. If its provider or model is no longer available, startup repairs that profile to the resolved fallback while preserving its fast-mode preference. Explicit CLI options and session restoration keep their usual precedence; a different model/reasoning/fast-mode selection detaches the profile for that run rather than overwriting a favorite. Fast mode remains subject to provider support. These favorites are separate from named Zutfile or subagent profiles.
498498
499499 **AZERTY and terminals:** Ctrl plus the French AZERTY top row (`&`, `é`, `"`, `'`, `(`, `-`, `è`, `_`, `ç`) selects slots 1–9 without Shift. Modified digits also work. Zut requests enhanced keyboard reporting (Kitty/CSI-u or xterm modifyOtherKeys), but your terminal or multiplexer must forward the chord. If it reserves the shortcut for tabs or emits an indistinguishable legacy control byte, configure it to forward the enhanced key sequence, or use `/profile N`. Zut does not reinterpret bare punctuation or ambiguous control bytes as profile shortcuts. On macOS, Command plus these keys also works if the terminal forwards Super.
500500
@@ -970,7 +970,7 @@ Slash commands also work while the agent is busy. Non-destructive ones (`/help`,
970970 | `ctrl+v` | Paste clipboard text into the focused chat, side chat, dialog, filter, or credential input. In the main chat, image clipboard content is attached to the next prompt when the platform exposes it (macOS pasteboard, Wayland `wl-paste`, or X11 `xclip`). On Linux, text uses `wl-paste`, `xclip`, or `xsel`; terminal-native bracketed paste remains available without those commands. |
971971 | `ctrl+o` | Expand or collapse long tool results (read, write, edit, bash, create_worktree, grep, lsp, and web_search outputs over ~12 lines). |
972972 | `ctrl+r` | Cycle through the reasoning levels supported by the active model. The selected level applies to subsequent model calls and persists to `config.json`. |
973-| `ctrl+1` ... `ctrl+9` | Activate a persistent model + reasoning profile; empty slots open the model picker. French AZERTY Ctrl + top-row symbols also work when forwarded by the terminal. Rejected while a turn is running. See [Model profiles](#model-profiles); `/profile N` is the terminal-independent fallback. |
973+| `ctrl+1` ... `ctrl+9` | Activate a persistent model + reasoning + fast-mode profile; empty slots open the model picker. French AZERTY Ctrl + top-row symbols also work when forwarded by the terminal. Rejected while a turn is running. See [Model profiles](#model-profiles); `/profile N` is the terminal-independent fallback. |
974974 | `@` | Open the file picker. Browse files and directories in the working directory. |
975975
976976 ### File picker (`@`)
@@ -326,9 +326,9 @@ Slash command names are case-insensitive in the TUI and messaging backends; argu
326 | `/login` | Log in via API key or subscription (opens a dialog). |326 | `/login` | Log in via API key or subscription (opens a dialog). |
327 | `/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. |327 | `/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. |
328 | `/model` | Pick a model from a list (or `/model <id>` to set directly). |328 | `/model` | Pick a model from a list (or `/model <id>` to set directly). |
329-| `/profile <1-9>` | Activate a saved model + reasoning profile, like `Ctrl+1` through `Ctrl+9`; empty slots open the model picker. See [Model profiles](#model-profiles). |329+| `/profile <1-9>` | Activate a saved model + reasoning + fast-mode profile, like `Ctrl+1` through `Ctrl+9`; empty slots open the model picker. See [Model profiles](#model-profiles). |
330 | `/reasoning` | Set the reasoning level for subsequent model calls. |330 | `/reasoning` | Set the reasoning level for subsequent model calls. |
331-| `/fast` | Toggle fast mode for subsequent model calls. |331+| `/fast` | Toggle fast mode for subsequent model calls; when a model profile is active, save the preference to that profile. |
332 | `/orchestrator` | Toggle proactive subagent delegation. When disabled, subagent tools remain available for explicit delegation. |332 | `/orchestrator` | Toggle proactive subagent delegation. When disabled, subagent tools remain available for explicit delegation. |
333 | `/goal <objective>` | Start an autonomous interactive-session mission. Use `/goal` for status, `/goal history` for prior goals, and `/goal pause`, `resume`, or `clear` for control. |333 | `/goal <objective>` | Start an autonomous interactive-session mission. Use `/goal` for status, `/goal history` for prior goals, and `/goal pause`, `resume`, or `clear` for control. |
334 | `/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. |334 | `/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. |
@@ -459,7 +459,7 @@ Opens a dialog with every persistent setting. `up`/`down` to navigate, `enter` o
459 - **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`.459 - **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`.
460 - **proactive delegation** — let the interactive primary delegate concrete, bounded sidecars while it keeps ownership of the critical path. Before spawning, it identifies useful local work and gives the child a non-overlapping question, responsibility, package, or file set. Immediate blockers and tightly coupled work stay local; after spawning, the worker owns its scope until completion and the primary continues only its preselected independent task. If a user request or active workflow explicitly delegates blocking work, the primary yields rather than reproducing it. Off by default. When off, the permitted canonical subagent tools and profile manifest remain available for user-requested delegation or when an active skill workflow requires it. The tools accept named profiles, reasoning, per-spawn fast-mode overrides, and shared/worktree isolation where applicable; lifecycle and result references are persisted. Completion and required-work state are host-driven through `[auto-subagents update]` and `[required-subagents update]` messages; required work stays asynchronous, persists its outcome, and prevents the terminal parent response until a successful completion. If launch-time restrictions prevent spawning, the interactive primary continues locally. Explicit headless `--orchestrate` remains a separate strict manager-only mode and reports unavailable delegation rather than implementing directly. Do not poll or inspect dashboards, metadata, event logs, or files solely to wait. See `/subagents` and [docs/subagents.md](docs/subagents.md) for details.460 - **proactive delegation** — let the interactive primary delegate concrete, bounded sidecars while it keeps ownership of the critical path. Before spawning, it identifies useful local work and gives the child a non-overlapping question, responsibility, package, or file set. Immediate blockers and tightly coupled work stay local; after spawning, the worker owns its scope until completion and the primary continues only its preselected independent task. If a user request or active workflow explicitly delegates blocking work, the primary yields rather than reproducing it. Off by default. When off, the permitted canonical subagent tools and profile manifest remain available for user-requested delegation or when an active skill workflow requires it. The tools accept named profiles, reasoning, per-spawn fast-mode overrides, and shared/worktree isolation where applicable; lifecycle and result references are persisted. Completion and required-work state are host-driven through `[auto-subagents update]` and `[required-subagents update]` messages; required work stays asynchronous, persists its outcome, and prevents the terminal parent response until a successful completion. If launch-time restrictions prevent spawning, the interactive primary continues locally. Explicit headless `--orchestrate` remains a separate strict manager-only mode and reports unavailable delegation rather than implementing directly. Do not poll or inspect dashboards, metadata, event logs, or files solely to wait. See `/subagents` and [docs/subagents.md](docs/subagents.md) for details.
461 - **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.461 - **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.
462-- **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.462+- **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`. When a model profile is active, the value is saved in that profile and restored when switching profiles. Other providers currently return an unsupported-provider error. Fast mode may cost more and depends on the selected model/account.
463 - **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`.463 - **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`.
464 - **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.464 - **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.
465 - **auto-compact threshold** — choose `off`, `70%`, `80%`, `85%` (default), `90%`, or `95%` of the model's advertised context window. The selected percentage controls automatic compaction before and after interactive turns and before sub-agent follow-up turns, and persists as `auto_compact_threshold`. Context accounting includes uncached input plus provider-reported cache reads and writes, so cached turns still trigger compaction. Structural unfinished tails and truncated output continue as before. After a successful after-turn threshold compaction, zut may make at most two hidden, bounded recovery continuations when the assistant's visible text appears to promise future work; clear completed text settles normally, and a newer queued user request wins. An interrupted interactive session retains an outstanding handoff when resumed, including after portable export and import. Manual `/compact` does not silently start work. `off` disables percentage-based triggers but keeps manual `/compact` and automatic recovery from context-window and payload-too-large responses. Truncated-output continuation remains a separate priority rule.465 - **auto-compact threshold** — choose `off`, `70%`, `80%`, `85%` (default), `90%`, or `95%` of the model's advertised context window. The selected percentage controls automatic compaction before and after interactive turns and before sub-agent follow-up turns, and persists as `auto_compact_threshold`. Context accounting includes uncached input plus provider-reported cache reads and writes, so cached turns still trigger compaction. Structural unfinished tails and truncated output continue as before. After a successful after-turn threshold compaction, zut may make at most two hidden, bounded recovery continuations when the assistant's visible text appears to promise future work; clear completed text settles normally, and a newer queued user request wins. An interrupted interactive session retains an outstanding handoff when resumed, including after portable export and import. Manual `/compact` does not silently start work. `off` disables percentage-based triggers but keeps manual `/compact` and automatic recovery from context-window and payload-too-large responses. Truncated-output continuation remains a separate priority rule.
@@ -469,15 +469,15 @@ Opens a dialog with every persistent setting. `up`/`down` to navigate, `enter` o
469 - **TUI settings**: opens a sub-view for input layout in display order: status, working spinner, input, then subagent operations. **Input style** can be `plain` (default prompt line), `lines` (separator lines above and below the input), or `block` (a user-bubble-style input block). **Status position** places model, usage, and working-directory information above or below the input. **Working spinner position** places the busy spinner above or below the input. Changes apply immediately and persist to `config.json` as `tui_input_style`, `tui_status_position`, and `tui_working_position`.469 - **TUI settings**: opens a sub-view for input layout in display order: status, working spinner, input, then subagent operations. **Input style** can be `plain` (default prompt line), `lines` (separator lines above and below the input), or `block` (a user-bubble-style input block). **Status position** places model, usage, and working-directory information above or below the input. **Working spinner position** places the busy spinner above or below the input. Changes apply immediately and persist to `config.json` as `tui_input_style`, `tui_status_position`, and `tui_working_position`.
470 - **reasoning level**: choose reasoning for supported models: off, minimum, low, medium, high, xhigh, or max. The `max` tier is opt-in and sent natively to GPT-5.6 and adaptive-thinking Claude models; unsupported backends clamp it to their highest accepted effort. The change is persisted to `config.json` and applied to the next model call. Use `/reasoning` to open this selector directly, or press `Ctrl+R` to cycle through the active model's supported levels. The selector only shows distinct levels supported by the active model; models without reasoning support only offer `off`.470 - **reasoning level**: choose reasoning for supported models: off, minimum, low, medium, high, xhigh, or max. The `max` tier is opt-in and sent natively to GPT-5.6 and adaptive-thinking Claude models; unsupported backends clamp it to their highest accepted effort. The change is persisted to `config.json` and applied to the next model call. Use `/reasoning` to open this selector directly, or press `Ctrl+R` to cycle through the active model's supported levels. The selector only shows distinct levels supported by the active model; models without reasoning support only offer `off`.
471 - **color theme** — `auto` follows the terminal foreground, background, ANSI palette, color depth, and supported live appearance changes. Choose fixed dark/light palettes or JSON overlays discovered under `$ZUT_HOME/themes` and loaded extensions. Active custom files reload safely; invalid edits retain the last valid appearance and persistent deletion resets to auto. See [docs/themes.md](docs/themes.md).471 - **color theme** — `auto` follows the terminal foreground, background, ANSI palette, color depth, and supported live appearance changes. Choose fixed dark/light palettes or JSON overlays discovered under `$ZUT_HOME/themes` and loaded extensions. Active custom files reload safely; invalid edits retain the last valid appearance and persistent deletion resets to auto. See [docs/themes.md](docs/themes.md).
472-- **model profiles** — nine favorites containing a provider, model, and reasoning level. `enter` on a slot opens the `/model` picker; `left`/`right` chooses that favorite's reasoning without changing the live model. `enter` saves the assignment; `backspace` clears it. Editing the active slot immediately applies and saves its new model and reasoning. Assigning other slots does not change the live model. Clearing the active slot detaches it for this run without changing the live model. See [Model profiles](#model-profiles).472+- **model profiles** — nine favorites containing a provider, model, reasoning level, and fast-mode preference. `enter` on a slot opens the `/model` picker; `left`/`right` chooses that favorite's reasoning without changing the live model. `enter` saves the assignment; `backspace` clears it. Editing the active slot immediately applies and saves its new model and reasoning. `/fast` while a profile is active applies and saves that profile's fast-mode preference. Assigning other slots does not change the live model. Clearing the active slot detaches it for this run without changing the live model. See [Model profiles](#model-profiles).
473 473
474 ### Model profiles474 ### Model profiles
475 475
476 Press `Ctrl+1` ... `Ctrl+9` from the editor, or run `/profile 1` ... `/profile 9`:476 Press `Ctrl+1` ... `Ctrl+9` from the editor, or run `/profile 1` ... `/profile 9`:
477 477
478-- An empty slot opens the `/model` picker. Choose a model and use `left`/`right` to adjust reasoning; `enter` saves and activates the slot. `esc` cancels without changing the current selection.478+- An empty slot opens the `/model` picker. Choose a model and use `left`/`right` to adjust reasoning; `enter` saves and activates the slot. It starts with the current fast-mode preference. `esc` cancels without changing the current selection.
479-- An assigned slot restores its saved selection, carrying the transcript and accumulated cost across provider changes. Reasoning is clamped to the selected model's supported levels.479+- An assigned slot restores its saved selection, reasoning, and fast-mode preference, carrying the transcript and accumulated cost across provider changes. Reasoning is clamped to the selected model's supported levels.
480-- While a slot is active, `/model`, `/reasoning`, and `Ctrl+R` changes automatically save back to it. Choose another slot first if you want to keep the old favorite.480+- While a slot is active, `/model`, `/reasoning`, `Ctrl+R`, and `/fast` changes automatically save back to it. Choose another slot first if you want to keep the old favorite.
481 - The status bar shows the active slot as `[1]` ... `[9]`. Status messages confirm creation, activation, and updates, or report switching/saving errors. Switching is rejected while a turn is running; dialogs retain their normal key handling.481 - The status bar shows the active slot as `[1]` ... `[9]`. Status messages confirm creation, activation, and updates, or report switching/saving errors. Switching is rejected while a turn is running; dialogs retain their normal key handling.
482 482
483 If a model switch succeeds but saving its profile fails, zut reports **model switched; profile not saved** and detaches the active indicator. The live model and any model/session metadata already saved are retained; the favorite and startup selection are not guaranteed to reflect that change. Fix the save error and select the profile again. Switching and saving are not a transaction across config and session files.483 If a model switch succeeds but saving its profile fails, zut reports **model switched; profile not saved** and detaches the active indicator. The live model and any model/session metadata already saved are retained; the favorite and startup selection are not guaranteed to reflect that change. Fix the save error and select the profile again. Switching and saving are not a transaction across config and session files.
@@ -488,13 +488,13 @@ Profiles and the active slot persist in `$ZUT_HOME/config.json`, using the exist
488 {488 {
489 "active_model_profile": 1,489 "active_model_profile": 1,
490 "quick_model_shortcuts": [490 "quick_model_shortcuts": [
491- {"provider": "openai-codex", "model": "gpt-6-astra", "reasoning": "medium"},491+ {"provider": "openai-codex", "model": "gpt-6-astra", "reasoning": "medium", "fast_mode": true},
492- {"provider": "openai", "model": "gpt-5.6-sol", "reasoning": "max"}492+ {"provider": "openai", "model": "gpt-5.6-sol", "reasoning": "max", "fast_mode": false}
493 ]493 ]
494 }494 }
495 ```495 ```
496 496
497-Use `{}` for an empty position. If `active_model_profile` is missing, `0`, or outside 1–9, **slot 1 is the default**. Once another slot is activated, that slot is remembered across restarts. On a fresh config, model/reasoning edits save automatically into the default slot; pressing its shortcut before assigning a model still opens the picker. Old model-only slots still load; missing or empty `reasoning` means off. The active profile supplies startup defaults, with provider aliases normalized and reasoning clamped to the model's supported levels. If its provider or model is no longer available, startup repairs that profile to the resolved fallback. Explicit CLI options and session restoration keep their usual precedence; a different model/reasoning selection detaches the profile for that run rather than overwriting a favorite. These favorites are separate from named Zutfile or subagent profiles.497+Use `{}` for an empty position. If `active_model_profile` is missing, `0`, or outside 1–9, **slot 1 is the default**. Once another slot is activated, that slot is remembered across restarts. On a fresh config, model/reasoning/fast-mode edits save automatically into the default slot; pressing its shortcut before assigning a model still opens the picker. Old model-only slots still load; missing or empty `reasoning` means off and missing `fast_mode` means off. The active profile supplies startup defaults, with provider aliases normalized and reasoning clamped to the model's supported levels. If its provider or model is no longer available, startup repairs that profile to the resolved fallback while preserving its fast-mode preference. Explicit CLI options and session restoration keep their usual precedence; a different model/reasoning/fast-mode selection detaches the profile for that run rather than overwriting a favorite. Fast mode remains subject to provider support. These favorites are separate from named Zutfile or subagent profiles.
498 498
499 **AZERTY and terminals:** Ctrl plus the French AZERTY top row (`&`, `é`, `"`, `'`, `(`, `-`, `è`, `_`, `ç`) selects slots 1–9 without Shift. Modified digits also work. Zut requests enhanced keyboard reporting (Kitty/CSI-u or xterm modifyOtherKeys), but your terminal or multiplexer must forward the chord. If it reserves the shortcut for tabs or emits an indistinguishable legacy control byte, configure it to forward the enhanced key sequence, or use `/profile N`. Zut does not reinterpret bare punctuation or ambiguous control bytes as profile shortcuts. On macOS, Command plus these keys also works if the terminal forwards Super.499 **AZERTY and terminals:** Ctrl plus the French AZERTY top row (`&`, `é`, `"`, `'`, `(`, `-`, `è`, `_`, `ç`) selects slots 1–9 without Shift. Modified digits also work. Zut requests enhanced keyboard reporting (Kitty/CSI-u or xterm modifyOtherKeys), but your terminal or multiplexer must forward the chord. If it reserves the shortcut for tabs or emits an indistinguishable legacy control byte, configure it to forward the enhanced key sequence, or use `/profile N`. Zut does not reinterpret bare punctuation or ambiguous control bytes as profile shortcuts. On macOS, Command plus these keys also works if the terminal forwards Super.
500 500
@@ -970,7 +970,7 @@ Slash commands also work while the agent is busy. Non-destructive ones (`/help`,
970 | `ctrl+v` | Paste clipboard text into the focused chat, side chat, dialog, filter, or credential input. In the main chat, image clipboard content is attached to the next prompt when the platform exposes it (macOS pasteboard, Wayland `wl-paste`, or X11 `xclip`). On Linux, text uses `wl-paste`, `xclip`, or `xsel`; terminal-native bracketed paste remains available without those commands. |970 | `ctrl+v` | Paste clipboard text into the focused chat, side chat, dialog, filter, or credential input. In the main chat, image clipboard content is attached to the next prompt when the platform exposes it (macOS pasteboard, Wayland `wl-paste`, or X11 `xclip`). On Linux, text uses `wl-paste`, `xclip`, or `xsel`; terminal-native bracketed paste remains available without those commands. |
971 | `ctrl+o` | Expand or collapse long tool results (read, write, edit, bash, create_worktree, grep, lsp, and web_search outputs over ~12 lines). |971 | `ctrl+o` | Expand or collapse long tool results (read, write, edit, bash, create_worktree, grep, lsp, and web_search outputs over ~12 lines). |
972 | `ctrl+r` | Cycle through the reasoning levels supported by the active model. The selected level applies to subsequent model calls and persists to `config.json`. |972 | `ctrl+r` | Cycle through the reasoning levels supported by the active model. The selected level applies to subsequent model calls and persists to `config.json`. |
973-| `ctrl+1` ... `ctrl+9` | Activate a persistent model + reasoning profile; empty slots open the model picker. French AZERTY Ctrl + top-row symbols also work when forwarded by the terminal. Rejected while a turn is running. See [Model profiles](#model-profiles); `/profile N` is the terminal-independent fallback. |973+| `ctrl+1` ... `ctrl+9` | Activate a persistent model + reasoning + fast-mode profile; empty slots open the model picker. French AZERTY Ctrl + top-row symbols also work when forwarded by the terminal. Rejected while a turn is running. See [Model profiles](#model-profiles); `/profile N` is the terminal-independent fallback. |
974 | `@` | Open the file picker. Browse files and directories in the working directory. |974 | `@` | Open the file picker. Browse files and directories in the working directory. |
975 975
976 ### File picker (`@`)976 ### File picker (`@`)
M packages/agent/build.go +1 -0
@@ -719,6 +719,7 @@ func Resolve(args Args, requireCred bool) (Resolved, error) {
719719 cfg.Reasoning = provider.ClampReasoningForModel(resolvedModel, cfg.Reasoning)
720720 cfg.QuickModelShortcuts[profileSlot-1] = QuickModelShortcut{
721721 Provider: cfg.Provider, Model: cfg.Model, Reasoning: cfg.Reasoning,
722+ FastMode: cfg.FastMode != nil && *cfg.FastMode,
722723 }
723724 cfg.ActiveModelProfile = profileSlot
724725 }
@@ -719,6 +719,7 @@ func Resolve(args Args, requireCred bool) (Resolved, error) {
719 cfg.Reasoning = provider.ClampReasoningForModel(resolvedModel, cfg.Reasoning)719 cfg.Reasoning = provider.ClampReasoningForModel(resolvedModel, cfg.Reasoning)
720 cfg.QuickModelShortcuts[profileSlot-1] = QuickModelShortcut{720 cfg.QuickModelShortcuts[profileSlot-1] = QuickModelShortcut{
721 Provider: cfg.Provider, Model: cfg.Model, Reasoning: cfg.Reasoning,721 Provider: cfg.Provider, Model: cfg.Model, Reasoning: cfg.Reasoning,
722+ FastMode: cfg.FastMode != nil && *cfg.FastMode,
722 }723 }
723 cfg.ActiveModelProfile = profileSlot724 cfg.ActiveModelProfile = profileSlot
724 }725 }
M packages/agent/cli.go +2 -1
@@ -1493,7 +1493,7 @@ func interactiveQuickModelShortcuts(shortcuts []QuickModelShortcut) []modes.Quic
14931493 result := make([]modes.QuickModelShortcut, len(shortcuts))
14941494 for idx, s := range shortcuts {
14951495 p := normalizedModelProfile(s)
1496- result[idx] = modes.QuickModelShortcut{Provider: p.Provider, Model: p.Model, Reasoning: p.Reasoning}
1496+ result[idx] = modes.QuickModelShortcut{Provider: p.Provider, Model: p.Model, Reasoning: p.Reasoning, FastMode: p.FastMode}
14971497 }
14981498 return result
14991499 }
@@ -2629,6 +2629,7 @@ func runInteractive(ctx context.Context, args Args, version string) (runErr erro
26292629 BaseURL: r.BaseURL,
26302630 Reasoning: r.Reasoning,
26312631 OnReasoningChanged: runtime.SetReasoning,
2632+ OnFastModeChanged: runtime.SetFastMode,
26322633 SystemPrompt: r.SystemPrompt,
26332634 WritingGuidance: WritingGuidance(),
26342635 Tools: r.ToolRegistry,
@@ -1493,7 +1493,7 @@ func interactiveQuickModelShortcuts(shortcuts []QuickModelShortcut) []modes.Quic
1493 result := make([]modes.QuickModelShortcut, len(shortcuts))1493 result := make([]modes.QuickModelShortcut, len(shortcuts))
1494 for idx, s := range shortcuts {1494 for idx, s := range shortcuts {
1495 p := normalizedModelProfile(s)1495 p := normalizedModelProfile(s)
1496- result[idx] = modes.QuickModelShortcut{Provider: p.Provider, Model: p.Model, Reasoning: p.Reasoning}1496+ result[idx] = modes.QuickModelShortcut{Provider: p.Provider, Model: p.Model, Reasoning: p.Reasoning, FastMode: p.FastMode}
1497 }1497 }
1498 return result1498 return result
1499 }1499 }
@@ -2629,6 +2629,7 @@ func runInteractive(ctx context.Context, args Args, version string) (runErr erro
2629 BaseURL: r.BaseURL,2629 BaseURL: r.BaseURL,
2630 Reasoning: r.Reasoning,2630 Reasoning: r.Reasoning,
2631 OnReasoningChanged: runtime.SetReasoning,2631 OnReasoningChanged: runtime.SetReasoning,
2632+ OnFastModeChanged: runtime.SetFastMode,
2632 SystemPrompt: r.SystemPrompt,2633 SystemPrompt: r.SystemPrompt,
2633 WritingGuidance: WritingGuidance(),2634 WritingGuidance: WritingGuidance(),
2634 Tools: r.ToolRegistry,2635 Tools: r.ToolRegistry,
M packages/agent/config.go +7 -3
@@ -16,11 +16,12 @@ import (
1616 "github.com/bnema/zut/packages/provider/auth"
1717 )
1818
19-// QuickModelShortcut is one configured keyboard shortcut slot.
19+// QuickModelShortcut is one configured model profile slot.
2020 type QuickModelShortcut struct {
2121 Provider string `json:"provider"`
2222 Model string `json:"model"`
2323 Reasoning string `json:"reasoning,omitempty"`
24+ FastMode bool `json:"fast_mode,omitempty"`
2425 }
2526
2627 // SubagentsConfig contains the resident manager policy. max_concurrent limits
@@ -50,7 +51,8 @@ type Config struct {
5051 Theme string `json:"theme"`
5152
5253 // FastMode requests OpenAI's fast service tier. Off by default;
53- // nil/missing means disabled. Other providers reject fast-mode
54+ // nil/missing means disabled. An active model profile supplies its
55+ // per-profile value at startup. Other providers reject fast-mode
5456 // requests at the provider boundary.
5557 FastMode *bool `json:"fast_mode,omitempty"`
5658
@@ -67,7 +69,7 @@ type Config struct {
6769 // overrides this when set.
6870 CompactInput *bool `json:"compact_input,omitempty"`
6971
70- // QuickModelShortcuts maps slots 1-9 to provider/model pairs used by
72+ // QuickModelShortcuts maps slots 1-9 to model profiles used by
7173 // Ctrl+1..9. Cmd+1..9 may also work on terminals that forward Super.
7274 QuickModelShortcuts []QuickModelShortcut `json:"quick_model_shortcuts,omitempty"`
7375 ActiveModelProfile int `json:"active_model_profile,omitempty"`
@@ -349,6 +351,8 @@ func (c *Config) applyActiveModelProfile() int {
349351 return 0
350352 }
351353 c.Provider, c.Model, c.Reasoning = p.Provider, p.Model, p.Reasoning
354+ fastMode := p.FastMode
355+ c.FastMode = &fastMode
352356 return slot
353357 }
354358
@@ -16,11 +16,12 @@ import (
16 "github.com/bnema/zut/packages/provider/auth"16 "github.com/bnema/zut/packages/provider/auth"
17 )17 )
18 18
19-// QuickModelShortcut is one configured keyboard shortcut slot.19+// QuickModelShortcut is one configured model profile slot.
20 type QuickModelShortcut struct {20 type QuickModelShortcut struct {
21 Provider string `json:"provider"`21 Provider string `json:"provider"`
22 Model string `json:"model"`22 Model string `json:"model"`
23 Reasoning string `json:"reasoning,omitempty"`23 Reasoning string `json:"reasoning,omitempty"`
24+ FastMode bool `json:"fast_mode,omitempty"`
24 }25 }
25 26
26 // SubagentsConfig contains the resident manager policy. max_concurrent limits27 // SubagentsConfig contains the resident manager policy. max_concurrent limits
@@ -50,7 +51,8 @@ type Config struct {
50 Theme string `json:"theme"`51 Theme string `json:"theme"`
51 52
52 // FastMode requests OpenAI's fast service tier. Off by default;53 // FastMode requests OpenAI's fast service tier. Off by default;
53- // nil/missing means disabled. Other providers reject fast-mode54+ // nil/missing means disabled. An active model profile supplies its
55+ // per-profile value at startup. Other providers reject fast-mode
54 // requests at the provider boundary.56 // requests at the provider boundary.
55 FastMode *bool `json:"fast_mode,omitempty"`57 FastMode *bool `json:"fast_mode,omitempty"`
56 58
@@ -67,7 +69,7 @@ type Config struct {
67 // overrides this when set.69 // overrides this when set.
68 CompactInput *bool `json:"compact_input,omitempty"`70 CompactInput *bool `json:"compact_input,omitempty"`
69 71
70- // QuickModelShortcuts maps slots 1-9 to provider/model pairs used by72+ // QuickModelShortcuts maps slots 1-9 to model profiles used by
71 // Ctrl+1..9. Cmd+1..9 may also work on terminals that forward Super.73 // Ctrl+1..9. Cmd+1..9 may also work on terminals that forward Super.
72 QuickModelShortcuts []QuickModelShortcut `json:"quick_model_shortcuts,omitempty"`74 QuickModelShortcuts []QuickModelShortcut `json:"quick_model_shortcuts,omitempty"`
73 ActiveModelProfile int `json:"active_model_profile,omitempty"`75 ActiveModelProfile int `json:"active_model_profile,omitempty"`
@@ -349,6 +351,8 @@ func (c *Config) applyActiveModelProfile() int {
349 return 0351 return 0
350 }352 }
351 c.Provider, c.Model, c.Reasoning = p.Provider, p.Model, p.Reasoning353 c.Provider, c.Model, c.Reasoning = p.Provider, p.Model, p.Reasoning
354+ fastMode := p.FastMode
355+ c.FastMode = &fastMode
352 return slot356 return slot
353 }357 }
354 358
M packages/agent/fast_mode_test.go +26 -0
@@ -3,6 +3,7 @@ package agent
33 import (
44 "context"
55 "encoding/json"
6+ "fmt"
67 "strings"
78 "testing"
89
@@ -65,6 +66,31 @@ func TestResolveFastModeRemainsEnabledForUnsupportedProviderUntilRequest(t *test
6566 }
6667 }
6768
69+func TestResolveFastModeFromActiveModelProfile(t *testing.T) {
70+ for _, want := range []bool{true, false} {
71+ t.Run(fmt.Sprintf("%v", want), func(t *testing.T) {
72+ t.Setenv("ZUT_HOME", t.TempDir())
73+ global := !want
74+ if err := SaveConfig(Config{
75+ Provider: "openai", Model: "gpt-5", FastMode: &global,
76+ ActiveModelProfile: 1,
77+ QuickModelShortcuts: []QuickModelShortcut{{
78+ Provider: "openai", Model: "gpt-5", FastMode: want,
79+ }},
80+ }); err != nil {
81+ t.Fatal(err)
82+ }
83+ r, err := Resolve(Args{}, false)
84+ if err != nil {
85+ t.Fatalf("Resolve failed: %v", err)
86+ }
87+ if r.FastMode != want {
88+ t.Fatalf("FastMode = %v, want %v", r.FastMode, want)
89+ }
90+ })
91+ }
92+}
93+
6894 func TestResolveFastModeRequiresExplicitChildOverride(t *testing.T) {
6995 t.Setenv("ZUT_HOME", t.TempDir())
7096 if err := SaveConfig(Config{Provider: "openai", Model: "gpt-5"}); err != nil {
@@ -3,6 +3,7 @@ package agent
3 import (3 import (
4 "context"4 "context"
5 "encoding/json"5 "encoding/json"
6+ "fmt"
6 "strings"7 "strings"
7 "testing"8 "testing"
8 9
@@ -65,6 +66,31 @@ func TestResolveFastModeRemainsEnabledForUnsupportedProviderUntilRequest(t *test
65 }66 }
66 }67 }
67 68
69+func TestResolveFastModeFromActiveModelProfile(t *testing.T) {
70+ for _, want := range []bool{true, false} {
71+ t.Run(fmt.Sprintf("%v", want), func(t *testing.T) {
72+ t.Setenv("ZUT_HOME", t.TempDir())
73+ global := !want
74+ if err := SaveConfig(Config{
75+ Provider: "openai", Model: "gpt-5", FastMode: &global,
76+ ActiveModelProfile: 1,
77+ QuickModelShortcuts: []QuickModelShortcut{{
78+ Provider: "openai", Model: "gpt-5", FastMode: want,
79+ }},
80+ }); err != nil {
81+ t.Fatal(err)
82+ }
83+ r, err := Resolve(Args{}, false)
84+ if err != nil {
85+ t.Fatalf("Resolve failed: %v", err)
86+ }
87+ if r.FastMode != want {
88+ t.Fatalf("FastMode = %v, want %v", r.FastMode, want)
89+ }
90+ })
91+ }
92+}
93+
68 func TestResolveFastModeRequiresExplicitChildOverride(t *testing.T) {94 func TestResolveFastModeRequiresExplicitChildOverride(t *testing.T) {
69 t.Setenv("ZUT_HOME", t.TempDir())95 t.Setenv("ZUT_HOME", t.TempDir())
70 if err := SaveConfig(Config{Provider: "openai", Model: "gpt-5"}); err != nil {96 if err := SaveConfig(Config{Provider: "openai", Model: "gpt-5"}); err != nil {
M packages/agent/model_profiles_test.go +24 -2
@@ -55,13 +55,35 @@ func TestActiveAliasedModelProfilePreservesAutosave(t *testing.T) {
5555 }
5656 }
5757
58+func TestLegacyQuickModelShortcutPreservesProfileState(t *testing.T) {
59+ t.Setenv("ZUT_HOME", t.TempDir())
60+ if err := SaveConfig(Config{
61+ QuickModelShortcuts: []QuickModelShortcut{{
62+ Provider: "openai", Model: "gpt-5.6-sol", Reasoning: "high", FastMode: true,
63+ }},
64+ }); err != nil {
65+ t.Fatal(err)
66+ }
67+ if err := (configSettingsStore{}).SetQuickModelShortcut(1, "openai", "gpt-5.5"); err != nil {
68+ t.Fatal(err)
69+ }
70+ cfg, err := LoadConfig()
71+ if err != nil {
72+ t.Fatal(err)
73+ }
74+ profile := cfg.QuickModelShortcuts[0]
75+ if profile.Model != "gpt-5.5" || profile.Reasoning != "high" || !profile.FastMode {
76+ t.Fatalf("legacy shortcut update lost profile state: %+v", profile)
77+ }
78+}
79+
5880 func TestModelProfileConfigRoundTrip(t *testing.T) {
5981 t.Setenv("ZUT_HOME", t.TempDir())
6082 if err := SaveConfig(Config{Theme: "dark"}); err != nil {
6183 t.Fatal(err)
6284 }
6385 store := configSettingsStore{}
64- p := modes.QuickModelShortcut{Provider: "openai", Model: "gpt-5.6-sol", Reasoning: "max"}
86+ p := modes.QuickModelShortcut{Provider: "openai", Model: "gpt-5.6-sol", Reasoning: "max", FastMode: true}
6587 if err := store.SetModelProfile(9, p, 9); err != nil {
6688 t.Fatal(err)
6789 }
@@ -69,7 +91,7 @@ func TestModelProfileConfigRoundTrip(t *testing.T) {
6991 if err != nil {
7092 t.Fatal(err)
7193 }
72- if cfg.ActiveModelProfile != 9 || len(cfg.QuickModelShortcuts) != 9 || cfg.Reasoning != "max" || cfg.Model != p.Model || cfg.Theme != "dark" {
94+ if cfg.ActiveModelProfile != 9 || len(cfg.QuickModelShortcuts) != 9 || cfg.Reasoning != "max" || cfg.Model != p.Model || cfg.FastMode == nil || !*cfg.FastMode || cfg.QuickModelShortcuts[8].FastMode != p.FastMode || cfg.Theme != "dark" {
7395 t.Fatalf("config=%+v", cfg)
7496 }
7597 // The selected profile is authoritative even after hand-editing its model
@@ -55,13 +55,35 @@ func TestActiveAliasedModelProfilePreservesAutosave(t *testing.T) {
55 }55 }
56 }56 }
57 57
58+func TestLegacyQuickModelShortcutPreservesProfileState(t *testing.T) {
59+ t.Setenv("ZUT_HOME", t.TempDir())
60+ if err := SaveConfig(Config{
61+ QuickModelShortcuts: []QuickModelShortcut{{
62+ Provider: "openai", Model: "gpt-5.6-sol", Reasoning: "high", FastMode: true,
63+ }},
64+ }); err != nil {
65+ t.Fatal(err)
66+ }
67+ if err := (configSettingsStore{}).SetQuickModelShortcut(1, "openai", "gpt-5.5"); err != nil {
68+ t.Fatal(err)
69+ }
70+ cfg, err := LoadConfig()
71+ if err != nil {
72+ t.Fatal(err)
73+ }
74+ profile := cfg.QuickModelShortcuts[0]
75+ if profile.Model != "gpt-5.5" || profile.Reasoning != "high" || !profile.FastMode {
76+ t.Fatalf("legacy shortcut update lost profile state: %+v", profile)
77+ }
78+}
79+
58 func TestModelProfileConfigRoundTrip(t *testing.T) {80 func TestModelProfileConfigRoundTrip(t *testing.T) {
59 t.Setenv("ZUT_HOME", t.TempDir())81 t.Setenv("ZUT_HOME", t.TempDir())
60 if err := SaveConfig(Config{Theme: "dark"}); err != nil {82 if err := SaveConfig(Config{Theme: "dark"}); err != nil {
61 t.Fatal(err)83 t.Fatal(err)
62 }84 }
63 store := configSettingsStore{}85 store := configSettingsStore{}
64- p := modes.QuickModelShortcut{Provider: "openai", Model: "gpt-5.6-sol", Reasoning: "max"}86+ p := modes.QuickModelShortcut{Provider: "openai", Model: "gpt-5.6-sol", Reasoning: "max", FastMode: true}
65 if err := store.SetModelProfile(9, p, 9); err != nil {87 if err := store.SetModelProfile(9, p, 9); err != nil {
66 t.Fatal(err)88 t.Fatal(err)
67 }89 }
@@ -69,7 +91,7 @@ func TestModelProfileConfigRoundTrip(t *testing.T) {
69 if err != nil {91 if err != nil {
70 t.Fatal(err)92 t.Fatal(err)
71 }93 }
72- if cfg.ActiveModelProfile != 9 || len(cfg.QuickModelShortcuts) != 9 || cfg.Reasoning != "max" || cfg.Model != p.Model || cfg.Theme != "dark" {94+ if cfg.ActiveModelProfile != 9 || len(cfg.QuickModelShortcuts) != 9 || cfg.Reasoning != "max" || cfg.Model != p.Model || cfg.FastMode == nil || !*cfg.FastMode || cfg.QuickModelShortcuts[8].FastMode != p.FastMode || cfg.Theme != "dark" {
73 t.Fatalf("config=%+v", cfg)95 t.Fatalf("config=%+v", cfg)
74 }96 }
75 // The selected profile is authoritative even after hand-editing its model97 // The selected profile is authoritative even after hand-editing its model
M packages/agent/modes/help.go +1 -1
@@ -23,7 +23,7 @@ var helpKeyRows = [][2]string{
2323 {"ctrl+l", "redraw the screen"},
2424 {"ctrl+o", "expand / collapse long tool results"},
2525 {"ctrl+r", "cycle reasoning levels for the active model"},
26- {"ctrl+1…9", "activate model + reasoning profile (AZERTY top row also works); empty slots open the model picker"},
26+ {"ctrl+1…9", "activate model + reasoning + fast-mode profile (AZERTY top row also works); empty slots open the model picker"},
2727 {"pgup / pgdn", "scroll the chat one page up / down"},
2828 {"up / down", "move in input, browse prompt history at top edge, scroll chat when empty"},
2929 }
@@ -23,7 +23,7 @@ var helpKeyRows = [][2]string{
23 {"ctrl+l", "redraw the screen"},23 {"ctrl+l", "redraw the screen"},
24 {"ctrl+o", "expand / collapse long tool results"},24 {"ctrl+o", "expand / collapse long tool results"},
25 {"ctrl+r", "cycle reasoning levels for the active model"},25 {"ctrl+r", "cycle reasoning levels for the active model"},
26- {"ctrl+1…9", "activate model + reasoning profile (AZERTY top row also works); empty slots open the model picker"},26+ {"ctrl+1…9", "activate model + reasoning + fast-mode profile (AZERTY top row also works); empty slots open the model picker"},
27 {"pgup / pgdn", "scroll the chat one page up / down"},27 {"pgup / pgdn", "scroll the chat one page up / down"},
28 {"up / down", "move in input, browse prompt history at top edge, scroll chat when empty"},28 {"up / down", "move in input, browse prompt history at top edge, scroll chat when empty"},
29 }29 }
M packages/agent/modes/interactive.go +12 -7
@@ -101,9 +101,10 @@ type InteractiveConfig struct {
101101 AutoSubagentsStopToolAllowed *bool
102102 AutoSubagentsResumeToolAllowed *bool
103103
104- // FastMode mirrors the persisted OpenAI fast-mode flag at startup.
105- // nil/missing means disabled. Unsupported providers reject attempts
106- // to enable it and reject requests when it remains enabled.
104+ // FastMode mirrors the active profile's or global persisted OpenAI
105+ // fast-mode flag at startup. nil/missing means disabled. Unsupported
106+ // providers reject attempts to enable it and reject requests when it
107+ // remains enabled.
107108 FastMode *bool
108109
109110 // LSPEnabled controls the main session's built-in lsp tool. nil means
@@ -171,9 +172,9 @@ type InteractiveConfig struct {
171172 // above or below the main input.
172173 TUIWorkingPosition string
173174
174- // QuickModelShortcuts maps slots 1-9 to provider/model pairs. The
175- // shortcuts are Ctrl+1..9. Cmd+1..9 may also work when the terminal
176- // forwards Command/Super keypresses, but Ctrl is the displayed chord.
175+ // QuickModelShortcuts maps slots 1-9 to model profiles. The shortcuts
176+ // are Ctrl+1..9. Cmd+1..9 may also work when the terminal forwards
177+ // Command/Super keypresses, but Ctrl is the displayed chord.
177178 QuickModelShortcuts []QuickModelShortcut
178179 ActiveModelProfile int // 0 means no active profile; otherwise 1–9
179180
@@ -355,6 +356,9 @@ type InteractiveConfig struct {
355356 // OnReasoningChanged updates host-owned runtime defaults after the setting
356357 // has been persisted successfully.
357358 OnReasoningChanged func(level string)
359+ // OnFastModeChanged updates host-owned runtime defaults after the setting
360+ // has been persisted successfully.
361+ OnFastModeChanged func(enabled bool)
358362
359363 // InitialSessionTitle is the persisted title of the session loaded before
360364 // the TUI starts. It is shown in the terminal without another model call.
@@ -479,11 +483,12 @@ type chatCacheKey struct {
479483 viewCacheRev uint64
480484 }
481485
482-// QuickModelShortcut is one configured quick model switch slot.
486+// QuickModelShortcut is one configured model profile slot.
483487 type QuickModelShortcut struct {
484488 Provider string
485489 Model string
486490 Reasoning string
491+ FastMode bool
487492 }
488493
489494 type extensionStatus struct {
@@ -101,9 +101,10 @@ type InteractiveConfig struct {
101 AutoSubagentsStopToolAllowed *bool101 AutoSubagentsStopToolAllowed *bool
102 AutoSubagentsResumeToolAllowed *bool102 AutoSubagentsResumeToolAllowed *bool
103 103
104- // FastMode mirrors the persisted OpenAI fast-mode flag at startup.104+ // FastMode mirrors the active profile's or global persisted OpenAI
105- // nil/missing means disabled. Unsupported providers reject attempts105+ // fast-mode flag at startup. nil/missing means disabled. Unsupported
106- // to enable it and reject requests when it remains enabled.106+ // providers reject attempts to enable it and reject requests when it
107+ // remains enabled.
107 FastMode *bool108 FastMode *bool
108 109
109 // LSPEnabled controls the main session's built-in lsp tool. nil means110 // LSPEnabled controls the main session's built-in lsp tool. nil means
@@ -171,9 +172,9 @@ type InteractiveConfig struct {
171 // above or below the main input.172 // above or below the main input.
172 TUIWorkingPosition string173 TUIWorkingPosition string
173 174
174- // QuickModelShortcuts maps slots 1-9 to provider/model pairs. The175+ // QuickModelShortcuts maps slots 1-9 to model profiles. The shortcuts
175- // shortcuts are Ctrl+1..9. Cmd+1..9 may also work when the terminal176+ // are Ctrl+1..9. Cmd+1..9 may also work when the terminal forwards
176- // forwards Command/Super keypresses, but Ctrl is the displayed chord.177+ // Command/Super keypresses, but Ctrl is the displayed chord.
177 QuickModelShortcuts []QuickModelShortcut178 QuickModelShortcuts []QuickModelShortcut
178 ActiveModelProfile int // 0 means no active profile; otherwise 1–9179 ActiveModelProfile int // 0 means no active profile; otherwise 1–9
179 180
@@ -355,6 +356,9 @@ type InteractiveConfig struct {
355 // OnReasoningChanged updates host-owned runtime defaults after the setting356 // OnReasoningChanged updates host-owned runtime defaults after the setting
356 // has been persisted successfully.357 // has been persisted successfully.
357 OnReasoningChanged func(level string)358 OnReasoningChanged func(level string)
359+ // OnFastModeChanged updates host-owned runtime defaults after the setting
360+ // has been persisted successfully.
361+ OnFastModeChanged func(enabled bool)
358 362
359 // InitialSessionTitle is the persisted title of the session loaded before363 // InitialSessionTitle is the persisted title of the session loaded before
360 // the TUI starts. It is shown in the terminal without another model call.364 // the TUI starts. It is shown in the terminal without another model call.
@@ -479,11 +483,12 @@ type chatCacheKey struct {
479 viewCacheRev uint64483 viewCacheRev uint64
480 }484 }
481 485
482-// QuickModelShortcut is one configured quick model switch slot.486+// QuickModelShortcut is one configured model profile slot.
483 type QuickModelShortcut struct {487 type QuickModelShortcut struct {
484 Provider string488 Provider string
485 Model string489 Model string
486 Reasoning string490 Reasoning string
491+ FastMode bool
487 }492 }
488 493
489 type extensionStatus struct {494 type extensionStatus struct {
M packages/agent/modes/interactive_settings.go +35 -10
@@ -409,7 +409,7 @@ func (i *Interactive) openSettingsDialog() {
409409 items = append(items, settingsItem{
410410 key: "quick_models",
411411 label: "model profiles",
412- desc: "configure " + quickModelShortcutPrefix() + "+1 through " + quickModelShortcutPrefix() + "+9 model and reasoning profiles",
412+ desc: "configure " + quickModelShortcutPrefix() + "+1 through " + quickModelShortcutPrefix() + "+9 model, reasoning, and fast-mode profiles",
413413 children: quickItems,
414414 })
415415 }
@@ -487,7 +487,7 @@ func (i *Interactive) quickModelSettingItem(slot int) settingsItem {
487487 return settingsItem{
488488 key: "quick_model_" + strconv.Itoa(slot),
489489 label: "profile " + strconv.Itoa(slot),
490- desc: quickModelShortcutLabel(slot) + " recalls model + reasoning. Enter assigns, Left/Right in the picker sets reasoning, Backspace clears.",
490+ desc: quickModelShortcutLabel(slot) + " recalls model, reasoning, and fast mode. Enter assigns, Left/Right in the picker sets reasoning, Backspace clears.",
491491 picker: true,
492492 hint: hint,
493493 }
@@ -542,7 +542,11 @@ func (i *Interactive) openQuickModelPicker(slot int) {
542542 i.modelDialog.Open(current, loggedIn, reasoning)
543543 }
544544 func (i *Interactive) applyQuickModelSelection(slot int, providerName, model string) {
545- p := QuickModelShortcut{Provider: providerName, Model: model, Reasoning: i.modelDialog.reasoning}
545+ fastMode := i.cfg.FastMode != nil && *i.cfg.FastMode
546+ if slot <= len(i.cfg.QuickModelShortcuts) && i.cfg.QuickModelShortcuts[slot-1].Model != "" {
547+ fastMode = i.cfg.QuickModelShortcuts[slot-1].FastMode
548+ }
549+ p := QuickModelShortcut{Provider: providerName, Model: model, Reasoning: i.modelDialog.reasoning, FastMode: fastMode}
546550 if i.quickModelActivate {
547551 i.activateModelProfile(slot, p)
548552 } else {
@@ -594,7 +598,10 @@ func (i *Interactive) applyQuickModelSetting(key, value string) {
594598 i.setQuickModelShortcut(slot, providerName, model)
595599 }
596600 func (i *Interactive) setQuickModelShortcut(slot int, providerName, model string) {
597- i.setQuickModelProfile(slot, QuickModelShortcut{Provider: providerName, Model: model, Reasoning: i.cfg.Reasoning})
601+ i.setQuickModelProfile(slot, QuickModelShortcut{
602+ Provider: providerName, Model: model, Reasoning: i.cfg.Reasoning,
603+ FastMode: i.cfg.FastMode != nil && *i.cfg.FastMode,
604+ })
598605 }
599606 func (i *Interactive) refreshQuickModelSettingsItem(slot int) {
600607 if i.settingsDialog == nil || !i.settingsDialog.Active() || len(i.settingsDialog.items) == 0 {
@@ -916,6 +923,25 @@ func (i *Interactive) applySettingToggle(key string, value bool) {
916923 i.mu.Unlock()
917924 return
918925 }
926+ if slot := i.cfg.ActiveModelProfile; slot > 0 {
927+ _, profileStore := i.cfg.SettingsStore.(modelProfileSettingsStore)
928+ if i.cfg.SettingsStore == nil || profileStore {
929+ p := QuickModelShortcut{
930+ Provider: i.cfg.Provider, Model: i.cfg.Model, Reasoning: i.cfg.Reasoning,
931+ FastMode: value,
932+ }
933+ if !i.persistModelProfile(slot, p, slot) {
934+ i.resetSettingsToggle(key, previous)
935+ return
936+ }
937+ i.setLiveFastMode(value)
938+ i.mu.Lock()
939+ i.statusOK = "fast mode " + onOff(value) + fmt.Sprintf(" — profile %d updated", slot)
940+ i.statusErr = ""
941+ i.mu.Unlock()
942+ return
943+ }
944+ }
919945 if store, ok := i.cfg.SettingsStore.(fastModeSettingsStore); ok {
920946 if err := store.SetFastMode(value); err != nil {
921947 i.resetSettingsToggle(key, previous)
@@ -925,11 +951,7 @@ func (i *Interactive) applySettingToggle(key string, value bool) {
925951 return
926952 }
927953 }
928- val := value
929- i.cfg.FastMode = &val
930- if i.agent != nil {
931- i.agent.SetFastMode(value)
932- }
954+ i.setLiveFastMode(value)
933955 i.mu.Lock()
934956 i.statusOK = "fast mode " + onOff(value)
935957 i.statusErr = ""
@@ -1253,7 +1275,10 @@ func (i *Interactive) applyReasoningSetting(level string) {
12531275 }()
12541276 level = provider.NormalizeReasoning(level)
12551277 if slot := i.cfg.ActiveModelProfile; slot > 0 {
1256- p := QuickModelShortcut{Provider: i.cfg.Provider, Model: i.cfg.Model, Reasoning: level}
1278+ p := QuickModelShortcut{
1279+ Provider: i.cfg.Provider, Model: i.cfg.Model, Reasoning: level,
1280+ FastMode: i.cfg.FastMode != nil && *i.cfg.FastMode,
1281+ }
12571282 if !i.persistModelProfile(slot, p, slot) {
12581283 return
12591284 }
@@ -409,7 +409,7 @@ func (i *Interactive) openSettingsDialog() {
409 items = append(items, settingsItem{409 items = append(items, settingsItem{
410 key: "quick_models",410 key: "quick_models",
411 label: "model profiles",411 label: "model profiles",
412- desc: "configure " + quickModelShortcutPrefix() + "+1 through " + quickModelShortcutPrefix() + "+9 model and reasoning profiles",412+ desc: "configure " + quickModelShortcutPrefix() + "+1 through " + quickModelShortcutPrefix() + "+9 model, reasoning, and fast-mode profiles",
413 children: quickItems,413 children: quickItems,
414 })414 })
415 }415 }
@@ -487,7 +487,7 @@ func (i *Interactive) quickModelSettingItem(slot int) settingsItem {
487 return settingsItem{487 return settingsItem{
488 key: "quick_model_" + strconv.Itoa(slot),488 key: "quick_model_" + strconv.Itoa(slot),
489 label: "profile " + strconv.Itoa(slot),489 label: "profile " + strconv.Itoa(slot),
490- desc: quickModelShortcutLabel(slot) + " recalls model + reasoning. Enter assigns, Left/Right in the picker sets reasoning, Backspace clears.",490+ desc: quickModelShortcutLabel(slot) + " recalls model, reasoning, and fast mode. Enter assigns, Left/Right in the picker sets reasoning, Backspace clears.",
491 picker: true,491 picker: true,
492 hint: hint,492 hint: hint,
493 }493 }
@@ -542,7 +542,11 @@ func (i *Interactive) openQuickModelPicker(slot int) {
542 i.modelDialog.Open(current, loggedIn, reasoning)542 i.modelDialog.Open(current, loggedIn, reasoning)
543 }543 }
544 func (i *Interactive) applyQuickModelSelection(slot int, providerName, model string) {544 func (i *Interactive) applyQuickModelSelection(slot int, providerName, model string) {
545- p := QuickModelShortcut{Provider: providerName, Model: model, Reasoning: i.modelDialog.reasoning}545+ fastMode := i.cfg.FastMode != nil && *i.cfg.FastMode
546+ if slot <= len(i.cfg.QuickModelShortcuts) && i.cfg.QuickModelShortcuts[slot-1].Model != "" {
547+ fastMode = i.cfg.QuickModelShortcuts[slot-1].FastMode
548+ }
549+ p := QuickModelShortcut{Provider: providerName, Model: model, Reasoning: i.modelDialog.reasoning, FastMode: fastMode}
546 if i.quickModelActivate {550 if i.quickModelActivate {
547 i.activateModelProfile(slot, p)551 i.activateModelProfile(slot, p)
548 } else {552 } else {
@@ -594,7 +598,10 @@ func (i *Interactive) applyQuickModelSetting(key, value string) {
594 i.setQuickModelShortcut(slot, providerName, model)598 i.setQuickModelShortcut(slot, providerName, model)
595 }599 }
596 func (i *Interactive) setQuickModelShortcut(slot int, providerName, model string) {600 func (i *Interactive) setQuickModelShortcut(slot int, providerName, model string) {
597- i.setQuickModelProfile(slot, QuickModelShortcut{Provider: providerName, Model: model, Reasoning: i.cfg.Reasoning})601+ i.setQuickModelProfile(slot, QuickModelShortcut{
602+ Provider: providerName, Model: model, Reasoning: i.cfg.Reasoning,
603+ FastMode: i.cfg.FastMode != nil && *i.cfg.FastMode,
604+ })
598 }605 }
599 func (i *Interactive) refreshQuickModelSettingsItem(slot int) {606 func (i *Interactive) refreshQuickModelSettingsItem(slot int) {
600 if i.settingsDialog == nil || !i.settingsDialog.Active() || len(i.settingsDialog.items) == 0 {607 if i.settingsDialog == nil || !i.settingsDialog.Active() || len(i.settingsDialog.items) == 0 {
@@ -916,6 +923,25 @@ func (i *Interactive) applySettingToggle(key string, value bool) {
916 i.mu.Unlock()923 i.mu.Unlock()
917 return924 return
918 }925 }
926+ if slot := i.cfg.ActiveModelProfile; slot > 0 {
927+ _, profileStore := i.cfg.SettingsStore.(modelProfileSettingsStore)
928+ if i.cfg.SettingsStore == nil || profileStore {
929+ p := QuickModelShortcut{
930+ Provider: i.cfg.Provider, Model: i.cfg.Model, Reasoning: i.cfg.Reasoning,
931+ FastMode: value,
932+ }
933+ if !i.persistModelProfile(slot, p, slot) {
934+ i.resetSettingsToggle(key, previous)
935+ return
936+ }
937+ i.setLiveFastMode(value)
938+ i.mu.Lock()
939+ i.statusOK = "fast mode " + onOff(value) + fmt.Sprintf(" — profile %d updated", slot)
940+ i.statusErr = ""
941+ i.mu.Unlock()
942+ return
943+ }
944+ }
919 if store, ok := i.cfg.SettingsStore.(fastModeSettingsStore); ok {945 if store, ok := i.cfg.SettingsStore.(fastModeSettingsStore); ok {
920 if err := store.SetFastMode(value); err != nil {946 if err := store.SetFastMode(value); err != nil {
921 i.resetSettingsToggle(key, previous)947 i.resetSettingsToggle(key, previous)
@@ -925,11 +951,7 @@ func (i *Interactive) applySettingToggle(key string, value bool) {
925 return951 return
926 }952 }
927 }953 }
928- val := value954+ i.setLiveFastMode(value)
929- i.cfg.FastMode = &val
930- if i.agent != nil {
931- i.agent.SetFastMode(value)
932- }
933 i.mu.Lock()955 i.mu.Lock()
934 i.statusOK = "fast mode " + onOff(value)956 i.statusOK = "fast mode " + onOff(value)
935 i.statusErr = ""957 i.statusErr = ""
@@ -1253,7 +1275,10 @@ func (i *Interactive) applyReasoningSetting(level string) {
1253 }()1275 }()
1254 level = provider.NormalizeReasoning(level)1276 level = provider.NormalizeReasoning(level)
1255 if slot := i.cfg.ActiveModelProfile; slot > 0 {1277 if slot := i.cfg.ActiveModelProfile; slot > 0 {
1256- p := QuickModelShortcut{Provider: i.cfg.Provider, Model: i.cfg.Model, Reasoning: level}1278+ p := QuickModelShortcut{
1279+ Provider: i.cfg.Provider, Model: i.cfg.Model, Reasoning: level,
1280+ FastMode: i.cfg.FastMode != nil && *i.cfg.FastMode,
1281+ }
1257 if !i.persistModelProfile(slot, p, slot) {1282 if !i.persistModelProfile(slot, p, slot) {
1258 return1283 return
1259 }1284 }
M packages/agent/modes/model_profiles.go +31 -3
@@ -15,7 +15,8 @@ func (c *InteractiveConfig) detachMismatchedModelProfile() {
1515 return
1616 }
1717 p := c.QuickModelShortcuts[slot-1]
18- if p.Provider != c.Provider || p.Model != c.Model || provider.NormalizeReasoning(p.Reasoning) != provider.NormalizeReasoning(c.Reasoning) {
18+ liveFastMode := c.FastMode != nil && *c.FastMode
19+ if p.Provider != c.Provider || p.Model != c.Model || provider.NormalizeReasoning(p.Reasoning) != provider.NormalizeReasoning(c.Reasoning) || p.FastMode != liveFastMode {
1920 c.ActiveModelProfile = 0
2021 }
2122 }
@@ -25,7 +26,11 @@ func modelProfileDescription(p QuickModelShortcut) string {
2526 if level == "" {
2627 level = "off"
2728 }
28- return p.Provider + " / " + p.Model + " (reasoning: " + level + ")"
29+ fast := "off"
30+ if p.FastMode {
31+ fast = "on"
32+ }
33+ return p.Provider + " / " + p.Model + " (reasoning: " + level + ", fast: " + fast + ")"
2934 }
3035
3136 // persistModelProfile changes in-memory favorites only after saving succeeds.
@@ -108,7 +113,10 @@ func (i *Interactive) saveActiveModelProfile() {
108113 if slot < 1 || slot > 9 {
109114 return
110115 }
111- p := QuickModelShortcut{Provider: i.cfg.Provider, Model: i.cfg.Model, Reasoning: i.cfg.Reasoning}
116+ p := QuickModelShortcut{
117+ Provider: i.cfg.Provider, Model: i.cfg.Model, Reasoning: i.cfg.Reasoning,
118+ FastMode: i.cfg.FastMode != nil && *i.cfg.FastMode,
119+ }
112120 if m, err := provider.FindModel(p.Provider, p.Model); err == nil {
113121 p.Reasoning = provider.ClampReasoningForModel(m, p.Reasoning)
114122 if p.Reasoning != i.cfg.Reasoning {
@@ -169,6 +177,7 @@ func (i *Interactive) activateModelProfile(slot int, p QuickModelShortcut) {
169177 p.Provider, p.Model = i.cfg.Provider, i.cfg.Model
170178 p.Reasoning = provider.ClampReasoningForModel(m, p.Reasoning)
171179 i.setLiveReasoning(p.Reasoning)
180+ i.setLiveFastMode(p.FastMode)
172181 if !i.persistModelProfile(slot, p, slot) {
173182 i.detachUnsavedModelProfile()
174183 return
@@ -198,3 +207,22 @@ func (i *Interactive) setLiveReasoning(level string) {
198207 }
199208 i.mu.Unlock()
200209 }
210+
211+func (i *Interactive) setLiveFastMode(enabled bool) {
212+ // Agent replacement publishes under agentMu -> mu. Hold agentMu across
213+ // the snapshot and update so a replacement cannot appear with stale mode.
214+ i.agentMu.Lock()
215+ i.mu.Lock()
216+ value := enabled
217+ i.cfg.FastMode = &value
218+ agent := i.agent
219+ onChanged := i.cfg.OnFastModeChanged
220+ i.mu.Unlock()
221+ if agent != nil {
222+ agent.SetFastMode(enabled)
223+ }
224+ i.agentMu.Unlock()
225+ if onChanged != nil {
226+ onChanged(enabled)
227+ }
228+}
@@ -15,7 +15,8 @@ func (c *InteractiveConfig) detachMismatchedModelProfile() {
15 return15 return
16 }16 }
17 p := c.QuickModelShortcuts[slot-1]17 p := c.QuickModelShortcuts[slot-1]
18- if p.Provider != c.Provider || p.Model != c.Model || provider.NormalizeReasoning(p.Reasoning) != provider.NormalizeReasoning(c.Reasoning) {18+ liveFastMode := c.FastMode != nil && *c.FastMode
19+ if p.Provider != c.Provider || p.Model != c.Model || provider.NormalizeReasoning(p.Reasoning) != provider.NormalizeReasoning(c.Reasoning) || p.FastMode != liveFastMode {
19 c.ActiveModelProfile = 020 c.ActiveModelProfile = 0
20 }21 }
21 }22 }
@@ -25,7 +26,11 @@ func modelProfileDescription(p QuickModelShortcut) string {
25 if level == "" {26 if level == "" {
26 level = "off"27 level = "off"
27 }28 }
28- return p.Provider + " / " + p.Model + " (reasoning: " + level + ")"29+ fast := "off"
30+ if p.FastMode {
31+ fast = "on"
32+ }
33+ return p.Provider + " / " + p.Model + " (reasoning: " + level + ", fast: " + fast + ")"
29 }34 }
30 35
31 // persistModelProfile changes in-memory favorites only after saving succeeds.36 // persistModelProfile changes in-memory favorites only after saving succeeds.
@@ -108,7 +113,10 @@ func (i *Interactive) saveActiveModelProfile() {
108 if slot < 1 || slot > 9 {113 if slot < 1 || slot > 9 {
109 return114 return
110 }115 }
111- p := QuickModelShortcut{Provider: i.cfg.Provider, Model: i.cfg.Model, Reasoning: i.cfg.Reasoning}116+ p := QuickModelShortcut{
117+ Provider: i.cfg.Provider, Model: i.cfg.Model, Reasoning: i.cfg.Reasoning,
118+ FastMode: i.cfg.FastMode != nil && *i.cfg.FastMode,
119+ }
112 if m, err := provider.FindModel(p.Provider, p.Model); err == nil {120 if m, err := provider.FindModel(p.Provider, p.Model); err == nil {
113 p.Reasoning = provider.ClampReasoningForModel(m, p.Reasoning)121 p.Reasoning = provider.ClampReasoningForModel(m, p.Reasoning)
114 if p.Reasoning != i.cfg.Reasoning {122 if p.Reasoning != i.cfg.Reasoning {
@@ -169,6 +177,7 @@ func (i *Interactive) activateModelProfile(slot int, p QuickModelShortcut) {
169 p.Provider, p.Model = i.cfg.Provider, i.cfg.Model177 p.Provider, p.Model = i.cfg.Provider, i.cfg.Model
170 p.Reasoning = provider.ClampReasoningForModel(m, p.Reasoning)178 p.Reasoning = provider.ClampReasoningForModel(m, p.Reasoning)
171 i.setLiveReasoning(p.Reasoning)179 i.setLiveReasoning(p.Reasoning)
180+ i.setLiveFastMode(p.FastMode)
172 if !i.persistModelProfile(slot, p, slot) {181 if !i.persistModelProfile(slot, p, slot) {
173 i.detachUnsavedModelProfile()182 i.detachUnsavedModelProfile()
174 return183 return
@@ -198,3 +207,22 @@ func (i *Interactive) setLiveReasoning(level string) {
198 }207 }
199 i.mu.Unlock()208 i.mu.Unlock()
200 }209 }
210+
211+func (i *Interactive) setLiveFastMode(enabled bool) {
212+ // Agent replacement publishes under agentMu -> mu. Hold agentMu across
213+ // the snapshot and update so a replacement cannot appear with stale mode.
214+ i.agentMu.Lock()
215+ i.mu.Lock()
216+ value := enabled
217+ i.cfg.FastMode = &value
218+ agent := i.agent
219+ onChanged := i.cfg.OnFastModeChanged
220+ i.mu.Unlock()
221+ if agent != nil {
222+ agent.SetFastMode(enabled)
223+ }
224+ i.agentMu.Unlock()
225+ if onChanged != nil {
226+ onChanged(enabled)
227+ }
228+}
M packages/agent/modes/model_profiles_test.go +47 -1
@@ -79,6 +79,46 @@ func TestModelProfileRecallAndAutosave(t *testing.T) {
7979 }
8080 }
8181
82+func TestModelProfileRestoresFastModePerSlot(t *testing.T) {
83+ i, s := newProfileInteractive(t)
84+ enabled := true
85+ i.cfg.FastMode = &enabled
86+ i.agent.SetFastMode(true)
87+ i.cfg.QuickModelShortcuts = []QuickModelShortcut{
88+ {Provider: "openai", Model: "gpt-5.6-sol", Reasoning: "high", FastMode: true},
89+ {Provider: "openai", Model: "gpt-5.5", FastMode: false},
90+ }
91+
92+ i.applyQuickModelShortcut(1)
93+ if !i.agent.FastModeEnabled() {
94+ t.Fatal("profile 1 did not restore fast mode")
95+ }
96+ i.applyQuickModelShortcut(2)
97+ if i.agent.FastModeEnabled() || i.cfg.FastMode == nil || *i.cfg.FastMode || s.profiles[2].FastMode {
98+ t.Fatalf("profile 2 did not restore fast mode off: agent=%v cfg=%v store=%+v", i.agent.FastModeEnabled(), i.cfg.FastMode, s)
99+ }
100+ i.applyQuickModelShortcut(1)
101+ if !i.agent.FastModeEnabled() || s.profiles[1].FastMode == false {
102+ t.Fatalf("profile 1 did not restore fast mode on: agent=%v store=%+v", i.agent.FastModeEnabled(), s)
103+ }
104+}
105+
106+func TestModelProfileFastModeToggleAutosavesActiveSlot(t *testing.T) {
107+ i, s := newProfileInteractive(t)
108+ var changed []bool
109+ i.cfg.OnFastModeChanged = func(enabled bool) { changed = append(changed, enabled) }
110+ i.applyQuickModelShortcut(1)
111+ changed = nil
112+ i.applySettingToggle("fast_mode", true)
113+ if i.cfg.FastMode == nil || !*i.cfg.FastMode || !i.agent.FastModeEnabled() || !s.profiles[1].FastMode || !reflect.DeepEqual(changed, []bool{true}) {
114+ t.Fatalf("fast mode enable was not applied to active profile: cfg=%v agent=%v store=%+v", i.cfg.FastMode, i.agent.FastModeEnabled(), s)
115+ }
116+ i.applySettingToggle("fast_mode", false)
117+ if i.cfg.FastMode == nil || *i.cfg.FastMode || i.agent.FastModeEnabled() || s.profiles[1].FastMode || !reflect.DeepEqual(changed, []bool{true, false}) {
118+ t.Fatalf("fast mode disable was not applied to active profile: cfg=%v agent=%v store=%+v", i.cfg.FastMode, i.agent.FastModeEnabled(), s)
119+ }
120+}
121+
82122 func TestModelProfileEmptyShortcutOpensPickerAndCancelPreservesSelection(t *testing.T) {
83123 i, s := newProfileInteractive(t)
84124 i.applyQuickModelShortcut(1)
@@ -202,6 +242,7 @@ type legacyProfileSettingsStore struct {
202242 SettingsStore
203243 shortcut QuickModelShortcut
204244 reasoning string
245+ fastMode bool
205246 }
206247
207248 func (s *legacyProfileSettingsStore) SetQuickModelShortcut(_ int, prov, model string) error {
@@ -212,6 +253,10 @@ func (s *legacyProfileSettingsStore) SetReasoning(level string) error {
212253 s.reasoning = level
213254 return nil
214255 }
256+func (s *legacyProfileSettingsStore) SetFastMode(enabled bool) error {
257+ s.fastMode = enabled
258+ return nil
259+}
215260
216261 func TestModelProfileLegacySettingsStoreCompatibility(t *testing.T) {
217262 s := &legacyProfileSettingsStore{}
@@ -222,9 +267,10 @@ func TestModelProfileLegacySettingsStoreCompatibility(t *testing.T) {
222267 t.Fatal("legacy store cannot persist an active profile")
223268 }
224269 i.applyReasoningSetting("low")
270+ i.applySettingToggle("fast_mode", true)
225271 i.setQuickModelProfile(2, QuickModelShortcut{Provider: "openai", Model: "gpt-5.5"})
226272 i.applyQuickModelShortcut(2)
227- if s.reasoning != "low" || s.shortcut.Model != "gpt-5.5" || i.agent.Model != "gpt-5.5" || i.cfg.ActiveModelProfile != 0 {
273+ if s.reasoning != "low" || !s.fastMode || !i.agent.FastModeEnabled() || s.shortcut.Model != "gpt-5.5" || i.agent.Model != "gpt-5.5" || i.cfg.ActiveModelProfile != 0 {
228274 t.Fatal("legacy reasoning/model-only settings no longer work")
229275 }
230276 }
@@ -79,6 +79,46 @@ func TestModelProfileRecallAndAutosave(t *testing.T) {
79 }79 }
80 }80 }
81 81
82+func TestModelProfileRestoresFastModePerSlot(t *testing.T) {
83+ i, s := newProfileInteractive(t)
84+ enabled := true
85+ i.cfg.FastMode = &enabled
86+ i.agent.SetFastMode(true)
87+ i.cfg.QuickModelShortcuts = []QuickModelShortcut{
88+ {Provider: "openai", Model: "gpt-5.6-sol", Reasoning: "high", FastMode: true},
89+ {Provider: "openai", Model: "gpt-5.5", FastMode: false},
90+ }
91+
92+ i.applyQuickModelShortcut(1)
93+ if !i.agent.FastModeEnabled() {
94+ t.Fatal("profile 1 did not restore fast mode")
95+ }
96+ i.applyQuickModelShortcut(2)
97+ if i.agent.FastModeEnabled() || i.cfg.FastMode == nil || *i.cfg.FastMode || s.profiles[2].FastMode {
98+ t.Fatalf("profile 2 did not restore fast mode off: agent=%v cfg=%v store=%+v", i.agent.FastModeEnabled(), i.cfg.FastMode, s)
99+ }
100+ i.applyQuickModelShortcut(1)
101+ if !i.agent.FastModeEnabled() || s.profiles[1].FastMode == false {
102+ t.Fatalf("profile 1 did not restore fast mode on: agent=%v store=%+v", i.agent.FastModeEnabled(), s)
103+ }
104+}
105+
106+func TestModelProfileFastModeToggleAutosavesActiveSlot(t *testing.T) {
107+ i, s := newProfileInteractive(t)
108+ var changed []bool
109+ i.cfg.OnFastModeChanged = func(enabled bool) { changed = append(changed, enabled) }
110+ i.applyQuickModelShortcut(1)
111+ changed = nil
112+ i.applySettingToggle("fast_mode", true)
113+ if i.cfg.FastMode == nil || !*i.cfg.FastMode || !i.agent.FastModeEnabled() || !s.profiles[1].FastMode || !reflect.DeepEqual(changed, []bool{true}) {
114+ t.Fatalf("fast mode enable was not applied to active profile: cfg=%v agent=%v store=%+v", i.cfg.FastMode, i.agent.FastModeEnabled(), s)
115+ }
116+ i.applySettingToggle("fast_mode", false)
117+ if i.cfg.FastMode == nil || *i.cfg.FastMode || i.agent.FastModeEnabled() || s.profiles[1].FastMode || !reflect.DeepEqual(changed, []bool{true, false}) {
118+ t.Fatalf("fast mode disable was not applied to active profile: cfg=%v agent=%v store=%+v", i.cfg.FastMode, i.agent.FastModeEnabled(), s)
119+ }
120+}
121+
82 func TestModelProfileEmptyShortcutOpensPickerAndCancelPreservesSelection(t *testing.T) {122 func TestModelProfileEmptyShortcutOpensPickerAndCancelPreservesSelection(t *testing.T) {
83 i, s := newProfileInteractive(t)123 i, s := newProfileInteractive(t)
84 i.applyQuickModelShortcut(1)124 i.applyQuickModelShortcut(1)
@@ -202,6 +242,7 @@ type legacyProfileSettingsStore struct {
202 SettingsStore242 SettingsStore
203 shortcut QuickModelShortcut243 shortcut QuickModelShortcut
204 reasoning string244 reasoning string
245+ fastMode bool
205 }246 }
206 247
207 func (s *legacyProfileSettingsStore) SetQuickModelShortcut(_ int, prov, model string) error {248 func (s *legacyProfileSettingsStore) SetQuickModelShortcut(_ int, prov, model string) error {
@@ -212,6 +253,10 @@ func (s *legacyProfileSettingsStore) SetReasoning(level string) error {
212 s.reasoning = level253 s.reasoning = level
213 return nil254 return nil
214 }255 }
256+func (s *legacyProfileSettingsStore) SetFastMode(enabled bool) error {
257+ s.fastMode = enabled
258+ return nil
259+}
215 260
216 func TestModelProfileLegacySettingsStoreCompatibility(t *testing.T) {261 func TestModelProfileLegacySettingsStoreCompatibility(t *testing.T) {
217 s := &legacyProfileSettingsStore{}262 s := &legacyProfileSettingsStore{}
@@ -222,9 +267,10 @@ func TestModelProfileLegacySettingsStoreCompatibility(t *testing.T) {
222 t.Fatal("legacy store cannot persist an active profile")267 t.Fatal("legacy store cannot persist an active profile")
223 }268 }
224 i.applyReasoningSetting("low")269 i.applyReasoningSetting("low")
270+ i.applySettingToggle("fast_mode", true)
225 i.setQuickModelProfile(2, QuickModelShortcut{Provider: "openai", Model: "gpt-5.5"})271 i.setQuickModelProfile(2, QuickModelShortcut{Provider: "openai", Model: "gpt-5.5"})
226 i.applyQuickModelShortcut(2)272 i.applyQuickModelShortcut(2)
227- if s.reasoning != "low" || s.shortcut.Model != "gpt-5.5" || i.agent.Model != "gpt-5.5" || i.cfg.ActiveModelProfile != 0 {273+ if s.reasoning != "low" || !s.fastMode || !i.agent.FastModeEnabled() || s.shortcut.Model != "gpt-5.5" || i.agent.Model != "gpt-5.5" || i.cfg.ActiveModelProfile != 0 {
228 t.Fatal("legacy reasoning/model-only settings no longer work")274 t.Fatal("legacy reasoning/model-only settings no longer work")
229 }275 }
230 }276 }
M packages/agent/modes/slash_suggest.go +1 -1
@@ -52,7 +52,7 @@ var slashCatalog = []slashCommand{
5252 {Name: "/logout", Desc: "clear a provider's credentials"},
5353 {Name: "/model", Desc: "pick a model (or /model <id>)"},
5454 {Name: "/reasoning", Desc: "set the reasoning level"},
55- {Name: "/profile", Desc: "activate model + reasoning profile (/profile <1-9>)"},
55+ {Name: "/profile", Desc: "activate model + reasoning + fast-mode profile (/profile <1-9>)"},
5656 {Name: "/fast", Desc: "toggle fast mode"},
5757 {Name: "/orchestrator", Desc: "toggle automatic subagent orchestration"},
5858 {Name: "/goal", Desc: "start or control an autonomous goal"},
@@ -52,7 +52,7 @@ var slashCatalog = []slashCommand{
52 {Name: "/logout", Desc: "clear a provider's credentials"},52 {Name: "/logout", Desc: "clear a provider's credentials"},
53 {Name: "/model", Desc: "pick a model (or /model <id>)"},53 {Name: "/model", Desc: "pick a model (or /model <id>)"},
54 {Name: "/reasoning", Desc: "set the reasoning level"},54 {Name: "/reasoning", Desc: "set the reasoning level"},
55- {Name: "/profile", Desc: "activate model + reasoning profile (/profile <1-9>)"},55+ {Name: "/profile", Desc: "activate model + reasoning + fast-mode profile (/profile <1-9>)"},
56 {Name: "/fast", Desc: "toggle fast mode"},56 {Name: "/fast", Desc: "toggle fast mode"},
57 {Name: "/orchestrator", Desc: "toggle automatic subagent orchestration"},57 {Name: "/orchestrator", Desc: "toggle automatic subagent orchestration"},
58 {Name: "/goal", Desc: "start or control an autonomous goal"},58 {Name: "/goal", Desc: "start or control an autonomous goal"},
M packages/agent/settings_store.go +13 -2
@@ -22,7 +22,13 @@ func (s configSettingsStore) SetQuickModelShortcut(slot int, providerName, model
2222 if cfg.ActiveModelProfile == slot {
2323 cfg.ActiveModelProfile = 0
2424 }
25- return s.SetModelProfile(slot, modes.QuickModelShortcut{Provider: providerName, Model: model}, cfg.ActiveModelProfile)
25+ profile := modes.QuickModelShortcut{Provider: providerName, Model: model}
26+ if slot <= len(cfg.QuickModelShortcuts) {
27+ current := cfg.QuickModelShortcuts[slot-1]
28+ profile.Reasoning = current.Reasoning
29+ profile.FastMode = current.FastMode
30+ }
31+ return s.SetModelProfile(slot, profile, cfg.ActiveModelProfile)
2632 }
2733
2834 func (configSettingsStore) SetModelProfile(slot int, profile modes.QuickModelShortcut, activeSlot int) error {
@@ -38,7 +44,10 @@ func (configSettingsStore) SetModelProfile(slot int, profile modes.QuickModelSho
3844 copy(next, cfg.QuickModelShortcuts)
3945 cfg.QuickModelShortcuts = next
4046 }
41- cfg.QuickModelShortcuts[slot-1] = QuickModelShortcut{Provider: profile.Provider, Model: profile.Model, Reasoning: provider.NormalizeReasoning(profile.Reasoning)}
47+ cfg.QuickModelShortcuts[slot-1] = QuickModelShortcut{
48+ Provider: profile.Provider, Model: profile.Model,
49+ Reasoning: provider.NormalizeReasoning(profile.Reasoning), FastMode: profile.FastMode,
50+ }
4251 cfg.ActiveModelProfile = activeSlot
4352 if activeSlot == slot {
4453 if profile.Provider == "" || profile.Model == "" {
@@ -46,6 +55,8 @@ func (configSettingsStore) SetModelProfile(slot int, profile modes.QuickModelSho
4655 }
4756 cfg.Provider, cfg.Model = profile.Provider, profile.Model
4857 cfg.Reasoning = provider.NormalizeReasoning(profile.Reasoning)
58+ fastMode := profile.FastMode
59+ cfg.FastMode = &fastMode
4960 }
5061 // Trim trailing empty slots so config.json stays compact.
5162 for len(cfg.QuickModelShortcuts) > 0 {
@@ -22,7 +22,13 @@ func (s configSettingsStore) SetQuickModelShortcut(slot int, providerName, model
22 if cfg.ActiveModelProfile == slot {22 if cfg.ActiveModelProfile == slot {
23 cfg.ActiveModelProfile = 023 cfg.ActiveModelProfile = 0
24 }24 }
25- return s.SetModelProfile(slot, modes.QuickModelShortcut{Provider: providerName, Model: model}, cfg.ActiveModelProfile)25+ profile := modes.QuickModelShortcut{Provider: providerName, Model: model}
26+ if slot <= len(cfg.QuickModelShortcuts) {
27+ current := cfg.QuickModelShortcuts[slot-1]
28+ profile.Reasoning = current.Reasoning
29+ profile.FastMode = current.FastMode
30+ }
31+ return s.SetModelProfile(slot, profile, cfg.ActiveModelProfile)
26 }32 }
27 33
28 func (configSettingsStore) SetModelProfile(slot int, profile modes.QuickModelShortcut, activeSlot int) error {34 func (configSettingsStore) SetModelProfile(slot int, profile modes.QuickModelShortcut, activeSlot int) error {
@@ -38,7 +44,10 @@ func (configSettingsStore) SetModelProfile(slot int, profile modes.QuickModelSho
38 copy(next, cfg.QuickModelShortcuts)44 copy(next, cfg.QuickModelShortcuts)
39 cfg.QuickModelShortcuts = next45 cfg.QuickModelShortcuts = next
40 }46 }
41- cfg.QuickModelShortcuts[slot-1] = QuickModelShortcut{Provider: profile.Provider, Model: profile.Model, Reasoning: provider.NormalizeReasoning(profile.Reasoning)}47+ cfg.QuickModelShortcuts[slot-1] = QuickModelShortcut{
48+ Provider: profile.Provider, Model: profile.Model,
49+ Reasoning: provider.NormalizeReasoning(profile.Reasoning), FastMode: profile.FastMode,
50+ }
42 cfg.ActiveModelProfile = activeSlot51 cfg.ActiveModelProfile = activeSlot
43 if activeSlot == slot {52 if activeSlot == slot {
44 if profile.Provider == "" || profile.Model == "" {53 if profile.Provider == "" || profile.Model == "" {
@@ -46,6 +55,8 @@ func (configSettingsStore) SetModelProfile(slot int, profile modes.QuickModelSho
46 }55 }
47 cfg.Provider, cfg.Model = profile.Provider, profile.Model56 cfg.Provider, cfg.Model = profile.Provider, profile.Model
48 cfg.Reasoning = provider.NormalizeReasoning(profile.Reasoning)57 cfg.Reasoning = provider.NormalizeReasoning(profile.Reasoning)
58+ fastMode := profile.FastMode
59+ cfg.FastMode = &fastMode
49 }60 }
50 // Trim trailing empty slots so config.json stays compact.61 // Trim trailing empty slots so config.json stays compact.
51 for len(cfg.QuickModelShortcuts) > 0 {62 for len(cfg.QuickModelShortcuts) > 0 {
M packages/agent/subagent_runtime_test.go +24 -0
@@ -67,6 +67,30 @@ func TestResidentChildSpecSnapshotsCurrentProviderTransportSettings(t *testing.T
6767 }
6868 }
6969
70+func TestResidentChildSpecTracksFastMode(t *testing.T) {
71+ runtime := newSubagentRuntime(subagentRuntimeConfig{
72+ Args: Args{}, Root: t.TempDir(), RepoRoot: t.TempDir(),
73+ Provider: "openai", Model: "gpt-5.6-sol", FastMode: false,
74+ })
75+ t.Cleanup(func() { _ = runtime.Close(context.Background()) })
76+
77+ spec, err := runtime.buildResidentChildSpec(context.Background(), tools.ResidentSpawnRequest{Task: "review"}, core.Registry{"read": nil})
78+ if err != nil {
79+ t.Fatal(err)
80+ }
81+ if spec.FastMode {
82+ t.Fatal("initial resident fast mode = true, want false")
83+ }
84+ runtime.SetFastMode(true)
85+ spec, err = runtime.buildResidentChildSpec(context.Background(), tools.ResidentSpawnRequest{Task: "review"}, core.Registry{"read": nil})
86+ if err != nil {
87+ t.Fatal(err)
88+ }
89+ if !spec.FastMode {
90+ t.Fatal("resident fast mode = false after update, want true")
91+ }
92+}
93+
7094 func TestResidentChildSpecUsesSelectedModelContextForBudget(t *testing.T) {
7195 runtime := newSubagentRuntime(subagentRuntimeConfig{
7296 Args: Args{}, Root: t.TempDir(), RepoRoot: t.TempDir(),
@@ -67,6 +67,30 @@ func TestResidentChildSpecSnapshotsCurrentProviderTransportSettings(t *testing.T
67 }67 }
68 }68 }
69 69
70+func TestResidentChildSpecTracksFastMode(t *testing.T) {
71+ runtime := newSubagentRuntime(subagentRuntimeConfig{
72+ Args: Args{}, Root: t.TempDir(), RepoRoot: t.TempDir(),
73+ Provider: "openai", Model: "gpt-5.6-sol", FastMode: false,
74+ })
75+ t.Cleanup(func() { _ = runtime.Close(context.Background()) })
76+
77+ spec, err := runtime.buildResidentChildSpec(context.Background(), tools.ResidentSpawnRequest{Task: "review"}, core.Registry{"read": nil})
78+ if err != nil {
79+ t.Fatal(err)
80+ }
81+ if spec.FastMode {
82+ t.Fatal("initial resident fast mode = true, want false")
83+ }
84+ runtime.SetFastMode(true)
85+ spec, err = runtime.buildResidentChildSpec(context.Background(), tools.ResidentSpawnRequest{Task: "review"}, core.Registry{"read": nil})
86+ if err != nil {
87+ t.Fatal(err)
88+ }
89+ if !spec.FastMode {
90+ t.Fatal("resident fast mode = false after update, want true")
91+ }
92+}
93+
70 func TestResidentChildSpecUsesSelectedModelContextForBudget(t *testing.T) {94 func TestResidentChildSpecUsesSelectedModelContextForBudget(t *testing.T) {
71 runtime := newSubagentRuntime(subagentRuntimeConfig{95 runtime := newSubagentRuntime(subagentRuntimeConfig{
72 Args: Args{}, Root: t.TempDir(), RepoRoot: t.TempDir(),96 Args: Args{}, Root: t.TempDir(), RepoRoot: t.TempDir(),

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