first pass of merging in warp (doesn't build)
This commit is contained in:
@@ -0,0 +1,90 @@
|
||||
# Orchestration Pill Bar in Shared Session Web Viewer
|
||||
|
||||
## Summary
|
||||
When viewing a shared session that used orchestration, the session viewer (web or native) displays a pill bar showing the orchestrator and all child agents. Each pill shows the agent's name and lifecycle status. Clicking a pill switches the view to that agent's conversation in-place, giving viewers full visibility into multi-agent sessions. Gated behind `FeatureFlag::OrchestrationViewerPillBar`.
|
||||
|
||||
## Problem
|
||||
Today, when a user views an orchestrated session on the web, only the orchestrator's conversation is visible. The existence of child agents, their names, statuses, and conversation transcripts are invisible to the viewer. This makes it impossible to understand what a multi-agent session did or to inspect individual agent work.
|
||||
|
||||
## Figma
|
||||
Figma: none provided. Visual treatment matches the native client's orchestration pill bar. On web, pane-management actions are unavailable since the web viewer is single-pane.
|
||||
|
||||
## Behavior
|
||||
|
||||
### Pill bar visibility
|
||||
1. The pill bar renders when the viewed shared session contains an orchestrated conversation — one where the orchestrator spawned at least one child agent.
|
||||
2. The pill bar is positioned above the agent view content, below the pane header — matching the native client's placement.
|
||||
3. The pill bar is hidden when the session has no orchestration (single-agent session with no children).
|
||||
4. When the viewer joins a live session before any children have been spawned, the pill bar appears once the first child is detected. A brief delay is acceptable.
|
||||
5. When viewing a completed session, the pill bar renders immediately with all children and their final statuses.
|
||||
|
||||
### Pill rendering
|
||||
6. The leftmost pill represents the orchestrator. It displays the Oz icon and the hardcoded label "Orchestrator", matching the native client's behavior.
|
||||
7. Child agent pills appear to the right of the orchestrator, in spawn order.
|
||||
8. Each child pill displays:
|
||||
a. A colored avatar disc with the first letter of the agent name, uppercased.
|
||||
b. The agent's display name, truncated with ellipsis if it exceeds ~110px.
|
||||
c. A status badge overlaid at the bottom-right of the avatar disc.
|
||||
9. Avatar disc colors are deterministic: same agent name always produces the same color, drawn from the same fixed 6-color palette used in the native client. A viewer and the sharer see the same color for the same agent.
|
||||
10. The currently-selected pill (whose conversation is displayed) has a visually distinct highlighted background. All other pills use a muted background.
|
||||
|
||||
### Status badges
|
||||
11. Each child pill shows a status badge reflecting the agent's lifecycle state:
|
||||
- **In Progress**: animated indicator (spinner or pulse).
|
||||
- **Succeeded**: checkmark.
|
||||
- **Failed / Errored**: error icon.
|
||||
- **Blocked**: pause icon.
|
||||
- **Cancelled**: cancelled icon.
|
||||
12. The orchestrator pill does not display a status badge.
|
||||
13. Status badges update as the sharer's session progresses (live sessions). A brief delay (a few seconds) between a child completing and its badge updating is acceptable. For completed sessions, badges show the final state immediately.
|
||||
|
||||
### Pill click interaction
|
||||
14. Clicking a non-selected pill switches the agent view in-place to display that agent's conversation transcript. The clicked pill becomes the selected pill. The pill bar remains visible.
|
||||
15. Clicking the currently-selected pill is a no-op.
|
||||
16. Clicking the orchestrator pill while viewing a child conversation switches back to the orchestrator's conversation. This is the primary "back" navigation.
|
||||
17. On web, there is no 3-dot overflow menu on pills. Pane-management actions (open in new pane, open in new tab, focus pane) are not available since the web viewer is single-pane and read-only. On native, the overflow menu and pane-management actions behave the same as for local orchestration.
|
||||
|
||||
### Hover details card
|
||||
18. Hovering over a pill for 300ms displays a details card below the hovered pill. Moving the mouse away dismisses the card after 80ms.
|
||||
19. The details card displays (when available):
|
||||
a. Agent avatar and full name (not truncated).
|
||||
b. Working directory path.
|
||||
c. Task description or prompt summary.
|
||||
d. Harness type chip (e.g. "Claude Code", "Codex") — omitted for the default Oz harness.
|
||||
e. PR branch name and number.
|
||||
f. Status label for child agents.
|
||||
20. Fields with no data are omitted from the card rather than shown empty.
|
||||
21. The card repositions to avoid overflowing the viewport.
|
||||
|
||||
### Conversation display
|
||||
22. When the view switches to a child agent's conversation, the agent view renders that child's full conversation transcript (messages, tool calls, tool results, agent output) in read-only mode.
|
||||
23. If the child's transcript has not been loaded yet, the agent view shows the default zero state (empty conversation view) while the transcript is fetched. The pill bar remains visible and interactive during loading. **Open question:** a dedicated loading indicator for transcript fetching would be better UX; this is deferred to a design follow-up that could improve loading states across all shared session views.
|
||||
24. The pill bar continues to show all agents from the orchestration while viewing any conversation in the group — including while viewing a child.
|
||||
25. Switching between conversations preserves scroll position per conversation. The existing pane swap mechanism handles this automatically.
|
||||
26. Once a child's session has been joined, the conversation state is retained. Switching away and back displays the retained state immediately without re-joining.
|
||||
|
||||
### Live session behavior
|
||||
27. New children that spawn while the viewer is connected appear as new pills appended to the right of existing child pills. A brief delay before newly spawned children appear is acceptable.
|
||||
28. When viewing the orchestrator conversation, inline orchestration cards (run_agents confirmation, lifecycle status) render as they appear in the orchestrator's transcript (these flow through the existing session WebSocket).
|
||||
29. Clicking a child pill joins that child's session. For a live child, new messages stream in after the initial replay completes. For a terminal child, the full transcript is replayed and the session completes.
|
||||
30. If the sharer's session ends while the viewer is watching, the pill bar remains visible with the last known statuses.
|
||||
|
||||
### Completed session behavior
|
||||
31. When a viewer opens a completed orchestrated session, the full orchestration structure (orchestrator + all children with final statuses) is available immediately.
|
||||
32. The viewer can click through all child conversations to review their work.
|
||||
|
||||
### Horizontal overflow
|
||||
33. When there are more child pills than fit in the available width, overflow pills are clipped at the pane boundary, matching the native pill bar's current behavior.
|
||||
|
||||
### Edge cases
|
||||
34. If the orchestrator called `run_agents` but no children have spawned yet (e.g. waiting for user approval), the pill bar is hidden. It appears once the first child is detected, matching the native pill bar's behavior.
|
||||
35. If a child agent's name is missing or empty, the pill displays a placeholder initial and a generic label like "Agent".
|
||||
36. If the viewer loses network connectivity, the pill bar retains its last known state. On reconnection, it reconciles with current session state by re-fetching the children list.
|
||||
37. If a child transcript fetch fails (network error, auth failure), the agent view shows an error state with a retry affordance. The pill bar remains functional.
|
||||
38. A session with multiple `run_agents` batches shows all children from all batches in a single flat pill list, ordered by spawn time.
|
||||
|
||||
### Non-goals
|
||||
39. Nested orchestration (a child that is itself an orchestrator with grandchildren) is out of scope. Only single-level parent→children is supported.
|
||||
40. The pill bar is not interactive for execution control — the viewer cannot cancel, restart, or message agents.
|
||||
41. Pinning pills or reordering them is not supported.
|
||||
42. The pill bar works in both the web (WASM) viewer and the native client when viewing shared sessions. On the web viewer, pane-management actions (open in new pane/tab) are not available since the web viewer is single-pane. On the native client, pane-management actions are available as they are for local orchestration.
|
||||
@@ -0,0 +1,300 @@
|
||||
# Orchestration Pill Bar in Shared Session Web Viewer
|
||||
|
||||
## Context
|
||||
|
||||
See `PRODUCT.md` for user-visible behavior.
|
||||
|
||||
### Current pill bar (native, non-shared)
|
||||
|
||||
The native pill bar renders in `app/src/ai/blocklist/agent_view/orchestration_pill_bar.rs` as a `View` that reads from `BlocklistAIHistoryModel`. Child conversations are created by `start_new_child_conversation()` in `app/src/ai/blocklist/history_model.rs (398-433)`, which registers the parent-child relationship in a `children_by_parent: HashMap` index. The pill bar queries `child_conversations_of(parent_id)` to build its pill list.
|
||||
|
||||
The pill bar is instantiated in `TerminalView` and rendered in `app/src/terminal/view/pane_impl.rs (528-537)` as a secondary header row, gated on `FeatureFlag::OrchestrationPillBar` and `AgentView` and the agent view being fullscreen.
|
||||
|
||||
Pill click actions dispatch `TerminalAction::SwitchAgentViewToConversation` (in-place switch), `OpenChildAgentInNewPane`, `OpenChildAgentInNewTab`, or `RevealChildAgent`. The last three depend on `WorkspaceRegistry` and `PaneGroup` APIs that don't exist on WASM.
|
||||
|
||||
### Current shared session viewer
|
||||
|
||||
The viewer connects via WebSocket to the session-sharing server, receiving `DownstreamMessage::OrderedTerminalEvent` events serialized as JSON. Events are defined in the external `session-sharing-protocol` crate (pinned at rev `3a12b87` in `Cargo.toml:252`).
|
||||
|
||||
`AgentResponseEvent` is the only event type carrying agent conversation data. It includes:
|
||||
- `response_event: String` — base64-encoded protobuf `ResponseEvent` (from `warp_multi_agent_api`)
|
||||
- `response_initiator: Option<ParticipantId>`
|
||||
- `forked_from_conversation_token: Option<ServerConversationToken>`
|
||||
|
||||
The fork mechanism already supports creating new conversations from a forked token via `link_forked_conversation_token()` in `app/src/ai/blocklist/controller/shared_session.rs (591-622)`. However, it treats forks as sequential continuations, not hierarchical parent-child relationships.
|
||||
|
||||
### The gap
|
||||
|
||||
Three things are missing:
|
||||
|
||||
1. **No child agent discovery in shared sessions.** The viewer has no mechanism to learn that the parent session spawned child agents, what their names/statuses are, or how to access their transcripts. Child tasks exist server-side with their own shared sessions, but the viewer doesn't query for them.
|
||||
|
||||
2. **No parent-child conversation registration.** The pill bar renders from `BlocklistAIHistoryModel.child_conversations_of(parent_id)`. In shared sessions, no child conversations are created and no parent-child relationships are registered, so the pill bar has no data.
|
||||
|
||||
3. **Pill bar has WASM-incompatible dependencies.** `is_conversation_open_in_other_visible_view()` and `pane_group_id_containing_terminal_view()` in `orchestration_pill_bar.rs (1696-1750)` call `WorkspaceRegistry` and `PaneGroup` APIs that don't exist in the WASM target. The 3-dot menu dispatches pane-split actions unavailable on web.
|
||||
|
||||
### Architectural lessons from the V1 attempt
|
||||
|
||||
An initial implementation routed all child shared-session traffic through the parent viewer's `TerminalView` + `BlocklistAIController`, opened child `Network`s on the parent's `TerminalManager`, and switched the parent's agent view in place when the user clicked a child pill. That approach is structurally incompatible with the existing app in three ways:
|
||||
|
||||
1. `BlocklistAIController` is single-stream. `shared_session_state.current_response_id` is a single `Option<ResponseStreamId>` set in `on_shared_init` and read by `on_shared_client_actions`. `ClientActions` carry no stream identifier, so two Networks feeding the same controller can interleave and route one stream's actions into the other stream's conversation.
|
||||
2. Conversation ownership is single-view. `BlocklistAIHistoryModel.live_conversation_ids_for_terminal_view` treats each conversation as owned by exactly one view, and `set_active_conversation_id` transfers ownership. Any flow that materializes a hidden child pane (e.g. `restore_missing_child_agent_panes_for_parent`, triggered on `AgentViewControllerEvent::EnteredAgentView` for a parent that has children registered) hands the child conversation to that new pane and the parent view's in-place switch then fails the ownership check.
|
||||
3. The parent session emits a fresh `Init` for every request the orchestrator processes. Each `Init` runs `set_pending_query_state_for_existing_conversation(parent)`, which calls `try_enter_agent_view(parent)`, which exits any child agent view the user navigated to and re-enters the parent's. This refires `EnteredAgentView`, which refires pane materialization, which then transfers child conversation ownership all over again.
|
||||
|
||||
The local-orchestration UI already has a clean answer to all three problems: each child agent gets its own `TerminalView`, `BlocklistAIController`, conversation, and (for remote children) `Network`. The pill bar is a navigation affordance over the pane group, and clicking a pill swaps the visible pane via `SwapPaneToConversation`. Shared-session viewing of an orchestration should mirror that architecture rather than invent a new pattern.
|
||||
|
||||
### Relevant files
|
||||
|
||||
**Pill bar (shared with native):**
|
||||
- `app/src/ai/blocklist/agent_view/orchestration_pill_bar.rs` — pill bar View (1994 lines); `pill_specs()` (418-479), `is_conversation_open_in_other_visible_view()` (1696-1724), `pane_group_id_containing_terminal_view()` (1731-1750)
|
||||
- `app/src/ai/blocklist/history_model.rs (388-450)` — parent-child conversation index (`child_conversations_of`, `start_new_child_conversation`, `set_parent_for_conversation`)
|
||||
- `app/src/terminal/view/pane_impl.rs (503-543)` — pill bar rendering integration
|
||||
- `crates/warp_features/src/lib.rs (665-720)` — feature flags (`OrchestrationPillBar`, `Orchestration`)
|
||||
|
||||
**Shared session viewer:**
|
||||
- `app/src/terminal/shared_session/viewer/terminal_manager.rs (730-750)` — task_id extraction from `SessionSourceType::AmbientAgent`
|
||||
- `app/src/terminal/shared_session/viewer/network.rs` — WebSocket session connection management
|
||||
- `app/src/terminal/shared_session/viewer/event_loop.rs (275-318)` — AgentResponseEvent processing
|
||||
- `app/src/ai/blocklist/controller/shared_session.rs (87-240)` — shared session init, `handle_shared_session_response_event()`
|
||||
- `app/src/ai/blocklist/task_status_sync_model.rs (125-137)` — skips status reporting for `is_viewing_shared_session` conversations
|
||||
- `app/src/workspace/view/wasm_view.rs (180-196)` — WASM viewer REST API calls via `ServerApiProvider`
|
||||
|
||||
**Server REST API (read-only, no changes needed):**
|
||||
- `warp-server/router/handlers/public_api/agent_webhooks.go (1807-1814)` — `ancestor_run_id` filter validation
|
||||
- `warp-server/public_api/types/types.gen.go (1579-1658)` — `RunItem` struct with `SessionId`, `SessionLink`, `State`, `Title`, `AgentConfig`, `Artifacts`
|
||||
- `warp-server/authz/engine.go (556-605)` — `aiTaskPolicies`, ownership-based `ViewAction`
|
||||
|
||||
## Design options
|
||||
|
||||
Two approaches can deliver orchestration data to the web viewer. They share the same client-side pill bar work (WASM compat, conversation switching) but differ in how orchestration metadata and child transcripts reach the viewer.
|
||||
|
||||
### Option A: REST + per-child hidden shared-session pane (recommended for V1)
|
||||
|
||||
The viewer queries existing warp-server REST APIs for child metadata and status. Each discovered child gets its own hidden shared-session viewer pane (its own `TerminalView`, `TerminalManager`, and child-session `Network`), reusing the pattern that local orchestration uses for remote child agents. Pill clicks navigate via the existing `SwapPaneToConversation` mechanism. No changes to `session-sharing-protocol` or `warp-proto-apis`.
|
||||
|
||||
**How it works:**
|
||||
|
||||
1. Viewer joins session → gets `task_id` from `SessionSourceType::AmbientAgent` in the join handshake (already implemented in `terminal_manager.rs:730-750`).
|
||||
2. Viewer calls `GET /agent/runs?ancestor_run_id={taskId}` → receives list of child `RunItem`s with names, states, agent configs, parent_run_id.
|
||||
3. For each child, the viewer creates a child conversation in `BlocklistAIHistoryModel` (with `is_viewing_shared_session = true`, `parent_conversation_id` set, `task_id` set) and — once `RunItem.SessionId` is known — materializes a hidden shared-session viewer pane that joins that child's session.
|
||||
4. Viewer periodically re-fetches `GET /agent/runs?ancestor_run_id={taskId}` to detect new children, surface `session_id`s that weren't ready at first discovery, and update status badges.
|
||||
5. When user clicks a child pill → `SwapPaneToConversation` swaps the visible pane for the hidden child pane. The user sees that pane's own agent view, fed by its own `Network` against the child's shared session.
|
||||
|
||||
**Existing infrastructure:**
|
||||
- HTTP client (`crates/http_client/`) works on WASM — the web viewer already makes REST calls.
|
||||
- `GET /agent/runs?ancestor_run_id=` can be polled periodically to get current state for all children in a single request.
|
||||
- `GET /agent/runs?ancestor_run_id=` returns `RunItem` with `State` (QUEUED, PENDING, CLAIMED, INPROGRESS, SUCCEEDED, FAILED, ERROR, BLOCKED, CANCELLED) which maps directly to `ConversationStatus` pill badges.
|
||||
- Child tasks have their own shared sessions (`RunItem.SessionId`). The viewer can join child sessions via WebSocket using the same session-sharing protocol as the parent, getting live `AgentResponseEvent` streams that are harness-agnostic.
|
||||
|
||||
**Pros:**
|
||||
- Repos touched: **1-2** (warp client + possibly minor warp-server authz). No `session-sharing-protocol` or `warp-proto-apis` changes.
|
||||
- Uses existing, tested APIs and client patterns.
|
||||
- Simpler server-side work.
|
||||
|
||||
**Cons:**
|
||||
- Status badge updates have polling latency (a few seconds between fetches).
|
||||
- Hidden panes for clicked-but-not-yet-running children are created lazily once a `session_id` is available, so the first click on a freshly queued child may briefly land on an empty agent view until the next poll surfaces the session id and the hidden pane is materialized.
|
||||
- External link-shared viewers (not on the owning team) cannot access child tasks via REST. Covered by ownership for the common cases (see Layer 1).
|
||||
|
||||
### Option B: Streaming through session-sharing protocol
|
||||
|
||||
The server forwards child `AgentResponseEvent`s and lifecycle status updates through the parent's shared session WebSocket, enriched with orchestration metadata.
|
||||
|
||||
**How it works:**
|
||||
|
||||
1. Extend `StreamInit` in `warp-proto-apis/apis/multi_agent/v1/response.proto` with optional orchestration fields: `agent_name`, `agent_role`, `parent_agent_id`, `harness_type`, `working_directory`, `agent_description`.
|
||||
2. Add `OrchestrationEvent::ChildStatusUpdate` variant to `OrderedTerminalEventType` in `session-sharing-protocol`.
|
||||
3. Server forwards child `ResponseEvent`s to the parent's shared session (via `forked_from_conversation_token`) and publishes lifecycle status updates as `OrchestrationEvent`s.
|
||||
4. Client processes forked child events to create child conversations and registers parent-child relationships. Status updates arrive via the existing WebSocket — no polling needed.
|
||||
|
||||
**Pros:**
|
||||
- Real-time: status and transcript updates arrive instantly via the existing WebSocket.
|
||||
- Child transcripts are always available (streamed proactively).
|
||||
- No auth gap: events flow through the already-authorized session connection.
|
||||
|
||||
**Cons:**
|
||||
- Repos touched: **4** (session-sharing-protocol, warp-proto-apis, warp-server, warp client).
|
||||
- Complex server plumbing: forwarding child events to parent session, lifecycle notifier integration.
|
||||
- Event volume scales with child count.
|
||||
- `session-sharing-protocol` is an external repo pinned by git rev.
|
||||
|
||||
### Recommendation
|
||||
|
||||
Option A for V1, with each child hosted in its own hidden shared-session pane (see Layer 2). The polling latency is acceptable for viewing sessions (status badges don't need sub-second updates), and touching 1-2 repos vs 4 significantly reduces implementation scope. Option B can be layered on later if real-time fidelity becomes important.
|
||||
|
||||
## Proposed changes (Option A)
|
||||
|
||||
### Layer 1: warp-server — authz (no changes needed)
|
||||
|
||||
The authz model is ownership-based (`authz/engine.go:556-605`): `ViewAction` on an AI task is granted if the principal owns it or belongs to the owning team. Child tasks spawned by `run_agents` inherit the same user/team ownership as the parent. Therefore, any principal who can view the parent task already has `ViewAction` on the children — no new authz rules are required.
|
||||
|
||||
This covers the two common cases:
|
||||
- User viewing their own orchestrated session (owns all tasks).
|
||||
- Teammate viewing a shared link (team-owned tasks are accessible).
|
||||
|
||||
External users with only a session link who are not on the owning team would not have access to child tasks. This is an edge case that can be addressed later if link-shared orchestrated sessions become common.
|
||||
|
||||
### Layer 2: warp client — children poller + per-child hidden shared-session panes
|
||||
Split the responsibilities cleanly:
|
||||
- A new `OrchestrationViewerModel` *only* discovers children via REST, registers them in `BlocklistAIHistoryModel`, and emits `TerminalViewEvent::EnsureSharedSessionViewerChildPane` on the parent's `TerminalView` once a child first reports a `session_id`. It owns **no** `Network`s.
|
||||
- The pane group's `ensure_shared_session_viewer_child_pane` handler materializes a hidden shared-session viewer pane per child. Each hidden pane is created via `create_shared_session_viewer(child_session_id, resources, view_size, /* enable_orchestration_polling */ false, ctx)`, giving the child its own `TerminalView`, `TerminalManager`, `BlocklistAIController`, and viewer-side `Network`. The pane is held off-tree in `child_agent_panes`, exactly like local-orchestration remote children.
|
||||
- Pill clicks dispatch `TerminalAction::SwitchAgentViewToConversation`, which `TerminalView::handle_action` translates to `Event::SwapPaneToConversation`. `TerminalPane::handle_view_event` then calls `ensure_hidden_child_agent_pane_for_conversation` + `swap_active_pane_to_conversation` (`pane_group/pane/terminal_pane.rs`).
|
||||
#### Initialization
|
||||
The viewer `TerminalManager` owns the `OrchestrationViewerModel` slot:
|
||||
```rust
|
||||
orchestration_viewer_model: Arc<FairMutex<Option<ModelHandle<OrchestrationViewerModel>>>>
|
||||
```
|
||||
The `Arc<FairMutex<Option<...>>>` matches the existing `current_network` storage pattern so the network-event closure can write into the slot without `&mut self`. Lazily created in the `JoinedSuccessfully` arm of the network-event subscription, only when:
|
||||
1. `enable_orchestration_polling == true` on this `TerminalManager` (root viewer pane), AND
|
||||
2. `FeatureFlag::OrchestrationViewerPillBar.is_enabled()`, AND
|
||||
3. The slot is currently `None` (`is_none()` guards against reconnect dupes), AND
|
||||
4. The session source is `SessionSourceType::AmbientAgent { task_id }`.
|
||||
The model captures the parent `WeakViewHandle<TerminalView>` so it can emit `EnsureSharedSessionViewerChildPane` on the host view.
|
||||
#### `enable_orchestration_polling` flag
|
||||
A single bool plumbed through `viewer::TerminalManager::new`/`new_deferred`, `ambient_agent::create_cloud_mode_view`, `PaneGroup::create_cloud_mode_terminal`, and `PaneGroup::create_shared_session_viewer`. `true` for the root viewer pane of an orchestrator, `false` for per-child viewer panes. Skipping polling on children avoids both duplicated REST traffic and grandchild double-registration via the transitive `ancestor_run_id` filter.
|
||||
Callers passing `true` (root viewers): `create_ambient_agent_terminal`, `replace_loading_pane_with_restored_ambient_cloud_mode_pane`, the restored-shared-session-viewer path in `restore_pane_leaf`, the `new_for_shared_session_viewer` constructor, the pending-restoration path in `register_pending_ambient_restorations`, and `view_impl.rs::start_cloud_mode`.
|
||||
Callers passing `false` (per-child viewers): `ensure_shared_session_viewer_child_pane`, `insert_ambient_agent_pane_hidden_for_child_agent`.
|
||||
#### a) Discover children on session join
|
||||
On construction, the model calls `GET /agent/runs?ancestor_run_id={taskId}` via `ServerApiProvider` (same HTTP client used in `wasm_view.rs`). For each `RunItem` in the response:
|
||||
- Create a child conversation via `start_new_child_conversation()` and call `set_viewing_shared_session_for_conversation(true)`. The `is_viewing_shared_session` flag suppresses `TaskStatusSyncModel` round-tripping.
|
||||
- Call `conversation.set_task_id(task_id)` so the pane group can look the task up later when it materializes the hidden pane.
|
||||
- Set `agent_name` from `RunItem.Title`.
|
||||
- Map `RunItem.State` → `ConversationStatus` and set it on the conversation.
|
||||
The model does **not** subscribe to `BlocklistAIHistoryEvent::SetActiveConversation`. It does not own `Network`s. It does not call `find_existing_conversation_by_server_token`. Its only side effects are updating the history model and emitting `EnsureSharedSessionViewerChildPane`.
|
||||
#### b) Polling state machine
|
||||
The model owns a single polling timer (`Option<SpawnedFutureHandle>`). `SpawnedFutureHandle` does **not** abort on drop, so `schedule_next_poll` explicitly calls `prior.abort()` before installing the next handle to prevent stacked timer chains.
|
||||
Two cadences:
|
||||
- **Active** (`STATUS_POLL_INTERVAL = 5s`): at least one tracked child is non-terminal.
|
||||
- **Idle** (`STATUS_POLL_INTERVAL_IDLE = 30s`): every known child is terminal. We don't stop polling entirely because follow-up user input on the orchestrator can spawn new children.
|
||||
The model **also subscribes to `BlocklistAIHistoryEvent::AppendedExchange`** and kicks the polling cadence back to fast whenever a new exchange arrives on the orchestrator or a tracked child (only during the idle→active transition, to avoid REST pile-up while already polling at the active cadence).
|
||||
#### c) Generation guard for stale fetches
|
||||
The model holds a `fetch_generation: u64` counter that is incremented immediately before each fetch dispatch. The response callback checks the captured generation against the live counter and silently drops responses from earlier generations. This guards against a slow timer-fired fetch clobbering a fresher kick fetch (e.g. timer-fired fetch starts, user types and triggers an AppendedExchange kick which aborts the timer and dispatches a fresh fetch — both responses now race, and only the freshest one should apply).
|
||||
#### d) Stop conditions
|
||||
`stop_orchestration_polling` aborts the polling handle and clears the model slot. Called from:
|
||||
- `NetworkEvent::SessionEnded` (non-ambient session) — viewer leaves a regular shared session.
|
||||
- `NetworkEvent::SessionEnded` (ambient session, **non-owner viewers only**) — owners may handoff via `attach_followup_session` (same `TerminalManager`, same orchestrator `task_id`), so their model is preserved. Non-owners (read-only viewers) won't get a follow-up session and should stop polling.
|
||||
- `NetworkEvent::ViewerRemoved` — viewer was removed and will not re-attach.
|
||||
- `NetworkEvent::FailedToReconnect` — terminal failure.
|
||||
#### e) Materialize hidden shared-session panes for joinable children
|
||||
`OrchestrationViewerModel` tracks each child in a `ChildAgentEntry` with a `pane_materialization_requested: bool` gate. When a child's `session_id` first transitions from `None` to `Some`, the model emits `Event::EnsureSharedSessionViewerChildPane { conversation_id, session_id }` on the parent's `TerminalView` exactly once per child (the gate prevents re-emission on subsequent polls).
|
||||
The parent `TerminalPane::handle_view_event` forwards the event to `PaneGroup::ensure_shared_session_viewer_child_pane`, which:
|
||||
1. **Race recovery**: if the user clicked a pill before this helper ran, `create_hidden_child_agent_pane`'s viewer-side branch will have already created a loading-placeholder pane (visible as a spinner with the pill bar) and registered it in `child_agent_panes`. The emission gate guarantees this helper runs at most once per child per model lifetime, so any existing entry must be that fallback — safe to discard. The helper captures the discarded pane's tree anchor (via `original_pane_for_replacement`) and re-swaps to the new pane after attaching it.
|
||||
2. Calls `create_shared_session_viewer(child_session_id, resources, view_size, /* enable_orchestration_polling */ false, ctx)` to mint a fully-equipped `(TerminalView, TerminalManager)` that begins joining the child's shared session.
|
||||
3. Wraps them in a `TerminalPane`, attaches off-tree via `attach_child_pane_off_tree`, stores the new `PaneId` in `child_agent_panes` keyed by the child conversation id.
|
||||
4. Restores the local child conversation into the new view (`restore_conversation_after_view_creation`), enters agent view, and opportunistically updates `ambient_agent_view_model` if the child happens to expose one.
|
||||
While a child does not yet have a `session_id`, clicking the pill renders a loading-placeholder pane (with the pill bar visible so the user can navigate back) instead of a stale empty agent view. The placeholder is replaced once the next poll surfaces the `session_id` and triggers this helper.
|
||||
#### f) Per-pane network lifecycle
|
||||
Each hidden child pane's `viewer::TerminalManager` owns its own `Network`. Events flow `Network → channel_event_proxy → BlocklistAIController` *inside that pane*, with no cross-talk into the parent viewer's controller. `current_response_id` is scoped to a single shared session per controller (the controller's existing single-stream invariant). When the parent viewer's pane group is torn down, the hidden child panes are dropped along with it (via `detach_panes_for_close` / `discard_pane`), which closes their networks.
|
||||
#### g) Click path
|
||||
1. User clicks child pill → `OrchestrationPillBar` dispatches `TerminalAction::SwitchAgentViewToConversation`.
|
||||
2. `TerminalView::handle_action` emits `Event::SwapPaneToConversation { conversation_id }` (no in-place branch — every child has a hidden pane, so swap to it).
|
||||
3. `TerminalPane::handle_view_event` calls `ensure_hidden_child_agent_pane_for_conversation` and `swap_active_pane_to_conversation`.
|
||||
4. The pane swap makes the child pane visible. Its agent view renders the child conversation that the child's own `BlocklistAIController` has been populating.
|
||||
#### h) Re-Inits don't disrupt user focus
|
||||
The per-child-pane architecture eliminates the cross-stream interference described in §V1 lessons. The parent's `BlocklistAIController` continues to receive `Init`s for the parent's own request streams; those `Init`s only affect the parent's pane because each child pane has its own controller, its own agent view state, and its own selected-conversation tracking. No changes to `on_shared_init` are required.
|
||||
|
||||
### Layer 3: warp client — make pill bar WASM-compatible
|
||||
|
||||
In `orchestration_pill_bar.rs`:
|
||||
|
||||
1. Gate pane-management helpers with `#[cfg(not(target_family = "wasm"))]`:
|
||||
- `is_conversation_open_in_other_visible_view()` (1696-1724) — on WASM, always return `false`
|
||||
- `pane_group_id_containing_terminal_view()` (1731-1750) — on WASM, always return `None`
|
||||
|
||||
2. Gate the 3-dot overflow menu creation and rendering with `#[cfg(not(target_family = "wasm"))]`. On WASM, no menu is created.
|
||||
|
||||
3. In `handle_action()` (532-660), the WASM path only handles `SwitchAgentViewToConversation` for pill clicks (no `OpenInNewPane`, `OpenInNewTab`, or `FocusOpenedConversation`).
|
||||
|
||||
4. In `render_pill()` (1224-1471), skip the overflow button rendering on WASM.
|
||||
|
||||
On native, the shared session viewer pill bar retains full pane-management actions (open in new pane/tab, focus pane) since the native client supports multi-pane layouts. No `#[cfg]` gates are needed for native — the existing pill bar logic applies as-is once child conversations are registered.
|
||||
|
||||
### Layer 4: warp client — enable pill bar for shared sessions
|
||||
|
||||
Add a new `FeatureFlag::OrchestrationViewerPillBar` flag in `crates/warp_features/src/lib.rs`. The pill bar for shared session viewers renders when `OrchestrationViewerPillBar` is enabled and the session has child agents.
|
||||
|
||||
In `pane_impl.rs (528-537)`, the existing pill bar rendering condition checks `FeatureFlag::OrchestrationPillBar` + `AgentView` + fullscreen. For shared session viewing, add a parallel condition: `FeatureFlag::OrchestrationViewerPillBar` + `AgentView` + fullscreen + the conversation has children registered via the REST data fetch.
|
||||
|
||||
The pill bar's pill-click action is plain `SwitchAgentViewToConversation`; `TerminalView`'s handler emits `SwapPaneToConversation` for shared-session children just like for local children. No in-place branch, no ownership-comparison guards, no `is_viewing_shared_session` fallback in `view.rs`.
|
||||
|
||||
Verify that `child_conversations_of()` returns the correct children for shared-session conversations and that `pill_specs()` (418-479) correctly builds the pill list from shared-session conversation data.
|
||||
|
||||
## Diagram (Option A)
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Server as warp-server REST API
|
||||
participant SSS as session-sharing-server
|
||||
participant ParentPane as parent viewer pane
|
||||
participant ChildPane as hidden child pane
|
||||
|
||||
Note over ParentPane: Joins parent session, gets task_id
|
||||
ParentPane->>Server: GET /agent/runs?ancestor_run_id={taskId}
|
||||
Server-->>ParentPane: [RunItem{name, state, session_id, config}, ...]
|
||||
|
||||
Note over ParentPane: Creates child conversation per child,<br/>renders pill bar.
|
||||
|
||||
Note over ParentPane,ChildPane: For each child with session_id known,<br/>materialize a hidden shared-session pane.
|
||||
ParentPane->>ChildPane: create_shared_session_viewer(session_id)
|
||||
ChildPane->>SSS: Join child session
|
||||
SSS-->>ChildPane: Init + ClientActions (replay)
|
||||
ChildPane->>ChildPane: Populate own child conversation
|
||||
|
||||
loop Poll while children active (~5s)
|
||||
ParentPane->>Server: GET /agent/runs?ancestor_run_id={taskId}
|
||||
Server-->>ParentPane: [RunItem{state, session_id?}, ...]
|
||||
Note over ParentPane,ChildPane: New session_ids surface ⇒<br/>materialize additional hidden child panes.
|
||||
end
|
||||
|
||||
Note over ParentPane: User clicks child pill ⇒ SwapPaneToConversation
|
||||
ParentPane-->>ChildPane: Pane swap (existing mechanism)
|
||||
Note over ChildPane: User sees child agent view fed by its<br/>own Network. No parent disruption.
|
||||
```
|
||||
|
||||
## Testing and validation
|
||||
|
||||
### Unit tests
|
||||
|
||||
- **Child discovery**: Test that after joining a session with `task_id`, calling `GET /agent/runs?ancestor_run_id={taskId}` and processing the response creates child conversations via `start_new_child_conversation()` with correct parent links, agent names, and metadata.
|
||||
- **Status polling**: Test that periodic re-fetch of `GET /agent/runs?ancestor_run_id={taskId}` detects state changes and triggers `conversation.update_status()` / `UpdatedConversationStatus`. Test that polling stops once all children reach terminal states. Test that newly spawned children are detected on re-fetch.
|
||||
- **RunItem.State → ConversationStatus mapping**: Test all state transitions: INPROGRESS → InProgress, SUCCEEDED → Success, FAILED/ERROR → Error, CANCELLED → Cancelled, BLOCKED → Blocked.
|
||||
- **Transcript loading**: Test that clicking a child pill joins the child's session via WebSocket, that `AgentResponseEvent`s are processed through `handle_shared_session_response_event()`, and that switching back to an already-joined child does not re-join.
|
||||
- **Pill bar WASM compat** (`orchestration_pill_bar.rs`): Verify `is_conversation_open_in_other_visible_view()` returns `false` under `cfg(target_family = "wasm")`. Verify no overflow menu is rendered on WASM.
|
||||
|
||||
### Manual validation
|
||||
|
||||
Mapped to PRODUCT.md invariants:
|
||||
|
||||
- **Invariants 1-5** (visibility): Open a completed orchestrated cloud session on web. Verify pill bar appears with orchestrator + all children. Open a non-orchestrated session; verify no pill bar.
|
||||
- **Invariants 6-10** (rendering): Verify orchestrator shows Oz icon, children show colored initials, avatar colors match between native client and web viewer for same agent names.
|
||||
- **Invariants 11-13** (status): Start a live orchestrated session, view on web, verify child badges update from In Progress → Succeeded/Failed as children complete.
|
||||
- **Invariants 14-17** (click): Click child pill, verify conversation switches in-place. Click orchestrator pill, verify it switches back. Verify no 3-dot menu appears.
|
||||
- **Invariants 18-21** (hover): Hover a pill for 300ms, verify details card appears with name, CWD, harness, description.
|
||||
- **Invariants 22-26** (conversation display): Switch to child, verify full transcript shows. Verify zero state shown during loading. Switch back to orchestrator, verify scroll position preserved. Verify terminal child transcript is cached; verify non-terminal child re-fetches.
|
||||
- **Invariants 27-30** (live): Join a live session mid-orchestration, verify children appear as they spawn on next poll, verify transcript loads on click.
|
||||
- **Invariant 33** (overflow): Test with 10+ children, verify overflow pills are clipped at pane boundary.
|
||||
|
||||
## Risks and mitigations
|
||||
|
||||
### Auth for external link-shared viewers
|
||||
The authz model is ownership-based: `ViewAction` is granted to the task owner and their team members. Child tasks inherit parent ownership, so the common cases (own session, teammate session) work without changes. However, external users with only a session link who are not on the owning team cannot access child tasks via REST APIs. Mitigation: defer this edge case; it does not block V1.
|
||||
|
||||
### Polling latency for status badges
|
||||
Status updates depend on polling frequency. With a ~5s poll interval on `GET /agent/runs?ancestor_run_id=`, badges may lag behind real status by a few seconds. Mitigation: acceptable for V1; can upgrade to Option B (streaming) later if real-time matters.
|
||||
|
||||
### Transcript loading latency
|
||||
First click on a child pill triggers a WebSocket join + session replay. Large conversations may have notable replay latency. Mitigation: the session connection persists after join, so subsequent views are instant. Zero state is shown during initial join (see product invariant 23).
|
||||
|
||||
### Children not yet visible at join time
|
||||
If the viewer joins before `run_agents` has completed, the initial `ancestor_run_id` query may return zero children. The viewer must re-poll the children list while the parent session is live. Mitigation: poll the children endpoint periodically (e.g. every 5s) until the session ends or all children reach terminal state.
|
||||
|
||||
### Backward compatibility
|
||||
Older servers that don't support `ancestor_run_id` filtering will return empty results — the viewer simply won't show a pill bar (graceful degradation per product invariant 3).
|
||||
|
||||
## Parallelization
|
||||
|
||||
With Option A, this work is entirely in the warp client repo (layers 2-4). No server changes are required (see Layer 1).
|
||||
|
||||
The client work itself is sequential: WASM pill bar compat (layer 3) and pill bar enablement (layer 4) can proceed without the data fetching (layer 2), but integration testing requires all layers. Given the small server-side scope, parallelization across repos provides limited benefit. A single agent can work through this sequentially.
|
||||
|
||||
Branch `matthew/orch-web-ui` is already set up across `warp-server` and `warp`.
|
||||
Reference in New Issue
Block a user