Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
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
2 changes: 1 addition & 1 deletion apps/staged/src-tauri/src/git/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
mod cli;
pub(crate) mod cli;
mod commit;
mod diff;
mod files;
Expand Down
148 changes: 141 additions & 7 deletions apps/staged/src-tauri/src/prs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,90 @@ struct PrStatusEvent {
pr_head_sha: Option<String>,
}

/// Maximum character length for the full diff output before truncation.
const DIFF_TRUNCATION_LIMIT: usize = 50_000;

struct GitContext {
log: String,
stat: String,
diff: String,
}

/// Run the three deterministic git analysis commands in parallel and return
/// their output. Returns `None` if any command fails so the caller can fall
/// back to letting the agent run them itself.
fn pre_compute_git_context(
is_remote: bool,
working_dir: &Path,
workspace_name: Option<&str>,
store: &Arc<Store>,
branch: &store::Branch,
base_branch: &str,
) -> Option<GitContext> {
let log_range = format!("origin/{}..HEAD", base_branch);
let diff_range = format!("origin/{}...HEAD", base_branch);

if is_remote {
let ws_name = workspace_name?;
let repo_subpath = crate::branches::resolve_branch_workspace_subpath(store, branch)
.ok()
.flatten();
let sp = repo_subpath.as_deref();

// For remote branches, run_workspace_git goes over SSH and cannot
// benefit from std::thread::scope parallelism (the SSH transport
// serialises anyway), so run them sequentially.
let log_output =
crate::branches::run_workspace_git(ws_name, sp, &["log", "--oneline", &log_range])
.ok()?;
let stat_output =
crate::branches::run_workspace_git(ws_name, sp, &["diff", &diff_range, "--stat"])
.ok()?;
let diff_output =
crate::branches::run_workspace_git(ws_name, sp, &["diff", &diff_range]).ok()?;

Some(GitContext {
log: log_output,
stat: stat_output,
diff: truncate_diff(diff_output),
})
} else {
let (log_result, stat_result, diff_result) = std::thread::scope(|s| {
let log = s.spawn(|| git::cli::run(working_dir, &["log", "--oneline", &log_range]));
let stat = s.spawn(|| git::cli::run(working_dir, &["diff", &diff_range, "--stat"]));
let diff = s.spawn(|| git::cli::run(working_dir, &["diff", &diff_range]));
(
log.join().unwrap(),
stat.join().unwrap(),
diff.join().unwrap(),
)
});

Some(GitContext {
log: log_result.ok()?,
stat: stat_result.ok()?,
diff: truncate_diff(diff_result.ok()?),
})
}
}

/// Truncate a diff to `DIFF_TRUNCATION_LIMIT` characters, appending a note
/// about the omitted content.
fn truncate_diff(diff: String) -> String {
if diff.len() <= DIFF_TRUNCATION_LIMIT {
return diff;
}
let truncated = &diff[..DIFF_TRUNCATION_LIMIT];
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 Avoid slicing diff at non-UTF-8 character boundaries

truncate_diff indexes the string with a raw byte offset (&diff[..DIFF_TRUNCATION_LIMIT]). If a large diff contains multibyte UTF-8 text, hitting the 50,000-byte cutoff in the middle of a character will panic, which can crash create_pr instead of gracefully falling back. This is reproducible when the diff exceeds the limit and includes non-ASCII content (for example in source strings/comments or filenames), so truncation needs to find a valid character boundary before slicing.

Useful? React with 👍 / 👎.

// Try to cut at a newline boundary for cleaner output.
let cut = truncated.rfind('\n').unwrap_or(DIFF_TRUNCATION_LIMIT);
let remaining_lines = diff[cut..].lines().count();
format!(
"{}\n\n(truncated, ~{} more lines — run the command yourself to see the full diff)",
&diff[..cut],
remaining_lines,
)
}

/// Create a pull request for a branch by kicking off an agent session.
#[tauri::command(rename_all = "camelCase")]
pub fn create_pr(
Expand Down Expand Up @@ -105,8 +189,57 @@ pub fn create_pr(
"pull request"
};

let prompt = format!(
r#"<action>
// Pre-compute git context in parallel so the agent can skip straight to
// pushing and creating the PR instead of running these deterministic
// commands itself.
let git_context = pre_compute_git_context(
is_remote,
&working_dir,
workspace_name.as_deref(),
&store,
&branch,
base_branch,
);

let prompt = if let Some(ctx) = git_context {
format!(
r#"<action>
Create a {pr_type} for the current branch.

The initial analysis has already been done:

$ git log --oneline origin/{base_branch}..HEAD
{log_output}

$ git diff origin/{base_branch}...HEAD --stat
{stat_output}

$ git diff origin/{base_branch}...HEAD
{diff_output}

Steps:
1. Push the current branch to the remote: `git push -u origin {branch_name}`
2. Create a PR using the GitHub CLI: `gh pr create --base {base_branch} --fill-first{draft_flag}`
- Title MUST use conventional commit style (e.g., "feat: add user authentication", "fix: resolve null pointer in parser", "refactor: extract validation logic")
- Choose the most appropriate conventional commit type (feat, fix, refactor, docs, style, test, chore, perf, ci, build) based on the actual changes
- The body should be a concise summary of the changes

IMPORTANT: After creating the PR, you MUST output the PR URL on its own line in this exact format:
PR_URL: https://github.com/...

This is critical - the application parses this to link the PR.
</action>"#,
pr_type = pr_type,
base_branch = base_branch,
branch_name = branch.branch_name,
draft_flag = draft_flag,
log_output = ctx.log,
stat_output = ctx.stat,
diff_output = ctx.diff,
)
} else {
format!(
r#"<action>
Create a {pr_type} for the current branch.

Steps:
Expand All @@ -122,11 +255,12 @@ PR_URL: https://github.com/...

This is critical - the application parses this to link the PR.
</action>"#,
pr_type = pr_type,
base_branch = base_branch,
branch_name = branch.branch_name,
draft_flag = draft_flag,
);
pr_type = pr_type,
base_branch = base_branch,
branch_name = branch.branch_name,
draft_flag = draft_flag,
)
};

let mut session = store::Session::new_running(&prompt, &working_dir);
if let Some(ref p) = provider {
Expand Down