Skip to content
Closed
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
23 changes: 23 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,29 @@ pin and a line to read before you move.

## [Unreleased]

### Added

- **Every authenticated GitHub host is now swept together.** The hosts `gh auth status` reports are
all queried at once, each with its own account, so one board can show github.com and GitHub
Enterprise work side by side. Reads and actions stay routed to the host that owns the item, and
with more than one host on the board the list is divided into one section per host rather than
interleaving them. An Enterprise repository's screenshots come through the daemon too, fetched
with that host's own token; hosts neither GitHub nor your logins know are still never fetched.
- **Open the original chat from a pull request.** A pull request that a Paseo workspace tracks is
linked to the first agent created in that workspace — the originating conversation — and a new
action on the row and in the panel jumps straight to that chat.
- **The board is painted before GitHub answers.** A board the daemon already has is shown at once
and refreshed behind it, and the header says how old the board on screen is.

### Fixed

- **The search limit is now spent per relation, not as one shared budget.** A full review queue
used to take the whole list and push the viewer's own work off the board. Pull request checks
are also asked for in batches of 100 ids, an expired login on one host no longer blanks the
board for the hosts that are fine, and the board no longer runs minutes behind GitHub: the
daemon re-sweeps once its board is more than a minute old, and a sweep in which a column failed
is not remembered.

## [1.0.1] — 2026-09-12

### Changed
Expand Down
26 changes: 23 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,21 @@ GitHub inside [Paseo](https://paseo.sh): the issues and pull requests you are at
waiting on your review, your Projects boards, and the review actions that finish a pull request —
without leaving the app, and with one click to hand any of them to a coding agent.

The board also links a pull request back to its original Paseo chat. It matches the PR tracked
by each active or retained workspace to the first agent created there, then adds an **Open original
chat** action to the PR row and detail panel.

Every authenticated host reported by `gh auth status --json hosts` is queried together. One board
can therefore show github.com and GitHub Enterprise work at the same time, using each host's active
account and keeping reads and actions routed to the host that owns the item. With more than one host
on the board, the list is divided into a section per host rather than interleaving them.

Opening the surface does not wait on GitHub: a board the daemon already has is painted immediately
and refreshed behind it, and the refreshed one replaces it as soon as the sweep lands. Every open,
and every return to the app, asks the daemon; the daemon sweeps GitHub again when its own board is
more than a minute old, so a pull request merged elsewhere leaves the list within seconds of the
board being opened. The header says how old the board on screen is.

![The GitHub surface: the Pull requests / Issues / Discussions / Projects switcher, the relation
chips with their counts, the owner and repository pickers, the search box, and the list of pull
requests.](images/board.png)
Expand Down Expand Up @@ -34,6 +49,9 @@ method the repository allows, edit labels, open on GitHub — or **send to chat*
Paseo agent with a prompt template of your choosing, in a workspace of your choosing, with the
issue or pull request pinned to the top of the conversation.

For pull requests associated with a Paseo workspace, **Open original chat** jumps directly to the
first agent in that workspace.

![One pull request open in the detail panel: title, author, branches, checks, the Approve, Merge,
Send to chat and Open on GitHub actions, and the rendered body.](images/detail-panel.png)

Expand Down Expand Up @@ -86,9 +104,11 @@ inputs.paseo-github.url = "github:alysnnix/paseo-github-integration";
- **GitHub search backs the sweep**, so its rules apply: results are capped per query, and a
watched owner with thousands of open items shows the most recently updated slice rather than all
of them.
- **One GitHub account at a time**, the one `gh` is authenticated as. There is no account switcher.
- **Images hosted outside GitHub are not loaded**, deliberately: they would report your IP address
to whoever wrote the comment. They render as a link you can open yourself.
- **One account per host**, the one `gh` is authenticated as there. Every authenticated host is
swept together, but there is no account switcher within a host.
- **Images from hosts neither GitHub nor your `gh` logins know are not loaded**, deliberately:
they would report your IP address to whoever wrote the comment. They render as a link you can
open yourself.

## Trust

Expand Down
26 changes: 20 additions & 6 deletions client/board/board-body.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import { ProjectsView } from "../projects/projects-view";
import { EMPTY_PROMPTS, PromptSettingsView } from "../settings/prompt-settings-view";
import type { Styles } from "../theme/use-styles";
import { openExternalUrl } from "../web";
import type { ListRow } from "./host-sections";
import type { ChatLink } from "./use-chat-links";

/**
* Everything below the header and the toolbar: the settings screen when it is
Expand All @@ -29,14 +31,15 @@ export function BoardBody({
board,
mode,
watchedOwners,
displayRows,
listRows,
renderRow,
modeRows,
refresh,
bodyWidth,
setBodyWidth,
detailTarget,
detailItem,
detailChatLink,
detailProgress,
closeDetails,
savedFraction,
Expand All @@ -55,14 +58,15 @@ export function BoardBody({
board: Board | null;
mode: BoardMode;
watchedOwners: readonly string[];
displayRows: readonly BoardRow[];
listRows: readonly ListRow[];
renderRow: (info: { item: BoardRow }) => React.JSX.Element;
modeRows: { rows: BoardRow[]; error: string | null };
refresh: (login?: string, force?: boolean) => Promise<void>;
bodyWidth: number | null;
setBodyWidth: (next: number | null) => void;
detailTarget: { item: BoardItem; type: ColumnId } | null;
detailItem: BoardItem | null;
detailChatLink: ChatLink | null;
detailProgress: Animated.Value;
closeDetails: () => void;
savedFraction: number | null;
Expand Down Expand Up @@ -94,16 +98,24 @@ export function BoardBody({
<ProjectsView
theme={surfaceProps.theme}
layout={surfaceProps.layout}
login={board.login}
owners={watchedOwners}
onOpenUrl={openExternalUrl}
/>
) : (
<FlatList
style={styles.rowList}
data={displayRows}
keyExtractor={(row) => row.item.id}
renderItem={renderRow}
data={listRows}
keyExtractor={(row) => row.key}
renderItem={({ item: row }) =>
row.kind === "host" ? (
<View style={styles.hostHeader}>
<Text style={styles.hostHeaderName}>{row.host}</Text>
<Text style={styles.hostHeaderCount}>{row.count}</Text>
</View>
) : (
renderRow({ item: row.row })
)
}
ListEmptyComponent={
modeRows.error !== null ? (
<Text style={[styles.danger, styles.empty]}>{modeRows.error}</Text>
Expand Down Expand Up @@ -141,6 +153,7 @@ export function BoardBody({
key={detailItem.id}
item={detailItem}
type={detailTarget.type}
chatLink={detailChatLink}
styles={styles}
accentColor={surfaceProps.theme.colors.accent}
foregroundColor={surfaceProps.theme.colors.foreground}
Expand All @@ -149,6 +162,7 @@ export function BoardBody({
onWidthCommitted={commitWidth}
progress={detailProgress}
onClose={closeDetails}
onOpenChat={(chat) => surfaceProps.navigation?.openAgent({ agentId: chat.agentId })}
onSend={openSendDialog}
onMerged={dropItem}
/>
Expand Down
28 changes: 28 additions & 0 deletions client/board/board-row.styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,33 @@ export function buildBoardRowStyles(
return {
rowList: { flex: 1 },
rowListContent: { paddingBottom: layout.compact ? 32 : 8 },
/**
* The heading above each host's run of cards, shown only when the board
* aggregates more than one GitHub host. Deliberately quieter than a card:
* it separates the list, it is not an entry in it.
*/
hostHeader: {
flexDirection: "row" as const,
alignItems: "center" as const,
justifyContent: "space-between" as const,
gap: 8,
paddingHorizontal: layout.compact ? 12 : 16,
paddingTop: layout.compact ? 14 : 12,
paddingBottom: 6,
backgroundColor: colors.surface1,
borderBottomWidth: 1,
borderBottomColor: separator,
},
hostHeaderName: {
color: colors.foregroundMuted,
fontSize: layout.compact ? 12 : 11,
fontWeight: "700" as const,
letterSpacing: 0.4,
},
hostHeaderCount: {
color: colors.foregroundMuted,
fontSize: layout.compact ? 12 : 11,
},
/**
* One line of the GitHub-style list: an icon, the title and its meta, and
* — on the wide layout only — the trailing checks/comments/labels. A
Expand Down Expand Up @@ -68,6 +95,7 @@ export function buildBoardRowStyles(
gap: 6,
marginTop: 6,
},
itemRowActions: { flexDirection: "row" as const, alignItems: "center" as const, gap: 4 },
/** Hidden until the row or the action itself is hovered; see `sendButtonHidden`. */
itemRowActionHidden: { opacity: 0 },
label: {
Expand Down
8 changes: 8 additions & 0 deletions client/board/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,11 @@
* catalogue the label menu keeps alongside it.
*/
export const STALE_AFTER_MS = 5 * 60_000;

/**
* How often the board asks again while the daemon is refreshing the answer it
* just served from its cache. Short enough that the fresh board lands without
* the user reaching for Refresh, and each ask is cheap: the daemon answers
* from memory until its own sweep finishes.
*/
export const STALE_POLL_MS = 3_000;
6 changes: 4 additions & 2 deletions client/board/github-board.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,13 +44,14 @@ export function GitHubBoard(props: PluginSurfaceProps) {
applyPrompts,
applyLogin,
watchedOwners,
displayRows,
listRows,
renderRow,
modeRows,
bodyWidth,
setBodyWidth,
detailTarget,
detailItem,
detailChatLink,
detailProgress,
closeDetails,
savedFraction,
Expand Down Expand Up @@ -146,14 +147,15 @@ export function GitHubBoard(props: PluginSurfaceProps) {
board={board}
mode={mode}
watchedOwners={watchedOwners}
displayRows={displayRows}
listRows={listRows}
renderRow={renderRow}
modeRows={modeRows}
refresh={refresh}
bodyWidth={bodyWidth}
setBodyWidth={setBodyWidth}
detailTarget={detailTarget}
detailItem={detailItem}
detailChatLink={detailChatLink}
detailProgress={detailProgress}
closeDetails={closeDetails}
savedFraction={savedFraction}
Expand Down
54 changes: 54 additions & 0 deletions client/board/host-sections.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { describe, expect, it } from "vitest";
import type { BoardItem } from "../../shared/board";
import type { BoardRow } from "../lib/sort";
import { withHostHeaders } from "./host-sections";

function makeRow(id: string, host: string): BoardRow {
return {
item: {
id,
number: 1,
title: id,
url: `https://${host}/owner/repo/pull/1`,
repository: "owner/repo",
host,
updatedAt: "2024-01-01T00:00:00Z",
createdAt: "2024-01-01T00:00:00Z",
lastCommitAt: null,
commentsCount: 0,
labels: [],
author: null,
detail: null,
owner: "owner",
relations: ["author"],
linkedIssues: [],
checks: null,
} satisfies BoardItem,
type: "open-prs",
};
}

describe("withHostHeaders", () => {
it("adds no heading when every row is from one host", () => {
const rows = [makeRow("a", "github.com"), makeRow("b", "github.com")];
expect(withHostHeaders(rows)).toEqual([
{ kind: "row", key: "a", row: rows[0] },
{ kind: "row", key: "b", row: rows[1] },
]);
});

it("groups each host under one heading, in the order the hosts first appear", () => {
const rows = [
makeRow("a", "ghe.example.com"),
makeRow("b", "github.com"),
makeRow("c", "ghe.example.com"),
];
expect(withHostHeaders(rows)).toEqual([
{ kind: "host", key: "host:ghe.example.com", host: "ghe.example.com", count: 2 },
{ kind: "row", key: "a", row: rows[0] },
{ kind: "row", key: "c", row: rows[2] },
{ kind: "host", key: "host:github.com", host: "github.com", count: 1 },
{ kind: "row", key: "b", row: rows[1] },
]);
});
});
43 changes: 43 additions & 0 deletions client/board/host-sections.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import type { BoardRow } from "../lib/sort";

/**
* What the board's list actually renders: a card, or the host heading that
* opens a run of cards from one GitHub host. Headings exist only when the
* board is aggregating more than one host — with a single account there is
* nothing to divide, and a lone "github.com" banner is just noise.
*/
export type ListRow =
| { kind: "host"; key: string; host: string; count: number }
| { kind: "row"; key: string; row: BoardRow };

/**
* Groups already-sorted rows by host without disturbing the order within a
* group. Hosts appear in the order their first row does, so whichever host
* owns the top card by the active sort keeps the top of the list rather than
* losing it to an alphabetical rule the sort dropdown never mentions.
*/
export function withHostHeaders(rows: readonly BoardRow[]): ListRow[] {
const order: string[] = [];
const byHost = new Map<string, BoardRow[]>();
for (const row of rows) {
const existing = byHost.get(row.item.host);
if (existing === undefined) {
byHost.set(row.item.host, [row]);
order.push(row.item.host);
} else {
existing.push(row);
}
}

if (order.length < 2) {
return rows.map((row) => ({ kind: "row", key: row.item.id, row }));
}

const list: ListRow[] = [];
for (const host of order) {
const group = byHost.get(host) ?? [];
list.push({ kind: "host", key: `host:${host}`, host, count: group.length });
for (const row of group) list.push({ kind: "row", key: row.item.id, row });
}
return list;
}
7 changes: 4 additions & 3 deletions client/board/item-row-format.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export interface RowDisplay {
*/
export function describeRow({
item,
viewerLogin,
viewerLogins,
order,
type,
accentColor,
Expand All @@ -48,7 +48,7 @@ export function describeRow({
isWeb,
}: {
item: BoardItem;
viewerLogin: string;
viewerLogins: readonly string[];
order: SortOrder;
type: ColumnId;
accentColor: string;
Expand All @@ -73,7 +73,8 @@ export function describeRow({
: "CircleDot";
const iconColor = type === "draft-prs" || type === "discussions" ? mutedColor : accentColor;

const byline = item.author !== null && item.author !== viewerLogin ? item.author : null;
const byline =
item.author !== null && !viewerLogins.includes(item.author) ? item.author : null;

const openedBy = byline === null ? "" : `, opened by ${byline}`;
const linkedTo = closes === "" ? "" : `, closes ${closes}`;
Expand Down
Loading