Files
galaxy/specs/REMOTE-1457/TECH.md
T

15 KiB
Raw Blame History

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.rsdisplay_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:

#[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:

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:

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 PersonalAll → 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.