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 Unverified
B5690EEEBB952194The 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 | |||
| 326 | 326 | | `/login` | Log in via API key or subscription (opens a dialog). | | |
| 327 | 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 | 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 | 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 | 332 | | `/orchestrator` | Toggle proactive subagent delegation. When disabled, subagent tools remain available for explicit delegation. | | |
| 333 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 474 | ### Model profiles | |
| 475 | 475 | ||
| 476 | 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. | |
| 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. | |
| 481 | 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 | 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 | 489 | "active_model_profile": 1, | |
| 490 | 490 | "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} | |
| 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 | 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 | 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 | 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 | 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 | 974 | | `@` | Open the file picker. Browse files and directories in the working directory. | | |
| 975 | 975 | ||
| 976 | 976 | ### 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 profiles | 474 | ### 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) { | |||
| 719 | 719 | cfg.Reasoning = provider.ClampReasoningForModel(resolvedModel, cfg.Reasoning) | |
| 720 | 720 | cfg.QuickModelShortcuts[profileSlot-1] = QuickModelShortcut{ | |
| 721 | 721 | Provider: cfg.Provider, Model: cfg.Model, Reasoning: cfg.Reasoning, | |
| 722 | + | FastMode: cfg.FastMode != nil && *cfg.FastMode, | |
| 722 | 723 | } | |
| 723 | 724 | cfg.ActiveModelProfile = profileSlot | |
| 724 | 725 | } | |
| @@ -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 = profileSlot | 724 | cfg.ActiveModelProfile = profileSlot | ||
| 724 | } | 725 | } | ||
M packages/agent/cli.go +2 -1
| @@ -1493,7 +1493,7 @@ func interactiveQuickModelShortcuts(shortcuts []QuickModelShortcut) []modes.Quic | |||
| 1493 | 1493 | result := make([]modes.QuickModelShortcut, len(shortcuts)) | |
| 1494 | 1494 | for idx, s := range shortcuts { | |
| 1495 | 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 | 1498 | return result | |
| 1499 | 1499 | } | |
| @@ -2629,6 +2629,7 @@ func runInteractive(ctx context.Context, args Args, version string) (runErr erro | |||
| 2629 | 2629 | BaseURL: r.BaseURL, | |
| 2630 | 2630 | Reasoning: r.Reasoning, | |
| 2631 | 2631 | OnReasoningChanged: runtime.SetReasoning, | |
| 2632 | + | OnFastModeChanged: runtime.SetFastMode, | |
| 2632 | 2633 | SystemPrompt: r.SystemPrompt, | |
| 2633 | 2634 | WritingGuidance: WritingGuidance(), | |
| 2634 | 2635 | 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 result | 1498 | 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 ( | |||
| 16 | 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 | 20 | type QuickModelShortcut struct { | |
| 21 | 21 | Provider string `json:"provider"` | |
| 22 | 22 | Model string `json:"model"` | |
| 23 | 23 | Reasoning string `json:"reasoning,omitempty"` | |
| 24 | + | FastMode bool `json:"fast_mode,omitempty"` | |
| 24 | 25 | } | |
| 25 | 26 | ||
| 26 | 27 | // SubagentsConfig contains the resident manager policy. max_concurrent limits | |
| @@ -50,7 +51,8 @@ type Config struct { | |||
| 50 | 51 | Theme string `json:"theme"` | |
| 51 | 52 | ||
| 52 | 53 | // 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 | |
| 54 | 56 | // requests at the provider boundary. | |
| 55 | 57 | FastMode *bool `json:"fast_mode,omitempty"` | |
| 56 | 58 | ||
| @@ -67,7 +69,7 @@ type Config struct { | |||
| 67 | 69 | // overrides this when set. | |
| 68 | 70 | CompactInput *bool `json:"compact_input,omitempty"` | |
| 69 | 71 | ||
| 70 | - | // QuickModelShortcuts maps slots 1-9 to provider/model pairs used by | |
| 72 | + | // QuickModelShortcuts maps slots 1-9 to model profiles used by | |
| 71 | 73 | // Ctrl+1..9. Cmd+1..9 may also work on terminals that forward Super. | |
| 72 | 74 | QuickModelShortcuts []QuickModelShortcut `json:"quick_model_shortcuts,omitempty"` | |
| 73 | 75 | ActiveModelProfile int `json:"active_model_profile,omitempty"` | |
| @@ -349,6 +351,8 @@ func (c *Config) applyActiveModelProfile() int { | |||
| 349 | 351 | return 0 | |
| 350 | 352 | } | |
| 351 | 353 | c.Provider, c.Model, c.Reasoning = p.Provider, p.Model, p.Reasoning | |
| 354 | + | fastMode := p.FastMode | |
| 355 | + | c.FastMode = &fastMode | |
| 352 | 356 | return slot | |
| 353 | 357 | } | |
| 354 | 358 | ||
| @@ -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 limits | 27 | // 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-mode | 54 | + | // 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 by | 72 | + | // 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 0 | 351 | return 0 | ||
| 350 | } | 352 | } | ||
| 351 | c.Provider, c.Model, c.Reasoning = p.Provider, p.Model, p.Reasoning | 353 | c.Provider, c.Model, c.Reasoning = p.Provider, p.Model, p.Reasoning | ||
| 354 | + | fastMode := p.FastMode | |||
| 355 | + | c.FastMode = &fastMode | |||
| 352 | return slot | 356 | return slot | ||
| 353 | } | 357 | } | ||
| 354 | 358 | ||||
M packages/agent/fast_mode_test.go +26 -0
| @@ -3,6 +3,7 @@ package agent | |||
| 3 | 3 | import ( | |
| 4 | 4 | "context" | |
| 5 | 5 | "encoding/json" | |
| 6 | + | "fmt" | |
| 6 | 7 | "strings" | |
| 7 | 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 | 94 | func TestResolveFastModeRequiresExplicitChildOverride(t *testing.T) { | |
| 69 | 95 | t.Setenv("ZUT_HOME", t.TempDir()) | |
| 70 | 96 | 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) { | |||
| 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 | 80 | func TestModelProfileConfigRoundTrip(t *testing.T) { | |
| 59 | 81 | t.Setenv("ZUT_HOME", t.TempDir()) | |
| 60 | 82 | if err := SaveConfig(Config{Theme: "dark"}); err != nil { | |
| 61 | 83 | t.Fatal(err) | |
| 62 | 84 | } | |
| 63 | 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 | 87 | if err := store.SetModelProfile(9, p, 9); err != nil { | |
| 66 | 88 | t.Fatal(err) | |
| 67 | 89 | } | |
| @@ -69,7 +91,7 @@ func TestModelProfileConfigRoundTrip(t *testing.T) { | |||
| 69 | 91 | if err != nil { | |
| 70 | 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 | 95 | t.Fatalf("config=%+v", cfg) | |
| 74 | 96 | } | |
| 75 | 97 | // 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 model | 97 | // 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{ | |||
| 23 | 23 | {"ctrl+l", "redraw the screen"}, | |
| 24 | 24 | {"ctrl+o", "expand / collapse long tool results"}, | |
| 25 | 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 | 27 | {"pgup / pgdn", "scroll the chat one page up / down"}, | |
| 28 | 28 | {"up / down", "move in input, browse prompt history at top edge, scroll chat when empty"}, | |
| 29 | 29 | } | |
| @@ -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 { | |||
| 101 | 101 | AutoSubagentsStopToolAllowed *bool | |
| 102 | 102 | AutoSubagentsResumeToolAllowed *bool | |
| 103 | 103 | ||
| 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. | |
| 107 | 108 | FastMode *bool | |
| 108 | 109 | ||
| 109 | 110 | // LSPEnabled controls the main session's built-in lsp tool. nil means | |
| @@ -171,9 +172,9 @@ type InteractiveConfig struct { | |||
| 171 | 172 | // above or below the main input. | |
| 172 | 173 | TUIWorkingPosition string | |
| 173 | 174 | ||
| 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. | |
| 177 | 178 | QuickModelShortcuts []QuickModelShortcut | |
| 178 | 179 | ActiveModelProfile int // 0 means no active profile; otherwise 1–9 | |
| 179 | 180 | ||
| @@ -355,6 +356,9 @@ type InteractiveConfig struct { | |||
| 355 | 356 | // OnReasoningChanged updates host-owned runtime defaults after the setting | |
| 356 | 357 | // has been persisted successfully. | |
| 357 | 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 | 363 | // InitialSessionTitle is the persisted title of the session loaded before | |
| 360 | 364 | // the TUI starts. It is shown in the terminal without another model call. | |
| @@ -479,11 +483,12 @@ type chatCacheKey struct { | |||
| 479 | 483 | viewCacheRev uint64 | |
| 480 | 484 | } | |
| 481 | 485 | ||
| 482 | - | // QuickModelShortcut is one configured quick model switch slot. | |
| 486 | + | // QuickModelShortcut is one configured model profile slot. | |
| 483 | 487 | type QuickModelShortcut struct { | |
| 484 | 488 | Provider string | |
| 485 | 489 | Model string | |
| 486 | 490 | Reasoning string | |
| 491 | + | FastMode bool | |
| 487 | 492 | } | |
| 488 | 493 | ||
| 489 | 494 | type extensionStatus struct { | |
| @@ -101,9 +101,10 @@ type InteractiveConfig struct { | |||||
| 101 | AutoSubagentsStopToolAllowed *bool | 101 | AutoSubagentsStopToolAllowed *bool | ||
| 102 | AutoSubagentsResumeToolAllowed *bool | 102 | 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 attempts | 105 | + | // 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 *bool | 108 | FastMode *bool | ||
| 108 | 109 | ||||
| 109 | // LSPEnabled controls the main session's built-in lsp tool. nil means | 110 | // 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 string | 173 | TUIWorkingPosition string | ||
| 173 | 174 | ||||
| 174 | - | // QuickModelShortcuts maps slots 1-9 to provider/model pairs. The | 175 | + | // QuickModelShortcuts maps slots 1-9 to model profiles. The shortcuts |
| 175 | - | // shortcuts are Ctrl+1..9. Cmd+1..9 may also work when the terminal | 176 | + | // 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 []QuickModelShortcut | 178 | QuickModelShortcuts []QuickModelShortcut | ||
| 178 | ActiveModelProfile int // 0 means no active profile; otherwise 1–9 | 179 | 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 setting | 356 | // 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 before | 363 | // 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 uint64 | 483 | 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 string | 488 | Provider string | ||
| 485 | Model string | 489 | Model string | ||
| 486 | Reasoning string | 490 | 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() { | |||
| 409 | 409 | items = append(items, settingsItem{ | |
| 410 | 410 | key: "quick_models", | |
| 411 | 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 | 413 | children: quickItems, | |
| 414 | 414 | }) | |
| 415 | 415 | } | |
| @@ -487,7 +487,7 @@ func (i *Interactive) quickModelSettingItem(slot int) settingsItem { | |||
| 487 | 487 | return settingsItem{ | |
| 488 | 488 | key: "quick_model_" + strconv.Itoa(slot), | |
| 489 | 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 | 491 | picker: true, | |
| 492 | 492 | hint: hint, | |
| 493 | 493 | } | |
| @@ -542,7 +542,11 @@ func (i *Interactive) openQuickModelPicker(slot int) { | |||
| 542 | 542 | i.modelDialog.Open(current, loggedIn, reasoning) | |
| 543 | 543 | } | |
| 544 | 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 | 550 | if i.quickModelActivate { | |
| 547 | 551 | i.activateModelProfile(slot, p) | |
| 548 | 552 | } else { | |
| @@ -594,7 +598,10 @@ func (i *Interactive) applyQuickModelSetting(key, value string) { | |||
| 594 | 598 | i.setQuickModelShortcut(slot, providerName, model) | |
| 595 | 599 | } | |
| 596 | 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 | 606 | func (i *Interactive) refreshQuickModelSettingsItem(slot int) { | |
| 600 | 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 | 923 | i.mu.Unlock() | |
| 917 | 924 | 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 | 945 | if store, ok := i.cfg.SettingsStore.(fastModeSettingsStore); ok { | |
| 920 | 946 | if err := store.SetFastMode(value); err != nil { | |
| 921 | 947 | i.resetSettingsToggle(key, previous) | |
| @@ -925,11 +951,7 @@ func (i *Interactive) applySettingToggle(key string, value bool) { | |||
| 925 | 951 | return | |
| 926 | 952 | } | |
| 927 | 953 | } | |
| 928 | - | val := value | |
| 929 | - | i.cfg.FastMode = &val | |
| 930 | - | if i.agent != nil { | |
| 931 | - | i.agent.SetFastMode(value) | |
| 932 | - | } | |
| 954 | + | i.setLiveFastMode(value) | |
| 933 | 955 | i.mu.Lock() | |
| 934 | 956 | i.statusOK = "fast mode " + onOff(value) | |
| 935 | 957 | i.statusErr = "" | |
| @@ -1253,7 +1275,10 @@ func (i *Interactive) applyReasoningSetting(level string) { | |||
| 1253 | 1275 | }() | |
| 1254 | 1276 | level = provider.NormalizeReasoning(level) | |
| 1255 | 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 | 1282 | if !i.persistModelProfile(slot, p, slot) { | |
| 1258 | 1283 | return | |
| 1259 | 1284 | } | |
| @@ -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 | return | 924 | 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 | return | 951 | return | ||
| 926 | } | 952 | } | ||
| 927 | } | 953 | } | ||
| 928 | - | val := value | 954 | + | 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 | return | 1283 | return | ||
| 1259 | } | 1284 | } | ||
M packages/agent/modes/model_profiles.go +31 -3
| @@ -15,7 +15,8 @@ func (c *InteractiveConfig) detachMismatchedModelProfile() { | |||
| 15 | 15 | return | |
| 16 | 16 | } | |
| 17 | 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 | 20 | c.ActiveModelProfile = 0 | |
| 20 | 21 | } | |
| 21 | 22 | } | |
| @@ -25,7 +26,11 @@ func modelProfileDescription(p QuickModelShortcut) string { | |||
| 25 | 26 | if level == "" { | |
| 26 | 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 | 36 | // persistModelProfile changes in-memory favorites only after saving succeeds. | |
| @@ -108,7 +113,10 @@ func (i *Interactive) saveActiveModelProfile() { | |||
| 108 | 113 | if slot < 1 || slot > 9 { | |
| 109 | 114 | 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 | 120 | if m, err := provider.FindModel(p.Provider, p.Model); err == nil { | |
| 113 | 121 | p.Reasoning = provider.ClampReasoningForModel(m, p.Reasoning) | |
| 114 | 122 | if p.Reasoning != i.cfg.Reasoning { | |
| @@ -169,6 +177,7 @@ func (i *Interactive) activateModelProfile(slot int, p QuickModelShortcut) { | |||
| 169 | 177 | p.Provider, p.Model = i.cfg.Provider, i.cfg.Model | |
| 170 | 178 | p.Reasoning = provider.ClampReasoningForModel(m, p.Reasoning) | |
| 171 | 179 | i.setLiveReasoning(p.Reasoning) | |
| 180 | + | i.setLiveFastMode(p.FastMode) | |
| 172 | 181 | if !i.persistModelProfile(slot, p, slot) { | |
| 173 | 182 | i.detachUnsavedModelProfile() | |
| 174 | 183 | return | |
| @@ -198,3 +207,22 @@ func (i *Interactive) setLiveReasoning(level string) { | |||
| 198 | 207 | } | |
| 199 | 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 | + | } | |
| @@ -15,7 +15,8 @@ func (c *InteractiveConfig) detachMismatchedModelProfile() { | |||||
| 15 | return | 15 | 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 = 0 | 20 | 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 | return | 114 | 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.Model | 177 | 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 | return | 183 | 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) { | |||
| 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 | 122 | func TestModelProfileEmptyShortcutOpensPickerAndCancelPreservesSelection(t *testing.T) { | |
| 83 | 123 | i, s := newProfileInteractive(t) | |
| 84 | 124 | i.applyQuickModelShortcut(1) | |
| @@ -202,6 +242,7 @@ type legacyProfileSettingsStore struct { | |||
| 202 | 242 | SettingsStore | |
| 203 | 243 | shortcut QuickModelShortcut | |
| 204 | 244 | reasoning string | |
| 245 | + | fastMode bool | |
| 205 | 246 | } | |
| 206 | 247 | ||
| 207 | 248 | func (s *legacyProfileSettingsStore) SetQuickModelShortcut(_ int, prov, model string) error { | |
| @@ -212,6 +253,10 @@ func (s *legacyProfileSettingsStore) SetReasoning(level string) error { | |||
| 212 | 253 | s.reasoning = level | |
| 213 | 254 | return nil | |
| 214 | 255 | } | |
| 256 | + | func (s *legacyProfileSettingsStore) SetFastMode(enabled bool) error { | |
| 257 | + | s.fastMode = enabled | |
| 258 | + | return nil | |
| 259 | + | } | |
| 215 | 260 | ||
| 216 | 261 | func TestModelProfileLegacySettingsStoreCompatibility(t *testing.T) { | |
| 217 | 262 | s := &legacyProfileSettingsStore{} | |
| @@ -222,9 +267,10 @@ func TestModelProfileLegacySettingsStoreCompatibility(t *testing.T) { | |||
| 222 | 267 | t.Fatal("legacy store cannot persist an active profile") | |
| 223 | 268 | } | |
| 224 | 269 | i.applyReasoningSetting("low") | |
| 270 | + | i.applySettingToggle("fast_mode", true) | |
| 225 | 271 | i.setQuickModelProfile(2, QuickModelShortcut{Provider: "openai", Model: "gpt-5.5"}) | |
| 226 | 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 | 274 | t.Fatal("legacy reasoning/model-only settings no longer work") | |
| 229 | 275 | } | |
| 230 | 276 | } | |
| @@ -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 | SettingsStore | 242 | SettingsStore | ||
| 203 | shortcut QuickModelShortcut | 243 | shortcut QuickModelShortcut | ||
| 204 | reasoning string | 244 | 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 = level | 253 | s.reasoning = level | ||
| 213 | return nil | 254 | 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{ | |||
| 52 | 52 | {Name: "/logout", Desc: "clear a provider's credentials"}, | |
| 53 | 53 | {Name: "/model", Desc: "pick a model (or /model <id>)"}, | |
| 54 | 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 | 56 | {Name: "/fast", Desc: "toggle fast mode"}, | |
| 57 | 57 | {Name: "/orchestrator", Desc: "toggle automatic subagent orchestration"}, | |
| 58 | 58 | {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 | |||
| 22 | 22 | if cfg.ActiveModelProfile == slot { | |
| 23 | 23 | 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 | 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 | 44 | copy(next, cfg.QuickModelShortcuts) | |
| 39 | 45 | 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 | 51 | cfg.ActiveModelProfile = activeSlot | |
| 43 | 52 | if activeSlot == slot { | |
| 44 | 53 | if profile.Provider == "" || profile.Model == "" { | |
| @@ -46,6 +55,8 @@ func (configSettingsStore) SetModelProfile(slot int, profile modes.QuickModelSho | |||
| 46 | 55 | } | |
| 47 | 56 | cfg.Provider, cfg.Model = profile.Provider, profile.Model | |
| 48 | 57 | cfg.Reasoning = provider.NormalizeReasoning(profile.Reasoning) | |
| 58 | + | fastMode := profile.FastMode | |
| 59 | + | cfg.FastMode = &fastMode | |
| 49 | 60 | } | |
| 50 | 61 | // Trim trailing empty slots so config.json stays compact. | |
| 51 | 62 | 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 = 0 | 23 | 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 = next | 45 | 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 = activeSlot | 51 | 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.Model | 56 | 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 | |||
| 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 | 94 | func TestResidentChildSpecUsesSelectedModelContextForBudget(t *testing.T) { | |
| 71 | 95 | runtime := newSubagentRuntime(subagentRuntimeConfig{ | |
| 72 | 96 | 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(), | ||