Initial public release of Warp.

Repo-Sync-Origin: warpdotdev/warp-internal@12af1d983b
This commit is contained in:
David Stern
2026-04-28 08:43:33 -05:00
commit 0dbd3d567a
4982 changed files with 1431549 additions and 0 deletions
+39
View File
@@ -0,0 +1,39 @@
# PRODUCT.md
## Summary
Add a Harness filter dropdown to `AgentManagementView` so users can narrow the Runs list to runs driven by a specific execution harness (Warp Agent, Claude Code, Gemini CLI), and surface the harness on each run card's metadata line.
## Figma
Figma: none provided. Visual treatment matches the existing filter dropdowns in `app/src/ai/agent_management/view.rs` (Status, Source, Has artifact, etc.) and the harness icon + brand-color treatment established by the conversation details sidebar in `specs/REMOTE-1455/PRODUCT.md`.
## Behavior
1. A Harness dropdown is present in the filters row of the Runs header on `AgentManagementView`, alongside the existing Status, Source, Created on, Has artifact, Environment, and Created by dropdowns. It is visible under the same conditions as the existing dropdowns (no extra feature-flag gating beyond whatever already gates the view).
2. The dropdown label prefix is `Harness`, so the button reads `Harness: <selected>` after a selection, matching the `Status: …` / `Source: …` treatment from the adjacent dropdowns.
3. The dropdown exposes exactly these options, in this order:
1. `All` — no harness filtering.
2. `Warp Agent` — the default/Oz harness.
3. `Claude Code` — the Claude harness.
4. `Gemini CLI` — the Gemini harness.
Display names match `app/src/ai/harness_display.rs::display_name`. If a new harness is added to `warp_cli::agent::Harness` later, it is expected to appear here too, but adding it is out of scope for this feature.
4. Each non-`All` option renders its harness's leading logo icon, tinted with that harness's brand color, matching the treatment used in the conversation details sidebar (REMOTE-1455): Warp/Oz uses the first-party Warp icon + theme foreground, Claude uses `Icon::ClaudeLogo` tinted Claude orange, Gemini uses `Icon::GeminiLogo` tinted Gemini blue. The `All` option has no leading icon, consistent with the existing Status dropdown's `All` row.
5. The selected harness appears in the dropdown's collapsed button label, e.g. `Harness: Claude Code`. The default selection is `All`.
6. Selecting a harness option filters the visible Runs list to items whose resolved harness equals the selected value:
* A cloud task whose `agent_config_snapshot.harness.harness_type` is set matches the option for that harness.
* A cloud task whose `agent_config_snapshot` is present but has no `harness` field is treated as `Warp Agent` and matches only the `Warp Agent` option.
* A cloud task whose `agent_config_snapshot` has not loaded yet (e.g. a stub row) has an unknown harness; it matches only the `All` option and is excluded from every specific-harness filter, including `Warp Agent`. This mirrors the conversation details sidebar behavior, which omits the harness row when the harness is unknown.
* A local/interactive conversation (no ambient task) is treated as `Warp Agent` and matches only the `Warp Agent` option.
* No item ever matches more than one specific-harness option.
7. Selecting `All` clears the harness constraint and restores the list to what it would be with no harness filter applied (all other filters still respected).
8. The Harness filter is independent of every other filter. It combines with the existing Owner, Status, Source, Created on, Has artifact, Environment, Creator, and search filters via logical AND — an item appears only if it matches all active filters.
9. When the Harness filter is set to anything other than `All`, `AgentManagementFilters::is_filtering()` reports `true`, so the existing `Clear all` chip appears and, when clicked, resets Harness to `All` along with the other non-owner filters.
10. `Clear filters` from the empty-results view (`render_no_results_view`) also resets Harness to `All`.
11. The selected Harness value persists across app restarts via `PersistedAgentManagementFilters`, matching how Status, Source, Created on, Artifact, and Environment already persist. On first launch after upgrade (no persisted value), the filter defaults to `All`.
12. Changing the Harness filter triggers the same server refresh path that other filter changes do — i.e. the view calls its common "filter changed" handler so the server list fetch is retried with the new filter set and the local list re-renders. If the server does not yet support filtering by harness, the visible list must still honor the selected harness by filtering client-side over tasks already loaded in the model; users must not see runs from harnesses other than the one selected.
13. The Harness filter never changes which tasks are loaded into the underlying model for other views (details panel, transcript panel, deep-link navigation). It only affects which of those already-loaded items are displayed in the Runs list.
14. If the Runs list ends up empty solely because the Harness filter excluded every loaded item, the existing no-results state (`No results matched your filters` + `Clear filters` button) is shown, identical to what other filters already produce.
15. The Owner toggle (`Personal` vs `All`) does not affect Harness semantics: Harness filters the same way regardless of whether the user is viewing personal-only or team-wide runs.
16. The deep-link flow that scopes the view to a specific environment (`apply_environment_filter_from_link`) resets Harness to `All` along with the other non-owner filters, so a deep link never leaves a stale harness constraint in place.
17. Telemetry: changing the Harness dropdown emits the existing `FilterChanged` telemetry event with a new `FilterType::Harness` variant, consistent with how Status, Source, CreatedOn, Owner, and Creator changes are already tracked.
18. Keyboard and focus behavior for the Harness dropdown matches the other filter dropdowns in the same row — it is reachable via Tab navigation from adjacent dropdowns and obeys the same open/close keybindings the existing filter dropdowns already honor. No new global keybinding is introduced for Harness.
19. Every run card in the list renders a `Harness: <display name>` segment on its metadata line, immediately after the `Source: …` segment and before `Run time: …` / `Credits used: …`, using the existing ` • ` separator between segments. Example: `Source: Oz Web • Harness: Claude Code • Run time: 2 minutes`.
20. The `<display name>` on each card uses the same resolution as the Harness filter (invariant 6): explicit, known `harness_type` → that harness's display name; snapshot-present-but-no-harness → `Warp Agent`; local/interactive conversations → `Warp Agent`. Display names match `app/src/ai/harness_display.rs::display_name` (`Warp Agent`, `Claude Code`, `Gemini CLI`). When a card's harness is unknown (per invariant 6, third bullet), the Harness segment is omitted entirely — the card does not guess `Warp Agent`.
21. The Harness segment is included on every card whose harness is known (per invariant 20). It is not gated on any filter, the Source segment's presence, or card hover state. If the Source segment is omitted (e.g. a local conversation with no `AgentSource`), a present Harness segment becomes the row's leading segment.
22. The Harness segment on cards is text-only — no leading icon, no brand color, no click target, no tooltip. It uses the same text color and font as the rest of the metadata row.
23. Selecting a specific harness in the Harness filter hides cards whose resolved harness does not match (including cards with an unknown harness); cards that remain visible continue to show their own Harness segment, which will match the selected filter value.
+110
View File
@@ -0,0 +1,110 @@
# TECH.md
Companion to `specs/REMOTE-1457/PRODUCT.md`.
## Context
Add a Harness dropdown to `AgentManagementView`. Behavior lives in `PRODUCT.md`; this plan covers the filter plumbing, where the dropdown plugs in, and how harness resolution reuses the helpers introduced by REMOTE-1455.
Relevant code:
- `app/src/ai/agent_conversations_model.rs (60-150)` — existing `StatusFilter` / `SourceFilter` / `ArtifactFilter` / `EnvironmentFilter` enums and the `AgentManagementFilters` aggregate (incl. `reset_all_but_owner` / `is_filtering`). New `HarnessFilter` lands here.
- `app/src/ai/agent_conversations_model.rs (332-728)``ConversationOrTask` impl (`source`, `environment_id`, `matches_status`, `matches_artifact`, `matches_owner_and_creator`).
- `app/src/ai/agent_conversations_model.rs (1379-1443)``get_tasks_and_conversations`: where the per-item `*_filter` closures are chained. The harness filter plugs in here.
- `app/src/ai/agent_conversations_model.rs (1585-1647)``build_task_list_filter`: converts `AgentManagementFilters` into the server-side `TaskListFilter`. Harness is *not* added to `TaskListFilter` in this change — client-side filtering only (see "Scope" below).
- `app/src/ai/agent_management/view.rs (154-198)``AgentManagementView` struct: new `harness_dropdown` field goes here.
- `app/src/ai/agent_management/view.rs (265-450)` — dropdown construction + `sync_with_loaded_filters` + `update_filter_buttons`.
- `app/src/ai/agent_management/view.rs (468-702)` — existing dropdown builders (`create_status_dropdown`, `create_artifact_dropdown`, `setup_filter_menu`): the new `create_harness_dropdown` mirrors these.
- `app/src/ai/agent_management/view.rs (847-881)``apply_environment_filter_from_link`: must reset the new harness dropdown too (invariant 16).
- `app/src/ai/agent_management/view.rs (1876-1892)``filters_wrap` in `render_task_list_header`: the new dropdown gets added to this wrap row.
- `app/src/ai/agent_management/view.rs (2142-2247)``AgentManagementViewAction` enum + `handle_action` (incl. `ClearFilters`): new `SetHarnessFilter` variant and handler.
- `app/src/ai/agent_management/view.rs (1741-1770)``render_metadata_row`: builds `metadata_parts: Vec<String>` joined by ` • ` (Source, Run time, Credits used). The card Harness segment is inserted here.
- `app/src/ai/agent_management/telemetry.rs (43-52)``FilterType` enum: add `Harness`.
- `app/src/ai/harness_display.rs``display_name`, `icon_for`, `brand_color` already exist (from REMOTE-1455). Reused unchanged.
- `app/src/ai/ambient_agents/task.rs (57-83)``AgentConfigSnapshot.harness: Option<HarnessConfig>`. Authoritative source for a task's harness.
- `app/src/app_state.rs (37-59)``PersistedAgentManagementFilters` wraps `AgentManagementFilters` via serde. Adding a new field with `#[serde(default)]` keeps old persisted state compatible.
## Proposed changes
### 1. `HarnessFilter` enum + plumbing in `AgentManagementFilters`
In `agent_conversations_model.rs`, add:
```rust path=null start=null
#[derive(Copy, Clone, PartialEq, Eq, Debug, Default, Serialize, Deserialize)]
pub enum HarnessFilter {
#[default]
All,
Specific(Harness),
}
```
`Harness` is `Copy` and already `Serialize + Deserialize` via `warp_cli::agent::Harness` (used elsewhere). `HarnessFilter` stays `Copy` so it threads through actions cheaply.
Extend `AgentManagementFilters`:
```rust path=null start=null
pub struct AgentManagementFilters {
// ...existing fields...
#[serde(default)]
pub harness: HarnessFilter,
}
```
`#[serde(default)]` keeps `PersistedAgentManagementFilters` backwards compatible with existing on-disk state (invariant 11). Update `reset_all_but_owner` to zero `harness` and `is_filtering` to include `self.harness != HarnessFilter::default()` (invariants 9, 10).
### 2. Resolve harness on `ConversationOrTask`
Add on `ConversationOrTask`:
```rust path=null start=null
pub fn harness(&self) -> Option<Harness> {
match self {
ConversationOrTask::Task(task) => task.agent_config_snapshot.as_ref().and_then(|c| {
c.harness
.as_ref()
.map(|h| h.harness_type)
.or(Some(Harness::Oz))
}),
// Local/interactive conversations always run on Warp Agent.
ConversationOrTask::Conversation(_) => Some(Harness::Oz),
}
}
fn matches_harness(&self, f: &HarnessFilter) -> bool {
match f {
HarnessFilter::All => true,
HarnessFilter::Specific(h) => self.harness() == Some(*h),
}
}
```
Resolution mirrors `ConversationDetailsData::from_task` from REMOTE-1455 exactly, so the filter and the details panel agree on which harness label belongs to a row:
- snapshot present + `harness` set → `Some(harness_type)`,
- snapshot present + no `harness` field → `Some(Harness::Oz)` (runtime default for an Oz-managed task),
- snapshot not loaded yet (stub) → `None` ("don't know yet"),
- local/interactive conversation → `Some(Harness::Oz)`.
`HarnessConfig.harness_type` is already a parsed `warp_cli::agent::Harness`, so the resolver no longer needs to re-parse a raw string — unknown values are already collapsed to `Harness::Oz` by the snapshot deserializer (`harness_from_name` in `ambient_agents/task.rs`). PRODUCT invariant 6 bullet 3 follows directly from `harness() == None` not matching any `HarnessFilter::Specific(_)`.
`HarnessFilter`'s `Deserialize` impl uses clap's `Harness::from_str` to coerce persisted `"oz" | "claude" | "gemini" | "all" | <unknown>` strings, falling back to `HarnessFilter::All` for unknown values; `harness()` itself doesn't parse strings.
Wire `matches_harness` into `get_tasks_and_conversations` as another closure in the `.filter(...)` chain, alongside the existing `source_filter` / `status_filter` / `environment_filter` (invariant 8).
### 3. View: dropdown construction and wiring
In `AgentManagementView`:
- New field `harness_dropdown: ViewHandle<Dropdown<AgentManagementViewAction>>`.
- New builder `create_harness_dropdown(ctx)` modeled on `create_status_dropdown`:
- `Self::setup_filter_menu(&mut dropdown, "Harness", ctx)` for the `Harness: <selected>` button label (invariant 2).
- Items: `All`, then one per `Harness` variant in the order listed in PRODUCT invariant 3 (`Oz`, `Claude`, `Gemini`). Each non-`All` item uses `MenuItemFields::new(display_name(h)).with_icon(icon_for(h))` and, when `brand_color(h)` is `Some(c)`, `.with_override_icon_color(Fill::from(c))`. Warp Agent renders with the default theme-foreground tint (invariant 4) because `brand_color(Harness::Oz)` is `None`. Relies on `harness_display::display_name` / `icon_for` / `brand_color` and the `MenuItemFields` icon helpers from `app/src/menu.rs`.
- `sync_with_loaded_filters` adds a block that calls `harness_dropdown.set_selected_by_action(SetHarnessFilter(self.filters.harness), ctx)`, mirroring the existing status/source/created_on/artifact blocks.
- `ClearFilters` handler resets the harness dropdown to its zeroth item (invariant 9).
- `apply_environment_filter_from_link` resets the harness dropdown to its zeroth item (invariant 16); `reset_all_but_owner` already clears `filters.harness` via (1).
- `render_task_list_header`: append `ChildView::new(&self.harness_dropdown)` to the `filters_wrap` (invariant 1). Exact placement: after `artifact_dropdown`, before `environment_dropdown`, to keep related filters adjacent. Visible under the same conditions as the other dropdowns, no feature-flag gating.
### 4. Action, telemetry, and refresh
- Add `AgentManagementViewAction::SetHarnessFilter(HarnessFilter)`.
- Add `FilterType::Harness` to `app/src/ai/agent_management/telemetry.rs` (invariant 17). `#[serde(rename_all = "snake_case")]` is already set on that enum, so the payload string is `"harness"`; `warp_core::telemetry::enum_events` picks up the new variant automatically via the existing `EnumDiscriminants` / `EnumIter` derives.
- `handle_action` branch mirrors the status branch: send `FilterChanged { filter_type: FilterType::Harness }`, update `self.filters.harness`, call `self.on_filter_changed(ctx)` (invariant 12).
### 5. Card metadata row
Extend `render_metadata_row` in `view.rs` with an `if let Some(h) = card_data.harness()` block that pushes `format!("Harness: {}", harness_display::display_name(h))` into `metadata_parts` immediately after the Source push, where `h` is `card_data.harness()` from change (2). The `if let` mirrors the details panel: when `harness()` returns `None` (a server-side stub task whose snapshot hasn't been fetched yet), the Harness segment is omitted instead of guessing — matching invariants 2021. Once the snapshot loads in a follow-up update, the segment appears on the next render. Use the unconditional `display_name` mapping for the three known variants; no icon, no color override, no click handler (invariant 22).
Ordering: pushed between the Source push and the Run time push so the resulting ``-joined string matches `Source: … • Harness: … • Run time: … • Credits used: …`. If Source is absent, a present Harness segment becomes the leading segment with no leading separator because `metadata_parts.join(" • ")` skips empty positions by construction (invariant 21).
No new view state, mouse state, or subscription is introduced for the card segment — it is pure data derived from `ConversationOrTask::harness()` at render time.
### 6. Scope: client-side only
This change deliberately does *not* add `harness` to `TaskListFilter` / `build_task_list_filter` / the server query string (`app/src/server/server_api/ai.rs (492-650)`). Invariant 12 is satisfied because `on_filter_changed` already re-issues `trigger_filter_fetch` with the remaining filters and calls `get_tasks_from_model`, and `matches_harness` enforces the harness constraint on the client over everything in the model — the server simply returns a possibly-larger superset which the client narrows. If we later want server support, the extension is a new `Option<Harness>` field on `TaskListFilter` + a `harness=` query param; listed under Follow-ups.
## Testing and validation
Behavior invariants from `PRODUCT.md` map as follows:
- Invariants 6, 8, 15 — unit tests in `agent_conversations_model` tests exercising `get_tasks_and_conversations` with fixtures that cover: (a) cloud task with `harness_type = Harness::Claude` → matches `Claude` only, (b) cloud task with `agent_config_snapshot = None` → matches **only** `All` (`harness() == None`), (c) cloud task with `agent_config_snapshot = Some { harness: None }` → matches `Warp Agent` only, (d) local conversation → matches `Warp Agent` only, (e) combinations of harness + status + owner to lock AND semantics and independence from the `Personal`/`All` toggle. Unknown `harness_type` strings are not separately tested at this layer because `HarnessConfig`'s deserializer collapses them to `Harness::Oz` before they reach `ConversationOrTask::harness()`; that mapping is exercised by the `task.rs` deserializer tests.
- Invariants 9, 10, 16 — unit tests in the same module asserting `AgentManagementFilters::is_filtering()` returns `true` when only `harness` is set, and `reset_all_but_owner()` zeroes `harness` back to `HarnessFilter::All`. Dropdown re-selection on `ClearFilters` / `apply_environment_filter_from_link` is review-only.
- Invariant 11 — a serde round-trip test on `AgentManagementFilters`: deserializing a JSON object without a `harness` key yields `HarnessFilter::All` (backwards compat with existing `PersistedAgentManagementFilters`), and serialize+deserialize preserves a `Specific(Claude)` value.
- Invariants 2, 3, 4, 5 — structural: the dropdown is built from the same ordered array of `Harness` variants and uses `harness_display::display_name` / `icon_for` / `brand_color`, which are themselves tested in `harness_display_tests.rs` from REMOTE-1455. A smoke snapshot of the dropdown items (labels only) guards against accidental reordering.
- Invariant 17 — assertion that `FilterType::Harness` serializes as `"harness"` (snake_case rename) and that the existing registered telemetry enumeration picks up the new variant via `AgentManagementTelemetryEventDiscriminants::iter()`.
- Invariants 1, 7, 13, 14, 18 — covered by manual verification, not dedicated tests: structurally, `get_tasks_from_model` is the only consumer that reads `self.filters.harness`, and the dropdown is placed inside the same `filters_wrap` as its siblings so it inherits their wrapping/focus/keybinding behavior.
- Invariants 19, 20, 21, 22 — review-only: `render_metadata_row` pushes the `Harness: <display_name>` segment from `ConversationOrTask::harness()`, whose resolution is unit-tested. Invariant 23 (per-card harness string aligns with filter selection) follows from both the card and the filter using the same `harness()` resolver, already exercised by the `matches_harness` tests.
- Manual verification — `cargo run`, open the management view, and in order: (a) open Harness dropdown → exactly four options in the documented order with brand-tinted icons on the three non-`All` rows; (b) pick `Claude Code` → list narrows; (c) toggle Owner `Personal` ↔ `All` → harness constraint still applies; (d) pick `Warp Agent` → see local conversations and cloud tasks with no `harness_type` set; (e) click `Clear all` → harness returns to `All`; (f) set to `Gemini CLI`, restart Warp → selection is restored; (g) follow a deep-link that invokes `apply_environment_filter_from_link` → harness resets to `All`.
- Pre-PR — `./script/presubmit` per repo rules. No WASM-specific paths; `warp_cli::agent::Harness` already compiles for WASM.
## Risks and mitigations
- **Server returns tasks whose harness does not match the selected filter.** Expected under invariant 12 when the server does not yet filter by harness; the client drops them in `get_tasks_and_conversations`. Result is a potentially smaller rendered page than the server intended, which is cosmetic. Mitigation: add server-side `harness` to `TaskListFilter` when available (see Follow-ups).
- **Persisted filters from older clients carry unknown fields.** Guarded by `#[serde(default)]` on `harness` and serde's default laxness on unknown fields already assumed elsewhere on this struct.
- **New `Harness` variants added in `warp_cli` would render only in three-item order.** `create_harness_dropdown` iterates the variants explicitly to control order and icon/color mapping, so adding a variant requires touching the dropdown builder. This matches PRODUCT invariant 3's "adding it is out of scope" stance — mitigation is a future PR that extends the explicit list.
## Follow-ups
- Add `harness: Option<Harness>` to `TaskListFilter` + `&harness=` query param in `build_list_agent_runs_url` once the public API supports it, and wire through `build_task_list_filter`. Reduces client-side filtering overhead but is not required for any invariant.
- If a new `Harness` variant ships, extend `create_harness_dropdown`'s ordered variant list in one place.