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

## [Unreleased]

### Added

- Named pull request search views with editable GitHub queries, Boolean operators, UTC relative
dates, preview, and pagination. Saved views preserve GitHub ordering and support PRs beyond the
viewer's personal relationships, including closed and merged work.

## [1.0.1] — 2026-09-12

### Changed
Expand Down
6 changes: 6 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -565,3 +565,9 @@ what let a *cold* client — a fresh reload, a different device, a daemon restar
calls too. `force` on `board.load` is still the Refresh button asking both layers to bypass
themselves at once. Whatever should outlive an unmount and is not shaped like a cache belongs in one
of the two settings stores instead — that is the only other thing here that survives one.

## Saved pull request searches

`shared/saved-views.ts` defines a separate host settings document and the `search.pull-requests` RPC. `client/search/` owns the view editor, selection, and paginated query state. `server/search/` runs an independent `ISSUE_ADVANCED` search and reuses PR mapping/checks from `server/board/`; do not pass its results through the relationship bucket merge or local board filters, since both alter query semantics and ordering. Search hits can have an empty `relations` array.

`shared/search-query.ts` scopes the whole Boolean expression to PRs and resolves unquoted relative date qualifiers. The raw query is persisted; the effective UTC date travels with subsequent page cursors. `client/lib/search-pages.ts` deduplicates overlapping pages without sorting. Search cache keys include authenticated identity, resolved query, and cursor. Label/review/merge handlers clear the search cache, and their client action hooks reset loaded search pages. `Cache.clear()` also prevents an in-flight pre-mutation answer from repopulating the cache.
20 changes: 18 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,8 @@ inputs.paseo-github.url = "github:alysnnix/paseo-github-integration";
itself and cannot log you in.
- **Projects needs a scope `gh auth login` does not grant.** Without `read:project` the Projects
tab shows the command to run instead of your boards.
- **The board is a `state:open` search.** Closed issues and merged pull requests leave it; there is
no archive view, and nothing here searches history.
- **The ordinary board is a `state:open` search.** Closed issues and merged pull requests leave it;
saved pull request views can search closed and merged work.
- **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.
Expand Down Expand Up @@ -131,3 +131,19 @@ and the other plugins that lived beside it there are not carried here. Thanks to
starting point and for the MIT licence that made it possible.

Licensed under the MIT licence — see [LICENSE](LICENSE).

## Saved pull request views

Open **Saved views** in the GitHub surface, choose **New view**, and enter a name and a GitHub search query. **Preview** runs the query without saving; **Save** keeps the view on this host for all its connected clients. Views can be renamed, edited, and deleted, and the selected view is remembered across workspace switches.

For example:

```text
(org:example OR org:sample) is:open draft:false review:required created:>@today-30d sort:updated-desc
```

Every saved view searches pull requests only. The query controls owner/repository scope, state, draft status, and ordering; the ordinary board's relationship and repository filters do not apply. An item need not have appeared on the ordinary board to appear in a saved view. Closed and merged PRs are supported when the query includes them.

Date qualifiers (`created:`, `updated:`, `closed:`, and `merged:`) accept `@today` and `@today-Nd`. The daemon expands them using the current **UTC calendar date**, preserving comparisons and ranges such as `updated:@today-7d..@today`. Quoted text is left literal. The saved query remains relative; the resolved query is shown above the results and used by **Open search on GitHub**.

**Refresh results** reloads the first page. **Load more** follows GitHub's cursor while keeping the same effective date. Results are cached for five minutes, and local label, review, and merge actions invalidate searches because they can change query membership. GitHub may take time to update its search index after a change. GitHub exposes at most 1,000 matches; narrow the query if you reach that ceiling. Saved views use advanced GraphQL search and show GitHub errors when that search is unavailable or the query is rejected.
72 changes: 72 additions & 0 deletions client/board/board-dialogs.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import type { PluginSurfaceProps } from "@getpaseo/plugin/client";
import { Pressable, View } from "react-native";
import { SendDialog } from "../launch/send-dialog";
import { LabelMenu } from "./label-menu";
import type { Styles } from "../theme/use-styles";
import type { UseBoardOverlaysResult } from "./use-board-overlays";

export function BoardDialogs({
props,
styles,
overlays,
}: {
props: PluginSurfaceProps;
styles: Styles;
overlays: Pick<
UseBoardOverlaysResult,
| "labelTarget"
| "setLabelTarget"
| "applyItemLabels"
| "sendTarget"
| "setSendTarget"
| "handleLaunched"
>;
}) {
const {
labelTarget,
setLabelTarget,
applyItemLabels,
sendTarget,
setSendTarget,
handleLaunched,
} = overlays;
return (
<>
{labelTarget !== null ? (
<View style={styles.menuLayer}>
<Pressable
accessibilityRole="button"
accessibilityLabel="Close labels menu"
style={styles.menuScrim}
onPress={() => setLabelTarget(null)}
/>
<LabelMenu
// Keyed by card: opening the menu on a second card must not inherit
// the first one's applied set or its in-flight toggles.
key={labelTarget.item.id}
target={labelTarget}
styles={styles}
accentColor={props.theme.colors.accent}
onClose={() => setLabelTarget(null)}
onChanged={applyItemLabels}
/>
</View>
) : null}

{sendTarget !== null ? (
<SendDialog
// Keyed by card, so opening a second one never inherits the first
// one's prompt or its half-made choices.
key={sendTarget.item.id}
item={sendTarget.item}
initialPrompt={sendTarget.prompt}
hostLabel={props.host.label}
styles={styles}
accentColor={props.theme.colors.accent}
onCancel={() => setSendTarget(null)}
onLaunched={handleLaunched}
/>
) : null}
</>
);
}
6 changes: 4 additions & 2 deletions client/board/board-header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@ export function BoardHeader({
busy,
refresh,
setOpenFilter,
showBoardStatus = true,
}: {
showBoardStatus?: boolean;
surfaceProps: PluginSurfaceProps;
styles: Styles;
showSettings: boolean;
Expand Down Expand Up @@ -52,7 +54,7 @@ export function BoardHeader({
</Pressable>
) : (
<>
{board !== null ? (
{showBoardStatus && board !== null ? (
<Text style={styles.headerAge} numberOfLines={1}>
Updated {relativeTime(board.fetchedAt)}
</Text>
Expand All @@ -71,7 +73,7 @@ export function BoardHeader({
{/* Compact refreshes by pulling the list down, so the button would
be a second way to do the same thing in the row with the least
room for one. */}
{surfaceProps.layout.compact ? null : (
{surfaceProps.layout.compact || !showBoardStatus ? null : (
<Pressable
accessibilityRole="button"
accessibilityLabel={busy ? "Loading the board" : "Refresh the board"}
Expand Down
112 changes: 47 additions & 65 deletions client/board/github-board.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ import { Pressable, Text, View } from "react-native";
import { BoardBody } from "./board-body";
import { BoardHeader } from "./board-header";
import { BoardFilterBar, BoardModeBar } from "./board-toolbar";
import { SendDialog } from "../launch/send-dialog";
import { BoardDialogs } from "./board-dialogs";
import { SavedViews } from "../search/saved-views";
import { useStyles } from "../theme/use-styles";
import { LabelMenu } from "./label-menu";
import { useGitHubBoard } from "./use-github-board";

export function GitHubBoard(props: PluginSurfaceProps) {
Expand Down Expand Up @@ -74,6 +74,7 @@ export function GitHubBoard(props: PluginSurfaceProps) {
<View ref={rootRef} style={styles.screen}>
<BoardHeader
surfaceProps={props}
showBoardStatus={mode !== "saved-views"}
styles={styles}
showSettings={showSettings}
setShowSettings={setShowSettings}
Expand All @@ -92,7 +93,7 @@ export function GitHubBoard(props: PluginSurfaceProps) {
/>
)}

{showSettings || mode === "projects" ? null : (
{showSettings || mode === "projects" || mode === "saved-views" ? null : (
<BoardFilterBar
visibleRelations={visibleRelations}
effectiveRelation={effectiveRelation}
Expand Down Expand Up @@ -128,75 +129,56 @@ export function GitHubBoard(props: PluginSurfaceProps) {
/>
) : null}

{error !== null ? (
{error !== null && mode !== "saved-views" ? (
<View style={styles.banner}>
<Text style={styles.danger}>{error}</Text>
</View>
) : null}

<BoardBody
surfaceProps={props}
styles={styles}
showSettings={showSettings}
promptValues={promptValues}
loginDraft={loginDraft}
busy={busy}
applyPrompts={applyPrompts}
applyLogin={applyLogin}
board={board}
mode={mode}
watchedOwners={watchedOwners}
displayRows={displayRows}
renderRow={renderRow}
modeRows={modeRows}
refresh={refresh}
bodyWidth={bodyWidth}
setBodyWidth={setBodyWidth}
detailTarget={detailTarget}
detailItem={detailItem}
detailProgress={detailProgress}
closeDetails={closeDetails}
savedFraction={savedFraction}
commitWidth={commitWidth}
openSendDialog={openSendDialog}
dropItem={dropItem}
/>

{labelTarget !== null ? (
<View style={styles.menuLayer}>
<Pressable
accessibilityRole="button"
accessibilityLabel="Close labels menu"
style={styles.menuScrim}
onPress={() => setLabelTarget(null)}
/>
<LabelMenu
// Keyed by card: opening the menu on a second card must not inherit
// the first one's applied set or its in-flight toggles.
key={labelTarget.item.id}
target={labelTarget}
styles={styles}
accentColor={props.theme.colors.accent}
onClose={() => setLabelTarget(null)}
onChanged={applyItemLabels}
/>
</View>
) : null}

{sendTarget !== null ? (
<SendDialog
// Keyed by card, so opening a second one never inherits the first
// one's prompt or its half-made choices.
key={sendTarget.item.id}
item={sendTarget.item}
initialPrompt={sendTarget.prompt}
hostLabel={props.host.label}
{!showSettings && mode === "saved-views" ? (
<SavedViews props={props} styles={styles} />
) : (
<BoardBody
surfaceProps={props}
styles={styles}
accentColor={props.theme.colors.accent}
onCancel={() => setSendTarget(null)}
onLaunched={handleLaunched}
showSettings={showSettings}
promptValues={promptValues}
loginDraft={loginDraft}
busy={busy}
applyPrompts={applyPrompts}
applyLogin={applyLogin}
board={board}
mode={mode}
watchedOwners={watchedOwners}
displayRows={displayRows}
renderRow={renderRow}
modeRows={modeRows}
refresh={refresh}
bodyWidth={bodyWidth}
setBodyWidth={setBodyWidth}
detailTarget={detailTarget}
detailItem={detailItem}
detailProgress={detailProgress}
closeDetails={closeDetails}
savedFraction={savedFraction}
commitWidth={commitWidth}
openSendDialog={openSendDialog}
dropItem={dropItem}
/>
) : null}
)}

<BoardDialogs
props={props}
styles={styles}
overlays={{
labelTarget,
setLabelTarget,
applyItemLabels,
sendTarget,
setSendTarget,
handleLaunched,
}}
/>
</View>
);
}
28 changes: 20 additions & 8 deletions client/board/item-row-format.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,13 +64,17 @@ export function describeRow({
const stampDate = stampMissing ? item.updatedAt : stamp;

const iconName =
type === "draft-prs"
? "GitPullRequestDraft"
: type === "open-prs"
? "GitPullRequest"
: type === "discussions"
? "MessageSquare"
: "CircleDot";
item.prState === "MERGED"
? "GitMerge"
: item.prState === "CLOSED"
? "GitPullRequestClosed"
: type === "draft-prs"
? "GitPullRequestDraft"
: type === "open-prs"
? "GitPullRequest"
: type === "discussions"
? "MessageSquare"
: "CircleDot";
const iconColor = type === "draft-prs" || type === "discussions" ? mutedColor : accentColor;

const byline = item.author !== null && item.author !== viewerLogin ? item.author : null;
Expand All @@ -85,5 +89,13 @@ export function describeRow({
accessibilityHint = isWeb ? "Right-click to edit labels." : "Press and hold to edit labels.";
}

return { stampLabel, stampDate, iconName, iconColor, byline, accessibilityLabel, accessibilityHint };
return {
stampLabel,
stampDate,
iconName,
iconColor,
byline,
accessibilityLabel,
accessibilityHint,
};
}
5 changes: 5 additions & 0 deletions client/board/item-row.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,11 @@ export const ItemRow = memo(function ItemRow({
<Text style={styles.itemRowTitle} numberOfLines={1}>
{item.title}
</Text>
{item.prState === "CLOSED" || item.prState === "MERGED" ? (
<Text style={styles.itemRowDraftPill}>
{item.prState === "MERGED" ? "Merged" : "Closed"}
</Text>
) : null}
{type === "draft-prs" ? <Text style={styles.itemRowDraftPill}>Draft</Text> : null}
</View>
<Text style={styles.itemRowMeta} numberOfLines={1}>
Expand Down
7 changes: 5 additions & 2 deletions client/board/label-menu.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { SEARCH_KEY } from "../lib/search-pages";
import { useCallback, useMemo, useState } from "react";
import {
ActivityIndicator,
Expand All @@ -8,7 +9,7 @@ import {
View,
} from "react-native";
import { useRpc } from "@getpaseo/plugin/client";
import { useQuery } from "@tanstack/react-query";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import type { BoardItem, RepositoryLabel } from "../../shared/board";
import { listLabels, toggleLabel } from "../../shared/board";
import type { Styles } from "../theme/use-styles";
Expand Down Expand Up @@ -58,6 +59,7 @@ export function LabelMenu({
const { item } = target;
const list = useRpc(listLabels);
const apply = useRpc(toggleLabel);
const queryClient = useQueryClient();

/**
* Each repository's label catalogue, keyed by `owner/name`. A label set
Expand Down Expand Up @@ -96,6 +98,7 @@ export function LabelMenu({
apply({ itemId: item.id, labelId: label.id, add })
.then((result) => {
setApplied(new Set(result.labels));
void queryClient.resetQueries({ queryKey: [SEARCH_KEY] });
onChanged(item.id, result.labels);
})
.catch((cause: unknown) => {
Expand All @@ -109,7 +112,7 @@ export function LabelMenu({
});
});
},
[applied, apply, item.id, onChanged, pending],
[applied, apply, item.id, onChanged, pending, queryClient],
);

const shown = useMemo(() => {
Expand Down
Loading