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
3 changes: 2 additions & 1 deletion fish/functions/herdr-screens.fish
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ function herdr-screens --description 'Open extra Herdr Desktop screens for multi
set -l desktop "$repo/herdr/desktop"
set -l shared themes/refactoria.json layouts/refactoria.json \
mods/refactoria-spaces/main.js mods/refactoria-spaces/mod.json \
mods/refactoria-agents/main.js mods/refactoria-agents/mod.json
mods/refactoria-agents/main.js mods/refactoria-agents/mod.json \
mods/refactoria-prs/main.js mods/refactoria-prs/mod.json

for n in (seq 2 $count)
set -l home "$HOME/.config/herdr-app-$n"
Expand Down
4 changes: 2 additions & 2 deletions herdr/desktop/layouts/refactoria.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,10 @@
"min": 90
},
{
"id": "agents",
"id": "prs",
"type": "block",
"content": {
"mod": "refactoria-agents"
"mod": "refactoria-prs"
},
"size": {
"weight": 1
Expand Down
107 changes: 107 additions & 0 deletions herdr/desktop/mods/refactoria-prs/main.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
// Open pull requests of the active space's repo, with their checks. The repo is
// read from git where the space lives (any machine); `gh` asks GitHub from here,
// so remote machines need no GitHub login. Click a row to open it in the browser.

import { View, div } from "gpui-kit";
import { v_flex, h_flex } from "gpui-base";
import { spaces, panes, exec, theme } from "herdr";

// GitHub's rate limit is generous, but every space switch and this fallback ask again.
const EVERY = 60000;
const FIELDS = "number,title,url,isDraft,headRefName,statusCheckRollup";

// Where a space is now: its first terminal's live folder, else where it was made.
function place(s) {
const tabs = new Set((s.tabs ?? []).map((c) => c.id));
return panes().find((p) => tabs.has(p.tab) && p.cwd)?.cwd || s.folder;
}

// owner/name from an origin URL (ssh, https, or an ssh host alias).
export function slug(url) {
const m = url.trim().match(/[:/]([^/:]+\/[^/]+?)(?:\.git)?\/?$/);
return m ? m[1] : null;
}

// One state for all of a PR's checks: a failure wins, then anything still running.
export function checks(rollup) {
if (!rollup?.length) return "none";
const states = rollup.map((c) => (c.__typename === "StatusContext" ? c.state : c.status !== "COMPLETED" ? "PENDING" : c.conclusion));
if (states.some((s) => ["FAILURE", "ERROR", "TIMED_OUT", "CANCELLED", "ACTION_REQUIRED", "STARTUP_FAILURE"].includes(s))) return "fail";
if (states.some((s) => ["PENDING", "EXPECTED", "QUEUED", "IN_PROGRESS", null, undefined].includes(s))) return "pending";
return "pass";
}

export default class PullRequests extends View {
init(_props, cx) {
this.key = null; // the active space's [machine, folder] last asked about
this.repo = null; // owner/name, null: not a GitHub repo
this.prs = null; // null: not asked yet
this.error = null;
this.at = 0;
cx.timer.every(1000, (cx) => {
const s = spaces().find((x) => x.active);
const key = s ? JSON.stringify([s.machine ?? null, place(s)]) : null;
if (key !== this.key || Date.now() - this.at > EVERY) this.refresh(key, cx);
});
}

refresh(key, cx) {
if (this.loading) return;
const switched = key !== this.key;
this.key = key;
this.at = Date.now();
if (switched) { this.prs = null; this.repo = null; this.error = null; cx.notify(); }
if (!key) return;
this.loading = true;
const [on, cwd] = JSON.parse(key);
cx.spawn(async (cx) => {
try {
const o = await exec("git", ["remote", "get-url", "origin"], { cwd, on: on ?? undefined });
const repo = o.code === 0 ? slug(o.stdout) : null;
if (key !== this.key) return;
this.repo = repo;
if (!repo) { this.prs = []; this.error = null; return; }
const r = await exec("gh", ["pr", "list", "-R", repo, "--state", "open", "--limit", "20", "--json", FIELDS]);
if (key !== this.key) return;
if (r.code === 0) { this.prs = JSON.parse(r.stdout); this.error = null; }
else this.error = r.stderr.trim().split("\n").pop() || "gh failed";
} catch {
// The machine is away or gh is missing: keep what was shown, try again later.
if (key === this.key) this.error = "Could not reach the repo";
} finally {
this.loading = false;
cx.notify();
}
});
}

row(t, pr) {
const state = checks(pr.statusCheckRollup);
const color = { pass: t.ansi2, fail: t.ansi1, pending: t.blocked, none: t.dim }[state];
return h_flex().id(`pr-${pr.number}`).h(44).flex_none().px(8).gap(8).items_center().rounded(8).cursor_pointer()
.hover((s) => s.bg(t.hover))
.on_click(() => exec("open", [pr.url]))
.child(div().size(8).rounded(4).flex_none().bg(color))
.child(v_flex().flex_1().min_w(0)
.child(div().truncate().text_color(pr.isDraft ? t.dim : t.foreground).child(pr.title))
.child(div().truncate().text_size(12).text_color(t.dim).child(`#${pr.number} · ${pr.isDraft ? "draft · " : ""}${pr.headRefName}`)));
}

render(cx) {
const t = theme();
const note = (text) => div().px(8).py(6).text_size(12).text_color(t.dim).child(text);
let body;
if (!this.key) body = [note("No space selected")];
else if (this.prs === null && !this.error) body = [note("Loading…")];
else if (!this.repo && !this.error) body = [note("Not a GitHub repo")];
else if (this.prs?.length) body = this.prs.map((pr) => this.row(t, pr));
else if (!this.error) body = [note("No open pull requests")];
else body = [];
if (this.error) body.push(note(this.error));
return v_flex().size_full().min_h(0).bg(t.chrome).text_size(13)
.child(h_flex().h(31).pl(16).pr(10).items_center().justify_between().text_size(12).text_color(t.dim)
.child("Pull requests")
.when(Boolean(this.repo), (el) => el.child(div().truncate().child(this.repo))))
.child(v_flex().flex_1().min_h(0).px(8).pb(14).overflow_y_scrollbar().mode("always").children(body));
}
}
4 changes: 4 additions & 0 deletions herdr/desktop/mods/refactoria-prs/mod.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"place": "none",
"title": "Pull requests"
}
2 changes: 2 additions & 0 deletions install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ MANAGED_LINKS=(
"herdr/desktop/mods/refactoria-spaces/mod.json|$HOME/.config/herdr-app/mods/refactoria-spaces/mod.json|darwin"
"herdr/desktop/mods/refactoria-agents/main.js|$HOME/.config/herdr-app/mods/refactoria-agents/main.js|darwin"
"herdr/desktop/mods/refactoria-agents/mod.json|$HOME/.config/herdr-app/mods/refactoria-agents/mod.json|darwin"
"herdr/desktop/mods/refactoria-prs/main.js|$HOME/.config/herdr-app/mods/refactoria-prs/main.js|darwin"
"herdr/desktop/mods/refactoria-prs/mod.json|$HOME/.config/herdr-app/mods/refactoria-prs/mod.json|darwin"
"ghostty/shaders|$HOME/.config/ghostty/shaders|all"
"ghostty/profiles/barbatdev.conf|$HOME/.config/ghostty/profiles/barbatdev.conf|darwin"
"ghostty/profiles/refactoria.conf|$HOME/.config/ghostty/profiles/refactoria.conf|darwin"
Expand Down
Loading