Skip to content

implement local cloud handoff#9455

Open
harryalbert wants to merge 1 commit intographite-base/9455from
harry/remote-1486-implement-local-cloud-handoff
Open

implement local cloud handoff#9455
harryalbert wants to merge 1 commit intographite-base/9455from
harry/remote-1486-implement-local-cloud-handoff

Conversation

@harryalbert
Copy link
Copy Markdown
Contributor

@harryalbert harryalbert commented Apr 29, 2026

Description

This PR implements local -> cloud handoff flow in the client. This includes a snapshotting flow (similar to the cloud -> cloud snapshotting), adding a cloud mode entrypoint for handing off a local conversation, and then actual handing off the conversation succesfully.

Don't over-index on the UI too much — in a follow-up PR I'll add the conversation into the cloud mode pane and also re-use the full cloud mode setup v2 UI.

The associated server PR for this client PR is here: https://github.com/warpdotdev/warp-server/pull/10777

Testing

demo: https://www.loom.com/share/a6caa2c974e34b49b2b038a8019c062c

Agent Mode

  • Warp Agent Mode - This PR was created via Warp's AI Agent Mode

@cla-bot cla-bot Bot added the cla-signed label Apr 29, 2026
Copy link
Copy Markdown
Contributor Author

harryalbert commented Apr 29, 2026

@harryalbert harryalbert marked this pull request as ready for review April 29, 2026 18:37
@harryalbert harryalbert requested a review from zachbai April 29, 2026 18:37
@oz-for-oss
Copy link
Copy Markdown
Contributor

oz-for-oss Bot commented Apr 29, 2026

@harryalbert

I'm starting a first review of this pull request.

I completed the review and posted feedback on this pull request.

Comment /oz-review on this pull request to retrigger a review (up to 3 times on the same pull request).

I completed the review and posted feedback on this pull request.

Comment /oz-review on this pull request to retrigger a review (up to 3 times on the same pull request).

I completed the review and posted feedback on this pull request.

Comment /oz-review on this pull request to retrigger a review (up to 3 times on the same pull request).

You can view the conversation on Warp.

I completed the review and posted feedback on this pull request.

Comment /oz-review on this pull request to retrigger a review (up to 3 times on the same pull request).

Powered by Oz

Copy link
Copy Markdown
Contributor

@oz-for-oss oz-for-oss Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overview

This PR adds client-side local-to-cloud handoff entry points, touched-workspace derivation, snapshot prep/upload plumbing, and spawn-request fields for forking a local conversation into a cloud agent.

Concerns

  • Orphan file collection currently stages any readable absolute file outside a git repo based on conversation action paths, which can upload local files unrelated to the workspace.
  • Handoff snapshot upload targets are matched by response order even though the response includes filenames, so reordered responses can corrupt the staged snapshot.
  • Submitting before touched-workspace derivation completes drops the user's prompt and attachments because the input has already been cleared.
  • Prep/upload failures are only logged/stored in unused handoff state, leaving the user without an inline error or retry prompt.

Verdict

Found: 1 critical, 3 important, 0 suggestions

Request changes

Comment /oz-review on this pull request to retrigger a review (up to 3 times on the same pull request).

Powered by Oz

Comment thread app/src/ai/blocklist/handoff/touched_repos.rs
Comment thread app/src/ai/agent_sdk/driver/snapshot.rs Outdated
Comment thread app/src/terminal/view/ambient_agent/model.rs
Comment thread app/src/terminal/view/ambient_agent/model.rs Outdated
@harryalbert harryalbert changed the base branch from master to graphite-base/9455 April 29, 2026 19:53
@harryalbert harryalbert force-pushed the harry/remote-1486-implement-local-cloud-handoff branch from 50d01b3 to 0dd168b Compare April 29, 2026 19:54
@harryalbert harryalbert changed the base branch from graphite-base/9455 to zb/continue-cloud-tombstone April 29, 2026 19:54
@zachbai zachbai changed the base branch from zb/continue-cloud-tombstone to graphite-base/9455 April 29, 2026 21:32
@harryalbert harryalbert force-pushed the harry/remote-1486-implement-local-cloud-handoff branch from 0dd168b to 8582656 Compare April 30, 2026 19:38
@harryalbert harryalbert changed the base branch from graphite-base/9455 to zb/continue-cloud-tombstone April 30, 2026 19:38
Copy link
Copy Markdown
Contributor

@oz-for-oss oz-for-oss Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overview

This PR adds local-to-cloud handoff entry points, touched-workspace derivation, snapshot prep/upload plumbing, and spawn-request fields for forking a local conversation into a cloud agent.

Concerns

  • Handoff snapshot upload targets are matched to requested files by response order instead of by the returned filename, so an out-of-order server response can upload blobs under the wrong object names and break rehydration.
  • Submitting before touched-workspace derivation completes, or while a submission is already starting, returns after the input layer has already cleared the prompt and attachments, silently dropping user input.

Security

  • The touched-workspace scan treats every exchange working directory as touched, so handoff can upload uncommitted local repo changes even when the agent never touched that repo. Limit cwd inclusion to actual shell-command actions to keep the upload scope aligned with user expectations.

Verdict

Found: 0 critical, 3 important, 0 suggestions

Request changes

Comment /oz-review on this pull request to retrigger a review (up to 3 times on the same pull request).

Powered by Oz

Comment thread app/src/ai/agent_sdk/driver/snapshot.rs Outdated
Comment thread app/src/ai/blocklist/handoff/touched_repos.rs
Comment thread app/src/terminal/view/ambient_agent/model.rs
oz-for-oss[bot]
oz-for-oss Bot previously requested changes Apr 30, 2026
Copy link
Copy Markdown
Contributor

@oz-for-oss oz-for-oss Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overview

This PR adds the client-side local-to-cloud Oz handoff flow, including the handoff entry points, touched-workspace derivation, snapshot prep/upload, and spawn-request wiring for forked cloud runs.

Concerns

  • Orphan-file collection can upload arbitrary local files referenced in the conversation, including files outside the workspace such as secrets under the home directory.
  • Handoff snapshot upload targets are matched by response order instead of the response filename, which can swap or skip uploaded blobs if the server returns URLs in a different order.
  • Submitting before touched-workspace derivation completes clears the input and then no-ops, dropping the user prompt without starting a run.
  • Fatal prep/upload failures only update an internal state that is not rendered by this PR, so users get no visible error and the cleared prompt is not restored.

Verdict

Found: 1 critical, 3 important, 0 suggestions

Request changes

Comment /oz-review on this pull request to retrigger a review (up to 3 times on the same pull request).

Powered by Oz

Comment thread app/src/ai/blocklist/handoff/touched_repos.rs
Comment thread app/src/ai/agent_sdk/driver/snapshot.rs Outdated
Comment thread app/src/terminal/view/ambient_agent/model.rs
Comment thread app/src/terminal/view/ambient_agent/model.rs
@harryalbert harryalbert dismissed oz-for-oss[bot]’s stale review April 30, 2026 20:08

I don't need oz to approve my PR

Copy link
Copy Markdown
Contributor

@oz-for-oss oz-for-oss Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overview

This PR adds client-side local-to-cloud handoff entry points, touched-workspace derivation, handoff snapshot upload prep, and spawn request plumbing for forking a local conversation into a cloud run.

Concerns

  • Handoff prep failures restore only the prompt even though pending attachments were already cleared from the input, so retrying loses user-selected attachments.
  • The touched-path scan walks action messages oldest-to-newest within the newest exchange, so a long exchange can exhaust the 500-action cap before reaching its latest edits.
  • The submit guard can silently no-op while touched-workspace derivation is still running; the user sees an enabled editor/send path but no feedback.

Security

  • The handoff snapshot path logs the server conversation token for correlation; prefer a redacted or hashed correlation value because this token is also used in conversation/debug links.

Verdict

Found: 0 critical, 3 important, 1 suggestions

Request changes

Comment /oz-review on this pull request to retrigger a review (up to 3 times on the same pull request).

Powered by Oz

Comment thread app/src/terminal/view/ambient_agent/model.rs
Comment thread app/src/ai/blocklist/handoff/touched_repos.rs Outdated
Comment thread app/src/terminal/input.rs Outdated
Comment thread app/src/ai/agent_sdk/driver/snapshot.rs
@harryalbert harryalbert force-pushed the zb/continue-cloud-tombstone branch from 70f0742 to 275dd8b Compare April 30, 2026 22:43
@harryalbert harryalbert force-pushed the harry/remote-1486-implement-local-cloud-handoff branch from 48ed3c0 to 9311573 Compare April 30, 2026 22:43
@zachbai zachbai changed the base branch from zb/continue-cloud-tombstone to graphite-base/9455 May 1, 2026 17:28
@harryalbert harryalbert force-pushed the harry/remote-1486-implement-local-cloud-handoff branch from 9311573 to eea298a Compare May 2, 2026 01:09
@harryalbert harryalbert force-pushed the graphite-base/9455 branch from 275dd8b to a44fdd6 Compare May 2, 2026 01:09
@harryalbert harryalbert changed the base branch from graphite-base/9455 to zb/continue-cloud-tombstone May 2, 2026 01:09
@zachbai zachbai changed the base branch from zb/continue-cloud-tombstone to graphite-base/9455 May 2, 2026 02:13
@harryalbert harryalbert force-pushed the graphite-base/9455 branch from a44fdd6 to cbef010 Compare May 2, 2026 20:42
@harryalbert harryalbert force-pushed the harry/remote-1486-implement-local-cloud-handoff branch from eea298a to 3e9b09c Compare May 2, 2026 20:42
@harryalbert harryalbert changed the base branch from graphite-base/9455 to zb/continue-cloud-tombstone May 2, 2026 20:42
@zachbai zachbai changed the base branch from zb/continue-cloud-tombstone to graphite-base/9455 May 3, 2026 00:00
@harryalbert harryalbert force-pushed the graphite-base/9455 branch from cbef010 to b2d5235 Compare May 3, 2026 19:56
@harryalbert harryalbert force-pushed the harry/remote-1486-implement-local-cloud-handoff branch from 3e9b09c to 305a73a Compare May 3, 2026 19:56
@harryalbert harryalbert changed the base branch from graphite-base/9455 to zb/continue-cloud-tombstone May 3, 2026 19:57
@zachbai zachbai changed the base branch from zb/continue-cloud-tombstone to graphite-base/9455 May 4, 2026 04:43
/// `/oz-cloud-handoff` slash command) that fork the active local Oz
/// conversation into a fresh cloud agent run with the current workspace
/// snapshot attached. Requires `OzHandoff` to also be enabled.
LocalToCloudHandoff,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ya didn't wanna just copy the convention HandoffLocalCloud lol

});

pub static OZ_CLOUD_HANDOFF: LazyLock<StaticCommand> = LazyLock::new(|| StaticCommand {
name: "/oz-cloud-handoff",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think we can do better with this slash command name lol

) {
let declarations_path = resolve_declarations_path(Some(task_id));
let _ = upload_snapshot_from_declarations_file(&declarations_path, client, task_id).await;
let log_label = format!("task {task_id}");
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is there a meaningful reason this 'log label' was factored out? Is it because there is no task id for the pre handoff snapshot

}
}

Ok(Some(prep_token))
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we make prep token typed

path: &Path,
client: Arc<dyn HarnessSupportClient>,
task_id: &AmbientAgentTaskId,
log_label: &str,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this log label piping is kinda ugly and not sure if the task Id actually serves any meaningful purpose in the logs

Comment on lines +118 to +128
let mut cursor: Option<&Path> = if path.is_dir() {
Some(path)
} else {
path.parent()
};
while let Some(dir) = cursor {
let candidate = dir.join(".git");
if candidate.exists() {
return Some(dir.to_path_buf());
}
cursor = dir.parent();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

these arefs calls that shouldn't happen on the main thread

Comment on lines +135 to +158
async fn gather_repo_metadata(git_root: PathBuf) -> TouchedRepo {
let remote_url = git_string(&git_root, &["remote", "get-url", "origin"]).await;
let repo_id = remote_url.as_deref().and_then(parse_github_repo);
TouchedRepo { git_root, repo_id }
}

/// Run `git <args>` in `repo_dir`, returning the trimmed stdout if the exit status is 0
/// and the output decodes as UTF-8. Caps each invocation at [`GIT_COMMAND_TIMEOUT`] so a
/// stalled git process can't pin the modal's loading state forever.
async fn git_string(repo_dir: &Path, args: &[&str]) -> Option<String> {
match git_string_inner(repo_dir, args).await {
Ok(s) => {
let trimmed = s.trim().to_string();
if trimmed.is_empty() {
None
} else {
Some(trimmed)
}
}
Err(_) => None,
}
}

async fn git_string_inner(repo_dir: &Path, args: &[&str]) -> Result<String> {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this feels like an unnecessary amount of abstraction for running a single command to get the remote origin URL

Comment on lines +270 to +271
// Paths are kept absolute when they look absolute, and resolved against the
// exchange's `working_directory` when they don't. Empty entries are dropped.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

? "look absolute" just means has a leading '/' no?

// call). Covers `RunShellCommand` cwds without walking action results.
if let Some(cwd) = cwd {
let cwd_path = PathBuf::from(cwd);
if cwd_path.is_absolute() && seen.insert(cwd_path.clone()) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does is_absolute() require a syscall? if so we should refactor this to first collect paths from the conversation, then do validation (or any syscall-requiring work) async

pub referenced_attachments: Vec<String>,
/// When set, instructs the server to fork the named conversation and use the resulting
/// fork id as `task.AgentConversationID`. Mutually exclusive with the existing
/// `conversation_id` field (resume semantics) on the server. Used by the local-to-cloud
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

where is the conversation_id field? I don't see one?

Copy link
Copy Markdown
Contributor

@zachbai zachbai left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would like to split this PR up between the touchedrepos stuff, snapshotting behavior, and the actual UI - there is a little too much going on at the same time right now

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants