Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
135 changes: 107 additions & 28 deletions apps/staged/src-tauri/src/session_commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -207,11 +207,14 @@ pub async fn resume_session(

// Check if this session is linked to a project note — if so, we need
// to start the MCP server so the agent has access to project tools.
let mcp_project_id = store
let project_note = store
.get_project_note_by_session(&session_id)
.ok()
.flatten()
.map(|note| note.project_id);
.flatten();
let mcp_project_id = project_note.as_ref().map(|note| note.project_id.clone());
let linked_commit = store.get_commit_by_session(&session_id).ok().flatten();
let linked_note = store.get_note_by_session(&session_id).ok().flatten();
let linked_review = store.get_review_by_session(&session_id).ok().flatten();

// If a branch_id is provided, look up the branch to get workspace_name
// and resolve remote_working_dir. This takes priority over the
Expand All @@ -220,34 +223,61 @@ pub async fn resume_session(
.as_deref()
.and_then(|bid| store.get_branch(bid).ok().flatten());

// If this session is linked to a commit, capture the current HEAD so we
// can detect new or amended commits when the session completes.
// This applies both when the commit already has a SHA (amend case) and
// when it's still pending (no SHA — the previous run didn't produce a
// commit, so we need to detect if this resumed run does).
let (pre_head_sha, workspace_name) = {
// Determine the branch to use: explicit branch_id takes priority,
// otherwise fall back to the commit-linked branch.
let branch = if branch_from_id.is_some() {
branch_from_id.clone()
} else {
store
.get_commit_by_session(&session_id)
.ok()
.flatten()
.and_then(|commit| store.get_branch(&commit.branch_id).ok().flatten())
};
let linked_branch = if branch_from_id.is_some() {
branch_from_id.clone()
} else if let Some(commit) = &linked_commit {
store.get_branch(&commit.branch_id).ok().flatten()
} else if let Some(note) = &linked_note {
store.get_branch(&note.branch_id).ok().flatten()
} else if let Some(review) = &linked_review {
store.get_branch(&review.branch_id).ok().flatten()
Comment on lines +230 to +233
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve remote repo path when inferring linked note/review branch

Falling back to linked_note/linked_review here sets linked_branch (and later workspace_name) even when branch_id was not passed to resume_session, but remote_working_dir is still only resolved from branch_from_id later in this function. In the Session Launcher flow (SessionModal opened without a branchId), resuming a remote note/review session will therefore run via workspace ACP with no repo path override, so commands execute in the workspace default directory instead of the branch repo/subpath. This can make resumed actions fail or operate on the wrong repo for multi-repo/subpath workspaces.

Useful? React with 👍 / 👎.

} else {
None
};

let session_type = if project_note.is_some() {
// Project notes and branch notes intentionally share the "note"
// session type because the frontend only needs a single "note work is
// running" signal for project-level activity indicators.
Some("note".to_string())
} else if linked_commit.is_some() {
Some("commit".to_string())
} else if linked_note.is_some() {
Some("note".to_string())
} else if linked_review.is_some() {
Some("review".to_string())
} else {
infer_branch_resume_session_type(&session.prompt).map(str::to_string)
};
let event_branch_id = linked_branch.as_ref().map(|branch| branch.id.clone());
let event_project_id = if let Some(note) = &project_note {
Some(note.project_id.clone())
} else {
linked_branch
.as_ref()
.map(|branch| branch.project_id.clone())
};

if let Some(ref branch) = branch {
// Only resumed commit sessions need a pre-run HEAD snapshot. The
// completion hook ignores non-commit sessions anyway, but keeping this
// narrow makes the intent explicit and avoids unnecessary git lookups.
let (pre_head_sha, workspace_name) = {
if let Some(ref branch) = linked_branch {
let ws_name = branch.workspace_name.clone();
let head = if let Some(ref ws) = ws_name {
let ws = ws.clone();
run_blox_blocking(move || crate::blox::ws_exec(&ws, &["git", "rev-parse", "HEAD"]))
let head = if linked_commit.is_some() {
if let Some(ref ws) = ws_name {
let ws = ws.clone();
run_blox_blocking(move || {
crate::blox::ws_exec(&ws, &["git", "rev-parse", "HEAD"])
})
.await
.map(|s| s.trim().to_string())
.ok()
} else {
crate::git::get_head_sha(&working_dir).ok()
}
} else {
crate::git::get_head_sha(&working_dir).ok()
None
};
(head, ws_name)
} else {
Expand Down Expand Up @@ -299,9 +329,9 @@ pub async fn resume_session(
status: "running".to_string(),
error_message: None,
completion_reason: None,
branch_id: None,
project_id: mcp_project_id.clone(),
session_type: None,
branch_id: event_branch_id,
project_id: event_project_id.or(mcp_project_id.clone()),
session_type,
is_auto_review: false,
},
);
Expand Down Expand Up @@ -338,6 +368,21 @@ pub async fn resume_session(
Ok(())
}

fn infer_branch_resume_session_type(prompt: &str) -> Option<&'static str> {
// Keep these checks aligned with the action prompts built in `prs.rs`.
if prompt.contains("Create a draft pull request for the current branch.")
|| prompt.contains("Create a pull request for the current branch.")
{
Some("pr")
} else if prompt.contains("Push the current branch to the remote using force-with-lease.")
|| prompt.contains("Push the current branch to the remote.")
{
Some("push")
} else {
None
}
}

#[tauri::command]
pub fn cancel_session(
registry: tauri::State<'_, Arc<session_runner::SessionRegistry>>,
Expand Down Expand Up @@ -2313,6 +2358,40 @@ Rules:
mod tests {
use super::*;

#[test]
fn infer_branch_resume_session_type_detects_pr_prompts() {
assert_eq!(
infer_branch_resume_session_type("Create a pull request for the current branch."),
Some("pr")
);
assert_eq!(
infer_branch_resume_session_type("Create a draft pull request for the current branch."),
Some("pr")
);
}

#[test]
fn infer_branch_resume_session_type_detects_push_prompts() {
assert_eq!(
infer_branch_resume_session_type("Push the current branch to the remote."),
Some("push")
);
assert_eq!(
infer_branch_resume_session_type(
"Push the current branch to the remote using force-with-lease."
),
Some("push")
);
}

#[test]
fn infer_branch_resume_session_type_ignores_other_prompts() {
assert_eq!(
infer_branch_resume_session_type("Write a project note."),
None
);
}

#[test]
fn review_prompt_requires_strict_fence_lines() {
let prompt = build_full_prompt(
Expand Down