Skip to content

Commit ecd0a9d

Browse files
committed
Add a /model picker that switches between the seven supported models with cross-provider greying, and rename /s and /n to /safe and /normal.
1 parent 74be0d7 commit ecd0a9d

27 files changed

Lines changed: 1251 additions & 448 deletions

CHANGELOG.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ All notable changes to Sofos are documented in this file.
66

77
### Added
88

9+
- **A new `/model` command opens an inline picker that lets you switch the active model.** Up and Down highlight an entry, Enter confirms, Esc cancels. Each row shows the model id, a short description, and a `(current)` tag on the model you are using right now. Models on the other provider are greyed out and tagged `(re-launch session to activate)`, because the underlying API client is built once at startup and cannot swap providers mid-session. The cursor skips greyed rows so you cannot land on a model the running session cannot reach. Typing `/model <name>` switches directly without opening the picker; same-provider switches happen in place, cross-provider attempts are refused with a clear "re-launch with `--model <name>`" message.
10+
- **`--model` now lists the supported models when it rejects a value.** Passing `--model gpt-9.9` (or any slug outside the supported set) exits with `[supported models: claude-opus-4-7, claude-sonnet-4-6, claude-haiku-4-5, gpt-5.5, gpt-5.4, gpt-5.4-mini, gpt-5.3-codex]`, mirroring the existing `--reasoning-effort` rejection so the failure mode is consistent.
911
- **An inline suggestion list appears the moment you type `/`.** As soon as the input begins with a slash, a small panel below the input box lists every available slash command together with a short description, filtered by what you have typed so far. Up and Down highlight an entry, Enter runs it, Tab inserts it into the input so you can finish typing arguments, and Esc closes the panel. The Tab autocomplete that previously worked silently for a single match still does, but it now goes through the same suggestion list.
1012
- **New `view_image` tool lets the model open an image on demand.** Given a local image file path or an `http(s)://` URL, the tool attaches the image to the conversation so the model can describe it. For a folder of images, the model is told to call `list_directory` first and then `view_image` once per file. Supports JPEG, PNG, GIF, and WebP up to 20 MB per local file; URLs are passed through to the model provider, which fetches them on its side. External paths reuse the same Read-permission prompt as `read_file`, so granting access to a directory once covers both tools. Local images larger than 2048 pixels on the long side are downscaled proportionally before they reach the model so a 4K screenshot does not burn through the per-image token budget; smaller images are sent unchanged.
1113

@@ -17,7 +19,7 @@ All notable changes to Sofos are documented in this file.
1719

1820
- **Shell command and process substitution are now blocked in bash commands.** Constructs such as `echo $(rm bad)`, backtick substitution, and process substitution `<(cmd)` / `>(cmd)` previously slipped past the permission system because only the outer command name was checked. They are now refused before the command runs, with a clear message that names the marker. Single-quoted literals and arithmetic expansion `$((expr))` continue to work.
1921
- **Workspace symlinks can no longer route bash reads outside the workspace.** When a bash command names a workspace-relative path that resolves through a symlink to a file outside the workspace, the path is now sent through the same external-path prompt that absolute and tilde paths already use.
20-
- **MCP tools are filtered out in safe mode by default.** Previously safe mode only restricted Sofos's native tools, so any tool exposed by a configured MCP server still ran in safe-mode sessions. Each MCP server entry now has a `safe_mode` setting — `disabled` (default, filtered), `read_only`, or `allow` — and only servers whose setting opts them in have their tools listed to the model when safe mode is on. The startup banner and `/s` confirmation list which servers were filtered out and which were opted in so the user can see the actual scope of the session.
22+
- **MCP tools are filtered out in safe mode by default.** Previously safe mode only restricted Sofos's native tools, so any tool exposed by a configured MCP server still ran in safe-mode sessions. Each MCP server entry now has a `safe_mode` setting — `disabled` (default, filtered), `read_only`, or `allow` — and only servers whose setting opts them in have their tools listed to the model when safe mode is on. The startup banner and `/safe` confirmation list which servers were filtered out and which were opted in so the user can see the actual scope of the session.
2123

2224
### Changed
2325

@@ -244,7 +246,7 @@ All notable changes to Sofos are documented in this file.
244246

245247
- **Claude Opus 4.7 adaptive-thinking support.** Opus 4.7 rejects the older `{thinking: {type: "enabled", budget_tokens: N}}` shape; sofos now sends `{thinking: {type: "adaptive"}, output_config: {effort}}` instead. `/think on` maps to `effort: high`, `/think off` to `effort: low`. Status line and startup banner show `Adaptive thinking effort: high|low` instead of a fake token budget.
246248
- **Confirmation modal now fits short terminals.** The 4-choice permission prompt grows the viewport when it can; when it can't, drops the separators / hint row and scrolls the choice list around the cursor with `` / `` cues.
247-
- **Visible feedback on `/s` and `/n`** (safe-mode toggles). Now prints a one-line status (`Safe mode: enabled / read-only tools only; no writes or bash`, `Safe mode: disabled / all tools available`, or a dimmed `already enabled/disabled`).
249+
- **Visible feedback on `/safe` and `/normal`** (safe-mode toggles). Now prints a one-line status (`Safe mode: enabled / read-only tools only; no writes or bash`, `Safe mode: disabled / all tools available`, or a dimmed `already enabled/disabled`).
248250

249251
### Security
250252

README.md

Lines changed: 37 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -182,17 +182,19 @@ sofos --resume
182182

183183
### Interactive commands
184184

185-
| Command | Description |
186-
|---|---|
187-
| `/resume` | Open the session picker and resume a saved conversation. |
188-
| `/clear` | Clear the current conversation history and start a fresh session id. |
189-
| `/compact` | Compact older context to reduce token usage. |
190-
| `/think` | Show the current reasoning-effort setting. |
185+
| Command | Description |
186+
|---------------------------------------------|---|
187+
| `/resume` | Open the session picker and resume a saved conversation. |
188+
| `/clear` | Clear the current conversation history and start a fresh session id. |
189+
| `/compact` | Compact older context to reduce token usage. |
190+
| `/think` | Show the current reasoning-effort setting. |
191191
| `/think off\|low\|medium\|high\|xhigh\|max` | Change reasoning effort when the active model supports the selected level. |
192-
| `/s` | Enable safe mode: read-only native tools. Prompt changes to `:`. |
193-
| `/n` | Return to normal mode. Prompt changes to `>`. |
194-
| `/exit`, `/quit`, `/q`, `Ctrl+D` | Save the session and exit with a cost summary. |
195-
| `ESC` or `Ctrl+C` while busy | Interrupt the current AI turn. |
192+
| `/model` | Open the model picker. Highlight an entry with **Up / Down**, **Enter** to switch, **Esc** to cancel. Models on the other provider are greyed out (the API client is fixed at startup) and the cursor skips them. |
193+
| `/model <name>` | Switch directly to a named model without opening the picker. Same-provider only — cross-provider switches require relaunching with `--model <name>`. |
194+
| `/safe` | Enable safe mode: read-only native tools. Prompt changes to `:`. |
195+
| `/normal` | Return to normal mode. Prompt changes to `>`. |
196+
| `/exit`, `/quit`, `/q`, `Ctrl+D` | Save the session and exit with a cost summary. |
197+
| `ESC` or `Ctrl+C` while busy | Interrupt the current AI turn. |
196198

197199
### Input behaviour
198200

@@ -258,6 +260,20 @@ Supported formats: JPEG, PNG, GIF, and WebP. Local images are capped at 20 MB. I
258260

259261
## Models and reasoning effort
260262

263+
Sofos supports seven models, in `/model` picker order:
264+
265+
| Model | Provider |
266+
|---|---|
267+
| `claude-opus-4-7` | Anthropic |
268+
| `claude-sonnet-4-6` (default) | Anthropic |
269+
| `claude-haiku-4-5` | Anthropic |
270+
| `gpt-5.5` | OpenAI |
271+
| `gpt-5.4` | OpenAI |
272+
| `gpt-5.4-mini` | OpenAI |
273+
| `gpt-5.3-codex` | OpenAI |
274+
275+
`--model <name>` accepts only the values above; any other slug is refused at startup with the supported list. The same whitelist drives the inline `/model` picker, so the two surfaces never disagree about which models exist.
276+
261277
Sofos exposes six reasoning levels:
262278

263279
```text
@@ -278,20 +294,20 @@ sofos -e xhigh --model gpt-5.5 # Highest OpenAI gpt-5 reasoning level.
278294

279295
Support matrix:
280296

281-
| Effort | Opus 4.7 | Opus 4.6 | Sonnet 4.6 | Haiku 4.5 / older Claude | OpenAI gpt-5 reasoning models |
282-
|---|:---:|:---:|:---:|:---:|:---:|
283-
| `off` ||||||
284-
| `low` ||||||
285-
| `medium` ||||||
286-
| `high` ||||||
287-
| `xhigh` |||| | |
288-
| `max` ||| | ||
297+
| Effort | Opus 4.7 | Sonnet 4.6 | Haiku 4.5 | OpenAI gpt-5 reasoning models |
298+
|---|:---:|:---:|:---:|:---:|
299+
| `off` |||||
300+
| `low` |||||
301+
| `medium` |||||
302+
| `high` |||||
303+
| `xhigh` |||||
304+
| `max` |||||
289305

290306
Provider mapping:
291307

292308
- **OpenAI** sends `reasoning.effort`; `off` maps to minimal reasoning and suppresses reasoning summaries.
293-
- **Claude Opus 4.7, Opus 4.6, and Sonnet 4.6** use adaptive thinking. The provider chooses the token budget from the effort level.
294-
- **Older Claude models** use fixed legacy thinking budgets for `low`, `medium`, and `high`. `off` disables extended thinking.
309+
- **Claude Opus 4.7 and Sonnet 4.6** use adaptive thinking. The provider chooses the token budget from the effort level.
310+
- **Claude Haiku 4.5** uses fixed legacy thinking budgets for `low`, `medium`, and `high`. `off` disables extended thinking.
295311

296312
---
297313

@@ -323,7 +339,7 @@ Clipboard pastes are not routed through a tool: pressing Ctrl-V in the prompt at
323339

324340
### Safe mode tools
325341

326-
Safe mode is enabled with `--safe-mode` or `/s`. It restricts the native tool set to:
342+
Safe mode is enabled with `--safe-mode` or `/safe`. It restricts the native tool set to:
327343

328344
- `list_directory`;
329345
- `read_file`;

STRUCTURE.md

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -551,6 +551,8 @@ Rules:
551551

552552
It contains:
553553

554+
- the `SUPPORTED_MODELS` whitelist — every model id accepted by `--model` and shown in the `/model` picker, with its description and provider;
555+
- helpers `canonical_model`, `model_support_error`, and `supported_models_label` that share one source of truth with the CLI rejection message and the picker rows;
554556
- model registry entries;
555557
- context-window sizes;
556558
- auto-compaction thresholds;
@@ -562,7 +564,8 @@ It contains:
562564
Rules:
563565

564566
- Model capability checks should use this registry instead of hard-coded scattered checks.
565-
- Adding a supported model should be primarily a registry change plus any provider-specific wire support if needed.
567+
- Adding a supported model is one struct literal in `SUPPORTED_MODELS` — the `Model` struct carries the user-facing description and provider alongside the context window, effort matrix, and pricing, so there is no separate `lookup` table to keep in sync.
568+
- Removing a model is one deletion in `SUPPORTED_MODELS`. The CLI and the picker share that array as their source of truth, so nothing else has to be touched.
566569

567570
### 4.7 `api/truncate.rs`
568571

@@ -608,7 +611,7 @@ It contains:
608611
- available-tool refresh;
609612
- one-shot prompt execution;
610613
- status-line snapshots;
611-
- `/think`, `/s`, `/n`, and `/clear` state handlers;
614+
- `/think`, `/safe`, `/normal`, and `/clear` state handlers;
612615
- shared interrupt and mid-turn steering buffers.
613616

614617
Rules:
@@ -759,6 +762,11 @@ It contains:
759762
- `slash_popup.rs` — state for the inline slash-command suggestion list;
760763
- `sgr.rs` — SGR escape helpers.
761764

765+
The TUI also carries two modal pickers as fields on `app::App` and corresponding job/event variants:
766+
767+
- the resume picker (`Picker` + `UiEvent::ShowResumePicker` + `Job::ResumeSelected`) drives `/resume`;
768+
- the model picker (`ModelPicker` + `UiEvent::ShowModelPicker` + `Job::ModelSelected`) drives `/model`. Rows on the other provider are flagged unavailable on the `ModelPickerEntry`, the renderer greys them out, and the navigation helper in `app.rs` skips past them so the cursor only lands on a model the running session can switch to.
769+
762770
Rules:
763771

764772
- The TUI owns terminal interaction and event routing; it does not decide provider request structure.
@@ -1298,8 +1306,8 @@ Built-in commands include:
12981306
- `/clear`;
12991307
- `/compact`;
13001308
- `/think`;
1301-
- `/s`;
1302-
- `/n`;
1309+
- `/safe`;
1310+
- `/normal`;
13031311
- `/exit`, `/quit`, `/q`.
13041312

13051313
Rules:

src/api/anthropic/mod.rs

Lines changed: 15 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -59,19 +59,14 @@ mod tests {
5959
}
6060

6161
#[test]
62-
fn requires_adaptive_thinking_covers_all_1m_anthropic_models() {
63-
// Opus 4.7 requires adaptive (legacy shape 400s); Opus 4.6 and
64-
// Sonnet 4.6 accept both but Anthropic recommends adaptive,
65-
// so sofos opts them in too. Earlier 4.x models stay on the
66-
// manual budget shape.
62+
fn requires_adaptive_thinking_covers_supported_anthropic_models() {
63+
// Opus 4.7 requires adaptive (legacy shape 400s); Sonnet 4.6
64+
// accepts both shapes but Anthropic recommends adaptive, so
65+
// sofos opts it in too. Haiku 4.5 stays on the legacy
66+
// `budget_tokens` shape.
6767
assert!(requires_adaptive_thinking("claude-opus-4-7"));
68-
assert!(requires_adaptive_thinking("claude-opus-4-7-20260301"));
69-
assert!(requires_adaptive_thinking("claude-opus-4-6"));
7068
assert!(requires_adaptive_thinking("claude-sonnet-4-6"));
71-
assert!(!requires_adaptive_thinking("claude-opus-4-5"));
72-
assert!(!requires_adaptive_thinking("claude-sonnet-4-5"));
7369
assert!(!requires_adaptive_thinking("claude-haiku-4-5"));
74-
assert!(!requires_adaptive_thinking(""));
7570
}
7671

7772
#[test]
@@ -92,21 +87,16 @@ mod tests {
9287
#[test]
9388
fn anthropic_beta_for_matches_model_info_predicate() {
9489
// The beta header and the request body's `context_management`
95-
// are gated off the same `ModelInfo::supports_server_compaction`
96-
// flag. An earlier version used a separate prefix list here that
97-
// could disagree with `ModelInfo` — e.g. `claude-opus-4-5` would
98-
// pick up the compaction beta even though the body never carried
99-
// the matching field. Cross-check the two sources of truth so any
100-
// future drift surfaces here instead of as a wire-format 400.
101-
for model in [
102-
"claude-opus-4-7",
103-
"claude-opus-4-6",
104-
"claude-sonnet-4-6",
105-
"claude-haiku-4-5",
106-
"claude-opus-4-5",
107-
"claude-sonnet-3-7",
108-
"some-unknown-future-model",
109-
] {
90+
// are gated off the same `Model::supports_server_compaction`
91+
// flag. An earlier version used a separate prefix list here
92+
// that could disagree with the per-model record; cross-check
93+
// the two sources of truth so any future drift surfaces here
94+
// instead of as a wire-format 400. Iterates the whitelist so
95+
// every supported model is covered automatically.
96+
let supported_models = crate::api::model_info::SUPPORTED_MODELS
97+
.iter()
98+
.map(|m| m.name);
99+
for model in supported_models.chain(std::iter::once("some-unknown-future-model")) {
110100
let expected = crate::api::model_info::lookup(model).supports_server_compaction;
111101
let header = anthropic_beta_for(model);
112102
assert_eq!(

src/api/anthropic/wire.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ pub(super) const BETA_TOKEN_EFFICIENT_AND_COMPACT: &str =
3939
"token-efficient-tools-2025-02-19,compact-2026-01-12";
4040

4141
/// Pick the `anthropic-beta` value for `model`. Compaction is gated
42-
/// off the same `ModelInfo::supports_server_compaction` flag the
42+
/// off the same `Model::supports_server_compaction` flag the
4343
/// request builder uses to attach the `context_management` field, so
4444
/// the beta header and the body field can never disagree about which
4545
/// models speak server-side compaction.
@@ -73,8 +73,8 @@ pub fn legacy_thinking_budget(effort: ReasoningEffort) -> u32 {
7373
match effort {
7474
ReasoningEffort::Off | ReasoningEffort::Low => LEGACY_THINKING_BUDGET_LOW,
7575
ReasoningEffort::Medium => LEGACY_THINKING_BUDGET_MEDIUM,
76-
// Legacy-thinking models (Sonnet 4.5, Opus 4.5, Haiku 4.5) only
77-
// expose budget tiers up to High. `XHigh` and `Max` are
76+
// Legacy-thinking models (Haiku 4.5) only expose budget tiers
77+
// up to High. `XHigh` and `Max` are
7878
// adaptive-only rungs that upstream validation refuses to pair
7979
// with a legacy model, so this branch is unreachable in
8080
// practice; clamp defensively to the highest legal budget.

src/api/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ pub mod truncate;
66
pub mod types;
77
pub mod utils;
88

9-
pub use model_info::ModelInfo;
9+
pub use model_info::Model;
1010

1111
pub use anthropic::AnthropicClient;
1212
pub use morph::MorphClient;

0 commit comments

Comments
 (0)