detached 1fa7669

Add native max thinking level

commit
1fa7669275469900f85c336f7bb7cf4f62e333b5
author
patriceckhart · · 2026-07-10 05:32 UTC
parent
c6cf9e7
signature
unsigned
refs
tag: v0.2.72
22 files · +253 -61
M README.md +3 -3
@@ -174,7 +174,7 @@ zot --help
174174 | `--insecure` | Skip TLS certificate verification for the explicit `--base-url` endpoint or a `baseUrl` defined for a user model in `models.json` (self-signed local/internal inference servers). Built-in providers, auth, and model discovery keep normal TLS verification. |
175175 | `--system-prompt <text>` | Replace the default system prompt for this run (also overrides `$ZOT_HOME/SYSTEM.md`). |
176176 | `--append-system-prompt <text>` | Append text to the system prompt (repeatable). |
177-| `--reasoning off\|minimum\|low\|medium\|high\|maximum` | Set thinking level on supported models (default: off). |
177+| `--reasoning off\|minimum\|low\|medium\|high\|xhigh\|max` | Set thinking level on supported models (default: off). `max` is a separate opt-in tier above `xhigh`. |
178178 | `-c`, `--continue` | Resume the latest session for this cwd. |
179179 | `-r`, `--resume` | Pick a session to resume. |
180180 | `--session <path>` | Resume a specific session file. |
@@ -333,7 +333,7 @@ Opens a dialog with every persistent setting. `up`/`down` to navigate, `enter` o
333333 - **auto-swarm** — let the main agent spawn background sub-agents in parallel via a built-in `swarm_spawn` tool. Off by default. When on, the tool is registered with the running agent, the system prompt gains a short addendum telling the model to delegate independent sub-tasks proactively, and zot watches every sub-agent the main agent spawns. As soon as the last sub-agent in a batch finishes its initial task, an `[auto-swarm update]` message is injected back into the chat with each agent's status / task / transcript tail, so the main agent can summarise the collective outcome. Flipping off mid-session removes the tool from the live agent and strips the addendum on the next turn — the model stops trying to delegate. See `/swarm` for the dashboard that lets you monitor, message, kill, or remove the spawned agents.
334334 - **compact transcript rendering**: reduce visual chrome in the chat transcript. Tool calls render as a quiet header plus indented output instead of a bordered box, and sent messages render without padded background bubbles. Off by default. Changes apply immediately and persist to `config.json` as `compact_mode`.
335335 - **TUI settings**: opens a sub-view for input layout and status placement. **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` (`above_input` or `below_input` for the position fields).
336-- **thinking level** — choose reasoning for supported models: off (default; no reasoning), minimum (~1k tokens), low (~2k), medium (~8k), high (~16k), maximum (~32k). The change is persisted to `config.json` and applied to the running agent's next model call.
336+- **thinking 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.
337337 - **color theme** — choose the built-in auto/dark/light theme or any JSON theme discovered under `$ZOT_HOME/themes` or a loaded extension. Theme files can override any subset of UI colors, syntax colors, and spinner frames/messages. Changes apply immediately; if a selected theme file is deleted, zot resets to auto. See [docs/themes.md](docs/themes.md).
338338 - **model shortcuts** — opens a sub-view with nine slots (`model 1` ... `model 9`). `enter` on a slot opens the same `/model` selector and binds the chosen provider/model to that slot; `backspace` clears a slot. Once assigned, press `Ctrl+1` ... `Ctrl+9` from the editor to switch the active model instantly (the same cross-provider swap `/model` performs, transcript and cost carried over). Assigning a shortcut does not change the current model. Shortcuts are skipped while a turn is running.
339339
@@ -530,7 +530,7 @@ Use `/login` and pick **api key** to paste an AI Studio key. zot probes `/v1beta
530530
531531 > **Free-tier rate limits.** AI Studio's free tier has tight per-minute and per-day caps that vary by model: `gemini-2.5-pro` is the strictest (a few requests per minute, ~50 per day), Flash and Flash-Lite are far more generous. If a Pro turn 429s with `"You exceeded your current quota"` while Flash on the same key still works, you've hit the Pro free-tier RPD. Either switch to Flash for agent loops, or [enable billing](https://aistudio.google.com/app/apikey) on your AI Studio project to flip the same key from free to pay-as-you-go pricing (`$1.25/M` input, `$10/M` output for Pro).
532532
533-Reasoning levels (`--reasoning off|minimum|low|medium|high|maximum`, also configurable in `/settings` as **thinking level**) map differently per generation. Budget-based providers use roughly 1k/2k/8k/16k/32k thinking tokens for minimum/low/medium/high/maximum, with provider/model caps applied (Gemini 2.5 Pro caps at 32k; Flash at 24k). Gemini 3.x uses the `thinkingLevel` enum (`MINIMAL`/`LOW`/`MEDIUM`/`HIGH`), with Gemini-3-Pro pinned to `LOW` minimum and `HIGH` for any "medium" or higher request. Effort-based OpenAI-compatible chat providers map minimum to `low`, low/medium directly, and high/maximum to `high`; the Codex/Responses backend maps maximum to `xhigh` where supported. `off` sends no reasoning config. 2.0-family Gemini models have no thinking config at all.
533+Reasoning levels (`--reasoning off|minimum|low|medium|high|xhigh|max`, also configurable in `/settings` as **thinking level**) map differently per generation. `max` is a distinct opt-in tier above `xhigh`. GPT-5.6 and adaptive-thinking Claude models receive native `max`; unsupported providers clamp it to their highest accepted effort. Budget-based providers retain their provider/model caps. Gemini 3.x uses the `thinkingLevel` enum (`MINIMAL`/`LOW`/`MEDIUM`/`HIGH`), with Gemini-3-Pro pinned to `LOW` minimum and `HIGH` for any medium-or-higher request. `off` sends no reasoning config. Gemini 2.0 models have no thinking config.
534534
535535 You can add additional Gemini model IDs to `models.json` under the `google` provider.
536536
@@ -174,7 +174,7 @@ zot --help
174 | `--insecure` | Skip TLS certificate verification for the explicit `--base-url` endpoint or a `baseUrl` defined for a user model in `models.json` (self-signed local/internal inference servers). Built-in providers, auth, and model discovery keep normal TLS verification. |174 | `--insecure` | Skip TLS certificate verification for the explicit `--base-url` endpoint or a `baseUrl` defined for a user model in `models.json` (self-signed local/internal inference servers). Built-in providers, auth, and model discovery keep normal TLS verification. |
175 | `--system-prompt <text>` | Replace the default system prompt for this run (also overrides `$ZOT_HOME/SYSTEM.md`). |175 | `--system-prompt <text>` | Replace the default system prompt for this run (also overrides `$ZOT_HOME/SYSTEM.md`). |
176 | `--append-system-prompt <text>` | Append text to the system prompt (repeatable). |176 | `--append-system-prompt <text>` | Append text to the system prompt (repeatable). |
177-| `--reasoning off\|minimum\|low\|medium\|high\|maximum` | Set thinking level on supported models (default: off). |177+| `--reasoning off\|minimum\|low\|medium\|high\|xhigh\|max` | Set thinking level on supported models (default: off). `max` is a separate opt-in tier above `xhigh`. |
178 | `-c`, `--continue` | Resume the latest session for this cwd. |178 | `-c`, `--continue` | Resume the latest session for this cwd. |
179 | `-r`, `--resume` | Pick a session to resume. |179 | `-r`, `--resume` | Pick a session to resume. |
180 | `--session <path>` | Resume a specific session file. |180 | `--session <path>` | Resume a specific session file. |
@@ -333,7 +333,7 @@ Opens a dialog with every persistent setting. `up`/`down` to navigate, `enter` o
333 - **auto-swarm** — let the main agent spawn background sub-agents in parallel via a built-in `swarm_spawn` tool. Off by default. When on, the tool is registered with the running agent, the system prompt gains a short addendum telling the model to delegate independent sub-tasks proactively, and zot watches every sub-agent the main agent spawns. As soon as the last sub-agent in a batch finishes its initial task, an `[auto-swarm update]` message is injected back into the chat with each agent's status / task / transcript tail, so the main agent can summarise the collective outcome. Flipping off mid-session removes the tool from the live agent and strips the addendum on the next turn — the model stops trying to delegate. See `/swarm` for the dashboard that lets you monitor, message, kill, or remove the spawned agents.333 - **auto-swarm** — let the main agent spawn background sub-agents in parallel via a built-in `swarm_spawn` tool. Off by default. When on, the tool is registered with the running agent, the system prompt gains a short addendum telling the model to delegate independent sub-tasks proactively, and zot watches every sub-agent the main agent spawns. As soon as the last sub-agent in a batch finishes its initial task, an `[auto-swarm update]` message is injected back into the chat with each agent's status / task / transcript tail, so the main agent can summarise the collective outcome. Flipping off mid-session removes the tool from the live agent and strips the addendum on the next turn — the model stops trying to delegate. See `/swarm` for the dashboard that lets you monitor, message, kill, or remove the spawned agents.
334 - **compact transcript rendering**: reduce visual chrome in the chat transcript. Tool calls render as a quiet header plus indented output instead of a bordered box, and sent messages render without padded background bubbles. Off by default. Changes apply immediately and persist to `config.json` as `compact_mode`.334 - **compact transcript rendering**: reduce visual chrome in the chat transcript. Tool calls render as a quiet header plus indented output instead of a bordered box, and sent messages render without padded background bubbles. Off by default. Changes apply immediately and persist to `config.json` as `compact_mode`.
335 - **TUI settings**: opens a sub-view for input layout and status placement. **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` (`above_input` or `below_input` for the position fields).335 - **TUI settings**: opens a sub-view for input layout and status placement. **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` (`above_input` or `below_input` for the position fields).
336-- **thinking level** — choose reasoning for supported models: off (default; no reasoning), minimum (~1k tokens), low (~2k), medium (~8k), high (~16k), maximum (~32k). The change is persisted to `config.json` and applied to the running agent's next model call.336+- **thinking 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.
337 - **color theme** — choose the built-in auto/dark/light theme or any JSON theme discovered under `$ZOT_HOME/themes` or a loaded extension. Theme files can override any subset of UI colors, syntax colors, and spinner frames/messages. Changes apply immediately; if a selected theme file is deleted, zot resets to auto. See [docs/themes.md](docs/themes.md).337 - **color theme** — choose the built-in auto/dark/light theme or any JSON theme discovered under `$ZOT_HOME/themes` or a loaded extension. Theme files can override any subset of UI colors, syntax colors, and spinner frames/messages. Changes apply immediately; if a selected theme file is deleted, zot resets to auto. See [docs/themes.md](docs/themes.md).
338 - **model shortcuts** — opens a sub-view with nine slots (`model 1` ... `model 9`). `enter` on a slot opens the same `/model` selector and binds the chosen provider/model to that slot; `backspace` clears a slot. Once assigned, press `Ctrl+1` ... `Ctrl+9` from the editor to switch the active model instantly (the same cross-provider swap `/model` performs, transcript and cost carried over). Assigning a shortcut does not change the current model. Shortcuts are skipped while a turn is running.338 - **model shortcuts** — opens a sub-view with nine slots (`model 1` ... `model 9`). `enter` on a slot opens the same `/model` selector and binds the chosen provider/model to that slot; `backspace` clears a slot. Once assigned, press `Ctrl+1` ... `Ctrl+9` from the editor to switch the active model instantly (the same cross-provider swap `/model` performs, transcript and cost carried over). Assigning a shortcut does not change the current model. Shortcuts are skipped while a turn is running.
339 339
@@ -530,7 +530,7 @@ Use `/login` and pick **api key** to paste an AI Studio key. zot probes `/v1beta
530 530
531 > **Free-tier rate limits.** AI Studio's free tier has tight per-minute and per-day caps that vary by model: `gemini-2.5-pro` is the strictest (a few requests per minute, ~50 per day), Flash and Flash-Lite are far more generous. If a Pro turn 429s with `"You exceeded your current quota"` while Flash on the same key still works, you've hit the Pro free-tier RPD. Either switch to Flash for agent loops, or [enable billing](https://aistudio.google.com/app/apikey) on your AI Studio project to flip the same key from free to pay-as-you-go pricing (`$1.25/M` input, `$10/M` output for Pro).531 > **Free-tier rate limits.** AI Studio's free tier has tight per-minute and per-day caps that vary by model: `gemini-2.5-pro` is the strictest (a few requests per minute, ~50 per day), Flash and Flash-Lite are far more generous. If a Pro turn 429s with `"You exceeded your current quota"` while Flash on the same key still works, you've hit the Pro free-tier RPD. Either switch to Flash for agent loops, or [enable billing](https://aistudio.google.com/app/apikey) on your AI Studio project to flip the same key from free to pay-as-you-go pricing (`$1.25/M` input, `$10/M` output for Pro).
532 532
533-Reasoning levels (`--reasoning off|minimum|low|medium|high|maximum`, also configurable in `/settings` as **thinking level**) map differently per generation. Budget-based providers use roughly 1k/2k/8k/16k/32k thinking tokens for minimum/low/medium/high/maximum, with provider/model caps applied (Gemini 2.5 Pro caps at 32k; Flash at 24k). Gemini 3.x uses the `thinkingLevel` enum (`MINIMAL`/`LOW`/`MEDIUM`/`HIGH`), with Gemini-3-Pro pinned to `LOW` minimum and `HIGH` for any "medium" or higher request. Effort-based OpenAI-compatible chat providers map minimum to `low`, low/medium directly, and high/maximum to `high`; the Codex/Responses backend maps maximum to `xhigh` where supported. `off` sends no reasoning config. 2.0-family Gemini models have no thinking config at all.533+Reasoning levels (`--reasoning off|minimum|low|medium|high|xhigh|max`, also configurable in `/settings` as **thinking level**) map differently per generation. `max` is a distinct opt-in tier above `xhigh`. GPT-5.6 and adaptive-thinking Claude models receive native `max`; unsupported providers clamp it to their highest accepted effort. Budget-based providers retain their provider/model caps. Gemini 3.x uses the `thinkingLevel` enum (`MINIMAL`/`LOW`/`MEDIUM`/`HIGH`), with Gemini-3-Pro pinned to `LOW` minimum and `HIGH` for any medium-or-higher request. `off` sends no reasoning config. Gemini 2.0 models have no thinking config.
534 534
535 You can add additional Gemini model IDs to `models.json` under the `google` provider.535 You can add additional Gemini model IDs to `models.json` under the `google` provider.
536 536
M docs/rpc.md +10 -0
@@ -153,6 +153,16 @@ Switch model within the same provider.
153153
154154 Cross-provider swaps require relaunching `zot rpc` with the new `--provider`.
155155
156+### `set_reasoning`
157+
158+Set the thinking level for subsequent prompts without restarting the process. Accepted values are `off`, `minimum`, `low`, `medium`, `high`, `xhigh`, and `max`.
159+
160+```json
161+{"id":"8","type":"set_reasoning","reasoning":"max"}
162+```
163+
164+The `max` tier is sent natively to supported models and clamped for other backends.
165+
156166 ### `get_models`
157167
158168 List models known for the current provider.
@@ -153,6 +153,16 @@ Switch model within the same provider.
153 153
154 Cross-provider swaps require relaunching `zot rpc` with the new `--provider`.154 Cross-provider swaps require relaunching `zot rpc` with the new `--provider`.
155 155
156+### `set_reasoning`
157+
158+Set the thinking level for subsequent prompts without restarting the process. Accepted values are `off`, `minimum`, `low`, `medium`, `high`, `xhigh`, and `max`.
159+
160+```json
161+{"id":"8","type":"set_reasoning","reasoning":"max"}
162+```
163+
164+The `max` tier is sent natively to supported models and clamped for other backends.
165+
156 ### `get_models`166 ### `get_models`
157 167
158 List models known for the current provider.168 List models known for the current provider.
M docs/themes.md +1 -0
@@ -191,6 +191,7 @@ Most color fields are xterm-256 indexes (`0`–`255`).
191191 - `error` — errors, refused calls, diff deletions.
192192 - `warning` — warnings and high context-usage state.
193193 - `spinner` — reserved spinner color slot.
194+- `thinkingMax` or `thinking_max` — status color used when the opt-in `max` thinking level is active. Missing values inherit the built-in theme color.
194195 - `selection_bg` — highlighted row background.
195196 - `selection_fg` — highlighted row foreground.
196197
@@ -191,6 +191,7 @@ Most color fields are xterm-256 indexes (`0`–`255`).
191 - `error` — errors, refused calls, diff deletions.191 - `error` — errors, refused calls, diff deletions.
192 - `warning` — warnings and high context-usage state.192 - `warning` — warnings and high context-usage state.
193 - `spinner` — reserved spinner color slot.193 - `spinner` — reserved spinner color slot.
194+- `thinkingMax` or `thinking_max` — status color used when the opt-in `max` thinking level is active. Missing values inherit the built-in theme color.
194 - `selection_bg` — highlighted row background.195 - `selection_bg` — highlighted row background.
195 - `selection_fg` — highlighted row foreground.196 - `selection_fg` — highlighted row foreground.
196 197
M packages/agent/args.go +3 -3
@@ -203,10 +203,10 @@ func ParseArgs(in []string) (Args, error) {
203203 return a, err
204204 }
205205 switch strings.ToLower(v) {
206- case "", "off", "minimum", "minimal", "low", "medium", "high", "maximum", "max":
206+ case "", "off", "minimum", "minimal", "low", "medium", "high", "xhigh", "maximum", "max":
207207 a.Reasoning = strings.ToLower(v)
208208 default:
209- return a, fmt.Errorf("--reasoning must be off|minimum|low|medium|high|maximum")
209+ return a, fmt.Errorf("--reasoning must be off|minimum|low|medium|high|xhigh|max")
210210 }
211211 case "--temperature":
212212 v, err := want(&i, arg)
@@ -379,7 +379,7 @@ func PrintHelp(version string) {
379379 row{"--api-key KEY", "api key for this run (env / auth.json fallback)"},
380380 row{"--base-url URL", "override provider api base url"},
381381 row{"--insecure", "skip TLS certificate verification (for self-signed-cert endpoints)"},
382- row{"--reasoning off|minimum|low|medium|high|maximum", "set thinking level on supported models"},
382+ row{"--reasoning off|minimum|low|medium|high|xhigh|max", "set thinking level on supported models"},
383383 row{"--temperature N", "sampling temperature, 0 to 2 (omit for provider default)"},
384384 )
385385 section("prompt and session flags",
@@ -203,10 +203,10 @@ func ParseArgs(in []string) (Args, error) {
203 return a, err203 return a, err
204 }204 }
205 switch strings.ToLower(v) {205 switch strings.ToLower(v) {
206- case "", "off", "minimum", "minimal", "low", "medium", "high", "maximum", "max":206+ case "", "off", "minimum", "minimal", "low", "medium", "high", "xhigh", "maximum", "max":
207 a.Reasoning = strings.ToLower(v)207 a.Reasoning = strings.ToLower(v)
208 default:208 default:
209- return a, fmt.Errorf("--reasoning must be off|minimum|low|medium|high|maximum")209+ return a, fmt.Errorf("--reasoning must be off|minimum|low|medium|high|xhigh|max")
210 }210 }
211 case "--temperature":211 case "--temperature":
212 v, err := want(&i, arg)212 v, err := want(&i, arg)
@@ -379,7 +379,7 @@ func PrintHelp(version string) {
379 row{"--api-key KEY", "api key for this run (env / auth.json fallback)"},379 row{"--api-key KEY", "api key for this run (env / auth.json fallback)"},
380 row{"--base-url URL", "override provider api base url"},380 row{"--base-url URL", "override provider api base url"},
381 row{"--insecure", "skip TLS certificate verification (for self-signed-cert endpoints)"},381 row{"--insecure", "skip TLS certificate verification (for self-signed-cert endpoints)"},
382- row{"--reasoning off|minimum|low|medium|high|maximum", "set thinking level on supported models"},382+ row{"--reasoning off|minimum|low|medium|high|xhigh|max", "set thinking level on supported models"},
383 row{"--temperature N", "sampling temperature, 0 to 2 (omit for provider default)"},383 row{"--temperature N", "sampling temperature, 0 to 2 (omit for provider default)"},
384 )384 )
385 section("prompt and session flags",385 section("prompt and session flags",
M packages/agent/modes/interactive.go +2 -1
@@ -2927,7 +2927,8 @@ func (i *Interactive) openSettingsDialog() {
29272927 {value: "low", label: "low", desc: "light (~2k tokens)"},
29282928 {value: "medium", label: "medium", desc: "moderate (~8k tokens)"},
29292929 {value: "high", label: "high", desc: "deep (~16k tokens)"},
2930- {value: "maximum", label: "maximum", desc: "highest (~32k tokens)"},
2930+ {value: "xhigh", label: "xhigh", desc: "extra-high effort"},
2931+ {value: "max", label: "max", desc: "unconstrained effort on supported models"},
29312932 }
29322933 reasoning := provider.NormalizeReasoning(i.cfg.Reasoning)
29332934 reasoningChoice := 0
@@ -2927,7 +2927,8 @@ func (i *Interactive) openSettingsDialog() {
2927 {value: "low", label: "low", desc: "light (~2k tokens)"},2927 {value: "low", label: "low", desc: "light (~2k tokens)"},
2928 {value: "medium", label: "medium", desc: "moderate (~8k tokens)"},2928 {value: "medium", label: "medium", desc: "moderate (~8k tokens)"},
2929 {value: "high", label: "high", desc: "deep (~16k tokens)"},2929 {value: "high", label: "high", desc: "deep (~16k tokens)"},
2930- {value: "maximum", label: "maximum", desc: "highest (~32k tokens)"},2930+ {value: "xhigh", label: "xhigh", desc: "extra-high effort"},
2931+ {value: "max", label: "max", desc: "unconstrained effort on supported models"},
2931 }2932 }
2932 reasoning := provider.NormalizeReasoning(i.cfg.Reasoning)2933 reasoning := provider.NormalizeReasoning(i.cfg.Reasoning)
2933 reasoningChoice := 02934 reasoningChoice := 0
M packages/agent/rpc.go +20 -0
@@ -30,6 +30,7 @@ import (
3030 // {"id":"3","type":"compact"}
3131 // {"id":"4","type":"get_state"}
3232 // {"id":"5","type":"set_model","model":"claude-opus-4-5"}
33+// {"id":"6","type":"set_reasoning","reasoning":"max"}
3334 // {"id":"6","type":"get_messages"}
3435 // {"id":"7","type":"clear"}
3536 // {"id":"8","type":"get_models"}
@@ -301,6 +302,24 @@ func (s *rpcServer) dispatch(cmd, id string, raw []byte) {
301302 s.model = req.Model
302303 s.writeResponse(id, cmd, map[string]any{"model": req.Model})
303304
305+ case "set_reasoning":
306+ var req struct {
307+ Reasoning string `json:"reasoning"`
308+ }
309+ if err := json.Unmarshal(raw, &req); err != nil {
310+ s.writeError(id, cmd, err.Error())
311+ return
312+ }
313+ reasoning := provider.NormalizeReasoning(req.Reasoning)
314+ switch reasoning {
315+ case "", "minimum", "low", "medium", "high", "xhigh", "max":
316+ default:
317+ s.writeError(id, cmd, "reasoning must be off|minimum|low|medium|high|xhigh|max")
318+ return
319+ }
320+ s.agent.Reasoning = reasoning
321+ s.writeResponse(id, cmd, map[string]any{"reasoning": reasoning})
322+
304323 case "get_models":
305324 out := []map[string]any{}
306325 for _, m := range provider.ModelsForProvider(s.provider) {
@@ -388,6 +407,7 @@ func (s *rpcServer) snapshotState() map[string]any {
388407 return map[string]any{
389408 "provider": s.provider,
390409 "model": s.model,
410+ "reasoning": s.agent.Reasoning,
391411 "cwd": s.args.CWD,
392412 "message_count": len(s.agent.Messages()),
393413 "busy": s.activeCancel != nil,
@@ -30,6 +30,7 @@ import (
30 // {"id":"3","type":"compact"}30 // {"id":"3","type":"compact"}
31 // {"id":"4","type":"get_state"}31 // {"id":"4","type":"get_state"}
32 // {"id":"5","type":"set_model","model":"claude-opus-4-5"}32 // {"id":"5","type":"set_model","model":"claude-opus-4-5"}
33+// {"id":"6","type":"set_reasoning","reasoning":"max"}
33 // {"id":"6","type":"get_messages"}34 // {"id":"6","type":"get_messages"}
34 // {"id":"7","type":"clear"}35 // {"id":"7","type":"clear"}
35 // {"id":"8","type":"get_models"}36 // {"id":"8","type":"get_models"}
@@ -301,6 +302,24 @@ func (s *rpcServer) dispatch(cmd, id string, raw []byte) {
301 s.model = req.Model302 s.model = req.Model
302 s.writeResponse(id, cmd, map[string]any{"model": req.Model})303 s.writeResponse(id, cmd, map[string]any{"model": req.Model})
303 304
305+ case "set_reasoning":
306+ var req struct {
307+ Reasoning string `json:"reasoning"`
308+ }
309+ if err := json.Unmarshal(raw, &req); err != nil {
310+ s.writeError(id, cmd, err.Error())
311+ return
312+ }
313+ reasoning := provider.NormalizeReasoning(req.Reasoning)
314+ switch reasoning {
315+ case "", "minimum", "low", "medium", "high", "xhigh", "max":
316+ default:
317+ s.writeError(id, cmd, "reasoning must be off|minimum|low|medium|high|xhigh|max")
318+ return
319+ }
320+ s.agent.Reasoning = reasoning
321+ s.writeResponse(id, cmd, map[string]any{"reasoning": reasoning})
322+
304 case "get_models":323 case "get_models":
305 out := []map[string]any{}324 out := []map[string]any{}
306 for _, m := range provider.ModelsForProvider(s.provider) {325 for _, m := range provider.ModelsForProvider(s.provider) {
@@ -388,6 +407,7 @@ func (s *rpcServer) snapshotState() map[string]any {
388 return map[string]any{407 return map[string]any{
389 "provider": s.provider,408 "provider": s.provider,
390 "model": s.model,409 "model": s.model,
410+ "reasoning": s.agent.Reasoning,
391 "cwd": s.args.CWD,411 "cwd": s.args.CWD,
392 "message_count": len(s.agent.Messages()),412 "message_count": len(s.agent.Messages()),
393 "busy": s.activeCancel != nil,413 "busy": s.activeCancel != nil,
A packages/agent/rpc_reasoning_test.go +35 -0
@@ -0,0 +1,35 @@
1+package agent
2+
3+import (
4+ "bytes"
5+ "strings"
6+ "testing"
7+
8+ "github.com/patriceckhart/zot/packages/core"
9+)
10+
11+func TestRPCSetReasoningMax(t *testing.T) {
12+ var out bytes.Buffer
13+ s := &rpcServer{agent: &core.Agent{}, out: &out}
14+ s.dispatch("set_reasoning", "1", []byte(`{"reasoning":"max"}`))
15+
16+ if s.agent.Reasoning != "max" {
17+ t.Fatalf("reasoning = %q, want max", s.agent.Reasoning)
18+ }
19+ if !strings.Contains(out.String(), `"reasoning":"max"`) {
20+ t.Fatalf("response = %q", out.String())
21+ }
22+}
23+
24+func TestRPCSetReasoningRejectsUnknownLevel(t *testing.T) {
25+ var out bytes.Buffer
26+ s := &rpcServer{agent: &core.Agent{}, out: &out}
27+ s.dispatch("set_reasoning", "1", []byte(`{"reasoning":"extreme"}`))
28+
29+ if s.agent.Reasoning != "" {
30+ t.Fatalf("reasoning = %q, want unchanged", s.agent.Reasoning)
31+ }
32+ if !strings.Contains(out.String(), `"success":false`) {
33+ t.Fatalf("response = %q", out.String())
34+ }
35+}
@@ -0,0 +1,35 @@
1+package agent
2+
3+import (
4+ "bytes"
5+ "strings"
6+ "testing"
7+
8+ "github.com/patriceckhart/zot/packages/core"
9+)
10+
11+func TestRPCSetReasoningMax(t *testing.T) {
12+ var out bytes.Buffer
13+ s := &rpcServer{agent: &core.Agent{}, out: &out}
14+ s.dispatch("set_reasoning", "1", []byte(`{"reasoning":"max"}`))
15+
16+ if s.agent.Reasoning != "max" {
17+ t.Fatalf("reasoning = %q, want max", s.agent.Reasoning)
18+ }
19+ if !strings.Contains(out.String(), `"reasoning":"max"`) {
20+ t.Fatalf("response = %q", out.String())
21+ }
22+}
23+
24+func TestRPCSetReasoningRejectsUnknownLevel(t *testing.T) {
25+ var out bytes.Buffer
26+ s := &rpcServer{agent: &core.Agent{}, out: &out}
27+ s.dispatch("set_reasoning", "1", []byte(`{"reasoning":"extreme"}`))
28+
29+ if s.agent.Reasoning != "" {
30+ t.Fatalf("reasoning = %q, want unchanged", s.agent.Reasoning)
31+ }
32+ if !strings.Contains(out.String(), `"success":false`) {
33+ t.Fatalf("response = %q", out.String())
34+ }
35+}
A packages/agent/sdk/reasoning_test.go +24 -0
@@ -0,0 +1,24 @@
1+package sdk
2+
3+import (
4+ "testing"
5+
6+ "github.com/patriceckhart/zot/packages/core"
7+)
8+
9+func TestRuntimeSetReasoningMax(t *testing.T) {
10+ r := &Runtime{agent: &core.Agent{}}
11+ if err := r.SetReasoning("max"); err != nil {
12+ t.Fatal(err)
13+ }
14+ if r.agent.Reasoning != "max" {
15+ t.Fatalf("reasoning = %q, want max", r.agent.Reasoning)
16+ }
17+}
18+
19+func TestRuntimeSetReasoningRejectsUnknownLevel(t *testing.T) {
20+ r := &Runtime{agent: &core.Agent{}}
21+ if err := r.SetReasoning("extreme"); err == nil {
22+ t.Fatal("expected invalid reasoning error")
23+ }
24+}
@@ -0,0 +1,24 @@
1+package sdk
2+
3+import (
4+ "testing"
5+
6+ "github.com/patriceckhart/zot/packages/core"
7+)
8+
9+func TestRuntimeSetReasoningMax(t *testing.T) {
10+ r := &Runtime{agent: &core.Agent{}}
11+ if err := r.SetReasoning("max"); err != nil {
12+ t.Fatal(err)
13+ }
14+ if r.agent.Reasoning != "max" {
15+ t.Fatalf("reasoning = %q, want max", r.agent.Reasoning)
16+ }
17+}
18+
19+func TestRuntimeSetReasoningRejectsUnknownLevel(t *testing.T) {
20+ r := &Runtime{agent: &core.Agent{}}
21+ if err := r.SetReasoning("extreme"); err == nil {
22+ t.Fatal("expected invalid reasoning error")
23+ }
24+}
M packages/agent/sdk/sdk.go +19 -1
@@ -58,7 +58,7 @@ type Config struct {
5858 AppendSystemPrompt []string
5959
6060 // Reasoning sets the reasoning effort for models that support it
61- // ("low", "medium", "high"). Empty = no reasoning.
61+ // ("minimum", "low", "medium", "high", "xhigh", "max"). Empty disables it.
6262 Reasoning string
6363
6464 // Temperature sets the sampling temperature. Nil = provider default.
@@ -296,6 +296,23 @@ func (r *Runtime) SetModel(model string) error {
296296 return nil
297297 }
298298
299+// SetReasoning changes the thinking level used by subsequent prompts.
300+func (r *Runtime) SetReasoning(level string) error {
301+ r.mu.Lock()
302+ defer r.mu.Unlock()
303+ if r.agent == nil {
304+ return fmt.Errorf("sdk: no agent")
305+ }
306+ reasoning := provider.NormalizeReasoning(level)
307+ switch reasoning {
308+ case "", "minimum", "low", "medium", "high", "xhigh", "max":
309+ r.agent.Reasoning = reasoning
310+ return nil
311+ default:
312+ return fmt.Errorf("sdk: reasoning must be off|minimum|low|medium|high|xhigh|max")
313+ }
314+}
315+
299316 // State returns a snapshot of the runtime's current state. Useful for
300317 // driving UIs.
301318 func (r *Runtime) State() State {
@@ -304,6 +321,7 @@ func (r *Runtime) State() State {
304321 return State{
305322 Provider: r.provider,
306323 Model: r.model,
324+ Reasoning: r.agent.Reasoning,
307325 CWD: r.cwd,
308326 Busy: r.activeCancel != nil,
309327 MessageCount: len(r.Messages()),
@@ -58,7 +58,7 @@ type Config struct {
58 AppendSystemPrompt []string58 AppendSystemPrompt []string
59 59
60 // Reasoning sets the reasoning effort for models that support it60 // Reasoning sets the reasoning effort for models that support it
61- // ("low", "medium", "high"). Empty = no reasoning.61+ // ("minimum", "low", "medium", "high", "xhigh", "max"). Empty disables it.
62 Reasoning string62 Reasoning string
63 63
64 // Temperature sets the sampling temperature. Nil = provider default.64 // Temperature sets the sampling temperature. Nil = provider default.
@@ -296,6 +296,23 @@ func (r *Runtime) SetModel(model string) error {
296 return nil296 return nil
297 }297 }
298 298
299+// SetReasoning changes the thinking level used by subsequent prompts.
300+func (r *Runtime) SetReasoning(level string) error {
301+ r.mu.Lock()
302+ defer r.mu.Unlock()
303+ if r.agent == nil {
304+ return fmt.Errorf("sdk: no agent")
305+ }
306+ reasoning := provider.NormalizeReasoning(level)
307+ switch reasoning {
308+ case "", "minimum", "low", "medium", "high", "xhigh", "max":
309+ r.agent.Reasoning = reasoning
310+ return nil
311+ default:
312+ return fmt.Errorf("sdk: reasoning must be off|minimum|low|medium|high|xhigh|max")
313+ }
314+}
315+
299 // State returns a snapshot of the runtime's current state. Useful for316 // State returns a snapshot of the runtime's current state. Useful for
300 // driving UIs.317 // driving UIs.
301 func (r *Runtime) State() State {318 func (r *Runtime) State() State {
@@ -304,6 +321,7 @@ func (r *Runtime) State() State {
304 return State{321 return State{
305 Provider: r.provider,322 Provider: r.provider,
306 Model: r.model,323 Model: r.model,
324+ Reasoning: r.agent.Reasoning,
307 CWD: r.cwd,325 CWD: r.cwd,
308 Busy: r.activeCancel != nil,326 Busy: r.activeCancel != nil,
309 MessageCount: len(r.Messages()),327 MessageCount: len(r.Messages()),
M packages/agent/sdk/types.go +1 -0
@@ -94,6 +94,7 @@ type Usage struct {
9494 type State struct {
9595 Provider string `json:"provider"`
9696 Model string `json:"model"`
97+ Reasoning string `json:"reasoning"`
9798 CWD string `json:"cwd"`
9899 Busy bool `json:"busy"`
99100 MessageCount int `json:"message_count"`
@@ -94,6 +94,7 @@ type Usage struct {
94 type State struct {94 type State struct {
95 Provider string `json:"provider"`95 Provider string `json:"provider"`
96 Model string `json:"model"`96 Model string `json:"model"`
97+ Reasoning string `json:"reasoning"`
97 CWD string `json:"cwd"`98 CWD string `json:"cwd"`
98 Busy bool `json:"busy"`99 Busy bool `json:"busy"`
99 MessageCount int `json:"message_count"`100 MessageCount int `json:"message_count"`
M packages/provider/gemini.go +1 -1
@@ -413,7 +413,7 @@ func geminiThinkingConfig(modelID, level string) *gemThinkingConfig {
413413 } else {
414414 lvl = "MEDIUM"
415415 }
416- case "high", "maximum":
416+ case "high", "xhigh", "max":
417417 lvl = "HIGH"
418418 default:
419419 return nil
@@ -413,7 +413,7 @@ func geminiThinkingConfig(modelID, level string) *gemThinkingConfig {
413 } else {413 } else {
414 lvl = "MEDIUM"414 lvl = "MEDIUM"
415 }415 }
416- case "high", "maximum":416+ case "high", "xhigh", "max":
417 lvl = "HIGH"417 lvl = "HIGH"
418 default:418 default:
419 return nil419 return nil
M packages/provider/openai_codex.go +1 -1
@@ -174,7 +174,7 @@ func (c *codexClient) buildRequest(req Request) (*codexRequest, error) {
174174 Include: []string{"reasoning.encrypted_content"},
175175 }
176176 if m.Reasoning {
177- if effort := OpenAICodexReasoningEffort(req.Reasoning); effort != "" {
177+ if effort := OpenAICodexReasoningEffort(req.Reasoning, req.Model); effort != "" {
178178 body.Reasoning = &codexReasoningConfig{Effort: effort}
179179 }
180180 }
@@ -174,7 +174,7 @@ func (c *codexClient) buildRequest(req Request) (*codexRequest, error) {
174 Include: []string{"reasoning.encrypted_content"},174 Include: []string{"reasoning.encrypted_content"},
175 }175 }
176 if m.Reasoning {176 if m.Reasoning {
177- if effort := OpenAICodexReasoningEffort(req.Reasoning); effort != "" {177+ if effort := OpenAICodexReasoningEffort(req.Reasoning, req.Model); effort != "" {
178 body.Reasoning = &codexReasoningConfig{Effort: effort}178 body.Reasoning = &codexReasoningConfig{Effort: effort}
179 }179 }
180 }180 }
M packages/provider/openai_codex_test.go +11 -0
@@ -114,6 +114,17 @@ func TestCodexPreviewModelUsesCodexCLIShape(t *testing.T) {
114114 }
115115 }
116116
117+func TestGPT56UsesNativeMaxReasoningEffort(t *testing.T) {
118+ c := NewOpenAICodex("token", "acct", "").(*codexClient)
119+ wire, err := c.buildRequest(Request{Model: "gpt-5.6-sol", Reasoning: "max"})
120+ if err != nil {
121+ t.Fatal(err)
122+ }
123+ if wire.Reasoning == nil || wire.Reasoning.Effort != "max" {
124+ t.Fatalf("reasoning = %+v", wire.Reasoning)
125+ }
126+}
127+
117128 func TestOpenAIGPT56DoesNotUseCodexCLIRouting(t *testing.T) {
118129 named := NewOpenAIResponsesNamed("token", "https://example.test/v1/responses", "openai").(*renamedClient)
119130 c := named.inner.(*codexClient)
@@ -114,6 +114,17 @@ func TestCodexPreviewModelUsesCodexCLIShape(t *testing.T) {
114 }114 }
115 }115 }
116 116
117+func TestGPT56UsesNativeMaxReasoningEffort(t *testing.T) {
118+ c := NewOpenAICodex("token", "acct", "").(*codexClient)
119+ wire, err := c.buildRequest(Request{Model: "gpt-5.6-sol", Reasoning: "max"})
120+ if err != nil {
121+ t.Fatal(err)
122+ }
123+ if wire.Reasoning == nil || wire.Reasoning.Effort != "max" {
124+ t.Fatalf("reasoning = %+v", wire.Reasoning)
125+ }
126+}
127+
117 func TestOpenAIGPT56DoesNotUseCodexCLIRouting(t *testing.T) {128 func TestOpenAIGPT56DoesNotUseCodexCLIRouting(t *testing.T) {
118 named := NewOpenAIResponsesNamed("token", "https://example.test/v1/responses", "openai").(*renamedClient)129 named := NewOpenAIResponsesNamed("token", "https://example.test/v1/responses", "openai").(*renamedClient)
119 c := named.inner.(*codexClient)130 c := named.inner.(*codexClient)
M packages/provider/provider.go +3 -4
@@ -216,10 +216,9 @@ type Request struct {
216216 Tools []Tool
217217 MaxTokens int
218218 Temperature *float32
219- // Reasoning is "", "minimum", "low", "medium", "high", or "maximum".
220- // Empty disables reasoning. Budget-based providers map these to roughly
221- // 1k/2k/8k/16k/32k thinking tokens; effort-based providers map them onto
222- // their closest supported reasoning_effort values.
219+ // Reasoning is "", "minimum", "low", "medium", "high", "xhigh", or
220+ // "max". Empty disables reasoning. The max tier is sent natively only to
221+ // models that support it and clamped elsewhere.
223222 Reasoning string
224223 }
225224
@@ -216,10 +216,9 @@ type Request struct {
216 Tools []Tool216 Tools []Tool
217 MaxTokens int217 MaxTokens int
218 Temperature *float32218 Temperature *float32
219- // Reasoning is "", "minimum", "low", "medium", "high", or "maximum".219+ // Reasoning is "", "minimum", "low", "medium", "high", "xhigh", or
220- // Empty disables reasoning. Budget-based providers map these to roughly220+ // "max". Empty disables reasoning. The max tier is sent natively only to
221- // 1k/2k/8k/16k/32k thinking tokens; effort-based providers map them onto221+ // models that support it and clamped elsewhere.
222- // their closest supported reasoning_effort values.
223 Reasoning string222 Reasoning string
224 }223 }
225 224
M packages/provider/provider_test.go +13 -0
@@ -118,6 +118,19 @@ func TestAnthropicAdaptiveThinking(t *testing.T) {
118118 t.Fatalf("want effort=xhigh, got %+v", wire.OutputConfig)
119119 }
120120
121+ // max is a separate native tier above xhigh on adaptive models.
122+ wire, err = c.buildRequest(Request{
123+ Model: "claude-opus-4-8",
124+ Reasoning: "max",
125+ Messages: []Message{{Role: RoleUser, Content: []Content{TextBlock{Text: "hi"}}}},
126+ })
127+ if err != nil {
128+ t.Fatal(err)
129+ }
130+ if wire.OutputConfig == nil || wire.OutputConfig.Effort != "max" {
131+ t.Fatalf("want effort=max, got %+v", wire.OutputConfig)
132+ }
133+
121134 // Sonnet 5 -> adaptive thinking, effort set, no budget, no temperature.
122135 wire, err = c.buildRequest(Request{
123136 Model: "claude-sonnet-5",
@@ -118,6 +118,19 @@ func TestAnthropicAdaptiveThinking(t *testing.T) {
118 t.Fatalf("want effort=xhigh, got %+v", wire.OutputConfig)118 t.Fatalf("want effort=xhigh, got %+v", wire.OutputConfig)
119 }119 }
120 120
121+ // max is a separate native tier above xhigh on adaptive models.
122+ wire, err = c.buildRequest(Request{
123+ Model: "claude-opus-4-8",
124+ Reasoning: "max",
125+ Messages: []Message{{Role: RoleUser, Content: []Content{TextBlock{Text: "hi"}}}},
126+ })
127+ if err != nil {
128+ t.Fatal(err)
129+ }
130+ if wire.OutputConfig == nil || wire.OutputConfig.Effort != "max" {
131+ t.Fatalf("want effort=max, got %+v", wire.OutputConfig)
132+ }
133+
121 // Sonnet 5 -> adaptive thinking, effort set, no budget, no temperature.134 // Sonnet 5 -> adaptive thinking, effort set, no budget, no temperature.
122 wire, err = c.buildRequest(Request{135 wire, err = c.buildRequest(Request{
123 Model: "claude-sonnet-5",136 Model: "claude-sonnet-5",
M packages/provider/reasoning.go +31 -28
@@ -3,7 +3,8 @@ package provider
33 import "strings"
44
55 // NormalizeReasoning canonicalizes zot's user-facing thinking levels.
6-// Empty string means reasoning/thinking is disabled.
6+// Empty string means reasoning/thinking is disabled. "maximum" remains
7+// an alias for xhigh; "max" is the separate opt-in tier above it.
78 func NormalizeReasoning(level string) string {
89 switch strings.ToLower(strings.TrimSpace(level)) {
910 case "", "off", "none", "no", "false", "disabled":
@@ -16,8 +17,10 @@ func NormalizeReasoning(level string) string {
1617 return "medium"
1718 case "hi", "high":
1819 return "high"
19- case "max", "maximum":
20- return "maximum"
20+ case "xhigh", "maximum":
21+ return "xhigh"
22+ case "max":
23+ return "max"
2124 default:
2225 return strings.ToLower(strings.TrimSpace(level))
2326 }
@@ -35,7 +38,7 @@ func ReasoningBudget(level string) int {
3538 return 8192
3639 case "high":
3740 return 16384
38- case "maximum":
41+ case "xhigh", "max":
3942 return 32768
4043 default:
4144 return 0
@@ -43,10 +46,8 @@ func ReasoningBudget(level string) int {
4346 }
4447
4548 // AnthropicAdaptiveEffort maps zot's user-facing thinking levels onto the
46-// effort enum used by Anthropic's adaptive-thinking models (Opus 4.7+).
47-// These models reject explicit thinking budgets; thinking depth is
48-// controlled by output_config.effort instead. Returns "" when reasoning
49-// is disabled.
49+// effort enum used by adaptive-thinking models. These models reject explicit
50+// thinking budgets; reasoning depth is controlled by output_config.effort.
5051 func AnthropicAdaptiveEffort(level string) string {
5152 switch NormalizeReasoning(level) {
5253 case "minimum", "low":
@@ -55,38 +56,34 @@ func AnthropicAdaptiveEffort(level string) string {
5556 return "medium"
5657 case "high":
5758 return "high"
58- case "maximum":
59+ case "xhigh":
5960 return "xhigh"
61+ case "max":
62+ return "max"
6063 default:
6164 return ""
6265 }
6366 }
6467
65-// OpenAIReasoningEffort maps zot's six-level setting onto the effort enum
66-// accepted by OpenAI-compatible chat-completions endpoints.
68+// OpenAIReasoningEffort maps zot's thinking setting onto the effort enum
69+// accepted by generic OpenAI-compatible chat-completions endpoints.
6770 func OpenAIReasoningEffort(level string) string {
6871 switch NormalizeReasoning(level) {
6972 case "minimum", "low":
70- // Many OpenAI-compatible endpoints only accept low/medium/high.
71- // Use low for zot's minimum instead of the newer minimal enum.
73+ // Many compatible endpoints only accept low/medium/high.
7274 return "low"
7375 case "medium":
7476 return "medium"
75- case "high", "maximum":
77+ case "high", "xhigh", "max":
7678 return "high"
7779 default:
7880 return ""
7981 }
8082 }
8183
82-// OpenAICompatAnthropicEffort maps zot's user-facing thinking levels
83-// onto reasoning_effort values when an adaptive-thinking Anthropic
84-// model (Opus 4.7+) is served over the OpenAI-compatible chat-
85-// completions wire (openrouter, opencode, ...). Differs from
86-// OpenAIReasoningEffort only at the top: zot's "maximum" maps to
87-// "xhigh" instead of being clamped to "high", so the model's full
88-// adaptive-thinking ceiling is preserved when reachable through a
89-// gateway that accepts the effort knob.
84+// OpenAICompatAnthropicEffort maps zot's thinking setting when an adaptive
85+// Anthropic model is served over an OpenAI-compatible chat-completions wire.
86+// Adaptive models accept native xhigh and max effort values.
9087 func OpenAICompatAnthropicEffort(level string) string {
9188 switch NormalizeReasoning(level) {
9289 case "minimum", "low":
@@ -95,17 +92,18 @@ func OpenAICompatAnthropicEffort(level string) string {
9592 return "medium"
9693 case "high":
9794 return "high"
98- case "maximum":
95+ case "xhigh":
9996 return "xhigh"
97+ case "max":
98+ return "max"
10099 default:
101100 return ""
102101 }
103102 }
104103
105-// OpenAICodexReasoningEffort maps zot levels onto the ChatGPT/Codex
106-// Responses backend enum. That backend rejects "minimal" and uses
107-// "xhigh" for the highest tier on recent GPT-5.x models.
108-func OpenAICodexReasoningEffort(level string) string {
104+// OpenAICodexReasoningEffort maps zot levels onto the Responses API effort
105+// enum. GPT-5.6 supports native max; other models clamp max to xhigh.
106+func OpenAICodexReasoningEffort(level, model string) string {
109107 switch NormalizeReasoning(level) {
110108 case "minimum", "low":
111109 return "low"
@@ -113,7 +111,12 @@ func OpenAICodexReasoningEffort(level string) string {
113111 return "medium"
114112 case "high":
115113 return "high"
116- case "maximum":
114+ case "xhigh":
115+ return "xhigh"
116+ case "max":
117+ if strings.HasPrefix(strings.ToLower(model), "gpt-5.6-") {
118+ return "max"
119+ }
117120 return "xhigh"
118121 default:
119122 return ""
@@ -3,7 +3,8 @@ package provider
3 import "strings"3 import "strings"
4 4
5 // NormalizeReasoning canonicalizes zot's user-facing thinking levels.5 // NormalizeReasoning canonicalizes zot's user-facing thinking levels.
6-// Empty string means reasoning/thinking is disabled.6+// Empty string means reasoning/thinking is disabled. "maximum" remains
7+// an alias for xhigh; "max" is the separate opt-in tier above it.
7 func NormalizeReasoning(level string) string {8 func NormalizeReasoning(level string) string {
8 switch strings.ToLower(strings.TrimSpace(level)) {9 switch strings.ToLower(strings.TrimSpace(level)) {
9 case "", "off", "none", "no", "false", "disabled":10 case "", "off", "none", "no", "false", "disabled":
@@ -16,8 +17,10 @@ func NormalizeReasoning(level string) string {
16 return "medium"17 return "medium"
17 case "hi", "high":18 case "hi", "high":
18 return "high"19 return "high"
19- case "max", "maximum":20+ case "xhigh", "maximum":
20- return "maximum"21+ return "xhigh"
22+ case "max":
23+ return "max"
21 default:24 default:
22 return strings.ToLower(strings.TrimSpace(level))25 return strings.ToLower(strings.TrimSpace(level))
23 }26 }
@@ -35,7 +38,7 @@ func ReasoningBudget(level string) int {
35 return 819238 return 8192
36 case "high":39 case "high":
37 return 1638440 return 16384
38- case "maximum":41+ case "xhigh", "max":
39 return 3276842 return 32768
40 default:43 default:
41 return 044 return 0
@@ -43,10 +46,8 @@ func ReasoningBudget(level string) int {
43 }46 }
44 47
45 // AnthropicAdaptiveEffort maps zot's user-facing thinking levels onto the48 // AnthropicAdaptiveEffort maps zot's user-facing thinking levels onto the
46-// effort enum used by Anthropic's adaptive-thinking models (Opus 4.7+).49+// effort enum used by adaptive-thinking models. These models reject explicit
47-// These models reject explicit thinking budgets; thinking depth is50+// thinking budgets; reasoning depth is controlled by output_config.effort.
48-// controlled by output_config.effort instead. Returns "" when reasoning
49-// is disabled.
50 func AnthropicAdaptiveEffort(level string) string {51 func AnthropicAdaptiveEffort(level string) string {
51 switch NormalizeReasoning(level) {52 switch NormalizeReasoning(level) {
52 case "minimum", "low":53 case "minimum", "low":
@@ -55,38 +56,34 @@ func AnthropicAdaptiveEffort(level string) string {
55 return "medium"56 return "medium"
56 case "high":57 case "high":
57 return "high"58 return "high"
58- case "maximum":59+ case "xhigh":
59 return "xhigh"60 return "xhigh"
61+ case "max":
62+ return "max"
60 default:63 default:
61 return ""64 return ""
62 }65 }
63 }66 }
64 67
65-// OpenAIReasoningEffort maps zot's six-level setting onto the effort enum68+// OpenAIReasoningEffort maps zot's thinking setting onto the effort enum
66-// accepted by OpenAI-compatible chat-completions endpoints.69+// accepted by generic OpenAI-compatible chat-completions endpoints.
67 func OpenAIReasoningEffort(level string) string {70 func OpenAIReasoningEffort(level string) string {
68 switch NormalizeReasoning(level) {71 switch NormalizeReasoning(level) {
69 case "minimum", "low":72 case "minimum", "low":
70- // Many OpenAI-compatible endpoints only accept low/medium/high.73+ // Many compatible endpoints only accept low/medium/high.
71- // Use low for zot's minimum instead of the newer minimal enum.
72 return "low"74 return "low"
73 case "medium":75 case "medium":
74 return "medium"76 return "medium"
75- case "high", "maximum":77+ case "high", "xhigh", "max":
76 return "high"78 return "high"
77 default:79 default:
78 return ""80 return ""
79 }81 }
80 }82 }
81 83
82-// OpenAICompatAnthropicEffort maps zot's user-facing thinking levels84+// OpenAICompatAnthropicEffort maps zot's thinking setting when an adaptive
83-// onto reasoning_effort values when an adaptive-thinking Anthropic85+// Anthropic model is served over an OpenAI-compatible chat-completions wire.
84-// model (Opus 4.7+) is served over the OpenAI-compatible chat-86+// Adaptive models accept native xhigh and max effort values.
85-// completions wire (openrouter, opencode, ...). Differs from
86-// OpenAIReasoningEffort only at the top: zot's "maximum" maps to
87-// "xhigh" instead of being clamped to "high", so the model's full
88-// adaptive-thinking ceiling is preserved when reachable through a
89-// gateway that accepts the effort knob.
90 func OpenAICompatAnthropicEffort(level string) string {87 func OpenAICompatAnthropicEffort(level string) string {
91 switch NormalizeReasoning(level) {88 switch NormalizeReasoning(level) {
92 case "minimum", "low":89 case "minimum", "low":
@@ -95,17 +92,18 @@ func OpenAICompatAnthropicEffort(level string) string {
95 return "medium"92 return "medium"
96 case "high":93 case "high":
97 return "high"94 return "high"
98- case "maximum":95+ case "xhigh":
99 return "xhigh"96 return "xhigh"
97+ case "max":
98+ return "max"
100 default:99 default:
101 return ""100 return ""
102 }101 }
103 }102 }
104 103
105-// OpenAICodexReasoningEffort maps zot levels onto the ChatGPT/Codex104+// OpenAICodexReasoningEffort maps zot levels onto the Responses API effort
106-// Responses backend enum. That backend rejects "minimal" and uses105+// enum. GPT-5.6 supports native max; other models clamp max to xhigh.
107-// "xhigh" for the highest tier on recent GPT-5.x models.106+func OpenAICodexReasoningEffort(level, model string) string {
108-func OpenAICodexReasoningEffort(level string) string {
109 switch NormalizeReasoning(level) {107 switch NormalizeReasoning(level) {
110 case "minimum", "low":108 case "minimum", "low":
111 return "low"109 return "low"
@@ -113,7 +111,12 @@ func OpenAICodexReasoningEffort(level string) string {
113 return "medium"111 return "medium"
114 case "high":112 case "high":
115 return "high"113 return "high"
116- case "maximum":114+ case "xhigh":
115+ return "xhigh"
116+ case "max":
117+ if strings.HasPrefix(strings.ToLower(model), "gpt-5.6-") {
118+ return "max"
119+ }
117 return "xhigh"120 return "xhigh"
118 default:121 default:
119 return ""122 return ""
M packages/provider/reasoning_test.go +13 -10
@@ -5,20 +5,23 @@ import "testing"
55 func TestReasoningEffortMappings(t *testing.T) {
66 cases := []struct {
77 level string
8+ model string
89 openai string
910 anthCompat string
1011 codex string
1112 budget int
1213 normalized string
1314 }{
14- {"off", "", "", "", 0, ""},
15- {"minimum", "low", "low", "low", 1024, "minimum"},
16- {"minimal", "low", "low", "low", 1024, "minimum"},
17- {"low", "low", "low", "low", 2048, "low"},
18- {"medium", "medium", "medium", "medium", 8192, "medium"},
19- {"high", "high", "high", "high", 16384, "high"},
20- {"maximum", "high", "xhigh", "xhigh", 32768, "maximum"},
21- {"max", "high", "xhigh", "xhigh", 32768, "maximum"},
15+ {"off", "gpt-5.6-sol", "", "", "", 0, ""},
16+ {"minimum", "gpt-5.6-sol", "low", "low", "low", 1024, "minimum"},
17+ {"minimal", "gpt-5.6-sol", "low", "low", "low", 1024, "minimum"},
18+ {"low", "gpt-5.6-sol", "low", "low", "low", 2048, "low"},
19+ {"medium", "gpt-5.6-sol", "medium", "medium", "medium", 8192, "medium"},
20+ {"high", "gpt-5.6-sol", "high", "high", "high", 16384, "high"},
21+ {"maximum", "gpt-5.6-sol", "high", "xhigh", "xhigh", 32768, "xhigh"},
22+ {"xhigh", "gpt-5.6-sol", "high", "xhigh", "xhigh", 32768, "xhigh"},
23+ {"max", "gpt-5.6-sol", "high", "max", "max", 32768, "max"},
24+ {"max", "gpt-5.5", "high", "max", "xhigh", 32768, "max"},
2225 }
2326 for _, tc := range cases {
2427 if got := NormalizeReasoning(tc.level); got != tc.normalized {
@@ -30,8 +33,8 @@ func TestReasoningEffortMappings(t *testing.T) {
3033 if got := OpenAICompatAnthropicEffort(tc.level); got != tc.anthCompat {
3134 t.Errorf("OpenAICompatAnthropicEffort(%q)=%q want %q", tc.level, got, tc.anthCompat)
3235 }
33- if got := OpenAICodexReasoningEffort(tc.level); got != tc.codex {
34- t.Errorf("OpenAICodexReasoningEffort(%q)=%q want %q", tc.level, got, tc.codex)
36+ if got := OpenAICodexReasoningEffort(tc.level, tc.model); got != tc.codex {
37+ t.Errorf("OpenAICodexReasoningEffort(%q, %q)=%q want %q", tc.level, tc.model, got, tc.codex)
3538 }
3639 if got := ReasoningBudget(tc.level); got != tc.budget {
3740 t.Errorf("ReasoningBudget(%q)=%d want %d", tc.level, got, tc.budget)
@@ -5,20 +5,23 @@ import "testing"
5 func TestReasoningEffortMappings(t *testing.T) {5 func TestReasoningEffortMappings(t *testing.T) {
6 cases := []struct {6 cases := []struct {
7 level string7 level string
8+ model string
8 openai string9 openai string
9 anthCompat string10 anthCompat string
10 codex string11 codex string
11 budget int12 budget int
12 normalized string13 normalized string
13 }{14 }{
14- {"off", "", "", "", 0, ""},15+ {"off", "gpt-5.6-sol", "", "", "", 0, ""},
15- {"minimum", "low", "low", "low", 1024, "minimum"},16+ {"minimum", "gpt-5.6-sol", "low", "low", "low", 1024, "minimum"},
16- {"minimal", "low", "low", "low", 1024, "minimum"},17+ {"minimal", "gpt-5.6-sol", "low", "low", "low", 1024, "minimum"},
17- {"low", "low", "low", "low", 2048, "low"},18+ {"low", "gpt-5.6-sol", "low", "low", "low", 2048, "low"},
18- {"medium", "medium", "medium", "medium", 8192, "medium"},19+ {"medium", "gpt-5.6-sol", "medium", "medium", "medium", 8192, "medium"},
19- {"high", "high", "high", "high", 16384, "high"},20+ {"high", "gpt-5.6-sol", "high", "high", "high", 16384, "high"},
20- {"maximum", "high", "xhigh", "xhigh", 32768, "maximum"},21+ {"maximum", "gpt-5.6-sol", "high", "xhigh", "xhigh", 32768, "xhigh"},
21- {"max", "high", "xhigh", "xhigh", 32768, "maximum"},22+ {"xhigh", "gpt-5.6-sol", "high", "xhigh", "xhigh", 32768, "xhigh"},
23+ {"max", "gpt-5.6-sol", "high", "max", "max", 32768, "max"},
24+ {"max", "gpt-5.5", "high", "max", "xhigh", 32768, "max"},
22 }25 }
23 for _, tc := range cases {26 for _, tc := range cases {
24 if got := NormalizeReasoning(tc.level); got != tc.normalized {27 if got := NormalizeReasoning(tc.level); got != tc.normalized {
@@ -30,8 +33,8 @@ func TestReasoningEffortMappings(t *testing.T) {
30 if got := OpenAICompatAnthropicEffort(tc.level); got != tc.anthCompat {33 if got := OpenAICompatAnthropicEffort(tc.level); got != tc.anthCompat {
31 t.Errorf("OpenAICompatAnthropicEffort(%q)=%q want %q", tc.level, got, tc.anthCompat)34 t.Errorf("OpenAICompatAnthropicEffort(%q)=%q want %q", tc.level, got, tc.anthCompat)
32 }35 }
33- if got := OpenAICodexReasoningEffort(tc.level); got != tc.codex {36+ if got := OpenAICodexReasoningEffort(tc.level, tc.model); got != tc.codex {
34- t.Errorf("OpenAICodexReasoningEffort(%q)=%q want %q", tc.level, got, tc.codex)37+ t.Errorf("OpenAICodexReasoningEffort(%q, %q)=%q want %q", tc.level, tc.model, got, tc.codex)
35 }38 }
36 if got := ReasoningBudget(tc.level); got != tc.budget {39 if got := ReasoningBudget(tc.level); got != tc.budget {
37 t.Errorf("ReasoningBudget(%q)=%d want %d", tc.level, got, tc.budget)40 t.Errorf("ReasoningBudget(%q)=%d want %d", tc.level, got, tc.budget)
M packages/tui/statusbar_test.go +16 -2
@@ -85,12 +85,26 @@ func TestStatusBarThinkingLevelBetweenModelAndStats(t *testing.T) {
8585 }
8686 }
8787
88+func TestStatusBarUsesThinkingMaxThemeColor(t *testing.T) {
89+ th := Dark
90+ th.ThinkingMax = 201
91+ lines := StatusBar(StatusBarParams{
92+ Theme: th, Provider: "openai", Model: "gpt-5.6-sol", Reasoning: "max", Cols: 200,
93+ })
94+ if len(lines) != 1 {
95+ t.Fatalf("lines = %d, want 1", len(lines))
96+ }
97+ if !strings.Contains(lines[0], "\x1b[38;5;201m") || !strings.Contains(stripANSI(lines[0]), "thinking: max") {
98+ t.Fatalf("max thinking style missing: %q", lines[0])
99+ }
100+}
101+
88102 func TestStatusBarNarrowKeepsModelAndThinkingTogetherWhenTheyFit(t *testing.T) {
89103 lines := StatusBar(StatusBarParams{
90104 Theme: Dark,
91105 Provider: "openai-codex",
92106 Model: "gpt-5.5",
93- Reasoning: "maximum",
107+ Reasoning: "xhigh",
94108 CWD: "/tmp/x",
95109 Usage: provider.Usage{
96110 CostUSD: 0,
@@ -107,7 +121,7 @@ func TestStatusBarNarrowKeepsModelAndThinkingTogetherWhenTheyFit(t *testing.T) {
107121 for i, line := range lines {
108122 plain[i] = stripANSI(line)
109123 }
110- if !strings.Contains(plain[0], "(openai-codex) gpt-5.5 thinking: maximum") {
124+ if !strings.Contains(plain[0], "(openai-codex) gpt-5.5 thinking: xhigh") {
111125 t.Fatalf("line 1 should contain model and thinking level, got %q", plain[0])
112126 }
113127 if !strings.Contains(plain[1], "$0.000 (sub)") || strings.Contains(plain[1], "thinking level") {
@@ -85,12 +85,26 @@ func TestStatusBarThinkingLevelBetweenModelAndStats(t *testing.T) {
85 }85 }
86 }86 }
87 87
88+func TestStatusBarUsesThinkingMaxThemeColor(t *testing.T) {
89+ th := Dark
90+ th.ThinkingMax = 201
91+ lines := StatusBar(StatusBarParams{
92+ Theme: th, Provider: "openai", Model: "gpt-5.6-sol", Reasoning: "max", Cols: 200,
93+ })
94+ if len(lines) != 1 {
95+ t.Fatalf("lines = %d, want 1", len(lines))
96+ }
97+ if !strings.Contains(lines[0], "\x1b[38;5;201m") || !strings.Contains(stripANSI(lines[0]), "thinking: max") {
98+ t.Fatalf("max thinking style missing: %q", lines[0])
99+ }
100+}
101+
88 func TestStatusBarNarrowKeepsModelAndThinkingTogetherWhenTheyFit(t *testing.T) {102 func TestStatusBarNarrowKeepsModelAndThinkingTogetherWhenTheyFit(t *testing.T) {
89 lines := StatusBar(StatusBarParams{103 lines := StatusBar(StatusBarParams{
90 Theme: Dark,104 Theme: Dark,
91 Provider: "openai-codex",105 Provider: "openai-codex",
92 Model: "gpt-5.5",106 Model: "gpt-5.5",
93- Reasoning: "maximum",107+ Reasoning: "xhigh",
94 CWD: "/tmp/x",108 CWD: "/tmp/x",
95 Usage: provider.Usage{109 Usage: provider.Usage{
96 CostUSD: 0,110 CostUSD: 0,
@@ -107,7 +121,7 @@ func TestStatusBarNarrowKeepsModelAndThinkingTogetherWhenTheyFit(t *testing.T) {
107 for i, line := range lines {121 for i, line := range lines {
108 plain[i] = stripANSI(line)122 plain[i] = stripANSI(line)
109 }123 }
110- if !strings.Contains(plain[0], "(openai-codex) gpt-5.5 thinking: maximum") {124+ if !strings.Contains(plain[0], "(openai-codex) gpt-5.5 thinking: xhigh") {
111 t.Fatalf("line 1 should contain model and thinking level, got %q", plain[0])125 t.Fatalf("line 1 should contain model and thinking level, got %q", plain[0])
112 }126 }
113 if !strings.Contains(plain[1], "$0.000 (sub)") || strings.Contains(plain[1], "thinking level") {127 if !strings.Contains(plain[1], "$0.000 (sub)") || strings.Contains(plain[1], "thinking level") {
M packages/tui/theme.go +3 -0
@@ -49,6 +49,7 @@ type Theme struct {
4949 Error int
5050 Warning int
5151 Spinner int // spinner + funny working line
52+ ThinkingMax int // status color for the opt-in max thinking level
5253 SelectionBG int // background for highlighted rows
5354 SelectionFG int // foreground for highlighted rows
5455
@@ -159,6 +160,7 @@ var Dark = Theme{
159160 Error: 203,
160161 Warning: 214,
161162 Spinner: 183, // soft purple
163+ ThinkingMax: 207, // vivid magenta
162164 SelectionBG: 24, // deep blue background
163165 SelectionFG: 231, // near-white foreground
164166 SpinnerFrames: defaultSpinnerFrames,
@@ -181,6 +183,7 @@ var Light = Theme{
181183 Error: 160,
182184 Warning: 166,
183185 Spinner: 91, // purple
186+ ThinkingMax: 127, // deep magenta
184187 SelectionBG: 153, // light blue
185188 SelectionFG: 232, // near-black
186189 SpinnerFrames: defaultSpinnerFrames,
@@ -49,6 +49,7 @@ type Theme struct {
49 Error int49 Error int
50 Warning int50 Warning int
51 Spinner int // spinner + funny working line51 Spinner int // spinner + funny working line
52+ ThinkingMax int // status color for the opt-in max thinking level
52 SelectionBG int // background for highlighted rows53 SelectionBG int // background for highlighted rows
53 SelectionFG int // foreground for highlighted rows54 SelectionFG int // foreground for highlighted rows
54 55
@@ -159,6 +160,7 @@ var Dark = Theme{
159 Error: 203,160 Error: 203,
160 Warning: 214,161 Warning: 214,
161 Spinner: 183, // soft purple162 Spinner: 183, // soft purple
163+ ThinkingMax: 207, // vivid magenta
162 SelectionBG: 24, // deep blue background164 SelectionBG: 24, // deep blue background
163 SelectionFG: 231, // near-white foreground165 SelectionFG: 231, // near-white foreground
164 SpinnerFrames: defaultSpinnerFrames,166 SpinnerFrames: defaultSpinnerFrames,
@@ -181,6 +183,7 @@ var Light = Theme{
181 Error: 160,183 Error: 160,
182 Warning: 166,184 Warning: 166,
183 Spinner: 91, // purple185 Spinner: 91, // purple
186+ ThinkingMax: 127, // deep magenta
184 SelectionBG: 153, // light blue187 SelectionBG: 153, // light blue
185 SelectionFG: 232, // near-black188 SelectionFG: 232, // near-black
186 SpinnerFrames: defaultSpinnerFrames,189 SpinnerFrames: defaultSpinnerFrames,
M packages/tui/theme_loader.go +7 -0
@@ -92,6 +92,8 @@ type ThemeOverrides struct {
9292 Error *int `json:"error,omitempty"`
9393 Warning *int `json:"warning,omitempty"`
9494 Spinner *int `json:"spinner,omitempty"`
95+ ThinkingMax *int `json:"thinking_max,omitempty"`
96+ ThinkingMaxCamel *int `json:"thinkingMax,omitempty"`
9597 SelectionBG *int `json:"selection_bg,omitempty"`
9698 SelectionFG *int `json:"selection_fg,omitempty"`
9799 SpinnerFrames []string `json:"spinner_frames,omitempty"`
@@ -383,6 +385,11 @@ func applyThemeOverrides(th Theme, o ThemeOverrides) Theme {
383385 if o.Spinner != nil {
384386 th.Spinner = *o.Spinner
385387 }
388+ if o.ThinkingMax != nil {
389+ th.ThinkingMax = *o.ThinkingMax
390+ } else if o.ThinkingMaxCamel != nil {
391+ th.ThinkingMax = *o.ThinkingMaxCamel
392+ }
386393 if o.SelectionBG != nil {
387394 th.SelectionBG = *o.SelectionBG
388395 }
@@ -92,6 +92,8 @@ type ThemeOverrides struct {
92 Error *int `json:"error,omitempty"`92 Error *int `json:"error,omitempty"`
93 Warning *int `json:"warning,omitempty"`93 Warning *int `json:"warning,omitempty"`
94 Spinner *int `json:"spinner,omitempty"`94 Spinner *int `json:"spinner,omitempty"`
95+ ThinkingMax *int `json:"thinking_max,omitempty"`
96+ ThinkingMaxCamel *int `json:"thinkingMax,omitempty"`
95 SelectionBG *int `json:"selection_bg,omitempty"`97 SelectionBG *int `json:"selection_bg,omitempty"`
96 SelectionFG *int `json:"selection_fg,omitempty"`98 SelectionFG *int `json:"selection_fg,omitempty"`
97 SpinnerFrames []string `json:"spinner_frames,omitempty"`99 SpinnerFrames []string `json:"spinner_frames,omitempty"`
@@ -383,6 +385,11 @@ func applyThemeOverrides(th Theme, o ThemeOverrides) Theme {
383 if o.Spinner != nil {385 if o.Spinner != nil {
384 th.Spinner = *o.Spinner386 th.Spinner = *o.Spinner
385 }387 }
388+ if o.ThinkingMax != nil {
389+ th.ThinkingMax = *o.ThinkingMax
390+ } else if o.ThinkingMaxCamel != nil {
391+ th.ThinkingMax = *o.ThinkingMaxCamel
392+ }
386 if o.SelectionBG != nil {393 if o.SelectionBG != nil {
387 th.SelectionBG = *o.SelectionBG394 th.SelectionBG = *o.SelectionBG
388 }395 }
M packages/tui/theme_loader_test.go +19 -0
@@ -34,6 +34,25 @@ func TestLoadThemeAllowsPartialColorOverrides(t *testing.T) {
3434 }
3535 }
3636
37+func TestLoadThemeAllowsThinkingMaxOverride(t *testing.T) {
38+ home := t.TempDir()
39+ if err := os.MkdirAll(filepath.Join(home, "themes"), 0o755); err != nil {
40+ t.Fatal(err)
41+ }
42+ path := filepath.Join(home, "themes", "thinking.json")
43+ if err := os.WriteFile(path, []byte(`{"colors":{"dark":{"thinkingMax":201}}}`), 0o644); err != nil {
44+ t.Fatal(err)
45+ }
46+
47+ th, _, err := LoadThemeFromHome(home, "thinking", Dark)
48+ if err != nil {
49+ t.Fatal(err)
50+ }
51+ if th.ThinkingMax != 201 {
52+ t.Fatalf("thinking max = %d, want 201", th.ThinkingMax)
53+ }
54+}
55+
3756 func TestLoadThemeAllowsSpinnerOnlyTopLevelOverrides(t *testing.T) {
3857 home := t.TempDir()
3958 if err := os.MkdirAll(filepath.Join(home, "themes"), 0o755); err != nil {
@@ -34,6 +34,25 @@ func TestLoadThemeAllowsPartialColorOverrides(t *testing.T) {
34 }34 }
35 }35 }
36 36
37+func TestLoadThemeAllowsThinkingMaxOverride(t *testing.T) {
38+ home := t.TempDir()
39+ if err := os.MkdirAll(filepath.Join(home, "themes"), 0o755); err != nil {
40+ t.Fatal(err)
41+ }
42+ path := filepath.Join(home, "themes", "thinking.json")
43+ if err := os.WriteFile(path, []byte(`{"colors":{"dark":{"thinkingMax":201}}}`), 0o644); err != nil {
44+ t.Fatal(err)
45+ }
46+
47+ th, _, err := LoadThemeFromHome(home, "thinking", Dark)
48+ if err != nil {
49+ t.Fatal(err)
50+ }
51+ if th.ThinkingMax != 201 {
52+ t.Fatalf("thinking max = %d, want 201", th.ThinkingMax)
53+ }
54+}
55+
37 func TestLoadThemeAllowsSpinnerOnlyTopLevelOverrides(t *testing.T) {56 func TestLoadThemeAllowsSpinnerOnlyTopLevelOverrides(t *testing.T) {
38 home := t.TempDir()57 home := t.TempDir()
39 if err := os.MkdirAll(filepath.Join(home, "themes"), 0o755); err != nil {58 if err := os.MkdirAll(filepath.Join(home, "themes"), 0o755); err != nil {
M packages/tui/view.go +17 -7
@@ -2453,8 +2453,8 @@ func StatusBar(p StatusBarParams) []string {
24532453 leftBuilder.WriteString(th.FG256(th.Muted, left))
24542454 if middle != "" {
24552455 leftBuilder.WriteString(pad)
2456- // `middle` already has colorized context segments; wrap the rest in muted.
2457- leftBuilder.WriteString(th.FG256(th.Muted, middle))
2456+ // Highlight the opt-in max tier; other status information stays muted.
2457+ leftBuilder.WriteString(th.FG256(thinkingStatusColor(th, thinkingText), middle))
24582458 }
24592459
24602460 cwd := shortenHome(p.CWD)
@@ -2489,7 +2489,7 @@ func StatusBar(p StatusBarParams) []string {
24892489 infoBuilder.WriteString(modelLine)
24902490 if middle != "" {
24912491 infoBuilder.WriteString(pad)
2492- infoBuilder.WriteString(th.FG256(th.Muted, middle))
2492+ infoBuilder.WriteString(th.FG256(thinkingStatusColor(th, thinkingText), middle))
24932493 }
24942494 lines = append(lines, infoBuilder.String())
24952495 }
@@ -2533,11 +2533,12 @@ func appendWrappedStatusLines(lines []string, th Theme, pad, modelText, thinking
25332533 }
25342534
25352535 modelThinkingPlain := pad + modelText + pad + thinkingText
2536+ thinkingColor := thinkingStatusColor(th, thinkingText)
25362537 if cols <= 0 || visibleWidth(modelThinkingPlain) <= cols {
2537- lines = append(lines, pad+th.FG256(th.Muted, modelText+pad+thinkingText))
2538+ lines = append(lines, pad+th.FG256(thinkingColor, modelText+pad+thinkingText))
25382539 } else {
25392540 lines = append(lines, modelLine)
2540- lines = append(lines, pad+th.FG256(th.Muted, thinkingText))
2541+ lines = append(lines, pad+th.FG256(thinkingColor, thinkingText))
25412542 }
25422543 if statsText != "" {
25432544 lines = append(lines, pad+th.FG256(th.Muted, statsText))
@@ -2545,14 +2546,23 @@ func appendWrappedStatusLines(lines []string, th Theme, pad, modelText, thinking
25452546 return lines
25462547 }
25472548
2549+func thinkingStatusColor(th Theme, thinkingText string) int {
2550+ if strings.HasSuffix(thinkingText, ": max") {
2551+ return th.ThinkingMax
2552+ }
2553+ return th.Muted
2554+}
2555+
25482556 func thinkingLevelLabel(level string) string {
25492557 switch strings.ToLower(strings.TrimSpace(level)) {
25502558 case "", "off", "none", "no", "false", "disabled":
25512559 return ""
25522560 case "minimum", "minimal", "min":
25532561 return "minimal"
2554- case "maximum", "max", "xhigh":
2555- return "maximum"
2562+ case "maximum", "xhigh":
2563+ return "xhigh"
2564+ case "max":
2565+ return "max"
25562566 default:
25572567 return strings.ToLower(strings.TrimSpace(level))
25582568 }
@@ -2453,8 +2453,8 @@ func StatusBar(p StatusBarParams) []string {
2453 leftBuilder.WriteString(th.FG256(th.Muted, left))2453 leftBuilder.WriteString(th.FG256(th.Muted, left))
2454 if middle != "" {2454 if middle != "" {
2455 leftBuilder.WriteString(pad)2455 leftBuilder.WriteString(pad)
2456- // `middle` already has colorized context segments; wrap the rest in muted.2456+ // Highlight the opt-in max tier; other status information stays muted.
2457- leftBuilder.WriteString(th.FG256(th.Muted, middle))2457+ leftBuilder.WriteString(th.FG256(thinkingStatusColor(th, thinkingText), middle))
2458 }2458 }
2459 2459
2460 cwd := shortenHome(p.CWD)2460 cwd := shortenHome(p.CWD)
@@ -2489,7 +2489,7 @@ func StatusBar(p StatusBarParams) []string {
2489 infoBuilder.WriteString(modelLine)2489 infoBuilder.WriteString(modelLine)
2490 if middle != "" {2490 if middle != "" {
2491 infoBuilder.WriteString(pad)2491 infoBuilder.WriteString(pad)
2492- infoBuilder.WriteString(th.FG256(th.Muted, middle))2492+ infoBuilder.WriteString(th.FG256(thinkingStatusColor(th, thinkingText), middle))
2493 }2493 }
2494 lines = append(lines, infoBuilder.String())2494 lines = append(lines, infoBuilder.String())
2495 }2495 }
@@ -2533,11 +2533,12 @@ func appendWrappedStatusLines(lines []string, th Theme, pad, modelText, thinking
2533 }2533 }
2534 2534
2535 modelThinkingPlain := pad + modelText + pad + thinkingText2535 modelThinkingPlain := pad + modelText + pad + thinkingText
2536+ thinkingColor := thinkingStatusColor(th, thinkingText)
2536 if cols <= 0 || visibleWidth(modelThinkingPlain) <= cols {2537 if cols <= 0 || visibleWidth(modelThinkingPlain) <= cols {
2537- lines = append(lines, pad+th.FG256(th.Muted, modelText+pad+thinkingText))2538+ lines = append(lines, pad+th.FG256(thinkingColor, modelText+pad+thinkingText))
2538 } else {2539 } else {
2539 lines = append(lines, modelLine)2540 lines = append(lines, modelLine)
2540- lines = append(lines, pad+th.FG256(th.Muted, thinkingText))2541+ lines = append(lines, pad+th.FG256(thinkingColor, thinkingText))
2541 }2542 }
2542 if statsText != "" {2543 if statsText != "" {
2543 lines = append(lines, pad+th.FG256(th.Muted, statsText))2544 lines = append(lines, pad+th.FG256(th.Muted, statsText))
@@ -2545,14 +2546,23 @@ func appendWrappedStatusLines(lines []string, th Theme, pad, modelText, thinking
2545 return lines2546 return lines
2546 }2547 }
2547 2548
2549+func thinkingStatusColor(th Theme, thinkingText string) int {
2550+ if strings.HasSuffix(thinkingText, ": max") {
2551+ return th.ThinkingMax
2552+ }
2553+ return th.Muted
2554+}
2555+
2548 func thinkingLevelLabel(level string) string {2556 func thinkingLevelLabel(level string) string {
2549 switch strings.ToLower(strings.TrimSpace(level)) {2557 switch strings.ToLower(strings.TrimSpace(level)) {
2550 case "", "off", "none", "no", "false", "disabled":2558 case "", "off", "none", "no", "false", "disabled":
2551 return ""2559 return ""
2552 case "minimum", "minimal", "min":2560 case "minimum", "minimal", "min":
2553 return "minimal"2561 return "minimal"
2554- case "maximum", "max", "xhigh":2562+ case "maximum", "xhigh":
2555- return "maximum"2563+ return "xhigh"
2564+ case "max":
2565+ return "max"
2556 default:2566 default:
2557 return strings.ToLower(strings.TrimSpace(level))2567 return strings.ToLower(strings.TrimSpace(level))
2558 }2568 }

Keyboard shortcuts

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

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

Branches 1

Tags 476

↑↓ moveenter openesc close