10 KiB
TECH.md
Companion to specs/REMOTE-1455/PRODUCT.md.
Context
Add a harness row to the conversation details panel. Behavior lives in PRODUCT.md; this plan covers where the row plugs in and how we source the harness value.
Relevant code:
app/src/ai/conversation_details_panel.rs (75-100)—PanelModeenum (Conversation/Task) that backs the panel.app/src/ai/conversation_details_panel.rs (177-198)—ConversationDetailsDatastruct. All new data the row needs must flow through here.app/src/ai/conversation_details_panel.rs (222-414)— constructors:from_conversation,from_task,from_task_id,from_conversation_metadata.app/src/ai/conversation_details_panel.rs (950-1028)—render_skill_section, the closest existing precedent for a single icon + label row.app/src/ai/conversation_details_panel.rs (1426-1781)—View::rendercomposes the sidebar; sections are appended to aFlex::columnin a fixed order withFIELD_SPACING/HEADER_SPACINGmargins. This is where the new row is inserted.app/src/ai/ambient_agents/task.rs (57-83)—AgentConfigSnapshot.harness: Option<HarnessConfig>, whereHarnessConfig { harness_type: String }— the source of truth for a task's harness once the snapshot is loaded.crates/warp_cli/src/agent.rs (118-138)—Harnessenum (Oz/Claude/Gemini) withclap::ValueEnum;Harness::from_str(viaValueEnum) is the canonical parser forharness_typestrings (values:oz,claude,gemini).app/src/terminal/view/ambient_agent/harness_selector.rs (59-75)— existingdisplay_name/icon_forhelpers used by the harness selector dropdown. We reuse and centralize these so the two surfaces cannot diverge.app/src/ai/conversation_details_panel_tests.rs— existing unit-test harness (App::test) used forConversationDetailsData.
Proposed changes
1. Centralize harness display metadata
Extract the display name + icon + brand color mapping out of harness_selector.rs into a small shared module (app/src/ai/harness_display.rs), exposing:
pub fn display_name(harness: Harness) -> &'static str→"Warp Agent","Claude Code","Gemini CLI".pub fn icon_for(harness: Harness) -> Icon→Warp/ClaudeLogo/GeminiLogo. Oz maps toIcon::Warp(notIcon::OzCloud) so first-party Warp surfaces visually match the existing skill row.pub fn brand_color(harness: Harness) -> Option<ColorU>→Nonefor Oz (caller falls back to theme foreground),CLAUDE_ORANGEfor Claude,GEMINI_BLUEfor Gemini.CLAUDE_ORANGEis re-used fromcrate::ai::blocklist;GEMINI_BLUEmirrors the value already interminal::cli_agent.pub fn parse_harness_type(raw: &str) -> Option<Harness>— thin wrapper aroundHarness::from_str(raw, /* ignore_case */ true). Unknown strings returnNone.impl From<AIAgentHarness> for Harness— 1:1 map from theServerAIConversationMetadata.harnessenum (Oz/ClaudeCode/Gemini) toHarness. Used by the conversation-sourced constructors so they can resolve the real harness instead of hardcoding Oz. Updateharness_selector.rsto call into this module. This renames the selector's"Oz"label to"Warp Agent"and swaps its leading icon fromIcon::OzCloudtoIcon::Warp, matchingPRODUCT.mdinvariant 2 and keeping the two surfaces in sync.
2. Data model: thread harness through ConversationDetailsData
Add harness: Option<Harness> to ConversationDetailsData. Resolution per constructor:
from_conversation(WASM) — readconversation.server_metadata().map(|m| Harness::from(m.harness)); fall back toSome(Harness::Oz)when the conversation has no server metadata (pure local run).from_conversation_metadata— takesharness: Option<Harness>as an explicit parameter. The management view caller resolves it viaBlocklistAIHistoryModel::get_server_conversation_metadata(&conversation_id).map(|m| Harness::from(m.harness)), falling back toSome(Harness::Oz)for pure local conversations. This covers the edge case where aManagementCardItemId::Conversationcard represents a cloud-task-backed conversation whose task row isn't inself.tasks(shadowing missed): the server-sideAIAgentHarnesson the merged metadata is the authoritative source.from_task— compute fromtask.agent_config_snapshot:Some(config)withconfig.harness = Some(HarnessConfig { harness_type })→parse_harness_type(&harness_type); unknown strings resolve toNone.Some(config)withconfig.harness = None→Some(Harness::Oz)(invariant 3: explicit default).None(snapshot not loaded) →None(invariant 5: omit until known).
from_task_id→None.HarnessisCopy, soOption<Harness>stays cheap and keepsConversationDetailsData: Clone.
3. Rendering: render_harness_section
Add fn render_harness_section(&self, appearance: &Appearance) -> Option<Box<dyn Element>> structured like render_simple_field (label-over-value) but with an icon in the value row:
- Returns
Nonewhenself.data.harness.is_none()— enforces invariant 6 (no placeholder, no reserved slot). - Emits a
Flex::columnwith two children separated byLABEL_VALUE_GAP:- A "Harness"
Textlabel, colored withblended_colors::text_sub(theme, theme.surface_1())atui_font_size(invariant 2). - A value
Flex::rowcontaining a 16pxConstrainedBox'dIcon(margin-right 4px) and a selectableTextwith the harness display name intheme.foreground()(invariants 2, 8).
- A "Harness"
- Icon tint =
brand_color(harness).map(Into::into).unwrap_or(theme.foreground()). Warp Agent picks up the theme foreground; Claude and Gemini render in their brand colors (invariants 2 and 3). - No click target, no copy button, no tooltip (invariant 7).
Insert in
View::renderdirectly below the Status section and above Artifacts / Directory / Run ID, wrapped inContainer::with_margin_bottom(FIELD_SPACING)so the field has the same outer spacing as siblingrender_simple_fieldfields (invariant 9). The placement is the same across bothPanelMode::ConversationandPanelMode::Task. Because the slot is conditional, when afrom_task_idstub later resolves into a full task, the panel only grows downward from that row — nothing above it moves (invariant 6's "no content moves out from under the cursor"). No new actions, mouse states, copy-feedback entries, or events.handle_actionandPanelMouseStatesare untouched.
4. No telemetry, no new surfaces
Row is read-only metadata. No telemetry, no changes to ConversationDetailsPanelAction, ConversationDetailsPanelEvent, or any caller of set_conversation_details.
Testing and validation
Behavior invariants from PRODUCT.md map as follows:
- Invariants 1, 4, 5, 6 — unit tests on
ConversationDetailsData.harness, added toapp/src/ai/conversation_details_panel_tests.rs:from_conversationwith noserver_metadata→Some(Harness::Oz); withserver_metadata.harness = AIAgentHarness::ClaudeCode→Some(Harness::Claude)(and analogous for Gemini).from_conversation_metadatais a pass-through: each ofSome(Oz)/Some(Claude)/Some(Gemini)/Noneround-trips into the data struct. (Resolution logic lives in the caller; that's exercised end-to-end via manual verification rather than a dedicated unit test, since it requires a populatedBlocklistAIHistoryModel.)from_task_id→None.from_taskwithagent_config_snapshot = None→None.from_taskwithagent_config_snapshot = Some { harness: None, .. }→Some(Harness::Oz).from_taskwithharness_typeeach of"oz","claude","gemini"→ matchingHarnessvariant.from_taskwith an unknownharness_typestring →None.- Parametrize one of the above across each
AmbientAgentTaskStatevariant to lock invariant 5 ("regardless of run status").
- Invariants 2, 3 (icon + label + brand color mapping) — unit tests on the new
harness_displaymodule coveringparse_harness_typeedge cases. Thedisplay_name/icon_for/brand_colormappings are enum match arms and don't get dedicated tests; instead the harness selector's existing item rows and the details row resolve from the same shared helper so they can't drift. - Invariants 7, 8, 9, 10 — covered structurally:
render_harness_sectionhas no click handler / mouse state, useswith_selectable(true), is inserted at a fixed offset inView::render, and the panel is the sole renderer across all hosting surfaces. Verified by a manual smoke check rather than a dedicated test. - Manual verification —
cargo runand, in order: (a) open a local conversation's details panel → row shows "Warp Agent" +Icon::Warpin theme foreground; (b) open a cloud task launched with default config → row shows "Warp Agent"; (c) open a cloud task launched with--harness claude→ row shows "Claude Code" +ClaudeLogotinted Claude orange; (d) open a shared task that still resolves viafrom_task_idbefore the task loads → confirm no placeholder row, then confirm the row appears once the task payload arrives without rows above visibly jumping. - Pre-PR —
./script/presubmit(cargo fmt, cargo clippy, cargo nextest) per repo rules. No WASM-specific paths are added, so WASM build should be unaffected.
Risks and mitigations
- Renaming "Oz" → "Warp Agent" in the existing harness selector. Separate user-visible change on another surface. Required for invariant 2; centralizing in
harness_display.rsprevents drift. If contested, we can parameterize per-surface without re-splitting the spec. - Unknown
harness_typestrings from newer server versions. Treating unknown strings asNonemeans future harnesses temporarily render no row on older clients — strictly better than a wrong label. Mitigation: add the newHarnessvariant inwarp_cliwhen the server does. from_task_id→from_tasktransition causing layout jump. Placing the row below Creator means growth is downward-only, so nothing above moves. If product later wants the row higher, we'd need a reserved slot or crossfade; defer until asked.