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
+108
View File
@@ -0,0 +1,108 @@
# Cloud agent tombstone and followup input behavior — Product Spec
Linear: APP-4483. Figma: none.
## Summary
When `FeatureFlag::HandoffCloudCloud` is enabled, make the idle-state UI for cloud agent conversations consistent and permission-aware. The decision to show the conversation-ended tombstone, the inline followup input, and any continue CTA should depend on two product concepts:
- Which harness produced the conversation: Oz vs. a third-party harness such as Claude Code.
- Whether the current user has edit access to the underlying AI conversation object.
Oz conversations should feel directly resumable when the user can edit the conversation. Third-party harness conversations should always preserve the terminal transcript boundary with a tombstone when no execution is active, because their continuation flow is different and they cannot be forked into a local Oz conversation.
## Problem
Today the tombstone/input behavior is scattered across session-sharing end handling, ambient task end handling, tombstone CTA rendering, and followup input state. The primary gate is currently whether the current user appears to own the ambient task. That is not the correct product model for shared cloud conversations: access should follow the underlying conversation permissions, not just task creator identity.
This leads to inconsistent outcomes when `FeatureFlag::HandoffCloudCloud` is enabled:
- A user with edit access to a shared Oz conversation may not get the inline followup input if they are not the task creator.
- A user who can only view an Oz conversation can see continuation affordances that imply they can mutate the original conversation.
- Third-party harness conversations can be treated like Oz conversations even though local forking is unsupported for those harnesses.
## Goals
- Use edit access to the underlying AI conversation as the product source of truth for whether the current user may continue the original cloud conversation.
- For Oz conversations:
- Hide the tombstone and show the followup input when the current user can edit the underlying conversation.
- Show the tombstone and the existing local continuation CTA when the current user can only view the underlying conversation.
- For third-party harness conversations:
- Always show the tombstone when there is no active execution.
- Show a cloud continuation CTA only when the current user can edit the underlying conversation.
- Never show a fork-local CTA.
- Keep active executions unchanged: while the cloud execution is live, the user should see the active shared/cloud session rather than an ended-state tombstone.
## Non-goals
- Implement fork-and-continue for third-party harness conversations.
- Change tombstone metadata, credits, artifacts, runtime, or error presentation.
- Change server-side authorization rules. The client should reflect permissions, but the server remains authoritative for followup submission.
- Redesign the tombstone UI layout beyond which CTA is present.
- Change behavior when `FeatureFlag::HandoffCloudCloud` is disabled.
## Product concepts
### Conversation edit access
“Edit access” means the current user has at least edit-level access to the underlying AI conversation object. This should be derived from the conversations server permissions, not from ambient task ownership.
If the client has not yet loaded permissions, it should avoid presenting mutation affordances that might be wrong. The safe default is to treat access as view-only until edit access is known.
### Harness
Harness should come from the conversation/task metadata already used to identify Oz vs. non-Oz runs. `Oz` is the only harness eligible for local fork continuation. Any other known harness, including Claude Code, Codex, Gemini, or future third-party harnesses, should follow the third-party behavior.
If the harness is unknown while metadata is still loading, the safe default is to show the tombstone and hide mutation CTAs until the harness and permissions are known.
### Active execution
This spec applies when the cloud agent conversation has no active execution. If an execution is currently active, the live session UI remains the source of truth and the ended-state tombstone should not be inserted.
## Behavior
### Oz harness
If the conversation was produced by Oz and there is no active execution:
- User has edit access:
- Tombstone: hidden.
- Followup input: shown and editable.
- CTA: none, because the input is already available.
- Submitting the input continues the same cloud conversation.
- User has view access only:
- Tombstone: shown.
- Followup input: hidden or non-editable.
- CTA: show `Continue locally`.
- Clicking the CTA forks the cloud conversation into a local Warp conversation and continues locally, without mutating the original shared conversation.
### Third-party harness
If the conversation was produced by a third-party harness and there is no active execution:
- User has edit access:
- Tombstone: shown.
- Followup input: hidden until the user explicitly chooses to continue.
- CTA: show `Continue`.
- Clicking `Continue` starts the third-party cloud followup flow for the same conversation/run.
- User has view access only:
- Tombstone: shown.
- Followup input: hidden or non-editable.
- CTA: none.
- The user can inspect the transcript but cannot continue or fork it from this UI.
### Behavior invariants
- B1: Oz + edit access + no active execution: no tombstone, show followup input.
- B2: Oz + view-only access + no active execution: show tombstone with `Continue locally`.
- B3: Third-party + edit access + no active execution: show tombstone with `Continue`.
- B4: Third-party + view-only access + no active execution: show tombstone with no continue CTA.
- B5: Any harness + active execution: no ended-state tombstone; keep live execution UI.
- B6: Unknown harness or unknown access + no active execution: show tombstone with no mutation CTA.
### UI state table
| Harness | Conversation access | Execution state | Tombstone | Followup input | CTA | Result |
| --- | --- | --- | --- | --- | --- | --- |
| Oz | Edit | Active execution | Hidden | Hidden while execution is active | None | User watches or interacts with the live cloud execution UI. |
| Oz | View only | Active execution | Hidden | Hidden while execution is active | None | User watches the live cloud execution UI without ended-state affordances. |
| Third-party | Edit | Active execution | Hidden | Hidden while execution is active | None | User watches or interacts with the live third-party cloud execution UI. |
| Third-party | View only | Active execution | Hidden | Hidden while execution is active | None | User watches the live third-party cloud execution UI without ended-state affordances. |
| Oz | Edit | No active execution | Hidden | Shown and editable | None | User submits a followup that continues the same cloud conversation. |
| Oz | View only | No active execution | Shown | Hidden or non-editable | `Continue locally` | User forks into a local Warp conversation before continuing. |
| Third-party | Edit | No active execution | Shown | Hidden until continuation starts | `Continue` | User continues via the existing third-party cloud followup execution flow. |
| Third-party | View only | No active execution | Shown | Hidden or non-editable | None | User can inspect the transcript but cannot continue from this UI. |
| Unknown | Any or unknown | No active execution | Shown | Hidden or non-editable | None | Client waits for metadata before showing mutation affordances. |
## Copy
Preferred CTA copy:
- Oz, view-only: `Continue locally`
- Third-party, edit access: `Continue`
Tooltips can clarify the distinction:
- Oz local continuation CTA: “Fork this conversation into a local Warp session.”
- Third-party continue CTA: “Continue this cloud conversation.”
Avoid showing “Continue locally” for third-party harnesses, because local continuation is unsupported and misleading.
## Edge cases
- Metadata not loaded: show the tombstone and hide continue CTAs until both harness and editability are known.
- Permissions change while viewing: recompute the tombstone/input state from the latest conversation permissions. If edit access is revoked, hide the followup input and remove any continue CTA that would mutate the original conversation.
- Conversation has task metadata but no conversation metadata: show the tombstone and hide mutation CTAs until conversation metadata is available, unless there is an existing server-confirmed editability signal.
- Followup submission fails due to server-side permission denial: keep the user in the ended-state UI, restore their draft if possible, and show a concise error toast.
- Third-party harness later gains local fork support: this spec should be revisited; until then, third-party harnesses must not show fork-local affordances.
## Success criteria
- An Oz cloud conversation where the current user has edit access ends with an editable followup input and no tombstone.
- An Oz cloud conversation where the current user only has view access ends with a tombstone containing `Continue locally`, and no inline followup input.
- A Claude Code cloud conversation where the current user has edit access ends with a tombstone containing `Continue`, and no fork-local CTA.
- A Claude Code cloud conversation where the current user only has view access ends with a tombstone and no continue CTA.
- Ambient task creator identity is no longer the product source of truth for showing the followup input; conversation edit access is.
- Active executions do not show the ended-state tombstone regardless of harness or permissions.
- Existing behavior remains unchanged when `FeatureFlag::HandoffCloudCloud` is disabled.
## Resolved decisions
- Keep the existing `Continue locally` copy for the Oz view-only local continuation CTA.
- Third-party `Continue` uses the existing third-party followup execution flow. This spec should not introduce a new modal, inline input mode, or alternate continuation concept.
- For view-only third-party conversations, the absence of a CTA is sufficient; no explanatory subtitle is required.
+127
View File
@@ -0,0 +1,127 @@
# Cloud agent tombstone and followup input behavior — Tech Spec
Product spec: `specs/APP-4483/PRODUCT.md`
## Context
`PRODUCT.md` defines behavior invariants B1B6. This technical spec implements those invariants when `FeatureFlag::HandoffCloudCloud` is enabled, while preserving existing behavior when that flag is disabled.
The current UI decision points are split across three places:
- `app/src/terminal/view/shared_session/view_impl.rs:729` computes `viewed_ambient_task_id_owned_by_current_user` and uses task creator ownership to decide whether `on_session_share_ended` inserts a tombstone.
- `app/src/terminal/view/shared_session/view_impl.rs:779` enables the followup input only when the current user owns the ambient task.
- `app/src/terminal/view/shared_session/view_impl.rs:811` repeats the same ownership model for `handle_non_running_ambient_agent_task`.
There is a second delayed path for already-loaded ambient tasks:
- `app/src/terminal/view.rs:7123` checks whether a non-running shared ambient task should get a tombstone.
- `app/src/terminal/view.rs:7149` again uses `owned_ambient_agent_task_id` plus `HandoffCloudCloud` to decide whether to show the input instead.
The tombstone currently makes an independent CTA decision:
- `app/src/terminal/view/shared_session/conversation_ended_tombstone_view.rs:207` creates a `Continue` cloud button whenever a task id exists and `HandoffCloudCloud` is enabled.
- `app/src/terminal/view/shared_session/conversation_ended_tombstone_view.rs:221` creates a `Continue locally` button whenever a conversation id exists.
- `app/src/terminal/view/shared_session/conversation_ended_tombstone_view.rs:473` hides local continuation for known non-Oz harnesses, but treats unknown harness metadata as local-continuable.
The data needed for the new product model is already present:
- `ServerAIConversationMetadata` contains `harness` and `permissions` in `app/src/ai/agent/conversation.rs:3849`.
- `AIAgentHarness` distinguishes Oz, Claude Code, Gemini, Codex, and Unknown in `app/src/ai/agent/conversation.rs:3821`.
- `BlocklistAIHistoryModel::get_server_conversation_metadata` already looks up loaded conversation metadata with a fallback to cached conversation metadata in `app/src/ai/blocklist/history_model.rs:1986`.
- `AgentConversationsModel::fetch_ambient_agent_tasks_and_cloud_convo_metadata` fetches ambient tasks and cloud conversation metadata together, including additional metadata for task conversation IDs missing from the first metadata response, in `app/src/ai/agent_conversations_model.rs:675`.
- `AmbientAgentTask` exposes `conversation_id`, active execution state, and whether cloud followup submission is allowed in `app/src/ai/ambient_agents/task.rs:317`.
- `SharingAccessLevel` is ordered as View < Edit < Full in `crates/warp_server_client/src/drive/sharing.rs:8`.
There is an object-access precedent, but it is tied to loaded Warp Drive objects:
- `CloudViewModel::access_level` defaults missing objects to view access in `app/src/cloud_object/model/view.rs:173`.
- `CloudViewModel::object_access_level` grants full access for personal/team-space objects, applies link and guest ACLs for shared-space objects, and upgrades creator access to edit in `app/src/cloud_object/model/view.rs:181`.
The APP-4483 implementation should reuse the same permission semantics where possible, but must operate directly on `ServerAIConversationMetadata.permissions` because AI conversation metadata is not a `CloudObject`.
## Proposed changes
### 1. Add a single continuation UI state resolver
Add a small helper module under `app/src/terminal/view/shared_session/`, for example `cloud_conversation_continuation.rs`, and use it from all tombstone/followup decision paths.
Suggested core types:
- `ContinuationHarness`: `Oz`, `ThirdParty`, or `Unknown`.
- `ConversationAccess`: `Edit`, `ViewOnly`, or `Unknown`.
- `TombstoneCta`: `ContinueLocally`, `ContinueInCloud { task_id }`, or none.
- `CloudConversationContinuationUiState`, containing:
- whether the ended-state tombstone should be present;
- whether the inline cloud followup input should be enabled;
- the task id to use for cloud followup input submission, if any;
- the tombstone CTA to render, if any.
The resolver should accept the terminal view id, optional ambient task id, whether a live shared session is still active, and `AppContext`. It should derive:
- active execution from the ambient task when present;
- harness from `ServerAIConversationMetadata.harness`, not from tombstone display metadata;
- edit access from `ServerAIConversationMetadata.permissions`;
- unknown state when task or conversation metadata is unavailable.
Mapping:
- B1: Oz + edit + no active execution returns no tombstone and followup input enabled for that task.
- B2: Oz + view-only + no active execution returns tombstone with `ContinueLocally`.
- B3: third-party + edit + no active execution returns tombstone with `ContinueInCloud`.
- B4: third-party + view-only + no active execution returns tombstone with no CTA.
- B5: any harness/access + active execution or live shared session returns no ended-state tombstone and no followup input transition.
- B6: unknown harness or unknown access + no active execution returns tombstone with no CTA.
When `FeatureFlag::HandoffCloudCloud` is disabled, keep the existing pre-APP-4483 behavior path: ended ambient sessions may show the tombstone, but the permission-aware cloud followup state should not be used.
### 2. Resolve conversation metadata by server token
Add a `BlocklistAIHistoryModel` helper that returns `ServerAIConversationMetadata` by `ServerConversationToken`, for example:
- check `server_token_to_conversation_id`;
- if found, reuse `get_server_conversation_metadata`;
- otherwise scan `all_conversations_metadata` for matching `server_conversation_token`;
- return `None` if metadata is not loaded.
This avoids forcing callers to manufacture or resolve an `AIConversationId` before they can inspect task-linked conversation permissions. `AmbientAgentTask::conversation_id` returns the token string at `app/src/ai/ambient_agents/task.rs:317`, so the resolver can construct a `ServerConversationToken` from that value and ask the history model for metadata.
If metadata is missing, do not fetch synchronously from the UI resolver. Treat the state as B6 and let the existing `AgentConversationsModel` fetch path populate metadata asynchronously.
### 3. Compute edit access from `ServerPermissions`
Add a pure helper near the resolver or in a small permission utility that computes the current user's effective `SharingAccessLevel` from `ServerAIConversationMetadata`.
Rules:
- If the current user is missing or logged out, start at `SharingAccessLevel::View`.
- If `permissions.space` is `Owner::User` for the current user, return at least `Full`.
- If `permissions.space` is `Owner::Team` and the team appears in `UserWorkspaces::team_from_uid_across_all_workspaces`, return at least `Full`.
- Apply `permissions.anyone_link_sharing` as a baseline when present.
- Apply user guest ACLs when `ServerGuestSubject::User { firebase_uid }` matches the current user.
- Apply team guest ACLs when the current user belongs to the guest team according to `UserWorkspaces`.
- Ignore pending-user ACLs for this UI decision.
- If `metadata.creator_uid` matches the current user, upgrade to at least `Edit`, matching the creator fallback in `CloudViewModel::object_access_level`.
- Return `ConversationAccess::Edit` for `Edit` or `Full`; return `ViewOnly` for `View`.
If team membership data is not loaded and access is only knowable through a team owner/guest ACL, do not assume edit access. The safe UI state remains view-only/unknown until workspace metadata is available.
### 4. Replace ownership-based UI branching
Replace the creator-owned task gate in these call sites with the resolver:
- `on_session_share_ended` in `app/src/terminal/view/shared_session/view_impl.rs:727`.
- `handle_non_running_ambient_agent_task` in `app/src/terminal/view/shared_session/view_impl.rs:811`.
- `maybe_insert_tombstone_for_non_running_shared_ambient_task` in `app/src/terminal/view.rs:7123`.
Add a shared method on `TerminalView`, for example `refresh_non_running_cloud_agent_continuation_ui`, that:
- exits early for disabled `CloudModeSetupV2`, active replay, existing pending cloud followup submission, or disabled `HandoffCloudCloud`;
- asks the resolver for the current state;
- removes an existing tombstone when the new state is B1 and enables the cloud followup input;
- inserts or updates the tombstone when the new state is B2, B3, B4, or B6;
- keeps input selectable/read-only for viewers when no continuation input should be shown.
`enable_owned_cloud_followup_input` should be renamed or wrapped with a permission-neutral name, such as `enable_cloud_followup_input`, because it will now be used for users with edit access who may not be the task creator.
Update `try_submit_pending_cloud_followup` in `app/src/terminal/view.rs:20026` so it does not fall back to `owned_ambient_agent_task_id`. Submission should use an explicit `pending_cloud_followup_task_id` or a task id that the resolver stored when it enabled the inline input. This prevents creator ownership from remaining a hidden permission bypass.
### 5. Make tombstone CTAs data-driven
Change `ConversationEndedTombstoneView::new` to accept a CTA decision from the resolver rather than constructing both buttons from `task_id` and `conversation_id`.
The tombstone should render:
- `Continue locally` only for `TombstoneCta::ContinueLocally`.
- `Continue` only for `TombstoneCta::ContinueInCloud { task_id }`.
- no button when the CTA is absent.
Keep `ConversationEndedTombstoneEvent::ContinueInCloud` and `start_cloud_followup_from_tombstone` for B3. Keep `ContinueLocally` behavior for B2. Keep `TombstoneDisplayData::enrich_from_task` for display metadata only; it should no longer decide CTA visibility after an async task fetch.
This removes the current mismatch where `Continue` is shown for any task id and `Continue locally` is shown for unknown harness metadata.
### 6. Recompute when metadata changes
Permissions and metadata can arrive after the tombstone is first inserted. Recompute the continuation UI state when:
- `AgentConversationsModelEvent::TasksUpdated` updates task active-execution state or task conversation id;
- `AgentConversationsModelEvent::ConversationsLoaded` merges cloud conversation metadata;
- `BlocklistAIHistoryEvent::UpdatedConversationMetadata` updates server metadata for a live conversation.
If recomputation transitions:
- from B6 to B1, remove the tombstone and enable the input;
- from B6 to B2/B3/B4, update or reinsert the tombstone with the correct CTA;
- from B1 to a non-edit state, clear/disable the input and show the tombstone state for the latest access.
Use the resolver as the only source of truth for these transitions.
## Testing and validation
Add focused unit tests for the resolver and update existing shared-session tests so validation maps directly to `PRODUCT.md` B1B6:
- B1: Oz metadata + edit access + ended execution produces no tombstone, editable followup input, and cloud submission uses the same task id even when task creator is someone else.
- B2: Oz metadata + view-only access produces a tombstone with `Continue locally`, no inline followup input, and no cloud CTA.
- B3: Claude Code/Codex/Gemini metadata + edit access produces a tombstone with `Continue`, and clicking it enters the existing cloud followup flow.
- B4: third-party metadata + view-only access produces a tombstone with no continue CTA.
- B5: active execution or live shared session does not insert an ended-state tombstone regardless of harness/access.
- B6: missing metadata, unknown harness, or unknown access produces a tombstone with no mutation CTA.
Permission helper tests should cover:
- user owner;
- team owner with current-user team membership;
- user guest view vs edit;
- team guest view vs edit;
- link sharing view only;
- creator fallback to edit;
- missing current user defaults to non-edit.
Update or replace creator-based assertions in `app/src/terminal/view/shared_session/view_impl_tests.rs`, especially the tests currently named around “owned” ambient sessions. Add tombstone CTA tests in `app/src/terminal/view/shared_session/conversation_ended_tombstone_view_tests.rs` once CTA state is data-driven.
Suggested targeted commands:
- `cargo test -p warp --lib terminal::view::shared_session::view_impl_tests`
- `cargo test -p warp --lib terminal::view::shared_session::conversation_ended_tombstone_view_tests`
- `cargo test -p warp --lib terminal::view::shared_session::cloud_conversation_continuation`
Before PR/update, run the repository-required format and clippy checks from the PR workflow. Do not use `cargo fmt --all` or file-specific `cargo fmt`.
## Parallelization
Do not split this implementation across sub-agents. The changes are tightly coupled across one UI state resolver, terminal view lifecycle transitions, tombstone CTA rendering, and existing shared-session tests. Parallel edits would likely touch the same files and increase merge overhead more than they reduce wall-clock time.