Skip to content
Merged
Show file tree
Hide file tree
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
1 change: 1 addition & 0 deletions apps/staged/src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1824,6 +1824,7 @@ pub fn run() {
prs::has_unpushed_commits,
prs::push_branch,
prs::clear_branch_pr_status,
prs::recover_branch_pr,
// Utilities
util_commands::open_url,
util_commands::is_sq_available,
Expand Down
89 changes: 89 additions & 0 deletions apps/staged/src-tauri/src/prs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,17 @@ This is critical - the application parses this to link the PR.
None
};

// Emit "running" event *before* returning so the global session listener
// registers this session atomically — avoiding the race where the session
// completes before the frontend `.then()` callback fires.
session_runner::emit_session_running(
&app_handle,
&session.id,
&branch_id,
&branch.project_id,
"pr",
);
Comment on lines +159 to +165
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Emit running status only after session start succeeds

This emits a running session event before start_session(...) can fail, so if startup fails (for example provider/driver initialization errors), the command returns an error but the frontend has already registered the session as running via the global listener and may never receive a terminal event to clean it up. That leaves project/session UI stuck in a running state for PR creation (and the same ordering is repeated in the push path below).

Useful? React with 👍 / 👎.


session_runner::start_session(
session_runner::SessionConfig {
session_id: session.id.clone(),
Expand Down Expand Up @@ -422,6 +433,73 @@ pub fn clear_branch_pr_status(
Ok(())
}

/// Look up an existing open PR for a branch on GitHub and persist it.
///
/// Called on component mount when `branch.prNumber` is null but the branch has
/// been pushed. Runs `gh pr view <branch>` in the background so the frontend
/// is not blocked. Returns the recovered PR number, or None if no PR exists.
#[tauri::command(rename_all = "camelCase")]
pub async fn recover_branch_pr(
store: tauri::State<'_, Mutex<Option<Arc<Store>>>>,
branch_id: String,
) -> Result<Option<u64>, String> {
let store = get_store(&store)?;

let branch = store
.get_branch(&branch_id)
.map_err(|e| e.to_string())?
.ok_or_else(|| format!("Branch not found: {branch_id}"))?;

// If the branch already has a PR number, nothing to recover
if branch.pr_number.is_some() {
return Ok(branch.pr_number);
}

let project = store
.get_project(&branch.project_id)
.map_err(|e| e.to_string())?
.ok_or_else(|| format!("Project not found: {}", branch.project_id))?;

let is_remote = branch.branch_type == store::BranchType::Remote;
let branch_name = branch.branch_name.clone();

let (repo_slug, _) = resolve_branch_repo_and_subpath(&store, &project, &branch)?;

let working_dir = if is_remote {
crate::paths::repos_dir()
.map(|d| d.join(&repo_slug))
.ok_or_else(|| "Cannot determine clone path for remote branch".to_string())?
} else {
let workdir = store
.get_workdir_for_branch(&branch_id)
.map_err(|e| e.to_string())?
.ok_or_else(|| format!("No worktree for branch: {branch_id}"))?;
PathBuf::from(&workdir.path)
};

let pr_info = tauri::async_runtime::spawn_blocking(move || {
git::get_pr_for_branch(&working_dir, &branch_name)
})
.await
.map_err(|e| format!("recover_branch_pr task failed: {e}"))?
.map_err(|e| e.to_string())?;

if let Some(ref info) = pr_info {
let pr_number = info.number;
store
.update_branch_pr_number(&branch_id, Some(pr_number))
.map_err(|e| e.to_string())?;
log::info!(
"recover_branch_pr: recovered PR #{} for branch_id={}",
pr_number,
branch_id
);
Ok(Some(pr_number))
} else {
Ok(None)
}
}

/// Check if a branch has commits that haven't been pushed to the remote.
#[tauri::command(rename_all = "camelCase")]
pub async fn has_unpushed_commits(
Expand Down Expand Up @@ -586,6 +664,17 @@ The push must succeed before you finish (unless you output the non-fast-forward
None
};

// Emit "running" event *before* returning so the global session listener
// registers this session atomically — avoiding the race where the session
// completes before the frontend `.then()` callback fires.
session_runner::emit_session_running(
&app_handle,
&session.id,
&branch_id,
&branch.project_id,
"push",
);

session_runner::start_session(
session_runner::SessionConfig {
session_id: session.id.clone(),
Expand Down
13 changes: 13 additions & 0 deletions apps/staged/src/App.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@
import { initBloxEnv } from './lib/stores/bloxEnv.svelte';
import { listenForSessionStatus } from './lib/listeners/sessionStatusListener';
import { darkMode } from './lib/stores/isDark.svelte';
import * as prPollingService from './lib/services/prPollingService';
import { projectsList } from './lib/features/projects/projectsSidebarState.svelte';
import type { StoreIncompatibility } from './lib/types';

const updaterEnabled = import.meta.env.VITE_UPDATER_ENABLED === 'true';
Expand All @@ -62,6 +64,17 @@
let resetting = $state(false);
let storeError = $state<string | null>(null);

// =========================================================================
// App-wide PR polling — sync project list and selected project reactively
// =========================================================================
$effect(() => {
prPollingService.setProjects(projectsList.current.map((p) => p.id));
});

$effect(() => {
prPollingService.setSelectedProject(navigation.selectedProjectId);
});

// Konami code: ↑↑↓↓←→←→BA
const konamiSequence = [
'ArrowUp',
Expand Down
6 changes: 6 additions & 0 deletions apps/staged/src/lib/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -852,6 +852,12 @@ export function updateBranchPr(branchId: string, prNumber: number | null): Promi
return invoke('update_branch_pr', { branchId, prNumber });
}

/** Look up an existing open PR for a branch on GitHub and persist it.
* Returns the recovered PR number, or null if no PR exists. */
export function recoverBranchPr(branchId: string): Promise<number | null> {
return invoke('recover_branch_pr', { branchId });
}

/** Check whether a branch has local commits not yet pushed to the remote. */
export function hasUnpushedCommits(branchId: string): Promise<boolean> {
return invoke('has_unpushed_commits', { branchId });
Expand Down
Loading