Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
dbbd417
refactor(guided-review): keep the review list's preferences and check…
sleeyax Oct 12, 2026
580e7ac
refactor(guided-review): name the forge's inbox listing for change re…
sleeyax Oct 12, 2026
94bd5ff
feat(guided-review): list the viewer's own open GitHub PRs beside the…
sleeyax Oct 12, 2026
c7f9c67
feat(guided-review): show the viewer's own change requests on an Auth…
sleeyax Oct 12, 2026
ca49884
feat(guided-review): list the viewer's own open GitLab MRs beside the…
sleeyax Oct 12, 2026
5145d6e
perf(guided-review): count a GitLab MR's drafts once when it is on bo…
sleeyax Oct 12, 2026
d4e9f18
feat(guided-review): show where reviewers stand on Authored by me
sleeyax Oct 12, 2026
1afdeee
feat(guided-review): check off change requests on Authored by me
sleeyax Oct 12, 2026
41c41c2
fix(guided-review): keep a re-requested reviewer's change request on …
sleeyax Oct 12, 2026
0faf5fb
fix(guided-review): read GitHub reviewers' verdicts past a later comm…
sleeyax Oct 12, 2026
2a15fae
fix(guided-review): time a GitLab review against the head's push on A…
sleeyax Oct 12, 2026
7710332
fix(guided-review): put an Assigned card's tinted line back in its ol…
sleeyax Oct 12, 2026
8616e6b
refactor(guided-review): key the review list's lists by tab
sleeyax Oct 12, 2026
0bafb38
refactor(guided-review): drop the review list's options no tab uses
sleeyax Oct 12, 2026
9e5f9ba
docs(guided-review): say what an approval reset means the same way ev…
sleeyax Oct 12, 2026
3153097
docs(guided-review): put each sentence of the listing comments on its…
sleeyax Oct 12, 2026
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: 17 additions & 6 deletions plugins/guided-review/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,18 +38,29 @@ To guide the branch you already have checked out, choose **Guided Review: guide
If your branch is clean and behind the PR/MR, the plugin fast-forwards it and writes the guide in your workspace.
Otherwise it leaves your branch alone and uses a new workspace.

### Pick from your review requests
### Pick from your review requests or your own

**Assigned to me for review** lists the open pull requests and merge requests you are a reviewer of, on github.com and on every GitLab host `glab` is logged in to.
The start tab lists open pull requests and merge requests in two tabs.
**Assigned to me** lists the ones you are a reviewer of, on github.com and on every GitLab host `glab` is logged in to.
**Authored by me** lists your own on the same hosts, drafts included.
Its review status combines your reviewers' latest verdicts, and changes requested by any reviewer outweigh approvals.
An approval GitLab removes, or GitHub dismisses, when you push shows as an approval reset.
On GitLab, an approval its reviewer revokes shows as an approval reset too.
A merge request you wrote and review is on both tabs, with the one review you started from either.

- Select **Review** to start a review, **Open** to open one, or **Continue** to reopen one whose workspace was closed.
- Filter by forge, by state under **States**, with **Needs my attention** or **Hide drafts**, or search.
- Filter by forge, by state under **States**, with **Needs my attention** or with **Hide drafts**, or search.
On **Authored by me**, the search also matches reviewers.
On **Authored by me**, **Needs my attention** keeps changes requested, a review since your last push, and failing CI.
- Select a column heading to sort, and **Columns** to choose what the table shows.
- Each tab keeps its own filters, sort, columns and search.
- Tick a row's box to check it off while it waits on someone else.
It comes back when someone pushes to it, asks you to review again, or GitLab resets your approval.
- Select **Refresh** to list again.
On **Assigned to me**, it comes back when someone pushes to it, asks you to review again, or GitLab resets your approval.
On **Authored by me**, it comes back when a reviewer reviews again or its review status changes, an approval reset included, but not when you push.
A check-off on one tab leaves the other tab alone.
- Select **Refresh** to list both tabs again.

The count beside **Guided Review** in the sidebar is how many reviews need your attention.
The count beside **Guided Review** in the sidebar is how many review requests on **Assigned to me** need your attention.

If a review workspace shows no panel, select **Open Guided Review** in the workspace's header.

Expand Down
45 changes: 30 additions & 15 deletions plugins/guided-review/client/check-off.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,40 +3,54 @@ import { useIsMutating, useMutation, useQueryClient } from "@tanstack/react-quer
import { createContext, useState } from "react";
import * as contracts from "../shared/contracts.ts";
import { PLUGIN_ID } from "../shared/identity.ts";
import type { Inbox, InboxItem } from "../shared/inbox.ts";
import type { AuthoredInboxItem, CheckOffChange, Inbox, InboxItem } from "../shared/inbox.ts";
import { withCheckedOff, type ListedItem } from "./inbox-filter.ts";

export const INBOX_QUERY_KEY = [PLUGIN_ID, "inbox"];
const MUTATION_KEY = [PLUGIN_ID, "check-off"];

export type CheckOffControl = {
toggle(item: InboxItem): void;
/** What a tab's check-off records of its row, so the server can tell when it came back. */
export type CheckOffRule<Item extends ListedItem> = (item: Item, checkedOff: boolean) => CheckOffChange;

export const ASSIGNED_CHECK_OFF: CheckOffRule<InboxItem> = (item, checkedOff) => ({
tab: "assigned",
url: item.url,
checkOff: checkedOff ? { headSha: item.headSha, state: item.state } : null,
});

export const AUTHORED_CHECK_OFF: CheckOffRule<AuthoredInboxItem> = (item, checkedOff) => ({
tab: "authored",
url: item.url,
checkOff: checkedOff ? { status: item.status, latestReviewAt: item.latestReviewAt } : null,
});

export type CheckOffControl<Item extends ListedItem> = {
toggle(item: Item): void;
/** The rows checked off or unchecked since the list was last refreshed, filtered or searched, which stay where they are until then. */
held: ReadonlySet<string>;
release(): void;
/** A check-off is being saved, so a refresh now could read the list from before it. */
/** A check-off on either tab is being saved, so a refresh now could read the list from before it. */
saving: boolean;
/** Why the latest check-off could not be kept, as a sentence. */
error: string | null;
};

/**
* Checking reviews off the list and unchecking them.
* Checking change requests off one tab of the list and unchecking them.
* A toggle shows at once in the listed inbox and is saved behind it, and put back if the save fails.
*/
export function useCheckOff(): CheckOffControl {
export function useCheckOff<Item extends ListedItem>(rule: CheckOffRule<Item>): CheckOffControl<Item> {
const setCheckedOff = useRpc(contracts.setCheckedOff);
const queryClient = useQueryClient();
const [held, setHeld] = useState<ReadonlySet<string>>(() => new Set());
const [error, setError] = useState<string | null>(null);
const mark = (url: string, checkedOff: boolean) =>
queryClient.setQueryData<Inbox>(INBOX_QUERY_KEY, (inbox) =>
inbox && { ...inbox, items: inbox.items.map((item) => (item.url === url ? { ...item, checkedOff } : item)) },
);
const mark = ({ tab, url }: CheckOffChange, checkedOff: boolean) =>
queryClient.setQueryData<Inbox>(INBOX_QUERY_KEY, (inbox) => inbox && withCheckedOff(inbox, tab, url, checkedOff));
const save = useMutation({
mutationKey: MUTATION_KEY,
mutationFn: setCheckedOff,
onError: (failure, { url, checkOff }) => {
mark(url, checkOff === null);
onError: (failure, change) => {
mark(change, change.checkOff === null);
setError(failure instanceof Error ? failure.message : String(failure));
},
});
Expand All @@ -45,11 +59,12 @@ export function useCheckOff(): CheckOffControl {
return {
toggle(item) {
const checkedOff = !item.checkedOff;
const change = rule(item, checkedOff);
void queryClient.cancelQueries({ queryKey: INBOX_QUERY_KEY });
mark(item.url, checkedOff);
mark(change, checkedOff);
setHeld((rows) => new Set(rows).add(item.url));
setError(null);
save.mutate({ url: item.url, checkOff: checkedOff ? { headSha: item.headSha, state: item.state } : null });
save.mutate(change);
},
held,
release: () => setHeld(new Set()),
Expand All @@ -59,7 +74,7 @@ export function useCheckOff(): CheckOffControl {
}

/** What a row's checkbox needs from the list, so the column's cell takes no more than every other cell. */
export const CheckOffContext = createContext<{ toggle(item: InboxItem): void; dimmed(item: InboxItem): boolean }>({
export const CheckOffContext = createContext<{ toggle(item: ListedItem): void; dimmed(item: ListedItem): boolean }>({
toggle: () => {},
dimmed: () => false,
});
Loading
Loading