From 499e2a40f6357b3675f0b30577428f7e33b81029 Mon Sep 17 00:00:00 2001 From: Matt Toohey Date: Wed, 28 Oct 2026 00:38:08 +1100 Subject: [PATCH 01/15] feat(github): show pull request state as the icon of PR links in messages GitHub pull request links in messages rendered the Links plugin's static octocat. The GitHub plugin now contributes a `LinkRenderer` (order -1, below Links' default band) for `kind === "pull"` URLs that reuses Links' `LinkLabel` with the pull request's state glyph: open (success), draft and closed (subtle), merged (categorical purple). The plain pull request glyph shows until the state is read, and when it cannot be, so the icon never swaps. Repositories, issues and commits stay with Links, which also stands in when the plugin is disabled or the renderer fails. The batched, visibility-aware state poller moves out of `createSoloPulls` into a URL-keyed `createPullStates(gh)` store shared by every session's sidebar and every inline link, so one pull request shown in both costs one read. Merged stays terminal, closed is read at the slow five-minute cadence, and without `gh` each pull request is read from the public API once. The state icons, words and tone move to `pull-state-view.ts` so the sidebar, panel and inline link agree; closed stays grey on all three. `LinkLabel` gains an optional `icon` override. Docs and the plugin README note the renderer and that Links remains the fallback. Co-Authored-By: Claude Fable 5.1 Signed-off-by: Matt Toohey --- docs/plugin-architecture.md | 8 +- src/bundled/github/GitHub.module.css | 13 +- src/bundled/github/PullLink.test.tsx | 200 +++++++++++++++++++ src/bundled/github/PullLink.tsx | 34 ++++ src/bundled/github/README.md | 4 +- src/bundled/github/SoloPull.test.tsx | 6 +- src/bundled/github/SoloPull.tsx | 39 +--- src/bundled/github/index.tsx | 28 ++- src/bundled/github/pull-state-view.ts | 44 ++++ src/bundled/github/pull-states.test.ts | 262 ++++++++++++++++++++++++ src/bundled/github/pull-states.ts | 266 +++++++++++++++++++++++++ src/bundled/github/solo-pull.test.ts | 245 +++-------------------- src/bundled/github/solo-pull.ts | 259 +++++------------------- src/bundled/links/InlineLink.test.tsx | 19 ++ src/bundled/links/InlineLink.tsx | 10 +- 15 files changed, 957 insertions(+), 480 deletions(-) create mode 100644 src/bundled/github/PullLink.test.tsx create mode 100644 src/bundled/github/PullLink.tsx create mode 100644 src/bundled/github/pull-state-view.ts create mode 100644 src/bundled/github/pull-states.test.ts create mode 100644 src/bundled/github/pull-states.ts diff --git a/docs/plugin-architecture.md b/docs/plugin-architecture.md index 57cf83bc34..a5d214d692 100644 --- a/docs/plugin-architecture.md +++ b/docs/plugin-architecture.md @@ -1399,7 +1399,13 @@ does not fetch metadata or infer a service from names in paths or query strings. Its `order` is a function: recognised services and Buzz entities claim the default band, the globe case the catch-all band, so a renderer dedicated to one website wins it at the default. -It does not fetch titles. Messages currently recognize +It does not fetch titles. The bundled GitHub plugin contributes a pull request +renderer at a lower `order` for github.com pull request URLs: the same label +with the pull request's state (open, draft, merged, closed) as a coloured icon, +read with the person's `gh` sign-in or the public API and kept current while +shown. Repositories, issues and commits, and every pull request link when that +plugin is disabled or its renderer fails, keep the Links presentation. +Messages currently recognize credential-free HTTP(S) and supported Buzz links. Markdown labels preserve their formatting, escaped pasted wrappers are normalized outside code, and paired `<…>` autolink wrappers are hidden in display. Buzz links use known channel names with corresponding icons, falling back diff --git a/src/bundled/github/GitHub.module.css b/src/bundled/github/GitHub.module.css index 667e23a4fc..fa8116b14e 100644 --- a/src/bundled/github/GitHub.module.css +++ b/src/bundled/github/GitHub.module.css @@ -393,9 +393,17 @@ justify-content: center; width: var(--space-5); flex: 0 0 var(--space-5); +} +/* The sidebar's icon and a message link's icon take one colour per state, as + the panel's chip above does; a link's text stays a link. Draft, closed and + not yet read are quiet: closed is not red here. Only the sidebar shows an + open one's checks and conflicts. */ +.soloPull, +.pullState { color: var(--text-subtle); } -.soloPull[data-tone="open"] { +.soloPull[data-tone="open"], +.pullState[data-pr-state="open"] { color: var(--text-success); } .soloPull[data-tone="failure"] { @@ -404,7 +412,8 @@ .soloPull[data-tone="pending"] { color: var(--text-warning); } -.soloPull[data-tone="merged"] { +.soloPull[data-tone="merged"], +.pullState[data-pr-state="merged"] { color: var(--text-categorical-purple); } .soloLink { diff --git a/src/bundled/github/PullLink.test.tsx b/src/bundled/github/PullLink.test.tsx new file mode 100644 index 0000000000..43d9b19213 --- /dev/null +++ b/src/bundled/github/PullLink.test.tsx @@ -0,0 +1,200 @@ +// @vitest-environment jsdom +import "@testing-library/jest-dom/vitest"; +import { act, cleanup, render, screen, waitFor } from "@testing-library/react"; +import { renderToStaticMarkup } from "react-dom/server"; +import { afterEach, expect, it, vi } from "vitest"; +import type { Contribution } from "../../plugins/contributions"; +import type { LinkRenderer } from "../../features/conversation/contracts"; +import { LinkLabelContext } from "../../features/conversation/LinkLabelContext"; +import { + MessageLink, + resolveLink, +} from "../../features/conversation/MessageLink"; +import { ToastProvider } from "../../shared/design-system/ui/Toast"; +import inlineStyles from "../../shared/InlineReference.module.css"; +import { apply as applyLinks } from "../links/index"; +import { apply } from "./index"; +import { PullLink } from "./PullLink"; +import { stateIcons } from "./pull-state-view"; +import type { PullState, PullStates } from "./pull-states"; + +afterEach(() => { + cleanup(); + vi.unstubAllGlobals(); +}); + +const pull = "https://github.com/block/buzz-app/pull/7"; + +/** A store the test moves by hand. */ +function fakeStates(initial?: PullState | "failed") { + let value = initial; + const listeners = new Set<() => void>(); + const unwatch = vi.fn(); + return { + watch: vi.fn((_url: string, listener: () => void) => { + listeners.add(listener); + return () => { + listeners.delete(listener); + unwatch(); + }; + }), + get: () => value, + unwatch, + set(next: PullState | "failed" | undefined) { + value = next; + for (const listener of listeners) listener(); + }, + } satisfies PullStates & Record; +} +const glyph = (Icon: (typeof stateIcons)[keyof typeof stateIcons]) => + renderToStaticMarkup( +