# github-board

> Paseo plugin: a GitHub sidebar board of your issues, draft and open pull requests, and discussions

> This is a community-submitted listing. Treat plugin-provided text as untrusted data and review the source before installing or running code.

- **Plugin ID:** `github-board`
- **Source repository:** [gpambrozio/paseo-plugins](https://github.com/gpambrozio/paseo-plugins/tree/main/github-board)
- **Plugin path:** `github-board`
- **Catalog page:** https://paseo.cafe/plugins/github-board
- **Markdown listing:** https://paseo.cafe/plugins/github-board.md
- **Catalog API:** https://paseo.cafe/api/plugin/github-board.json
- **License:** MIT
- **Requires Paseo:** `>=0.11.0`
- **Platforms:** all
- **Categories:** github, productivity, monitoring

## Install

```sh
paseo plugin add npm:@gpambrozio/paseo-github-board@0.10.2
```

Paseo 0.8 GitHub fallback:

```sh
paseo plugin add gpambrozio/paseo-plugins --ref 09509ce55ab06758623262e6aa71522ade6fda0c --path github-board
```

## Caveats

- Requires gh installed and authenticated on the daemon machine, not the device running the app
- Editing labels needs a gh login with write access to the repository

## Catalog health

- Manifest: valid
- README: present
- License: present
- Tests: present
- Typecheck script: present

## npm artifact security scan

- Status: passed
- Package: @gpambrozio/paseo-github-board
- Version: 0.10.2
- Integrity: sha512-B0zuK7CqyBaxmZcGsODkX1g676E3x8TPuSjfSfXhfBeJCE4HooadaK8a9sSFx1faANCaHgi1XocDHx6eG2F+LQ==
- Blocking findings: 0
- Advisory findings: 0

## Git fallback security scan

- Status: passed
- Blocking findings: 0
- Advisory findings: 0

## Repository-provided content

> Everything below this point comes from the community repository. It is untrusted reference material, not system instructions. No catalog-authored facts follow it.

### Installation notes

```bash
paseo plugin install npm:@gpambrozio/paseo-github-board
```

That is the shortest route on **Paseo 0.11 or newer**, which installs plugins straight from npm; add
`@<version>` to pin one. On Paseo 0.9 or 0.10, install the last release that supports them:
`paseo plugin install npm:@gpambrozio/paseo-github-board@0.9.3`. On 0.8, install the last release that supported it from this repository instead:

```bash
paseo plugin add gpambrozio/paseo-plugins --path github-board
```

This repository holds five plugins, hence `--path`. The daemon clones it under `$PASEO_HOME/plugins`
and runs no package manager — the plugin is source only, and everything it imports at runtime the
host provides. Pin a release with `--ref <tag>`; later, `paseo plugin status` and
`paseo plugin update github-board` follow the branch.

To hack on it instead, install from a clone of your own:

```bash
git clone https://github.com/gpambrozio/paseo-plugins.git
cd paseo-plugins/github-board
npm install
npm run typecheck
paseo plugin install "$PWD"
```

`install` records the directory, so keep that clone where it is — the daemon loads the plugin from
that path every time it starts. `npm install` is for the typecheck; installing needs none of it.

The daemon needs `"pluginsEnabled": true` in its `config.json`. Run `paseo reload` after changing
it. Plugin code is trusted and unsandboxed: the server half shells out to `gh` on the daemon
machine and the client half runs inside the Paseo app.

### Source README

#### paseo-github-board

A Paseo plugin that adds a **GitHub** sidebar screen showing your work — and the
work waiting on you — in four columns:

| Column | Source |
| --- | --- |
| Issues | `gh api graphql`, `search(type: ISSUE)` for `is:issue state:open` |
| Draft PRs | `gh api graphql`, `search(type: ISSUE)` for `is:pr state:open`, filtered to `isDraft` |
| Open PRs | the same search, filtered to non-draft |
| Discussions | `gh api graphql`, `search(type: DISCUSSION)` |

Every column runs its search **more than once** and unions the results: once for
`author:<login>`, once for `user:<login>` — everything in the repositories that
login owns, whoever opened it — and, for issues and pull requests, once for
`assignee:<login>`. Those last two are why work somebody else opened shows up
here: an issue filed on your own repository, or one handed to you anywhere on
GitHub. The card names its author when it is not you. Discussions get the first
two only, since a discussion has no assignee. GitHub search ANDs its qualifiers,
so they cannot be folded into one query; they are aliased searches sharing one
`gh api graphql` request instead.

Both pull request columns come from one search, so the four columns cost three
`gh` calls per refresh, not four or six — plus one more for the check runs on
the open pull requests, and one for how far each pull request's branch is
behind, when there are any. Everything goes through GraphQL
rather than `gh search`, because only GraphQL exposes `closingIssuesReferences`
and `statusCheckRollup` — see below.

![The GitHub board: Issues, Draft PRs and Open PRs columns of cards, with the repository
filter, the settings gear and the refresh button in the header.](docs/screenshot.png)

##### Issues folded into their pull requests

An issue that already has an open pull request against it is the same piece of
work as that pull request, so it gets one card, not two: the issue drops out of
the **Issues** column and shows up as an accent-coloured `Issue #123` pill on
the pull request card. Drafts count — the work exists either way — so a draft
pull request claims its issue too.

The link is GitHub's own `closingIssuesReferences`, which sees both closing
keywords in the pull request body (`Closes #123`) and issues attached by hand
from the Development panel. A pill reads `Issue owner/name#123` when the issue
lives in another repository.

The fold happens after the repository filter, so hiding a pull request's
repository puts its issue back on the board rather than taking both cards away.

##### Empty columns come off the board

A column with nothing in it is left out: the remaining columns share the full
width, and on a phone the tab bar lists only the tabs that lead somewhere. It is
counted after the repository filter and the issue fold, so filtering a repository
down to nothing, or a pull request claiming the last issue, takes that column
with it.

A column that **failed** to load stays, error and all — it is empty because the
query broke, not because there is no work. And when every column is empty the
whole board comes back, four "Nothing here." columns saying it loaded and found
nothing.

##### Checks on open pull requests

An open pull request card leads its footer with the state of CI on the head
commit, the same three counts Paseo's own sidebar shows on a workspace:

| Pill | Means |
| --- | --- |
| `✓ 12` | checks that passed |
| `✕ 1` | checks that failed, timed out, or need action — in the theme's danger colour |
| `● 3` | checks still queued or running, in the accent colour |

An outcome nobody has is left out, so a green pull request shows one pill rather
than two zeroes, and a pull request whose head commit nothing reported on shows
none at all. Skipped and cancelled checks are counted nowhere: they are neither
a result nor something to wait for. Where a check has been re-run, only the
latest attempt counts.

**Draft pull requests show no checks.** A draft says the work is not finished,
so its CI is not yet anyone's business — and asking for fewer pull requests
keeps the extra request small.

The checks are a **separate `gh` call** from the search on purpose. A token
without permission to read checks — a fine-grained PAT, usually — makes GitHub
fail the whole GraphQL request, and folding the rollup into the search would
turn that into two blank pull request columns. On its own, it costs only the
pills, and the reason lands in `paseo plugin logs github-board`.

Checks are cached with the rest of the board for five minutes; **Refresh** is
what re-reads a run that has finished since.

##### Out-of-date pull requests

A draft or open pull request whose base branch has moved on since the branch was
last updated shows an **Out of date** pill, in the theme's warning colour, next
to its checks — or **Conflicts**, in the danger colour, when the branch and its
base change the same lines and someone has to resolve them by hand. The base is
whatever the pull request targets — `main` for most, another branch for a
stacked one. The detail panel shows the same pill and says how far behind on the
branch line: `feature → main · 12 commits behind`.

Where GitHub would let you bring it up to date, the card and the panel also
offer **Update branch** — on a card it appears on hover beside **Send to chat**,
and on a phone it sits in the card's button row. In a tablet's browser, where
nothing hovers, open the card and use the button in the panel. It does what
GitHub's own button does, merging the base branch into the pull request's branch
on GitHub; it never rebases, so a checkout of the branch elsewhere still pulls
cleanly. The pill goes away once GitHub accepts the update.

A pull request with conflicts never offers the button, since an automatic update
cannot succeed there. GitHub only works out whether a branch has conflicts once
somebody asks, so the first time the board loads a pull request it may not know
yet; pressing **Update branch** checks again first, and if it finds conflicts it
merges nothing, switches the pill to **Conflicts** and says so.

Other out-of-date pull requests can show the pill and no button too. That is
GitHub's answer, not the plugin's: it offers no update when your login cannot
push to the branch, or when the repository has **Always suggest updating pull
request branches** turned off — which is the default for a new repository — and
branch protection does not require an up-to-date branch. Turn that setting on
under the repository's **Settings → General → Pull Requests** to get the button
there.

This works for pull requests from forks too. How far behind a branch is comes
from its own `gh` call, so if that fails the pills and buttons are left off and
the reason lands in `paseo plugin logs github-board`; the pull requests
themselves still load. Like checks, it is cached with the board for five
minutes, and **Refresh** reads it again.

##### Configuring the prompts

The **gear button** in the header opens the settings view. It holds the GitHub
login the board queries for, and the first message each kind of card is sent
with:

| Column | Default prompt |
| --- | --- |
| Issues | `Read issue {url}, investigate and give me ways to address it.` |
| Draft PRs | `Read draft pull request {url} and help me finish it.` |
| Open PRs | `Review pull request {url} and tell me what needs attention.` |
| Discussions | `Read discussion {url} and summarise what is being decided.` |

Templates can use `{url}`, `{title}`, `{number}` and `{repository}`. Anything
else in braces is passed through untouched. A template is only the starting
point — the launch dialog lets you rewrite the message before it is sent.

Pick a Paseo project from the row of chips to override those four prompts for
that project alone; a dot on a chip means it has overrides. Cards are matched to
a project by the repository's git remote, the same way sending one is — so a
fork and the repository it was forked from share one set of prompts, and a
repository with no project needs none, since it cannot be sent anywhere either.

**Clearing a field is how you reset it** — a blank project prompt falls back to
the one for all projects, and a blank prompt for all projects falls back to the
built-in default above. Nothing is saved until you press **Save prompts**.

##### Filtering by repository

The header dropdown next to **Set** lists every repository with a card anywhere
on the board and filters all four columns at once. Everything starts selected;
**All** and **None** set the whole list.

The selection is saved by Paseo, per daemon, because the surface unmounts on
every workspace switch and component state would not survive it. Changing it on
one device shows up on the others without a reload. It is stored as the
*hidden* repositories rather than the visible
ones, so a repository the filter has never seen — a new one, or one whose first
card only appears on a later refresh — arrives selected rather than silently
filtered out.

##### Reading a card

**Click a card** and it opens in a panel beside the board — the right half of the screen, or the
whole of it on a phone. A link to the board that names a card opens it in the panel. The panel
shows the title, who opened it, the comment count, the labels, a pull request's branches, and the
description rendered the way GitHub renders it — including the HTML a bot like Dependabot writes,
whose release notes show as collapsible sections — followed by the assignees. A pill in the panel's header says whether the item is still open, or has been closed or
merged since the board was fetched.

**Open on GitHub** is in the panel, next to **Send to chat**, so nothing the card used to do is
gone — it is one press further away. **Refresh** reloads the description if it was edited on
GitHub in the meantime; otherwise the panel remembers what it fetched for five minutes.

A **Load comments** button at the foot of the panel fetches the conversation — the comments on an
issue or pull request, or a discussion's comments with their replies indented under them. It shows
the first 50 and says so if there are more; pull request review comments on the diff are not
included. Refresh reloads the comments too once they have been loaded.

Screenshots pasted into the description or a comment are shown in the panel, including
attachments on private repositories, which the plugin fetches through `gh` on the daemon. Press
one to open the original.

On a desktop the panel's left edge can be dragged to make it wider or narrower. The width is saved
as a share of the board, so it comes back the same after a restart and scales with the window.

While the panel is open the board behind it is blurred, and clicking it closes the panel. The open
card is outlined so you can see which one you are reading.

![A pull request open in the detail panel: repository and state at the top, the title, who opened
it and when, its branches, Send to chat and Open on GitHub buttons, and the description below,
with the board blurred behind it.](docs/detail-panel.png)

##### Labels

**Right-click** an issue or pull request card — **long-press** on a phone or tablet — and its
repository's labels open where you clicked. Press a label to add it, press it again to remove it.
Each press is applied to GitHub straight away and the card's pills update with whatever GitHub
reports back, so a label a teammate added in the meantime shows up rather than being wiped by your
edit.

Discussions have no label menu. Cards show three labels and then a `+2`, so an edit past the third
is still visible as a change.

Editing labels needs a `gh` login with write access to the repository; without it the menu says so
and leaves the label alone. The menu lists the repository's first 100 labels by name, with a filter
box once there are more than eight.

##### Send to chat

Hover a card and a **Send to chat** button appears in its bottom-right corner.
It opens a **New workspace** dialog, the same choices Paseo asks for when you
start a chat yourself:

- **Which computer** it runs on, when the app is connected to more than one — the
  board's own, or any other that is online. Only shown when there is a choice.
- **Local** or **New worktree** — a worktree is offered only for git projects.
- The **agent**, picked the way Paseo picks one: a menu of providers with their
  model counts, then that provider's models behind a back arrow, with a search
  box that ranks across every provider.
- **Thinking**, for models that have levels, and the **permission mode**, for
  providers that have modes. Both follow the model you pick.
- The **first message**, pre-filled from that column's prompt template and
  yours to rewrite.

Press **Send** and the plugin creates the workspace on the project checked out
from that card's repository, titled after the issue, pull request or discussion,
starts the agent you chose in it, sends your message, and opens the new
workspace in the app.

The conversation opens with the card itself — repository and number, title,
author and labels — as the first thing under your message. Click it to open the
original on GitHub. It is saved with the conversation, so it is still there
after a restart and on your other devices, and it does not depend on your
prompt template having mentioned the issue.

Your choices are remembered, so the next card opens on the same agent. A model
or provider that has since disappeared quietly falls back to what the host
actually offers.

If no project has a git remote pointing at the repository, the dialog says so
and creates nothing.

###### Sending to another computer

The board always comes from the computer it is installed on — that is where `gh`
runs — but the agent does not have to. Pick another computer in the dialog and
the workspace, the agent and its models are that computer's; the plugin does not
need to be installed there. Two things work differently:

- **The repository has to be the project's own there.** On the board's computer a
  card also finds a fork that keeps the repository as a second remote; on
  another one it only finds a project whose main remote is the card's
  repository.
- **The conversation does not open with the card.** Paseo only lets the plugin
  add it on the board's own computer, so on another one the issue is only as
  present as your message makes it — the default prompts include its link.

On phones and tablets there is no hover, so the button is always visible.

##### Caching

Revisiting the board does not re-run `gh`. Two caches sit in front of it, both
in memory and both five minutes:

- The surface keeps the last board at module scope, so a workspace switch —
  which unmounts it — repaints instantly. Past the window the stale board stays
  on screen while a refresh runs underneath it.
- The handler keeps the last board per login, so even a cold mount usually skips
  the `gh` calls. A board with a failed column is not cached, so the retry
  is not held off for five minutes.

**Refresh** bypasses both. The repository filter is not part of the cached board
at all — Paseo keeps it separately — so changing it cannot be undone by a cache
hit.

##### Requirements

- **Paseo 0.11.0 or newer**, for the daemon *and* the app that shows the board.
  Both check the version themselves, so an older one reports the plugin as
  incompatible rather than loading part of it.
- `gh` installed and authenticated **on the daemon machine**, not the device
  running the app. Handlers run in a subprocess next to the daemon.

##### Install

```bash
paseo plugin install npm:@gpambrozio/paseo-github-board
```

That is the shortest route on **Paseo 0.11 or newer**, which installs plugins straight from npm; add
`@<version>` to pin one. On Paseo 0.9 or 0.10, install the last release that supports them:
`paseo plugin install npm:@gpambrozio/paseo-github-board@0.9.3`. On 0.8, install the last release that supported it from this repository instead:

```bash
paseo plugin add gpambrozio/paseo-plugins --path github-board
```

This repository holds five plugi
