Files
openidea/src/features/github/toolwindow/store.ts
T
imacross 059fd8a69b feat(github): pull request tool window, timeline and review flow (#477)
Replaces the Pull Requests tool window with a full review workflow on top of gh's authentication.

- List with search, quick filters and paging; a tab per pull request and a New Pull Request tab.
- Details: commit picker, changes tree with viewed state, CI and reviewer status, merge, close and reopen.
- Timeline editor: comments, review threads, reactions and suggested changes.
- Review in the diff viewer and, for the checked-out branch, in the editor (review mode).
- New generic vcs extension points: registerDiffViewerProvider, lineInlays, reviewGutter, branch widget extensions; new vcs_merge_base and gh_api commands.
- CSP: img-src allows https://avatars.githubusercontent.com for avatars.

Fixed while testing against a real repository: remotes behind an SSH host alias, mergeability stuck on UNKNOWN, Apply Locally staying disabled after a checkout, relative links navigating the app's page, and review inlays wider than the pane.

Co-authored-by: imacross <[email protected]>
2026-10-31 07:50:43 +08:00

75 lines
2.6 KiB
TypeScript

/**
* Content tabs of the Pull Requests tool window (IntelliJ's `ReviewToolwindowTabsManager`):
* the list tab (not closable), one `#N` tab per opened pull request and the "New Pull
* Request" tab, each opened at most once. Also the list's selection, which actions such as
* Checkout Pull Request… use.
*/
import { create } from "zustand";
import { setContext } from "@/platform/context";
export type GhTab = { kind: "pr"; number: number } | { kind: "create" };
export type GhTabId = "list" | `pr:${number}` | "create";
interface TabsState {
tabs: GhTab[];
active: GhTabId;
/** The pull request selected in the list. */
listSelection: number | null;
}
export const useGhTabs = create<TabsState>(() => ({ tabs: [], active: "list", listSelection: null }));
export const tabId = (t: GhTab): GhTabId => (t.kind === "pr" ? `pr:${t.number}` : "create");
/** Opens (or selects) the `#N` tab. */
export function openPullRequestTab(number: number) {
useGhTabs.setState((s) => ({
tabs: s.tabs.some((t) => t.kind === "pr" && t.number === number) ? s.tabs : [...s.tabs, { kind: "pr", number }],
active: `pr:${number}`,
}));
}
/** Opens (or selects) the "New Pull Request at {repo}" tab. */
export function openCreateTab() {
useGhTabs.setState((s) => ({
tabs: s.tabs.some((t) => t.kind === "create") ? s.tabs : [...s.tabs, { kind: "create" }],
active: "create",
}));
}
export function selectTab(id: GhTabId) {
useGhTabs.setState({ active: id });
}
/** Closes a `#N` or the New Pull Request tab; the tab before it becomes active. */
export function closeTab(id: GhTabId) {
if (id === "list") return;
useGhTabs.setState((s) => {
const i = s.tabs.findIndex((t) => tabId(t) === id);
if (i < 0) return s;
const tabs = s.tabs.filter((_, j) => j !== i);
const before = tabs[i - 1];
const active = s.active === id ? (before ? tabId(before) : "list") : s.active;
return { tabs, active };
});
}
export function setListSelection(number: number | null) {
useGhTabs.setState({ listSelection: number });
}
/** The pull request of the active `#N` tab, else the list's selection. */
export function contextPullRequest(): number | null {
const s = useGhTabs.getState();
const m = /^pr:(\d+)$/.exec(s.active);
return m ? Number(m[1]) : s.listSelection;
}
/** Another repository or project: only the list tab stays. */
export function resetTabs() {
useGhTabs.setState({ tabs: [], active: "list", listSelection: null });
}
/** `githubPrTab`: `list`, `pr` or `create`, for the tab-specific shortcuts (Refresh). */
useGhTabs.subscribe((s) => setContext("githubPrTab", s.active.startsWith("pr:") ? "pr" : s.active));