Repository navigation
Conversation
Design MODEL-ROUTING-CATALOG-20260901 §10, slice F1. A fleet model is a Pod
member: the selected Pod file's operator route plus every member row that
pins an exact provider + model; the roles a model fills are the member rows
that pin it. No second store.
- crate::fleet::members: fleet_models / add_fleet_model / remove_fleet_model
/ toggle_fleet_model + change_receipt; Config::fleet_members(workspace) is
the read seam for the operator-awareness slice (F2).
- /pod models | add <provider> <model> [role…] | remove <provider> <model>
(also via the /fleet alias). A model the configured provider does not
serve is rejected; the first add creates and selects a user-global Pod
named 'My fleet'.
- /model picker: ⇧F adds or removes the row's exact route; fleet models
lead the list labelled 'fleet · <roles>', ahead of ⇧P pins and providers.
- /models prints the fleet before the provider list ('Your fleet is the
session model only' when empty).
- PickerActionFleet message in all 15 locales; docs/FLEET.md 'Your fleet
as models'.
Tests: scripts/dev-test.sh tui fleet::members groups::core::fleet
model_picker format_helpers — Summary 37 tests run: 37 passed, 11834
skipped.
Signed-off-by: CodeWhale Bot <bot@codewhale.net>
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
Bugbot couldn't run - usage limit reachedBugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit. A user or team admin can review and increase usage limits in the Cursor dashboard. (requestId: serverGenReqId_07f0a4d5-de39-4cd8-a357-4c9858656fac) |
cargo clippy -p codewhale-tui --all-targets -- -D warnings -A clippy::too_many_arguments -A clippy::uninlined_format_args -A clippy::unnecessary_map_or: no findings. Signed-off-by: CodeWhale Bot <bot@codewhale.net>
Bugbot couldn't run - usage limit reachedBugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit. A user or team admin can review and increase usage limits in the Cursor dashboard. (requestId: serverGenReqId_9fd3fad3-4e6e-4719-b899-ca0ecaf5a174) |
|
Claude finished @Hmbown's task in 3m 52s —— View job Review: fleet-as-models (F1)
Findings1. let present = fleet_models(workspace)
.iter()
.any(|m| m.matches(provider, model) && !m.roles.iter().any(|r| r == "operator"));The operator's route is always shown in Suggest: when the only match is the operator role, either no-op with a message explaining the route is the operator ("change it with 2. Fleet "current" marker in let marker = if member.model == current_model { "*" } else { " " };Fleet models span multiple providers, but the marker only checks the bare model id string against Notes (good choices)
|
There was a problem hiding this comment.
🟡 Changes recommended
The new fleet toggle logic has confirmed incorrect behavior around operator routes and receipt role handling, and it also masks selected-Pod load errors as “empty fleet,” which can mislead runtime surfaces.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
Implements “fleet-as-models” (design §10 F1) so the user’s chosen models (from the selected Pod’s operator route + exact pinned member routes) lead model listings and can be mutated via picker and /pod verbs, without introducing a second storage system beyond the Pod file.
Changes:
- Adds
crate::fleet::membersas the read/write seam for fleet models (list/add/remove/toggle + receipts). - Updates TUI model picker to sort/label fleet models first and adds ⇧F to toggle fleet membership from
/model. - Extends
/podwithmodels|add|removeand updates/modelsoutput to lead with fleet summary; adds docs + localization keys.
File summaries
| File | Description |
|---|---|
| docs/FLEET.md | Documents “Your fleet as models” behavior and related commands/UX. |
| crates/tui/src/tui/views/mod.rs | Adds ViewEvent::ModelPickerToggleFleet for ⇧F picker action. |
| crates/tui/src/tui/ui/handlers.rs | Handles ModelPickerToggleFleet by mutating fleet and refreshing picker. |
| crates/tui/src/tui/ui/apply.rs | Extends /models message rendering to include fleet-first section. |
| crates/tui/src/tui/model_picker.rs | Builds “picker pins” with fleet first; adds ⇧F handling + footer hint + tests. |
| crates/tui/src/tui/format_helpers.rs | Renders fleet-first header/rows in available models message. |
| crates/tui/src/localization.rs | Adds PickerActionFleet to message IDs + test lists. |
| crates/tui/src/fleet/store.rs | Exposes slugify for reuse by fleet-members logic. |
| crates/tui/src/fleet/mod.rs | Exports new fleet::members module. |
| crates/tui/src/fleet/members.rs | New module implementing fleet model projection + add/remove/toggle APIs. |
| crates/tui/src/config.rs | Adds Config::fleet_members accessor as a seam for later slices. |
| crates/tui/src/commands/groups/core/fleet.rs | Adds `/pod models |
| crates/tui/locales/*.json | Adds translations for PickerActionFleet across locales. |
Review details
- Files reviewed: 27/27 changed files
- Comments generated: 4
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| let Ok(Some(selected)) = resolve_selected_fleet(workspace) else { | ||
| return Vec::new(); | ||
| }; | ||
| let Ok((fleet, _scope)) = load_fleet_at(&selected.path) else { | ||
| return Vec::new(); |
| let roles: Vec<String> = roles | ||
| .iter() | ||
| .map(|r| r.trim().to_string()) | ||
| .filter(|r| !r.is_empty()) | ||
| .collect(); |
| pub fn toggle_fleet_model( | ||
| workspace: &Path, | ||
| provider: &str, | ||
| model: &str, | ||
| ) -> Result<FleetModelChange, FleetStoreError> { | ||
| let present = fleet_models(workspace) | ||
| .iter() | ||
| .any(|m| m.matches(provider, model) && !m.roles.iter().any(|r| r == "operator")); | ||
| if present { | ||
| remove_fleet_model(workspace, provider, model) | ||
| } else { | ||
| add_fleet_model(workspace, provider, model, &[]) | ||
| } | ||
| } |
| /// Default name for the Pod created by the first `/pod add` or `a` in | ||
| /// `/models` when no Pod is selected yet. |
There was a problem hiding this comment.
Codewhale review
Adds a fleet-as-models feature that reuses Pod member rows, with catalog facts and a model-picker toggle. The core store seam is well isolated and tested, but the toggle has an operator-route edge case and /pod add does not enforce configured-provider validation.
Findings
- [WARNING] ⇧F toggle cannot remove member rows when the route is also the operator route (
crates/tui/src/fleet/members.rs:237)
toggle_fleet_model determines presence via fleet_models() and excludes any model whose roles contain "operator". If a route is the Pod operator and also has member rows (or is toggled once to add a role-less member on top of the operator), the next ⇧F still reports absent, so remove_fleet_model is never called; repeated toggles leave redundant member rows. The presence decision should be based on actual member rows, not the unioned operator role. - [WARNING] /pod add accepts unconfigured provider ids (
crates/tui/src/commands/groups/core/fleet.rs:129)
fleet_add validates a model only when ApiProvider::parse(provider) returns Some. An unknown or custom provider id falls through to add_fleet_model and is persisted without checking that the provider is configured, so a typo such as openruter can create a bogus fleet member. The PR specifies the provider must be one you configured; the command should reject unconfigured provider ids before writing. - [INFO] Stale doc comment points at
ain/models(crates/tui/src/fleet/members.rs:17)
The new members.rs default-fleet doc says the first Pod is created by/pod addorain/models, but the picker action is ⇧F in /model and the PR explicitly rejected keya.
Suggestions
-
crates/tui/src/fleet/members.rs:232— Base the toggle on member rows instead of fleet_models roles, so operator-only routes can be added as members and then removed on the next toggle.pub fn toggle_fleet_model( workspace: &Path, provider: &str, model: &str, ) -> Result<FleetModelChange, FleetStoreError> { let Some(selected) = resolve_selected_fleet(workspace)? else { return add_fleet_model(workspace, provider, model, &[]); }; let (fleet, _scope) = load_fleet_at(&selected.path)?; let present = fleet.members.iter().any(|m| { m.provider .as_deref() .is_some_and(|p| p.eq_ignore_ascii_case(provider.trim())) && m.model .as_deref() .is_some_and(|id| id.eq_ignore_ascii_case(model.trim())) }); if present { remove_fleet_model(workspace, provider, model) } else { add_fleet_model(workspace, provider, model, &[]) } } -
crates/tui/src/fleet/members.rs:17— Update the doc comment to match the shipped picker key and path./// Default name for the Pod created by the first `/pod add` or `⇧F` in /// `/model` when no Pod is selected yet. pub const DEFAULT_FLEET_NAME: &str = "My fleet";
Assessment
The PR is well structured and reuses the Pod store as intended, with meaningful tests. Before merge, fix the picker toggle operator-route bug and add configured-provider validation for /pod add; the stale doc comment is a small cleanup.
Advisory review by Codewhale (codewhale review --pr 5815 --post, head e1fea634a0b47394f020fcfdc8ebc1052591c097). Line-specific findings are also posted as inline review comments; mechanical fixes arrive as committable suggestions you can apply from the Files tab. CODEOWNERS approval still governs merge.
| provider: &str, | ||
| model: &str, | ||
| ) -> Result<FleetModelChange, FleetStoreError> { | ||
| let present = fleet_models(workspace) |
There was a problem hiding this comment.
[WARNING] ⇧F toggle cannot remove member rows when the route is also the operator route
toggle_fleet_model determines presence via fleet_models() and excludes any model whose roles contain "operator". If a route is the Pod operator and also has member rows (or is toggled once to add a role-less member on top of the operator), the next ⇧F still reports absent, so remove_fleet_model is never called; repeated toggles leave redundant member rows. The presence decision should be based on actual member rows, not the unioned operator role.
| ); | ||
| }; | ||
| let roles: Vec<String> = words.map(str::to_string).collect(); | ||
| if let Some(known) = crate::config::ApiProvider::parse(provider) { |
There was a problem hiding this comment.
[WARNING] /pod add accepts unconfigured provider ids
fleet_add validates a model only when ApiProvider::parse(provider) returns Some. An unknown or custom provider id falls through to add_fleet_model and is persisted without checking that the provider is configured, so a typo such as openruter can create a bogus fleet member. The PR specifies the provider must be one you configured; the command should reject unconfigured provider ids before writing.
| save_fleet, set_selected, slugify, | ||
| }; | ||
|
|
||
| /// Default name for the Pod created by the first `/pod add` or `a` in |
There was a problem hiding this comment.
[INFO] Stale doc comment points at a in /models
The new members.rs default-fleet doc says the first Pod is created by /pod add or a in /models, but the picker action is ⇧F in /model and the PR explicitly rejected key a.
| pub fn toggle_fleet_model( | ||
| workspace: &Path, | ||
| provider: &str, | ||
| model: &str, | ||
| ) -> Result<FleetModelChange, FleetStoreError> { | ||
| let present = fleet_models(workspace) | ||
| .iter() | ||
| .any(|m| m.matches(provider, model) && !m.roles.iter().any(|r| r == "operator")); | ||
| if present { | ||
| remove_fleet_model(workspace, provider, model) | ||
| } else { | ||
| add_fleet_model(workspace, provider, model, &[]) | ||
| } | ||
| } |
There was a problem hiding this comment.
Base the toggle on member rows instead of fleet_models roles, so operator-only routes can be added as members and then removed on the next toggle.
| pub fn toggle_fleet_model( | |
| workspace: &Path, | |
| provider: &str, | |
| model: &str, | |
| ) -> Result<FleetModelChange, FleetStoreError> { | |
| let present = fleet_models(workspace) | |
| .iter() | |
| .any(|m| m.matches(provider, model) && !m.roles.iter().any(|r| r == "operator")); | |
| if present { | |
| remove_fleet_model(workspace, provider, model) | |
| } else { | |
| add_fleet_model(workspace, provider, model, &[]) | |
| } | |
| } | |
| pub fn toggle_fleet_model( | |
| workspace: &Path, | |
| provider: &str, | |
| model: &str, | |
| ) -> Result<FleetModelChange, FleetStoreError> { | |
| let Some(selected) = resolve_selected_fleet(workspace)? else { | |
| return add_fleet_model(workspace, provider, model, &[]); | |
| }; | |
| let (fleet, _scope) = load_fleet_at(&selected.path)?; | |
| let present = fleet.members.iter().any(|m| { | |
| m.provider | |
| .as_deref() | |
| .is_some_and(|p| p.eq_ignore_ascii_case(provider.trim())) | |
| && m.model | |
| .as_deref() | |
| .is_some_and(|id| id.eq_ignore_ascii_case(model.trim())) | |
| }); | |
| if present { | |
| remove_fleet_model(workspace, provider, model) | |
| } else { | |
| add_fleet_model(workspace, provider, model, &[]) | |
| } | |
| } |
| /// Default name for the Pod created by the first `/pod add` or `a` in | ||
| /// `/models` when no Pod is selected yet. | ||
| pub const DEFAULT_FLEET_NAME: &str = "My fleet"; |
There was a problem hiding this comment.
Update the doc comment to match the shipped picker key and path.
| /// Default name for the Pod created by the first `/pod add` or `a` in | |
| /// `/models` when no Pod is selected yet. | |
| pub const DEFAULT_FLEET_NAME: &str = "My fleet"; | |
| /// Default name for the Pod created by the first `/pod add` or `⇧F` in | |
| /// `/model` when no Pod is selected yet. | |
| pub const DEFAULT_FLEET_NAME: &str = "My fleet"; |
…ion\n\n- Reject unconfigured provider ids in "/pod add" before writing, reusing\n the existing provider_is_configured_for_active predicate and custom\n provider table checks.\n- Add App.config snapshot so commands can consult the loaded config.\n- Update the stale DEFAULT_FLEET_NAME doc comment to mention ⇧F.\n- Sync crates/tui/CHANGELOG.md.
Bugbot couldn't run - usage limit reachedBugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit. A user or team admin can review and increase usage limits in the Cursor dashboard. (requestId: serverGenReqId_7b9d0654-05a5-4a24-ac33-5cf2ab74c0f7) |
|
Claude encountered an error after 0s —— View job I'll analyze this and get back to you. |
| let fleet = FleetFile::new( | ||
| DEFAULT_FLEET_NAME.to_string(), | ||
| Some("Models added from /models and /pod add.".to_string()), | ||
| )?; | ||
| save_fleet(&fleet, FleetScope::Personal, workspace)?; |
There was a problem hiding this comment.
🔴 Default creation erases an existing Pod
With no selection, selected_or_new overwrites an existing My fleet Pod before adding the requested model. Its operator and members are lost.
Prompt for agents
Fix default Pod creation in crates/tui/src/fleet/members.rs:selected_or_new so an unselected existing personal Pod named "My fleet" is never overwritten. Detect the existing file and either load and select it or choose a collision-free default name. Preserve all existing operator/member data and add regression coverage for an existing unselected "My fleet" Pod.
Was this helpful? React with 👍 or 👎 to provide feedback.
| match crate::fleet::members::toggle_fleet_model( | ||
| &app.workspace, | ||
| &provider_key, | ||
| &model, | ||
| ) { | ||
| Ok(change) => { | ||
| crate::fleet::members::change_receipt(&provider_key, &model, &change) | ||
| } |
There was a problem hiding this comment.
🔴 Fleet changes leave dispatch stale
After /pod add, /pod remove, or ⇧F, the engine keeps its old roster. New members remain unavailable, and removed members remain dispatchable.
Prompt for agents
After every successful fleet-model mutation, refresh the running engine's FleetRoster through the same Op::SetFleetRoster path used by ViewEvent::FleetStoreChanged. Cover both the ModelPickerToggleFleet handler and slash-command add/remove flows; failures must remain visible and mutations must not report a fully active change while dispatch still uses stale state.
Was this helpful? React with 👍 or 👎 to provide feedback.
| let Ok(Some(selected)) = resolve_selected_fleet(workspace) else { | ||
| return Vec::new(); | ||
| }; | ||
| let Ok((fleet, _scope)) = load_fleet_at(&selected.path) else { | ||
| return Vec::new(); |
There was a problem hiding this comment.
🟡 Broken selections appear empty
When the selected Pod is missing or invalid, fleet_models reports an empty fleet. Users see “session model only” instead of the repairable error.
Prompt for agents
Make crates/tui/src/fleet/members.rs:fleet_models preserve errors from resolve_selected_fleet and load_fleet_at instead of collapsing them into an empty fleet. Update `/pod models`, `/models`, and model-picker callers to distinguish no selection from a broken explicit selection and surface the existing repair guidance.
Was this helpful? React with 👍 or 👎 to provide feedback.
| let provider_key = provider_id.unwrap_or_else(|| provider.as_str().to_string()); | ||
| app.status_message = Some( | ||
| match crate::fleet::members::toggle_fleet_model( | ||
| &app.workspace, | ||
| &provider_key, | ||
| &model, | ||
| ) { |
There was a problem hiding this comment.
🟡 Locked providers enter the fleet
On an unauthenticated catalog row, ModelPickerToggleFleet adds the model without provider validation. The fleet then contains a route the user cannot run.
Prompt for agents
Apply the same configured-provider validation used by `/pod add` before adding a model through ModelPickerToggleFleet. Preserve the ability to remove an existing stale fleet entry even when its provider is no longer configured, and show a typed error toast when an add is rejected.
Was this helpful? React with 👍 or 👎 to provide feedback.
| if let Some(provider) = crate::config::ApiProvider::parse(provider_id) { | ||
| return crate::config::provider_is_configured_for_active( | ||
| &app.config, | ||
| provider, | ||
| app.api_provider, | ||
| ); |
There was a problem hiding this comment.
🟡 Provider validation goes stale
After in-session provider or profile changes, /pod add checks the startup app.config snapshot. It rejects newly configured providers and can accept removed ones.
Prompt for agents
Remove the unsynchronized Config snapshot used by `/pod add`, or keep it synchronized with every mutation of the event loop's live Config. Provider validation must observe provider setup, profile switches, revocation, and route changes completed during the current session. Prefer passing current provider facts through the command boundary rather than maintaining two Config authorities.
Was this helpful? React with 👍 or 👎 to provide feedback.
| save_fleet(&fleet, FleetScope::Personal, workspace)?; | ||
| set_selected(DEFAULT_FLEET_NAME, FleetScope::Personal, workspace)?; | ||
| Ok((fleet, FleetScope::Personal, true)) |
| fn fleet_models_text(app: &App) -> String { | ||
| use crate::fleet::members::fleet_models; | ||
| let models = fleet_models(&app.workspace); | ||
| if models.is_empty() { | ||
| return "Your fleet is the session model only. Add one: /pod add <provider> <model> [role…] (or ⇧F on a row in /model).".to_string(); | ||
| } | ||
| let mut lines = vec![format!( | ||
| "Your fleet `{}` ({} models)", | ||
| models[0].fleet, | ||
| models.len() | ||
| )]; | ||
| for member in &models { | ||
| let provider = crate::config::ApiProvider::parse(&member.provider); | ||
| let facts = provider | ||
| .and_then(|p| crate::provider_lake::catalog_offering_for_model(p, &member.model)) | ||
| .map(|row| { | ||
| let mut parts = Vec::new(); | ||
| if let Some(cost) = row.cost.as_ref() | ||
| && let (Some(input), Some(output)) = (cost.input, cost.output) | ||
| { | ||
| parts.push(format!("${input:.2}/{output:.2} per M")); | ||
| } | ||
| if let Some(limit) = row.limit.as_ref() | ||
| && let Some(context) = limit.context | ||
| { | ||
| parts.push(format!("{}k ctx", context / 1000)); | ||
| } | ||
| if row.tool_call == Some(true) { | ||
| parts.push("tools".to_string()); | ||
| } | ||
| parts.join(" · ") | ||
| }) | ||
| .filter(|facts| !facts.is_empty()) | ||
| .map(|facts| format!(" · {facts}")) | ||
| .unwrap_or_default(); | ||
| lines.push(format!( | ||
| " {}/{} · {}{facts}", | ||
| member.provider, | ||
| member.model, | ||
| member.roles_label() | ||
| )); | ||
| } | ||
| lines.push( | ||
| "Add: /pod add <provider> <model> [role…] · Remove: /pod remove <provider> <model>" | ||
| .to_string(), | ||
| ); | ||
| lines.join("\n") |
| app.status_message = Some( | ||
| match crate::fleet::members::toggle_fleet_model( | ||
| &app.workspace, | ||
| &provider_key, | ||
| &model, | ||
| ) { | ||
| Ok(change) => { | ||
| crate::fleet::members::change_receipt(&provider_key, &model, &change) | ||
| } | ||
| Err(error) => format!("Could not update the fleet: {error}"), | ||
| }, | ||
| ); |
| for role in wanted { | ||
| let already = fleet.members.iter().any(|m| { | ||
| m.provider | ||
| .as_deref() | ||
| .is_some_and(|p| p.eq_ignore_ascii_case(provider)) | ||
| && m.model | ||
| .as_deref() | ||
| .is_some_and(|id| id.eq_ignore_ascii_case(model)) | ||
| && role.is_none_or(|role| m.role.eq_ignore_ascii_case(role)) | ||
| }); | ||
| if already { | ||
| continue; | ||
| } | ||
| let base = role.map_or_else(|| model_slug.clone(), slugify); | ||
| let id = unique_member_id(&fleet, &base, &model_slug); | ||
| fleet.members.push(FleetMember { | ||
| id, | ||
| display_name: None, | ||
| role: role.unwrap_or_default().to_string(), | ||
| model: Some(model.to_string()), | ||
| provider: Some(provider.to_string()), | ||
| reasoning: None, | ||
| instructions: None, | ||
| requires: Vec::new(), | ||
| }); | ||
| } | ||
| save_fleet(&fleet, scope, workspace)?; | ||
| Ok(FleetModelChange::Added { |
Bugbot couldn't run - usage limit reachedBugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit. A user or team admin can review and increase usage limits in the Cursor dashboard. (requestId: serverGenReqId_c10b2a20-fc24-4bb7-8d68-a2eca2ec5aa9) |
|
Claude encountered an error after 0s —— View job I'll analyze this and get back to you. |
There was a problem hiding this comment.
Codewhale review
PR #5815 implements the fleet-as-models feature with /pod models, /pod add/remove, and picker ⇧F toggling via a new fleet::members module. The main correctness issue is a bug in toggle_fleet_model that prevents removing role-less models from the picker. There are also risks around creating a new user-global Pod without checking for an existing one, and a config snapshot that may become stale.
Findings
- [ERROR] toggle_fleet_model treats role-less models as the operator route and never removes them (
crates/tui/src/fleet/members.rs:256)
In toggle_fleet_model,let only_operator = present.roles.iter().all(|r| r == "operator");usesIterator::allon an emptyrolesvector. For a model added without a role (e.g., via picker ⇧F with no role),present.rolesis empty, soallreturns true. The function then returnsUnchangedinstead of callingremove_fleet_model. This means pressing ⇧F again on a role-less fleet model does not remove it, violating the PR description that ⇧F toggles. The/pod removecommand is unaffected because it operates directly on member rows. - [WARNING] selected_or_new may overwrite or conflict with an existing user-global 'My fleet' Pod (
crates/tui/src/fleet/members.rs:311)
When no Pod is selected, selected_or_new unconditionally constructs a new FleetFile named DEFAULT_FLEET_NAME and calls save_fleet with FleetScope::Personal. If a personal Pod named 'My fleet' already exists (e.g., created by another workspace), save_fleet may either error (preventing the add) or overwrite the existing file, losing its members. The code should first attempt to load an existing personal Pod with that name before creating a new one. - [WARNING] App.config snapshot may become stale after configuration changes (
crates/tui/src/tui/app/init.rs)
The newpub config: Configfield on App is initialized from the config used during app construction. If the configuration is later updated in-memory (e.g., adding a provider or changing credentials via the UI or slash commands), this snapshot may not be refreshed. Since/pod addrelies onprovider_id_is_configuredreadingapp.config, a newly configured provider might be rejected until the app is restarted. Verify that all configuration mutation paths also updateapp.config. - [INFO] Custom provider names that collide with built-in ApiProvider ids are misclassified (
crates/tui/src/commands/groups/core/fleet.rs)
provider_id_is_configured parses the provider id with ApiProvider::parse first and returns based on provider_is_configured_for_active if successful. It does not fall through to the custom-provider check for names like 'openrouter' that could be a user-defined[providers.<name>]table. Such a custom provider would be validated as the built-in provider and may produce wrong rejections. - [INFO] Missing tests for important edge cases
There is no test covering toggle removal of a role-less model (the bug above), no test for selected_or_new when a personal 'My fleet' already exists, no test for provider_id_is_configured with a custom provider whose name collides with a built-in, and no test ensuring the App.config snapshot stays in sync after configuration changes.
Suggestions
-
crates/tui/src/fleet/members.rs:256— Role-less models have an emptyrolesvector, soIterator::allreturns true and the function incorrectly treats them as the operator route. Replace the check with an exact one-role operator test so that only true operator-only routes are protected, and role-less models can be removed.let only_operator = present.roles.len() == 1 && present.roles[0] == "operator";
Assessment
The feature is well-structured and reuses existing Pod persistence correctly, but the toggle bug is a blocker for the primary picker interaction and must be fixed before merge. The selected_or_new and config snapshot concerns should be investigated and addressed, with missing tests added for the edge cases noted.
Advisory review by Codewhale (codewhale review --pr 5815 --post, head ff78c680b225a644eadced5d5497170c7e3d6325). Line-specific findings are also posted as inline review comments; mechanical fixes arrive as committable suggestions you can apply from the Files tab. CODEOWNERS approval still governs merge.
| }; | ||
| // Present by exact route, whatever roles it fills: never write a | ||
| // duplicate row. The operator route with no member rows of its own is | ||
| // your current model; it cannot be toggled off, only switched. |
There was a problem hiding this comment.
[ERROR] toggle_fleet_model treats role-less models as the operator route and never removes them
In toggle_fleet_model, let only_operator = present.roles.iter().all(|r| r == "operator"); uses Iterator::all on an empty roles vector. For a model added without a role (e.g., via picker ⇧F with no role), present.roles is empty, so all returns true. The function then returns Unchanged instead of calling remove_fleet_model. This means pressing ⇧F again on a role-less fleet model does not remove it, violating the PR description that ⇧F toggles. The /pod remove command is unaffected because it operates directly on member rows.
| } | ||
| } | ||
| } | ||
|
|
There was a problem hiding this comment.
[WARNING] selected_or_new may overwrite or conflict with an existing user-global 'My fleet' Pod
When no Pod is selected, selected_or_new unconditionally constructs a new FleetFile named DEFAULT_FLEET_NAME and calls save_fleet with FleetScope::Personal. If a personal Pod named 'My fleet' already exists (e.g., created by another workspace), save_fleet may either error (preventing the add) or overwrite the existing file, losing its members. The code should first attempt to load an existing personal Pod with that name before creating a new one.
| }; | ||
| // Present by exact route, whatever roles it fills: never write a | ||
| // duplicate row. The operator route with no member rows of its own is | ||
| // your current model; it cannot be toggled off, only switched. |
There was a problem hiding this comment.
Role-less models have an empty roles vector, so Iterator::all returns true and the function incorrectly treats them as the operator route. Replace the check with an exact one-role operator test so that only true operator-only routes are protected, and role-less models can be removed.
| // your current model; it cannot be toggled off, only switched. | |
| let only_operator = present.roles.len() == 1 && present.roles[0] == "operator"; |
Bugbot couldn't run - usage limit reachedBugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit. A user or team admin can review and increase usage limits in the Cursor dashboard. (requestId: serverGenReqId_eacb30ed-5d1e-4986-9b10-2db63beb41a3) |
|
Claude finished @Hmbown's task in 2m 47s —— View job Review: fleet-as-models (F1) — re-review after latest push
Findings (verified against the current HEAD, not the earlier bot rounds)1. let only_operator = present.roles.iter().all(|r| r == "operator");
This directly contradicts the PR description's primary UX claim (" Fix: guard on a real match, e.g. 2. fn selected_or_new(workspace: &Path) -> Result<(FleetFile, FleetScope, bool), FleetStoreError> {
if let Some(selected) = resolve_selected_fleet(workspace)? { ... }
let fleet = FleetFile::new(DEFAULT_FLEET_NAME.to_string(), ...)?;
save_fleet(&fleet, FleetScope::Personal, workspace)?;
...
}
Fix: before creating, attempt to load an existing personal Pod named Minor / non-blocking
Notes (good choices, confirmed fixed since earlier review rounds)
|
There was a problem hiding this comment.
Codewhale review
PR adds fleet-as-models listing and picker toggling with persistence via Pod members. Overall structure reuses Pod store and tests pass, but there's a logic issue in duplicate detection for role-less adds.
Findings
- [WARNING] Role-less add treats any existing role as a duplicate (
crates/tui/src/fleet/members.rs:169)
In add_fleet_model, the duplicate check for a role-less add (wantedcontainsNone) usesrole.is_none_or(...).Option::is_none_orreturns true forNonewithout evaluating the predicate, so any existing member on the same route satisfies the check, not just a role-less member. This prevents adding a distinct role-less member after a role-specific member, and later removing that role drops the route entirely instead of leaving the role-less member.
Suggestions
-
crates/tui/src/fleet/members.rs:169— For a role-less add, compare only against empty-role members; for a role-specific add, compare against that role. Usemap_or_elseinstead ofis_none_orsoNoneis handled explicitly.&& role.map_or_else(|| m.role.trim().is_empty(), |role| m.role.eq_ignore_ascii_case(role))
Assessment
Solid feature addition that reuses existing Pod storage; one edge-case bug in membership duplicate detection should be fixed, ideally with a test for adding role-first then role-less.
Advisory review by Codewhale (codewhale review --pr 5815 --post, head 046859ba327444a159d807f0d856df7bed1ccd97). Line-specific findings are also posted as inline review comments; mechanical fixes arrive as committable suggestions you can apply from the Files tab. CODEOWNERS approval still governs merge.
| && m.model | ||
| .as_deref() | ||
| .is_some_and(|id| id.eq_ignore_ascii_case(model)) | ||
| && role.is_none_or(|role| m.role.eq_ignore_ascii_case(role)) |
There was a problem hiding this comment.
[WARNING] Role-less add treats any existing role as a duplicate
In add_fleet_model, the duplicate check for a role-less add (wanted contains None) uses role.is_none_or(...). Option::is_none_or returns true for None without evaluating the predicate, so any existing member on the same route satisfies the check, not just a role-less member. This prevents adding a distinct role-less member after a role-specific member, and later removing that role drops the route entirely instead of leaving the role-less member.
| && m.model | ||
| .as_deref() | ||
| .is_some_and(|id| id.eq_ignore_ascii_case(model)) | ||
| && role.is_none_or(|role| m.role.eq_ignore_ascii_case(role)) |
There was a problem hiding this comment.
For a role-less add, compare only against empty-role members; for a role-specific add, compare against that role. Use map_or_else instead of is_none_or so None is handled explicitly.
| && role.is_none_or(|role| m.role.eq_ignore_ascii_case(role)) | |
| && role.map_or_else(|| m.role.trim().is_empty(), |role| m.role.eq_ignore_ascii_case(role)) |
…ntract The ⇧F picker action label localized its value as 'fleet' (and locale-translated fleet words) while the customer-facing term for the assembled model team is Pod (PRODUCT_PRD: Pod is the public product term; Fleet remains internal-only). The Lint product-vocabulary gate caught en.json; fixed the value in all 15 locales so no locale ships fleet-family copy. Keys stay (internal identifiers may keep Fleet). Signed-off-by: CodeWhale Bot <bot@codewhale.net>
Bugbot couldn't run - usage limit reachedBugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit. A user or team admin can review and increase usage limits in the Cursor dashboard. (requestId: serverGenReqId_c9470f4b-3527-4580-85a5-7e82a065426e) |
|
Claude finished @Hmbown's task in 2m 40s —— View job Review: fleet-as-models (F1) — re-review after latest push (b364c63)
The latest commit ( That means the substantive logic in 1.
|
There was a problem hiding this comment.
Codewhale review
The PR introduces fleet-as-models: a crate::fleet::members store that reuses Pod members as the source of truth, /pod add|remove|models commands, a ⇧F picker toggle, and updated /models output. The implementation is generally well-structured and tested, but there are a few correctness and compatibility concerns, including a bug that prevents removing member rows with the 'operator' role and a provider validation edge case for custom providers named like built-ins.
Findings
- [ERROR]
toggle_fleet_modelcannot remove member rows that have the 'operator' role (crates/tui/src/fleet/members.rs)
When a model is both the operator route and also has member rows with the role 'operator' (e.g.,/pod add openrouter model operator),models_ofdeduplicates roles so the resultingFleetModel.rolesis["operator"](the member row's 'operator' is ignored as duplicate). Intoggle_fleet_model, the checkpresent.roles.iter().all(|r| r == "operator")therefore returns true even though member rows exist. The function then returnsUnchangedwithout removing those member rows, leaving stale rows that cannot be toggled off via⇧For removed via/pod remove(which would work if called directly, but toggle never calls it). This is a correctness bug in the main user-facing toggle path. - [WARNING]
provider_id_is_configuredrejects custom providers whose name matches a built-in provider (crates/tui/src/commands/groups/core/fleet.rs)
The function checksApiProvider::parse(provider_id)and, if it returnsSome, immediately returns the result ofprovider_is_configured_for_activewithout ever checking the custom provider table. If a user has configured a custom provider with a name that coincides with a built-in provider (e.g., a customopenrouterendpoint different from the built-in), the function will incorrectly report it as unconfigured because it never falls through to theapp.config.providers.customcheck. This violates the PR description's claim that models can be added from 'the providers you configured'. - [WARNING] Let-chains (
&&) may raise the Minimum Supported Rust Version (MSRV) (crates/tui/src/commands/groups/core/fleet.rs)
The code infleet_models_textusesif let Some(cost) = row.cost.as_ref() && let (Some(input), Some(output)) = (cost.input, cost.output)and similar constructs forrow.limit. This let-chain syntax became stable in Rust 1.88. If the repository's MSRV is lower (e.g., 1.75), this will fail to compile. This should be verified against the project'srust-toolchainor CI configuration, and replaced with nestedif letblocks if needed. - [INFO] Missing tests for
fleet_models_textcatalog formatting and provider validation edge cases (crates/tui/src/commands/groups/core/fleet.rs)
The PR adds unit tests for the members store and picker sorting, butfleet_models_text(which formats price, context, and tool facts) has no test verifying the output. Likewise,provider_id_is_configuredhas no test for the custom-name-collision scenario or for built-in vs custom precedence. Given the feature's reliance on this validation, these tests should be added.
Suggestions
crates/tui/src/fleet/members.rs— Refactortoggle_fleet_modelto determine whether the route has any member rows pinning it, rather than relying solely on the unioned roles fromfleet_models. For example, callresolve_selected_fleetandload_fleet_atto inspectfleet.membersdirectly: if there are no member rows and the route is only the operator, returnUnchanged; otherwise callremove_fleet_model. This will correctly remove member rows even when the only role is 'operator'.crates/tui/src/commands/groups/core/fleet.rs— Inprovider_id_is_configured, reorder the checks: first check the custom provider table (including the active custom route) for a matching name, and only if no match is found, proceed to parse as a built-in and use the built-in configuration check. This ensures custom providers with names that coincide with built-ins are correctly recognized.crates/tui/src/commands/groups/core/fleet.rs— Replace the let-chainif let Some(cost) = ... && let (Some(input), Some(output)) = ...with nestedif letblocks to maintain compatibility with older Rust versions. Similarly handle therow.limitblock.
Assessment
The feature is well-designed and reuses existing Pod storage appropriately. The toggle bug and provider validation edge case are high-priority correctness issues that should be fixed before merge. The let-chain syntax may be a build-breaker if MSRV is older. Test coverage for the new commands and edge cases needs to be expanded.
Advisory review by Codewhale (codewhale review --pr 5815 --post, head b364c635ac3b312893df237334e124657c9ab1f5). Line-specific findings are also posted as inline review comments; mechanical fixes arrive as committable suggestions you can apply from the Files tab. CODEOWNERS approval still governs merge.
|
Superseded by #5826 (the 0.9.12 mega PR), which contains this PR's fleet-members work re-landed through the fleet slice with all ten review findings fixed. Note: this branch's tip commit ("picker fleet toggle says Pod") encodes the pre-2026-09-01-evening vocabulary; the founder has since made |
Pull request was closed
* feat(fleet): your fleet is the models you added, and it comes first
Design MODEL-ROUTING-CATALOG-20260901 §10, slice F1. A fleet model is a Pod
member: the selected Pod file's operator route plus every member row that
pins an exact provider + model; the roles a model fills are the member rows
that pin it. No second store.
- crate::fleet::members: fleet_models / add_fleet_model / remove_fleet_model
/ toggle_fleet_model + change_receipt; Config::fleet_members(workspace) is
the read seam for the operator-awareness slice (F2).
- /pod models | add <provider> <model> [role…] | remove <provider> <model>
(also via the /fleet alias). A model the configured provider does not
serve is rejected; the first add creates and selects a user-global Pod
named 'My fleet'.
- /model picker: ⇧F adds or removes the row's exact route; fleet models
lead the list labelled 'fleet · <roles>', ahead of ⇧P pins and providers.
- /models prints the fleet before the provider list ('Your fleet is the
session model only' when empty).
- PickerActionFleet message in all 15 locales; docs/FLEET.md 'Your fleet
as models'.
Tests: scripts/dev-test.sh tui fleet::members groups::core::fleet
model_picker format_helpers — Summary 37 tests run: 37 passed, 11834
skipped.
Signed-off-by: CodeWhale Bot <bot@codewhale.net>
* fix(fleet): pass slugify by name (clippy redundant_closure)
cargo clippy -p codewhale-tui --all-targets -- -D warnings -A clippy::too_many_arguments -A clippy::uninlined_format_args -A clippy::unnecessary_map_or: no findings.
Signed-off-by: CodeWhale Bot <bot@codewhale.net>
* fix(tui): review fixes for fleet toggle and /pod add provider validation\n\n- Reject unconfigured provider ids in "/pod add" before writing, reusing\n the existing provider_is_configured_for_active predicate and custom\n provider table checks.\n- Add App.config snapshot so commands can consult the loaded config.\n- Update the stale DEFAULT_FLEET_NAME doc comment to mention ⇧F.\n- Sync crates/tui/CHANGELOG.md.
* style: cargo fmt
* fix(web): align react with react-dom 19.2.8 to unbreak npm ci
Dependabot #5801 bumped react-dom to 19.2.8, whose peer range requires
react 19.2.8; the lockfile still resolved react 19.2.6, so 'npm ci' in
web/ failed ERESOLVE on main and on every branch that merged it
(Lint & Type Check red). Align react to 19.2.8; install verified clean.
Signed-off-by: CodeWhale Bot <bot@codewhale.net>
* brand: trace supplied whale assets
Signed-off-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Hunter Bown <hmbown@gmail.com>
* brand: align icon ombre and generated tokens
Signed-off-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Hunter Bown <hmbown@gmail.com>
* brand: use white icon tile
Signed-off-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Hunter Bown <hmbown@gmail.com>
* brand: deepen ombre light stop
Signed-off-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Hunter Bown <hmbown@gmail.com>
* brand: wordmark takes the blue ombre
Signed-off-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Hunter Bown <hmbown@gmail.com>
* tui: recover from image-input rejections by non-vision routes
Signed-off-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Hunter Bown <hmbown@gmail.com>
* tui: localize image rejection recovery
Signed-off-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Hunter Bown <hmbown@gmail.com>
* chore: format 0.9.12 mega branch
Signed-off-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Hunter Bown <hmbown@gmail.com>
* Redesign Fleet role labels and agent cards
* feat(tui): launch hero as wordmark + small surfacing mark
Signed-off-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Hunter Bown <hmbown@gmail.com>
* design(tui): retune whale palette to codewhale navy / ombre sky
Field, chrome, panel, plate and raised surfaces move onto the brand navy
(#070C1D → #142352 → #1A2C63); interaction blue becomes the ombre sky
#6AA6DC, light-mode action the ombre cobalt #1535B2; ice/cyan/border/tool
tints follow. web/app/tokens.css regenerated via
scripts/export-design-tokens.py.
Co-Authored-By: Hunter Bown <hmbown@gmail.com>
* test(tui): re-bless ink goldens for navy palette
Signed-off-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Hunter Bown <hmbown@gmail.com>
* web: Space Mono wordmark, quiet layout refresh, fleet vocabulary in site + docs
Space Mono (OFL) outlined wordmark rebuilt via scripts/build-wordmark.py,
wired as --font-display through next/font/google; body stays IBM Plex Sans,
code stays JetBrains Mono. Nav loses the issue strip, strapline, Discord badge
and second filled CTA; home loses the ticker, seals and tilt figure; docs
shell hero collapses to a one-line band; footer uses the inverted wordmark.
Public noun is fleet (/fleet, codewhale fleet, /docs/fleet canonical; /pod,
codewhale pod, /docs/pod remain compatibility aliases) across docs/, site
dictionaries, vocabulary contract and public-surface facts.
No-Issue: 0.9.12 website lane
* brand: keep the traced wordmark; drop Space Mono outline build
* web: IBM Plex Sans Condensed as display face
* brand: Plex Sans Condensed wordmark; nav mark; drop fabricated home demos; AA meta text
* feat(tui): bottom dock tabs — clickable panel switch + close
Signed-off-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Hunter Bown <hmbown@gmail.com>
* Fix Fleet role migration verification
* fix(tui): dock keys yield Tab to mode/permission cycles
Signed-off-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Hunter Bown <hmbown@gmail.com>
* web: Impeccable polish — type floors, heading outline, docs measure; add PRODUCT.md/DESIGN.md
* Resolve canonical Fleet roles to legacy members
* web: flat hero — drop cyan glow/gradients/shadow, muted eyebrows
* design: PRODUCT.md/DESIGN.md at repo root — shell direction, bottom dock, anti-slop rules, 0.9.12 tokens
* Auto-enroll used models into the Fleet
* brand: keep the founder's wordmark rasters as the source of truth
The web lane replaced brand/wordmark0901.png and brand/wordmarkinverted.png
with Plex Sans Condensed renders. The founder-supplied PNGs are the brand
source; the SVGs are re-traced from them in a following commit.
* tui(mark): the launch mark has one rung
The hero now paints the small mark over the wordmark, so the medium and
large rungs and the for_area ladder have no consumer and fail the
dead-code lint. Remove them rather than allow them.
* brand: trace the founder's wordmark to SVG
brand/wordmark.svg and wordmark-inverted.svg were an IBM Plex Sans
Condensed text render; the founder's wordmark is the rounded monoline in
brand/wordmark0901.png. scripts/brand/trace-brand.py now traces that PNG
(magick threshold 60% + trim, potrace -s --flat -t 20 -O 0.4 -a 1.2),
folds potrace's transform into one compact path in a tight 1874x264
viewBox, and writes the navy #142352 and white colourways from the same
geometry. The Plex builder scripts/build-wordmark.py is gone with it.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HSVsAXZJnKGZmqkwH1CeKH
* web: derive icons and OG image from the traced mark
app/icon.svg is now the white whale on the #142352 rounded tile as on the
founder's sheet; favicon.ico (48/32/16), apple-icon.png, icon-192.png and
icon-512.png are rasterised from it by scripts/brand/trace-brand.py, and
the manifest colours are the same navy. The social card keeps the navy
ground, white mark and traced wordmark and restores the identity phrase
the page-meta contract expects.
The nav sits on the dark field on every route, so it renders the inline
Whale (white brand ink) and the inverted wordmark instead of a
prefers-color-scheme picture pair; the wordmark box uses object-fit so
the ~7.1:1 trace scales inside the compact nav instead of squashing.
Exploration rasters web/public/brand/codewhale-mark-*.png and their
web/brand/mark tile sources had no consumers and are removed;
codewhale-mark.png stays (public-auth-routes pins its hash).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HSVsAXZJnKGZmqkwH1CeKH
* web: map stray hard-coded colours to navy tokens
globals.css carried a handful of literal navy-family greys, ice hairlines,
seafoam borders and a cyan glow beside the generated --whale-* tokens.
Each now reads the token it was approximating (whale-bg/chrome/panel,
whale-ice, whale-accent-secondary, whale-action, whale-cyan,
whale-text-dim), and the docs light sheet inks the mark in the brand
navy via --whale-composer (#142352).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HSVsAXZJnKGZmqkwH1CeKH
* palette: inventory WHALE_* tokens before the one-name-per-colour collapse
Shell design §2.6 (SHELL-DESIGN-20260901) measured "58 WHALE_* symbols;
one colour under five names; 5 dead tokens". Receipt before touching
anything, generated from crates/tui/src/palette/tokens.rs. "uses" is the
whole-word count across crates/ excluding the const's own definition and
`use`/`pub use` lines (wrapper consts inside tokens.rs count).
name value alias-of dead uses
WHALE_BG_RGB (7, 12, 29) 3
WHALE_CHROME_RGB (12, 21, 49) 1
WHALE_PANEL_RGB (16, 28, 64) 3
WHALE_COMPOSER_RGB (20, 35, 82) 3
WHALE_ELEVATED_RGB (26, 44, 99) 4
WHALE_SELECTION_RGB (30, 60, 143) 3
WHALE_TEXT_BODY_RGB (246, 242, 232) 10
WHALE_TEXT_SOFT_RGB (182, 192, 212) 4
WHALE_TEXT_MUTED_RGB (147, 160, 184) 3
WHALE_TEXT_HINT_RGB (138, 153, 179) 3
WHALE_TEXT_DIM_RGB (105, 119, 145) yes 0
WHALE_ACTION_RGB (106, 166, 220) 6
WHALE_COBALT_RGB (21, 53, 178) yes 0 (web: --whale-cobalt x3)
WHALE_ICE_RGB (221, 238, 249) yes 0 (web: --whale-ice, rustRgb("WHALE_ICE"))
WHALE_CYAN_RGB (120, 188, 232) 2
WHALE_ACCENT_SECONDARY_RGB (79, 209, 197) 11
WHALE_BRAND_ORANGE_RGB (255, 138, 61) 1
WHALE_BRAND_MAGENTA_RGB (240, 78, 184) 1
WHALE_HUMAN_RGB (246, 196, 83) 5
WHALE_ACCENT_PRIMARY_RGB = WHALE_ACTION_RGB WHALE_ACTION_RGB 8
WHALE_WORKING_GREEN_RGB (155, 214, 111) 5
WHALE_ACCENT_ACTION_RGB = WHALE_ACTION_RGB WHALE_ACTION_RGB yes 0
WHALE_ERROR_RGB (255, 134, 178) 9
WHALE_ERROR_HOVER_RGB (255, 156, 194) 3
WHALE_ERROR_SURFACE_RGB (43, 21, 34) 6
WHALE_ERROR_BORDER_RGB = WHALE_ERROR_RGB WHALE_ERROR_RGB 3
WHALE_ERROR_TEXT_RGB (255, 219, 232) 3
WHALE_WARNING_RGB (255, 122, 89) 4
WHALE_SUCCESS_RGB = WHALE_WORKING_GREEN_RGB WHALE_WORKING_GREEN_RGB 9
WHALE_INFO_RGB = WHALE_ACTION_RGB WHALE_ACTION_RGB 17
WHALE_BORDER_RGB (42, 63, 114) 1
WHALE_REASONING_TEXT_RGB (224, 153, 72) 13
WHALE_REASONING_SURFACE_RGB (42, 34, 24) 3
WHALE_REASONING_TINT_RGB (22, 36, 74) 7
WHALE_DIFF_ADDED_RGB (87, 199, 133) 3
WHALE_DIFF_DELETED_RGB = WHALE_ERROR_RGB WHALE_ERROR_RGB yes 0
WHALE_DIFF_ADDED_BG_RGB (18, 42, 34) 3
WHALE_DIFF_DELETED_BG_RGB (52, 24, 39) 3
WHALE_MODE_AGENT_RGB (126, 180, 232) 4 (via MODE_AGENT: 13)
WHALE_MODE_YOLO_RGB (255, 112, 160) 4 (via MODE_YOLO: 13)
WHALE_MODE_PLAN_RGB (185, 220, 236) 4 (via MODE_PLAN: 13)
WHALE_MODE_OPERATE_RGB (173, 136, 255) 4 (via MODE_OPERATE: 14)
WHALE_TOOL_LIVE_RGB = WHALE_ACCENT_SECONDARY_RGB 3 (via ACCENT_TOOL_LIVE: 5)
WHALE_TOOL_ISSUE_RGB = WHALE_ERROR_RGB 3 (via ACCENT_TOOL_ISSUE: 5)
WHALE_TOOL_OUTPUT_RGB = WHALE_TEXT_SOFT_RGB 3 (via TEXT_TOOL_OUTPUT: 13)
WHALE_TOOL_SURFACE_RGB (15, 26, 58) 3 (via SURFACE_TOOL: 5)
WHALE_TOOL_ACTIVE_RGB (24, 44, 94) 3 (via SURFACE_TOOL_ACTIVE: 9)
WHALE_ACCENT_PRIMARY Color(WHALE_ACCENT_PRIMARY_RGB) -> WHALE_ACTION 9
WHALE_ACTION Color(WHALE_ACTION_RGB) 85
WHALE_LIVE Color(WHALE_ACCENT_SECONDARY_RGB) 17
WHALE_HUMAN Color(WHALE_HUMAN_RGB) 41
WHALE_INFO Color(WHALE_INFO_RGB) -> WHALE_ACTION 105
WHALE_BG Color(WHALE_BG_RGB) 96
WHALE_CHROME Color(WHALE_CHROME_RGB) 5
WHALE_PANEL Color(WHALE_PANEL_RGB) 15
WHALE_COMPOSER Color(WHALE_COMPOSER_RGB) 5
WHALE_ERROR Color(WHALE_ERROR_RGB) 35
57 WHALE_* consts. Pure aliases (9): ACCENT_PRIMARY_RGB, ACCENT_ACTION_RGB,
ERROR_BORDER_RGB, SUCCESS_RGB, INFO_RGB, DIFF_DELETED_RGB, TOOL_LIVE_RGB,
TOOL_ISSUE_RGB, TOOL_OUTPUT_RGB. #[expect(dead_code)] (5): TEXT_DIM_RGB,
COBALT_RGB, ICE_RGB, ACCENT_ACTION_RGB, DIFF_DELETED_RGB.
Non-WHALE aliases of the same blue in tokens.rs: STATUS_INFO (8 uses),
ACCENT_PRIMARY (dead, 0). One colour, #6AA6DC, under seven symbols:
WHALE_ACTION(_RGB), WHALE_INFO(_RGB), WHALE_ACCENT_PRIMARY(_RGB),
WHALE_ACCENT_ACTION_RGB, STATUS_INFO, ACCENT_PRIMARY — 225 call sites.
Script: python3 over tokens.rs + grep -rnw crates; kept out of scripts/
(one-off receipt, the numbers live here).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HSVsAXZJnKGZmqkwH1CeKH
* palette: one name per colour — collapse WHALE_INFO / WHALE_ACCENT_PRIMARY into WHALE_ACTION
Shell design §2.6: "`WHALE_INFO`, `WHALE_ACTION`, `WHALE_ACCENT_PRIMARY`
and their `_RGB` twins are one colour. Collapse to `WHALE_ACTION`."
Mechanical rename across crates/ (word-boundary sed, no value changes):
WHALE_INFO, WHALE_INFO_RGB -> WHALE_ACTION, WHALE_ACTION_RGB
WHALE_ACCENT_PRIMARY(_RGB) -> WHALE_ACTION(_RGB)
palette::STATUS_INFO -> palette::WHALE_ACTION
WHALE_ACCENT_ACTION_RGB, ACCENT_PRIMARY (dead aliases) -> deleted
The `STATUS_INFO` static in commands/groups/config is an unrelated
CommandInfo and is untouched.
Where two names met in one predicate (adapt.rs light/solarized/community
remaps, grayscale text-soft bucket, SemanticForegroundRole::Action) the
duplicate disjuncts are dropped; `use` lists deduped; the
"primary accent aligns with action" test collapses to its one live
assertion (action blue != human gold). The Blue Stage doc comment moves
onto WHALE_ACTION_RGB. `palette::grammar` untouched: it reads UiTheme
slots, never these consts.
Evidence (CARGO_BUILD_BUILD_DIR=.../mega-tokens):
cargo check -p codewhale-tui --all-targets -> Finished, 0 warnings
cargo clippy -p codewhale-tui --all-targets --all-features --locked
-- -D warnings (CI allow-list) -> clean
cargo test -p codewhale-tui --lib palette::tests:: --locked -- --skip command_palette
-> test result: ok. 59 passed; 0 failed
RUST_MIN_STACK=67108864 cargo test -p codewhale-tui --lib startup_ink --locked
-> test result: ok. 1 passed; 0 failed (ink goldens unchanged)
cargo test -p codewhale-tui --lib --locked -- menu_style cursor_accent color_compat
-> test result: ok. 36 passed; 0 failed
Pre-existing, not from this diff (reproduced on the stashed tree):
tui::command_palette tests, feat012_ac1 and the startup_ink golden
overflow the default test-thread stack in a debug build; they pass with
RUST_MIN_STACK=64MiB.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HSVsAXZJnKGZmqkwH1CeKH
* palette: delete the dead whale tokens
Shell design §2.6: "delete the five dead tokens". Two of the five went
with the alias collapse (WHALE_ACCENT_ACTION_RGB, ACCENT_PRIMARY); this
removes the rest that have no consumer in crates/ and no web consumer:
WHALE_TEXT_DIM_RGB (105,119,145) 0 uses, no --whale-text-dim on the site
WHALE_DIFF_DELETED_RGB = WHALE_ERROR_RGB 0 uses, no --whale-diff-deleted
ACCENT_SECONDARY Color(WHALE_ACCENT_SECONDARY_RGB) 0 uses (TEXT_ACCENT/WHALE_LIVE carry it)
STATUS_NEUTRAL = TEXT_MUTED 0 uses
Kept, with a comment saying why the `#[expect(dead_code)]` is honest:
WHALE_COBALT_RGB and WHALE_ICE_RGB have no TUI consumer but the site
reads them through the token export (`--whale-cobalt` x3, `--whale-ice`
and `rustRgb("WHALE_ICE")` in web/lib/blue-stage-contract.test.ts).
Mode and tool-surface `_RGB` tuples stay: each is consumed through its
Color wrapper (MODE_AGENT/YOLO/PLAN/OPERATE 13-14 call sites each,
themes.rs + color_compat.rs; SURFACE_TOOL 5, SURFACE_TOOL_ACTIVE 9,
ACCENT_TOOL_LIVE 5, ACCENT_TOOL_ISSUE 5, TEXT_TOOL_OUTPUT 13). The §1
"12 tokens with zero consumers" counted the tuples, not their wrappers.
Evidence: cargo check -p codewhale-tui --all-targets -> Finished, 0 warnings;
cargo test -p codewhale-tui --lib palette::tests:: --locked -- --skip command_palette
-> test result: ok. 59 passed; 0 failed.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HSVsAXZJnKGZmqkwH1CeKH
* web: regenerate tokens.css after the whale token collapse
scripts/export-design-tokens.py (never hand-edited). Ten lines gone:
--whale-accent-primary(-rgb), --whale-accent-action(-rgb),
--whale-info(-rgb), --whale-text-dim(-rgb), --whale-diff-deleted(-rgb).
No site stylesheet or component consumed any of them
(grep -rn "\-\-whale-" web/app web/components web/lib); the only
reference was the alias-chain example in web/lib/whale-tokens.ts's doc
comment, now `--whale-success` -> `--whale-working-green` -> `#9bd66f`
(the old example also quoted a hex that stopped being true a retune ago).
Evidence:
cd web && python3 ../scripts/export-design-tokens.py --check (CI: npm run check:tokens)
-> design tokens up to date (1 file(s), 42 tokens)
vitest run lib/blue-stage-contract lib/docs-theme-contract
-> Test Files 2 passed (2) / Tests 6 passed (6)
(vitest ran against the main checkout's node_modules via a temporary
symlink; this worktree has none installed.)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HSVsAXZJnKGZmqkwH1CeKH
* docs(design): the status-bar grammar names the one blue token
STATUS_BAR_COLOR_GRAMMAR.md never named a collapsed token, so this is the
one sentence it needed: the Identity blue is `WHALE_ACTION`, its former
aliases (`WHALE_INFO`, `WHALE_ACCENT_PRIMARY`, `STATUS_INFO`) are gone, and
the whale theme's `info` / `accent_primary` slots both hold it. No other
document in the repo named them (grep over *.md, *.ts, *.tsx, *.css,
*.py, *.toml, *.yml, *.json outside node_modules); the root DESIGN.md
already speaks in CSS names.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HSVsAXZJnKGZmqkwH1CeKH
* palette: the field follows the terminal-owned shell; `underwater` aliases deepsea
Shell design §2.0 decision 1 (founder: "We aren't supposed to be using a
blue background anymore"): ground is the terminal's; the navy field is
painted only under the opt-in deepsea column.
What was already true, verified before changing anything:
- Settings::default().theme is "terminal" (settings.rs:103).
- The whale pair (UI_THEME / LIGHT_UI_THEME) ends in
`.with_terminal_native_shell()`: surface, panel, composer, header and
footer are `Color::Reset`, pinned by
`whale_pair_flat_shells_are_terminal_native_without_erasing_semantic_surfaces`.
- The ink goldens' legend reads `a reset on reset`.
- OceanTreatment::Flat is the default; Deepsea repaints Reset cells through
OceanRamp::for_theme, which matches the whale pair by name + Reset shell.
The reviewer's citations (tokens.rs:6/:250/:465) are the token definitions
deepsea and the semantic surfaces still need, not the theme.
What was not true: ~90 direct `bg(palette::WHALE_BG)` paints in pickers,
overlays and full-screen views (provider_picker 14, views/mod.rs 11,
user_input, live_transcript, help, session/file/model pickers ...) bypass
the theme, and `adapt_bg_for_theme` only remapped them for
`theme_remap_active` presets. On the whale theme they laid navy patches
over the terminal ground. Rung 2 fix, one rule in palette/adapt.rs: the
field (`WHALE_BG` / `BACKGROUND_DARK`) always follows `ui.surface_bg` —
Reset on the whale pair, the user's `background_color` override when set,
the preset surface elsewhere. Panels, selection, elevation, error and
diff surfaces are untouched; no widget file changes.
`underwater` is now an accepted alias of `deepsea` in
settings.rs (normalize + `set`), OceanTreatment::parse and the
config_ui serde enum. Tests extended in place; the color_compat light
test now expects the Reset shell it already had for theme consumers.
DESIGN.md "Field" says the TUI ground is the terminal's own background
and the navy field is deepsea-only.
Contrast, all whale text/accent tokens on #000, #1e1e1e (VS Code),
#282c34 (One Dark), #002b36 (Solarized dark), #300a24 (Ubuntu), #0d1117
(GitHub dark), #282a36 (Dracula): body 12.5-18.8:1, soft 7.7-11.5, muted
5.3-8.0, hint 4.9-7.3 (floor 3:1), action 5.4-8.1, human 8.6-12.9, live
7.5-11.3, error 6.2-9.3, warning 5.5-8.2, green 8.2-12.2, reasoning
5.9-8.8. Only `border` (#2A3F72) is low, 1.4-2.1:1: a non-text hairline.
No token value changed.
Seen, tmux 80x24 PTY, TERM=xterm-256color COLORTERM=truecolor, hermetic
HOME, debug build, counting `48;2;R;G;B` background sequences:
default (Terminal theme): startup, /theme picker, Help — no painted
background before or after (picker shows only accent swatches).
Blue Stage selected via T/Down/Enter, then F1 Help:
before: 15 x `48;2;7;12;29` (WHALE_BG) + 1 x selection row
after: 1 x `48;2;30;60;143` (selection row only)
Startup stage on Blue Stage: none, before and after.
Evidence (CARGO_BUILD_BUILD_DIR=.../mega-tokens, RUST_MIN_STACK=16 MiB as CI):
cargo check -p codewhale-tui --all-targets -> Finished, 0 warnings
cargo test -p codewhale-tui --lib --locked -- color_compat palette::tests::
ocean:: ocean_treatment live_transcript views::tests startup_ink
--skip command_palette -> test result: ok. 238 passed; 0 failed
cargo test -p codewhale-tui --lib --locked (full) ->
test result: FAILED. 11901 passed; 7 failed; 13 ignored
1 was this change (color_compat light test, updated above); the other 6
are role-name / slash-list assertions from other lanes on this branch
(scout<->explore, worker<->general, slash.impeccable) and untouched.
Ink goldens unchanged.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HSVsAXZJnKGZmqkwH1CeKH
* tests(palette_audit): re-pin whale roles to the retuned palette
`whale_roles_are_pinned_and_non_colliding` still pinned the pre-navy
values (WHALE_BG (3,7,13), ACTION (106,174,242), ...) and failed on this
branch before the token slice touched anything. Pins now match tokens.rs;
no colour value changes.
cargo test -p codewhale-tui --test integration --locked palette_audit
-> test result: ok. 3 passed; 0 failed
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HSVsAXZJnKGZmqkwH1CeKH
* tui/cli/web: fleet is the public product term; /pod, codewhale pod stay aliases
Founder decision 2026-09-01: "fleet" is the customer-facing name for the
assembled model team and "Pod" is retired from product copy. `/fleet` is
the canonical slash command and `codewhale fleet` the canonical CLI verb;
`/pod`, `codewhale pod`, `loadout`, and `party` remain parser aliases.
Storage keys, the ledger file name, config tables, protocol identifiers,
and MessageId variant names keep their current spelling.
- CommandInfo name/aliases/usage, help text, and the unknown-verb error
flip to /fleet; `/fleet fleets` (saved/manage) is the saved-fleet picker
with `/fleet pods` kept as an alias.
- All 15 locale packs: localized values say fleet; the settings goldens
follow. `KbCompleteCycleModes` names the modes as Plan → Work → Operate
(Act is only a compatibility alias per docs/MODES.md).
- `scripts/check-tui-product-vocabulary.sh` now rejects `Pod` in en.json
instead of rejecting `fleet` in every pack.
- Hotbar id `slash.fleet` is canonical; persisted `slash.pod` normalizes.
- Fleet store error prose says fleet.
- Docs: PRODUCT.md lists the current role tokens (general, explore,
planner, reviewer, implement, test, advisor, custom) and names the old
spellings as aliases; docs/FLEET.md uses one role vocabulary;
web/lib/content/vocabulary.ts ADVISORY_ROLE is Advisor with consultant/
oracle as the legacy spellings (matches fleet/profile.rs migration).
Evidence:
sh scripts/check-tui-product-vocabulary.sh -> exit 0
cargo test -p codewhale-config -p codewhale-lane --locked
-> 638 passed; 0 failed / 62 passed; 0 failed
cargo test -p codewhale-cli --locked -- fleet pod -> 3 passed; 0 failed
cargo test -p codewhale-tui --lib --locked -- fleet::store fleet::members
fleet::identity -> 24 passed; 0 failed
cargo test -p codewhale-tui --lib --locked -- groups::core::fleet
localization command_palette hotbar fleet_roster settings widgets
fleet::control pod_workers -> 605 passed; 1 failed (the failure is
slash_source_matches_command_palette_command_entries, which reads the
machine's ~/.claude/skills and finds an `impeccable` skill; it fails
identically without this change)
cd web && npm test -- lib/content/vocabulary.test.ts -> 11 passed
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HSVsAXZJnKGZmqkwH1CeKH
* chore(tui): clear the six clippy 1.98 errors the base lanes left
needless_borrow on provider_identity_for_persistence (five sites) and a
collapsible_if in the work-surface mouse path. No behaviour change.
* tui(composer): restore double-tap Enter as the send-now gesture
While a turn is running, the first bare Enter queues the message (as
before) and opens a 500 ms window (`App::DOUBLE_TAP_WINDOW`, the value
the removed code in c5c42b7 used). A second bare Enter inside that
window with an empty composer promotes the just-queued message to a
Steer through `attempt_steer_with_queue_fallback` — the same path
Ctrl+Enter takes, so there is one steering path. A second Enter with
new text is an ordinary queue; Ctrl+Enter still steers immediately;
outside a turn Enter is unchanged. `enter_with_double_tap` is the one
decision point again (`take_queued_for_double_tap_steer` routes through
it), and `submit_disposition_does_not_mutate_the_queue` stays true.
The posture bar advertises the gesture while the window is open
(`PostureHintEnterAgain`, next commit).
Tests (cargo test -p codewhale-tui --lib <filter> --locked):
double_tap: test result: ok. 3 passed; 0 failed
enter_with: test result: ok. 5 passed; 0 failed
submit_disposition: test result: ok. 6 passed; 0 failed
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HSVsAXZJnKGZmqkwH1CeKH
* tui(shell): one owner per fact — posture bar, metrics line, no dead hints
Design: SHELL-DESIGN-20260901 §2.0 item 3, §2.2, §2.3, §2.3b, §2.11 and
the founder's 2026-09-02 redirect (Claude Code's grammar, less always-on
information). Under the composer there are exactly two chrome rows in
the default state, then the work surface only when it has content:
▶▶ ask (Shift+Tab) · work (Tab) · 2 agents · Esc to interrupt /rc …
deepseek-v4 · ctx 61% · $0.42 · ttft 400ms · 40 tok/s · ↓ 1.2K Ctrl+/ help
Fact → owner, before → after (composed 80x24 / 120x32 frames, working
turn with two sub-agents; "strip" = the work-surface bottom view):
fact before after
context % info line metrics line
cost footer (+ info line when priced) metrics line
model info line metrics line
provider info line (wide) metrics line (wide)
ttft / tok/s / ↓ behind /cost only metrics line
repo slug info line (+ idle empty state) launch header / git view (not chrome)
branch info line (+ idle empty state) launch header / git view (not chrome)
mode footer posture bar
permission footer posture bar
phase word footer ("sub-agents underway") transcript active row (not chrome)
elapsed footer ("1m 15s") roster rows (per agent)
agent count indicator row + info "pod 2/2" + "whales 2/4" posture bar ("2 agents")
+ dock tab + strip header (+ dock tab, strip header — other slice)
task/shell count indicator row above the composer posture bar
help hint footer keys legend (F1) + info line (Ctrl+/) metrics line (Ctrl+/), from the binding
other key hints footer "⌥V:output", compact "? help" none; cycle keys next to the chip they cycle
live hint footer "Esc to interrupt" posture bar hint slot
≥80 % microcopy footer right slot posture bar hint slot (outranks the hint)
notice / rc footer right slot / — posture bar right slot
Dead key hints removed: `F1:keys` / `fn+F1:keys` (Help binding's
`footer_chord` is now `Ctrl+/`; `info_help_hint` derives from the
binding), compact `? help`, and the `footer_action_hints` family. The
mode/permission cycle keys print only when the binding table admits
them at the current focus (no `(Tab)` on the launch stage).
Row order: composer → posture bar → metrics line → roster/to-do. The
#5286 background-work chip above the composer is gone (it repeated the
posture bar's counts); `PendingWork` stays as the counts' source.
Goldens re-blessed and read: footer_* (posture bar), infoline_startup_*,
infoline_work_* (metrics line), settings_* (the settings preview's
bottom row); infoline_settings_* deleted with the settings-path segment.
Commands run (CARGO_BUILD_BUILD_DIR=…/mega-frame, RUST_MIN_STACK=16777216):
cargo check -p codewhale-tui --all-targets clean
cargo test -p codewhale-tui --lib infoline --locked test result: ok. 11 passed; 0 failed
cargo test -p codewhale-tui --lib tideline_tests test result: ok. 64 passed; 0 failed
cargo test -p codewhale-tui --lib one_owner_tests test result: ok. 4 passed; 0 failed
cargo test -p codewhale-tui --lib shell_key_routing test result: ok. 13 passed; 0 failed
cargo test -p codewhale-tui --lib localization::tests test result: ok. 49 passed; 0 failed
cargo test -p codewhale-tui --lib --locked test result: FAILED. 11893 passed; 8 failed
(config_panel golden re-blessed after; the other 7:
4 fail on HEAD without this change (fleet rename
in flight), tmux clipboard passes alone, none in
files this change touches)
cargo clippy … -D warnings 6 pre-existing errors, none in this change's hunks
(config.rs:2106/2796, apply.rs:759, event_loop.rs:464,
session_state.rs:1004, work_surface/input.rs:401)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HSVsAXZJnKGZmqkwH1CeKH
* wip(launch): checkpoint for overnight takeover — braille mark + kitty tier + Claude-Code launch header compile and pass lib tests; PTY cucumber tests still wait on the old heading
Compiles (cargo check --all-targets clean). Lib tests for mark::, underwater::,
mouse_ui::, localization:: pass: test result: ok. 120 passed; 0 failed
(needs RUST_MIN_STACK=16777216 like scripts/dev-test.sh; the rust_i18n static
overflows a 2 MiB test thread with or without this change). Startup goldens
re-blessed and read. Clippy is red only in files outside this slice
(config.rs, apply.rs, session_state.rs, work_surface/input.rs, and a
pre-existing event_loop.rs borrow).
Not done: crates/tui/tests/cucumber/{screen_mode_inline_pty,
active_composer_pointer_pty,plugin_e2e_acceptance}.rs still wait for
"What are we working on?" and press 'w'; they need the new marker
("Codewhale v") and a typed message + Enter to begin the session.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HSVsAXZJnKGZmqkwH1CeKH
* wip(rail): checkpoint for overnight takeover — dock views compile, 136/137 work_surface tests pass, files/notepad/git views are stubs
Foundation for the one bottom dock (founder redirect 2026-09-02):
RailPanel is now the eight-view cycle (agents, tasks, background, files,
notepad, context, git, price; Pinned folded into tasks), an auto rule opens
agents/tasks/background while they have content, explicit picks stick until
Esc, and Ctrl+Tab / Ctrl+] (fwd) + Ctrl+Shift+Tab (back) cycle. Context and
price views render as rows; files, notepad, git are stubs in views.rs. The
classic sidebar line panels and their dead consumers are deleted.
Known: agent_rows_show_role_assignment_and_open_the_agent_transcript fails
(role_label 'worker' vs 'general'); role derivation is untouched here and
the failure is believed to predate this work — unverified.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HSVsAXZJnKGZmqkwH1CeKH
* wip(operate): checkpoint for overnight takeover — auto-goal + contract land; one Operate approval test needs a goal-complete mock
Operate now turns a non-trivial prompt into the goal through the same
GoalState::create path as explicit_goal_directive, appends the Operate
contract once as a user-role runtime message (append-only history, not
the prefix), shows the Operate goal receipt, and carries the new mode copy
in all 15 locale packs plus docs/MODES.md.
Compiles. Passing: goal (134), prompts (133, incl.
every_mode_shares_one_prompt_per_host), localization (49),
runtime_handoff (14), session_peek (15), history_cells (2), both new
engine tests. Known failing:
core::engine::tests::operate_model_shell_uses_normal_approval_and_workspace_sandbox
— its mocked model never reports the auto-set goal complete, so the turn
re-prompts to max_steps (wiremock expect(1) sees 199). Six clippy
needless_borrow/collapsible_if hits pre-exist on the branch base.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HSVsAXZJnKGZmqkwH1CeKH
* wip(fleet): checkpoint for overnight takeover — #5815 review findings 1-9 fixed, compiles, fleet tests green
Findings against the fleet-as-models work (verified against the current
tree, then fixed):
1. `toggle_fleet_model` decides presence by member rows, not the projected
role list (a role-less row projects to no role, so the old
`all(== "operator")` was vacuously true). Regression test
`toggle_removes_a_role_less_member_row` fails on the previous commit
("got Unchanged { … operator route … }") and passes now.
2. `selected_or_default` loads an existing personal `My fleet` instead of
overwriting it and never writes or selects before the add succeeds;
`FleetModelChange::Added` carries `created_fleet` + `selected_fleet`.
3. `fleet_models` returns `Result<Vec<_>, FleetStoreError>`: a broken
explicit selection is surfaced in `/fleet models`, `/models`, and the
picker's ⇧F instead of reading as "session model only".
4. `add_fleet_model` dedupes roles (case-insensitive) and returns
`Unchanged` without touching the file when every role is present
(test compares bytes and mtime).
5. `App.config` startup snapshot removed. `/fleet add|remove` now return
`AppAction::FleetAddModel|FleetRemoveModel`; the UI arm validates the
provider against the live `Config` (`fleet_provider_rejection`,
`fleet_catalog_rejection`, re-exported from `commands`).
6. ⇧F applies the same provider gate as `/fleet add`.
7. One roster path: `sync_fleet_roster` (extracted from the
FleetStoreChanged arm) plus `App::fleet_roster_stale`, flushed once per
event-loop iteration; `/fleet add|remove`, ⇧F, and every UI-side
auto-enroll site set it (`auto_enroll_fleet_model` now returns bool).
8. ⇧F receipts go through `push_status_toast` (Success/Info, 6 s) and
`set_sticky_status` (Error); no new `status_message` writes.
9. All new fleet prose is `tr(locale, MessageId::Fleet…)` (27 keys,
translated in all 15 packs); `FleetModelError` is typed with a
localized `message(locale)`.
10. No stale "`a` in /models" doc comment exists in the current tree.
Also re-blessed `config_panel_{80x24,120x32}` goldens (the Config tab
label says Fleet); the diff is that one label.
Evidence (this tree):
cargo check -p codewhale-tui --all-targets --locked -> Finished
cargo test -p codewhale-tui --lib --locked -- fleet::members
groups::core::fleet model_picker format_helpers fleet_roster
localization golden hotbar command_palette fleet::store
-> 257 passed; 1 failed (slash_source_matches_command_palette_
command_entries: reads ~/.claude/skills and finds `impeccable`;
fails identically on main in this environment)
cargo clippy … -D warnings (CI flags) -> the only remaining error is
crates/tui/src/tui/work_surface/input.rs:401 collapsible_if, which
belongs to the work_surface lane and predates this commit
cargo fmt --all -- --check -> clean
sh scripts/check-tui-product-vocabulary.sh -> exit 0
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HSVsAXZJnKGZmqkwH1CeKH
* feat(tui): launch card, canonical role vocabulary, DashScope descriptor, test fixes
- Launch is now our own card take (founder, 2026-09-02): thin top line
⑂ branch path; centred bordered card with the whale mark, Codewhale +
version, one true announcement (no-model warning / MCP news), and the
menu New worktree / Resume session / Changelog / Quit with real chords
right-aligned; Enter runs the highlighted entry, Up/Down move it, and
typing goes straight to the composer. The card dissolves on the first
keystroke or command (≤240ms, instant under reduced motion); the
working screen then shows ⑂ branch path + ⋮ MCP n/m, the
session_start receipt, and the posture bar + metrics line (hidden
while the card is up). The composer's bottom rule carries
model (effort) · permission — the route's one launch reading.
- Role vocabulary: fixtures and the stopship fleet/workflow now use the
canonical tokens (explore/implement/reviewer/test); the workflow JS
wire accepts canonical spellings with the pre-rename ones as aliases
(AgentType serde rename+alias, serialized form is canonical).
- Alibaba Model Studio (DashScope) joins the data-driven descriptor
table: international compatible-mode endpoint, DASHSCOPE_API_KEY,
live /v1/models as the Qwen model authority (never a compiled id).
- Tests: role-keyed gate fixtures moved to canonical tokens; the operate
model-shell test now seals the goal through the deferred update_goal
tool (deferral retry included) instead of pausing blindly; the
slash-source hotbar test isolates HOME; ⚠ and ⋮ gained ASCII
fallbacks; launch goldens re-blessed for the card.
* feat(tui): retire Pod from copy; canonical workflow fixture; gate clean-up
- Pod literal sweep across fleet views, worker runtime, sub-agent tool,
managed API, and command groups: user-facing copy now says Fleet
(founder vocabulary decision; /fleet canonical, /pod and
'codewhale pod' stay as compatibility aliases). Roster tests that
encoded the retired Pod-public/Fleet-internal split now assert the
public Fleet vocabulary.
- workflows/stopship + fleets/stopship use canonical role names
(explore/implement/reviewer/test); the workflow crate's own stopship
tests and required-roles list follow.
- Operate mode-picker hint shortened to fit 80 columns in every locale.
- Cucumber PTY launch flows: wait for the launch card, type the first
prompt and press Enter; the live shell is proven by the launch stage's
top line disappearing and the metrics line's ctx reading (the
interrupt hint needs a live turn, which an offline route never
starts). The stopship acceptance feature expects the canonical /fleet
help copy.
- CHANGELOG receipts synced; DESIGN.md shell direction records the card,
posture bar + metrics line, and the bottom view cycle.
* test(cucumber): launch-card PTY contract fixes
- The launch-card wait uses the menu's New worktree entry — unique to the
card; the bare wordmark also matches onboarding copy.
- The live-shell proof is the launch stage's top line disappearing plus
the metrics line's ctx reading; the interrupt hint needs a live turn,
which an offline route never starts, and the help hint sheds first at
the 40-column floor by design (SHELL-DESIGN-20260901 §2.2).
- The pointer-submit queue proof takes the offline onboarding seed into
account: the receipt toast proves the gesture, and the queue count
grows by exactly one.
* docs(readme): restore the canonical product screenshot the web contract pins
The brand header redesign dropped the assets/screenshot.webp embed; the
web public-surface contract pins the README and the website to one
canonical optimized screenshot (byte-identical, 1562x1256 lossless
VP8L). Re-embed it.
* test: platform-robust pointer-submit wait and cap-warning diagnostics
- The pointer queue proof accepts either the transient receipt toast or
the queue-count increment: toast timing differs across runners, and a
20 s wait missed a toast the queue dump proved had fired.
- The context-cap posture test dumps the drawn rows when the warning
count misses, instead of a bare 0 != 1, so a platform-specific shed
(the hint sheds first when the left run exceeds its budget) is
visible in CI.
* test: fix the linux-only context-cap shed; bounded pointer-click retry
- The context-cap posture test drew at 100 columns, where a backend-less
platform (linux CI paints 'files: workspace (unenforced)') sheds the
cap hint first, so the warning count read 0. Draw at 140 columns,
where the hint survives with the notice present; verified locally.
- The pointer queue proof retries the [↑] click once, re-finding the
affordance first: under runner load a redraw can shift cells between
the find and the click, so the first SGR gesture lands nowhere.
* fix(gates): tool-catalog budget covers the fleet rename; readme stamps; pointer baseline
- The Pod->Fleet sweep grew every mode's tool-schema surface by 58 bytes
(+14 tokens). The receipts are re-measured and the one-way ceilings in
scripts/runtime-contract-budget.json are raised to them as the
explicit maintainer decision the gate asks for (the rename is the
founder's 2026-09-01 vocabulary call).
- The README screenshot embed changed README.md; the 18 translated
READMEs re-stamp (the embed is language-neutral HTML - no prose
changed, so no retranslation was needed).
- The pointer queue baseline is captured while the composer is empty:
the pending preview row hides while a draft sits in the composer, so
the pre-click depth read None and the growth proof could not fire.
* test: re-click then keep polling until the deadline
The qa_harness Instant wrapper does not implement Div, and the retry's
single read raced the app processing the second gesture: poll to the
full deadline, re-click once at the half-way point.
* test: pointer queue diagnostics (baseline/expected/last-seen) in the failure output
* test: pointer poll keeps per-iteration state only (unused-assignment gate)
* fix: Copilot review findings — planner wire spelling and Advisor copy
- workflow::AgentType::Plan serializes as the canonical 'planner'
('plan'/'awaiter' stay accepted aliases), matching the FleetRole
vocabulary the mega PR declares.
- Web: the vocabulary docs metadata, the vocabulary module header, and
the docs-map topic description say Advisor (the public advisory term)
instead of the retired Consultant spelling.
- Polish home dictionary: restore 'Podwodna powłoka terminala' — the
fleet-vocabulary sweep had merged 'Fleet' into the compound word
'Podwodna' (underwater), producing the non-word 'fleetwodna'.
* test: pointer proof accepts preview-appears when no baseline count is painted
* test: the tolerant preview-appears proof (the arm the last commit missed)
---------
Signed-off-by: CodeWhale Bot <bot@codewhale.net>
Signed-off-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: CodeWhale Bot <bot@codewhale.net>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
…umber hang, mobile smoke, release receipts) - Windows warnings (§5.1): restructure link and error bindings in document.rs and runtime_api/tests.rs so variables exist only on targets that use them. Note: Windows CI cannot be run locally on this host. - macOS PTY acceptance (§5.2): in plugin_e2e_acceptance, iterate over all marker match indices in review_confirmation_in_text rather than only the first, preventing false stalls on the composer's typed command echo. Full 31/31 cucumber tests passed. - Mobile smoke (§5.3): update scripts/mobile-smoke.sh Test Group 3 to assert loopback security warning, matching the loopback-only hardening from e80be8c. 9/9 passed. - Release-note receipts (§5.4): cite genuine issues (codewhale-hq#5533, codewhale-hq#5831, codewhale-hq#5825, codewhale-hq#5829, codewhale-hq#5801, codewhale-hq#5815, codewhale-hq#5826, codewhale-hq#5286, codewhale-hq#5809, codewhale-hq#5778, codewhale-hq#5725, codewhale-hq#5701, codewhale-hq#5712, codewhale-hq#5747, codewhale-hq#5782, codewhale-hq#4394) in CHANGELOG.md; filter commit-body scrape noise (#000, codewhale-hq#1, codewhale-hq#24, codewhale-hq#26, codewhale-hq#1834, #142352) in check-feature-release-notes.sh. check-versions passes in strict release mode (71 receipts checked). Evidence: - cargo nextest run --workspace --all-features --locked: 14229 passed; 0 failed; 15 skipped - cargo clippy --workspace --all-targets --all-features --locked -- -D warnings: clean (0 warnings) - cargo test --workspace --all-features --doc --locked: 3 passed; 0 failed; 8 ignored - cargo test -p codewhale-tui --test cucumber --features long-running-tests --locked: 31 passed; 0 failed; 0 ignored - scripts/mobile-smoke.sh: 9 passed; 0 failed - scripts/release/check-versions.sh --require-dated-release: Feature release-note receipts OK (71 linked issues); Version state OK - Python policy gates: check-provider-registry, check-command-crate-boundaries, check-dead-code-budget, check-runtime-contract-budget, check-readme-translations, check-tui-locale-parity all PASS


No-Issue: internal 0.9.12 shell wave slice.
What changed for the user
Your fleet is the models you added, and it comes first.
/model,⇧Fon any row adds that exact route (provider + model id) to your fleet;⇧Fagain removes it. Fleet models are listed first, labelledfleet · <roles>, ahead of your⇧Ppins and the provider lists./pod modelsprints the fleet with facts from the catalog (price per M, context, tools)./pod add <provider> <model> [role…]and/pod remove <provider> <model>do the same from the composer. A model the configured provider does not serve is rejected with a one-line reason. With no Pod selected, the first add creates and selects a user-global Pod namedMy fleet./modelsprints your fleet before the provider's full list. With nothing added it says so: "Your fleet is the session model only".Why
The founder's direction (design
MODEL-ROUTING-CATALOG-20260901.md§10, slice F1): you build your own inventory of the models you want, from the providers you configured, and the list should lead with what you chose, not with a provider's alphabet. The operator model will pick sub-agent routes from this list (slice F2).How it reuses what exists (no second store)
A fleet model is a Pod member: the selected Pod file's operator route plus every member row that pins an exact
provider+model. Roles are the member rows that pin the route./pod addwrites member rows throughfleet/store.rs; the picker rides the existing pin machinery for ordering and labels.crate::fleet::membersis the one read/write seam (fleet_models,add_fleet_model,remove_fleet_model,toggle_fleet_model) for the F2 slice to stack on.Deviations from the brief, with reasons:
[[fleet.members]]inconfig.toml: the Pod file already stores exactly this (members with exact pins and roles), so a config-table copy would be a second system.⇧F, nota: the picker's rule (same as⇧P) is that an unshifted letter is a search character, never an action./modelsis a transcript message, not a view, so it lists the fleet first but has no key; the key lives in/model.Evidence
Clippy with CI flags: see the commit body.
Refs: design §10 F1 (codewhale-ops). Docs:
docs/FLEET.md"Your fleet as models".Canonical command name: this PR keeps
/podcanonical with/fleetas the alias (verbs work through both). Making/fleetcanonical is a one-line follow-up tied to the PRD wording (§4.3 "Pod is the public product term" vs the founder's "keep it as fleets").Note
Medium Risk
Changes Pod persistence and model-picker routing via new slash commands and keyboard actions, but logic is guarded by provider/catalog validation and covered by unit tests rather than touching auth or payments.
Overview
Introduces fleet-as-models (design §10 F1): the selected Pod’s operator route and pinned member rows are exposed as exact
provider/modelroutes with roles, without a separate store.Composer:
/pod modelslists the fleet with catalog facts;/pod addand/pod remove(plusrm/drop) mutate Pod member rows, with checks for configured providers and catalog-served models, auto-creating user-global My fleet when needed. Command usage strings and help are updated accordingly.UI: In
/model, ⇧F toggles fleet membership (same persistence as/pod add/remove); fleet entries sort ahead of personal pins withfleet · <roles>labels, and wide pickers show a Pod footer hint./modelsand model-fetch output now lead with the fleet section before the provider catalog.Plumbing: New
crate::fleet::membersseam (fleet_models, add/remove/toggle, receipts);Config::fleet_membersaccessor;Appkeeps aconfigsnapshot for/pod addvalidation;slugifyis shared from fleet store. Locales addPickerActionFleet;docs/FLEET.mddocuments the behavior.Reviewed by Cursor Bugbot for commit b364c63. Bugbot is set up for automated code reviews on this repo. Configure here.