Flesh out forge support in lite - #15212
Conversation
74a1492 to
89bca83
Compare
89bca83 to
e95204e
Compare
e95204e to
3dd7271
Compare
3dd7271 to
306673f
Compare
306673f to
9989d5e
Compare
9989d5e to
c2b5ee0
Compare
There was a problem hiding this comment.
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_enabledto 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/setLocalDocumentduring 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);
c2b5ee0 to
1d15daa
Compare
1d15daa to
f2bdb31
Compare
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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
childrenfrom react-markdown is commonly an array of strings (ReactNode), not a single string. The currenttypeof children === "string"check will skip theCodeBlockpath 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 !== falseevaluates totruewhileforgeInfois still loading (or null), so this can firelistReviewSubmissionsbefore 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
reactorsis still undefined (query in-flight). In that window,Reactionscan’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/setLocalDocumentduring render whenremoteHasUpdatedis 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 anifin the render body.
f2bdb31 to
9a087b7
Compare
There was a problem hiding this comment.
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/setLocalDocumentinside theif (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 auseEffect(keyed ontitle/body) and only update local state when the user hasn’t diverged.
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.
9a087b7 to
aae37b0
Compare
There was a problem hiding this comment.
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
listReviewSubmissionsQueryOptionsis gated withenabled: forgeInfo?.capabilities.reviewComments !== false, which evaluates totruewhileforgeInfois still loading (undefined). That can trigger an early submissions request even on forges/backends wherereviewCommentslater resolves tofalse, potentially producing avoidable errors and wasted work. Gate onforgeInfo != nullfirst 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) insideif (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 ontitle/body(and keepremoteOrEmptyDocumentstable), so updates happen after commit rather than during render.
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
+buttonsThe 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
ForgeReviewwith 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 newForgeCapabilitiesentries 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 nativetitletooltips never show in the Electron window.Left for follow-up branches: