Skip to content

Flesh out forge support in lite - #15212

Merged
mtsgrd merged 1 commit into
masterfrom
fable/lite-pr-panel
Aug 7, 2026
Merged

Flesh out forge support in lite#15212
mtsgrd merged 1 commit into
masterfrom
fable/lite-pr-panel

Conversation

@mtsgrd

@mtsgrd mtsgrd commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

The PR tab in lite grows up from a title/description form into an actual pull request view, closely following the Figma design.

Fittingly, this description uses the same GitHub-flavored markdown the branch teaches lite to render — the screenshot below is lite drawing a table just like the one here.

The tour

Area What you get
Metadata panel Status badge, author, reviewers with verdicts, labels, checks, branches, created/updated — a bordered card like the design
Markdown Rendered descriptions + comments, shiki syntax highlighting, edit mode with Cancel
Activity Comments, review submissions, pushed commits, and review requests interleaved with connector dashes, plus a composer. Edit or delete your own comments, quote-reply, and long content folds behind Show more
Reactions Chips on the PR and its comments with who-reacted tooltips; toggle your own or add from an animated emoji popover
Merging Method picker (remembered per project), working auto-merge toggle, a tooltip explaining why merge is blocked, Close under a kebab
Management Add/remove labels and request/withdraw reviewers from the panel's + buttons
Freshness Polls while focused, sleeps in the background, catches up on refocus. Posting, reaction toggles, and the auto-merge switch apply optimistically — instant UI, clean rollback on failure, and the composer draft survives a failed post

The markdown gets GitHub-parity sanitization (rehype-sanitize's GitHub-modeled schema): safe HTML like the <details> block below folds in lite exactly like it does here, while scripts, styles, event handlers, and unsafe URLs are stripped — all pinned by tests. Images inline when GitHub hosts them (same privacy story as github.com — GitHub's image CDNs don't leak viewer requests to authors); any other host renders as a link and is never fetched.

Under the hood

New forge endpoints end to end (GitHub client → but-forge → but-api → SDK → electron IPC): comments list/create/edit/delete, review submissions, issue-timeline events, reaction reads and writes (kinds are an open set of forge-native names, ready for GitLab's arbitrary award emoji), repo labels, assignable users, label add/remove, review request/withdraw, a current-login lookup so the UI knows which comments are yours, plus auto-merge state on ForgeReview with a DB migration and struct-version bump (the e2e review-seeding fixture follows the bump). GitLab/Bitbucket read as empty for the new listings instead of erroring; writes are GitHub-only for now, gated through new ForgeCapabilities entries rather than forge-name checks.

The PR view's pieces live in their own modules now (PullRequestTab, Panel, Comments, Reactions), and every relative timestamp carries its absolute time in a tooltip — rendered by the app's own tooltip component, since native title tooltips never show in the Electron window.

image

Left for follow-up branches:

  • Fire the remaining write paths at a real PR (merge, labels, reviewer requests — comments and reactions have been exercised live)
  • GitLab parity for the new listings

@mtsgrd
mtsgrd force-pushed the fable/lite-pr-panel branch from 74a1492 to 89bca83 Compare August 6, 2026 09:06
@mtsgrd
mtsgrd force-pushed the fable/lite-pr-panel branch from 89bca83 to e95204e Compare August 6, 2026 09:56
@mtsgrd
mtsgrd force-pushed the fable/lite-pr-panel branch from e95204e to 3dd7271 Compare August 6, 2026 10:47
@github-actions github-actions Bot added rust Pull requests that update Rust code CLI The command-line program `but` labels Aug 6, 2026
@mtsgrd
mtsgrd force-pushed the fable/lite-pr-panel branch from 3dd7271 to 306673f Compare August 6, 2026 20:39
@mtsgrd
mtsgrd force-pushed the fable/lite-pr-panel branch from 306673f to 9989d5e Compare August 6, 2026 22:44
@mtsgrd
mtsgrd force-pushed the fable/lite-pr-panel branch from 9989d5e to c2b5ee0 Compare August 6, 2026 23:04
@mtsgrd
mtsgrd marked this pull request as ready for review August 6, 2026 23:50
Copilot AI lite review requested due to automatic review settings August 6, 2026 23:50
@mtsgrd
mtsgrd enabled auto-merge August 6, 2026 23:51
@mtsgrd
mtsgrd disabled auto-merge August 6, 2026 23:57

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Expands Lite’s forge (PR) support from a simple title/description form into a fuller pull request experience (metadata panel, markdown rendering, activity timeline, reactions, and management actions), backed by new but-forge/but-api/but-sdk endpoints and a DB schema bump for auto-merge state.

Changes:

  • Add end-to-end forge APIs for PR conversation comments, reactions, timeline/submissions, labels, reviewer requests, and current login discovery.
  • Implement new Lite PR tab UI modules (description + edit mode, activity feed + composer, reactions UI, metadata panel) with GitHub-flavored markdown rendering and sanitization.
  • Add auto_merge_enabled to cached forge review storage (DB migration + fixtures/tests) and wire it through clients (GitHub/GitLab).

Reviewed changes

Copilot reviewed 47 out of 54 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
packages/but-sdk/src/generated/linear/index.js Export newly generated forge/PR endpoints from the Linear-flavored SDK bundle.
packages/but-sdk/src/generated/linear/index.d.ts Add type declarations for new forge/PR endpoints and new forge review/comment/reaction types.
packages/but-sdk/src/generated/graph/index.js Export newly generated forge/PR endpoints from the Graph-flavored SDK bundle.
packages/but-sdk/src/generated/graph/index.d.ts Add type declarations for new forge/PR endpoints and new forge review/comment/reaction types.
e2e/playwright/scripts/cache-forge-review.sh Bump cached review struct_version for the new forge review schema.
crates/but/src/command/mcp/mod.rs Update test fixtures to include auto_merge_enabled.
crates/but/src/command/legacy/forge/review.rs Update legacy forge review test fixtures for auto_merge_enabled.
crates/but-gitlab/src/client.rs Plumb GitLab “merge when pipeline succeeds” into auto_merge_enabled.
crates/but-github/src/pr.rs Add GitHub PR sub-APIs for comments, reactions, labels, reviewers, timeline, reviews.
crates/but-github/src/lib.rs Re-export new GitHub client types used by the forge layer.
crates/but-forge/src/lib.rs Re-export new forge review/comment/reaction APIs and add current_forge_login.
crates/but-forge/src/forge_info.rs Extend capabilities with review_comments and review_management flags.
crates/but-forge/src/db.rs Store/load auto_merge_enabled in the forge review DB mapping.
crates/but-forge/src/association.rs Update association tests for the new forge review field.
crates/but-db/tests/db/table/forge_review.rs Update DB table tests to include auto_merge_enabled.
crates/but-db/tests/db/migration.rs Update migration snapshot and add the new migration timestamp.
crates/but-db/src/table/forge_reviews.rs Add migration + schema field for auto_merge_enabled and update queries/inserts.
crates/but-api/tests/api/forge_pr_association.rs Update API tests’ review fixtures for auto_merge_enabled.
crates/but-api/tests/api/branch_list.rs Update branch list test fixture reviews for auto_merge_enabled.
crates/but-api/src/workspace.rs Update workspace tests’ review fixtures for auto_merge_enabled.
crates/but-api/src/legacy/forge.rs Add NAPI endpoints for comments, reactions, timeline/submissions, labels/reviewers, current login.
apps/lite/ui/src/routes/project/$id/workspace/UpstreamList.tsx Switch commit timestamp rendering to RelativeTime with tooltip absolute time.
apps/lite/ui/src/routes/project/$id/workspace/PullRequestTab.tsx New PR tab building blocks: description rendering/edit form and primary action row.
apps/lite/ui/src/routes/project/$id/workspace/PullRequestTab.module.css Styles for PR tab form/view and primary action layout.
apps/lite/ui/src/routes/project/$id/workspace/PullRequestReactions.tsx New reactions chip row + picker with who-reacted tooltips and toggling.
apps/lite/ui/src/routes/project/$id/workspace/PullRequestReactions.module.css Styles for reactions chips and emoji picker popover.
apps/lite/ui/src/routes/project/$id/workspace/PullRequestPanel.tsx New metadata side panel (status, checks summary, labels, reviewers, branches, timestamps).
apps/lite/ui/src/routes/project/$id/workspace/PullRequestPanel.module.css Styles for the PR metadata panel and sections.
apps/lite/ui/src/routes/project/$id/workspace/PullRequestComments.tsx New activity timeline (opened, comments, reviews, events) + composer and comment actions.
apps/lite/ui/src/routes/project/$id/workspace/PullRequestComments.module.css Styles for activity timeline rows, cards, connectors, and composer.
apps/lite/ui/src/routes/project/$id/workspace/Details.tsx Refactor to compose new PR tab modules and wire edit-mode state into header actions.
apps/lite/ui/src/routes/project/$id/workspace/Details.module.css Add PR layout sizing variables and PR tab layout styles.
apps/lite/ui/src/routes/project/$id/workspace/BranchesList.tsx Switch “last touched” timestamp rendering to RelativeTime.
apps/lite/ui/src/pr.ts Persist per-project merge method selection via idb + react-query.
apps/lite/ui/src/components/RelativeTime.tsx New tooltip-backed relative timestamp component (absolute time on hover).
apps/lite/ui/src/components/Markdown.tsx New markdown renderer with GFM, sanitization, shiki highlighting, and GitHub-only inline images.
apps/lite/ui/src/components/Markdown.test.tsx Add tests enforcing markdown sanitization and inline-image policy.
apps/lite/ui/src/components/Markdown.module.css Styling for rendered markdown, tables, blockquotes, and code highlighting.
apps/lite/ui/src/components/icons/smiley.svg Add “smiley” icon for reaction picker trigger.
apps/lite/ui/src/components/icons/kebab-vertical.svg Add vertical kebab icon for comment actions.
apps/lite/ui/src/components/iconNames.ts Regenerate icon name union to include new icons.
apps/lite/ui/src/components/Clamped.tsx New fold/expand component with ResizeObserver remeasurement and fade/toggle UI.
apps/lite/ui/src/components/Clamped.stories.tsx Storybook coverage for clamp behaviors and late content growth.
apps/lite/ui/src/components/Clamped.module.css Styles for clamp overflow fade and toggle button.
apps/lite/ui/src/components/Annotation.tsx Switch annotation timestamp rendering to RelativeTime.
apps/lite/ui/src/api/queries.ts Add new PR-related react-query query options and polling behaviors.
apps/lite/ui/src/api/mutations.ts Add PR mutations (labels, comments, reactions, reviewer requests) incl. optimistic updates and invalidations.
apps/lite/package.json Add markdown/rendering dependencies (react-markdown, rehype-raw/sanitize, remark-gfm).
apps/lite/electron/src/preload.cts Expose new PR-related IPC APIs to the renderer preload.
apps/lite/electron/src/main.ts Register IPC handlers for new PR endpoints and widen CSP img-src for GitHub images.
apps/lite/electron/src/ipc.ts Add IPC types + channels for PR comments/reactions/labels/reviewers/timeline/current login.
Files not reviewed (1)
  • pnpm-lock.yaml: Generated file
Suppressed comments (2)

apps/lite/ui/src/routes/project/$id/workspace/PullRequestTab.tsx:124

  • This component calls setPrevRemote / setLocalDocument during render (if (remoteHasUpdated) { ... }). Setting state while rendering can cause render loops and will trigger React warnings; this sync should be moved into an effect that runs when the remote title/body changes.
	if (remoteHasUpdated) {
		setPrevRemote(remoteOrEmptyDocument);

		const localHasDiverged =
			localDocument.title !== prevRemote.title || localDocument.body !== prevRemote.body;

apps/lite/ui/src/routes/project/$id/workspace/PullRequestComments.tsx:348

  • timelineItems(...) sorts and allocates a new array on every render, so typing in the comment composer (state updates) will repeatedly rebuild the whole activity list. Memoize this derivation so it only recomputes when the review/comments/submissions/events change.
	const items = timelineItems(review, comments, submissions, events);

Comment thread apps/lite/ui/src/routes/project/$id/workspace/PullRequestTab.tsx Outdated
Comment thread apps/lite/ui/src/routes/project/$id/workspace/PullRequestReactions.tsx Outdated
@mtsgrd
mtsgrd enabled auto-merge August 7, 2026 00:02
@mtsgrd
mtsgrd disabled auto-merge August 7, 2026 00:03
@mtsgrd
mtsgrd enabled auto-merge August 7, 2026 00:04
@mtsgrd
mtsgrd disabled auto-merge August 7, 2026 00:05
@mtsgrd
mtsgrd enabled auto-merge August 7, 2026 00:11
@mtsgrd
mtsgrd disabled auto-merge August 7, 2026 00:13
Copilot AI review requested due to automatic review settings August 7, 2026 00:23
@mtsgrd
mtsgrd force-pushed the fable/lite-pr-panel branch from c2b5ee0 to 1d15daa Compare August 7, 2026 00:23
@mtsgrd
mtsgrd force-pushed the fable/lite-pr-panel branch from 1d15daa to f2bdb31 Compare August 7, 2026 00:27

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 51 out of 58 changed files in this pull request and generated no new comments.

Files not reviewed (1)
  • pnpm-lock.yaml: Generated file
Suppressed comments (1)

apps/lite/ui/src/routes/project/$id/workspace/PullRequestTab.tsx:132

  • This block calls setPrevRemote() / setLocalDocument() during render when the remote title/body changes. React state updates during render can cause warnings and (in edge cases) render loops; this should be driven by an effect/ref so the update happens after commit.

One minimal option is to track the previous remote document in a ref and defer the setLocalDocument update with a microtask (or move this logic into a useEffect).

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 51 out of 58 changed files in this pull request and generated no new comments.

Files not reviewed (1)
  • pnpm-lock.yaml: Generated file
Suppressed comments (4)

apps/lite/ui/src/components/Markdown.tsx:180

  • Fenced code highlighting likely never triggers because children from react-markdown is commonly an array of strings (ReactNode), not a single string. The current typeof children === "string" check will skip the CodeBlock path for most fenced blocks, leaving them permanently unhighlighted.
				code: ({ node: _node, className, children, ...props }) => {
					const language = fencedLanguage(className);
					return language !== undefined && typeof children === "string" ? (
						<CodeBlock language={language} code={children.replace(/\n$/, "")} />
					) : (

apps/lite/ui/src/routes/project/$id/workspace/PullRequestPanel.tsx:198

  • enabled: forgeInfo?.capabilities.reviewComments !== false evaluates to true while forgeInfo is still loading (or null), so this can fire listReviewSubmissions before capabilities are known. That defeats the intended gating and can cause avoidable errors/requests on non-supporting forges/backends.
	const { data: reviewers } = useQuery({
		...listReviewSubmissionsQueryOptions({ projectId, reviewId: review.number }),
		// Fail open: an older backend without the field can still read comments.
		enabled: forgeInfo?.capabilities.reviewComments !== false,
		select: (submissions) => reviewerRows(review.reviewers, submissions),

apps/lite/ui/src/routes/project/$id/workspace/PullRequestComments.tsx:176

  • Reactions are toggleable even while reactors is still undefined (query in-flight). In that window, Reactions can’t detect the caller’s existing reaction, so clicking a chip can incorrectly attempt an add (and the optimistic update will bump counts) even if the user already reacted.
							<Reactions
								reactions={comment.reactions}
								reactors={reactors}
								myLogin={currentLogin}
								onToggle={toggleReaction}

apps/lite/ui/src/routes/project/$id/workspace/PullRequestTab.tsx:121

  • This component calls setPrevRemote / setLocalDocument during render when remoteHasUpdated is true. Updating state during render can cause React warnings and (in concurrent rendering) can lead to render loops or inconsistent state; this sync should be done in an effect (or via refs) instead of an if in the render body.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 52 out of 59 changed files in this pull request and generated 1 comment.

Files not reviewed (1)
  • pnpm-lock.yaml: Generated file
Suppressed comments (1)

apps/lite/ui/src/routes/project/$id/workspace/PullRequestTab.tsx:132

  • This component updates state during render (setPrevRemote / setLocalDocument inside the if (remoteHasUpdated) block). That can trigger render loops and React warnings, and it makes the update order hard to reason about. Move this remote-sync logic into a useEffect (keyed on title/body) and only update local state when the user hasn’t diverged.

Comment thread apps/lite/ui/src/api/mutations.ts
The lite PR tab becomes a full pull request view: a metadata panel
(status, author, reviewers with verdicts, labels, checks, branches),
GitHub-parity markdown with shiki syntax highlighting and an edit mode,
an activity timeline interleaving comments, review submissions, pushed
commits, and review requests with a composer — own comments editable and
deletable, with quote-reply — merging with a per-project method picker
and auto-merge toggle, label and reviewer management, reactions on the
PR and its comments — chips toggle your own, an animated popover picker
adds, who-reacted in the tooltip — absolute-time tooltips on every
relative timestamp, and review-state polling that only runs while the
app is focused. Comment posting, reaction toggles, and the auto-merge
switch apply optimistically: instant UI, clean rollback on failure, and
the composer draft comes back if a post fails.

Under the hood, the previously missing forge endpoints land end to end
(GitHub client → but-forge → but-api → SDK → electron IPC): comment
CRUD, review submissions, issue-timeline events, reaction reads and
writes, repo labels, assignable users, review requests, a current-login
lookup for ownership checks, and auto-merge state on ForgeReview with a
DB migration (the e2e review-seeding fixture follows the struct_version
bump). Failed forge calls report what GitHub said, not just the
status, while keeping the 401 classification downcast. GitLab and
Bitbucket read as empty for the new listings rather than erroring;
writes are GitHub-only for now. Markdown gets
GitHub-parity sanitization, and images inline when GitHub hosts them —
other hosts render as links and are never fetched. Safety is pinned by
tests, and the review pass caught the url crate silently dropping "."
and ".." path segments — which would have turned "remove one label"
into "remove all labels".

The PR view's components live in their own modules (PullRequestTab,
Panel, Comments), and per-forge feature gating goes through new
ForgeCapabilities entries (review_comments, review_management) instead
of forge-name checks — GitLab support becomes two booleans away.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 52 out of 59 changed files in this pull request and generated no new comments.

Files not reviewed (1)
  • pnpm-lock.yaml: Generated file
Suppressed comments (2)

apps/lite/ui/src/routes/project/$id/workspace/PullRequestPanel.tsx:200

  • listReviewSubmissionsQueryOptions is gated with enabled: forgeInfo?.capabilities.reviewComments !== false, which evaluates to true while forgeInfo is still loading (undefined). That can trigger an early submissions request even on forges/backends where reviewComments later resolves to false, potentially producing avoidable errors and wasted work. Gate on forgeInfo != null first so the capability check is only evaluated once forge info has loaded (still “fail-open” when the field is missing).
		...listReviewSubmissionsQueryOptions({ projectId, reviewId: review.number }),
		// Fail open: an older backend without the field can still read comments.
		enabled: forgeInfo?.capabilities.reviewComments !== false,
		select: (submissions) => reviewerRows(review.reviewers, submissions),
	});

apps/lite/ui/src/routes/project/$id/workspace/PullRequestTab.tsx:127

  • This component performs render-phase state updates (setPrevRemote / setLocalDocument) inside if (remoteHasUpdated) { ... }. Even though the condition converges, render-time state updates can cause extra render passes and are fragile under StrictMode / React Compiler assumptions. Consider moving this “sync local draft from remote unless locally diverged” logic into an effect keyed on title/body (and keep remoteOrEmptyDocument stable), so updates happen after commit rather than during render.

@krlvi krlvi left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@mtsgrd
mtsgrd merged commit 124f0ed into master Aug 7, 2026
44 checks passed
@mtsgrd
mtsgrd deleted the fable/lite-pr-panel branch August 7, 2026 10:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLI The command-line program `but` rust Pull requests that update Rust code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants