Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d05bb87a23 | ||
|
|
52eca6477e | ||
|
|
8b87e11e53 | ||
|
|
62e1d53f60 | ||
|
|
39e0c14eec | ||
|
|
909c28d587 | ||
|
|
a2d2549df4 |
Generated
+1
-1
@@ -226,7 +226,7 @@ checksum = "8ae3f5d315924270530207e2a68396c3cc547f6dca3fbdca317cfb1a51edb593"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cassady"
|
name = "cassady"
|
||||||
version = "0.3.0"
|
version = "0.3.3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "cassady"
|
name = "cassady"
|
||||||
version = "0.3.0"
|
version = "0.3.3"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
description = "Cassady/Cass minimal terminal coding agent"
|
description = "Cassady/Cass minimal terminal coding agent"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
|
|||||||
@@ -80,10 +80,11 @@ Common in-chat commands:
|
|||||||
- `/branch` or `/restore`: open the branch/restore menu.
|
- `/branch` or `/restore`: open the branch/restore menu.
|
||||||
- `/login`: configure or update provider login settings.
|
- `/login`: configure or update provider login settings.
|
||||||
- `/logout`: remove saved provider config and associated model entries.
|
- `/logout`: remove saved provider config and associated model entries.
|
||||||
|
- `/fast`, `/fast on`, `/fast off`, `/fast status`: prefer faster inference when the active provider/model supports it. ChatGPT Codex models, including `gpt-5.5`, are treated as fast-capable.
|
||||||
- `/model <model>`: switch to a model from `~/.cass/models.json`.
|
- `/model <model>`: switch to a model from `~/.cass/models.json`.
|
||||||
- `/new`: create a new chat for the current directory.
|
- `/new`: create a new chat for the current directory.
|
||||||
- `/resume <chat>`: resume a saved chat for the current directory.
|
- `/resume <chat>`: resume a saved chat for the current directory.
|
||||||
- `/status`: show chat id, model, mode, cwd, record count, and current status.
|
- `/status`: show chat id, model, fast-mode state, mode, cwd, record count, and current status.
|
||||||
|
|
||||||
Helpful keys:
|
Helpful keys:
|
||||||
|
|
||||||
|
|||||||
+90
-3
@@ -1,6 +1,93 @@
|
|||||||
# Cassady (Cass) Roadmap
|
# Cassady (Cass) Roadmap
|
||||||
|
|
||||||
## v0.3.0 — ChatGPT Codex Provider
|
## v0.3.3 — Codex Fast-Mode Compatibility
|
||||||
|
|
||||||
|
This release focuses on keeping fast mode available for ChatGPT Codex users when local model metadata predates the fast-mode capability flag. Cassady should treat active `chatgpt-codex` provider models, including `gpt-5.5`, as fast-capable while leaving OpenAI-compatible and custom providers capability-gated by model metadata.
|
||||||
|
|
||||||
|
### Fast Mode Compatibility
|
||||||
|
|
||||||
|
- [x] **Treat ChatGPT Codex models as fast-capable at runtime.** Make `/fast` active for any active `chatgpt-codex` provider model even when legacy `models.json` metadata says `fast_mode.supported` is false.
|
||||||
|
- Keep non-Codex providers governed by their model metadata.
|
||||||
|
- Preserve the saved fast-mode preference behavior and status reporting.
|
||||||
|
|
||||||
|
- [x] **Document the Codex capability fallback.** Update README and bundled docs so users understand that ChatGPT Codex models, including `gpt-5.5`, can honor fast mode without refreshed metadata.
|
||||||
|
- Keep docs clear that provider-specific fast-mode request shaping remains Codex-only.
|
||||||
|
|
||||||
|
- [x] **Add regression coverage for legacy metadata.** Test that a ChatGPT Codex model with older `fast_mode.supported: false` metadata still reports fast mode as supported and active when preferred.
|
||||||
|
- Verify `cargo fmt` and `cargo test --locked --all-targets` pass before handoff.
|
||||||
|
|
||||||
|
## v0.3.4 — Tool Output Context Reliability
|
||||||
|
|
||||||
|
This release focuses on making large tool outputs easier for the assistant to recover from when model-context compaction or truncation hides important details. Cassady should guide the assistant toward smaller, targeted reads and searches, preserve enough provenance for follow-up inspection, and add regression coverage for broad-output workflows that previously stalled safe edits. See `plans/V0_3_4_TOOL_OUTPUT_CONTEXT_RELIABILITY_PLAN.md`.
|
||||||
|
|
||||||
|
### Model Context Recovery
|
||||||
|
|
||||||
|
- [ ] **Improve compacted tool-output guidance.** Replace generic head/tail compaction notices with actionable guidance that tells the assistant what was omitted and how to inspect it again safely.
|
||||||
|
- Include tool name, output size, retained excerpt shape, and suggested narrower follow-up reads or searches when available.
|
||||||
|
- Keep model-facing guidance concise enough that it does not worsen context pressure.
|
||||||
|
|
||||||
|
- [ ] **Preserve targeted reinspection metadata.** Track enough structured context for large reads and command output so the assistant can recover omitted details without repeating broad requests.
|
||||||
|
- For file reads, preserve path and line-range coverage even after compaction.
|
||||||
|
- For shell and search output, prefer guidance toward narrower commands or `grep`/`read` follow-ups rather than blindly rerunning the same broad command.
|
||||||
|
|
||||||
|
### Tool Behavior and Prompting
|
||||||
|
|
||||||
|
- [ ] **Bias tool use toward smaller inspections.** Update tool descriptions, prompt guidance, and result messages so broad reads become a fallback rather than the default.
|
||||||
|
- Encourage search-first workflows for large files and unknown locations.
|
||||||
|
- Mention result limits before or at truncation points so the assistant knows when context may be incomplete.
|
||||||
|
|
||||||
|
- [ ] **Make truncation and compaction visible across layers.** Align model-facing messages, stored conversation records, and UI summaries so users and the assistant can tell when output was incomplete.
|
||||||
|
- Do not let UI-only collapsed output change what is stored or sent to the model.
|
||||||
|
- Keep existing conversation files readable and resumable.
|
||||||
|
|
||||||
|
### Validation
|
||||||
|
|
||||||
|
- [ ] **Add regression coverage for broad-output recovery.** Test workflows where an early broad read or command output is compacted before the assistant needs exact context for an edit.
|
||||||
|
- Cover superseded reads, compacted non-newest tool outputs, provider-message validity, and suggested follow-up guidance.
|
||||||
|
- Verify `cargo fmt` and `cargo test --locked --all-targets` pass before handoff.
|
||||||
|
|
||||||
|
## v0.3.2 — Provider Fast Mode
|
||||||
|
|
||||||
|
This release focuses on adding a `/fast` command that lets users prefer faster inference when the active provider/model supports it. The first supported provider is `ChatGPT Codex`; other providers can add their own fast-mode request behavior later without changing the user-facing command. See `plans/V0_3_2_FAST_MODE_PLAN.md`.
|
||||||
|
|
||||||
|
### Fast Mode Command
|
||||||
|
|
||||||
|
- [ ] **Add a persisted `/fast` preference.** Let users toggle fast mode from an idle chat and keep that preference across sessions.
|
||||||
|
- Store the preference in `config.json` without disturbing provider/model configuration.
|
||||||
|
- Keep the preference separate from whether the current provider/model can honor it.
|
||||||
|
|
||||||
|
- [ ] **Show capability-aware fast-mode status.** Display fast mode as enabled only when the active provider/model supports it.
|
||||||
|
- If the user switches to an unsupported provider/model, hide the enabled state and report fast mode as unavailable when relevant.
|
||||||
|
- If the user switches back to a supported provider/model, apply the existing preference again.
|
||||||
|
|
||||||
|
### Provider Support
|
||||||
|
|
||||||
|
- [ ] **Implement ChatGPT Codex fast-mode requests.** Add the provider-specific request option for Codex when fast mode is active.
|
||||||
|
- Verify and test the exact Codex responses request shape during implementation.
|
||||||
|
- Do not send Codex-specific fast-mode fields to OpenAI-compatible providers.
|
||||||
|
|
||||||
|
- [ ] **Add extensible provider/model capability metadata.** Model fast-mode support as provider/model metadata so future providers can opt in case by case.
|
||||||
|
- Default unknown and custom providers to unsupported.
|
||||||
|
- Mark built-in ChatGPT Codex model metadata as supported when Cassady can send the fast-mode request.
|
||||||
|
|
||||||
|
### Documentation and Validation
|
||||||
|
|
||||||
|
- [ ] **Document fast mode behavior and limits.** Update README and bundled docs for `/fast`, `default_fast_mode`, model capability metadata, and Codex-only initial support.
|
||||||
|
- Explain the difference between a saved fast-mode preference and active fast-mode support.
|
||||||
|
|
||||||
|
- [ ] **Test fast-mode preference, switching, and provider requests.** Cover command parsing, persistence, status rendering, model/provider switching, and Codex request body behavior.
|
||||||
|
- Verify `cargo fmt` and `cargo test --locked --all-targets` pass before handoff.
|
||||||
|
|
||||||
|
## v0.3.1 — Transcript Scroll Stability
|
||||||
|
|
||||||
|
This release focuses on keeping the live transcript anchored correctly above the input and footer during long sessions with blank reasoning or tool-output lines.
|
||||||
|
|
||||||
|
### TUI Reliability
|
||||||
|
|
||||||
|
- [x] **Fix bottom-scroll row counting for whitespace-only lines.** Count indented blank transcript rows the same way Ratatui renders them so accumulated blank rows no longer hide recent transcript content above the footer.
|
||||||
|
- Add regression coverage for whitespace-only wrapped row counting.
|
||||||
|
|
||||||
|
## v0.3.0 — ChatGPT Codex Provider ✅ Completed
|
||||||
|
|
||||||
This release focuses on letting users who are already signed in to Codex with a ChatGPT subscription use that account from Cassady. `ChatGPT Codex` becomes a provider preset that calls the Codex responses endpoint and reads its bearer token from local Codex auth instead of an API-key environment variable. See `plans/V0_3_0_CHATGPT_CODEX_PROVIDER_PLAN.md`.
|
This release focuses on letting users who are already signed in to Codex with a ChatGPT subscription use that account from Cassady. `ChatGPT Codex` becomes a provider preset that calls the Codex responses endpoint and reads its bearer token from local Codex auth instead of an API-key environment variable. See `plans/V0_3_0_CHATGPT_CODEX_PROVIDER_PLAN.md`.
|
||||||
|
|
||||||
@@ -34,7 +121,7 @@ This release focuses on letting users who are already signed in to Codex with a
|
|||||||
- [x] **Test Codex auth and provider behavior.** Cover Codex auth fixtures, config validation, setup catalog behavior, provider dispatch, streaming response parsing, tool calls, and secret redaction.
|
- [x] **Test Codex auth and provider behavior.** Cover Codex auth fixtures, config validation, setup catalog behavior, provider dispatch, streaming response parsing, tool calls, and secret redaction.
|
||||||
- Verify `cargo fmt` and `cargo test --locked --all-targets` pass before handoff.
|
- Verify `cargo fmt` and `cargo test --locked --all-targets` pass before handoff.
|
||||||
|
|
||||||
## v0.2.9 — Provider Login Management
|
## v0.2.9 — Provider Login Management ✅ Completed
|
||||||
|
|
||||||
This release focuses on making provider configuration available from both the shell and an active Cassady chat. Users can add or update OpenAI-compatible provider/model settings with `cass login` or `/login`, and remove saved providers and their associated models with `cass logout` or `/logout`. See `plans/V0_2_9_PROVIDER_LOGIN_MANAGEMENT_PLAN.md`.
|
This release focuses on making provider configuration available from both the shell and an active Cassady chat. Users can add or update OpenAI-compatible provider/model settings with `cass login` or `/login`, and remove saved providers and their associated models with `cass logout` or `/logout`. See `plans/V0_2_9_PROVIDER_LOGIN_MANAGEMENT_PLAN.md`.
|
||||||
|
|
||||||
@@ -66,7 +153,7 @@ This release focuses on making provider configuration available from both the sh
|
|||||||
- [x] **Test provider management behavior.** Cover provider/model removal, active default repair, local command parsing, and autocomplete.
|
- [x] **Test provider management behavior.** Cover provider/model removal, active default repair, local command parsing, and autocomplete.
|
||||||
- Verify `cargo fmt` and `cargo test --locked --all-targets` pass before handoff.
|
- Verify `cargo fmt` and `cargo test --locked --all-targets` pass before handoff.
|
||||||
|
|
||||||
## v0.2.8 — Conversation Branch and Restore
|
## v0.2.8 — Conversation Branch and Restore ✅ Completed
|
||||||
|
|
||||||
This release focuses on making conversation recovery safe and explorable. Pressing `Esc` twice while idle opens a branch/restore menu where users can browse prior user messages, assistant messages, and tool calls, create a new branch from a selected checkpoint, and optionally restore Cassady-tracked file edits without destroying the original conversation. See `plans/V0_2_8_CONVERSATION_BRANCH_RESTORE_PLAN.md`.
|
This release focuses on making conversation recovery safe and explorable. Pressing `Esc` twice while idle opens a branch/restore menu where users can browse prior user messages, assistant messages, and tool calls, create a new branch from a selected checkpoint, and optionally restore Cassady-tracked file edits without destroying the original conversation. See `plans/V0_2_8_CONVERSATION_BRANCH_RESTORE_PLAN.md`.
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -8,7 +8,7 @@ Cassady tools may list, search, and read this directory. Mutating tools are bloc
|
|||||||
|
|
||||||
- [Commands](commands.md): CLI forms, global flags, `cass update`, in-chat commands, and keys.
|
- [Commands](commands.md): CLI forms, global flags, `cass update`, in-chat commands, and keys.
|
||||||
- [Configuration](configuration.md): `~/.cass` files, setup, precedence, schema examples, and validation.
|
- [Configuration](configuration.md): `~/.cass` files, setup, precedence, schema examples, and validation.
|
||||||
- [Providers and models](providers.md): built-in provider presets, custom OpenAI-compatible endpoints, ChatGPT Codex auth, model discovery, and reasoning metadata.
|
- [Providers and models](providers.md): built-in provider presets, custom OpenAI-compatible endpoints, ChatGPT Codex auth, model discovery, reasoning metadata, and fast-mode support.
|
||||||
- [Access modes and tool safety](access-modes.md): what tools can read, write, edit, and run in each mode.
|
- [Access modes and tool safety](access-modes.md): what tools can read, write, edit, and run in each mode.
|
||||||
- [Experimental Rust embedding API](embedding.md): import Cassady from Rust, start headless sessions, stream events, and handle approvals.
|
- [Experimental Rust embedding API](embedding.md): import Cassady from Rust, start headless sessions, stream events, and handle approvals.
|
||||||
- [Workflows](workflows.md): common ways to inspect code, apply edits, run checks, switch models, and resume chats.
|
- [Workflows](workflows.md): common ways to inspect code, apply edits, run checks, switch models, and resume chats.
|
||||||
|
|||||||
+2
-1
@@ -107,12 +107,13 @@ The updater does not invoke `sudo` or administrator prompts. If the install dire
|
|||||||
Type `/` to open command autocomplete.
|
Type `/` to open command autocomplete.
|
||||||
|
|
||||||
- `/branch` or `/restore`: open the branch/restore menu for the current conversation family.
|
- `/branch` or `/restore`: open the branch/restore menu for the current conversation family.
|
||||||
|
- `/fast`, `/fast on`, `/fast off`, `/fast status`: toggle or inspect a persisted fast-mode preference. Fast mode is active only when the current provider/model supports it; ChatGPT Codex models, including `gpt-5.5`, are treated as fast-capable.
|
||||||
- `/login`: configure or update provider login settings, then reload active provider/model config.
|
- `/login`: configure or update provider login settings, then reload active provider/model config.
|
||||||
- `/logout`: remove saved providers and their associated models, then reload active provider/model config when any remain.
|
- `/logout`: remove saved providers and their associated models, then reload active provider/model config when any remain.
|
||||||
- `/model <model>`: switch the model for future turns. Autocomplete lists models from `~/.cass/models.json`.
|
- `/model <model>`: switch the model for future turns. Autocomplete lists models from `~/.cass/models.json`.
|
||||||
- `/new`: create a new chat for the current directory.
|
- `/new`: create a new chat for the current directory.
|
||||||
- `/resume <chat>`: resume a saved chat from the current directory. Autocomplete lists matching chats.
|
- `/resume <chat>`: resume a saved chat from the current directory. Autocomplete lists matching chats.
|
||||||
- `/status`: show chat id, state, model, access mode, cwd, record count, and current status.
|
- `/status`: show chat id, state, model, fast-mode state, access mode, cwd, record count, and current status.
|
||||||
|
|
||||||
Local commands can be used only when the agent is idle.
|
Local commands can be used only when the agent is idle.
|
||||||
|
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ Example:
|
|||||||
"default_provider": "openai",
|
"default_provider": "openai",
|
||||||
"default_model": "gpt-4.1",
|
"default_model": "gpt-4.1",
|
||||||
"default_reasoning_effort": "medium",
|
"default_reasoning_effort": "medium",
|
||||||
|
"default_fast_mode": false,
|
||||||
"default_access_mode": "read-only",
|
"default_access_mode": "read-only",
|
||||||
"context_message_limit": 80,
|
"context_message_limit": 80,
|
||||||
"model_tool_result_limit": 24000,
|
"model_tool_result_limit": 24000,
|
||||||
@@ -56,6 +57,7 @@ Fields:
|
|||||||
- `default_provider`: optional provider id from `providers.json`. If omitted, Cassady infers the provider from `default_model` when possible.
|
- `default_provider`: optional provider id from `providers.json`. If omitted, Cassady infers the provider from `default_model` when possible.
|
||||||
- `default_model`: optional model id to use by default.
|
- `default_model`: optional model id to use by default.
|
||||||
- `default_reasoning_effort`: optional `off`, `low`, `medium`, or `high`, clamped to model metadata.
|
- `default_reasoning_effort`: optional `off`, `low`, `medium`, or `high`, clamped to model metadata.
|
||||||
|
- `default_fast_mode`: optional boolean, defaults to `false`. When `true`, Cassady requests faster inference only for provider/model combinations that advertise fast-mode support.
|
||||||
- `default_access_mode`: `"read-only"`, `"workspace-edit"`, or `"full-access"`.
|
- `default_access_mode`: `"read-only"`, `"workspace-edit"`, or `"full-access"`.
|
||||||
- `context_message_limit`: optional legacy upper bound for recent non-system messages. Cassady primarily budgets context from model metadata and trims along valid tool-call boundaries.
|
- `context_message_limit`: optional legacy upper bound for recent non-system messages. Cassady primarily budgets context from model metadata and trims along valid tool-call boundaries.
|
||||||
- `model_tool_result_limit`: optional max bytes of tool output sent back to the model.
|
- `model_tool_result_limit`: optional max bytes of tool output sent back to the model.
|
||||||
@@ -136,6 +138,9 @@ Example:
|
|||||||
"required": false,
|
"required": false,
|
||||||
"default_effort": "medium",
|
"default_effort": "medium",
|
||||||
"request_format": "reasoning_effort"
|
"request_format": "reasoning_effort"
|
||||||
|
},
|
||||||
|
"fast_mode": {
|
||||||
|
"supported": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -156,9 +161,13 @@ Fields:
|
|||||||
- `required`: optional boolean, defaults to `false`.
|
- `required`: optional boolean, defaults to `false`.
|
||||||
- `default_effort`: optional `off`, `low`, `medium`, or `high`; defaults to `medium`. Cannot effectively be `off` when `required` is `true`.
|
- `default_effort`: optional `off`, `low`, `medium`, or `high`; defaults to `medium`. Cannot effectively be `off` when `required` is `true`.
|
||||||
- `request_format`: optional `reasoning_effort` or `reasoning_object`; defaults to `reasoning_effort`.
|
- `request_format`: optional `reasoning_effort` or `reasoning_object`; defaults to `reasoning_effort`.
|
||||||
|
- `fast_mode`: optional object. Defaults to unsupported.
|
||||||
|
- `supported`: optional boolean, defaults to `false`. Cassady treats active `chatgpt-codex` provider models as fast-capable even if older metadata says otherwise; custom and OpenAI-compatible model entries default to unsupported.
|
||||||
|
|
||||||
Reasoning effort is a runtime per-turn setting. Press `Tab` to cycle it while idle. Provider-streamed reasoning is persisted and sent back in future model context using the provider's reasoning field, such as `reasoning_content` or `reasoning`.
|
Reasoning effort is a runtime per-turn setting. Press `Tab` to cycle it while idle. Provider-streamed reasoning is persisted and sent back in future model context using the provider's reasoning field, such as `reasoning_content` or `reasoning`.
|
||||||
|
|
||||||
|
Fast mode is a persisted preference, not a guarantee. Use `/fast` to toggle it while idle. `/status` shows `enabled` only when the preference is on and the current provider/model can honor it; otherwise it reports `off` or `preferred, unavailable ...`. Fast-mode request shaping is implemented only for `chatgpt-codex`.
|
||||||
|
|
||||||
## Precedence
|
## Precedence
|
||||||
|
|
||||||
- CLI access-mode flags override `default_access_mode` for the current session.
|
- CLI access-mode flags override `default_access_mode` for the current session.
|
||||||
|
|||||||
+3
-1
@@ -14,9 +14,11 @@
|
|||||||
|
|
||||||
**Exact edit**: An `edit` tool replacement where each `old_text` must match exactly once in the original file before anything is written.
|
**Exact edit**: An `edit` tool replacement where each `old_text` must match exactly once in the original file before anything is written.
|
||||||
|
|
||||||
|
**Fast mode**: A saved preference enabled with `/fast`. It is active only when the current provider/model advertises fast-mode support; otherwise Cassady keeps the preference but reports it as unavailable.
|
||||||
|
|
||||||
**Global instructions**: Optional text in `~/.cass/global.md` included in new chat system prompts. Cassady follows these instructions when they fit the active request, but they cannot override runtime safety constraints such as access modes, tool denials, approvals, or workspace boundaries.
|
**Global instructions**: Optional text in `~/.cass/global.md` included in new chat system prompts. Cassady follows these instructions when they fit the active request, but they cannot override runtime safety constraints such as access modes, tool denials, approvals, or workspace boundaries.
|
||||||
|
|
||||||
**Model metadata**: The `models.json` entry describing a model id, owning provider, display name, context limits, tool/streaming support, and reasoning behavior.
|
**Model metadata**: The `models.json` entry describing a model id, owning provider, display name, context limits, tool/streaming support, reasoning behavior, and fast-mode support.
|
||||||
|
|
||||||
**OpenAI-compatible provider**: A provider exposing an API compatible with the OpenAI-style chat/completions behavior Cassady uses.
|
**OpenAI-compatible provider**: A provider exposing an API compatible with the OpenAI-style chat/completions behavior Cassady uses.
|
||||||
|
|
||||||
|
|||||||
+12
-2
@@ -74,7 +74,8 @@ Provider protocols that are not OpenAI-compatible are supported only when Cassad
|
|||||||
- display name;
|
- display name;
|
||||||
- context length and max output tokens;
|
- context length and max output tokens;
|
||||||
- tool and streaming support;
|
- tool and streaming support;
|
||||||
- reasoning support and request format.
|
- reasoning support and request format;
|
||||||
|
- fast-mode support.
|
||||||
|
|
||||||
`config.json` selects active defaults, such as `default_provider`, `default_model`, and `default_access_mode`.
|
`config.json` selects active defaults, such as `default_provider`, `default_model`, and `default_access_mode`.
|
||||||
|
|
||||||
@@ -90,6 +91,15 @@ Reasoning metadata controls how the runtime reasoning effort behaves:
|
|||||||
|
|
||||||
Reasoning display is separate. `show_reasoning` controls whether provider-streamed reasoning is visible in the transcript; press `Ctrl-Shift-R` or `Ctrl-R` to toggle it at runtime.
|
Reasoning display is separate. `show_reasoning` controls whether provider-streamed reasoning is visible in the transcript; press `Ctrl-Shift-R` or `Ctrl-R` to toggle it at runtime.
|
||||||
|
|
||||||
|
## Fast-mode metadata
|
||||||
|
|
||||||
|
Fast mode has two parts:
|
||||||
|
|
||||||
|
- `default_fast_mode` in `config.json`: the user's saved preference.
|
||||||
|
- `fast_mode.supported` in `models.json`: whether non-Codex provider/model metadata can honor that preference.
|
||||||
|
|
||||||
|
Cassady sends fast-mode requests only for `ChatGPT Codex`. Any active `chatgpt-codex` provider model, including `gpt-5.5`, is treated as fast-capable so older model metadata does not block the feature. OpenAI-compatible and custom model entries default to unsupported, so `/fast` can remember the preference without sending provider-specific fields.
|
||||||
|
|
||||||
## Switching models
|
## Switching models
|
||||||
|
|
||||||
Use one of these approaches:
|
Use one of these approaches:
|
||||||
@@ -104,7 +114,7 @@ or inside a chat:
|
|||||||
/model MODEL
|
/model MODEL
|
||||||
```
|
```
|
||||||
|
|
||||||
The in-chat model autocomplete lists entries from `~/.cass/models.json`. Switching the model also updates the default model and reasoning effort in `config.json` for future sessions.
|
The in-chat model autocomplete lists entries from `~/.cass/models.json`. Switching the model also updates the default provider, default model, and reasoning effort in `config.json` for future sessions. If fast mode is preferred, Cassady recomputes whether it is active after the switch.
|
||||||
|
|
||||||
## Health checks
|
## Health checks
|
||||||
|
|
||||||
|
|||||||
+13
-1
@@ -95,7 +95,7 @@ Inside a chat:
|
|||||||
/model MODEL_ID
|
/model MODEL_ID
|
||||||
```
|
```
|
||||||
|
|
||||||
Autocomplete lists models from `~/.cass/models.json`. Switching models is allowed only when idle. Cassady persists the last used model and reasoning effort into `config.json`.
|
Autocomplete lists models from `~/.cass/models.json`. Switching models is allowed only when idle. Cassady persists the last used provider, model, and reasoning effort into `config.json`.
|
||||||
|
|
||||||
You can also launch with a model override:
|
You can also launch with a model override:
|
||||||
|
|
||||||
@@ -103,6 +103,18 @@ You can also launch with a model override:
|
|||||||
cass --model MODEL_ID
|
cass --model MODEL_ID
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Prefer fast mode
|
||||||
|
|
||||||
|
Inside a chat:
|
||||||
|
|
||||||
|
```text
|
||||||
|
/fast
|
||||||
|
```
|
||||||
|
|
||||||
|
Use `/fast on`, `/fast off`, or `/fast status` when you want an explicit action. Cassady saves the preference in `config.json`, but fast mode is active only when the current provider/model supports it. ChatGPT Codex models, including `gpt-5.5`, are treated as fast-capable.
|
||||||
|
|
||||||
|
Switching to an unsupported provider/model keeps the preference but makes `/status` show fast mode as unavailable. Switching back to ChatGPT Codex enables it again.
|
||||||
|
|
||||||
## Resume a chat
|
## Resume a chat
|
||||||
|
|
||||||
List chats for the current directory:
|
List chats for the current directory:
|
||||||
|
|||||||
@@ -0,0 +1,285 @@
|
|||||||
|
# v0.3.2 Fast Mode Implementation Plan
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
v0.3.2 adds a `/fast` command that lets users opt into faster provider inference when the active provider/model supports it. The setting should feel like a user preference, but the runtime state should be capability-aware: fast mode is shown as enabled only when the current provider/model can actually honor it.
|
||||||
|
|
||||||
|
Success statement:
|
||||||
|
|
||||||
|
> A ChatGPT Codex user can type `/fast`, see fast mode enabled for Codex models that support it, switch to an unsupported provider/model and see fast mode become unavailable, then switch back and have the preference apply again.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
### In scope
|
||||||
|
|
||||||
|
- Add an idle-only `/fast` local command that toggles the user's fast-mode preference.
|
||||||
|
- Persist the preference in Cassady config so it survives new chats and restarts.
|
||||||
|
- Add provider/model capability metadata that determines whether fast mode is currently active.
|
||||||
|
- Implement fast-mode request support for `ChatGPT Codex` first.
|
||||||
|
- Keep unsupported providers/models explicit: the preference can remain on, but the UI/status should say fast mode is unavailable rather than enabled.
|
||||||
|
- Update `/status`, the bottom/status line, command autocomplete/help, README, and bundled docs.
|
||||||
|
- Add focused tests for command parsing, persistence, capability gating, provider request shaping, and model switching.
|
||||||
|
|
||||||
|
### Out of scope
|
||||||
|
|
||||||
|
- Adding fast-mode support for OpenAI-compatible providers in v0.3.2.
|
||||||
|
- Guessing provider-specific fast-mode request fields without verified behavior.
|
||||||
|
- Adding latency benchmarking, automatic mode selection, or per-turn speed/quality controls beyond the `/fast` toggle.
|
||||||
|
- Changing the default model selection flow except to record fast-mode capability for known built-in presets.
|
||||||
|
- Treating fast mode as a quality guarantee; providers may still vary in latency and output behavior.
|
||||||
|
|
||||||
|
## Context or Current State
|
||||||
|
|
||||||
|
Cassady already has several runtime preferences and model/provider capability paths that should guide this work:
|
||||||
|
|
||||||
|
- `src/app.rs` parses local slash commands such as `/model`, `/login`, `/logout`, and `/status`, and already restricts provider/model changes to idle state.
|
||||||
|
- `src/config.rs` persists default model and reasoning effort in `config.json` and stores model metadata in `models.json`.
|
||||||
|
- `ModelDefinition` already includes capability-like metadata such as `supports_tools`, `supports_streaming`, and `reasoning`.
|
||||||
|
- `ProviderClient::from_config` in `src/providers/mod.rs` dispatches by provider kind to `OpenAiCompatibleProvider` or `ChatGptCodexProvider`.
|
||||||
|
- `src/providers/chatgpt_codex.rs` is the first provider where fast mode should affect the outgoing request.
|
||||||
|
- `/model` already reloads model metadata and resets reasoning effort based on the new model.
|
||||||
|
- `/status` currently reports chat id, model, mode, cwd, records, and current status.
|
||||||
|
|
||||||
|
The important product behavior is that "fast mode preference" and "fast mode active" are different states:
|
||||||
|
|
||||||
|
- Preference: whether the user wants fast mode when possible.
|
||||||
|
- Active: whether the current provider/model supports fast mode and the preference is enabled.
|
||||||
|
|
||||||
|
## Design Principles
|
||||||
|
|
||||||
|
1. **Preference stays stable, capability controls activation.** Switching to an unsupported provider/model should not erase the user's preference; it should only make fast mode inactive until support is available again.
|
||||||
|
2. **Provider-specific request details stay behind provider clients.** The agent loop should pass a normalized fast-mode flag; each provider decides whether and how to encode it.
|
||||||
|
3. **UI wording must distinguish enabled from unavailable.** Avoid showing "fast mode enabled" when Cassady cannot send a fast-mode request for the active provider/model.
|
||||||
|
4. **Extend metadata, do not hardcode every check in the TUI.** Use provider/model capability helpers so future providers can add fast mode without rewriting command handling.
|
||||||
|
5. **Keep unsupported behavior quiet and compatible.** Existing OpenAI-compatible providers should continue working unchanged and should not receive unknown request fields.
|
||||||
|
|
||||||
|
## Design
|
||||||
|
|
||||||
|
### User model
|
||||||
|
|
||||||
|
Add a user preference to `config.json`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"default_fast_mode": true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Suggested behavior:
|
||||||
|
|
||||||
|
- Missing `default_fast_mode` defaults to `false`.
|
||||||
|
- `/fast` toggles the preference while idle.
|
||||||
|
- `/fast on` and `/fast off` may be supported if easy, but the minimum required command is the toggle form.
|
||||||
|
- The preference is persisted immediately, similar to last-used model/reasoning persistence.
|
||||||
|
- The active state is recomputed whenever config, provider, model, or model metadata changes.
|
||||||
|
|
||||||
|
Status examples:
|
||||||
|
|
||||||
|
```text
|
||||||
|
fast mode: enabled
|
||||||
|
fast mode: unavailable for provider fireworks
|
||||||
|
fast mode: off
|
||||||
|
```
|
||||||
|
|
||||||
|
If the user toggles fast mode on while using an unsupported provider:
|
||||||
|
|
||||||
|
```text
|
||||||
|
fast mode preference on; unavailable for this provider/model
|
||||||
|
```
|
||||||
|
|
||||||
|
If the user later switches to a supported Codex model, the UI should show:
|
||||||
|
|
||||||
|
```text
|
||||||
|
fast mode enabled
|
||||||
|
```
|
||||||
|
|
||||||
|
### Capability model
|
||||||
|
|
||||||
|
Add a small capability representation, preferably on model metadata with provider-kind fallback:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "gpt-5.5",
|
||||||
|
"provider": "chatgpt-codex",
|
||||||
|
"fast_mode": {
|
||||||
|
"supported": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
|
||||||
|
- `fast_mode.supported` defaults to `false` unless a provider-specific built-in preset intentionally marks it true.
|
||||||
|
- For the `ChatGPT Codex` built-in setup path, saved Codex model metadata should mark fast mode as supported when the implementation can send the fast-mode request for that provider.
|
||||||
|
- Manual custom providers and discovered OpenAI-compatible models should default to unsupported.
|
||||||
|
- If a provider supports fast mode for all models but model metadata is missing, provider-specific capability fallback may return supported. Keep that fallback in config/provider capability helpers, not in UI string matching.
|
||||||
|
|
||||||
|
Add helper APIs along these lines:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub struct FastModeState {
|
||||||
|
pub preferred: bool,
|
||||||
|
pub supported: bool,
|
||||||
|
pub active: bool,
|
||||||
|
pub unavailable_reason: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Config {
|
||||||
|
pub fn fast_mode_state(&self) -> FastModeState;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`active` should be exactly `preferred && supported`.
|
||||||
|
|
||||||
|
### Provider request behavior
|
||||||
|
|
||||||
|
Thread the active fast-mode boolean through the provider settings:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
ProviderClient::from_config(&config, reasoning_effort, fast_mode_active)
|
||||||
|
```
|
||||||
|
|
||||||
|
or include it in a runtime options struct if that is cleaner:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub struct ProviderRuntimeOptions {
|
||||||
|
pub reasoning_effort: ReasoningEffort,
|
||||||
|
pub fast_mode: bool,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`ChatGptCodexProvider` should encode fast mode using the verified Codex responses request shape. During implementation, verify the exact field against the current Codex behavior and capture the resulting request body in tests. The plan intentionally does not prescribe a speculative field name.
|
||||||
|
|
||||||
|
OpenAI-compatible providers should ignore the setting until support is explicitly added. They must not receive experimental Codex-only fields.
|
||||||
|
|
||||||
|
### UI and command behavior
|
||||||
|
|
||||||
|
Add `LocalCommand::Fast(FastModeCommand)` in `src/app.rs`.
|
||||||
|
|
||||||
|
Minimum command behavior:
|
||||||
|
|
||||||
|
```text
|
||||||
|
/fast
|
||||||
|
```
|
||||||
|
|
||||||
|
Recommended optional forms:
|
||||||
|
|
||||||
|
```text
|
||||||
|
/fast on
|
||||||
|
/fast off
|
||||||
|
/fast status
|
||||||
|
```
|
||||||
|
|
||||||
|
Command handling:
|
||||||
|
|
||||||
|
- Only allow changes while idle.
|
||||||
|
- Persist the preference to `config.json`.
|
||||||
|
- Recompute active state from the current provider/model after toggling.
|
||||||
|
- Append or show a concise status message.
|
||||||
|
- Include `/fast` in autocomplete/local command help.
|
||||||
|
|
||||||
|
Update `/status` to include both preference and active state, for example:
|
||||||
|
|
||||||
|
```text
|
||||||
|
fast: enabled
|
||||||
|
```
|
||||||
|
|
||||||
|
or:
|
||||||
|
|
||||||
|
```text
|
||||||
|
fast: preferred, unavailable for provider fireworks
|
||||||
|
```
|
||||||
|
|
||||||
|
The bottom/status line should include a compact signal only when useful:
|
||||||
|
|
||||||
|
- `fast` when active.
|
||||||
|
- No `fast` label when off.
|
||||||
|
- Optional `fast unavailable` only immediately after toggling or in `/status`, to avoid clutter.
|
||||||
|
|
||||||
|
### Model and provider switching
|
||||||
|
|
||||||
|
When `/model` changes the active model:
|
||||||
|
|
||||||
|
- Reload `config.model_metadata` as today.
|
||||||
|
- Recompute reasoning effort as today.
|
||||||
|
- Recompute fast-mode state.
|
||||||
|
- Show the model status with fast-mode state when the preference is on.
|
||||||
|
|
||||||
|
Expected examples:
|
||||||
|
|
||||||
|
```text
|
||||||
|
model: gpt-5.5 · fast enabled
|
||||||
|
model: accounts/fireworks/models/qwen3p7-plus · fast unavailable
|
||||||
|
```
|
||||||
|
|
||||||
|
When `/login` or `/logout` updates provider config:
|
||||||
|
|
||||||
|
- Reload config as today.
|
||||||
|
- Preserve `default_fast_mode`.
|
||||||
|
- Recompute fast-mode state for the new active provider/model.
|
||||||
|
|
||||||
|
### Configuration and docs
|
||||||
|
|
||||||
|
Update docs to describe:
|
||||||
|
|
||||||
|
- `default_fast_mode` in `config.json`.
|
||||||
|
- `fast_mode.supported` in `models.json`.
|
||||||
|
- `/fast` command behavior.
|
||||||
|
- Provider support status: v0.3.2 supports fast mode only for `ChatGPT Codex`.
|
||||||
|
- The distinction between fast-mode preference and active fast-mode support.
|
||||||
|
|
||||||
|
## Implementation Steps
|
||||||
|
|
||||||
|
1. Extend config types in `src/config.rs`:
|
||||||
|
- Add `default_fast_mode: Option<bool>` to the config file representation.
|
||||||
|
- Add `fast_mode` metadata to model definitions.
|
||||||
|
- Add a `FastModeState` helper that computes preferred/supported/active.
|
||||||
|
2. Add persistence helpers:
|
||||||
|
- Save fast-mode preference without disturbing unrelated config fields.
|
||||||
|
- Ensure existing `save_last_used` behavior preserves the new field.
|
||||||
|
3. Mark built-in `ChatGPT Codex` model metadata as fast-mode capable during setup/login when the provider implementation supports it.
|
||||||
|
4. Add provider runtime options and pass `fast_mode_state.active` into `ProviderClient` construction from `src/agent.rs`.
|
||||||
|
5. Implement Codex request support in `src/providers/chatgpt_codex.rs` using the verified fast-mode request shape.
|
||||||
|
6. Keep `OpenAiCompatibleProvider` behavior unchanged and add tests proving it does not receive fast-mode fields.
|
||||||
|
7. Add `/fast` parsing and idle command handling in `src/app.rs`, including optional `on`, `off`, and `status` forms if the implementation remains small.
|
||||||
|
8. Update `/status`, status-line rendering, autocomplete/help text, and model-switch status messages.
|
||||||
|
9. Update `README.md`, `docs/commands.md`, `docs/configuration.md`, `docs/providers.md`, `docs/workflows.md`, and `docs/glossary.md`.
|
||||||
|
10. Add focused tests and run `cargo fmt` plus `cargo test --locked --all-targets`.
|
||||||
|
|
||||||
|
## Tests
|
||||||
|
|
||||||
|
- `config.json` without `default_fast_mode` defaults to fast mode off.
|
||||||
|
- `default_fast_mode: true` loads and persists without losing existing config fields.
|
||||||
|
- `models.json` parses `fast_mode.supported`.
|
||||||
|
- Unsupported or missing fast-mode metadata produces `FastModeState { preferred: true, supported: false, active: false }`.
|
||||||
|
- A supported ChatGPT Codex model produces `active: true` when preference is on.
|
||||||
|
- `/fast` parses as a local command and rejects unexpected arguments unless explicit `on`/`off`/`status` forms are implemented.
|
||||||
|
- `/fast` cannot change preference during an active turn.
|
||||||
|
- Toggling `/fast` while on an unsupported provider stores the preference but reports unavailable.
|
||||||
|
- Switching from a supported Codex model to an unsupported model hides the active fast-mode signal without clearing the preference.
|
||||||
|
- Switching back to a supported Codex model restores active fast mode.
|
||||||
|
- `ChatGptCodexProvider` includes the verified fast-mode request field only when active.
|
||||||
|
- `OpenAiCompatibleProvider` request bodies are unchanged when fast-mode preference is on but unsupported.
|
||||||
|
- `/status` includes fast-mode state.
|
||||||
|
- README and bundled docs mention `/fast` and provider-specific support.
|
||||||
|
- `cargo fmt` passes.
|
||||||
|
- `cargo test --locked --all-targets` passes when practical.
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
- README command list and provider setup notes.
|
||||||
|
- `docs/commands.md` for `/fast` syntax and idle-only behavior.
|
||||||
|
- `docs/configuration.md` for `default_fast_mode` and `models.json` fast-mode capability metadata.
|
||||||
|
- `docs/providers.md` for the initial ChatGPT Codex-only support.
|
||||||
|
- `docs/workflows.md` for switching models/providers with fast-mode preference preserved.
|
||||||
|
- `docs/glossary.md` for "Fast mode" as a preference plus provider/model capability.
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
- `/fast` toggles a persisted fast-mode preference.
|
||||||
|
- Fast mode is shown as enabled only when the active provider/model supports it.
|
||||||
|
- Unsupported providers/models do not show fast mode enabled and do not receive fast-mode request fields.
|
||||||
|
- ChatGPT Codex requests include the verified fast-mode option when the preference is on and the active Codex model supports it.
|
||||||
|
- Switching models/providers recomputes fast-mode active state without clearing the user's preference.
|
||||||
|
- `/status`, autocomplete/help, README, and bundled docs describe the feature accurately.
|
||||||
|
- `cargo fmt` and `cargo test --locked --all-targets` pass before implementation handoff.
|
||||||
@@ -0,0 +1,175 @@
|
|||||||
|
# v0.3.4 Tool Output Context Reliability Implementation Plan
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
v0.3.4 makes Cassady more reliable after broad tool output has been truncated, compacted, or superseded in the model context. The assistant should be able to tell when details are missing, understand which file range or command produced them, and quickly recover by using narrower reads or searches instead of stalling or making unsafe edits from incomplete context.
|
||||||
|
|
||||||
|
Success statement:
|
||||||
|
|
||||||
|
> After a large read or command output is compacted out of the request context, the assistant receives concise recovery guidance with enough provenance to inspect the exact missing area again before editing.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
### In scope
|
||||||
|
|
||||||
|
- Improve model-facing compaction notes for large tool outputs.
|
||||||
|
- Preserve useful provenance for compacted `read`, `grep`, and `shell` outputs.
|
||||||
|
- Add focused guidance that nudges the assistant toward smaller line ranges and search-first workflows.
|
||||||
|
- Keep existing provider message structure valid when tool outputs are compacted or earlier records are omitted.
|
||||||
|
- Align UI summaries, stored records, and model-facing transformed output so truncation/compaction is understandable without changing the full conversation history.
|
||||||
|
- Add regression tests for broad-output recovery and context-budget trimming.
|
||||||
|
- Update README and bundled docs where they describe context management, tool output limits, and recommended inspection workflows.
|
||||||
|
|
||||||
|
### Out of scope
|
||||||
|
|
||||||
|
- Implementing semantic summarization with an additional model call.
|
||||||
|
- Replacing Cassady's approximate token estimator with provider-specific tokenizers.
|
||||||
|
- Adding a full retrieval index over prior tool outputs or repository contents.
|
||||||
|
- Changing the JSONL conversation storage format in a way that makes existing chats unreadable.
|
||||||
|
- Changing UI collapsed-tool behavior except where labels or summaries need to expose truncation/compaction status.
|
||||||
|
- Automatically editing files based on compacted output without reinspection.
|
||||||
|
|
||||||
|
## Context or Current State
|
||||||
|
|
||||||
|
Relevant current behavior:
|
||||||
|
|
||||||
|
- `src/agent.rs` converts conversation records into provider messages, supersedes older repeated read outputs, compacts older tool outputs with `compact_tool_outputs`, and trims records with `trim_to_context_budget` and `trim_to_message_limit`.
|
||||||
|
- `compact_text` currently emits a generic head/tail note: `Cass compacted this tool output from ... chars to fit the model context`.
|
||||||
|
- `superseded_read_note` already preserves file path and line range when a later read covers an earlier read range.
|
||||||
|
- `src/tools/read.rs` returns headers like `--- path lines start-end ---`, followed by numbered lines. This is good provenance, but compaction can obscure the most useful middle section.
|
||||||
|
- `src/tools/grep.rs` already recommends narrowing when `max_matches` is reached.
|
||||||
|
- `src/tools/shell.rs` returns complete stdout/stderr/exit-code text to the agent loop; if the output is large, current compaction does not know the original command or suggest a narrower command.
|
||||||
|
- `src/ui/render.rs` has separate collapsed/full tool-output presentation. That display choice must remain UI-only and must not affect the stored record or model-facing context.
|
||||||
|
|
||||||
|
The main reliability gap is that once a broad output has been compacted, the assistant may see only a generic excerpt and lose the clue needed to make the next targeted tool call.
|
||||||
|
|
||||||
|
## Design Principles
|
||||||
|
|
||||||
|
1. **Never hide incompleteness.** If Cassady compacts or truncates output before sending it to the model, the transformed content must say so plainly.
|
||||||
|
2. **Recovery beats summarization.** Prefer actionable provenance and follow-up instructions over trying to summarize omitted content heuristically.
|
||||||
|
3. **Keep guidance compact.** The fix must not consume enough context to make context pressure worse.
|
||||||
|
4. **Preserve valid provider conversations.** Tool result messages must still match their assistant tool calls after compaction and trimming.
|
||||||
|
5. **Do not mutate history for UI convenience.** JSONL records should retain original tool outputs unless a future storage migration explicitly changes that contract.
|
||||||
|
|
||||||
|
## Design
|
||||||
|
|
||||||
|
### Model-facing compaction notes
|
||||||
|
|
||||||
|
Replace the generic `compact_text(content, target_chars)` path with a metadata-aware formatter, for example:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
struct ToolOutputCompactionHint {
|
||||||
|
tool_name: Option<String>,
|
||||||
|
original_chars: usize,
|
||||||
|
retained_head_chars: usize,
|
||||||
|
retained_tail_chars: usize,
|
||||||
|
provenance: ToolOutputProvenance,
|
||||||
|
}
|
||||||
|
|
||||||
|
enum ToolOutputProvenance {
|
||||||
|
Read { sections: Vec<ReadOutputSection> },
|
||||||
|
Grep { stopped_after: Option<usize> },
|
||||||
|
Shell { command: Option<String> },
|
||||||
|
Unknown,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The first implementation can infer provenance from the tool result text and nearby conversation/tool-call data rather than changing stored record schemas.
|
||||||
|
|
||||||
|
Example compacted read result:
|
||||||
|
|
||||||
|
```text
|
||||||
|
[Cass compacted this read output from 48,212 chars to fit the model context. The omitted content came from src/app.rs lines 1-1820. Use read with a narrower line range, or grep for a symbol before reading, before relying on omitted details.]
|
||||||
|
--- retained head excerpt ---
|
||||||
|
...
|
||||||
|
--- omitted middle ---
|
||||||
|
--- retained tail excerpt ---
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
Example compacted shell result:
|
||||||
|
|
||||||
|
```text
|
||||||
|
[Cass compacted this shell output from 81,004 chars to fit the model context. Rerun a narrower command, pipe through grep/head/tail, or inspect the specific files named in the excerpt before making edits based on omitted lines.]
|
||||||
|
```
|
||||||
|
|
||||||
|
Keep notes deterministic and short; avoid per-line summaries of omitted content.
|
||||||
|
|
||||||
|
### Read-output provenance
|
||||||
|
|
||||||
|
Reuse and extend the existing `ReadOutputSection` parsing in `src/agent.rs`:
|
||||||
|
|
||||||
|
- Detect every `--- path lines start-end ---` section before compaction.
|
||||||
|
- Preserve observed line coverage from numbered lines when available.
|
||||||
|
- Include one compact range summary in compaction notes:
|
||||||
|
- Single section: `path lines 35-220`.
|
||||||
|
- Multiple sections: `3 read sections including path_a lines 1-120 and path_b lines 40-90`.
|
||||||
|
- When a later read supersedes an earlier range, continue using the current superseded-read note and ensure tests cover interaction with compaction.
|
||||||
|
|
||||||
|
### Grep and shell guidance
|
||||||
|
|
||||||
|
For `grep` output:
|
||||||
|
|
||||||
|
- Preserve existing `… stopped after N matches` text.
|
||||||
|
- If compacted, add a note suggesting a narrower query, smaller path scope, lower `max_matches`, or a focused `read` around matching lines.
|
||||||
|
|
||||||
|
For `shell` output:
|
||||||
|
|
||||||
|
- If the tool-call arguments are available in the message conversion path, include a sanitized command preview in the note when reasonably short.
|
||||||
|
- Suggest command narrowing patterns without prescribing platform-specific syntax unless the command itself is already shell-specific, e.g. `grep`, `head`, `tail`, or a more targeted subcommand.
|
||||||
|
- Do not rerun shell commands automatically.
|
||||||
|
|
||||||
|
### Prompt and tool descriptions
|
||||||
|
|
||||||
|
Update the base prompt and tool descriptions only enough to reinforce reliable behavior:
|
||||||
|
|
||||||
|
- Prefer `grep` before broad `read` when the target location is unknown.
|
||||||
|
- Read smaller line ranges when files are large or when previous output says it was compacted.
|
||||||
|
- Treat compacted/truncated output as incomplete evidence; reinspect before editing.
|
||||||
|
|
||||||
|
Avoid bloating `src/prompt.rs`; keep additions short and test expected key phrases rather than full prompt text.
|
||||||
|
|
||||||
|
### UI and storage alignment
|
||||||
|
|
||||||
|
- Stored JSONL should keep the original tool result content.
|
||||||
|
- Model-facing transformed messages may contain compacted/superseded notes.
|
||||||
|
- UI collapsed mode should keep using summaries, but summaries should not imply the model saw the full output when it did not.
|
||||||
|
- If practical, make collapsed tool summaries include a compact `compacted` or `truncated` marker only when the stored/result text itself says that Cassady truncated or stopped output.
|
||||||
|
|
||||||
|
## Implementation Steps
|
||||||
|
|
||||||
|
1. Inspect provider-message conversion in `src/agent.rs` and identify where tool-call names/arguments are still available when compacting tool results.
|
||||||
|
2. Refactor `compact_text` into metadata-aware helpers that can produce deterministic compaction notes for read, grep, shell, and unknown outputs.
|
||||||
|
3. Reuse existing read-section parsing to build concise read range summaries for compacted read outputs.
|
||||||
|
4. Add shell and grep-specific recovery guidance based on tool name and output markers.
|
||||||
|
5. Preserve the newest tool result behavior unless tests show that the newest result can still exceed practical context limits; if changed, document the tradeoff explicitly.
|
||||||
|
6. Update `src/tools/read.rs`, `src/tools/grep.rs`, and `src/prompt.rs` descriptions with concise search-first and narrow-range guidance.
|
||||||
|
7. Add or update UI summary helpers in `src/ui/render.rs` only if needed to expose stored truncation/compaction markers consistently.
|
||||||
|
8. Update README and bundled docs for context reliability, broad-output recovery, and recommended inspection workflow.
|
||||||
|
9. Run `cargo fmt` and `cargo test --locked --all-targets`.
|
||||||
|
|
||||||
|
## Tests
|
||||||
|
|
||||||
|
- Large read output compacts to a note that includes original size, path, line range, and a narrower-read/search suggestion.
|
||||||
|
- Multi-file read output compacts to a concise multi-section provenance summary.
|
||||||
|
- Superseded read output still produces the superseded note and does not lose provider-message validity after context trimming.
|
||||||
|
- Large grep output compaction preserves or adds narrowing guidance.
|
||||||
|
- Large shell output compaction suggests rerunning a narrower command and does not include unsafe automatic actions.
|
||||||
|
- Context-budget trimming does not leave orphaned tool results or assistant tool calls.
|
||||||
|
- Stored conversation records retain original tool output while model-facing messages can be compacted.
|
||||||
|
- Prompt/tool spec tests verify the presence of concise search-first and reinspection guidance.
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
- Update `README.md` where tool output/context behavior is described.
|
||||||
|
- Update `docs/workflows.md` with recommended search-first and narrow-read workflows.
|
||||||
|
- Update `docs/troubleshooting.md` with recovery steps for compacted or truncated output.
|
||||||
|
- Update `docs/glossary.md` if terms such as compacted output, superseded read, or model-facing context need clarification.
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
- Compacted tool outputs include actionable provenance and recovery guidance.
|
||||||
|
- Broad read and command-output workflows have regression coverage demonstrating safe reinspection before edits.
|
||||||
|
- Existing conversations remain loadable and resumable.
|
||||||
|
- Provider message conversion remains valid for tool-call/tool-result pairs after compaction and trimming.
|
||||||
|
- `cargo fmt` and `cargo test --locked --all-targets` pass.
|
||||||
+8
-2
@@ -3,7 +3,7 @@ use crate::config::{Config, ReasoningEffort};
|
|||||||
use crate::conversation::{now_ts, Conversation, Record, StoredToolCall};
|
use crate::conversation::{now_ts, Conversation, Record, StoredToolCall};
|
||||||
use crate::prompt;
|
use crate::prompt;
|
||||||
use crate::providers::types::ModelMessage;
|
use crate::providers::types::ModelMessage;
|
||||||
use crate::providers::ProviderClient;
|
use crate::providers::{ProviderClient, ProviderRuntimeOptions};
|
||||||
use crate::security::PolicyDecision;
|
use crate::security::PolicyDecision;
|
||||||
use crate::tools::{self, ToolContext, ToolRuntimeEvent};
|
use crate::tools::{self, ToolContext, ToolRuntimeEvent};
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
@@ -88,7 +88,13 @@ pub async fn run_turn_with_commands(
|
|||||||
let reasoning_effort = settings
|
let reasoning_effort = settings
|
||||||
.reasoning_effort
|
.reasoning_effort
|
||||||
.clamp_for_model(settings.config.model_metadata.as_ref());
|
.clamp_for_model(settings.config.model_metadata.as_ref());
|
||||||
let provider = match ProviderClient::from_config(&settings.config, reasoning_effort) {
|
let provider = match ProviderClient::from_config(
|
||||||
|
&settings.config,
|
||||||
|
ProviderRuntimeOptions {
|
||||||
|
reasoning_effort,
|
||||||
|
fast_mode: settings.config.fast_mode_state().active,
|
||||||
|
},
|
||||||
|
) {
|
||||||
Ok(provider) => provider,
|
Ok(provider) => provider,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
append_visible_assistant(
|
append_visible_assistant(
|
||||||
|
|||||||
+225
-13
@@ -1,6 +1,6 @@
|
|||||||
use crate::agent::{self, AgentCommand, AgentEvent, AgentSettings};
|
use crate::agent::{self, AgentCommand, AgentEvent, AgentSettings};
|
||||||
use crate::cli::{self, Cli, Command};
|
use crate::cli::{self, Cli, Command};
|
||||||
use crate::config::{self, Config, ModelDefinition, ReasoningEffort};
|
use crate::config::{self, Config, FastModeState, ModelDefinition, ReasoningEffort};
|
||||||
use crate::conversation::{self, Conversation, Record};
|
use crate::conversation::{self, Conversation, Record};
|
||||||
use crate::prompt;
|
use crate::prompt;
|
||||||
use crate::ui::autofill::{AutoFillItem, AutoFillMenu};
|
use crate::ui::autofill::{AutoFillItem, AutoFillMenu};
|
||||||
@@ -397,6 +397,7 @@ async fn run_tui(
|
|||||||
show_full_tools,
|
show_full_tools,
|
||||||
show_reasoning,
|
show_reasoning,
|
||||||
reasoning_effort,
|
reasoning_effort,
|
||||||
|
fast_mode_active: config.fast_mode_state().active,
|
||||||
scroll,
|
scroll,
|
||||||
autofill: if branch_menu.is_some() {
|
autofill: if branch_menu.is_some() {
|
||||||
None
|
None
|
||||||
@@ -862,10 +863,46 @@ async fn run_tui(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Ok(LocalCommand::Fast(command)) => {
|
||||||
|
if busy {
|
||||||
|
status = "fast mode can be changed when idle".into();
|
||||||
|
} else {
|
||||||
|
input.clear();
|
||||||
|
autofill_selected = 0;
|
||||||
|
match apply_fast_mode_command(&mut config, command) {
|
||||||
|
Ok(message) => {
|
||||||
|
transcript.push(TranscriptBlock {
|
||||||
|
kind: TranscriptKind::Status,
|
||||||
|
title: "fast".into(),
|
||||||
|
content: message.clone(),
|
||||||
|
});
|
||||||
|
status = message;
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
status =
|
||||||
|
format!("fast mode update failed: {err}");
|
||||||
|
transcript.push(TranscriptBlock {
|
||||||
|
kind: TranscriptKind::Error,
|
||||||
|
title: "fast".into(),
|
||||||
|
content: err.to_string(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if stick_to_bottom {
|
||||||
|
scroll = bottom_scroll(
|
||||||
|
&terminal,
|
||||||
|
&input,
|
||||||
|
&transcript,
|
||||||
|
show_full_tools,
|
||||||
|
show_reasoning,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Ok(LocalCommand::Status) => {
|
Ok(LocalCommand::Status) => {
|
||||||
let content = chat_status(
|
let content = chat_status(
|
||||||
&chat_id,
|
&chat_id,
|
||||||
&config.model,
|
&config,
|
||||||
mode,
|
mode,
|
||||||
&cwd,
|
&cwd,
|
||||||
busy,
|
busy,
|
||||||
@@ -894,14 +931,13 @@ async fn run_tui(
|
|||||||
if busy {
|
if busy {
|
||||||
status = "model can be changed when idle".into();
|
status = "model can be changed when idle".into();
|
||||||
} else {
|
} else {
|
||||||
config.model = model.clone();
|
apply_model_selection(&mut config, &model)?;
|
||||||
config.model_metadata =
|
|
||||||
model_metadata_for(&config, &model)?;
|
|
||||||
reasoning_effort = ReasoningEffort::default_for_model(
|
reasoning_effort = ReasoningEffort::default_for_model(
|
||||||
config.model_metadata.as_ref(),
|
config.model_metadata.as_ref(),
|
||||||
);
|
);
|
||||||
let _ = crate::config::save_last_used(
|
let _ = crate::config::save_last_used_provider(
|
||||||
&config.root,
|
&config.root,
|
||||||
|
&config.provider_id,
|
||||||
&config.model,
|
&config.model,
|
||||||
reasoning_effort,
|
reasoning_effort,
|
||||||
);
|
);
|
||||||
@@ -910,9 +946,9 @@ async fn run_tui(
|
|||||||
transcript.push(TranscriptBlock {
|
transcript.push(TranscriptBlock {
|
||||||
kind: TranscriptKind::Status,
|
kind: TranscriptKind::Status,
|
||||||
title: "model".into(),
|
title: "model".into(),
|
||||||
content: format!("model changed to {model}"),
|
content: model_status_message(&config),
|
||||||
});
|
});
|
||||||
status = format!("model: {model}");
|
status = model_status_message(&config);
|
||||||
if stick_to_bottom {
|
if stick_to_bottom {
|
||||||
scroll = bottom_scroll(
|
scroll = bottom_scroll(
|
||||||
&terminal,
|
&terminal,
|
||||||
@@ -1885,6 +1921,7 @@ fn assistant_content_matches(a: &str, b: &str) -> bool {
|
|||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
enum LocalCommand {
|
enum LocalCommand {
|
||||||
Branch,
|
Branch,
|
||||||
|
Fast(FastModeCommand),
|
||||||
Login,
|
Login,
|
||||||
Logout,
|
Logout,
|
||||||
Model(String),
|
Model(String),
|
||||||
@@ -1893,6 +1930,14 @@ enum LocalCommand {
|
|||||||
Status,
|
Status,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
enum FastModeCommand {
|
||||||
|
Toggle,
|
||||||
|
On,
|
||||||
|
Off,
|
||||||
|
Status,
|
||||||
|
}
|
||||||
|
|
||||||
struct CommandSpec {
|
struct CommandSpec {
|
||||||
name: &'static str,
|
name: &'static str,
|
||||||
usage: &'static str,
|
usage: &'static str,
|
||||||
@@ -1907,6 +1952,12 @@ const COMMANDS: &[CommandSpec] = &[
|
|||||||
description: "open branch/restore menu",
|
description: "open branch/restore menu",
|
||||||
takes_value: false,
|
takes_value: false,
|
||||||
},
|
},
|
||||||
|
CommandSpec {
|
||||||
|
name: "fast",
|
||||||
|
usage: "/fast [on|off|status]",
|
||||||
|
description: "toggle faster Codex inference when supported",
|
||||||
|
takes_value: false,
|
||||||
|
},
|
||||||
CommandSpec {
|
CommandSpec {
|
||||||
name: "login",
|
name: "login",
|
||||||
usage: "/login",
|
usage: "/login",
|
||||||
@@ -2038,14 +2089,38 @@ fn model_autofill(input: &str, selected: usize, config: &Config) -> Result<Optio
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn model_metadata_for(config: &Config, model_id: &str) -> Result<Option<ModelDefinition>> {
|
fn apply_model_selection(config: &mut Config, model_id: &str) -> Result<()> {
|
||||||
let models = crate::config::load_or_create_default_model_registry(&config.root)?;
|
let models = crate::config::load_or_create_default_model_registry(&config.root)?;
|
||||||
Ok(models
|
let metadata = models
|
||||||
.models
|
.models
|
||||||
.iter()
|
.iter()
|
||||||
.find(|model| model.id == model_id && model.provider == config.provider_id)
|
.find(|model| model.id == model_id && model.provider == config.provider_id)
|
||||||
.cloned()
|
.cloned()
|
||||||
.or_else(|| models.models.into_iter().find(|model| model.id == model_id)))
|
.or_else(|| {
|
||||||
|
models
|
||||||
|
.models
|
||||||
|
.iter()
|
||||||
|
.find(|model| model.id == model_id)
|
||||||
|
.cloned()
|
||||||
|
});
|
||||||
|
|
||||||
|
if let Some(model) = &metadata {
|
||||||
|
if model.provider != config.provider_id {
|
||||||
|
let providers = crate::config::load_or_create_default_provider_registry(&config.root)?;
|
||||||
|
if let Some(provider) = providers
|
||||||
|
.providers
|
||||||
|
.iter()
|
||||||
|
.find(|provider| provider.id == model.provider)
|
||||||
|
{
|
||||||
|
config.provider_id = provider.id.clone();
|
||||||
|
config.active_provider = provider.to_resolved();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
config.model = model_id.to_string();
|
||||||
|
config.model_metadata = metadata;
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn model_matches(model: &ModelDefinition, query: &str) -> bool {
|
fn model_matches(model: &ModelDefinition, query: &str) -> bool {
|
||||||
@@ -2175,6 +2250,19 @@ fn parse_local_command(input: &str) -> std::result::Result<LocalCommand, String>
|
|||||||
}
|
}
|
||||||
Ok(LocalCommand::Branch)
|
Ok(LocalCommand::Branch)
|
||||||
}
|
}
|
||||||
|
"/fast" => {
|
||||||
|
let command = match parts.next() {
|
||||||
|
None => FastModeCommand::Toggle,
|
||||||
|
Some("on") => FastModeCommand::On,
|
||||||
|
Some("off") => FastModeCommand::Off,
|
||||||
|
Some("status") => FastModeCommand::Status,
|
||||||
|
Some(_) => return Err("usage: /fast [on|off|status]".into()),
|
||||||
|
};
|
||||||
|
if parts.next().is_some() {
|
||||||
|
return Err("usage: /fast [on|off|status]".into());
|
||||||
|
}
|
||||||
|
Ok(LocalCommand::Fast(command))
|
||||||
|
}
|
||||||
"/login" => {
|
"/login" => {
|
||||||
if parts.next().is_some() {
|
if parts.next().is_some() {
|
||||||
return Err("usage: /login".into());
|
return Err("usage: /login".into());
|
||||||
@@ -2223,7 +2311,7 @@ fn parse_local_command(input: &str) -> std::result::Result<LocalCommand, String>
|
|||||||
|
|
||||||
fn chat_status(
|
fn chat_status(
|
||||||
chat_id: &str,
|
chat_id: &str,
|
||||||
model: &str,
|
config: &Config,
|
||||||
mode: crate::access::AccessMode,
|
mode: crate::access::AccessMode,
|
||||||
cwd: &Path,
|
cwd: &Path,
|
||||||
busy: bool,
|
busy: bool,
|
||||||
@@ -2231,13 +2319,76 @@ fn chat_status(
|
|||||||
record_count: usize,
|
record_count: usize,
|
||||||
) -> String {
|
) -> String {
|
||||||
format!(
|
format!(
|
||||||
"chat: {chat_id}\nstate: {}\nmodel: {model}\nmode: {mode}\ncwd: {}\nrecords: {record_count}\nstatus: {}",
|
"chat: {chat_id}\nstate: {}\nmodel: {}\nfast: {}\nmode: {mode}\ncwd: {}\nrecords: {record_count}\nstatus: {}",
|
||||||
if busy { "running" } else { "idle" },
|
if busy { "running" } else { "idle" },
|
||||||
|
config.model,
|
||||||
|
fast_mode_status(&config.fast_mode_state()),
|
||||||
cwd.display(),
|
cwd.display(),
|
||||||
if status.is_empty() { "idle" } else { status }
|
if status.is_empty() { "idle" } else { status }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn apply_fast_mode_command(config: &mut Config, command: FastModeCommand) -> Result<String> {
|
||||||
|
match command {
|
||||||
|
FastModeCommand::Status => Ok(fast_mode_status(&config.fast_mode_state())),
|
||||||
|
FastModeCommand::Toggle | FastModeCommand::On | FastModeCommand::Off => {
|
||||||
|
let enabled = match command {
|
||||||
|
FastModeCommand::Toggle => !config.default_fast_mode,
|
||||||
|
FastModeCommand::On => true,
|
||||||
|
FastModeCommand::Off => false,
|
||||||
|
FastModeCommand::Status => unreachable!(),
|
||||||
|
};
|
||||||
|
crate::config::save_fast_mode_preference(&config.root, enabled)?;
|
||||||
|
config.default_fast_mode = enabled;
|
||||||
|
Ok(fast_mode_change_message(&config.fast_mode_state()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fast_mode_change_message(state: &FastModeState) -> String {
|
||||||
|
if state.active {
|
||||||
|
"fast mode enabled".into()
|
||||||
|
} else if state.preferred {
|
||||||
|
format!(
|
||||||
|
"fast mode preference on; unavailable for {}",
|
||||||
|
state
|
||||||
|
.unavailable_reason
|
||||||
|
.as_deref()
|
||||||
|
.unwrap_or("this provider/model")
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
"fast mode off".into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fast_mode_status(state: &FastModeState) -> String {
|
||||||
|
if state.active {
|
||||||
|
"enabled".into()
|
||||||
|
} else if state.preferred {
|
||||||
|
format!(
|
||||||
|
"preferred, unavailable for {}",
|
||||||
|
state
|
||||||
|
.unavailable_reason
|
||||||
|
.as_deref()
|
||||||
|
.unwrap_or("this provider/model")
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
"off".into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn model_status_message(config: &Config) -> String {
|
||||||
|
let model = &config.model;
|
||||||
|
let state = config.fast_mode_state();
|
||||||
|
if state.active {
|
||||||
|
format!("model: {model} · fast enabled")
|
||||||
|
} else if state.preferred {
|
||||||
|
format!("model: {model} · fast unavailable")
|
||||||
|
} else {
|
||||||
|
format!("model: {model}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn transcript_from_loaded(
|
fn transcript_from_loaded(
|
||||||
conversation: &Conversation,
|
conversation: &Conversation,
|
||||||
warning: Option<String>,
|
warning: Option<String>,
|
||||||
@@ -2667,6 +2818,67 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_local_command_accepts_fast_forms() {
|
||||||
|
assert_eq!(
|
||||||
|
parse_local_command("/fast").unwrap(),
|
||||||
|
LocalCommand::Fast(FastModeCommand::Toggle)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
parse_local_command("/fast on").unwrap(),
|
||||||
|
LocalCommand::Fast(FastModeCommand::On)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
parse_local_command("/fast off").unwrap(),
|
||||||
|
LocalCommand::Fast(FastModeCommand::Off)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
parse_local_command("/fast status").unwrap(),
|
||||||
|
LocalCommand::Fast(FastModeCommand::Status)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
parse_local_command("/fast maybe"),
|
||||||
|
Err("usage: /fast [on|off|status]".into())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn fast_mode_status_distinguishes_preference_and_activation() {
|
||||||
|
let mut config = Config {
|
||||||
|
default_fast_mode: true,
|
||||||
|
..Config::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
fast_mode_status(&config.fast_mode_state()),
|
||||||
|
"preferred, unavailable for provider fireworks"
|
||||||
|
);
|
||||||
|
|
||||||
|
config.provider_id = config::CHATGPT_CODEX_PROVIDER_ID.into();
|
||||||
|
config.active_provider.kind = config::CHATGPT_CODEX_PROVIDER_KIND.into();
|
||||||
|
config.model = config::CHATGPT_CODEX_DEFAULT_MODEL.into();
|
||||||
|
config.model_metadata = Some(config::ModelDefinition {
|
||||||
|
id: config::CHATGPT_CODEX_DEFAULT_MODEL.into(),
|
||||||
|
provider: config::CHATGPT_CODEX_PROVIDER_ID.into(),
|
||||||
|
display_name: None,
|
||||||
|
context_length: None,
|
||||||
|
max_output_tokens: None,
|
||||||
|
supports_tools: true,
|
||||||
|
supports_streaming: true,
|
||||||
|
reasoning: Default::default(),
|
||||||
|
fast_mode: config::FastModeMetadata { supported: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
assert_eq!(fast_mode_status(&config.fast_mode_state()), "enabled");
|
||||||
|
assert_eq!(
|
||||||
|
model_status_message(&config),
|
||||||
|
format!(
|
||||||
|
"model: {} · fast enabled",
|
||||||
|
config::CHATGPT_CODEX_DEFAULT_MODEL
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn cancelled_turn_repairs_missing_tool_results() {
|
fn cancelled_turn_repairs_missing_tool_results() {
|
||||||
let root = tempdir().unwrap();
|
let root = tempdir().unwrap();
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ pub struct ConfigFile {
|
|||||||
pub default_model: Option<String>,
|
pub default_model: Option<String>,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub default_reasoning_effort: Option<ReasoningEffort>,
|
pub default_reasoning_effort: Option<ReasoningEffort>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub default_fast_mode: Option<bool>,
|
||||||
|
|
||||||
// Deprecated compatibility fields accepted from older config.json files.
|
// Deprecated compatibility fields accepted from older config.json files.
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
@@ -97,6 +99,8 @@ pub struct ModelDefinition {
|
|||||||
pub supports_streaming: bool,
|
pub supports_streaming: bool,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub reasoning: ReasoningMetadata,
|
pub reasoning: ReasoningMetadata,
|
||||||
|
#[serde(default)]
|
||||||
|
pub fast_mode: FastModeMetadata,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
@@ -112,6 +116,13 @@ pub struct ReasoningMetadata {
|
|||||||
pub request_format: ReasoningRequestFormat,
|
pub request_format: ReasoningRequestFormat,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
|
pub struct FastModeMetadata {
|
||||||
|
#[serde(default)]
|
||||||
|
pub supported: bool,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
#[serde(rename_all = "lowercase")]
|
#[serde(rename_all = "lowercase")]
|
||||||
pub enum ReasoningEffort {
|
pub enum ReasoningEffort {
|
||||||
@@ -145,6 +156,7 @@ pub struct Config {
|
|||||||
pub provider_id: String,
|
pub provider_id: String,
|
||||||
pub model: String,
|
pub model: String,
|
||||||
pub reasoning_effort: ReasoningEffort,
|
pub reasoning_effort: ReasoningEffort,
|
||||||
|
pub default_fast_mode: bool,
|
||||||
pub active_provider: ResolvedProviderConfig,
|
pub active_provider: ResolvedProviderConfig,
|
||||||
pub model_metadata: Option<ModelDefinition>,
|
pub model_metadata: Option<ModelDefinition>,
|
||||||
pub default_access_mode: AccessMode,
|
pub default_access_mode: AccessMode,
|
||||||
@@ -157,6 +169,14 @@ pub struct Config {
|
|||||||
pub docs_dir: PathBuf,
|
pub docs_dir: PathBuf,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct FastModeState {
|
||||||
|
pub preferred: bool,
|
||||||
|
pub supported: bool,
|
||||||
|
pub active: bool,
|
||||||
|
pub unavailable_reason: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Default)]
|
#[derive(Debug, Clone, Default)]
|
||||||
pub struct ConfigOverrides {
|
pub struct ConfigOverrides {
|
||||||
pub model: Option<String>,
|
pub model: Option<String>,
|
||||||
@@ -202,6 +222,12 @@ impl Default for ReasoningMetadata {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Default for FastModeMetadata {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self { supported: false }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Default for ReasoningRequestFormat {
|
impl Default for ReasoningRequestFormat {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self::ReasoningEffort
|
Self::ReasoningEffort
|
||||||
@@ -287,6 +313,7 @@ impl Default for Config {
|
|||||||
provider_id: DEFAULT_PROVIDER_ID.to_string(),
|
provider_id: DEFAULT_PROVIDER_ID.to_string(),
|
||||||
model: DEFAULT_MODEL.to_string(),
|
model: DEFAULT_MODEL.to_string(),
|
||||||
reasoning_effort: ReasoningEffort::Medium,
|
reasoning_effort: ReasoningEffort::Medium,
|
||||||
|
default_fast_mode: false,
|
||||||
active_provider,
|
active_provider,
|
||||||
model_metadata: Some(default_model_definition()),
|
model_metadata: Some(default_model_definition()),
|
||||||
default_access_mode: AccessMode::ReadOnly,
|
default_access_mode: AccessMode::ReadOnly,
|
||||||
@@ -374,6 +401,9 @@ impl Config {
|
|||||||
if let Some(v) = file.confirm_destructive_operations {
|
if let Some(v) = file.confirm_destructive_operations {
|
||||||
cfg.confirm_destructive_operations = v;
|
cfg.confirm_destructive_operations = v;
|
||||||
}
|
}
|
||||||
|
if let Some(v) = file.default_fast_mode {
|
||||||
|
cfg.default_fast_mode = v;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(access_mode) = overrides.access_mode {
|
if let Some(access_mode) = overrides.access_mode {
|
||||||
@@ -443,6 +473,41 @@ impl Config {
|
|||||||
kind => bail!("unsupported provider kind `{kind}`"),
|
kind => bail!("unsupported provider kind `{kind}`"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn fast_mode_state(&self) -> FastModeState {
|
||||||
|
let preferred = self.default_fast_mode;
|
||||||
|
let supported = self.fast_mode_supported();
|
||||||
|
let active = preferred && supported;
|
||||||
|
let unavailable_reason = if preferred && !supported {
|
||||||
|
Some(self.fast_mode_unavailable_reason())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
FastModeState {
|
||||||
|
preferred,
|
||||||
|
supported,
|
||||||
|
active,
|
||||||
|
unavailable_reason,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fast_mode_supported(&self) -> bool {
|
||||||
|
if self.active_provider.kind == CHATGPT_CODEX_PROVIDER_KIND {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.model_metadata
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|model| model.fast_mode.supported)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fast_mode_unavailable_reason(&self) -> String {
|
||||||
|
if self.active_provider.kind != CHATGPT_CODEX_PROVIDER_KIND {
|
||||||
|
format!("provider {}", self.provider_id)
|
||||||
|
} else {
|
||||||
|
format!("model {}", self.model)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ProviderDefinition {
|
impl ProviderDefinition {
|
||||||
@@ -483,6 +548,28 @@ pub fn save_last_used(root: &Path, model: &str, reasoning_effort: ReasoningEffor
|
|||||||
file.default_reasoning_effort = Some(reasoning_effort);
|
file.default_reasoning_effort = Some(reasoning_effort);
|
||||||
write_json_pretty(&path, &file)
|
write_json_pretty(&path, &file)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn save_last_used_provider(
|
||||||
|
root: &Path,
|
||||||
|
provider_id: &str,
|
||||||
|
model: &str,
|
||||||
|
reasoning_effort: ReasoningEffort,
|
||||||
|
) -> Result<()> {
|
||||||
|
let path = config_path(root);
|
||||||
|
let mut file = load_config_file(root)?.unwrap_or_default();
|
||||||
|
file.default_provider = Some(provider_id.to_string());
|
||||||
|
file.default_model = Some(model.to_string());
|
||||||
|
file.default_reasoning_effort = Some(reasoning_effort);
|
||||||
|
write_json_pretty(&path, &file)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn save_fast_mode_preference(root: &Path, enabled: bool) -> Result<()> {
|
||||||
|
let path = config_path(root);
|
||||||
|
let mut file = load_config_file(root)?.unwrap_or_default();
|
||||||
|
file.default_fast_mode = Some(enabled);
|
||||||
|
write_json_pretty(&path, &file)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn load_or_create_default_provider_registry(root: &Path) -> Result<ProvidersFile> {
|
pub fn load_or_create_default_provider_registry(root: &Path) -> Result<ProvidersFile> {
|
||||||
fs::create_dir_all(root).with_context(|| format!("creating {}", root.display()))?;
|
fs::create_dir_all(root).with_context(|| format!("creating {}", root.display()))?;
|
||||||
let path = providers_path(root);
|
let path = providers_path(root);
|
||||||
@@ -533,6 +620,7 @@ pub fn default_model_definition() -> ModelDefinition {
|
|||||||
supports_tools: true,
|
supports_tools: true,
|
||||||
supports_streaming: true,
|
supports_streaming: true,
|
||||||
reasoning: ReasoningMetadata::default(),
|
reasoning: ReasoningMetadata::default(),
|
||||||
|
fast_mode: FastModeMetadata::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ pub struct ChatGptCodexProvider {
|
|||||||
model: String,
|
model: String,
|
||||||
endpoint: String,
|
endpoint: String,
|
||||||
reasoning_effort: ReasoningEffort,
|
reasoning_effort: ReasoningEffort,
|
||||||
|
fast_mode: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
@@ -24,6 +25,7 @@ pub struct ChatGptCodexSettings {
|
|||||||
pub model: String,
|
pub model: String,
|
||||||
pub endpoint: String,
|
pub endpoint: String,
|
||||||
pub reasoning_effort: ReasoningEffort,
|
pub reasoning_effort: ReasoningEffort,
|
||||||
|
pub fast_mode: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Default, Clone)]
|
#[derive(Debug, Default, Clone)]
|
||||||
@@ -40,6 +42,7 @@ impl ChatGptCodexProvider {
|
|||||||
model: settings.model,
|
model: settings.model,
|
||||||
endpoint: normalize_endpoint(&settings.endpoint),
|
endpoint: normalize_endpoint(&settings.endpoint),
|
||||||
reasoning_effort: settings.reasoning_effort,
|
reasoning_effort: settings.reasoning_effort,
|
||||||
|
fast_mode: settings.fast_mode,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,7 +54,13 @@ impl ChatGptCodexProvider {
|
|||||||
) -> Result<CompletionResult> {
|
) -> Result<CompletionResult> {
|
||||||
let token = load_codex_access_token()?;
|
let token = load_codex_access_token()?;
|
||||||
let secret = token.as_secret().to_string();
|
let secret = token.as_secret().to_string();
|
||||||
let body = responses_body(&self.model, messages, tools, self.reasoning_effort);
|
let body = responses_body(
|
||||||
|
&self.model,
|
||||||
|
messages,
|
||||||
|
tools,
|
||||||
|
self.reasoning_effort,
|
||||||
|
self.fast_mode,
|
||||||
|
);
|
||||||
let resp = self
|
let resp = self
|
||||||
.client
|
.client
|
||||||
.post(&self.endpoint)
|
.post(&self.endpoint)
|
||||||
@@ -128,6 +137,7 @@ fn responses_body(
|
|||||||
messages: Vec<ModelMessage>,
|
messages: Vec<ModelMessage>,
|
||||||
tools: Vec<ToolSpec>,
|
tools: Vec<ToolSpec>,
|
||||||
reasoning_effort: ReasoningEffort,
|
reasoning_effort: ReasoningEffort,
|
||||||
|
fast_mode: bool,
|
||||||
) -> Value {
|
) -> Value {
|
||||||
let mut instructions = Vec::new();
|
let mut instructions = Vec::new();
|
||||||
let mut input = Vec::new();
|
let mut input = Vec::new();
|
||||||
@@ -183,7 +193,9 @@ fn responses_body(
|
|||||||
if !instructions.is_empty() {
|
if !instructions.is_empty() {
|
||||||
body["instructions"] = Value::String(instructions.join("\n\n"));
|
body["instructions"] = Value::String(instructions.join("\n\n"));
|
||||||
}
|
}
|
||||||
if let Some(effort) = reasoning_effort.request_value() {
|
if fast_mode {
|
||||||
|
body["reasoning"] = json!({"effort": "minimal", "summary": "auto"});
|
||||||
|
} else if let Some(effort) = reasoning_effort.request_value() {
|
||||||
body["reasoning"] = json!({"effort": effort, "summary": "auto"});
|
body["reasoning"] = json!({"effort": effort, "summary": "auto"});
|
||||||
}
|
}
|
||||||
body
|
body
|
||||||
@@ -464,6 +476,7 @@ mod tests {
|
|||||||
],
|
],
|
||||||
Vec::new(),
|
Vec::new(),
|
||||||
ReasoningEffort::Off,
|
ReasoningEffort::Off,
|
||||||
|
false,
|
||||||
);
|
);
|
||||||
|
|
||||||
assert_eq!(body["model"], "gpt-test");
|
assert_eq!(body["model"], "gpt-test");
|
||||||
@@ -473,6 +486,24 @@ mod tests {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn responses_body_uses_minimal_reasoning_for_fast_mode() {
|
||||||
|
let body = responses_body(
|
||||||
|
"gpt-test",
|
||||||
|
vec![ModelMessage::User {
|
||||||
|
content: "hello".into(),
|
||||||
|
}],
|
||||||
|
Vec::new(),
|
||||||
|
ReasoningEffort::High,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
body["reasoning"],
|
||||||
|
json!({"effort": "minimal", "summary": "auto"})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn stream_parser_collects_text_and_function_call() {
|
fn stream_parser_collects_text_and_function_call() {
|
||||||
let (tx, _rx) = mpsc::unbounded_channel();
|
let (tx, _rx) = mpsc::unbounded_channel();
|
||||||
|
|||||||
+10
-3
@@ -17,8 +17,14 @@ pub enum ProviderClient {
|
|||||||
ChatGptCodex(ChatGptCodexProvider),
|
ChatGptCodex(ChatGptCodexProvider),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub struct ProviderRuntimeOptions {
|
||||||
|
pub reasoning_effort: ReasoningEffort,
|
||||||
|
pub fast_mode: bool,
|
||||||
|
}
|
||||||
|
|
||||||
impl ProviderClient {
|
impl ProviderClient {
|
||||||
pub fn from_config(config: &Config, reasoning_effort: ReasoningEffort) -> Result<Self> {
|
pub fn from_config(config: &Config, options: ProviderRuntimeOptions) -> Result<Self> {
|
||||||
match config.active_provider.kind.as_str() {
|
match config.active_provider.kind.as_str() {
|
||||||
DEFAULT_PROVIDER_KIND => {
|
DEFAULT_PROVIDER_KIND => {
|
||||||
let api_key = config.resolved_api_key()?;
|
let api_key = config.resolved_api_key()?;
|
||||||
@@ -32,7 +38,7 @@ impl ProviderClient {
|
|||||||
model: config.model.clone(),
|
model: config.model.clone(),
|
||||||
base_url: config.active_provider.base_url.clone(),
|
base_url: config.active_provider.base_url.clone(),
|
||||||
api_key,
|
api_key,
|
||||||
reasoning_effort,
|
reasoning_effort: options.reasoning_effort,
|
||||||
reasoning_request_format,
|
reasoning_request_format,
|
||||||
},
|
},
|
||||||
)))
|
)))
|
||||||
@@ -41,7 +47,8 @@ impl ProviderClient {
|
|||||||
ChatGptCodexSettings {
|
ChatGptCodexSettings {
|
||||||
model: config.model.clone(),
|
model: config.model.clone(),
|
||||||
endpoint: config.active_provider.base_url.clone(),
|
endpoint: config.active_provider.base_url.clone(),
|
||||||
reasoning_effort,
|
reasoning_effort: options.reasoning_effort,
|
||||||
|
fast_mode: options.fast_mode,
|
||||||
},
|
},
|
||||||
))),
|
))),
|
||||||
kind => bail!("unsupported provider kind `{kind}`"),
|
kind => bail!("unsupported provider kind `{kind}`"),
|
||||||
|
|||||||
+7
-4
@@ -2,10 +2,10 @@ use crate::check;
|
|||||||
use crate::cli::Cli;
|
use crate::cli::Cli;
|
||||||
use crate::codex_auth;
|
use crate::codex_auth;
|
||||||
use crate::config::{
|
use crate::config::{
|
||||||
self, ConfigFile, ModelDefinition, ModelsFile, ProviderDefinition, ProvidersFile,
|
self, ConfigFile, FastModeMetadata, ModelDefinition, ModelsFile, ProviderDefinition,
|
||||||
ReasoningEffort, ReasoningMetadata, ReasoningRequestFormat, CHATGPT_CODEX_DEFAULT_MODEL,
|
ProvidersFile, ReasoningEffort, ReasoningMetadata, ReasoningRequestFormat,
|
||||||
CHATGPT_CODEX_PROVIDER_ID, CHATGPT_CODEX_PROVIDER_KIND, CHATGPT_CODEX_PROVIDER_NAME,
|
CHATGPT_CODEX_DEFAULT_MODEL, CHATGPT_CODEX_PROVIDER_ID, CHATGPT_CODEX_PROVIDER_KIND,
|
||||||
CHATGPT_CODEX_RESPONSES_URL, DEFAULT_PROVIDER_KIND,
|
CHATGPT_CODEX_PROVIDER_NAME, CHATGPT_CODEX_RESPONSES_URL, DEFAULT_PROVIDER_KIND,
|
||||||
};
|
};
|
||||||
use crate::menu::{Menu, MenuItem, TextPrompt};
|
use crate::menu::{Menu, MenuItem, TextPrompt};
|
||||||
use anyhow::{bail, Context, Result};
|
use anyhow::{bail, Context, Result};
|
||||||
@@ -970,6 +970,9 @@ fn upsert_model(models: &mut ModelsFile, selection: &SetupSelection) {
|
|||||||
},
|
},
|
||||||
request_format: ReasoningRequestFormat::ReasoningEffort,
|
request_format: ReasoningRequestFormat::ReasoningEffort,
|
||||||
},
|
},
|
||||||
|
fast_mode: FastModeMetadata {
|
||||||
|
supported: is_chatgpt_codex_provider(&selection.provider_id),
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(existing) = models.models.iter_mut().find(|existing| {
|
if let Some(existing) = models.models.iter_mut().find(|existing| {
|
||||||
|
|||||||
+9
-3
@@ -53,6 +53,7 @@ pub struct RenderState<'a> {
|
|||||||
pub show_full_tools: bool,
|
pub show_full_tools: bool,
|
||||||
pub show_reasoning: bool,
|
pub show_reasoning: bool,
|
||||||
pub reasoning_effort: ReasoningEffort,
|
pub reasoning_effort: ReasoningEffort,
|
||||||
|
pub fast_mode_active: bool,
|
||||||
pub scroll: u16,
|
pub scroll: u16,
|
||||||
pub autofill: Option<&'a AutoFillMenu>,
|
pub autofill: Option<&'a AutoFillMenu>,
|
||||||
pub overlay: Option<&'a OverlayView>,
|
pub overlay: Option<&'a OverlayView>,
|
||||||
@@ -600,9 +601,6 @@ fn ratatui_wrapped_row_count(line: &str, content_width: usize) -> usize {
|
|||||||
non_whitespace_previous = !is_whitespace;
|
non_whitespace_previous = !is_whitespace;
|
||||||
}
|
}
|
||||||
|
|
||||||
if !pending_line_has_symbols && word_symbols == 0 && whitespace_symbols > 0 {
|
|
||||||
rows = rows.saturating_add(1);
|
|
||||||
}
|
|
||||||
if whitespace_symbols > 0 || word_symbols > 0 {
|
if whitespace_symbols > 0 || word_symbols > 0 {
|
||||||
pending_line_has_symbols = true;
|
pending_line_has_symbols = true;
|
||||||
}
|
}
|
||||||
@@ -727,6 +725,9 @@ fn footer_text(state: &RenderState<'_>) -> String {
|
|||||||
parts.push("tools:full".into());
|
parts.push("tools:full".into());
|
||||||
}
|
}
|
||||||
parts.push(format!("reasoning:{}", state.reasoning_effort));
|
parts.push(format!("reasoning:{}", state.reasoning_effort));
|
||||||
|
if state.fast_mode_active {
|
||||||
|
parts.push("fast".into());
|
||||||
|
}
|
||||||
if state.show_reasoning {
|
if state.show_reasoning {
|
||||||
parts.push("reasoning:visible".into());
|
parts.push("reasoning:visible".into());
|
||||||
}
|
}
|
||||||
@@ -993,6 +994,11 @@ mod tests {
|
|||||||
assert_eq!(max_transcript_scroll(&transcript, false, false, area), 0);
|
assert_eq!(max_transcript_scroll(&transcript, false, false, area), 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn whitespace_only_line_counts_as_one_wrapped_row() {
|
||||||
|
assert_eq!(ratatui_wrapped_row_count(" ", 80), 1);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn input_height_wraps_long_line() {
|
fn input_height_wraps_long_line() {
|
||||||
// A single long line with no newlines should occupy more than 1 row
|
// A single long line with no newlines should occupy more than 1 row
|
||||||
|
|||||||
@@ -123,6 +123,62 @@ async fn reasoning_effort_supports_reasoning_object_format() {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn fast_mode_preference_does_not_change_openai_compatible_request() {
|
||||||
|
let server = MockServer::start().await;
|
||||||
|
Mock::given(method("POST"))
|
||||||
|
.and(path("/chat/completions"))
|
||||||
|
.respond_with(sse(
|
||||||
|
"data: {\"choices\":[{\"index\":0,\"delta\":{\"content\":\"Done.\"}}]}\r\n\r\ndata: [DONE]\r\n\r\n",
|
||||||
|
))
|
||||||
|
.expect(1)
|
||||||
|
.mount(&server)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let root = tempdir().unwrap();
|
||||||
|
let cwd = tempdir().unwrap();
|
||||||
|
let docs = tempdir().unwrap();
|
||||||
|
let config = Config {
|
||||||
|
root: root.path().to_path_buf(),
|
||||||
|
docs_dir: docs.path().to_path_buf(),
|
||||||
|
model: "test-model".into(),
|
||||||
|
default_fast_mode: true,
|
||||||
|
active_provider: cassady::config::ResolvedProviderConfig {
|
||||||
|
base_url: server.uri(),
|
||||||
|
api_key: "test-key".into(),
|
||||||
|
..Config::default().active_provider
|
||||||
|
},
|
||||||
|
..Config::default()
|
||||||
|
};
|
||||||
|
let conversation = Conversation::create(
|
||||||
|
&config.conversations_dir(),
|
||||||
|
&config.model,
|
||||||
|
cwd.path(),
|
||||||
|
"base prompt".into(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let (tx, _rx) = mpsc::unbounded_channel::<AgentEvent>();
|
||||||
|
|
||||||
|
run_turn(
|
||||||
|
conversation,
|
||||||
|
"stay compatible".into(),
|
||||||
|
AgentSettings {
|
||||||
|
config,
|
||||||
|
cwd: cwd.path().to_path_buf(),
|
||||||
|
mode: AccessMode::ReadOnly,
|
||||||
|
reasoning_effort: ReasoningEffort::Off,
|
||||||
|
},
|
||||||
|
tx,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let requests = server.received_requests().await.unwrap();
|
||||||
|
let body = String::from_utf8_lossy(&requests[0].body);
|
||||||
|
assert!(!body.contains("\"effort\":\"minimal\""));
|
||||||
|
assert!(!body.contains("\"fast_mode\""));
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn reasoning_is_streamed_persisted_and_sent_back() {
|
async fn reasoning_is_streamed_persisted_and_sent_back() {
|
||||||
let server = MockServer::start().await;
|
let server = MockServer::start().await;
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ fn default_provider_and_model_files_are_created() {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(models.models[0].provider, "fireworks");
|
assert_eq!(models.models[0].provider, "fireworks");
|
||||||
assert!(models.models[0].reasoning.supported);
|
assert!(models.models[0].reasoning.supported);
|
||||||
|
assert!(!models.models[0].fast_mode.supported);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
models.models[0].reasoning.default_effort,
|
models.models[0].reasoning.default_effort,
|
||||||
ReasoningEffort::Medium
|
ReasoningEffort::Medium
|
||||||
@@ -127,6 +128,174 @@ fn reasoning_defaults_to_supported_medium_for_model_metadata() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn fast_mode_defaults_to_off_and_unsupported() {
|
||||||
|
let model: config::ModelDefinition = serde_json::from_str(
|
||||||
|
r#"{
|
||||||
|
"id": "test-model",
|
||||||
|
"provider": "test-provider"
|
||||||
|
}
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert!(!model.fast_mode.supported);
|
||||||
|
|
||||||
|
let root = tempdir().unwrap();
|
||||||
|
let cfg = Config::load_from_root_with_docs(
|
||||||
|
root.path().to_path_buf(),
|
||||||
|
root.path().join("docs"),
|
||||||
|
&cli(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let state = cfg.fast_mode_state();
|
||||||
|
assert!(!state.preferred);
|
||||||
|
assert!(!state.supported);
|
||||||
|
assert!(!state.active);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn fast_mode_preference_persists_without_losing_config_fields() {
|
||||||
|
let root = tempdir().unwrap();
|
||||||
|
std::fs::write(
|
||||||
|
root.path().join("config.json"),
|
||||||
|
r#"{
|
||||||
|
"default_access_mode": "workspace-edit",
|
||||||
|
"show_reasoning": true
|
||||||
|
}
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
config::save_fast_mode_preference(root.path(), true).unwrap();
|
||||||
|
|
||||||
|
let cfg = Config::load_from_root_with_docs(
|
||||||
|
root.path().to_path_buf(),
|
||||||
|
root.path().join("docs"),
|
||||||
|
&cli(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert!(cfg.default_fast_mode);
|
||||||
|
assert!(cfg.show_reasoning);
|
||||||
|
assert_eq!(cfg.default_access_mode.to_string(), "workspace-edit");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn fast_mode_state_is_active_for_supported_codex_model() {
|
||||||
|
let root = tempdir().unwrap();
|
||||||
|
std::fs::write(
|
||||||
|
root.path().join("providers.json"),
|
||||||
|
r#"{
|
||||||
|
"providers": [
|
||||||
|
{
|
||||||
|
"id": "chatgpt-codex",
|
||||||
|
"name": "ChatGPT Codex",
|
||||||
|
"kind": "chatgpt-codex",
|
||||||
|
"base_url": "https://chatgpt.com/backend-api/codex/responses",
|
||||||
|
"api_key": "",
|
||||||
|
"default_model": "gpt-5.5",
|
||||||
|
"models": ["gpt-5.5"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
std::fs::write(
|
||||||
|
root.path().join("models.json"),
|
||||||
|
r#"{
|
||||||
|
"models": [
|
||||||
|
{
|
||||||
|
"id": "gpt-5.5",
|
||||||
|
"provider": "chatgpt-codex",
|
||||||
|
"fast_mode": { "supported": true }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
std::fs::write(
|
||||||
|
root.path().join("config.json"),
|
||||||
|
r#"{
|
||||||
|
"default_provider": "chatgpt-codex",
|
||||||
|
"default_model": "gpt-5.5",
|
||||||
|
"default_fast_mode": true
|
||||||
|
}
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let cfg = Config::load_from_root_with_docs(
|
||||||
|
root.path().to_path_buf(),
|
||||||
|
root.path().join("docs"),
|
||||||
|
&cli(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let state = cfg.fast_mode_state();
|
||||||
|
assert!(state.preferred);
|
||||||
|
assert!(state.supported);
|
||||||
|
assert!(state.active);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn fast_mode_state_is_active_for_chatgpt_codex_even_with_legacy_metadata() {
|
||||||
|
let root = tempdir().unwrap();
|
||||||
|
std::fs::write(
|
||||||
|
root.path().join("providers.json"),
|
||||||
|
r#"{
|
||||||
|
"providers": [
|
||||||
|
{
|
||||||
|
"id": "chatgpt-codex",
|
||||||
|
"name": "ChatGPT Codex",
|
||||||
|
"kind": "chatgpt-codex",
|
||||||
|
"base_url": "https://chatgpt.com/backend-api/codex/responses",
|
||||||
|
"api_key": "",
|
||||||
|
"default_model": "gpt-5.5",
|
||||||
|
"models": ["gpt-5.5"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
std::fs::write(
|
||||||
|
root.path().join("models.json"),
|
||||||
|
r#"{
|
||||||
|
"models": [
|
||||||
|
{
|
||||||
|
"id": "gpt-5.5",
|
||||||
|
"provider": "chatgpt-codex",
|
||||||
|
"fast_mode": { "supported": false }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
std::fs::write(
|
||||||
|
root.path().join("config.json"),
|
||||||
|
r#"{
|
||||||
|
"default_provider": "chatgpt-codex",
|
||||||
|
"default_model": "gpt-5.5",
|
||||||
|
"default_fast_mode": true
|
||||||
|
}
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let cfg = Config::load_from_root_with_docs(
|
||||||
|
root.path().to_path_buf(),
|
||||||
|
root.path().join("docs"),
|
||||||
|
&cli(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let state = cfg.fast_mode_state();
|
||||||
|
assert!(state.preferred);
|
||||||
|
assert!(state.supported);
|
||||||
|
assert!(state.active);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn validation_accepts_chatgpt_codex_without_api_key() {
|
fn validation_accepts_chatgpt_codex_without_api_key() {
|
||||||
let providers = ProvidersFile {
|
let providers = ProvidersFile {
|
||||||
@@ -150,6 +319,7 @@ fn validation_accepts_chatgpt_codex_without_api_key() {
|
|||||||
supports_tools: true,
|
supports_tools: true,
|
||||||
supports_streaming: true,
|
supports_streaming: true,
|
||||||
reasoning: Default::default(),
|
reasoning: Default::default(),
|
||||||
|
fast_mode: Default::default(),
|
||||||
}],
|
}],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -174,6 +174,11 @@ fn apply_setup_writes_chatgpt_codex_without_api_key() {
|
|||||||
assert_eq!(provider.id, config::CHATGPT_CODEX_PROVIDER_ID);
|
assert_eq!(provider.id, config::CHATGPT_CODEX_PROVIDER_ID);
|
||||||
assert_eq!(provider.kind, config::CHATGPT_CODEX_PROVIDER_KIND);
|
assert_eq!(provider.kind, config::CHATGPT_CODEX_PROVIDER_KIND);
|
||||||
assert!(provider.api_key.is_empty());
|
assert!(provider.api_key.is_empty());
|
||||||
|
|
||||||
|
let models: ModelsFile =
|
||||||
|
serde_json::from_str(&std::fs::read_to_string(root.path().join("models.json")).unwrap())
|
||||||
|
.unwrap();
|
||||||
|
assert!(models.models[0].fast_mode.supported);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
Reference in New Issue
Block a user