first pass of merging in warp (doesn't build)

This commit is contained in:
Ryan Ward
2026-07-01 16:08:58 -05:00
parent 2f64909469
commit 4770ac06b5
3662 changed files with 414574 additions and 89772 deletions
+189
View File
@@ -0,0 +1,189 @@
# Harness-Specific Model Selection — Tech Spec
Linear: [QUALITY-643](https://linear.app/warpdotdev/issue/QUALITY-643)
Companion product spec: `specs/QUALITY-643/PRODUCT.md`
## Context
The orchestration config UI (plan card and run_agents confirmation card) lets users pick a harness and model for child agents. Both cards share picker logic in `orchestration_controls.rs`. Two problems exist today:
1. **Harness picker** is hardcoded to `[Oz, Claude, Codex]` — it doesn't read from the server's `availableHarnesses` list, doesn't include Gemini, and doesn't respect admin enabled/disabled state.
2. **Model picker** always shows Warp's internal LLM catalog filtered by provider (Anthropic for Claude, OpenAI for Codex). Those IDs (e.g. `claude-4-6-opus-high`) are not recognized by third-party harness CLIs. The server maintains separate harness-specific model catalogs that the desktop client already fetches and caches in `HarnessAvailabilityModel`, but the orchestration UI doesn't use them.
Additionally, model_id is not delivered to local child harness processes: Claude Code doesn't receive `ANTHROPIC_MODEL`. Codex model delivery uses `~/.codex/config.toml`, which is only safe in cloud/remote environments where the filesystem is isolated — local children must not touch it (see `local_harness_launch.rs:143` comment).
### Relevant files
**Shared picker logic (model/harness dropdowns)**
- `app/src/ai/blocklist/inline_action/orchestration_controls.rs``populate_harness_picker()` (line 380), `populate_model_picker_for_harness()` (line 314), `is_model_in_filtered_choices()` (line 353), `first_filtered_model_id()` (line 368), `sync_picker_selections()` (line 508), `matches_harness_filter()` (line 299)
**UI card views that consume the shared pickers**
- `app/src/ai/document/orchestration_config_block.rs` — plan card; subscribes to `LLMPreferencesEvent` for model refresh
- `app/src/ai/blocklist/inline_action/run_agents_card_view.rs` — confirmation card; `HarnessChanged` handler (line 752) resets model on harness change
**Harness availability and model data (already fetched and cached)**
- `app/src/ai/harness_availability.rs``HarnessAvailabilityModel` singleton with:
- `available_harnesses()``&[HarnessAvailability]` (harness, display_name, enabled, available_models)
- `models_for(harness)``Option<&[HarnessModelInfo]>` (id, display_name)
- `is_harness_enabled(harness)``bool`
- Emits `HarnessAvailabilityEvent::Changed`
**Harness display metadata**
- `app/src/ai/harness_display.rs``display_name()`, `icon_for()`, `brand_color()` per `Harness` variant
**Model ID delivery to harness processes**
- `app/src/ai/agent_sdk/driver/harness/mod.rs``harness_model_env_vars()` (line 373): sets `ANTHROPIC_MODEL` for Claude, no-op for Codex
- `app/src/pane_group/pane/local_harness_launch.rs``prepare_local_harness_child_launch()` (line 77): builds child env_vars but does not include model_id
- `app/src/pane_group/pane/terminal_pane.rs``launch_local_harness_child()` (line 1302): passes `model_id` to `apply_child_model_id_override` which only sets Oz LLM preference
- `app/src/ai/agent_sdk/driver/harness/codex.rs``prepare_codex_config_toml()` (line 573): writes `~/.codex/config.toml` but does not write a `model` key; top-level key name is `"model"` (confirmed by test at `codex_tests.rs:201`)
## Proposed changes
### 1. Populate harness picker from `HarnessAvailabilityModel`
**File**: `orchestration_controls.rs``populate_harness_picker()`
Replace the hardcoded `[Harness::Oz, Harness::Claude, Harness::Codex]` iteration (line 392) with a read from `HarnessAvailabilityModel::as_ref(ctx).available_harnesses()`.
For each `HarnessAvailability` entry:
- Use `harness_display::icon_for()` and `harness_display::brand_color()` for icons (these already cover all variants including Gemini).
- Use `harness_display::display_name()` for the label (the server's `display_name` field could also be used, but the client-side names already match and are guaranteed non-empty before the server responds).
- If `!entry.enabled`: render as disabled (non-selectable, greyed text). The `MenuItemFields` API supports `.with_disabled(true)` or equivalent — check the existing `MenuItem` disabled patterns in the codebase.
- Sort enabled entries before disabled entries.
Also subscribe both card views to `HarnessAvailabilityEvent::Changed` to repopulate the harness picker when the server list updates.
Addresses PRODUCT.md behaviors 15.
### 2. Switch model picker to harness-specific models with "Default model" entry
**File**: `orchestration_controls.rs``populate_model_picker_for_harness()`
Add an `is_local: bool` parameter (or pass the current `RunAgentsExecutionMode`) so the picker can be execution-mode-aware. Replace the current provider-filtered `LLMPreferences` logic with harness-aware branching:
```
let harness = Harness::parse_orchestration_harness(harness_type);
match harness {
Some(Harness::Oz) | None => {
// Current behavior: LLMPreferences filtered by provider
}
Some(Harness::Codex) if is_local => {
// Local Codex: only "Default model" entry (no model delivery possible)
}
Some(harness) => {
// 1. Always add a "Default model" entry first (value: empty string)
// 2. Read HarnessAvailabilityModel::as_ref(ctx).models_for(harness)
// 3. If Some(models): append each HarnessModelInfo as a menu item
// with display_name as label and id as the model_changed action value
// 4. If None: only "Default model" is shown (loading/empty state)
}
}
```
The "Default model" entry should use label `"Default model"` and emit `A::model_changed(String::new())` (empty string). This matches the web UI which adds this entry with value `""`.
When execution mode toggles between Local and Cloud, the `HarnessChanged` / `ExecutionModeToggled` handlers must repopulate the model picker since Codex's available models depend on the mode.
Apply the same Oz-vs-non-Oz branching to:
- `is_model_in_filtered_choices()` — for non-Oz, check model_id against `HarnessAvailabilityModel::models_for()` OR accept empty string (the "Default model" entry). For local Codex, only empty string is valid.
- `first_filtered_model_id()` — for non-Oz, return `Some(String::new())` (the "Default model" entry) as the default.
- `sync_picker_selections()` — for non-Oz, find display_name from `HarnessAvailabilityModel::models_for()` instead of `LLMPreferences`. Map empty model_id to the "Default model" label.
Addresses behaviors 610, 1114, 1516.
### 3. Subscribe both card views to `HarnessAvailabilityEvent::Changed`
**Files**: `orchestration_config_block.rs`, `run_agents_card_view.rs`
Both views already subscribe to `LLMPreferencesEvent::UpdatedAvailableLLMs`. Add an analogous subscription to `HarnessAvailabilityModel`:
- Repopulate the **harness picker** when the harness list changes (behaviors 15).
- Repopulate the **model picker** when harness models arrive, but only when the current harness is non-Oz (behavior 15).
Addresses behaviors 15, 15.
### 4. Propagate model_id to local child harness processes (Claude Code only)
**File**: `local_harness_launch.rs`
Add `model_id: Option<String>` parameter to `prepare_local_harness_child_launch()`. After building `env_vars` from `task_env_vars()`:
- For Claude: merge `harness_model_env_vars(harness, model_id.as_deref())` into env_vars. This sets `ANTHROPIC_MODEL` when model_id is non-empty.
- For Codex: no model delivery for local children. The UI ensures model_id is empty for local Codex (behavior 8), so no action is needed here. The existing code already skips `prepare_codex_environment_config()` for local children (`local_harness_launch.rs:143`) and this spec preserves that constraint.
**File**: `terminal_pane.rs`
Update the call to `prepare_local_harness_child_launch()` in `launch_local_harness_child()` (line 1325) to pass the `model_id` value.
Addresses behaviors 17, 20.
### 5. Write Codex model to config.toml (cloud/remote path only)
**File**: `codex.rs`
Add `model_id: Option<&str>` parameter to `prepare_codex_environment_config()` and `prepare_codex_config_toml()`.
In `prepare_codex_config_toml()`, after `set_codex_openai_base_url()`:
- If `model_id` is `Some(id)` where `id` is non-empty and not `"default"`: `doc["model"] = toml_edit::value(id)`.
- Otherwise: `doc.remove("model")` to clear any pre-existing key.
Add constant `CODEX_MODEL_KEY: &str = "model"`.
The existing test at `codex_tests.rs:201` verifies a pre-existing `model` key is preserved — update it to verify the new write/remove behavior.
Update the caller of `prepare_codex_environment_config()` in `codex.rs` `build_runner()` to pass `model_id`. No changes needed in `local_harness_launch.rs` — local Codex children don't call this function and don't receive model overrides.
Addresses behavior 18.
### 6. No changes needed for remote launch path
The remote launch path already passes `model_id` to the server via `StartAgentExecutionMode::Remote { model_id }` in `run_agents_to_start_agent_mode()`. With the UI now storing harness-native IDs (or empty for "Default model"), the server receives the correct value without translation.
Addresses behaviors 17 (remote), 18 (remote).
## Testing and validation
### Unit tests
**orchestration_controls tests** — new tests:
- Harness picker populated from `HarnessAvailabilityModel`; disabled harnesses shown but not selectable. (Behaviors 14)
- `populate_model_picker_for_harness` with harness="claude": "Default model" entry at top, then harness-specific models from `HarnessAvailabilityModel`. (Behavior 7)
- `populate_model_picker_for_harness` with harness="codex", cloud mode: "Default model" entry at top, then Codex models. (Behavior 8)
- `populate_model_picker_for_harness` with harness="codex", local mode: only "Default model" entry. (Behavior 8)
- `populate_model_picker_for_harness` with harness="oz": Warp LLM catalog (existing behavior). (Behavior 6)
- `is_model_in_filtered_choices` returns false for Warp IDs when harness is non-Oz, true for empty string ("Default model"). (Behavior 12)
- `first_filtered_model_id` returns empty string for non-Oz harness. (Behavior 11)
- Harness change from Claude (model="opus") to Oz: model resets to first Warp LLM. (Behavior 12)
**local_harness_launch tests** — new/updated tests:
- `prepare_local_harness_child_launch` merges `ANTHROPIC_MODEL` into env_vars when harness is Claude and model_id is provided. (Behavior 17)
- `prepare_local_harness_child_launch` does NOT set `ANTHROPIC_MODEL` when model_id is None or empty. (Behavior 20)
**codex config.toml tests** — update existing tests in `codex_tests.rs`:
- `prepare_codex_config_toml` writes `model = "gpt-5.4"` when model_id is `Some("gpt-5.4")`. (Behavior 18)
- `prepare_codex_config_toml` removes existing `model` key when model_id is `Some("default")`. (Behavior 18)
- `prepare_codex_config_toml` removes existing `model` key when model_id is `None`. (Behavior 20)
- Pre-existing non-model keys (openai_base_url, projects, mcp_servers) are preserved in all cases.
**orchestration_config_tests** — existing tests must continue to pass. (Behavior 21)
### Presubmit
Run `cargo fmt`, `cargo clippy`, and `./script/presubmit` before PR.
### Manual validation
- Open orchestration config on a plan card → harness picker shows Oz, Claude Code, Codex (and Gemini if server returns it, possibly disabled).
- Select Claude Code → model picker shows "Default model" at top, then `best`, `opus`, `sonnet`, etc.
- Select Codex (Cloud mode) → model picker shows "Default model" at top, then `default`, `GPT-5.5`, `GPT-5.4`, etc.
- Select Codex (Local mode) → model picker shows only "Default model".
- Toggle Local → Cloud with Codex selected → model picker repopulates with full Codex catalog.
- Select Oz → model picker returns to Warp LLM catalog.
- Change harness from Claude (with "opus" selected) to Oz → model resets.
- Launch local agents with Claude Code + "opus" → verify `ANTHROPIC_MODEL=opus` in child env.
- Launch local agents with Claude Code + "Default model" → verify no `ANTHROPIC_MODEL` in child env.
- Launch cloud agents with Codex + "gpt-5.4" → verify `model = "gpt-5.4"` in `~/.codex/config.toml` inside the cloud env.
- Launch cloud agents with Codex + "default" → verify no `model` key in `~/.codex/config.toml`.
## Parallelization
Not recommended. The changes are tightly coupled — the harness picker (change 1) and model picker (change 2) share state in `orchestration_controls.rs`, the card view subscriptions (change 3) depend on both pickers, and the launch-side changes (45) depend on the correct model_id format flowing from the picker. Total scope is ~7 files with moderate changes each, well suited for sequential execution by a single agent.