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
6 changes: 4 additions & 2 deletions Panel.qml
Original file line number Diff line number Diff line change
Expand Up @@ -379,7 +379,9 @@ Panel {

PanelSectionHeader {
width: parent.width
text: "OWNED REPOSITORIES " + root.displayedRepositories.length + "/" + github.repositories.length
// Driven by the fetched scope, not the setting, so it cannot claim
// to list organization repositories before a refresh brings them in.
text: (github.fetchedRepositoryScope === "owned" ? "OWNED REPOSITORIES " : "REPOSITORIES ") + root.displayedRepositories.length + "/" + github.repositories.length
foreground: root.foreground
fontFamily: root.fontFamily
}
Expand Down Expand Up @@ -450,7 +452,7 @@ Panel {
Text {
visible: root.displayedRepositories.length === 0
width: parent.width
text: github.repositories.length === 0 ? "No owned repositories loaded." : "No repositories match these filters."
text: github.repositories.length === 0 ? "No repositories loaded." : "No repositories match these filters."
color: root.dim
font.family: root.fontFamily
font.pixelSize: Style.font.body
Expand Down
50 changes: 35 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ The dashboard is ordered by urgency so the most actionable work appears first:
- **Assigned issues** — keep track of open issues assigned to you
- **Active GitHub Actions** — monitor queued, pending, requested, waiting, and running workflows
- **Recent workflow failures** — jump directly to failed, timed-out, or action-required runs
- **Owned repositories** — browse every repository you own with open issue, open PR, star, and active workflow counts
- **Repositories** — browse the repositories you own, and optionally those you reach through an organization, with open issue, open PR, star, and active workflow counts

Repository search, metric filters, and sorting make even large GitHub accounts manageable. Filter to repositories with issues, PRs, stars, or active Actions, then sort by the metric that matters.

Expand Down Expand Up @@ -118,7 +118,7 @@ The notifications footer also carries **Mark all read**. The first click capture

## Repository dashboard

Every owned repository includes:
Every listed repository includes:

- Open issue count
- Open pull request count
Expand All @@ -130,20 +130,40 @@ Use the filter chips to show all repositories or only repositories with a non-ze

## Settings

Configure the widget through Omarchy's bar widget settings:
Configure the widget through Omarchy's bar widget settings. Existing installations retain the narrower repository scope and bounded Actions scan:

- Refresh interval
- Include archived repositories
- Include forks
- Include review requests and issues from archived repositories
- Include review requests on drafts
- Maximum displayed repositories
- Actions scan mode: off, recent repositories, or all repositories
- Number of recent repositories to scan
- Actions request concurrency
- Failed Actions time window and maximum result count
| Setting | Default |
| --- | --- |
| Refresh interval | 900 seconds (15 minutes) |
| Include archived repositories | Off |
| Include forks | Off |
| Repository scope | **Owned** |
| Include review requests and issues from archived repositories | Off |
| Include review requests on drafts | Off |
| Maximum displayed repositories | 25 |
| Actions scan | **Recent repositories** |
| Recent repository scan limit | 15 |
| Actions request concurrency | 6 |
| Failed Actions window | 7 days |
| Maximum failed Actions | 20 |

**Repository scope** controls both the repository dashboard and the candidate repositories for Actions scanning. **Owned and organizations** is opt-in. With the default **Recent repositories** scan, Actions requests remain capped to the 15 most recently updated repositories in that wider scope.

**All repositories** is also opt-in and starts six paginated Actions request streams per repository on every refresh. Combining it with **Owned and organizations** can consume substantial GitHub API capacity in large organizations. Use **Recent repositories** or **Off** for a bounded scan, and increase the refresh interval when broader monitoring is required.

Set these options from the command line after installing the plugin:

```bash
omarchy bar set robzolkos.github repositoryScope "Owned and organizations"
omarchy bar set robzolkos.github actionScanBehavior "Recent repositories"
```

Restore the narrowest behavior with:

The defaults deliberately balance freshness and GitHub API usage. Accounts that need exhaustive workflow monitoring can select **All repositories**.
```bash
omarchy bar set robzolkos.github repositoryScope "Owned"
omarchy bar set robzolkos.github actionScanBehavior "Off"
```

Review requests and assigned issues from archived repositories are hidden by default because archived repositories are read-only. Review requests on draft pull requests are also hidden by default, while teams that use drafts for early feedback can include them. Each behavior has its own setting.

Expand All @@ -170,7 +190,7 @@ The shell watches local plugin files, making QML iteration fast.

`Service.qml` schedules an executable helper, `omarchy-github-fetch`, which calls GitHub exclusively through `gh api` and processes responses with `jq`.

- GraphQL retrieves every owned repository and exact aggregate counts.
- GraphQL retrieves every repository in the configured scope and exact aggregate counts.
- REST retrieves notifications and workflow runs.
- GitHub issue search retrieves review requests and assigned issues.
- GraphQL search retrieves your authored pull requests together with the head commit's `statusCheckRollup`, so check state costs no extra request.
Expand Down
13 changes: 11 additions & 2 deletions Service.qml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ Item {
property string state: "loading"
property string message: "Loading GitHub…"
property string login: ""
property string fetchedRepositoryScope: "owned"
property string fetchedAt: ""
property var notifications: []
property int notificationsRevision: 0
Expand Down Expand Up @@ -83,12 +84,19 @@ Item {
return text === "true" || text === "yes" || text === "on" || text === "1";
}

// Matched against the known options rather than by substring, so an option
// added later falls back to the narrower scope instead of silently widening
// it. `fetchedRepositoryScope` reports what the last payload contained.
function repositoryMode() {
return String(setting("repositoryScope", "Owned")).toLowerCase() === "owned and organizations" ? "organizations" : "owned";
}

function actionMode() {
var value = String(setting("actionScanBehavior", "Recent repositories")).toLowerCase();
if (value === "off")
return "off";

if (value.indexOf("all") === 0)
if (value === "all repositories")
return "all";

return "recent";
Expand All @@ -99,7 +107,7 @@ Item {
}

function command() {
return [helperPath(), "--include-archived", boolSetting("includeArchived", false) ? "true" : "false", "--include-forks", boolSetting("includeForks", false) ? "true" : "false", "--include-archived-reviews", boolSetting("includeArchivedReviewRequests", false) ? "true" : "false", "--include-draft-reviews", boolSetting("includeDraftReviewRequests", false) ? "true" : "false", "--action-scan", actionMode(), "--action-repo-limit", String(intSetting("actionScanRepoLimit", 15, 5, 200)), "--concurrency", String(intSetting("actionScanConcurrency", 6, 1, 12)), "--failed-days", String(intSetting("failedActionDays", 7, 1, 30)), "--failed-limit", String(intSetting("failedActionLimit", 20, 1, 100))];
return [helperPath(), "--include-archived", boolSetting("includeArchived", false) ? "true" : "false", "--include-forks", boolSetting("includeForks", false) ? "true" : "false", "--repository-scope", repositoryMode(), "--include-archived-reviews", boolSetting("includeArchivedReviewRequests", false) ? "true" : "false", "--include-draft-reviews", boolSetting("includeDraftReviewRequests", false) ? "true" : "false", "--action-scan", actionMode(), "--action-repo-limit", String(intSetting("actionScanRepoLimit", 15, 5, 200)), "--concurrency", String(intSetting("actionScanConcurrency", 6, 1, 12)), "--failed-days", String(intSetting("failedActionDays", 7, 1, 30)), "--failed-limit", String(intSetting("failedActionLimit", 20, 1, 100))];
}

function refresh() {
Expand All @@ -121,6 +129,7 @@ Item {
state = String(data.state || "error");
message = String(data.message || "");
login = String(data.login || "");
fetchedRepositoryScope = String(data.repositoryScope || "owned");
fetchedAt = String(data.fetchedAt || "");
notifications = Array.isArray(data.notifications) ? data.notifications : [];
notificationsRevision++;
Expand Down
6 changes: 4 additions & 2 deletions manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"version": "0.2.2",
"author": "Rob Zolkos",
"license": "MIT",
"description": "A keyboard-friendly GitHub inbox for notifications, reviews, your own pull requests, assigned issues, Actions, and owned repositories.",
"description": "A keyboard-friendly GitHub inbox for notifications, reviews, your own pull requests, assigned issues, Actions, and repositories.",
"kinds": ["bar-widget"],
"activation": "on-demand",
"entryPoints": {
Expand All @@ -22,6 +22,7 @@
"refreshIntervalSec": 900,
"includeArchived": false,
"includeForks": false,
"repositoryScope": "Owned",
"includeArchivedReviewRequests": false,
"includeDraftReviewRequests": false,
"maxDisplayedRepos": 25,
Expand All @@ -35,10 +36,11 @@
{ "key": "refreshIntervalSec", "type": "integer", "label": "Refresh interval (seconds)", "min": 60, "max": 3600, "step": 60, "defaultValue": 900 },
{ "key": "includeArchived", "type": "boolean", "label": "Include archived repositories", "defaultValue": false },
{ "key": "includeForks", "type": "boolean", "label": "Include forked repositories", "defaultValue": false },
{ "key": "repositoryScope", "type": "enum", "label": "Repository scope", "options": ["Owned", "Owned and organizations"], "defaultValue": "Owned", "description": "List only repositories you own, or include organization repositories. This scope also supplies candidates for Actions scanning." },
{ "key": "includeArchivedReviewRequests", "type": "boolean", "label": "Include review requests and issues from archived repositories", "defaultValue": false, "description": "An archived repository is read-only, so the request cannot be withdrawn and the work cannot be done." },
{ "key": "includeDraftReviewRequests", "type": "boolean", "label": "Include review requests on drafts", "defaultValue": false, "description": "Draft pull requests are not ready for review, but some teams do ask for early feedback on them." },
{ "key": "maxDisplayedRepos", "type": "integer", "label": "Maximum displayed repositories", "min": 10, "max": 500, "step": 5, "defaultValue": 25, "description": "Caps rendered repository rows while search and filtering continue to use the complete fetched list." },
{ "key": "actionScanBehavior", "type": "enum", "label": "Actions scan", "options": ["Off", "Recent repositories", "All repositories"], "defaultValue": "Recent repositories", "description": "Scan no repositories, the most recently updated repositories, or every owned repository for active and recent failed workflow runs." },
{ "key": "actionScanBehavior", "type": "enum", "label": "Actions scan", "options": ["Off", "Recent repositories", "All repositories"], "defaultValue": "Recent repositories", "description": "Scan no repositories, a capped set of recent repositories, or every repository in the configured scope. All repositories can use substantial API capacity with organization scope." },
{ "key": "actionScanRepoLimit", "type": "integer", "label": "Recent repository scan limit", "min": 5, "max": 200, "step": 5, "defaultValue": 15 },
{ "key": "actionScanConcurrency", "type": "integer", "label": "Actions scan concurrency", "min": 1, "max": 12, "step": 1, "defaultValue": 6 },
{ "key": "failedActionDays", "type": "integer", "label": "Failed Actions window (days)", "min": 1, "max": 30, "step": 1, "defaultValue": 7 },
Expand Down
15 changes: 12 additions & 3 deletions omarchy-github-fetch
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ include_archived=false
include_forks=false
include_archived_reviews=false
include_draft_reviews=false
repository_scope="owned"
action_scan="recent"
action_limit=15
concurrency=6
Expand All @@ -26,6 +27,9 @@ Usage: omarchy-github-fetch [options]
archived repositories (default: false)
--include-draft-reviews BOOL Include review requests on draft pull requests
(default: false)
--repository-scope MODE owned, or organizations to also list the
repositories you reach through an organization
(default: owned)
--action-scan MODE off, recent, or all (default: recent)
--action-repo-limit N Repositories scanned in recent mode (default: 15)
--concurrency N Concurrent Actions requests, 1-12 (default: 6)
Expand All @@ -48,6 +52,7 @@ while (($#)); do
--include-forks) [[ $# -ge 2 ]] || { echo "missing value for $1" >&2; exit 2; }; include_forks=$2; shift 2 ;;
--include-archived-reviews) [[ $# -ge 2 ]] || { echo "missing value for $1" >&2; exit 2; }; include_archived_reviews=$2; shift 2 ;;
--include-draft-reviews) [[ $# -ge 2 ]] || { echo "missing value for $1" >&2; exit 2; }; include_draft_reviews=$2; shift 2 ;;
--repository-scope) [[ $# -ge 2 ]] || { echo "missing value for $1" >&2; exit 2; }; repository_scope=${2,,}; shift 2 ;;
--action-scan) [[ $# -ge 2 ]] || { echo "missing value for $1" >&2; exit 2; }; action_scan=${2,,}; shift 2 ;;
--action-repo-limit) [[ $# -ge 2 ]] || { echo "missing value for $1" >&2; exit 2; }; action_limit=$2; shift 2 ;;
--concurrency) [[ $# -ge 2 ]] || { echo "missing value for $1" >&2; exit 2; }; concurrency=$2; shift 2 ;;
Expand All @@ -66,9 +71,13 @@ integer "$concurrency" && ((concurrency >= 1 && concurrency <= 12)) || { echo "i
integer "$failed_days" && ((failed_days >= 1 && failed_days <= 30)) || { echo "invalid --failed-days" >&2; exit 2; }
integer "$failed_limit" && ((failed_limit >= 1 && failed_limit <= 100)) || { echo "invalid --failed-limit" >&2; exit 2; }
[[ $action_scan == off || $action_scan == recent || $action_scan == all ]] || { echo "invalid --action-scan" >&2; exit 2; }
[[ $repository_scope == owned || $repository_scope == organizations ]] || { echo "invalid --repository-scope" >&2; exit 2; }
# Interpolated into the GraphQL document, so it comes from the validated enum
# above. The argument itself never reaches the query.
if [[ $repository_scope == organizations ]]; then owner_affiliations="[OWNER,ORGANIZATION_MEMBER]"; else owner_affiliations="OWNER"; fi

emit_state() {
jq -n --arg state "$1" --arg message "$2" '{schemaVersion:1,state:$state,message:$message,login:"",fetchedAt:(now|todateiso8601),notifications:[],reviewRequests:[],assignedIssues:[],myPullRequests:[],myPullRequestsTotal:0,actions:[],failedActions:[],repositories:[],warnings:[],rateLimit:null}'
jq -n --arg state "$1" --arg message "$2" --arg scope "$repository_scope" '{schemaVersion:1,state:$state,message:$message,login:"",repositoryScope:$scope,fetchedAt:(now|todateiso8601),notifications:[],reviewRequests:[],assignedIssues:[],myPullRequests:[],myPullRequestsTotal:0,actions:[],failedActions:[],repositories:[],warnings:[],rateLimit:null}'
}

command -v jq >/dev/null 2>&1 || { printf '%s\n' '{"schemaVersion":1,"state":"error","message":"jq is required.","notifications":[],"reviewRequests":[],"assignedIssues":[],"myPullRequests":[],"myPullRequestsTotal":0,"actions":[],"failedActions":[],"repositories":[],"warnings":[]}' ; exit 0; }
Expand Down Expand Up @@ -232,7 +241,7 @@ cursor=null
printf '[]\n' >"$tmp/repos.json"
graphql_ok=true
while :; do
if ! page=$(gh api graphql -f query='query($cursor:String) { viewer { login repositories(first:100,after:$cursor,ownerAffiliations:OWNER,orderBy:{field:UPDATED_AT,direction:DESC}) { nodes { name nameWithOwner url isArchived isFork stargazerCount updatedAt issues(states:OPEN){totalCount} pullRequests(states:OPEN){totalCount} } pageInfo { hasNextPage endCursor } } } rateLimit { remaining resetAt cost } }' -F cursor="$cursor" 2>"$tmp/graphql.err"); then api_error repositories "$tmp/graphql.err"; graphql_ok=false; break; fi
if ! page=$(gh api graphql -f query='query($cursor:String) { viewer { login repositories(first:100,after:$cursor,ownerAffiliations:'"$owner_affiliations"',orderBy:{field:UPDATED_AT,direction:DESC}) { nodes { name nameWithOwner url isArchived isFork stargazerCount updatedAt issues(states:OPEN){totalCount} pullRequests(states:OPEN){totalCount} } pageInfo { hasNextPage endCursor } } } rateLimit { remaining resetAt cost } }' -F cursor="$cursor" 2>"$tmp/graphql.err"); then api_error repositories "$tmp/graphql.err"; graphql_ok=false; break; fi
printf '%s\n' "$page" >"$tmp/page.json"
login=$(jq -r '.data.viewer.login // ""' "$tmp/page.json")
jq '[.data.viewer.repositories.nodes[] | {name,nameWithOwner,url,archived:.isArchived,fork:.isFork,stars:.stargazerCount,issues:.issues.totalCount,prs:.pullRequests.totalCount,updatedAt,activeActions:0}]' "$tmp/page.json" >"$tmp/page.repos"
Expand Down Expand Up @@ -280,4 +289,4 @@ repo_count=$(jq 'length' "$tmp/repos.json")
if [[ $graphql_ok == false && $repo_count -eq 0 ]]; then state=error; message="GitHub repositories could not be loaded."; else state=ready; message=""; fi
rate_remaining=$(jq -r '.remaining // 999999' "$tmp/rate.json")
if { [[ $rate_remaining =~ ^[0-9]+$ ]] && ((rate_remaining<=0)); } || grep -qiE 'rate.?limit|secondary rate' "$warnings"; then state=rate-limited; message="GitHub API rate limit reached."; fi
jq -n --arg state "$state" --arg message "$message" --arg login "${login:-}" --slurpfile notifications "$tmp/notifications.json" --slurpfile reviews "$tmp/reviews.json" --slurpfile issues "$tmp/issues.json" --slurpfile mypulls "$tmp/mypulls.json" --slurpfile mypullstotal "$tmp/mypulls-total.json" --slurpfile actions "$tmp/actions.json" --slurpfile failed "$tmp/failed.json" --slurpfile repositories "$tmp/repos.json" --argjson warnings "$warnings_json" --slurpfile rate "$tmp/rate.json" '{schemaVersion:1,state:$state,message:$message,login:$login,fetchedAt:(now|todateiso8601),notifications:$notifications[0],reviewRequests:$reviews[0],assignedIssues:$issues[0],myPullRequests:$mypulls[0],myPullRequestsTotal:$mypullstotal[0],actions:$actions[0],failedActions:$failed[0],repositories:$repositories[0],warnings:$warnings,rateLimit:$rate[0]}'
jq -n --arg state "$state" --arg message "$message" --arg login "${login:-}" --arg scope "$repository_scope" --slurpfile notifications "$tmp/notifications.json" --slurpfile reviews "$tmp/reviews.json" --slurpfile issues "$tmp/issues.json" --slurpfile mypulls "$tmp/mypulls.json" --slurpfile mypullstotal "$tmp/mypulls-total.json" --slurpfile actions "$tmp/actions.json" --slurpfile failed "$tmp/failed.json" --slurpfile repositories "$tmp/repos.json" --argjson warnings "$warnings_json" --slurpfile rate "$tmp/rate.json" '{schemaVersion:1,state:$state,message:$message,login:$login,repositoryScope:$scope,fetchedAt:(now|todateiso8601),notifications:$notifications[0],reviewRequests:$reviews[0],assignedIssues:$issues[0],myPullRequests:$mypulls[0],myPullRequestsTotal:$mypullstotal[0],actions:$actions[0],failedActions:$failed[0],repositories:$repositories[0],warnings:$warnings,rateLimit:$rate[0]}'
Loading