Skip to content

Commit 3ccfb32

Browse files
committed
show branch checks in status views
Status check data was coupled to pull request summaries, leaving branches without a pull request unable to populate sidebar and dashboard check indicators. This made check visibility depend on PR creation rather than the branch commit itself. Query branch refs alongside pull requests in a batched GraphQL request and carry checks through dedicated caches and status fields. Prefer PR checks when available, then fall back to the remote branch tip. Main and master branches show failures while successful and pending checks stay hidden. Keep daemon polling bounded to one batched status request per active repository every 30 seconds, with an immediate refresh only when its branch set changes. Group linked worktrees by their shared git directory and cache repository resolution once per process to avoid duplicate GitHub calls. Render the same checks in sidebar and dashboard views, and include the indicator in the default tile and horizontal templates. Local-only and unpushed branches leave the check indicator empty.
1 parent bc0f153 commit 3ccfb32

20 files changed

Lines changed: 956 additions & 194 deletions

File tree

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,11 @@ description: Release notes and version history for workmux
2222

2323
# Changelog
2424

25+
## Unreleased
26+
27+
- Show GitHub checks for remote branches without pull requests in the sidebar and dashboard, with check indicators in the default tile and horizontal sidebar layouts and failure-only indicators on main branches.
28+
- Batch sidebar GitHub polling across linked worktrees and reuse repository metadata so each repository stays within one status request per polling interval.
29+
2530
## v0.1.231 (2026-07-28)
2631

2732
- Clear acknowledged waiting or completed statuses from the sidebar immediately when you focus an agent pane.

docs/guide/dashboard/index.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,8 @@ See [command reference](/reference/commands/dashboard) for CLI options.
4040

4141
The dashboard has two views, toggled with `Tab`:
4242

43-
- **Agents**: Shows all running agent panes with their status, git info, and live terminal preview
44-
- **Worktrees**: Shows all git worktrees with branch, PR status, and agent summary. Press `r` to remove a worktree (kills agent, removes worktree, deletes branch).
43+
- **Agents**: Shows all running agent panes with their status, git info, GitHub branch checks, and live terminal preview
44+
- **Worktrees**: Shows all git worktrees with branch, PR status, GitHub branch checks, and agent summary. Press `r` to remove a worktree (kills agent, removes worktree, deletes branch).
4545

4646
## Keybindings (Agents view)
4747

docs/guide/sidebar/customization.md

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,13 @@ sidebar:
1717
tiles:
1818
- "{primary} {pane_suffix} {fill} {elapsed}"
1919
- "{secondary} {fill} {git_stats}"
20-
- "{pane_title}"
20+
- "{pane_title} {fill} {pr_checks}"
2121

2222
# Horizontal mode: one string per visual line in each top bar chip.
2323
horizontal:
2424
- "{status_icon} {primary} {pane_suffix} {fill} {elapsed}"
2525
- "{secondary} {fill} {git_stats}"
26-
- "{pane_title}"
26+
- "{pane_title} {fill} {pr_checks}"
2727
```
2828
2929
The values shown above are also the built-in defaults, so leaving these keys
@@ -53,7 +53,7 @@ sidebars without a restart.
5353
| `{git_stats}` | Composite git diff stats: committed (`+1278 -400`), pen icon, uncommitted (`+21`). Self-degrades to fit the space it gets. |
5454
| `{git_branch}` | Current branch name. Empty when detached HEAD or git status unavailable. |
5555
| `{pr_number}` | Pull request number as `#123`. Empty when the current branch has no matching PR. |
56-
| `{pr_checks}` | Pull request check status icon, with counts when space allows. Empty when no PR or no checks are known. |
56+
| `{pr_checks}` | GitHub check status for the current remote branch, with counts when space allows. Empty when no checks are known. |
5757
| `{git_ahead}` | Commits ahead of upstream as `↑N` when N greater than 0. Empty when 0 or no upstream. |
5858
| `{git_behind}` | Commits behind upstream as `↓N` when N greater than 0. Empty when 0 or no upstream. |
5959
| `{git_dirty}` | Diff glyph when the working tree is dirty. Empty when clean. |
@@ -68,11 +68,15 @@ wrap them with another `↑` / `↓` literal in your template, otherwise a stray
6868
glyph would remain when the count is zero. The same applies to `{git_dirty}`
6969
and `{git_conflict}`, which are self-contained glyph indicators.
7070

71-
`{pr_number}` and `{pr_checks}` are populated from the current git branch's
72-
matching GitHub pull request. They render empty on `main`/`master`, when the
73-
branch has no PR, when git status is unavailable, or while PR data has not been
74-
fetched yet. Check status shows an icon and, when space allows, passed/total
75-
counts for pending or failing checks.
71+
`{pr_number}` is populated from the current git branch's matching GitHub pull
72+
request. It renders empty on `main`/`master`, when the branch has no PR, when git
73+
status is unavailable, or while PR data has not been fetched yet.
74+
75+
`{pr_checks}` is populated from the matching pull request's head commit when a PR
76+
exists, then from the remote branch tip when the branch has no PR check data.
77+
On `main` and `master`, only failing checks render. Local-only branches and
78+
commits that have not been pushed render empty. Check status shows an icon and,
79+
when space allows, passed/total counts for pending or failing checks.
7680

7781
Unknown tokens or unbalanced braces cause that template section to be rejected
7882
and the previous valid section (or the built-in default) is kept. The sidebar
@@ -127,7 +131,7 @@ sidebar:
127131
tiles:
128132
- "#[fg=cyan,bold]{primary}#[default] {pane_suffix} {fill} {elapsed}"
129133
- "{secondary} {fill} #[fg=green]{git_stats}#[default]"
130-
- "{pane_title}"
134+
- "{pane_title} {fill} {pr_checks}"
131135
```
132136

133137
Supported attributes are `fg=`, `bg=`, `bold`, `dim`, `italics`,
@@ -259,7 +263,7 @@ sidebar:
259263
tiles:
260264
- "{agent_icon} {primary} {pane_suffix} {fill} {elapsed}"
261265
- "{secondary} {fill} {git_stats}"
262-
- "{pane_title}"
266+
- "{pane_title} {fill} {pr_checks}"
263267
```
264268

265269
Configure only two tile lines and show git stats inline on line one:
@@ -272,7 +276,7 @@ sidebar:
272276
- "{secondary} {fill} {elapsed}"
273277
```
274278

275-
Show pull request number and check status in tile mode:
279+
Show pull request number and GitHub check status in tile mode:
276280

277281
```yaml
278282
sidebar:

src/command/dashboard/app/agents.rs

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ use std::time::{SystemTime, UNIX_EPOCH};
77

88
use ratatui::style::Style;
99

10-
use crate::github::PrSummary;
10+
use crate::github::{CheckSummary, PrSummary};
1111
use crate::multiplexer::{AgentPane, AgentStatus};
1212

1313
use super::DashboardTab;
@@ -496,23 +496,54 @@ impl App {
496496
self.pr_statuses.get(repo_root)?.get(branch)
497497
}
498498

499+
pub fn get_checks_for_agent(&self, agent: &AgentPane) -> Option<&CheckSummary> {
500+
let repo_root = self.repo_roots.get(&agent.path)?;
501+
let branch = self.git_statuses.get(&agent.path)?.branch.as_ref()?;
502+
self.check_statuses
503+
.get(repo_root)?
504+
.get(branch)
505+
.filter(|summary| summary.should_display_for_branch(branch))
506+
}
507+
508+
pub fn get_checks_for_worktree(
509+
&self,
510+
worktree: &crate::workflow::types::WorktreeInfo,
511+
) -> Option<&CheckSummary> {
512+
let project = agent::extract_project_name(&worktree.path);
513+
let repo_root = self
514+
.all_worktrees
515+
.iter()
516+
.find(|candidate| {
517+
candidate.is_main && agent::extract_project_name(&candidate.path) == project
518+
})
519+
.map(|candidate| &candidate.path)?;
520+
self.check_statuses
521+
.get(repo_root)?
522+
.get(&worktree.branch)
523+
.filter(|summary| summary.should_display_for_branch(&worktree.branch))
524+
}
525+
499526
/// Whether a PR fetch is currently in progress
500527
pub fn is_pr_fetching(&self) -> bool {
501528
self.is_pr_fetching.load(Ordering::Relaxed)
502529
}
503530

504-
/// Whether any agent has a matching PR (for column visibility)
505-
pub fn has_any_pr(&self) -> bool {
506-
self.agents
507-
.iter()
508-
.any(|agent| self.get_pr_for_agent(agent).is_some())
531+
/// Whether any agent has a matching PR or GitHub checks.
532+
pub fn has_any_github_status(&self) -> bool {
533+
self.agents.iter().any(|agent| {
534+
self.get_pr_for_agent(agent).is_some() || self.get_checks_for_agent(agent).is_some()
535+
})
509536
}
510537

511538
/// Get PR statuses for caching
512539
pub fn pr_statuses(&self) -> &HashMap<PathBuf, HashMap<String, PrSummary>> {
513540
&self.pr_statuses
514541
}
515542

543+
pub fn check_statuses(&self) -> &HashMap<PathBuf, HashMap<String, CheckSummary>> {
544+
&self.check_statuses
545+
}
546+
516547
/// Open the PR associated with the selected agent or worktree in the browser.
517548
pub fn open_pr_for_selected(&mut self) {
518549
self.open_pr_url(|url| url.to_string());

src/command/dashboard/app/background.rs

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
//! Background thread spawning for git status and PR status fetches.
1+
//! Background thread spawning for git and GitHub status fetches.
22
33
use std::collections::{HashMap, VecDeque};
44
use std::path::PathBuf;
@@ -51,7 +51,7 @@ impl App {
5151
});
5252
}
5353

54-
/// Spawn a background thread to fetch PR status for all repos.
54+
/// Spawn a background thread to fetch GitHub status for all repos.
5555
/// Returns true if a fetch was started, false if one is already in progress.
5656
pub(super) fn spawn_pr_status_fetch(&self) -> bool {
5757
// Skip if already fetching
@@ -74,9 +74,6 @@ impl App {
7474
let Some(ref branch) = status.branch else {
7575
continue;
7676
};
77-
if branch == "main" || branch == "master" {
78-
continue;
79-
}
8077
if let Some(repo_root) = self.repo_roots.get(&agent.path) {
8178
repo_branches
8279
.entry(repo_root.clone())
@@ -97,9 +94,6 @@ impl App {
9794
})
9895
.collect();
9996
for wt in &self.all_worktrees {
100-
if wt.is_main || wt.branch == "main" || wt.branch == "master" {
101-
continue;
102-
}
10397
let project = agent::extract_project_name(&wt.path);
10498
if let Some(repo_root) = main_paths.get(&project) {
10599
repo_branches
@@ -168,13 +162,13 @@ impl App {
168162
else {
169163
break;
170164
};
171-
match crate::github::list_prs_for_branches(&repo_root, &branches) {
172-
Ok(prs) => {
173-
let _ = tx.send(AppEvent::PrStatus(repo_root, prs));
165+
match crate::github::list_branch_summaries(&repo_root, &branches) {
166+
Ok(summaries) => {
167+
let _ = tx.send(AppEvent::GithubStatus(repo_root, summaries));
174168
}
175169
Err(e) => {
176170
tracing::warn!(
177-
"Failed to fetch PRs for {:?}: {}",
171+
"Failed to fetch GitHub status for {:?}: {}",
178172
repo_root,
179173
e
180174
);

src/command/dashboard/app/events.rs

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,27 @@ impl App {
1212
AppEvent::GitStatus(path, status) => {
1313
self.git_statuses.insert(path, status);
1414
}
15-
AppEvent::PrStatus(repo_root, prs) => {
16-
// Clear stale state when a repo has no PRs, otherwise update
15+
AppEvent::GithubStatus(repo_root, summaries) => {
16+
let mut prs = std::collections::HashMap::new();
17+
let mut checks = std::collections::HashMap::new();
18+
for (branch, summary) in summaries {
19+
if let Some(pr) = summary.pr {
20+
prs.insert(branch.clone(), pr);
21+
}
22+
if let Some(check_summary) = summary.checks {
23+
checks.insert(branch, check_summary);
24+
}
25+
}
1726
if prs.is_empty() {
1827
self.pr_statuses.remove(&repo_root);
1928
} else {
20-
self.pr_statuses.insert(repo_root, prs);
29+
self.pr_statuses.insert(repo_root.clone(), prs);
30+
}
31+
if checks.is_empty() {
32+
self.check_statuses.remove(&repo_root);
33+
} else {
34+
self.check_statuses.insert(repo_root, checks);
2135
}
22-
// Re-apply worktree filters to merge new PR data
2336
if !self.all_worktrees.is_empty() {
2437
self.apply_worktree_filters();
2538
}

src/command/dashboard/app/mod.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ use std::time::{Duration, SystemTime, UNIX_EPOCH};
2020

2121
use crate::config::Config;
2222
use crate::git::{self, GitStatus};
23-
use crate::github::PrSummary;
23+
use crate::github::{CheckSummary, PrSummary};
2424
use crate::multiplexer::{AgentPane, Multiplexer};
2525
use crate::state::StateStore;
2626
use crate::workflow::types::WorktreeInfo;
@@ -73,6 +73,8 @@ pub struct App {
7373
pub is_git_fetching: Arc<AtomicBool>,
7474
/// PR info indexed by repo root, then branch name
7575
pr_statuses: HashMap<PathBuf, HashMap<String, PrSummary>>,
76+
/// GitHub check info indexed by repo root, then branch name
77+
check_statuses: HashMap<PathBuf, HashMap<String, CheckSummary>>,
7678
/// Last PR fetch time
7779
last_pr_fetch: std::time::Instant,
7880
/// Flag to prevent concurrent PR fetches
@@ -198,6 +200,7 @@ impl App {
198200
let launch_session = mux.current_session();
199201
let git_statuses = git::load_status_cache();
200202
let pr_statuses = crate::github::load_pr_cache();
203+
let check_statuses = crate::github::load_check_cache();
201204
let hide_stale = load_hide_stale();
202205
let last_pane_id = load_last_pane_id();
203206

@@ -225,6 +228,7 @@ impl App {
225228
last_git_fetch: std::time::Instant::now() - Duration::from_secs(60),
226229
is_git_fetching: Arc::new(AtomicBool::new(false)),
227230
pr_statuses,
231+
check_statuses,
228232
// Set to past to trigger immediate fetch on first refresh
229233
last_pr_fetch: std::time::Instant::now() - PR_FETCH_INTERVAL,
230234
is_pr_fetching: Arc::new(AtomicBool::new(false)),

src/command/dashboard/app/types.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use std::collections::{HashMap, HashSet};
44
use std::path::PathBuf;
55

66
use crate::git::GitStatus;
7-
use crate::github::{PrListEntry, PrSummary};
7+
use crate::github::{BranchSummary, PrListEntry};
88
use crate::workflow::types::WorktreeInfo;
99

1010
use super::super::diff::DiffView;
@@ -16,8 +16,8 @@ pub enum AppEvent {
1616
Terminal(crossterm::event::Event),
1717
/// Git status update for a worktree path
1818
GitStatus(PathBuf, GitStatus),
19-
/// PR status update for a repo root
20-
PrStatus(PathBuf, HashMap<String, PrSummary>),
19+
/// GitHub status update for a repo root
20+
GithubStatus(PathBuf, HashMap<String, BranchSummary>),
2121
/// Full worktree list from background fetch
2222
WorktreeList(Vec<WorktreeInfo>),
2323
/// Git log preview for a worktree path

src/command/dashboard/mod.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -232,8 +232,9 @@ pub fn run(
232232
// Save git status cache before exiting
233233
git::save_status_cache(&app.git_statuses);
234234

235-
// Save PR status cache before exiting
235+
// Save GitHub status caches before exiting
236236
github::save_pr_cache(app.pr_statuses());
237+
github::save_check_cache(app.check_statuses());
237238

238239
// Restore terminal
239240
disable_raw_mode()?;

src/command/dashboard/ui/dashboard.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -149,8 +149,8 @@ pub fn render_dashboard(f: &mut Frame, app: &mut App) {
149149
}
150150

151151
fn render_table(f: &mut Frame, app: &mut App, area: Rect) {
152-
// Check if we should show the PR column (only when at least one agent has a PR)
153-
let show_pr_column = app.has_any_pr();
152+
// Show the GitHub column when at least one agent has a PR or checks.
153+
let show_pr_column = app.has_any_github_status();
154154
let show_check_counts = app.config.dashboard.show_check_counts();
155155

156156
let header = format::resource_table_header(
@@ -249,8 +249,10 @@ fn render_table(f: &mut Frame, app: &mut App, area: Rect) {
249249
// Get PR status for this agent (only if column is shown)
250250
let pr_spans = if show_pr_column {
251251
let pr = app.get_pr_for_agent(agent);
252+
let checks = app.get_checks_for_agent(agent);
252253
Some(format_pr_status(
253254
pr,
255+
checks,
254256
show_check_counts,
255257
app.spinner_frame,
256258
&app.palette,

0 commit comments

Comments
 (0)