2026-06-23 12:27:51 -07:00
|
|
|
import { useMutation, useQueryClient } from "@tanstack/react-query"
|
|
|
|
|
import {
|
feat: inline PR comments in the reviews UI (#1600)
* feat: inline PR comments in the reviews UI
Click the diff gutter "+" on a line to open an inline comment composer
(rendered like the finding card via a Pierre annotation); submitting
posts a real inline PR review comment as the signed-in user through a
new POST /reviews/{owner}/{repo}/{number}/comments. The "+" press-drag →
"Add to Chat" selection path is unchanged.
* feat: GitHub-parity comment box, PR comments dropdown, collapse nav
- Comment composer now mirrors GitHub's box: Write/Preview tabs (markdown
rendered via the existing Markdown component) and a markdown toolbar
(heading, bold, italic, quote, code, link, bulleted/numbered/task list).
- Surface other people's inline PR comments in a Devin-style dropdown in the
review header (search + link to the thread on GitHub). New
GET /reviews/{owner}/{repo}/{number}/comments lists them and flags the
reviewer's own (marker-bearing) comments so they're filtered out.
- Collapse the global nav by default on a review detail page, restoring the
prior preference on leave.
* feat: bigger comment-toolbar icons; open dropdown comments inline
- Enlarge the markdown toolbar glyphs (Phosphor) in the comment composer —
they were rendering at 10px.
- Clicking a comment in the PR comments dropdown now opens it inline in the
diff as a read-only finding-style card (InlineComment), scrolling its line
into view, instead of navigating to GitHub. Falls back to GitHub when the
comment's file/line isn't in the current diff.
* fix: drive "Add to Chat" from native text selection
The gutter "+" is now comment-only; wiring its click to the composer
conflicted with its old double-duty as the drag-to-select handle, which
broke selection → "Add to Chat". Switch to Devin's model: disable Pierre's
interactive line selection and instead map a native text highlight in the
diff to a line range (via the data-line / data-line-type attributes Pierre
stamps on each line, read from the diff's open shadow root) to show the
"Add to Chat" popup. ⌘L and the existing attachment/popup path are unchanged.
* feat: gutter "+" drag selects a range for multi-line comments
Re-enable Pierre's gutter line selection so dragging the "+" down the
gutter comments across a range (click still comments on a single line);
onLineSelectionEnd routes the range to the composer. Native code-text
selection still drives "Add to Chat" — Pierre only line-selects from the
gutter, and onLineSelectionEnd bails when a native text selection is
present, so a code highlight never opens the composer.
* fix: keep the range highlighted while its comment composer is open
Previously opening the composer cleared the selection, so the lines being
commented on lost their highlight. Drive the controlled selection from the
open comment draft's range so the rows stay highlighted until the composer
is closed.
* fix: address PR review — paginate comments, fall back for outdated ones
- list_review_comments now pages through all PR review comments (bounded by
_MAX_REVIEW_COMMENT_PAGES) instead of returning only the first 100, so older
comments still show in the dropdown.
- Surface GitHub's outdated flag (position == null) as is_outdated; opening such
a comment (or one whose line isn't in the diff) now opens it on GitHub instead
of silently rendering nothing, plus a timeout fallback if the annotation never
mounts (e.g. collapsed context).
2026-06-23 16:01:46 -07:00
|
|
|
Fragment,
|
2026-06-23 12:27:51 -07:00
|
|
|
createContext,
|
|
|
|
|
memo,
|
|
|
|
|
useCallback,
|
|
|
|
|
useContext,
|
|
|
|
|
useEffect,
|
|
|
|
|
useMemo,
|
|
|
|
|
useRef,
|
|
|
|
|
useState,
|
|
|
|
|
} from "react"
|
|
|
|
|
import {
|
|
|
|
|
ArrowClockwiseIcon,
|
|
|
|
|
ArrowSquareOutIcon,
|
|
|
|
|
BugBeetleIcon,
|
|
|
|
|
CaretDownIcon,
|
feat: inline PR comments in the reviews UI (#1600)
* feat: inline PR comments in the reviews UI
Click the diff gutter "+" on a line to open an inline comment composer
(rendered like the finding card via a Pierre annotation); submitting
posts a real inline PR review comment as the signed-in user through a
new POST /reviews/{owner}/{repo}/{number}/comments. The "+" press-drag →
"Add to Chat" selection path is unchanged.
* feat: GitHub-parity comment box, PR comments dropdown, collapse nav
- Comment composer now mirrors GitHub's box: Write/Preview tabs (markdown
rendered via the existing Markdown component) and a markdown toolbar
(heading, bold, italic, quote, code, link, bulleted/numbered/task list).
- Surface other people's inline PR comments in a Devin-style dropdown in the
review header (search + link to the thread on GitHub). New
GET /reviews/{owner}/{repo}/{number}/comments lists them and flags the
reviewer's own (marker-bearing) comments so they're filtered out.
- Collapse the global nav by default on a review detail page, restoring the
prior preference on leave.
* feat: bigger comment-toolbar icons; open dropdown comments inline
- Enlarge the markdown toolbar glyphs (Phosphor) in the comment composer —
they were rendering at 10px.
- Clicking a comment in the PR comments dropdown now opens it inline in the
diff as a read-only finding-style card (InlineComment), scrolling its line
into view, instead of navigating to GitHub. Falls back to GitHub when the
comment's file/line isn't in the current diff.
* fix: drive "Add to Chat" from native text selection
The gutter "+" is now comment-only; wiring its click to the composer
conflicted with its old double-duty as the drag-to-select handle, which
broke selection → "Add to Chat". Switch to Devin's model: disable Pierre's
interactive line selection and instead map a native text highlight in the
diff to a line range (via the data-line / data-line-type attributes Pierre
stamps on each line, read from the diff's open shadow root) to show the
"Add to Chat" popup. ⌘L and the existing attachment/popup path are unchanged.
* feat: gutter "+" drag selects a range for multi-line comments
Re-enable Pierre's gutter line selection so dragging the "+" down the
gutter comments across a range (click still comments on a single line);
onLineSelectionEnd routes the range to the composer. Native code-text
selection still drives "Add to Chat" — Pierre only line-selects from the
gutter, and onLineSelectionEnd bails when a native text selection is
present, so a code highlight never opens the composer.
* fix: keep the range highlighted while its comment composer is open
Previously opening the composer cleared the selection, so the lines being
commented on lost their highlight. Drive the controlled selection from the
open comment draft's range so the rows stay highlighted until the composer
is closed.
* fix: address PR review — paginate comments, fall back for outdated ones
- list_review_comments now pages through all PR review comments (bounded by
_MAX_REVIEW_COMMENT_PAGES) instead of returning only the first 100, so older
comments still show in the dropdown.
- Surface GitHub's outdated flag (position == null) as is_outdated; opening such
a comment (or one whose line isn't in the diff) now opens it on GitHub instead
of silently rendering nothing, plus a timeout fallback if the annotation never
mounts (e.g. collapsed context).
2026-06-23 16:01:46 -07:00
|
|
|
ChatCircleIcon,
|
2026-06-23 12:27:51 -07:00
|
|
|
CheckCircleIcon,
|
|
|
|
|
CheckIcon,
|
|
|
|
|
CircleIcon,
|
feat: inline PR comments in the reviews UI (#1600)
* feat: inline PR comments in the reviews UI
Click the diff gutter "+" on a line to open an inline comment composer
(rendered like the finding card via a Pierre annotation); submitting
posts a real inline PR review comment as the signed-in user through a
new POST /reviews/{owner}/{repo}/{number}/comments. The "+" press-drag →
"Add to Chat" selection path is unchanged.
* feat: GitHub-parity comment box, PR comments dropdown, collapse nav
- Comment composer now mirrors GitHub's box: Write/Preview tabs (markdown
rendered via the existing Markdown component) and a markdown toolbar
(heading, bold, italic, quote, code, link, bulleted/numbered/task list).
- Surface other people's inline PR comments in a Devin-style dropdown in the
review header (search + link to the thread on GitHub). New
GET /reviews/{owner}/{repo}/{number}/comments lists them and flags the
reviewer's own (marker-bearing) comments so they're filtered out.
- Collapse the global nav by default on a review detail page, restoring the
prior preference on leave.
* feat: bigger comment-toolbar icons; open dropdown comments inline
- Enlarge the markdown toolbar glyphs (Phosphor) in the comment composer —
they were rendering at 10px.
- Clicking a comment in the PR comments dropdown now opens it inline in the
diff as a read-only finding-style card (InlineComment), scrolling its line
into view, instead of navigating to GitHub. Falls back to GitHub when the
comment's file/line isn't in the current diff.
* fix: drive "Add to Chat" from native text selection
The gutter "+" is now comment-only; wiring its click to the composer
conflicted with its old double-duty as the drag-to-select handle, which
broke selection → "Add to Chat". Switch to Devin's model: disable Pierre's
interactive line selection and instead map a native text highlight in the
diff to a line range (via the data-line / data-line-type attributes Pierre
stamps on each line, read from the diff's open shadow root) to show the
"Add to Chat" popup. ⌘L and the existing attachment/popup path are unchanged.
* feat: gutter "+" drag selects a range for multi-line comments
Re-enable Pierre's gutter line selection so dragging the "+" down the
gutter comments across a range (click still comments on a single line);
onLineSelectionEnd routes the range to the composer. Native code-text
selection still drives "Add to Chat" — Pierre only line-selects from the
gutter, and onLineSelectionEnd bails when a native text selection is
present, so a code highlight never opens the composer.
* fix: keep the range highlighted while its comment composer is open
Previously opening the composer cleared the selection, so the lines being
commented on lost their highlight. Drive the controlled selection from the
open comment draft's range so the rows stay highlighted until the composer
is closed.
* fix: address PR review — paginate comments, fall back for outdated ones
- list_review_comments now pages through all PR review comments (bounded by
_MAX_REVIEW_COMMENT_PAGES) instead of returning only the first 100, so older
comments still show in the dropdown.
- Surface GitHub's outdated flag (position == null) as is_outdated; opening such
a comment (or one whose line isn't in the diff) now opens it on GitHub instead
of silently rendering nothing, plus a timeout fallback if the annotation never
mounts (e.g. collapsed context).
2026-06-23 16:01:46 -07:00
|
|
|
CodeIcon,
|
2026-06-23 12:27:51 -07:00
|
|
|
CopyIcon,
|
|
|
|
|
FlagIcon,
|
|
|
|
|
InfoIcon,
|
feat: inline PR comments in the reviews UI (#1600)
* feat: inline PR comments in the reviews UI
Click the diff gutter "+" on a line to open an inline comment composer
(rendered like the finding card via a Pierre annotation); submitting
posts a real inline PR review comment as the signed-in user through a
new POST /reviews/{owner}/{repo}/{number}/comments. The "+" press-drag →
"Add to Chat" selection path is unchanged.
* feat: GitHub-parity comment box, PR comments dropdown, collapse nav
- Comment composer now mirrors GitHub's box: Write/Preview tabs (markdown
rendered via the existing Markdown component) and a markdown toolbar
(heading, bold, italic, quote, code, link, bulleted/numbered/task list).
- Surface other people's inline PR comments in a Devin-style dropdown in the
review header (search + link to the thread on GitHub). New
GET /reviews/{owner}/{repo}/{number}/comments lists them and flags the
reviewer's own (marker-bearing) comments so they're filtered out.
- Collapse the global nav by default on a review detail page, restoring the
prior preference on leave.
* feat: bigger comment-toolbar icons; open dropdown comments inline
- Enlarge the markdown toolbar glyphs (Phosphor) in the comment composer —
they were rendering at 10px.
- Clicking a comment in the PR comments dropdown now opens it inline in the
diff as a read-only finding-style card (InlineComment), scrolling its line
into view, instead of navigating to GitHub. Falls back to GitHub when the
comment's file/line isn't in the current diff.
* fix: drive "Add to Chat" from native text selection
The gutter "+" is now comment-only; wiring its click to the composer
conflicted with its old double-duty as the drag-to-select handle, which
broke selection → "Add to Chat". Switch to Devin's model: disable Pierre's
interactive line selection and instead map a native text highlight in the
diff to a line range (via the data-line / data-line-type attributes Pierre
stamps on each line, read from the diff's open shadow root) to show the
"Add to Chat" popup. ⌘L and the existing attachment/popup path are unchanged.
* feat: gutter "+" drag selects a range for multi-line comments
Re-enable Pierre's gutter line selection so dragging the "+" down the
gutter comments across a range (click still comments on a single line);
onLineSelectionEnd routes the range to the composer. Native code-text
selection still drives "Add to Chat" — Pierre only line-selects from the
gutter, and onLineSelectionEnd bails when a native text selection is
present, so a code highlight never opens the composer.
* fix: keep the range highlighted while its comment composer is open
Previously opening the composer cleared the selection, so the lines being
commented on lost their highlight. Drive the controlled selection from the
open comment draft's range so the rows stay highlighted until the composer
is closed.
* fix: address PR review — paginate comments, fall back for outdated ones
- list_review_comments now pages through all PR review comments (bounded by
_MAX_REVIEW_COMMENT_PAGES) instead of returning only the first 100, so older
comments still show in the dropdown.
- Surface GitHub's outdated flag (position == null) as is_outdated; opening such
a comment (or one whose line isn't in the diff) now opens it on GitHub instead
of silently rendering nothing, plus a timeout fallback if the annotation never
mounts (e.g. collapsed context).
2026-06-23 16:01:46 -07:00
|
|
|
LinkIcon,
|
|
|
|
|
ListBulletsIcon,
|
|
|
|
|
ListChecksIcon,
|
|
|
|
|
ListNumbersIcon,
|
|
|
|
|
QuotesIcon,
|
2026-06-23 12:27:51 -07:00
|
|
|
RowsIcon,
|
|
|
|
|
SquareSplitHorizontalIcon,
|
feat: inline PR comments in the reviews UI (#1600)
* feat: inline PR comments in the reviews UI
Click the diff gutter "+" on a line to open an inline comment composer
(rendered like the finding card via a Pierre annotation); submitting
posts a real inline PR review comment as the signed-in user through a
new POST /reviews/{owner}/{repo}/{number}/comments. The "+" press-drag →
"Add to Chat" selection path is unchanged.
* feat: GitHub-parity comment box, PR comments dropdown, collapse nav
- Comment composer now mirrors GitHub's box: Write/Preview tabs (markdown
rendered via the existing Markdown component) and a markdown toolbar
(heading, bold, italic, quote, code, link, bulleted/numbered/task list).
- Surface other people's inline PR comments in a Devin-style dropdown in the
review header (search + link to the thread on GitHub). New
GET /reviews/{owner}/{repo}/{number}/comments lists them and flags the
reviewer's own (marker-bearing) comments so they're filtered out.
- Collapse the global nav by default on a review detail page, restoring the
prior preference on leave.
* feat: bigger comment-toolbar icons; open dropdown comments inline
- Enlarge the markdown toolbar glyphs (Phosphor) in the comment composer —
they were rendering at 10px.
- Clicking a comment in the PR comments dropdown now opens it inline in the
diff as a read-only finding-style card (InlineComment), scrolling its line
into view, instead of navigating to GitHub. Falls back to GitHub when the
comment's file/line isn't in the current diff.
* fix: drive "Add to Chat" from native text selection
The gutter "+" is now comment-only; wiring its click to the composer
conflicted with its old double-duty as the drag-to-select handle, which
broke selection → "Add to Chat". Switch to Devin's model: disable Pierre's
interactive line selection and instead map a native text highlight in the
diff to a line range (via the data-line / data-line-type attributes Pierre
stamps on each line, read from the diff's open shadow root) to show the
"Add to Chat" popup. ⌘L and the existing attachment/popup path are unchanged.
* feat: gutter "+" drag selects a range for multi-line comments
Re-enable Pierre's gutter line selection so dragging the "+" down the
gutter comments across a range (click still comments on a single line);
onLineSelectionEnd routes the range to the composer. Native code-text
selection still drives "Add to Chat" — Pierre only line-selects from the
gutter, and onLineSelectionEnd bails when a native text selection is
present, so a code highlight never opens the composer.
* fix: keep the range highlighted while its comment composer is open
Previously opening the composer cleared the selection, so the lines being
commented on lost their highlight. Drive the controlled selection from the
open comment draft's range so the rows stay highlighted until the composer
is closed.
* fix: address PR review — paginate comments, fall back for outdated ones
- list_review_comments now pages through all PR review comments (bounded by
_MAX_REVIEW_COMMENT_PAGES) instead of returning only the first 100, so older
comments still show in the dropdown.
- Surface GitHub's outdated flag (position == null) as is_outdated; opening such
a comment (or one whose line isn't in the diff) now opens it on GitHub instead
of silently rendering nothing, plus a timeout fallback if the annotation never
mounts (e.g. collapsed context).
2026-06-23 16:01:46 -07:00
|
|
|
TextBIcon,
|
|
|
|
|
TextHIcon,
|
|
|
|
|
TextItalicIcon,
|
2026-06-23 12:27:51 -07:00
|
|
|
XCircleIcon,
|
feat: inline PR comments in the reviews UI (#1600)
* feat: inline PR comments in the reviews UI
Click the diff gutter "+" on a line to open an inline comment composer
(rendered like the finding card via a Pierre annotation); submitting
posts a real inline PR review comment as the signed-in user through a
new POST /reviews/{owner}/{repo}/{number}/comments. The "+" press-drag →
"Add to Chat" selection path is unchanged.
* feat: GitHub-parity comment box, PR comments dropdown, collapse nav
- Comment composer now mirrors GitHub's box: Write/Preview tabs (markdown
rendered via the existing Markdown component) and a markdown toolbar
(heading, bold, italic, quote, code, link, bulleted/numbered/task list).
- Surface other people's inline PR comments in a Devin-style dropdown in the
review header (search + link to the thread on GitHub). New
GET /reviews/{owner}/{repo}/{number}/comments lists them and flags the
reviewer's own (marker-bearing) comments so they're filtered out.
- Collapse the global nav by default on a review detail page, restoring the
prior preference on leave.
* feat: bigger comment-toolbar icons; open dropdown comments inline
- Enlarge the markdown toolbar glyphs (Phosphor) in the comment composer —
they were rendering at 10px.
- Clicking a comment in the PR comments dropdown now opens it inline in the
diff as a read-only finding-style card (InlineComment), scrolling its line
into view, instead of navigating to GitHub. Falls back to GitHub when the
comment's file/line isn't in the current diff.
* fix: drive "Add to Chat" from native text selection
The gutter "+" is now comment-only; wiring its click to the composer
conflicted with its old double-duty as the drag-to-select handle, which
broke selection → "Add to Chat". Switch to Devin's model: disable Pierre's
interactive line selection and instead map a native text highlight in the
diff to a line range (via the data-line / data-line-type attributes Pierre
stamps on each line, read from the diff's open shadow root) to show the
"Add to Chat" popup. ⌘L and the existing attachment/popup path are unchanged.
* feat: gutter "+" drag selects a range for multi-line comments
Re-enable Pierre's gutter line selection so dragging the "+" down the
gutter comments across a range (click still comments on a single line);
onLineSelectionEnd routes the range to the composer. Native code-text
selection still drives "Add to Chat" — Pierre only line-selects from the
gutter, and onLineSelectionEnd bails when a native text selection is
present, so a code highlight never opens the composer.
* fix: keep the range highlighted while its comment composer is open
Previously opening the composer cleared the selection, so the lines being
commented on lost their highlight. Drive the controlled selection from the
open comment draft's range so the rows stay highlighted until the composer
is closed.
* fix: address PR review — paginate comments, fall back for outdated ones
- list_review_comments now pages through all PR review comments (bounded by
_MAX_REVIEW_COMMENT_PAGES) instead of returning only the first 100, so older
comments still show in the dropdown.
- Surface GitHub's outdated flag (position == null) as is_outdated; opening such
a comment (or one whose line isn't in the diff) now opens it on GitHub instead
of silently rendering nothing, plus a timeout fallback if the annotation never
mounts (e.g. collapsed context).
2026-06-23 16:01:46 -07:00
|
|
|
XIcon,
|
2026-06-23 12:27:51 -07:00
|
|
|
} from "@phosphor-icons/react"
|
|
|
|
|
import { IoLogoGithub } from "react-icons/io5"
|
|
|
|
|
import {
|
|
|
|
|
MultiFileDiff,
|
|
|
|
|
Virtualizer,
|
|
|
|
|
WorkerPoolContextProvider,
|
|
|
|
|
} from "@pierre/diffs/react"
|
feat: inline PR comments in the reviews UI (#1600)
* feat: inline PR comments in the reviews UI
Click the diff gutter "+" on a line to open an inline comment composer
(rendered like the finding card via a Pierre annotation); submitting
posts a real inline PR review comment as the signed-in user through a
new POST /reviews/{owner}/{repo}/{number}/comments. The "+" press-drag →
"Add to Chat" selection path is unchanged.
* feat: GitHub-parity comment box, PR comments dropdown, collapse nav
- Comment composer now mirrors GitHub's box: Write/Preview tabs (markdown
rendered via the existing Markdown component) and a markdown toolbar
(heading, bold, italic, quote, code, link, bulleted/numbered/task list).
- Surface other people's inline PR comments in a Devin-style dropdown in the
review header (search + link to the thread on GitHub). New
GET /reviews/{owner}/{repo}/{number}/comments lists them and flags the
reviewer's own (marker-bearing) comments so they're filtered out.
- Collapse the global nav by default on a review detail page, restoring the
prior preference on leave.
* feat: bigger comment-toolbar icons; open dropdown comments inline
- Enlarge the markdown toolbar glyphs (Phosphor) in the comment composer —
they were rendering at 10px.
- Clicking a comment in the PR comments dropdown now opens it inline in the
diff as a read-only finding-style card (InlineComment), scrolling its line
into view, instead of navigating to GitHub. Falls back to GitHub when the
comment's file/line isn't in the current diff.
* fix: drive "Add to Chat" from native text selection
The gutter "+" is now comment-only; wiring its click to the composer
conflicted with its old double-duty as the drag-to-select handle, which
broke selection → "Add to Chat". Switch to Devin's model: disable Pierre's
interactive line selection and instead map a native text highlight in the
diff to a line range (via the data-line / data-line-type attributes Pierre
stamps on each line, read from the diff's open shadow root) to show the
"Add to Chat" popup. ⌘L and the existing attachment/popup path are unchanged.
* feat: gutter "+" drag selects a range for multi-line comments
Re-enable Pierre's gutter line selection so dragging the "+" down the
gutter comments across a range (click still comments on a single line);
onLineSelectionEnd routes the range to the composer. Native code-text
selection still drives "Add to Chat" — Pierre only line-selects from the
gutter, and onLineSelectionEnd bails when a native text selection is
present, so a code highlight never opens the composer.
* fix: keep the range highlighted while its comment composer is open
Previously opening the composer cleared the selection, so the lines being
commented on lost their highlight. Drive the controlled selection from the
open comment draft's range so the rows stay highlighted until the composer
is closed.
* fix: address PR review — paginate comments, fall back for outdated ones
- list_review_comments now pages through all PR review comments (bounded by
_MAX_REVIEW_COMMENT_PAGES) instead of returning only the first 100, so older
comments still show in the dropdown.
- Surface GitHub's outdated flag (position == null) as is_outdated; opening such
a comment (or one whose line isn't in the diff) now opens it on GitHub instead
of silently rendering nothing, plus a timeout fallback if the annotation never
mounts (e.g. collapsed context).
2026-06-23 16:01:46 -07:00
|
|
|
import type { Icon } from "@phosphor-icons/react"
|
2026-06-23 12:27:51 -07:00
|
|
|
import type { FileContents } from "@pierre/diffs/react"
|
|
|
|
|
import type {
|
|
|
|
|
FileDiff as CoreFileDiff,
|
|
|
|
|
DiffLineAnnotation,
|
|
|
|
|
SelectedLineRange,
|
|
|
|
|
SelectionSide,
|
|
|
|
|
} from "@pierre/diffs"
|
|
|
|
|
|
|
|
|
|
import type {
|
feat: inline PR comments in the reviews UI (#1600)
* feat: inline PR comments in the reviews UI
Click the diff gutter "+" on a line to open an inline comment composer
(rendered like the finding card via a Pierre annotation); submitting
posts a real inline PR review comment as the signed-in user through a
new POST /reviews/{owner}/{repo}/{number}/comments. The "+" press-drag →
"Add to Chat" selection path is unchanged.
* feat: GitHub-parity comment box, PR comments dropdown, collapse nav
- Comment composer now mirrors GitHub's box: Write/Preview tabs (markdown
rendered via the existing Markdown component) and a markdown toolbar
(heading, bold, italic, quote, code, link, bulleted/numbered/task list).
- Surface other people's inline PR comments in a Devin-style dropdown in the
review header (search + link to the thread on GitHub). New
GET /reviews/{owner}/{repo}/{number}/comments lists them and flags the
reviewer's own (marker-bearing) comments so they're filtered out.
- Collapse the global nav by default on a review detail page, restoring the
prior preference on leave.
* feat: bigger comment-toolbar icons; open dropdown comments inline
- Enlarge the markdown toolbar glyphs (Phosphor) in the comment composer —
they were rendering at 10px.
- Clicking a comment in the PR comments dropdown now opens it inline in the
diff as a read-only finding-style card (InlineComment), scrolling its line
into view, instead of navigating to GitHub. Falls back to GitHub when the
comment's file/line isn't in the current diff.
* fix: drive "Add to Chat" from native text selection
The gutter "+" is now comment-only; wiring its click to the composer
conflicted with its old double-duty as the drag-to-select handle, which
broke selection → "Add to Chat". Switch to Devin's model: disable Pierre's
interactive line selection and instead map a native text highlight in the
diff to a line range (via the data-line / data-line-type attributes Pierre
stamps on each line, read from the diff's open shadow root) to show the
"Add to Chat" popup. ⌘L and the existing attachment/popup path are unchanged.
* feat: gutter "+" drag selects a range for multi-line comments
Re-enable Pierre's gutter line selection so dragging the "+" down the
gutter comments across a range (click still comments on a single line);
onLineSelectionEnd routes the range to the composer. Native code-text
selection still drives "Add to Chat" — Pierre only line-selects from the
gutter, and onLineSelectionEnd bails when a native text selection is
present, so a code highlight never opens the composer.
* fix: keep the range highlighted while its comment composer is open
Previously opening the composer cleared the selection, so the lines being
commented on lost their highlight. Drive the controlled selection from the
open comment draft's range so the rows stay highlighted until the composer
is closed.
* fix: address PR review — paginate comments, fall back for outdated ones
- list_review_comments now pages through all PR review comments (bounded by
_MAX_REVIEW_COMMENT_PAGES) instead of returning only the first 100, so older
comments still show in the dropdown.
- Surface GitHub's outdated flag (position == null) as is_outdated; opening such
a comment (or one whose line isn't in the diff) now opens it on GitHub instead
of silently rendering nothing, plus a timeout fallback if the annotation never
mounts (e.g. collapsed context).
2026-06-23 16:01:46 -07:00
|
|
|
PrReviewComment,
|
2026-06-23 12:27:51 -07:00
|
|
|
ReviewCheckRun,
|
feat: inline PR comments in the reviews UI (#1600)
* feat: inline PR comments in the reviews UI
Click the diff gutter "+" on a line to open an inline comment composer
(rendered like the finding card via a Pierre annotation); submitting
posts a real inline PR review comment as the signed-in user through a
new POST /reviews/{owner}/{repo}/{number}/comments. The "+" press-drag →
"Add to Chat" selection path is unchanged.
* feat: GitHub-parity comment box, PR comments dropdown, collapse nav
- Comment composer now mirrors GitHub's box: Write/Preview tabs (markdown
rendered via the existing Markdown component) and a markdown toolbar
(heading, bold, italic, quote, code, link, bulleted/numbered/task list).
- Surface other people's inline PR comments in a Devin-style dropdown in the
review header (search + link to the thread on GitHub). New
GET /reviews/{owner}/{repo}/{number}/comments lists them and flags the
reviewer's own (marker-bearing) comments so they're filtered out.
- Collapse the global nav by default on a review detail page, restoring the
prior preference on leave.
* feat: bigger comment-toolbar icons; open dropdown comments inline
- Enlarge the markdown toolbar glyphs (Phosphor) in the comment composer —
they were rendering at 10px.
- Clicking a comment in the PR comments dropdown now opens it inline in the
diff as a read-only finding-style card (InlineComment), scrolling its line
into view, instead of navigating to GitHub. Falls back to GitHub when the
comment's file/line isn't in the current diff.
* fix: drive "Add to Chat" from native text selection
The gutter "+" is now comment-only; wiring its click to the composer
conflicted with its old double-duty as the drag-to-select handle, which
broke selection → "Add to Chat". Switch to Devin's model: disable Pierre's
interactive line selection and instead map a native text highlight in the
diff to a line range (via the data-line / data-line-type attributes Pierre
stamps on each line, read from the diff's open shadow root) to show the
"Add to Chat" popup. ⌘L and the existing attachment/popup path are unchanged.
* feat: gutter "+" drag selects a range for multi-line comments
Re-enable Pierre's gutter line selection so dragging the "+" down the
gutter comments across a range (click still comments on a single line);
onLineSelectionEnd routes the range to the composer. Native code-text
selection still drives "Add to Chat" — Pierre only line-selects from the
gutter, and onLineSelectionEnd bails when a native text selection is
present, so a code highlight never opens the composer.
* fix: keep the range highlighted while its comment composer is open
Previously opening the composer cleared the selection, so the lines being
commented on lost their highlight. Drive the controlled selection from the
open comment draft's range so the rows stay highlighted until the composer
is closed.
* fix: address PR review — paginate comments, fall back for outdated ones
- list_review_comments now pages through all PR review comments (bounded by
_MAX_REVIEW_COMMENT_PAGES) instead of returning only the first 100, so older
comments still show in the dropdown.
- Surface GitHub's outdated flag (position == null) as is_outdated; opening such
a comment (or one whose line isn't in the diff) now opens it on GitHub instead
of silently rendering nothing, plus a timeout fallback if the annotation never
mounts (e.g. collapsed context).
2026-06-23 16:01:46 -07:00
|
|
|
ReviewCommentCreate,
|
2026-06-23 12:27:51 -07:00
|
|
|
ReviewDetail,
|
|
|
|
|
ReviewDiffFile,
|
|
|
|
|
ReviewFinding,
|
|
|
|
|
ReviewUserRef,
|
|
|
|
|
} from "@/lib/api"
|
|
|
|
|
import type {
|
|
|
|
|
ReviewSidebarGroup,
|
|
|
|
|
ReviewSidebarView,
|
|
|
|
|
} from "@/components/agents/ReviewSidebar"
|
|
|
|
|
import type { ChatAttachment } from "@/components/agents/ReviewChat"
|
|
|
|
|
import type { DiffStyle } from "@/components/agents/utils/diffUtils"
|
|
|
|
|
import { Markdown } from "@/components/agents/ported"
|
chore: sync upstream/main, defer #1621 modular webhooks (#81)
* chore: bake sfw binary into sandbox image (#1611)
sfw only ships a launcher that fetches its real binary at first run and does a
daily update check against api.github.com/repos/SocketDev/sfw-free. Both fail in
the sandbox (restricted egress; the proxy injects the GitHub App installation
token, which lacks access to that repo), so `sfw yarn install` errors with
"could not fetch its binary". Pin sfw 2.0.6, warm + verify the binary cache at
build, and set SFW_SKIP_UPDATE_CHECK=1 so runs use the baked binary offline.
* feat: editable plan mode + fix review-plan banner overlap (#1610)
* feat: editable plan mode + fix review-plan banner overlap
Lets the thread owner edit the plan markdown by hand from the plan-review
page (Edit -> textarea -> Save) via a new PUT /dashboard/api/plan/{id}
endpoint that re-publishes the plan and mirrors it into the sandbox
plan.md, so approve hands the edited plan to the agent as the source of
truth. Also fixes the collapsed git-panel's floating expand button
covering the "Review plan ->" banner by reserving space for it.
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* fix: abort plan approval when the published plan read fails
get_plan_content() swallowed store errors and returned None, so a
transient failure during approve would still mark the plan approved and
dispatch the generic fallback text — silently dropping an owner's edited
plan. Read the plan strictly (raise_on_error=True) so approval aborts
instead, matching the comment read.
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
---------
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* feat: show message timestamps (#1609)
* feat: show message timestamps
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* fix: suppress fallback message timestamps
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* feat: stable message + tool-call hover timestamps
Stamp a stable client-side arrival time per message and tool call (keyed
by id, persisted to localStorage). Messages render the timestamp inline;
tool rows reveal a dim timestamp chip on hover. Real backend created_at
still takes precedence when present.
* fix: hide client-stamped message timestamps
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
---------
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* feat: add PR trace resolution (#1612)
* feat: add PR trace resolution
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* fix: inject reviewer trace context as JSON
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* fix: address review on PR trace resolution
Use the documented LangSmith metadata filter syntax
(and(eq(metadata_key,...), eq(metadata_value,...))) instead of
has(metadata, '{...}'), which does not match runs — _list_thread_runs
was silently returning nothing. Bound full-text searches to a 90-day
window so they don't hit LangSmith's large-window rate limit.
Also folds in the best-effort branch->head-sha resolver (dropping the
weighted scoring/threshold + repo/file evidence + GitHub hydration),
sandbox JSON injection, and the admin "Resolve trace" dry-run endpoint.
The IDOR findings are moot: resolve_pr_to_threads/summarize_agent_session
were removed; resolution now runs deterministically from the trusted run
config with no model-controlled pr_url or thread_id.
* fix: scope branch trace search to the repo
Branch names like fix-tests aren't unique across repos (or older PRs) in
a shared tracing project, so an unscoped branch hit could resolve to an
unrelated thread and write its runs into the reviewer sandbox. Require
the repo slug to co-occur with the branch in matched runs; the full head
SHA stays unscoped since it is globally unique. Addresses open-swe review
on PR #1612.
---------
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* feat: include plan links in PR descriptions (#1613)
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* feat: gate workflow pushes with approval (#1614)
* feat: gate workflow pushes with approval
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* fix: preserve proxy refresh test compatibility
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* fix: bind workflow approvals to pushed ref
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
---------
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* feat: recover thread work as patch (#1615)
* feat: recover thread work as patch
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* fix: search sandbox cwd for recovery patches
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
---------
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* fix: omit plan link in PR description when no plan exists (#1618)
Plan links in PR descriptions were always built from the thread id, so
runs that never produced a plan linked to an empty plan-review page.
Now the plan content store is consulted first; the link is only added
when a plan with non-empty markdown actually exists. A transient store
failure degrades gracefully (no link) rather than blocking PR creation.
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* feat: add filter & grouping menu to agents threads sidebar (#1617)
Add a Cursor-style control to the agents sidebar that groups (None/Date/
Status/Project), filters (ownership, status, source, pull request, model,
repo, include-resolved), and compacts the threads list. All client-side over
already-fetched sidebar threads; preferences persist in localStorage.
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* chore: update langsmith sdk to 0.9.3 (#1616)
Co-authored-by: Ramon Nogueira <270434257+ramon-langchain@users.noreply.github.com>
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* feat: clickable shared PR header in git panel and reviews (#1620)
* feat: clickable shared PR header in git panel and reviews
Replace the standalone "View PR" button in the agent git panel with a
clickable PR title, matching the reviews view. Extract a shared PrHeader
component reused by both the git panel and the review main body.
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* refactor: drop PrHeader wrapper, use shared component directly
The review-side PrHeader was just a thin adapter mapping detail -> the
shared component's props. Inline it at the call site and use the shared
PrHeader directly so there's a single component.
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
---------
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* refactor: durable interrupt dispatch + completion webhook (#1621)
* wip(rebuild): core reliability spine
- remove PR-babysitting (ci_autofix + ci_monitor graph + webhook wiring)
- dispatch core: agent/dispatch.py with multitask_strategy=interrupt +
durability=sync + completion webhook; reroute all webhook + plan triggers;
drop the racy in-process lock + is_thread_active busy-check
- completion webhook: agent/completion.py + /webhooks/run-complete loopback
route for failure/timeout replies (idempotent)
Co-authored-by: open-swe[bot]
* feat(rebuild): async tools, reconcile, shared http timeouts, assembly tuning
Parallel batch on top of the reliability spine:
- async-ify all 24 tools (drop asyncio.run; requests->httpx); re-implement the
http_request/fetch_url SSRF + DNS-rebinding defense httpx-natively and harden
the IP check to 'not is_global' (+ IPv4-mapped unwrap)
- reconcile.py: stale pending-run sweep (threads.search -> per-thread runs.list
-> cancel_many), wired into the scheduler graph via task='reconcile'
- shared DEFAULT_HTTP_TIMEOUT (agent/utils/http.py) on every bare
httpx.AsyncClient() across utils/dashboard/webapp/middleware
- run budget: MODEL_CALL_RECURSION_LIMIT 5000->250
- fix stale OpenAI->Anthropic fallback id (claude-opus-4-5 -> 4-8)
- drop redundant custom repair middleware (deepagents auto-adds PatchToolCalls)
- confirm tool-result eviction + summarization auto-wired via backend
- slim system prompt ~8% (full harness-profile rewrite deferred)
Co-authored-by: open-swe[bot]
* feat(rebuild): harness-profile prompt + split webhooks out of webapp
- prompt.py: own the system prompt via a registered harness profile
(OPEN_SWE_SHARED_BASE, kept neutral so the read-only reviewer/analyzer that
share it stay safe), registered across all 4 providers; per-thread values
stay in construct_system_prompt. Assembled main-agent prompt ~6.8k -> ~3.1k
tokens (~55% smaller); de-duped PR/commit/suite/force-push guidance; dropped
ALL-CAPS markers.
- webapp.py 3325 -> 1890 LOC: moved 14 per-source handlers into
agent/webhooks/{linear,slack,github}.py; webapp re-exports them for the
routes + tests; moved handlers reach shared helpers via the webapp namespace
to preserve the test suite's monkeypatch targets.
Full suite: 1168 passing, lint clean.
Co-authored-by: open-swe[bot]
* Restore MODEL_CALL_RECURSION_LIMIT to 5000 for long-running tasks
Reverts the 250 cap from the run-budget change — long-running tasks legitimately
need many model calls. The notify_step_limit_reached safety net still fires if a
run does hit the cap, so runs end with a signal either way.
Co-authored-by: open-swe[bot]
* fix: address PR review (auth, SSRF, interrupted status, redirect headers)
- completion.py: drop `interrupted` from failure statuses — with
multitask_strategy=interrupt a follow-up ends the prior run as interrupted,
which is healthy, not a failure to report. [open-swe]
- /webhooks/run-complete: shared-secret auth — dispatch appends ?token= when
RUN_COMPLETE_WEBHOOK_SECRET is set; route verifies via hmac.compare_digest.
[corridor-security]
- SSRF: extract the URL validator to agent/utils/url_safety.py and apply it
before server-side image fetches in multimodal.fetch_image_block.
[corridor-security]
- http_request: preserve caller headers/extensions across redirect hops instead
of dropping them on the first hop. [open-swe]
Co-authored-by: open-swe[bot]
* chore: remove REBUILD_PLAN.md (planning doc, not needed in the repo)
Co-authored-by: open-swe[bot]
* fix: fail closed on run-complete webhook auth when secret unset
Corridor follow-up: verify_run_complete_token returns False (not True) when
RUN_COMPLETE_WEBHOOK_SECRET is unset, so the public route is never
unauthenticated. Logs a startup warning when the secret is absent, and dispatch
skips registering the webhook when there's no secret (no rejected callbacks).
Co-authored-by: open-swe[bot]
---------
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* feat: restore forced tool call to prevent premature run stops (#1622)
Restore the ensure_no_empty_msg middleware and the always-call-a-tool system-prompt instruction that #1535 removed. When the model emits a message with no tool call (and hasn't already messaged the user or confirmed completion), the middleware re-injects a no_op / confirming_completion tool call so the run continues instead of ending mid-task.
Shipping to test whether it fixes runs that stop halfway through.
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* chore(deps): bump langgraph-checkpoint from 4.1.0 to 4.1.1 (#1619)
Bumps [langgraph-checkpoint](https://github.com/langchain-ai/langgraph) from 4.1.0 to 4.1.1.
- [Release notes](https://github.com/langchain-ai/langgraph/releases)
- [Commits](https://github.com/langchain-ai/langgraph/compare/checkpoint==4.1.0...checkpoint==4.1.1)
---
updated-dependencies:
- dependency-name: langgraph-checkpoint
dependency-version: 4.1.1
dependency-type: indirect
...
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
* fix: post reviewer resolution notes verbatim (#1624)
* fix: post reviewer resolution notes verbatim
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* fix: stabilize dashboard follow-up e2e
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* fix: preserve dashboard attribution in e2e
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* fix: make e2e attribution marker durable
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* fix: only echo found e2e attribution
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* fix: check live dashboard attribution in e2e
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
---------
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* hotfix: stop prompting agent/reviewer to wrap installs in sfw (#1625)
Installs hung when prefixed with sfw inside the sandbox (trace 019f0608
stalled on a pending `sfw npm install` execute, never returned). Strip the
Socket Firewall guidance from the agent and reviewer prompts so installs run
through the project's package manager directly. sfw stays in the Docker image;
nothing invokes it now.
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* fix: make plan view mobile friendly (#1636)
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* fix: fall back to vision model for image threads (#1626)
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* fix: surface Slack thread errors (#1627)
* fix: surface Slack thread errors
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* fix: don't set failure_reply_posted on Slack preprocessing errors
The preprocessing error handler was setting failure_reply_posted=True,
the same idempotency flag handle_run_completion checks to suppress
duplicate run-failure replies. Since preprocessing failures happen
before any run exists but the flag persists on the thread, a subsequent
run failure on the same thread would be silently ignored.
The preprocessing handler already posts its own Slack reply, so the
run-completion idempotency flag should not be set here.
---------
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* chore: avoid recapping Slack replies (#1629)
* chore: avoid recapping Slack replies
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* chore: simplify Slack reply prompt wording
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
---------
Co-authored-by: Ramon Nogueira <270434257+ramon-langchain@users.noreply.github.com>
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* fix: update Slack trace reply on web handoff (#1630)
* fix: update Slack trace reply on web handoff
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* fix: trigger web handoff on dashboard starts
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* fix: format web handoff as contextual fragment
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* fix: preserve trace_message_ts when overwriting Slack run mapping
When store_slack_run_mapping is called without trace_message_ts (e.g. on
follow-up Slack mentions), it was unconditionally overwriting the
thread-level mapping and clobbering the timestamp captured from the
initial trace reply. After that, _notify_slack_web_handoff could not find
the original message, so a subsequent move to Web silently skipped the
Slack trace update.
Now, when trace_message_ts is not passed, the existing thread mapping is
read first and its trace_message_ts is preserved.
* style: ruff format
---------
Co-authored-by: Ramon Nogueira <270434257+ramon-langchain@users.noreply.github.com>
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
Co-authored-by: Johannes du Plessis <51395795+johannes117@users.noreply.github.com>
Co-authored-by: Johannes du Plessis <johannes@langchain.dev>
* fix: pre-bundle shiki/@pierre deps to stop dev dynamic-import failures (#1643)
* fix(ui): pre-bundle shiki/@pierre deps to stop dev dynamic-import failures
shiki lazy-imports a grammar per language and these libs only live inside
lazy route components, so Vite's startup scanner never sees them. They get
discovered on first thread navigation, triggering a dep re-optimize +
force-reload that aborts the in-flight route-chunk import, surfacing as
"Failed to fetch dynamically imported module: .../$threadId.tsx".
Pre-bundle them (and the github themes + common code-block languages) via
optimizeDeps.include so the optimize happens once at startup. Dev-only;
production bundles are unaffected.
* fix: pre-bundle canonical shiki docker/make langs instead of aliases
---------
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* feat: show queued dashboard follow-ups (#1631)
* feat: show queued dashboard follow-ups
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* fix: de-dupe queued follow-ups while streaming
---------
Co-authored-by: Ramon Nogueira <270434257+ramon-langchain@users.noreply.github.com>
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
Co-authored-by: Johannes du Plessis <johannes@langchain.dev>
* feat: notify Slack on plan approval (#1632)
* feat: notify Slack on plan approval
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* fix: post Slack approval notice after successful dispatch
Move the _maybe_post_plan_approved_to_slack call until after
_dispatch_followup succeeds so the Slack thread is not told
implementation is beginning before the LangGraph run is created.
Addresses PR review comment.
---------
Co-authored-by: Ramon Nogueira <270434257+ramon-langchain@users.noreply.github.com>
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
Co-authored-by: Johannes du Plessis <johannes@langchain.dev>
* feat: include Slack channel context in prompts (#1633)
Add cached Slack channel metadata enrichment for Slack-triggered runs so prompts can include channel names and descriptions without duplicate conversations.info calls.\n\nCo-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
Co-authored-by: Ramon Nogueira <270434257+ramon-langchain@users.noreply.github.com>
* chore: keep plan guidance high-level (#1634)
Co-authored-by: Ramon Nogueira <270434257+ramon-langchain@users.noreply.github.com>
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* feat: publish plans from sandbox files (#1635)
* feat: publish plans from sandbox files
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* fix: avoid fixed plan filenames
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* fix: virtualize local sandbox file paths
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* fix: preserve plan_file_path across set_plan_status
set_plan_status was rewriting the content record with only markdown
and status, dropping plan_file_path. After a reject, the owner's
dashboard edit would mirror to a different file than the agent's
original, and the next save_plan could republish the stale file.
Preserve plan_file_path when updating status.
---------
Co-authored-by: Ramon Nogueira <270434257+ramon-langchain@users.noreply.github.com>
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* fix: return to thread after plan approval (#1637)
Co-authored-by: Ramon Nogueira <270434257+ramon-langchain@users.noreply.github.com>
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* feat: add Slack breakout thread tool (#1638)
* feat: add Slack breakout thread tool
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* chore: make fake LLM scripts declarative
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* fix: exclude slack_start_new_thread from plan mode
The breakout tool can dispatch a fresh agent run that starts outside the
current plan-mode state, bypassing the approval flow. Add it to
PLAN_MODE_EXCLUDED_TOOLS so it's hidden alongside the other mutating
tools while planning.
---------
Co-authored-by: Ramon Nogueira <270434257+ramon-langchain@users.noreply.github.com>
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* chore: require bun for ui agent work (#1639)
Co-authored-by: Ramon Nogueira <270434257+ramon-langchain@users.noreply.github.com>
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* fix: request actions read for sandbox logs (#1642)
* fix: request actions read for sandbox logs
Request optional Actions read permission for sandbox proxy tokens, with fallback for installations that have not approved it yet. Update setup docs and prompt guidance for safe GitHub Actions log usage.
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* fix: restore actions:read scope after workflow push
After an approved workflow push, the guard was restoring the proxy with
BASE_RUNTIME_PROXY_TOKEN_PERMISSIONS, which excludes the actions: read
scope this PR adds. Restore with RUNTIME_PROXY_TOKEN_PERMISSIONS (which
includes actions: read) and fall back to BASE if the install hasn't
granted Actions read — mirroring the pattern in _create_sandbox_with_proxy.
Addresses review comment on PR #1642.
---------
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* fix: widen split review diffs (#1647)
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* chore: install missing deps before verification (#1646)
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* chore: switch ui to pnpm (#1645)
* chore: require pnpm for ui agent work
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* chore: switch ui to pnpm
Replace Bun and Yarn lockfiles with pnpm lockfile and update UI/Vercel commands to use pnpm.
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* ci: use corepack for ui pnpm e2e build
Run pnpm through Corepack in the E2E global setup so CI can use the pinned package manager without a separate pnpm install step.
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
---------
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* feat: add Sonnet 5 to model picker (#1651)
* chore: update Sonnet examples to Sonnet 5
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* chore: add Sonnet 5 to model picker
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
---------
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* Remove dead breakout-thread e2e scenario after dropping the tool
The merge resolution deferred upstream's Slack breakout-thread tool
(slack_start_new_thread, #1638) since it depends on the #1621 dispatch
module, but the e2e harness still scripted it. Removing the tool name
from fake_llm.py's _tool_step call left a malformed scenario, crashing
the langgraph-dev web server at import (TypeError: _tool_step() missing
'call_id') and failing Playwright E2E.
Drop the "breakout" script scenario, its _is_breakout_request helper +
ScriptRule, and the corresponding full_flow.spec.ts test.
* Revert upstream pnpm switch; keep bun for the UI build
The merge auto-adopted upstream's pnpm switch (#1645) in tests/e2e/
global-setup.ts and ui/package.json, but our fork builds the UI with
bun (vercel.json + the E2E workflow's setup-bun). That left the
Playwright globalSetup running `corepack pnpm install --frozen-lockfile`
with no pnpm-lock.yaml, failing E2E at UI build time.
Revert global-setup.ts and ui/package.json to the dev (bun) baseline,
drop the merge-added ui/pnpm-lock.yaml, and remove the re-added
ui/AGENTS.md (our fork had deleted it).
* Align plan-review e2e + UI with the HEAD (pre-#1635) backend
The merge left a split plan vertical: the backend save_plan/plan_api are
HEAD (we deferred the editable-plan/sandbox-publish features #1610/#1635/
#1637 per #80), but the plan UI and e2e harness were upstream's. The
fake_llm scenario called save_plan(plan_file_path=...) — upstream's
file-based #1635 contract — while HEAD save_plan takes plan_markdown,
so the plan never saved and PlanReview never rendered (E2E failure on
the plan-review locator).
Pass plan_markdown to save_plan, and revert PlanReview.tsx / plan.ts /
$threadId_.plan.tsx / plan_review.spec.ts to the dev baseline so the
whole plan flow (save -> render -> approve -> implement) is consistent
with the HEAD backend.
---------
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: Johannes du Plessis <johannes@langchain.dev>
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
Co-authored-by: Ramon Nogueira <ramon.nogueira@langchain.dev>
Co-authored-by: Ramon Nogueira <270434257+ramon-langchain@users.noreply.github.com>
Co-authored-by: Caroline di Vittorio <43390382+carolinedivittorio@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Johannes du Plessis <51395795+johannes117@users.noreply.github.com>
Co-authored-by: Ankush Gola <9536492+agola11@users.noreply.github.com>
Co-authored-by: Mukil Loganathan <mukil@langchain.dev>
2026-06-30 16:45:19 -04:00
|
|
|
import { PrHeader } from "@/components/agents/PrHeader"
|
2026-06-23 12:27:51 -07:00
|
|
|
import {
|
|
|
|
|
ReviewChat,
|
|
|
|
|
ReviewChatComposerProvider,
|
|
|
|
|
useReviewChatComposer,
|
|
|
|
|
} from "@/components/agents/ReviewChat"
|
|
|
|
|
import { ReviewSidebarPanel } from "@/components/agents/ReviewSidebar"
|
|
|
|
|
import {
|
|
|
|
|
DIFF_VIRTUALIZER_CONFIG,
|
|
|
|
|
DIFF_VIRTUAL_METRICS,
|
|
|
|
|
DIFF_WORKER_HIGHLIGHTER_OPTIONS,
|
|
|
|
|
DIFF_WORKER_POOL_OPTIONS,
|
|
|
|
|
fileContentsCacheKey,
|
|
|
|
|
useDiffOptions,
|
|
|
|
|
warmDiffHighlighter,
|
|
|
|
|
} from "@/components/agents/utils/diffUtils"
|
feat: inline PR comments in the reviews UI (#1600)
* feat: inline PR comments in the reviews UI
Click the diff gutter "+" on a line to open an inline comment composer
(rendered like the finding card via a Pierre annotation); submitting
posts a real inline PR review comment as the signed-in user through a
new POST /reviews/{owner}/{repo}/{number}/comments. The "+" press-drag →
"Add to Chat" selection path is unchanged.
* feat: GitHub-parity comment box, PR comments dropdown, collapse nav
- Comment composer now mirrors GitHub's box: Write/Preview tabs (markdown
rendered via the existing Markdown component) and a markdown toolbar
(heading, bold, italic, quote, code, link, bulleted/numbered/task list).
- Surface other people's inline PR comments in a Devin-style dropdown in the
review header (search + link to the thread on GitHub). New
GET /reviews/{owner}/{repo}/{number}/comments lists them and flags the
reviewer's own (marker-bearing) comments so they're filtered out.
- Collapse the global nav by default on a review detail page, restoring the
prior preference on leave.
* feat: bigger comment-toolbar icons; open dropdown comments inline
- Enlarge the markdown toolbar glyphs (Phosphor) in the comment composer —
they were rendering at 10px.
- Clicking a comment in the PR comments dropdown now opens it inline in the
diff as a read-only finding-style card (InlineComment), scrolling its line
into view, instead of navigating to GitHub. Falls back to GitHub when the
comment's file/line isn't in the current diff.
* fix: drive "Add to Chat" from native text selection
The gutter "+" is now comment-only; wiring its click to the composer
conflicted with its old double-duty as the drag-to-select handle, which
broke selection → "Add to Chat". Switch to Devin's model: disable Pierre's
interactive line selection and instead map a native text highlight in the
diff to a line range (via the data-line / data-line-type attributes Pierre
stamps on each line, read from the diff's open shadow root) to show the
"Add to Chat" popup. ⌘L and the existing attachment/popup path are unchanged.
* feat: gutter "+" drag selects a range for multi-line comments
Re-enable Pierre's gutter line selection so dragging the "+" down the
gutter comments across a range (click still comments on a single line);
onLineSelectionEnd routes the range to the composer. Native code-text
selection still drives "Add to Chat" — Pierre only line-selects from the
gutter, and onLineSelectionEnd bails when a native text selection is
present, so a code highlight never opens the composer.
* fix: keep the range highlighted while its comment composer is open
Previously opening the composer cleared the selection, so the lines being
commented on lost their highlight. Drive the controlled selection from the
open comment draft's range so the rows stay highlighted until the composer
is closed.
* fix: address PR review — paginate comments, fall back for outdated ones
- list_review_comments now pages through all PR review comments (bounded by
_MAX_REVIEW_COMMENT_PAGES) instead of returning only the first 100, so older
comments still show in the dropdown.
- Surface GitHub's outdated flag (position == null) as is_outdated; opening such
a comment (or one whose line isn't in the diff) now opens it on GitHub instead
of silently rendering nothing, plus a timeout fallback if the annotation never
mounts (e.g. collapsed context).
2026-06-23 16:01:46 -07:00
|
|
|
import { IconButton } from "@/components/ui/button"
|
2026-06-23 12:27:51 -07:00
|
|
|
import { Skeleton } from "@/components/ui/skeleton"
|
feat: inline PR comments in the reviews UI (#1600)
* feat: inline PR comments in the reviews UI
Click the diff gutter "+" on a line to open an inline comment composer
(rendered like the finding card via a Pierre annotation); submitting
posts a real inline PR review comment as the signed-in user through a
new POST /reviews/{owner}/{repo}/{number}/comments. The "+" press-drag →
"Add to Chat" selection path is unchanged.
* feat: GitHub-parity comment box, PR comments dropdown, collapse nav
- Comment composer now mirrors GitHub's box: Write/Preview tabs (markdown
rendered via the existing Markdown component) and a markdown toolbar
(heading, bold, italic, quote, code, link, bulleted/numbered/task list).
- Surface other people's inline PR comments in a Devin-style dropdown in the
review header (search + link to the thread on GitHub). New
GET /reviews/{owner}/{repo}/{number}/comments lists them and flags the
reviewer's own (marker-bearing) comments so they're filtered out.
- Collapse the global nav by default on a review detail page, restoring the
prior preference on leave.
* feat: bigger comment-toolbar icons; open dropdown comments inline
- Enlarge the markdown toolbar glyphs (Phosphor) in the comment composer —
they were rendering at 10px.
- Clicking a comment in the PR comments dropdown now opens it inline in the
diff as a read-only finding-style card (InlineComment), scrolling its line
into view, instead of navigating to GitHub. Falls back to GitHub when the
comment's file/line isn't in the current diff.
* fix: drive "Add to Chat" from native text selection
The gutter "+" is now comment-only; wiring its click to the composer
conflicted with its old double-duty as the drag-to-select handle, which
broke selection → "Add to Chat". Switch to Devin's model: disable Pierre's
interactive line selection and instead map a native text highlight in the
diff to a line range (via the data-line / data-line-type attributes Pierre
stamps on each line, read from the diff's open shadow root) to show the
"Add to Chat" popup. ⌘L and the existing attachment/popup path are unchanged.
* feat: gutter "+" drag selects a range for multi-line comments
Re-enable Pierre's gutter line selection so dragging the "+" down the
gutter comments across a range (click still comments on a single line);
onLineSelectionEnd routes the range to the composer. Native code-text
selection still drives "Add to Chat" — Pierre only line-selects from the
gutter, and onLineSelectionEnd bails when a native text selection is
present, so a code highlight never opens the composer.
* fix: keep the range highlighted while its comment composer is open
Previously opening the composer cleared the selection, so the lines being
commented on lost their highlight. Drive the controlled selection from the
open comment draft's range so the rows stay highlighted until the composer
is closed.
* fix: address PR review — paginate comments, fall back for outdated ones
- list_review_comments now pages through all PR review comments (bounded by
_MAX_REVIEW_COMMENT_PAGES) instead of returning only the first 100, so older
comments still show in the dropdown.
- Surface GitHub's outdated flag (position == null) as is_outdated; opening such
a comment (or one whose line isn't in the diff) now opens it on GitHub instead
of silently rendering nothing, plus a timeout fallback if the annotation never
mounts (e.g. collapsed context).
2026-06-23 16:01:46 -07:00
|
|
|
import { Textarea } from "@/components/ui/textarea"
|
2026-06-23 12:27:51 -07:00
|
|
|
import { api, reviewImageProxyUrl } from "@/lib/api"
|
|
|
|
|
import { cn } from "@/lib/utils"
|
|
|
|
|
|
|
|
|
|
type SideTab = "info" | "chat"
|
|
|
|
|
|
feat: inline PR comments in the reviews UI (#1600)
* feat: inline PR comments in the reviews UI
Click the diff gutter "+" on a line to open an inline comment composer
(rendered like the finding card via a Pierre annotation); submitting
posts a real inline PR review comment as the signed-in user through a
new POST /reviews/{owner}/{repo}/{number}/comments. The "+" press-drag →
"Add to Chat" selection path is unchanged.
* feat: GitHub-parity comment box, PR comments dropdown, collapse nav
- Comment composer now mirrors GitHub's box: Write/Preview tabs (markdown
rendered via the existing Markdown component) and a markdown toolbar
(heading, bold, italic, quote, code, link, bulleted/numbered/task list).
- Surface other people's inline PR comments in a Devin-style dropdown in the
review header (search + link to the thread on GitHub). New
GET /reviews/{owner}/{repo}/{number}/comments lists them and flags the
reviewer's own (marker-bearing) comments so they're filtered out.
- Collapse the global nav by default on a review detail page, restoring the
prior preference on leave.
* feat: bigger comment-toolbar icons; open dropdown comments inline
- Enlarge the markdown toolbar glyphs (Phosphor) in the comment composer —
they were rendering at 10px.
- Clicking a comment in the PR comments dropdown now opens it inline in the
diff as a read-only finding-style card (InlineComment), scrolling its line
into view, instead of navigating to GitHub. Falls back to GitHub when the
comment's file/line isn't in the current diff.
* fix: drive "Add to Chat" from native text selection
The gutter "+" is now comment-only; wiring its click to the composer
conflicted with its old double-duty as the drag-to-select handle, which
broke selection → "Add to Chat". Switch to Devin's model: disable Pierre's
interactive line selection and instead map a native text highlight in the
diff to a line range (via the data-line / data-line-type attributes Pierre
stamps on each line, read from the diff's open shadow root) to show the
"Add to Chat" popup. ⌘L and the existing attachment/popup path are unchanged.
* feat: gutter "+" drag selects a range for multi-line comments
Re-enable Pierre's gutter line selection so dragging the "+" down the
gutter comments across a range (click still comments on a single line);
onLineSelectionEnd routes the range to the composer. Native code-text
selection still drives "Add to Chat" — Pierre only line-selects from the
gutter, and onLineSelectionEnd bails when a native text selection is
present, so a code highlight never opens the composer.
* fix: keep the range highlighted while its comment composer is open
Previously opening the composer cleared the selection, so the lines being
commented on lost their highlight. Drive the controlled selection from the
open comment draft's range so the rows stay highlighted until the composer
is closed.
* fix: address PR review — paginate comments, fall back for outdated ones
- list_review_comments now pages through all PR review comments (bounded by
_MAX_REVIEW_COMMENT_PAGES) instead of returning only the first 100, so older
comments still show in the dropdown.
- Surface GitHub's outdated flag (position == null) as is_outdated; opening such
a comment (or one whose line isn't in the diff) now opens it on GitHub instead
of silently rendering nothing, plus a timeout fallback if the annotation never
mounts (e.g. collapsed context).
2026-06-23 16:01:46 -07:00
|
|
|
// Metadata carried by a Pierre diff line annotation. Findings render as the
|
|
|
|
|
// read-only InlineFinding card; a draftComment renders the inline composer; a
|
|
|
|
|
// comment renders an existing PR comment opened from the comments dropdown.
|
|
|
|
|
type ReviewAnnotation =
|
|
|
|
|
| { kind: "finding"; finding: ReviewFinding }
|
|
|
|
|
| { kind: "draftComment"; path: string; range: SelectedLineRange }
|
|
|
|
|
| { kind: "comment"; comment: PrReviewComment }
|
|
|
|
|
|
2026-06-23 12:27:51 -07:00
|
|
|
const REVIEW_VIEW_STORAGE_KEY = "open-swe.review.view"
|
|
|
|
|
const REVIEW_DIFF_STYLE_STORAGE_KEY = "open-swe.review.diffStyle"
|
|
|
|
|
const FINDING_SCROLL_MAX_FRAMES = 120
|
|
|
|
|
|
|
|
|
|
function readStoredDiffStyle(): DiffStyle {
|
|
|
|
|
if (typeof window === "undefined") return "unified"
|
|
|
|
|
return window.localStorage.getItem(REVIEW_DIFF_STYLE_STORAGE_KEY) === "split"
|
|
|
|
|
? "split"
|
|
|
|
|
: "unified"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// One attachment for a single-side line range. Deletions resolve against the
|
|
|
|
|
// original file, additions against the modified file.
|
|
|
|
|
function makeSideAttachment(
|
|
|
|
|
file: ReviewDiffFile,
|
|
|
|
|
side: "deletions" | "additions",
|
|
|
|
|
fromLine: number,
|
|
|
|
|
toLine: number
|
|
|
|
|
): ChatAttachment {
|
|
|
|
|
const source =
|
|
|
|
|
side === "deletions" ? file.originalContent : file.modifiedContent
|
|
|
|
|
const lines = source.split("\n")
|
|
|
|
|
const start = Math.max(1, Math.min(fromLine, toLine))
|
|
|
|
|
const end = Math.max(fromLine, toLine)
|
|
|
|
|
const snippet = lines.slice(start - 1, end).join("\n")
|
|
|
|
|
const sideLabel = side === "deletions" ? "L" : "R"
|
|
|
|
|
const lineLabel =
|
|
|
|
|
start === end ? `${sideLabel}${start}` : `${sideLabel}${start}-${end}`
|
|
|
|
|
const language = file.path.includes(".")
|
|
|
|
|
? (file.path.split(".").pop() ?? "")
|
|
|
|
|
: ""
|
|
|
|
|
return {
|
|
|
|
|
id: crypto.randomUUID(),
|
|
|
|
|
path: file.path,
|
|
|
|
|
lineLabel,
|
|
|
|
|
language,
|
|
|
|
|
snippet,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Build chat attachments from the selected range. A range can span from a
|
|
|
|
|
// deletion to an addition (side !== endSide) when dragging across a replaced
|
|
|
|
|
// block; slicing one file by start..end would paste the wrong lines, so each
|
|
|
|
|
// side is collected separately.
|
|
|
|
|
function buildSelectionAttachments(
|
|
|
|
|
file: ReviewDiffFile,
|
|
|
|
|
range: SelectedLineRange
|
|
|
|
|
): Array<ChatAttachment> {
|
|
|
|
|
const startSide = range.side ?? "additions"
|
|
|
|
|
const endSide = range.endSide ?? startSide
|
|
|
|
|
if (startSide === endSide) {
|
|
|
|
|
return [makeSideAttachment(file, startSide, range.start, range.end)]
|
|
|
|
|
}
|
|
|
|
|
const deletionLine = startSide === "deletions" ? range.start : range.end
|
|
|
|
|
const additionLine = startSide === "additions" ? range.start : range.end
|
|
|
|
|
return [
|
|
|
|
|
makeSideAttachment(file, "deletions", deletionLine, deletionLine),
|
|
|
|
|
makeSideAttachment(file, "additions", additionLine, additionLine),
|
|
|
|
|
]
|
|
|
|
|
}
|
|
|
|
|
|
feat: inline PR comments in the reviews UI (#1600)
* feat: inline PR comments in the reviews UI
Click the diff gutter "+" on a line to open an inline comment composer
(rendered like the finding card via a Pierre annotation); submitting
posts a real inline PR review comment as the signed-in user through a
new POST /reviews/{owner}/{repo}/{number}/comments. The "+" press-drag →
"Add to Chat" selection path is unchanged.
* feat: GitHub-parity comment box, PR comments dropdown, collapse nav
- Comment composer now mirrors GitHub's box: Write/Preview tabs (markdown
rendered via the existing Markdown component) and a markdown toolbar
(heading, bold, italic, quote, code, link, bulleted/numbered/task list).
- Surface other people's inline PR comments in a Devin-style dropdown in the
review header (search + link to the thread on GitHub). New
GET /reviews/{owner}/{repo}/{number}/comments lists them and flags the
reviewer's own (marker-bearing) comments so they're filtered out.
- Collapse the global nav by default on a review detail page, restoring the
prior preference on leave.
* feat: bigger comment-toolbar icons; open dropdown comments inline
- Enlarge the markdown toolbar glyphs (Phosphor) in the comment composer —
they were rendering at 10px.
- Clicking a comment in the PR comments dropdown now opens it inline in the
diff as a read-only finding-style card (InlineComment), scrolling its line
into view, instead of navigating to GitHub. Falls back to GitHub when the
comment's file/line isn't in the current diff.
* fix: drive "Add to Chat" from native text selection
The gutter "+" is now comment-only; wiring its click to the composer
conflicted with its old double-duty as the drag-to-select handle, which
broke selection → "Add to Chat". Switch to Devin's model: disable Pierre's
interactive line selection and instead map a native text highlight in the
diff to a line range (via the data-line / data-line-type attributes Pierre
stamps on each line, read from the diff's open shadow root) to show the
"Add to Chat" popup. ⌘L and the existing attachment/popup path are unchanged.
* feat: gutter "+" drag selects a range for multi-line comments
Re-enable Pierre's gutter line selection so dragging the "+" down the
gutter comments across a range (click still comments on a single line);
onLineSelectionEnd routes the range to the composer. Native code-text
selection still drives "Add to Chat" — Pierre only line-selects from the
gutter, and onLineSelectionEnd bails when a native text selection is
present, so a code highlight never opens the composer.
* fix: keep the range highlighted while its comment composer is open
Previously opening the composer cleared the selection, so the lines being
commented on lost their highlight. Drive the controlled selection from the
open comment draft's range so the rows stay highlighted until the composer
is closed.
* fix: address PR review — paginate comments, fall back for outdated ones
- list_review_comments now pages through all PR review comments (bounded by
_MAX_REVIEW_COMMENT_PAGES) instead of returning only the first 100, so older
comments still show in the dropdown.
- Surface GitHub's outdated flag (position == null) as is_outdated; opening such
a comment (or one whose line isn't in the diff) now opens it on GitHub instead
of silently rendering nothing, plus a timeout fallback if the annotation never
mounts (e.g. collapsed context).
2026-06-23 16:01:46 -07:00
|
|
|
interface ShadowRootWithSelection {
|
|
|
|
|
getSelection?: () => Selection | null
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Read the active selection inside a <diffs-container>'s open shadow root.
|
|
|
|
|
// Chromium exposes ShadowRoot.getSelection(); elsewhere fall back to the document
|
|
|
|
|
// selection (events from open shadow DOM are composed/retargeted).
|
|
|
|
|
function readDiffSelection(
|
|
|
|
|
container: Element | null | undefined
|
|
|
|
|
): Selection | null {
|
|
|
|
|
const root = container?.shadowRoot
|
|
|
|
|
if (root) {
|
|
|
|
|
const scoped = (root as ShadowRoot & ShadowRootWithSelection).getSelection
|
|
|
|
|
if (typeof scoped === "function") return scoped.call(root)
|
|
|
|
|
}
|
|
|
|
|
return typeof document !== "undefined" ? document.getSelection() : null
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Map a selection boundary node to its file line number + side via the
|
|
|
|
|
// data-line / data-line-type attributes Pierre stamps on every line div.
|
|
|
|
|
function lineMetaFromNode(
|
|
|
|
|
node: Node | null
|
|
|
|
|
): { line: number; side: SelectionSide } | null {
|
|
|
|
|
const el = node instanceof Element ? node : (node?.parentElement ?? null)
|
|
|
|
|
const lineEl = el?.closest("[data-line]")
|
|
|
|
|
if (!lineEl) return null
|
|
|
|
|
const line = Number(lineEl.getAttribute("data-line"))
|
|
|
|
|
if (!Number.isInteger(line)) return null
|
|
|
|
|
const type = lineEl.getAttribute("data-line-type") ?? ""
|
|
|
|
|
return { line, side: type.includes("deletion") ? "deletions" : "additions" }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Resolve the current native text selection inside a diff to a line range, so a
|
|
|
|
|
// plain text highlight can drive "Add to Chat" (Devin-style) instead of a
|
|
|
|
|
// gutter drag.
|
|
|
|
|
function selectedRangeFromDiff(
|
|
|
|
|
container: Element | null | undefined
|
|
|
|
|
): SelectedLineRange | null {
|
|
|
|
|
const selection = readDiffSelection(container)
|
|
|
|
|
if (!selection || selection.isCollapsed || selection.rangeCount === 0)
|
|
|
|
|
return null
|
|
|
|
|
const range = selection.getRangeAt(0)
|
|
|
|
|
const start = lineMetaFromNode(range.startContainer)
|
|
|
|
|
const end = lineMetaFromNode(range.endContainer)
|
|
|
|
|
if (!start || !end) return null
|
|
|
|
|
return {
|
|
|
|
|
start: start.line,
|
|
|
|
|
side: start.side,
|
|
|
|
|
end: end.line,
|
|
|
|
|
endSide: end.side,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-23 12:27:51 -07:00
|
|
|
// Scroll a file card / group flush to the top of the diff scroller. Under
|
|
|
|
|
// virtualization, scrollIntoView computes its target against estimated row
|
|
|
|
|
// heights; scrolling past unmeasured files reconciles their real heights
|
|
|
|
|
// mid-animation and the Virtualizer re-pins its scroll anchor, which leaves the
|
|
|
|
|
// target off the top. Once the smooth scroll settles, re-assert alignment (now
|
|
|
|
|
// against measured heights) until the target sits at the top or the budget runs
|
|
|
|
|
// out. Respects the element's scroll-margin-top.
|
|
|
|
|
function scrollCardToTop(el: HTMLElement, scroller: HTMLElement | null): void {
|
|
|
|
|
el.scrollIntoView({ block: "start", behavior: "smooth" })
|
|
|
|
|
if (!scroller) return
|
|
|
|
|
let frames = 0
|
|
|
|
|
let lastTop = Number.NaN
|
|
|
|
|
let stableFrames = 0
|
|
|
|
|
let corrections = 0
|
|
|
|
|
const align = () => {
|
|
|
|
|
if (frames++ > 240) return
|
|
|
|
|
const top = scroller.scrollTop
|
|
|
|
|
if (top === lastTop) stableFrames++
|
|
|
|
|
else {
|
|
|
|
|
stableFrames = 0
|
|
|
|
|
lastTop = top
|
|
|
|
|
}
|
|
|
|
|
// Wait for the smooth scroll + height reconciliation to settle.
|
|
|
|
|
if (stableFrames < 3) {
|
|
|
|
|
requestAnimationFrame(align)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
const marginTop = parseFloat(getComputedStyle(el).scrollMarginTop) || 0
|
|
|
|
|
const delta =
|
|
|
|
|
el.getBoundingClientRect().top -
|
|
|
|
|
scroller.getBoundingClientRect().top -
|
|
|
|
|
marginTop
|
|
|
|
|
if (Math.abs(delta) > 1 && corrections++ < 5) {
|
|
|
|
|
el.scrollIntoView({ block: "start", behavior: "smooth" })
|
|
|
|
|
stableFrames = 0
|
|
|
|
|
lastTop = Number.NaN
|
|
|
|
|
requestAnimationFrame(align)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
requestAnimationFrame(align)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface PositionedDiffInstance {
|
|
|
|
|
getLinePosition: (
|
|
|
|
|
lineNumber: number,
|
|
|
|
|
side?: SelectionSide
|
|
|
|
|
) => { top: number; height: number } | undefined
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface RegisteredDiffInstance {
|
|
|
|
|
host: HTMLElement
|
feat: inline PR comments in the reviews UI (#1600)
* feat: inline PR comments in the reviews UI
Click the diff gutter "+" on a line to open an inline comment composer
(rendered like the finding card via a Pierre annotation); submitting
posts a real inline PR review comment as the signed-in user through a
new POST /reviews/{owner}/{repo}/{number}/comments. The "+" press-drag →
"Add to Chat" selection path is unchanged.
* feat: GitHub-parity comment box, PR comments dropdown, collapse nav
- Comment composer now mirrors GitHub's box: Write/Preview tabs (markdown
rendered via the existing Markdown component) and a markdown toolbar
(heading, bold, italic, quote, code, link, bulleted/numbered/task list).
- Surface other people's inline PR comments in a Devin-style dropdown in the
review header (search + link to the thread on GitHub). New
GET /reviews/{owner}/{repo}/{number}/comments lists them and flags the
reviewer's own (marker-bearing) comments so they're filtered out.
- Collapse the global nav by default on a review detail page, restoring the
prior preference on leave.
* feat: bigger comment-toolbar icons; open dropdown comments inline
- Enlarge the markdown toolbar glyphs (Phosphor) in the comment composer —
they were rendering at 10px.
- Clicking a comment in the PR comments dropdown now opens it inline in the
diff as a read-only finding-style card (InlineComment), scrolling its line
into view, instead of navigating to GitHub. Falls back to GitHub when the
comment's file/line isn't in the current diff.
* fix: drive "Add to Chat" from native text selection
The gutter "+" is now comment-only; wiring its click to the composer
conflicted with its old double-duty as the drag-to-select handle, which
broke selection → "Add to Chat". Switch to Devin's model: disable Pierre's
interactive line selection and instead map a native text highlight in the
diff to a line range (via the data-line / data-line-type attributes Pierre
stamps on each line, read from the diff's open shadow root) to show the
"Add to Chat" popup. ⌘L and the existing attachment/popup path are unchanged.
* feat: gutter "+" drag selects a range for multi-line comments
Re-enable Pierre's gutter line selection so dragging the "+" down the
gutter comments across a range (click still comments on a single line);
onLineSelectionEnd routes the range to the composer. Native code-text
selection still drives "Add to Chat" — Pierre only line-selects from the
gutter, and onLineSelectionEnd bails when a native text selection is
present, so a code highlight never opens the composer.
* fix: keep the range highlighted while its comment composer is open
Previously opening the composer cleared the selection, so the lines being
commented on lost their highlight. Drive the controlled selection from the
open comment draft's range so the rows stay highlighted until the composer
is closed.
* fix: address PR review — paginate comments, fall back for outdated ones
- list_review_comments now pages through all PR review comments (bounded by
_MAX_REVIEW_COMMENT_PAGES) instead of returning only the first 100, so older
comments still show in the dropdown.
- Surface GitHub's outdated flag (position == null) as is_outdated; opening such
a comment (or one whose line isn't in the diff) now opens it on GitHub instead
of silently rendering nothing, plus a timeout fallback if the annotation never
mounts (e.g. collapsed context).
2026-06-23 16:01:46 -07:00
|
|
|
instance: CoreFileDiff<ReviewAnnotation>
|
2026-06-23 12:27:51 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function hasLinePosition(
|
feat: inline PR comments in the reviews UI (#1600)
* feat: inline PR comments in the reviews UI
Click the diff gutter "+" on a line to open an inline comment composer
(rendered like the finding card via a Pierre annotation); submitting
posts a real inline PR review comment as the signed-in user through a
new POST /reviews/{owner}/{repo}/{number}/comments. The "+" press-drag →
"Add to Chat" selection path is unchanged.
* feat: GitHub-parity comment box, PR comments dropdown, collapse nav
- Comment composer now mirrors GitHub's box: Write/Preview tabs (markdown
rendered via the existing Markdown component) and a markdown toolbar
(heading, bold, italic, quote, code, link, bulleted/numbered/task list).
- Surface other people's inline PR comments in a Devin-style dropdown in the
review header (search + link to the thread on GitHub). New
GET /reviews/{owner}/{repo}/{number}/comments lists them and flags the
reviewer's own (marker-bearing) comments so they're filtered out.
- Collapse the global nav by default on a review detail page, restoring the
prior preference on leave.
* feat: bigger comment-toolbar icons; open dropdown comments inline
- Enlarge the markdown toolbar glyphs (Phosphor) in the comment composer —
they were rendering at 10px.
- Clicking a comment in the PR comments dropdown now opens it inline in the
diff as a read-only finding-style card (InlineComment), scrolling its line
into view, instead of navigating to GitHub. Falls back to GitHub when the
comment's file/line isn't in the current diff.
* fix: drive "Add to Chat" from native text selection
The gutter "+" is now comment-only; wiring its click to the composer
conflicted with its old double-duty as the drag-to-select handle, which
broke selection → "Add to Chat". Switch to Devin's model: disable Pierre's
interactive line selection and instead map a native text highlight in the
diff to a line range (via the data-line / data-line-type attributes Pierre
stamps on each line, read from the diff's open shadow root) to show the
"Add to Chat" popup. ⌘L and the existing attachment/popup path are unchanged.
* feat: gutter "+" drag selects a range for multi-line comments
Re-enable Pierre's gutter line selection so dragging the "+" down the
gutter comments across a range (click still comments on a single line);
onLineSelectionEnd routes the range to the composer. Native code-text
selection still drives "Add to Chat" — Pierre only line-selects from the
gutter, and onLineSelectionEnd bails when a native text selection is
present, so a code highlight never opens the composer.
* fix: keep the range highlighted while its comment composer is open
Previously opening the composer cleared the selection, so the lines being
commented on lost their highlight. Drive the controlled selection from the
open comment draft's range so the rows stay highlighted until the composer
is closed.
* fix: address PR review — paginate comments, fall back for outdated ones
- list_review_comments now pages through all PR review comments (bounded by
_MAX_REVIEW_COMMENT_PAGES) instead of returning only the first 100, so older
comments still show in the dropdown.
- Surface GitHub's outdated flag (position == null) as is_outdated; opening such
a comment (or one whose line isn't in the diff) now opens it on GitHub instead
of silently rendering nothing, plus a timeout fallback if the annotation never
mounts (e.g. collapsed context).
2026-06-23 16:01:46 -07:00
|
|
|
instance: CoreFileDiff<ReviewAnnotation>
|
|
|
|
|
): instance is CoreFileDiff<ReviewAnnotation> & PositionedDiffInstance {
|
2026-06-23 12:27:51 -07:00
|
|
|
return (
|
|
|
|
|
typeof (instance as { getLinePosition?: unknown }).getLinePosition ===
|
|
|
|
|
"function"
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function clampScrollTop(scroller: HTMLElement, top: number): number {
|
|
|
|
|
return Math.max(
|
|
|
|
|
0,
|
|
|
|
|
Math.min(top, scroller.scrollHeight - scroller.clientHeight)
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function scrollElementToCenter(el: HTMLElement, scroller: HTMLElement): number {
|
|
|
|
|
const elementRect = el.getBoundingClientRect()
|
|
|
|
|
const scrollerRect = scroller.getBoundingClientRect()
|
|
|
|
|
const delta =
|
|
|
|
|
elementRect.top -
|
|
|
|
|
scrollerRect.top -
|
|
|
|
|
(scroller.clientHeight - elementRect.height) / 2
|
|
|
|
|
const targetTop = clampScrollTop(scroller, scroller.scrollTop + delta)
|
|
|
|
|
scroller.scrollTo({ top: targetTop, behavior: "auto" })
|
|
|
|
|
return Math.abs(delta)
|
|
|
|
|
}
|
|
|
|
|
|
feat: inline PR comments in the reviews UI (#1600)
* feat: inline PR comments in the reviews UI
Click the diff gutter "+" on a line to open an inline comment composer
(rendered like the finding card via a Pierre annotation); submitting
posts a real inline PR review comment as the signed-in user through a
new POST /reviews/{owner}/{repo}/{number}/comments. The "+" press-drag →
"Add to Chat" selection path is unchanged.
* feat: GitHub-parity comment box, PR comments dropdown, collapse nav
- Comment composer now mirrors GitHub's box: Write/Preview tabs (markdown
rendered via the existing Markdown component) and a markdown toolbar
(heading, bold, italic, quote, code, link, bulleted/numbered/task list).
- Surface other people's inline PR comments in a Devin-style dropdown in the
review header (search + link to the thread on GitHub). New
GET /reviews/{owner}/{repo}/{number}/comments lists them and flags the
reviewer's own (marker-bearing) comments so they're filtered out.
- Collapse the global nav by default on a review detail page, restoring the
prior preference on leave.
* feat: bigger comment-toolbar icons; open dropdown comments inline
- Enlarge the markdown toolbar glyphs (Phosphor) in the comment composer —
they were rendering at 10px.
- Clicking a comment in the PR comments dropdown now opens it inline in the
diff as a read-only finding-style card (InlineComment), scrolling its line
into view, instead of navigating to GitHub. Falls back to GitHub when the
comment's file/line isn't in the current diff.
* fix: drive "Add to Chat" from native text selection
The gutter "+" is now comment-only; wiring its click to the composer
conflicted with its old double-duty as the drag-to-select handle, which
broke selection → "Add to Chat". Switch to Devin's model: disable Pierre's
interactive line selection and instead map a native text highlight in the
diff to a line range (via the data-line / data-line-type attributes Pierre
stamps on each line, read from the diff's open shadow root) to show the
"Add to Chat" popup. ⌘L and the existing attachment/popup path are unchanged.
* feat: gutter "+" drag selects a range for multi-line comments
Re-enable Pierre's gutter line selection so dragging the "+" down the
gutter comments across a range (click still comments on a single line);
onLineSelectionEnd routes the range to the composer. Native code-text
selection still drives "Add to Chat" — Pierre only line-selects from the
gutter, and onLineSelectionEnd bails when a native text selection is
present, so a code highlight never opens the composer.
* fix: keep the range highlighted while its comment composer is open
Previously opening the composer cleared the selection, so the lines being
commented on lost their highlight. Drive the controlled selection from the
open comment draft's range so the rows stay highlighted until the composer
is closed.
* fix: address PR review — paginate comments, fall back for outdated ones
- list_review_comments now pages through all PR review comments (bounded by
_MAX_REVIEW_COMMENT_PAGES) instead of returning only the first 100, so older
comments still show in the dropdown.
- Surface GitHub's outdated flag (position == null) as is_outdated; opening such
a comment (or one whose line isn't in the diff) now opens it on GitHub instead
of silently rendering nothing, plus a timeout fallback if the annotation never
mounts (e.g. collapsed context).
2026-06-23 16:01:46 -07:00
|
|
|
function scrollDiffLineToCenter(
|
|
|
|
|
target: RegisteredDiffInstance,
|
|
|
|
|
lineNumber: number,
|
|
|
|
|
side: SelectionSide,
|
2026-06-23 12:27:51 -07:00
|
|
|
scroller: HTMLElement
|
feat: inline PR comments in the reviews UI (#1600)
* feat: inline PR comments in the reviews UI
Click the diff gutter "+" on a line to open an inline comment composer
(rendered like the finding card via a Pierre annotation); submitting
posts a real inline PR review comment as the signed-in user through a
new POST /reviews/{owner}/{repo}/{number}/comments. The "+" press-drag →
"Add to Chat" selection path is unchanged.
* feat: GitHub-parity comment box, PR comments dropdown, collapse nav
- Comment composer now mirrors GitHub's box: Write/Preview tabs (markdown
rendered via the existing Markdown component) and a markdown toolbar
(heading, bold, italic, quote, code, link, bulleted/numbered/task list).
- Surface other people's inline PR comments in a Devin-style dropdown in the
review header (search + link to the thread on GitHub). New
GET /reviews/{owner}/{repo}/{number}/comments lists them and flags the
reviewer's own (marker-bearing) comments so they're filtered out.
- Collapse the global nav by default on a review detail page, restoring the
prior preference on leave.
* feat: bigger comment-toolbar icons; open dropdown comments inline
- Enlarge the markdown toolbar glyphs (Phosphor) in the comment composer —
they were rendering at 10px.
- Clicking a comment in the PR comments dropdown now opens it inline in the
diff as a read-only finding-style card (InlineComment), scrolling its line
into view, instead of navigating to GitHub. Falls back to GitHub when the
comment's file/line isn't in the current diff.
* fix: drive "Add to Chat" from native text selection
The gutter "+" is now comment-only; wiring its click to the composer
conflicted with its old double-duty as the drag-to-select handle, which
broke selection → "Add to Chat". Switch to Devin's model: disable Pierre's
interactive line selection and instead map a native text highlight in the
diff to a line range (via the data-line / data-line-type attributes Pierre
stamps on each line, read from the diff's open shadow root) to show the
"Add to Chat" popup. ⌘L and the existing attachment/popup path are unchanged.
* feat: gutter "+" drag selects a range for multi-line comments
Re-enable Pierre's gutter line selection so dragging the "+" down the
gutter comments across a range (click still comments on a single line);
onLineSelectionEnd routes the range to the composer. Native code-text
selection still drives "Add to Chat" — Pierre only line-selects from the
gutter, and onLineSelectionEnd bails when a native text selection is
present, so a code highlight never opens the composer.
* fix: keep the range highlighted while its comment composer is open
Previously opening the composer cleared the selection, so the lines being
commented on lost their highlight. Drive the controlled selection from the
open comment draft's range so the rows stay highlighted until the composer
is closed.
* fix: address PR review — paginate comments, fall back for outdated ones
- list_review_comments now pages through all PR review comments (bounded by
_MAX_REVIEW_COMMENT_PAGES) instead of returning only the first 100, so older
comments still show in the dropdown.
- Surface GitHub's outdated flag (position == null) as is_outdated; opening such
a comment (or one whose line isn't in the diff) now opens it on GitHub instead
of silently rendering nothing, plus a timeout fallback if the annotation never
mounts (e.g. collapsed context).
2026-06-23 16:01:46 -07:00
|
|
|
): boolean {
|
|
|
|
|
if (!hasLinePosition(target.instance)) return false
|
|
|
|
|
const line = target.instance.getLinePosition(lineNumber, side)
|
2026-06-23 12:27:51 -07:00
|
|
|
if (!line) return false
|
|
|
|
|
const hostTop =
|
|
|
|
|
target.host.getBoundingClientRect().top -
|
|
|
|
|
scroller.getBoundingClientRect().top +
|
|
|
|
|
scroller.scrollTop
|
|
|
|
|
const targetTop = clampScrollTop(
|
|
|
|
|
scroller,
|
|
|
|
|
hostTop + line.top - (scroller.clientHeight - line.height) / 2
|
|
|
|
|
)
|
|
|
|
|
scroller.scrollTo({ top: targetTop, behavior: "auto" })
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
|
feat: inline PR comments in the reviews UI (#1600)
* feat: inline PR comments in the reviews UI
Click the diff gutter "+" on a line to open an inline comment composer
(rendered like the finding card via a Pierre annotation); submitting
posts a real inline PR review comment as the signed-in user through a
new POST /reviews/{owner}/{repo}/{number}/comments. The "+" press-drag →
"Add to Chat" selection path is unchanged.
* feat: GitHub-parity comment box, PR comments dropdown, collapse nav
- Comment composer now mirrors GitHub's box: Write/Preview tabs (markdown
rendered via the existing Markdown component) and a markdown toolbar
(heading, bold, italic, quote, code, link, bulleted/numbered/task list).
- Surface other people's inline PR comments in a Devin-style dropdown in the
review header (search + link to the thread on GitHub). New
GET /reviews/{owner}/{repo}/{number}/comments lists them and flags the
reviewer's own (marker-bearing) comments so they're filtered out.
- Collapse the global nav by default on a review detail page, restoring the
prior preference on leave.
* feat: bigger comment-toolbar icons; open dropdown comments inline
- Enlarge the markdown toolbar glyphs (Phosphor) in the comment composer —
they were rendering at 10px.
- Clicking a comment in the PR comments dropdown now opens it inline in the
diff as a read-only finding-style card (InlineComment), scrolling its line
into view, instead of navigating to GitHub. Falls back to GitHub when the
comment's file/line isn't in the current diff.
* fix: drive "Add to Chat" from native text selection
The gutter "+" is now comment-only; wiring its click to the composer
conflicted with its old double-duty as the drag-to-select handle, which
broke selection → "Add to Chat". Switch to Devin's model: disable Pierre's
interactive line selection and instead map a native text highlight in the
diff to a line range (via the data-line / data-line-type attributes Pierre
stamps on each line, read from the diff's open shadow root) to show the
"Add to Chat" popup. ⌘L and the existing attachment/popup path are unchanged.
* feat: gutter "+" drag selects a range for multi-line comments
Re-enable Pierre's gutter line selection so dragging the "+" down the
gutter comments across a range (click still comments on a single line);
onLineSelectionEnd routes the range to the composer. Native code-text
selection still drives "Add to Chat" — Pierre only line-selects from the
gutter, and onLineSelectionEnd bails when a native text selection is
present, so a code highlight never opens the composer.
* fix: keep the range highlighted while its comment composer is open
Previously opening the composer cleared the selection, so the lines being
commented on lost their highlight. Drive the controlled selection from the
open comment draft's range so the rows stay highlighted until the composer
is closed.
* fix: address PR review — paginate comments, fall back for outdated ones
- list_review_comments now pages through all PR review comments (bounded by
_MAX_REVIEW_COMMENT_PAGES) instead of returning only the first 100, so older
comments still show in the dropdown.
- Surface GitHub's outdated flag (position == null) as is_outdated; opening such
a comment (or one whose line isn't in the diff) now opens it on GitHub instead
of silently rendering nothing, plus a timeout fallback if the annotation never
mounts (e.g. collapsed context).
2026-06-23 16:01:46 -07:00
|
|
|
function scrollFindingLineToCenter({
|
|
|
|
|
target,
|
|
|
|
|
finding,
|
|
|
|
|
scroller,
|
|
|
|
|
}: {
|
|
|
|
|
target: RegisteredDiffInstance
|
|
|
|
|
finding: ReviewFinding
|
|
|
|
|
scroller: HTMLElement
|
|
|
|
|
}): boolean {
|
|
|
|
|
if (finding.end_line === null) return false
|
|
|
|
|
return scrollDiffLineToCenter(
|
|
|
|
|
target,
|
|
|
|
|
finding.end_line,
|
|
|
|
|
findingSide(finding),
|
|
|
|
|
scroller
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-23 12:27:51 -07:00
|
|
|
interface ResolvedGroup {
|
|
|
|
|
index: number
|
|
|
|
|
title: string
|
|
|
|
|
summary: string
|
|
|
|
|
files: Array<ReviewDiffFile>
|
|
|
|
|
additions: number
|
|
|
|
|
deletions: number
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const GROUP_STYLES = {
|
|
|
|
|
bug: { label: "Bug", className: "text-destructive", Icon: BugBeetleIcon },
|
|
|
|
|
investigate: {
|
|
|
|
|
label: "Investigate",
|
|
|
|
|
className: "text-amber-500",
|
|
|
|
|
Icon: FlagIcon,
|
|
|
|
|
},
|
|
|
|
|
informational: {
|
|
|
|
|
label: "Informational",
|
|
|
|
|
className: "text-muted-foreground",
|
|
|
|
|
Icon: InfoIcon,
|
|
|
|
|
},
|
|
|
|
|
} as const
|
|
|
|
|
|
|
|
|
|
function findingAnchorLabel(finding: ReviewFinding): string {
|
|
|
|
|
if (finding.start_line === null || finding.end_line === null)
|
|
|
|
|
return finding.file
|
|
|
|
|
if (finding.start_line === finding.end_line)
|
|
|
|
|
return `${finding.file}:${finding.end_line}`
|
|
|
|
|
return `${finding.file}:${finding.start_line}-${finding.end_line}`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function isAnchored(finding: ReviewFinding): boolean {
|
|
|
|
|
return Boolean(finding.file) && finding.in_diff && finding.end_line !== null
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function findingSide(finding: ReviewFinding): "deletions" | "additions" {
|
|
|
|
|
return finding.side === "LEFT" ? "deletions" : "additions"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function findingSelectedRange(
|
|
|
|
|
finding: ReviewFinding
|
|
|
|
|
): SelectedLineRange | null {
|
|
|
|
|
if (finding.end_line === null) return null
|
|
|
|
|
const side = findingSide(finding)
|
|
|
|
|
return {
|
|
|
|
|
start: finding.start_line ?? finding.end_line,
|
|
|
|
|
end: finding.end_line,
|
|
|
|
|
side,
|
|
|
|
|
endSide: side,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
feat: inline PR comments in the reviews UI (#1600)
* feat: inline PR comments in the reviews UI
Click the diff gutter "+" on a line to open an inline comment composer
(rendered like the finding card via a Pierre annotation); submitting
posts a real inline PR review comment as the signed-in user through a
new POST /reviews/{owner}/{repo}/{number}/comments. The "+" press-drag →
"Add to Chat" selection path is unchanged.
* feat: GitHub-parity comment box, PR comments dropdown, collapse nav
- Comment composer now mirrors GitHub's box: Write/Preview tabs (markdown
rendered via the existing Markdown component) and a markdown toolbar
(heading, bold, italic, quote, code, link, bulleted/numbered/task list).
- Surface other people's inline PR comments in a Devin-style dropdown in the
review header (search + link to the thread on GitHub). New
GET /reviews/{owner}/{repo}/{number}/comments lists them and flags the
reviewer's own (marker-bearing) comments so they're filtered out.
- Collapse the global nav by default on a review detail page, restoring the
prior preference on leave.
* feat: bigger comment-toolbar icons; open dropdown comments inline
- Enlarge the markdown toolbar glyphs (Phosphor) in the comment composer —
they were rendering at 10px.
- Clicking a comment in the PR comments dropdown now opens it inline in the
diff as a read-only finding-style card (InlineComment), scrolling its line
into view, instead of navigating to GitHub. Falls back to GitHub when the
comment's file/line isn't in the current diff.
* fix: drive "Add to Chat" from native text selection
The gutter "+" is now comment-only; wiring its click to the composer
conflicted with its old double-duty as the drag-to-select handle, which
broke selection → "Add to Chat". Switch to Devin's model: disable Pierre's
interactive line selection and instead map a native text highlight in the
diff to a line range (via the data-line / data-line-type attributes Pierre
stamps on each line, read from the diff's open shadow root) to show the
"Add to Chat" popup. ⌘L and the existing attachment/popup path are unchanged.
* feat: gutter "+" drag selects a range for multi-line comments
Re-enable Pierre's gutter line selection so dragging the "+" down the
gutter comments across a range (click still comments on a single line);
onLineSelectionEnd routes the range to the composer. Native code-text
selection still drives "Add to Chat" — Pierre only line-selects from the
gutter, and onLineSelectionEnd bails when a native text selection is
present, so a code highlight never opens the composer.
* fix: keep the range highlighted while its comment composer is open
Previously opening the composer cleared the selection, so the lines being
commented on lost their highlight. Drive the controlled selection from the
open comment draft's range so the rows stay highlighted until the composer
is closed.
* fix: address PR review — paginate comments, fall back for outdated ones
- list_review_comments now pages through all PR review comments (bounded by
_MAX_REVIEW_COMMENT_PAGES) instead of returning only the first 100, so older
comments still show in the dropdown.
- Surface GitHub's outdated flag (position == null) as is_outdated; opening such
a comment (or one whose line isn't in the diff) now opens it on GitHub instead
of silently rendering nothing, plus a timeout fallback if the annotation never
mounts (e.g. collapsed context).
2026-06-23 16:01:46 -07:00
|
|
|
function selectionSideToGithub(
|
|
|
|
|
side: SelectionSide | undefined
|
|
|
|
|
): "LEFT" | "RIGHT" {
|
|
|
|
|
return side === "deletions" ? "LEFT" : "RIGHT"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Map a Pierre selection range to a GitHub inline-comment payload. GitHub
|
|
|
|
|
// forbids multi-line ranges that span sides, so a cross-side selection collapses
|
|
|
|
|
// to a single line on the end side; same-side ranges keep their start_line.
|
|
|
|
|
function buildCommentPayload(
|
|
|
|
|
path: string,
|
|
|
|
|
range: SelectedLineRange,
|
|
|
|
|
body: string
|
|
|
|
|
): ReviewCommentCreate {
|
|
|
|
|
const startSide = range.side ?? "additions"
|
|
|
|
|
const endSide = range.endSide ?? startSide
|
|
|
|
|
if (startSide !== endSide) {
|
|
|
|
|
return {
|
|
|
|
|
path,
|
|
|
|
|
line: range.end,
|
|
|
|
|
side: selectionSideToGithub(endSide),
|
|
|
|
|
body,
|
|
|
|
|
start_line: null,
|
|
|
|
|
start_side: null,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
const side = selectionSideToGithub(endSide)
|
|
|
|
|
const lo = Math.min(range.start, range.end)
|
|
|
|
|
const hi = Math.max(range.start, range.end)
|
|
|
|
|
return {
|
|
|
|
|
path,
|
|
|
|
|
line: hi,
|
|
|
|
|
side,
|
|
|
|
|
body,
|
|
|
|
|
start_line: lo < hi ? lo : null,
|
|
|
|
|
start_side: lo < hi ? side : null,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function commentRangeLabel(range: SelectedLineRange): string {
|
|
|
|
|
const side = (range.endSide ?? range.side) === "deletions" ? "L" : "R"
|
|
|
|
|
const lo = Math.min(range.start, range.end)
|
|
|
|
|
const hi = Math.max(range.start, range.end)
|
|
|
|
|
return lo === hi ? `${side}${hi}` : `${side}${lo}-${hi}`
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-23 12:27:51 -07:00
|
|
|
function findingClipboardText(finding: ReviewFinding): string {
|
|
|
|
|
const style = GROUP_STYLES[finding.group]
|
|
|
|
|
const lines = [
|
|
|
|
|
`**${style.label}: ${finding.title}**`,
|
|
|
|
|
`${findingAnchorLabel(finding)}`,
|
|
|
|
|
"",
|
|
|
|
|
finding.description,
|
|
|
|
|
]
|
|
|
|
|
if (finding.suggestion)
|
|
|
|
|
lines.push("", "```suggestion", finding.suggestion, "```")
|
|
|
|
|
return lines.join("\n")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Inline findings live inside Pierre's diff via React portals, so their
|
|
|
|
|
// expand/collapse state is lifted here and shared through context — surviving
|
|
|
|
|
// the annotation's mount/unmount as rows window in and out under
|
|
|
|
|
// virtualization, and letting the side panel drive the same expansion.
|
|
|
|
|
interface ExpandedFindingContextValue {
|
|
|
|
|
expandedId: string | null
|
|
|
|
|
reviewUrl: string
|
|
|
|
|
toggle: (finding: ReviewFinding) => void
|
|
|
|
|
registerAnnotation: (id: string, node: HTMLElement | null) => void
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const ExpandedFindingContext =
|
|
|
|
|
createContext<ExpandedFindingContextValue | null>(null)
|
|
|
|
|
|
|
|
|
|
function useExpandedFinding(): ExpandedFindingContextValue {
|
|
|
|
|
const ctx = useContext(ExpandedFindingContext)
|
|
|
|
|
if (!ctx)
|
|
|
|
|
throw new Error("useExpandedFinding must be used within its provider")
|
|
|
|
|
return ctx
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const NO_FINDINGS: Array<ReviewFinding> = []
|
|
|
|
|
|
|
|
|
|
interface UserSelection {
|
|
|
|
|
file: string
|
|
|
|
|
range: SelectedLineRange
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export type ReviewMainBodyVariant = "full" | "embedded"
|
|
|
|
|
|
|
|
|
|
export interface ReviewMainBodyProps {
|
|
|
|
|
detail: ReviewDetail
|
|
|
|
|
diffFiles: Array<ReviewDiffFile> | null
|
|
|
|
|
// "full" renders the side panel + chat alongside the diffs; "embedded" renders
|
|
|
|
|
// just the main body with an expand affordance (used inside the git panel).
|
|
|
|
|
variant?: ReviewMainBodyVariant
|
|
|
|
|
onExpand?: () => void
|
feat: inline PR comments in the reviews UI (#1600)
* feat: inline PR comments in the reviews UI
Click the diff gutter "+" on a line to open an inline comment composer
(rendered like the finding card via a Pierre annotation); submitting
posts a real inline PR review comment as the signed-in user through a
new POST /reviews/{owner}/{repo}/{number}/comments. The "+" press-drag →
"Add to Chat" selection path is unchanged.
* feat: GitHub-parity comment box, PR comments dropdown, collapse nav
- Comment composer now mirrors GitHub's box: Write/Preview tabs (markdown
rendered via the existing Markdown component) and a markdown toolbar
(heading, bold, italic, quote, code, link, bulleted/numbered/task list).
- Surface other people's inline PR comments in a Devin-style dropdown in the
review header (search + link to the thread on GitHub). New
GET /reviews/{owner}/{repo}/{number}/comments lists them and flags the
reviewer's own (marker-bearing) comments so they're filtered out.
- Collapse the global nav by default on a review detail page, restoring the
prior preference on leave.
* feat: bigger comment-toolbar icons; open dropdown comments inline
- Enlarge the markdown toolbar glyphs (Phosphor) in the comment composer —
they were rendering at 10px.
- Clicking a comment in the PR comments dropdown now opens it inline in the
diff as a read-only finding-style card (InlineComment), scrolling its line
into view, instead of navigating to GitHub. Falls back to GitHub when the
comment's file/line isn't in the current diff.
* fix: drive "Add to Chat" from native text selection
The gutter "+" is now comment-only; wiring its click to the composer
conflicted with its old double-duty as the drag-to-select handle, which
broke selection → "Add to Chat". Switch to Devin's model: disable Pierre's
interactive line selection and instead map a native text highlight in the
diff to a line range (via the data-line / data-line-type attributes Pierre
stamps on each line, read from the diff's open shadow root) to show the
"Add to Chat" popup. ⌘L and the existing attachment/popup path are unchanged.
* feat: gutter "+" drag selects a range for multi-line comments
Re-enable Pierre's gutter line selection so dragging the "+" down the
gutter comments across a range (click still comments on a single line);
onLineSelectionEnd routes the range to the composer. Native code-text
selection still drives "Add to Chat" — Pierre only line-selects from the
gutter, and onLineSelectionEnd bails when a native text selection is
present, so a code highlight never opens the composer.
* fix: keep the range highlighted while its comment composer is open
Previously opening the composer cleared the selection, so the lines being
commented on lost their highlight. Drive the controlled selection from the
open comment draft's range so the rows stay highlighted until the composer
is closed.
* fix: address PR review — paginate comments, fall back for outdated ones
- list_review_comments now pages through all PR review comments (bounded by
_MAX_REVIEW_COMMENT_PAGES) instead of returning only the first 100, so older
comments still show in the dropdown.
- Surface GitHub's outdated flag (position == null) as is_outdated; opening such
a comment (or one whose line isn't in the diff) now opens it on GitHub instead
of silently rendering nothing, plus a timeout fallback if the annotation never
mounts (e.g. collapsed context).
2026-06-23 16:01:46 -07:00
|
|
|
// A PR comment opened from the comments dropdown: shown inline at its line.
|
|
|
|
|
openComment?: PrReviewComment | null
|
|
|
|
|
onCloseOpenComment?: () => void
|
2026-06-23 12:27:51 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function ReviewMainBody({
|
|
|
|
|
detail,
|
|
|
|
|
diffFiles,
|
|
|
|
|
variant = "full",
|
|
|
|
|
onExpand,
|
feat: inline PR comments in the reviews UI (#1600)
* feat: inline PR comments in the reviews UI
Click the diff gutter "+" on a line to open an inline comment composer
(rendered like the finding card via a Pierre annotation); submitting
posts a real inline PR review comment as the signed-in user through a
new POST /reviews/{owner}/{repo}/{number}/comments. The "+" press-drag →
"Add to Chat" selection path is unchanged.
* feat: GitHub-parity comment box, PR comments dropdown, collapse nav
- Comment composer now mirrors GitHub's box: Write/Preview tabs (markdown
rendered via the existing Markdown component) and a markdown toolbar
(heading, bold, italic, quote, code, link, bulleted/numbered/task list).
- Surface other people's inline PR comments in a Devin-style dropdown in the
review header (search + link to the thread on GitHub). New
GET /reviews/{owner}/{repo}/{number}/comments lists them and flags the
reviewer's own (marker-bearing) comments so they're filtered out.
- Collapse the global nav by default on a review detail page, restoring the
prior preference on leave.
* feat: bigger comment-toolbar icons; open dropdown comments inline
- Enlarge the markdown toolbar glyphs (Phosphor) in the comment composer —
they were rendering at 10px.
- Clicking a comment in the PR comments dropdown now opens it inline in the
diff as a read-only finding-style card (InlineComment), scrolling its line
into view, instead of navigating to GitHub. Falls back to GitHub when the
comment's file/line isn't in the current diff.
* fix: drive "Add to Chat" from native text selection
The gutter "+" is now comment-only; wiring its click to the composer
conflicted with its old double-duty as the drag-to-select handle, which
broke selection → "Add to Chat". Switch to Devin's model: disable Pierre's
interactive line selection and instead map a native text highlight in the
diff to a line range (via the data-line / data-line-type attributes Pierre
stamps on each line, read from the diff's open shadow root) to show the
"Add to Chat" popup. ⌘L and the existing attachment/popup path are unchanged.
* feat: gutter "+" drag selects a range for multi-line comments
Re-enable Pierre's gutter line selection so dragging the "+" down the
gutter comments across a range (click still comments on a single line);
onLineSelectionEnd routes the range to the composer. Native code-text
selection still drives "Add to Chat" — Pierre only line-selects from the
gutter, and onLineSelectionEnd bails when a native text selection is
present, so a code highlight never opens the composer.
* fix: keep the range highlighted while its comment composer is open
Previously opening the composer cleared the selection, so the lines being
commented on lost their highlight. Drive the controlled selection from the
open comment draft's range so the rows stay highlighted until the composer
is closed.
* fix: address PR review — paginate comments, fall back for outdated ones
- list_review_comments now pages through all PR review comments (bounded by
_MAX_REVIEW_COMMENT_PAGES) instead of returning only the first 100, so older
comments still show in the dropdown.
- Surface GitHub's outdated flag (position == null) as is_outdated; opening such
a comment (or one whose line isn't in the diff) now opens it on GitHub instead
of silently rendering nothing, plus a timeout fallback if the annotation never
mounts (e.g. collapsed context).
2026-06-23 16:01:46 -07:00
|
|
|
openComment,
|
|
|
|
|
onCloseOpenComment,
|
2026-06-23 12:27:51 -07:00
|
|
|
}: ReviewMainBodyProps) {
|
|
|
|
|
// The composer provider lives here so it remounts in lockstep with the
|
|
|
|
|
// head_sha-keyed body (and the activeId-keyed chat thread). The embedded
|
|
|
|
|
// variant has no chat, so it skips the provider.
|
|
|
|
|
if (variant === "embedded") {
|
|
|
|
|
return (
|
|
|
|
|
<ReviewBodyInner
|
|
|
|
|
detail={detail}
|
|
|
|
|
diffFiles={diffFiles}
|
|
|
|
|
variant="embedded"
|
|
|
|
|
onExpand={onExpand}
|
|
|
|
|
/>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
return (
|
|
|
|
|
<ReviewChatComposerProvider>
|
feat: inline PR comments in the reviews UI (#1600)
* feat: inline PR comments in the reviews UI
Click the diff gutter "+" on a line to open an inline comment composer
(rendered like the finding card via a Pierre annotation); submitting
posts a real inline PR review comment as the signed-in user through a
new POST /reviews/{owner}/{repo}/{number}/comments. The "+" press-drag →
"Add to Chat" selection path is unchanged.
* feat: GitHub-parity comment box, PR comments dropdown, collapse nav
- Comment composer now mirrors GitHub's box: Write/Preview tabs (markdown
rendered via the existing Markdown component) and a markdown toolbar
(heading, bold, italic, quote, code, link, bulleted/numbered/task list).
- Surface other people's inline PR comments in a Devin-style dropdown in the
review header (search + link to the thread on GitHub). New
GET /reviews/{owner}/{repo}/{number}/comments lists them and flags the
reviewer's own (marker-bearing) comments so they're filtered out.
- Collapse the global nav by default on a review detail page, restoring the
prior preference on leave.
* feat: bigger comment-toolbar icons; open dropdown comments inline
- Enlarge the markdown toolbar glyphs (Phosphor) in the comment composer —
they were rendering at 10px.
- Clicking a comment in the PR comments dropdown now opens it inline in the
diff as a read-only finding-style card (InlineComment), scrolling its line
into view, instead of navigating to GitHub. Falls back to GitHub when the
comment's file/line isn't in the current diff.
* fix: drive "Add to Chat" from native text selection
The gutter "+" is now comment-only; wiring its click to the composer
conflicted with its old double-duty as the drag-to-select handle, which
broke selection → "Add to Chat". Switch to Devin's model: disable Pierre's
interactive line selection and instead map a native text highlight in the
diff to a line range (via the data-line / data-line-type attributes Pierre
stamps on each line, read from the diff's open shadow root) to show the
"Add to Chat" popup. ⌘L and the existing attachment/popup path are unchanged.
* feat: gutter "+" drag selects a range for multi-line comments
Re-enable Pierre's gutter line selection so dragging the "+" down the
gutter comments across a range (click still comments on a single line);
onLineSelectionEnd routes the range to the composer. Native code-text
selection still drives "Add to Chat" — Pierre only line-selects from the
gutter, and onLineSelectionEnd bails when a native text selection is
present, so a code highlight never opens the composer.
* fix: keep the range highlighted while its comment composer is open
Previously opening the composer cleared the selection, so the lines being
commented on lost their highlight. Drive the controlled selection from the
open comment draft's range so the rows stay highlighted until the composer
is closed.
* fix: address PR review — paginate comments, fall back for outdated ones
- list_review_comments now pages through all PR review comments (bounded by
_MAX_REVIEW_COMMENT_PAGES) instead of returning only the first 100, so older
comments still show in the dropdown.
- Surface GitHub's outdated flag (position == null) as is_outdated; opening such
a comment (or one whose line isn't in the diff) now opens it on GitHub instead
of silently rendering nothing, plus a timeout fallback if the annotation never
mounts (e.g. collapsed context).
2026-06-23 16:01:46 -07:00
|
|
|
<ReviewBodyInner
|
|
|
|
|
detail={detail}
|
|
|
|
|
diffFiles={diffFiles}
|
|
|
|
|
variant="full"
|
|
|
|
|
openComment={openComment ?? null}
|
|
|
|
|
onCloseOpenComment={onCloseOpenComment}
|
|
|
|
|
/>
|
2026-06-23 12:27:51 -07:00
|
|
|
</ReviewChatComposerProvider>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function ReviewBodyInner({
|
|
|
|
|
detail,
|
|
|
|
|
diffFiles,
|
|
|
|
|
variant,
|
|
|
|
|
onExpand,
|
feat: inline PR comments in the reviews UI (#1600)
* feat: inline PR comments in the reviews UI
Click the diff gutter "+" on a line to open an inline comment composer
(rendered like the finding card via a Pierre annotation); submitting
posts a real inline PR review comment as the signed-in user through a
new POST /reviews/{owner}/{repo}/{number}/comments. The "+" press-drag →
"Add to Chat" selection path is unchanged.
* feat: GitHub-parity comment box, PR comments dropdown, collapse nav
- Comment composer now mirrors GitHub's box: Write/Preview tabs (markdown
rendered via the existing Markdown component) and a markdown toolbar
(heading, bold, italic, quote, code, link, bulleted/numbered/task list).
- Surface other people's inline PR comments in a Devin-style dropdown in the
review header (search + link to the thread on GitHub). New
GET /reviews/{owner}/{repo}/{number}/comments lists them and flags the
reviewer's own (marker-bearing) comments so they're filtered out.
- Collapse the global nav by default on a review detail page, restoring the
prior preference on leave.
* feat: bigger comment-toolbar icons; open dropdown comments inline
- Enlarge the markdown toolbar glyphs (Phosphor) in the comment composer —
they were rendering at 10px.
- Clicking a comment in the PR comments dropdown now opens it inline in the
diff as a read-only finding-style card (InlineComment), scrolling its line
into view, instead of navigating to GitHub. Falls back to GitHub when the
comment's file/line isn't in the current diff.
* fix: drive "Add to Chat" from native text selection
The gutter "+" is now comment-only; wiring its click to the composer
conflicted with its old double-duty as the drag-to-select handle, which
broke selection → "Add to Chat". Switch to Devin's model: disable Pierre's
interactive line selection and instead map a native text highlight in the
diff to a line range (via the data-line / data-line-type attributes Pierre
stamps on each line, read from the diff's open shadow root) to show the
"Add to Chat" popup. ⌘L and the existing attachment/popup path are unchanged.
* feat: gutter "+" drag selects a range for multi-line comments
Re-enable Pierre's gutter line selection so dragging the "+" down the
gutter comments across a range (click still comments on a single line);
onLineSelectionEnd routes the range to the composer. Native code-text
selection still drives "Add to Chat" — Pierre only line-selects from the
gutter, and onLineSelectionEnd bails when a native text selection is
present, so a code highlight never opens the composer.
* fix: keep the range highlighted while its comment composer is open
Previously opening the composer cleared the selection, so the lines being
commented on lost their highlight. Drive the controlled selection from the
open comment draft's range so the rows stay highlighted until the composer
is closed.
* fix: address PR review — paginate comments, fall back for outdated ones
- list_review_comments now pages through all PR review comments (bounded by
_MAX_REVIEW_COMMENT_PAGES) instead of returning only the first 100, so older
comments still show in the dropdown.
- Surface GitHub's outdated flag (position == null) as is_outdated; opening such
a comment (or one whose line isn't in the diff) now opens it on GitHub instead
of silently rendering nothing, plus a timeout fallback if the annotation never
mounts (e.g. collapsed context).
2026-06-23 16:01:46 -07:00
|
|
|
openComment = null,
|
|
|
|
|
onCloseOpenComment,
|
2026-06-23 12:27:51 -07:00
|
|
|
}: {
|
|
|
|
|
detail: ReviewDetail
|
|
|
|
|
diffFiles: Array<ReviewDiffFile> | null
|
|
|
|
|
variant: ReviewMainBodyVariant
|
|
|
|
|
onExpand?: () => void
|
feat: inline PR comments in the reviews UI (#1600)
* feat: inline PR comments in the reviews UI
Click the diff gutter "+" on a line to open an inline comment composer
(rendered like the finding card via a Pierre annotation); submitting
posts a real inline PR review comment as the signed-in user through a
new POST /reviews/{owner}/{repo}/{number}/comments. The "+" press-drag →
"Add to Chat" selection path is unchanged.
* feat: GitHub-parity comment box, PR comments dropdown, collapse nav
- Comment composer now mirrors GitHub's box: Write/Preview tabs (markdown
rendered via the existing Markdown component) and a markdown toolbar
(heading, bold, italic, quote, code, link, bulleted/numbered/task list).
- Surface other people's inline PR comments in a Devin-style dropdown in the
review header (search + link to the thread on GitHub). New
GET /reviews/{owner}/{repo}/{number}/comments lists them and flags the
reviewer's own (marker-bearing) comments so they're filtered out.
- Collapse the global nav by default on a review detail page, restoring the
prior preference on leave.
* feat: bigger comment-toolbar icons; open dropdown comments inline
- Enlarge the markdown toolbar glyphs (Phosphor) in the comment composer —
they were rendering at 10px.
- Clicking a comment in the PR comments dropdown now opens it inline in the
diff as a read-only finding-style card (InlineComment), scrolling its line
into view, instead of navigating to GitHub. Falls back to GitHub when the
comment's file/line isn't in the current diff.
* fix: drive "Add to Chat" from native text selection
The gutter "+" is now comment-only; wiring its click to the composer
conflicted with its old double-duty as the drag-to-select handle, which
broke selection → "Add to Chat". Switch to Devin's model: disable Pierre's
interactive line selection and instead map a native text highlight in the
diff to a line range (via the data-line / data-line-type attributes Pierre
stamps on each line, read from the diff's open shadow root) to show the
"Add to Chat" popup. ⌘L and the existing attachment/popup path are unchanged.
* feat: gutter "+" drag selects a range for multi-line comments
Re-enable Pierre's gutter line selection so dragging the "+" down the
gutter comments across a range (click still comments on a single line);
onLineSelectionEnd routes the range to the composer. Native code-text
selection still drives "Add to Chat" — Pierre only line-selects from the
gutter, and onLineSelectionEnd bails when a native text selection is
present, so a code highlight never opens the composer.
* fix: keep the range highlighted while its comment composer is open
Previously opening the composer cleared the selection, so the lines being
commented on lost their highlight. Drive the controlled selection from the
open comment draft's range so the rows stay highlighted until the composer
is closed.
* fix: address PR review — paginate comments, fall back for outdated ones
- list_review_comments now pages through all PR review comments (bounded by
_MAX_REVIEW_COMMENT_PAGES) instead of returning only the first 100, so older
comments still show in the dropdown.
- Surface GitHub's outdated flag (position == null) as is_outdated; opening such
a comment (or one whose line isn't in the diff) now opens it on GitHub instead
of silently rendering nothing, plus a timeout fallback if the annotation never
mounts (e.g. collapsed context).
2026-06-23 16:01:46 -07:00
|
|
|
openComment?: PrReviewComment | null
|
|
|
|
|
onCloseOpenComment?: () => void
|
2026-06-23 12:27:51 -07:00
|
|
|
}) {
|
|
|
|
|
const embedded = variant === "embedded"
|
|
|
|
|
const composer = useReviewChatComposer()
|
|
|
|
|
const transformPrImage = useCallback(
|
|
|
|
|
(src: string) =>
|
|
|
|
|
reviewImageProxyUrl(detail.owner, detail.repo, detail.number, src),
|
|
|
|
|
[detail.owner, detail.repo, detail.number]
|
|
|
|
|
)
|
|
|
|
|
const [sideTab, setSideTab] = useState<SideTab>("info")
|
|
|
|
|
const [selectedFile, setSelectedFile] = useState<string | null>(null)
|
|
|
|
|
const fileRefs = useRef<Record<string, HTMLDivElement | null>>({})
|
|
|
|
|
const diffInstanceRefs = useRef<
|
|
|
|
|
Record<string, RegisteredDiffInstance | undefined>
|
|
|
|
|
>({})
|
|
|
|
|
const annotationRefs = useRef<Record<string, HTMLElement | null>>({})
|
|
|
|
|
const [expandedFiles, setExpandedFiles] = useState<Record<string, boolean>>(
|
|
|
|
|
{}
|
|
|
|
|
)
|
|
|
|
|
const [expandedId, setExpandedId] = useState<string | null>(null)
|
|
|
|
|
const [userSelection, setUserSelection] = useState<UserSelection | null>(null)
|
feat: inline PR comments in the reviews UI (#1600)
* feat: inline PR comments in the reviews UI
Click the diff gutter "+" on a line to open an inline comment composer
(rendered like the finding card via a Pierre annotation); submitting
posts a real inline PR review comment as the signed-in user through a
new POST /reviews/{owner}/{repo}/{number}/comments. The "+" press-drag →
"Add to Chat" selection path is unchanged.
* feat: GitHub-parity comment box, PR comments dropdown, collapse nav
- Comment composer now mirrors GitHub's box: Write/Preview tabs (markdown
rendered via the existing Markdown component) and a markdown toolbar
(heading, bold, italic, quote, code, link, bulleted/numbered/task list).
- Surface other people's inline PR comments in a Devin-style dropdown in the
review header (search + link to the thread on GitHub). New
GET /reviews/{owner}/{repo}/{number}/comments lists them and flags the
reviewer's own (marker-bearing) comments so they're filtered out.
- Collapse the global nav by default on a review detail page, restoring the
prior preference on leave.
* feat: bigger comment-toolbar icons; open dropdown comments inline
- Enlarge the markdown toolbar glyphs (Phosphor) in the comment composer —
they were rendering at 10px.
- Clicking a comment in the PR comments dropdown now opens it inline in the
diff as a read-only finding-style card (InlineComment), scrolling its line
into view, instead of navigating to GitHub. Falls back to GitHub when the
comment's file/line isn't in the current diff.
* fix: drive "Add to Chat" from native text selection
The gutter "+" is now comment-only; wiring its click to the composer
conflicted with its old double-duty as the drag-to-select handle, which
broke selection → "Add to Chat". Switch to Devin's model: disable Pierre's
interactive line selection and instead map a native text highlight in the
diff to a line range (via the data-line / data-line-type attributes Pierre
stamps on each line, read from the diff's open shadow root) to show the
"Add to Chat" popup. ⌘L and the existing attachment/popup path are unchanged.
* feat: gutter "+" drag selects a range for multi-line comments
Re-enable Pierre's gutter line selection so dragging the "+" down the
gutter comments across a range (click still comments on a single line);
onLineSelectionEnd routes the range to the composer. Native code-text
selection still drives "Add to Chat" — Pierre only line-selects from the
gutter, and onLineSelectionEnd bails when a native text selection is
present, so a code highlight never opens the composer.
* fix: keep the range highlighted while its comment composer is open
Previously opening the composer cleared the selection, so the lines being
commented on lost their highlight. Drive the controlled selection from the
open comment draft's range so the rows stay highlighted until the composer
is closed.
* fix: address PR review — paginate comments, fall back for outdated ones
- list_review_comments now pages through all PR review comments (bounded by
_MAX_REVIEW_COMMENT_PAGES) instead of returning only the first 100, so older
comments still show in the dropdown.
- Surface GitHub's outdated flag (position == null) as is_outdated; opening such
a comment (or one whose line isn't in the diff) now opens it on GitHub instead
of silently rendering nothing, plus a timeout fallback if the annotation never
mounts (e.g. collapsed context).
2026-06-23 16:01:46 -07:00
|
|
|
// The single open inline comment composer (at most one across all files).
|
|
|
|
|
const [commentDraft, setCommentDraft] = useState<{
|
|
|
|
|
file: string
|
|
|
|
|
range: SelectedLineRange
|
|
|
|
|
} | null>(null)
|
2026-06-23 12:27:51 -07:00
|
|
|
const diffScrollElRef = useRef<HTMLDivElement | null>(null)
|
|
|
|
|
const findingScrollRequestRef = useRef(0)
|
|
|
|
|
const groupRefs = useRef<Record<number, HTMLDivElement | null>>({})
|
|
|
|
|
const [diffStyle, setDiffStyleState] = useState<DiffStyle>(() =>
|
|
|
|
|
readStoredDiffStyle()
|
|
|
|
|
)
|
|
|
|
|
const setDiffStyle = useCallback((next: DiffStyle) => {
|
|
|
|
|
setDiffStyleState(next)
|
|
|
|
|
if (typeof window !== "undefined") {
|
|
|
|
|
window.localStorage.setItem(REVIEW_DIFF_STYLE_STORAGE_KEY, next)
|
|
|
|
|
}
|
|
|
|
|
}, [])
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
void warmDiffHighlighter()
|
|
|
|
|
}, [])
|
|
|
|
|
|
|
|
|
|
// Latest-value refs so the callbacks below can stay referentially stable
|
|
|
|
|
// (so memo(FileDiffCard) actually skips unrelated re-renders) while still
|
|
|
|
|
// reading current state.
|
|
|
|
|
const expandedFinding = useMemo(
|
|
|
|
|
() => detail.findings.find((f) => f.id === expandedId) ?? null,
|
|
|
|
|
[detail.findings, expandedId]
|
|
|
|
|
)
|
|
|
|
|
const expandedFindingRef = useRef(expandedFinding)
|
|
|
|
|
expandedFindingRef.current = expandedFinding
|
|
|
|
|
|
|
|
|
|
const viewedStorageKey = `open-swe.review.viewed.${detail.owner}/${detail.repo}/${detail.number}.${detail.head_sha}`
|
|
|
|
|
const [viewed, setViewed] = useState<Set<string>>(() => {
|
|
|
|
|
if (typeof window === "undefined") return new Set()
|
|
|
|
|
try {
|
|
|
|
|
const raw = window.localStorage.getItem(viewedStorageKey)
|
|
|
|
|
return new Set(raw ? (JSON.parse(raw) as Array<string>) : [])
|
|
|
|
|
} catch {
|
|
|
|
|
return new Set()
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
const viewedRef = useRef(viewed)
|
|
|
|
|
viewedRef.current = viewed
|
|
|
|
|
const expandedRef = useRef(expandedFiles)
|
|
|
|
|
expandedRef.current = expandedFiles
|
|
|
|
|
|
|
|
|
|
const toggleViewed = useCallback(
|
|
|
|
|
(path: string) => {
|
|
|
|
|
const becomingViewed = !viewedRef.current.has(path)
|
|
|
|
|
setViewed((prev) => {
|
|
|
|
|
const next = new Set(prev)
|
|
|
|
|
if (becomingViewed) next.add(path)
|
|
|
|
|
else next.delete(path)
|
|
|
|
|
window.localStorage.setItem(
|
|
|
|
|
viewedStorageKey,
|
|
|
|
|
JSON.stringify(Array.from(next))
|
|
|
|
|
)
|
|
|
|
|
return next
|
|
|
|
|
})
|
|
|
|
|
if (becomingViewed && expandedFindingRef.current?.file === path)
|
|
|
|
|
setExpandedId(null)
|
|
|
|
|
setExpandedFiles((prev) => ({ ...prev, [path]: !becomingViewed }))
|
|
|
|
|
},
|
|
|
|
|
[viewedStorageKey]
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const readStorageKey = `open-swe.review.read.${detail.thread_id}`
|
|
|
|
|
const [read, setRead] = useState<Set<string>>(() => {
|
|
|
|
|
if (typeof window === "undefined") return new Set()
|
|
|
|
|
try {
|
|
|
|
|
const raw = window.localStorage.getItem(readStorageKey)
|
|
|
|
|
return new Set(raw ? (JSON.parse(raw) as Array<string>) : [])
|
|
|
|
|
} catch {
|
|
|
|
|
return new Set()
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
const persistRead = useCallback(
|
|
|
|
|
(next: Set<string>) => {
|
|
|
|
|
window.localStorage.setItem(
|
|
|
|
|
readStorageKey,
|
|
|
|
|
JSON.stringify(Array.from(next))
|
|
|
|
|
)
|
|
|
|
|
},
|
|
|
|
|
[readStorageKey]
|
|
|
|
|
)
|
|
|
|
|
const markRead = useCallback(
|
|
|
|
|
(id: string) => {
|
|
|
|
|
setRead((prev) => {
|
|
|
|
|
const next = new Set(prev).add(id)
|
|
|
|
|
persistRead(next)
|
|
|
|
|
return next
|
|
|
|
|
})
|
|
|
|
|
},
|
|
|
|
|
[persistRead]
|
|
|
|
|
)
|
|
|
|
|
const markAllRead = useCallback(() => {
|
|
|
|
|
const next = new Set(detail.findings.map((f) => f.id))
|
|
|
|
|
setRead(next)
|
|
|
|
|
persistRead(next)
|
|
|
|
|
}, [detail.findings, persistRead])
|
|
|
|
|
|
|
|
|
|
const findingsByFile = useMemo(() => {
|
|
|
|
|
const byFile = new Map<string, Array<ReviewFinding>>()
|
|
|
|
|
for (const finding of detail.findings) {
|
|
|
|
|
if (!isAnchored(finding)) continue
|
|
|
|
|
const list = byFile.get(finding.file) ?? []
|
|
|
|
|
list.push(finding)
|
|
|
|
|
byFile.set(finding.file, list)
|
|
|
|
|
}
|
|
|
|
|
return byFile
|
|
|
|
|
}, [detail.findings])
|
|
|
|
|
|
|
|
|
|
const linesLeft = useMemo(() => {
|
|
|
|
|
if (!diffFiles) return null
|
|
|
|
|
return diffFiles
|
|
|
|
|
.filter((file) => !viewed.has(file.path))
|
|
|
|
|
.reduce((acc, file) => acc + file.additions + file.deletions, 0)
|
|
|
|
|
}, [diffFiles, viewed])
|
|
|
|
|
|
|
|
|
|
// Resolve the AI-sorted groups against the actual diff: drop stale groups
|
|
|
|
|
// (generated for a previous head) so the file-tree fallback is used, drop
|
|
|
|
|
// paths no longer in the diff and empty groups, and collect any unassigned
|
|
|
|
|
// files into a trailing "Other changes" group so nothing ever disappears.
|
|
|
|
|
const groupedView = useMemo<Array<ResolvedGroup> | null>(() => {
|
|
|
|
|
if (
|
|
|
|
|
!diffFiles ||
|
|
|
|
|
detail.diff_groups_stale ||
|
|
|
|
|
detail.diff_groups.length === 0
|
|
|
|
|
)
|
|
|
|
|
return null
|
|
|
|
|
const byPath = new Map(diffFiles.map((file) => [file.path, file]))
|
|
|
|
|
const assigned = new Set<string>()
|
|
|
|
|
const resolved: Array<Omit<ResolvedGroup, "index">> = []
|
|
|
|
|
for (const group of detail.diff_groups) {
|
|
|
|
|
const files: Array<ReviewDiffFile> = []
|
|
|
|
|
for (const path of group.files) {
|
|
|
|
|
const file = byPath.get(path)
|
|
|
|
|
if (file && !assigned.has(path)) {
|
|
|
|
|
assigned.add(path)
|
|
|
|
|
files.push(file)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (files.length === 0) continue
|
|
|
|
|
resolved.push({
|
|
|
|
|
title: group.title,
|
|
|
|
|
summary: group.summary,
|
|
|
|
|
files,
|
|
|
|
|
additions: files.reduce((acc, file) => acc + file.additions, 0),
|
|
|
|
|
deletions: files.reduce((acc, file) => acc + file.deletions, 0),
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
const leftover = diffFiles.filter((file) => !assigned.has(file.path))
|
|
|
|
|
if (leftover.length > 0) {
|
|
|
|
|
resolved.push({
|
|
|
|
|
title: "Other changes",
|
|
|
|
|
summary: "",
|
|
|
|
|
files: leftover,
|
|
|
|
|
additions: leftover.reduce((acc, file) => acc + file.additions, 0),
|
|
|
|
|
deletions: leftover.reduce((acc, file) => acc + file.deletions, 0),
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
if (resolved.length === 0) return null
|
|
|
|
|
return resolved.map((group, i) => ({ ...group, index: i + 1 }))
|
|
|
|
|
}, [diffFiles, detail.diff_groups, detail.diff_groups_stale])
|
|
|
|
|
|
|
|
|
|
const sidebarGroups = useMemo<Array<ReviewSidebarGroup> | null>(() => {
|
|
|
|
|
if (!groupedView) return null
|
|
|
|
|
return groupedView.map((group) => ({
|
|
|
|
|
index: group.index,
|
|
|
|
|
title: group.title,
|
|
|
|
|
summary: group.summary,
|
|
|
|
|
additions: group.additions,
|
|
|
|
|
deletions: group.deletions,
|
|
|
|
|
fileCount: group.files.length,
|
|
|
|
|
files: group.files.map((file) => file.path),
|
|
|
|
|
}))
|
|
|
|
|
}, [groupedView])
|
|
|
|
|
|
|
|
|
|
// The view follows fresh-group availability until the user explicitly picks
|
|
|
|
|
// one, after which the choice persists across PRs.
|
|
|
|
|
const hasFreshGroups =
|
|
|
|
|
detail.diff_groups.length > 0 && !detail.diff_groups_stale
|
|
|
|
|
const [explicitView, setExplicitView] = useState<ReviewSidebarView | null>(
|
|
|
|
|
() => {
|
|
|
|
|
if (typeof window === "undefined") return null
|
|
|
|
|
const stored = window.localStorage.getItem(REVIEW_VIEW_STORAGE_KEY)
|
|
|
|
|
return stored === "ai" || stored === "files" ? stored : null
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
const view: ReviewSidebarView =
|
|
|
|
|
explicitView ?? (hasFreshGroups ? "ai" : "files")
|
|
|
|
|
const setView = useCallback((next: ReviewSidebarView) => {
|
|
|
|
|
setExplicitView(next)
|
|
|
|
|
if (typeof window !== "undefined") {
|
|
|
|
|
window.localStorage.setItem(REVIEW_VIEW_STORAGE_KEY, next)
|
|
|
|
|
}
|
|
|
|
|
}, [])
|
|
|
|
|
|
|
|
|
|
const scrollToFile = useCallback((path: string) => {
|
|
|
|
|
setSelectedFile(path)
|
|
|
|
|
setExpandedFiles((prev) => ({ ...prev, [path]: true }))
|
|
|
|
|
requestAnimationFrame(() => {
|
|
|
|
|
const el = fileRefs.current[path]
|
|
|
|
|
if (el) scrollCardToTop(el, diffScrollElRef.current)
|
|
|
|
|
})
|
|
|
|
|
}, [])
|
|
|
|
|
|
|
|
|
|
const scrollToGroup = useCallback((index: number) => {
|
|
|
|
|
requestAnimationFrame(() => {
|
|
|
|
|
const el = groupRefs.current[index]
|
|
|
|
|
if (el) scrollCardToTop(el, diffScrollElRef.current)
|
|
|
|
|
})
|
|
|
|
|
}, [])
|
|
|
|
|
|
|
|
|
|
const filesByPath = useMemo(
|
|
|
|
|
() => new Map((diffFiles ?? []).map((file) => [file.path, file])),
|
|
|
|
|
[diffFiles]
|
|
|
|
|
)
|
|
|
|
|
const filesByPathRef = useRef(filesByPath)
|
|
|
|
|
filesByPathRef.current = filesByPath
|
|
|
|
|
|
|
|
|
|
// The Virtualizer doesn't forward a ref; grab its scroll element (the
|
|
|
|
|
// grandparent of this hidden probe, which lives in its content div) so
|
|
|
|
|
// scroll-to-file/group can align against it.
|
|
|
|
|
const scrollerProbe = useCallback((node: HTMLDivElement | null) => {
|
|
|
|
|
const scroller = node?.parentElement?.parentElement
|
|
|
|
|
diffScrollElRef.current =
|
|
|
|
|
scroller instanceof HTMLDivElement ? scroller : null
|
|
|
|
|
}, [])
|
|
|
|
|
|
|
|
|
|
const registerSection = useCallback(
|
|
|
|
|
(path: string, node: HTMLDivElement | null) => {
|
|
|
|
|
fileRefs.current[path] = node
|
|
|
|
|
},
|
|
|
|
|
[]
|
|
|
|
|
)
|
|
|
|
|
const registerAnnotation = useCallback(
|
|
|
|
|
(id: string, node: HTMLElement | null) => {
|
|
|
|
|
annotationRefs.current[id] = node
|
|
|
|
|
},
|
|
|
|
|
[]
|
|
|
|
|
)
|
|
|
|
|
const registerDiffInstance = useCallback(
|
|
|
|
|
(path: string, target: RegisteredDiffInstance | null) => {
|
|
|
|
|
if (target) diffInstanceRefs.current[path] = target
|
|
|
|
|
else delete diffInstanceRefs.current[path]
|
|
|
|
|
},
|
|
|
|
|
[]
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const toggleExpanded = useCallback((path: string) => {
|
|
|
|
|
const current = expandedRef.current[path] ?? !viewedRef.current.has(path)
|
|
|
|
|
const next = !current
|
|
|
|
|
if (!next && expandedFindingRef.current?.file === path) setExpandedId(null)
|
|
|
|
|
setExpandedFiles((prev) => ({ ...prev, [path]: next }))
|
|
|
|
|
}, [])
|
|
|
|
|
|
|
|
|
|
const selectLines = useCallback(
|
|
|
|
|
(path: string, range: SelectedLineRange | null) => {
|
|
|
|
|
if (range) {
|
|
|
|
|
setUserSelection({ file: path, range })
|
|
|
|
|
if (expandedFindingRef.current) setExpandedId(null)
|
|
|
|
|
} else {
|
|
|
|
|
setUserSelection((prev) => (prev?.file === path ? null : prev))
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
[]
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const addToChat = useCallback(
|
|
|
|
|
(path: string, range: SelectedLineRange) => {
|
|
|
|
|
const file = filesByPathRef.current.get(path)
|
|
|
|
|
if (!file) return
|
|
|
|
|
for (const attachment of buildSelectionAttachments(file, range)) {
|
|
|
|
|
composer?.addAttachment(attachment)
|
|
|
|
|
}
|
|
|
|
|
setSideTab("chat")
|
|
|
|
|
setUserSelection(null)
|
|
|
|
|
},
|
|
|
|
|
[composer]
|
|
|
|
|
)
|
|
|
|
|
|
feat: inline PR comments in the reviews UI (#1600)
* feat: inline PR comments in the reviews UI
Click the diff gutter "+" on a line to open an inline comment composer
(rendered like the finding card via a Pierre annotation); submitting
posts a real inline PR review comment as the signed-in user through a
new POST /reviews/{owner}/{repo}/{number}/comments. The "+" press-drag →
"Add to Chat" selection path is unchanged.
* feat: GitHub-parity comment box, PR comments dropdown, collapse nav
- Comment composer now mirrors GitHub's box: Write/Preview tabs (markdown
rendered via the existing Markdown component) and a markdown toolbar
(heading, bold, italic, quote, code, link, bulleted/numbered/task list).
- Surface other people's inline PR comments in a Devin-style dropdown in the
review header (search + link to the thread on GitHub). New
GET /reviews/{owner}/{repo}/{number}/comments lists them and flags the
reviewer's own (marker-bearing) comments so they're filtered out.
- Collapse the global nav by default on a review detail page, restoring the
prior preference on leave.
* feat: bigger comment-toolbar icons; open dropdown comments inline
- Enlarge the markdown toolbar glyphs (Phosphor) in the comment composer —
they were rendering at 10px.
- Clicking a comment in the PR comments dropdown now opens it inline in the
diff as a read-only finding-style card (InlineComment), scrolling its line
into view, instead of navigating to GitHub. Falls back to GitHub when the
comment's file/line isn't in the current diff.
* fix: drive "Add to Chat" from native text selection
The gutter "+" is now comment-only; wiring its click to the composer
conflicted with its old double-duty as the drag-to-select handle, which
broke selection → "Add to Chat". Switch to Devin's model: disable Pierre's
interactive line selection and instead map a native text highlight in the
diff to a line range (via the data-line / data-line-type attributes Pierre
stamps on each line, read from the diff's open shadow root) to show the
"Add to Chat" popup. ⌘L and the existing attachment/popup path are unchanged.
* feat: gutter "+" drag selects a range for multi-line comments
Re-enable Pierre's gutter line selection so dragging the "+" down the
gutter comments across a range (click still comments on a single line);
onLineSelectionEnd routes the range to the composer. Native code-text
selection still drives "Add to Chat" — Pierre only line-selects from the
gutter, and onLineSelectionEnd bails when a native text selection is
present, so a code highlight never opens the composer.
* fix: keep the range highlighted while its comment composer is open
Previously opening the composer cleared the selection, so the lines being
commented on lost their highlight. Drive the controlled selection from the
open comment draft's range so the rows stay highlighted until the composer
is closed.
* fix: address PR review — paginate comments, fall back for outdated ones
- list_review_comments now pages through all PR review comments (bounded by
_MAX_REVIEW_COMMENT_PAGES) instead of returning only the first 100, so older
comments still show in the dropdown.
- Surface GitHub's outdated flag (position == null) as is_outdated; opening such
a comment (or one whose line isn't in the diff) now opens it on GitHub instead
of silently rendering nothing, plus a timeout fallback if the annotation never
mounts (e.g. collapsed context).
2026-06-23 16:01:46 -07:00
|
|
|
// Open the inline comment composer for a line (gutter "+" click). Clearing the
|
|
|
|
|
// chat selection + expanded finding keeps the "+" owned by the composer alone.
|
|
|
|
|
const startComment = useCallback((path: string, range: SelectedLineRange) => {
|
|
|
|
|
setUserSelection(null)
|
|
|
|
|
setExpandedId(null)
|
|
|
|
|
setCommentDraft({ file: path, range })
|
|
|
|
|
}, [])
|
|
|
|
|
const closeComment = useCallback(() => setCommentDraft(null), [])
|
|
|
|
|
|
2026-06-23 12:27:51 -07:00
|
|
|
// ⌘L / Ctrl+L adds the current line selection to the chat (Cursor-style).
|
|
|
|
|
const userSelectionRef = useRef(userSelection)
|
|
|
|
|
userSelectionRef.current = userSelection
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
const onKeyDown = (event: KeyboardEvent) => {
|
|
|
|
|
if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === "l") {
|
|
|
|
|
const sel = userSelectionRef.current
|
|
|
|
|
if (sel) {
|
|
|
|
|
event.preventDefault()
|
|
|
|
|
addToChat(sel.file, sel.range)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
window.addEventListener("keydown", onKeyDown)
|
|
|
|
|
return () => window.removeEventListener("keydown", onKeyDown)
|
|
|
|
|
}, [addToChat])
|
|
|
|
|
|
|
|
|
|
// Clicking away from the highlighted rows clears the selection. A pointer-down
|
|
|
|
|
// that begins a fresh selection clears here first, then the new drag repaints.
|
|
|
|
|
// Reads the ref so the listener is registered once (no churn during a drag).
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
const onPointerDown = (event: PointerEvent) => {
|
|
|
|
|
if (!userSelectionRef.current) return
|
|
|
|
|
const target = event.target
|
|
|
|
|
if (target instanceof Element && target.closest("[data-add-to-chat]"))
|
|
|
|
|
return
|
|
|
|
|
setUserSelection(null)
|
|
|
|
|
}
|
|
|
|
|
window.addEventListener("pointerdown", onPointerDown)
|
|
|
|
|
return () => window.removeEventListener("pointerdown", onPointerDown)
|
|
|
|
|
}, [])
|
|
|
|
|
|
|
|
|
|
// Toggle a finding from its in-diff header — it's already on-screen, so no
|
|
|
|
|
// scrolling is needed.
|
|
|
|
|
const toggleInline = useCallback(
|
|
|
|
|
(finding: ReviewFinding) => {
|
|
|
|
|
markRead(finding.id)
|
|
|
|
|
setExpandedId((prev) => (prev === finding.id ? null : finding.id))
|
|
|
|
|
},
|
|
|
|
|
[markRead]
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// Open a finding from the side panel. Anchored findings expand inline in the
|
|
|
|
|
// diff: open the file, scroll it into view, then poll a few frames for the
|
|
|
|
|
// annotation node (its diff rows window in/out under virtualization) and
|
|
|
|
|
// scroll that into view. Non-anchored findings expand inline in the panel.
|
|
|
|
|
const openFromPanel = useCallback(
|
|
|
|
|
(finding: ReviewFinding) => {
|
|
|
|
|
markRead(finding.id)
|
|
|
|
|
const willExpand = expandedFindingRef.current?.id !== finding.id
|
|
|
|
|
const requestId = ++findingScrollRequestRef.current
|
|
|
|
|
setUserSelection(null)
|
|
|
|
|
setExpandedId(willExpand ? finding.id : null)
|
|
|
|
|
if (!willExpand || !isAnchored(finding)) return
|
|
|
|
|
setSelectedFile(finding.file)
|
|
|
|
|
setExpandedFiles((prev) => ({ ...prev, [finding.file]: true }))
|
|
|
|
|
let frames = 0
|
|
|
|
|
let lineScrollDone = false
|
|
|
|
|
const snap = () => {
|
|
|
|
|
if (requestId !== findingScrollRequestRef.current) return
|
|
|
|
|
const scroller = diffScrollElRef.current
|
|
|
|
|
if (!scroller) return
|
|
|
|
|
|
|
|
|
|
const annotation = annotationRefs.current[finding.id]
|
|
|
|
|
if (annotation?.isConnected && annotation.getClientRects().length > 0) {
|
|
|
|
|
const delta = scrollElementToCenter(annotation, scroller)
|
|
|
|
|
if (delta <= 1 || frames >= FINDING_SCROLL_MAX_FRAMES) return
|
|
|
|
|
frames += 1
|
|
|
|
|
requestAnimationFrame(snap)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const diffTarget = diffInstanceRefs.current[finding.file]
|
|
|
|
|
if (diffTarget) {
|
|
|
|
|
lineScrollDone = scrollFindingLineToCenter({
|
|
|
|
|
target: diffTarget,
|
|
|
|
|
finding,
|
|
|
|
|
scroller,
|
|
|
|
|
})
|
|
|
|
|
} else if (!lineScrollDone) {
|
|
|
|
|
const fileNode = fileRefs.current[finding.file]
|
|
|
|
|
if (fileNode) scrollElementToCenter(fileNode, scroller)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (frames++ < FINDING_SCROLL_MAX_FRAMES) requestAnimationFrame(snap)
|
|
|
|
|
}
|
|
|
|
|
requestAnimationFrame(snap)
|
|
|
|
|
},
|
|
|
|
|
[markRead]
|
|
|
|
|
)
|
|
|
|
|
|
feat: inline PR comments in the reviews UI (#1600)
* feat: inline PR comments in the reviews UI
Click the diff gutter "+" on a line to open an inline comment composer
(rendered like the finding card via a Pierre annotation); submitting
posts a real inline PR review comment as the signed-in user through a
new POST /reviews/{owner}/{repo}/{number}/comments. The "+" press-drag →
"Add to Chat" selection path is unchanged.
* feat: GitHub-parity comment box, PR comments dropdown, collapse nav
- Comment composer now mirrors GitHub's box: Write/Preview tabs (markdown
rendered via the existing Markdown component) and a markdown toolbar
(heading, bold, italic, quote, code, link, bulleted/numbered/task list).
- Surface other people's inline PR comments in a Devin-style dropdown in the
review header (search + link to the thread on GitHub). New
GET /reviews/{owner}/{repo}/{number}/comments lists them and flags the
reviewer's own (marker-bearing) comments so they're filtered out.
- Collapse the global nav by default on a review detail page, restoring the
prior preference on leave.
* feat: bigger comment-toolbar icons; open dropdown comments inline
- Enlarge the markdown toolbar glyphs (Phosphor) in the comment composer —
they were rendering at 10px.
- Clicking a comment in the PR comments dropdown now opens it inline in the
diff as a read-only finding-style card (InlineComment), scrolling its line
into view, instead of navigating to GitHub. Falls back to GitHub when the
comment's file/line isn't in the current diff.
* fix: drive "Add to Chat" from native text selection
The gutter "+" is now comment-only; wiring its click to the composer
conflicted with its old double-duty as the drag-to-select handle, which
broke selection → "Add to Chat". Switch to Devin's model: disable Pierre's
interactive line selection and instead map a native text highlight in the
diff to a line range (via the data-line / data-line-type attributes Pierre
stamps on each line, read from the diff's open shadow root) to show the
"Add to Chat" popup. ⌘L and the existing attachment/popup path are unchanged.
* feat: gutter "+" drag selects a range for multi-line comments
Re-enable Pierre's gutter line selection so dragging the "+" down the
gutter comments across a range (click still comments on a single line);
onLineSelectionEnd routes the range to the composer. Native code-text
selection still drives "Add to Chat" — Pierre only line-selects from the
gutter, and onLineSelectionEnd bails when a native text selection is
present, so a code highlight never opens the composer.
* fix: keep the range highlighted while its comment composer is open
Previously opening the composer cleared the selection, so the lines being
commented on lost their highlight. Drive the controlled selection from the
open comment draft's range so the rows stay highlighted until the composer
is closed.
* fix: address PR review — paginate comments, fall back for outdated ones
- list_review_comments now pages through all PR review comments (bounded by
_MAX_REVIEW_COMMENT_PAGES) instead of returning only the first 100, so older
comments still show in the dropdown.
- Surface GitHub's outdated flag (position == null) as is_outdated; opening such
a comment (or one whose line isn't in the diff) now opens it on GitHub instead
of silently rendering nothing, plus a timeout fallback if the annotation never
mounts (e.g. collapsed context).
2026-06-23 16:01:46 -07:00
|
|
|
// Open an existing PR comment inline: expand its file and scroll its line to
|
|
|
|
|
// center (mirrors openFromPanel). Comments whose file/line aren't in the
|
|
|
|
|
// current diff (e.g. outdated) have no inline anchor, so fall back to GitHub.
|
|
|
|
|
const closeOpenCommentRef = useRef(onCloseOpenComment)
|
|
|
|
|
closeOpenCommentRef.current = onCloseOpenComment
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (!openComment) return
|
|
|
|
|
const { path, line } = openComment
|
|
|
|
|
const fallbackToGitHub = () => {
|
|
|
|
|
if (openComment.html_url) {
|
|
|
|
|
window.open(openComment.html_url, "_blank", "noopener,noreferrer")
|
|
|
|
|
}
|
|
|
|
|
closeOpenCommentRef.current?.()
|
|
|
|
|
}
|
|
|
|
|
const file = filesByPathRef.current.get(path)
|
|
|
|
|
// No inline anchor: the file isn't in the diff, the comment has no line, or
|
|
|
|
|
// it's outdated (its line no longer appears in the current diff).
|
|
|
|
|
if (!file || line === null || openComment.is_outdated) {
|
|
|
|
|
fallbackToGitHub()
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
setSelectedFile(path)
|
|
|
|
|
setExpandedFiles((prev) => ({ ...prev, [path]: true }))
|
|
|
|
|
const requestId = ++findingScrollRequestRef.current
|
|
|
|
|
const side: SelectionSide =
|
|
|
|
|
openComment.side === "LEFT" ? "deletions" : "additions"
|
|
|
|
|
const key = `comment:${openComment.id}`
|
|
|
|
|
let frames = 0
|
|
|
|
|
let lineScrollDone = false
|
|
|
|
|
let mounted = false
|
|
|
|
|
const snap = () => {
|
|
|
|
|
if (requestId !== findingScrollRequestRef.current) return
|
|
|
|
|
const scroller = diffScrollElRef.current
|
|
|
|
|
if (!scroller) return
|
|
|
|
|
const annotation = annotationRefs.current[key]
|
|
|
|
|
if (annotation?.isConnected && annotation.getClientRects().length > 0) {
|
|
|
|
|
mounted = true
|
|
|
|
|
const delta = scrollElementToCenter(annotation, scroller)
|
|
|
|
|
if (delta <= 1 || frames >= FINDING_SCROLL_MAX_FRAMES) return
|
|
|
|
|
frames += 1
|
|
|
|
|
requestAnimationFrame(snap)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
const diffTarget = diffInstanceRefs.current[path]
|
|
|
|
|
if (diffTarget) {
|
|
|
|
|
lineScrollDone = scrollDiffLineToCenter(
|
|
|
|
|
diffTarget,
|
|
|
|
|
line,
|
|
|
|
|
side,
|
|
|
|
|
scroller
|
|
|
|
|
)
|
|
|
|
|
} else if (!lineScrollDone) {
|
|
|
|
|
const fileNode = fileRefs.current[path]
|
|
|
|
|
if (fileNode) scrollElementToCenter(fileNode, scroller)
|
|
|
|
|
}
|
|
|
|
|
if (frames++ < FINDING_SCROLL_MAX_FRAMES) {
|
|
|
|
|
requestAnimationFrame(snap)
|
|
|
|
|
} else if (!mounted) {
|
|
|
|
|
// The line never rendered (e.g. collapsed context) — fall back to GitHub
|
|
|
|
|
// rather than leaving the menu closed with nothing shown.
|
|
|
|
|
fallbackToGitHub()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
requestAnimationFrame(snap)
|
|
|
|
|
}, [openComment])
|
|
|
|
|
|
2026-06-23 12:27:51 -07:00
|
|
|
const renderFileCard = (file: ReviewDiffFile) => {
|
feat: inline PR comments in the reviews UI (#1600)
* feat: inline PR comments in the reviews UI
Click the diff gutter "+" on a line to open an inline comment composer
(rendered like the finding card via a Pierre annotation); submitting
posts a real inline PR review comment as the signed-in user through a
new POST /reviews/{owner}/{repo}/{number}/comments. The "+" press-drag →
"Add to Chat" selection path is unchanged.
* feat: GitHub-parity comment box, PR comments dropdown, collapse nav
- Comment composer now mirrors GitHub's box: Write/Preview tabs (markdown
rendered via the existing Markdown component) and a markdown toolbar
(heading, bold, italic, quote, code, link, bulleted/numbered/task list).
- Surface other people's inline PR comments in a Devin-style dropdown in the
review header (search + link to the thread on GitHub). New
GET /reviews/{owner}/{repo}/{number}/comments lists them and flags the
reviewer's own (marker-bearing) comments so they're filtered out.
- Collapse the global nav by default on a review detail page, restoring the
prior preference on leave.
* feat: bigger comment-toolbar icons; open dropdown comments inline
- Enlarge the markdown toolbar glyphs (Phosphor) in the comment composer —
they were rendering at 10px.
- Clicking a comment in the PR comments dropdown now opens it inline in the
diff as a read-only finding-style card (InlineComment), scrolling its line
into view, instead of navigating to GitHub. Falls back to GitHub when the
comment's file/line isn't in the current diff.
* fix: drive "Add to Chat" from native text selection
The gutter "+" is now comment-only; wiring its click to the composer
conflicted with its old double-duty as the drag-to-select handle, which
broke selection → "Add to Chat". Switch to Devin's model: disable Pierre's
interactive line selection and instead map a native text highlight in the
diff to a line range (via the data-line / data-line-type attributes Pierre
stamps on each line, read from the diff's open shadow root) to show the
"Add to Chat" popup. ⌘L and the existing attachment/popup path are unchanged.
* feat: gutter "+" drag selects a range for multi-line comments
Re-enable Pierre's gutter line selection so dragging the "+" down the
gutter comments across a range (click still comments on a single line);
onLineSelectionEnd routes the range to the composer. Native code-text
selection still drives "Add to Chat" — Pierre only line-selects from the
gutter, and onLineSelectionEnd bails when a native text selection is
present, so a code highlight never opens the composer.
* fix: keep the range highlighted while its comment composer is open
Previously opening the composer cleared the selection, so the lines being
commented on lost their highlight. Drive the controlled selection from the
open comment draft's range so the rows stay highlighted until the composer
is closed.
* fix: address PR review — paginate comments, fall back for outdated ones
- list_review_comments now pages through all PR review comments (bounded by
_MAX_REVIEW_COMMENT_PAGES) instead of returning only the first 100, so older
comments still show in the dropdown.
- Surface GitHub's outdated flag (position == null) as is_outdated; opening such
a comment (or one whose line isn't in the diff) now opens it on GitHub instead
of silently rendering nothing, plus a timeout fallback if the annotation never
mounts (e.g. collapsed context).
2026-06-23 16:01:46 -07:00
|
|
|
// Keep the range highlighted while its comment composer is open, so the
|
|
|
|
|
// user can see exactly which lines they're commenting on.
|
2026-06-23 12:27:51 -07:00
|
|
|
const selectedLines =
|
|
|
|
|
expandedFinding?.file === file.path && isAnchored(expandedFinding)
|
|
|
|
|
? findingSelectedRange(expandedFinding)
|
feat: inline PR comments in the reviews UI (#1600)
* feat: inline PR comments in the reviews UI
Click the diff gutter "+" on a line to open an inline comment composer
(rendered like the finding card via a Pierre annotation); submitting
posts a real inline PR review comment as the signed-in user through a
new POST /reviews/{owner}/{repo}/{number}/comments. The "+" press-drag →
"Add to Chat" selection path is unchanged.
* feat: GitHub-parity comment box, PR comments dropdown, collapse nav
- Comment composer now mirrors GitHub's box: Write/Preview tabs (markdown
rendered via the existing Markdown component) and a markdown toolbar
(heading, bold, italic, quote, code, link, bulleted/numbered/task list).
- Surface other people's inline PR comments in a Devin-style dropdown in the
review header (search + link to the thread on GitHub). New
GET /reviews/{owner}/{repo}/{number}/comments lists them and flags the
reviewer's own (marker-bearing) comments so they're filtered out.
- Collapse the global nav by default on a review detail page, restoring the
prior preference on leave.
* feat: bigger comment-toolbar icons; open dropdown comments inline
- Enlarge the markdown toolbar glyphs (Phosphor) in the comment composer —
they were rendering at 10px.
- Clicking a comment in the PR comments dropdown now opens it inline in the
diff as a read-only finding-style card (InlineComment), scrolling its line
into view, instead of navigating to GitHub. Falls back to GitHub when the
comment's file/line isn't in the current diff.
* fix: drive "Add to Chat" from native text selection
The gutter "+" is now comment-only; wiring its click to the composer
conflicted with its old double-duty as the drag-to-select handle, which
broke selection → "Add to Chat". Switch to Devin's model: disable Pierre's
interactive line selection and instead map a native text highlight in the
diff to a line range (via the data-line / data-line-type attributes Pierre
stamps on each line, read from the diff's open shadow root) to show the
"Add to Chat" popup. ⌘L and the existing attachment/popup path are unchanged.
* feat: gutter "+" drag selects a range for multi-line comments
Re-enable Pierre's gutter line selection so dragging the "+" down the
gutter comments across a range (click still comments on a single line);
onLineSelectionEnd routes the range to the composer. Native code-text
selection still drives "Add to Chat" — Pierre only line-selects from the
gutter, and onLineSelectionEnd bails when a native text selection is
present, so a code highlight never opens the composer.
* fix: keep the range highlighted while its comment composer is open
Previously opening the composer cleared the selection, so the lines being
commented on lost their highlight. Drive the controlled selection from the
open comment draft's range so the rows stay highlighted until the composer
is closed.
* fix: address PR review — paginate comments, fall back for outdated ones
- list_review_comments now pages through all PR review comments (bounded by
_MAX_REVIEW_COMMENT_PAGES) instead of returning only the first 100, so older
comments still show in the dropdown.
- Surface GitHub's outdated flag (position == null) as is_outdated; opening such
a comment (or one whose line isn't in the diff) now opens it on GitHub instead
of silently rendering nothing, plus a timeout fallback if the annotation never
mounts (e.g. collapsed context).
2026-06-23 16:01:46 -07:00
|
|
|
: commentDraft?.file === file.path
|
|
|
|
|
? commentDraft.range
|
|
|
|
|
: userSelection?.file === file.path
|
|
|
|
|
? userSelection.range
|
|
|
|
|
: null
|
2026-06-23 12:27:51 -07:00
|
|
|
return (
|
|
|
|
|
<FileDiffCard
|
|
|
|
|
key={file.path}
|
|
|
|
|
file={file}
|
|
|
|
|
findings={findingsByFile.get(file.path) ?? NO_FINDINGS}
|
|
|
|
|
selectedLines={selectedLines}
|
|
|
|
|
viewed={viewed.has(file.path)}
|
|
|
|
|
onToggleViewed={toggleViewed}
|
|
|
|
|
expanded={expandedFiles[file.path] ?? !viewed.has(file.path)}
|
|
|
|
|
onToggleExpanded={toggleExpanded}
|
|
|
|
|
onSelectLines={selectLines}
|
|
|
|
|
onAddToChat={embedded ? undefined : addToChat}
|
|
|
|
|
registerSection={registerSection}
|
|
|
|
|
registerDiffInstance={registerDiffInstance}
|
|
|
|
|
diffStyle={diffStyle}
|
feat: inline PR comments in the reviews UI (#1600)
* feat: inline PR comments in the reviews UI
Click the diff gutter "+" on a line to open an inline comment composer
(rendered like the finding card via a Pierre annotation); submitting
posts a real inline PR review comment as the signed-in user through a
new POST /reviews/{owner}/{repo}/{number}/comments. The "+" press-drag →
"Add to Chat" selection path is unchanged.
* feat: GitHub-parity comment box, PR comments dropdown, collapse nav
- Comment composer now mirrors GitHub's box: Write/Preview tabs (markdown
rendered via the existing Markdown component) and a markdown toolbar
(heading, bold, italic, quote, code, link, bulleted/numbered/task list).
- Surface other people's inline PR comments in a Devin-style dropdown in the
review header (search + link to the thread on GitHub). New
GET /reviews/{owner}/{repo}/{number}/comments lists them and flags the
reviewer's own (marker-bearing) comments so they're filtered out.
- Collapse the global nav by default on a review detail page, restoring the
prior preference on leave.
* feat: bigger comment-toolbar icons; open dropdown comments inline
- Enlarge the markdown toolbar glyphs (Phosphor) in the comment composer —
they were rendering at 10px.
- Clicking a comment in the PR comments dropdown now opens it inline in the
diff as a read-only finding-style card (InlineComment), scrolling its line
into view, instead of navigating to GitHub. Falls back to GitHub when the
comment's file/line isn't in the current diff.
* fix: drive "Add to Chat" from native text selection
The gutter "+" is now comment-only; wiring its click to the composer
conflicted with its old double-duty as the drag-to-select handle, which
broke selection → "Add to Chat". Switch to Devin's model: disable Pierre's
interactive line selection and instead map a native text highlight in the
diff to a line range (via the data-line / data-line-type attributes Pierre
stamps on each line, read from the diff's open shadow root) to show the
"Add to Chat" popup. ⌘L and the existing attachment/popup path are unchanged.
* feat: gutter "+" drag selects a range for multi-line comments
Re-enable Pierre's gutter line selection so dragging the "+" down the
gutter comments across a range (click still comments on a single line);
onLineSelectionEnd routes the range to the composer. Native code-text
selection still drives "Add to Chat" — Pierre only line-selects from the
gutter, and onLineSelectionEnd bails when a native text selection is
present, so a code highlight never opens the composer.
* fix: keep the range highlighted while its comment composer is open
Previously opening the composer cleared the selection, so the lines being
commented on lost their highlight. Drive the controlled selection from the
open comment draft's range so the rows stay highlighted until the composer
is closed.
* fix: address PR review — paginate comments, fall back for outdated ones
- list_review_comments now pages through all PR review comments (bounded by
_MAX_REVIEW_COMMENT_PAGES) instead of returning only the first 100, so older
comments still show in the dropdown.
- Surface GitHub's outdated flag (position == null) as is_outdated; opening such
a comment (or one whose line isn't in the diff) now opens it on GitHub instead
of silently rendering nothing, plus a timeout fallback if the annotation never
mounts (e.g. collapsed context).
2026-06-23 16:01:46 -07:00
|
|
|
owner={detail.owner}
|
|
|
|
|
repo={detail.repo}
|
|
|
|
|
prNumber={detail.number}
|
|
|
|
|
commentDraftRange={
|
|
|
|
|
commentDraft?.file === file.path ? commentDraft.range : null
|
|
|
|
|
}
|
|
|
|
|
onStartComment={embedded ? undefined : startComment}
|
|
|
|
|
onCloseComment={closeComment}
|
|
|
|
|
openComment={openComment?.path === file.path ? openComment : null}
|
|
|
|
|
onCloseOpenComment={onCloseOpenComment}
|
2026-06-23 12:27:51 -07:00
|
|
|
/>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const sidebarData = useMemo(
|
|
|
|
|
() => ({
|
|
|
|
|
title: `PR #${detail.number}`,
|
|
|
|
|
files: diffFiles,
|
|
|
|
|
selected: selectedFile,
|
|
|
|
|
viewed,
|
|
|
|
|
onSelect: scrollToFile,
|
|
|
|
|
groups: sidebarGroups,
|
|
|
|
|
view,
|
|
|
|
|
onViewChange: setView,
|
|
|
|
|
onSelectGroup: scrollToGroup,
|
|
|
|
|
}),
|
|
|
|
|
[
|
|
|
|
|
detail.number,
|
|
|
|
|
diffFiles,
|
|
|
|
|
selectedFile,
|
|
|
|
|
viewed,
|
|
|
|
|
scrollToFile,
|
|
|
|
|
sidebarGroups,
|
|
|
|
|
view,
|
|
|
|
|
setView,
|
|
|
|
|
scrollToGroup,
|
|
|
|
|
]
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (!expandedId) return
|
|
|
|
|
const onKeyDown = (event: KeyboardEvent) => {
|
|
|
|
|
if (event.key === "Escape") setExpandedId(null)
|
|
|
|
|
}
|
|
|
|
|
window.addEventListener("keydown", onKeyDown)
|
|
|
|
|
return () => window.removeEventListener("keydown", onKeyDown)
|
|
|
|
|
}, [expandedId])
|
|
|
|
|
|
|
|
|
|
const expandedFindingCtx = useMemo<ExpandedFindingContextValue>(
|
|
|
|
|
() => ({
|
|
|
|
|
expandedId,
|
|
|
|
|
reviewUrl: detail.url,
|
|
|
|
|
toggle: toggleInline,
|
|
|
|
|
registerAnnotation,
|
|
|
|
|
}),
|
|
|
|
|
[expandedId, detail.url, toggleInline, registerAnnotation]
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<ExpandedFindingContext.Provider value={expandedFindingCtx}>
|
|
|
|
|
<div className="flex min-h-0 flex-1 overflow-hidden">
|
|
|
|
|
<main className="relative flex min-h-0 min-w-0 flex-1">
|
|
|
|
|
{!embedded && (
|
|
|
|
|
<div className="hidden w-72 shrink-0 flex-col border-r border-border bg-[var(--ui-sidebar)] lg:flex">
|
|
|
|
|
<ReviewSidebarPanel data={sidebarData} />
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
<div className="relative flex min-h-0 min-w-0 flex-1 flex-col">
|
|
|
|
|
{embedded && (
|
|
|
|
|
<div className="flex h-9 shrink-0 items-center justify-end border-b border-border px-3">
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={onExpand}
|
|
|
|
|
className="inline-flex items-center gap-1.5 rounded-md border border-border px-2 py-1 text-[11px] text-muted-foreground transition-colors hover:text-foreground"
|
|
|
|
|
>
|
|
|
|
|
<ArrowSquareOutIcon className="size-3" />
|
|
|
|
|
Open full review
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
<WorkerPoolContextProvider
|
|
|
|
|
poolOptions={DIFF_WORKER_POOL_OPTIONS}
|
|
|
|
|
highlighterOptions={DIFF_WORKER_HIGHLIGHTER_OPTIONS}
|
|
|
|
|
>
|
|
|
|
|
<Virtualizer
|
|
|
|
|
className="relative min-h-0 flex-1 overflow-y-auto"
|
chore: sync upstream/main, defer #1621 modular webhooks (#81)
* chore: bake sfw binary into sandbox image (#1611)
sfw only ships a launcher that fetches its real binary at first run and does a
daily update check against api.github.com/repos/SocketDev/sfw-free. Both fail in
the sandbox (restricted egress; the proxy injects the GitHub App installation
token, which lacks access to that repo), so `sfw yarn install` errors with
"could not fetch its binary". Pin sfw 2.0.6, warm + verify the binary cache at
build, and set SFW_SKIP_UPDATE_CHECK=1 so runs use the baked binary offline.
* feat: editable plan mode + fix review-plan banner overlap (#1610)
* feat: editable plan mode + fix review-plan banner overlap
Lets the thread owner edit the plan markdown by hand from the plan-review
page (Edit -> textarea -> Save) via a new PUT /dashboard/api/plan/{id}
endpoint that re-publishes the plan and mirrors it into the sandbox
plan.md, so approve hands the edited plan to the agent as the source of
truth. Also fixes the collapsed git-panel's floating expand button
covering the "Review plan ->" banner by reserving space for it.
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* fix: abort plan approval when the published plan read fails
get_plan_content() swallowed store errors and returned None, so a
transient failure during approve would still mark the plan approved and
dispatch the generic fallback text — silently dropping an owner's edited
plan. Read the plan strictly (raise_on_error=True) so approval aborts
instead, matching the comment read.
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
---------
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* feat: show message timestamps (#1609)
* feat: show message timestamps
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* fix: suppress fallback message timestamps
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* feat: stable message + tool-call hover timestamps
Stamp a stable client-side arrival time per message and tool call (keyed
by id, persisted to localStorage). Messages render the timestamp inline;
tool rows reveal a dim timestamp chip on hover. Real backend created_at
still takes precedence when present.
* fix: hide client-stamped message timestamps
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
---------
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* feat: add PR trace resolution (#1612)
* feat: add PR trace resolution
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* fix: inject reviewer trace context as JSON
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* fix: address review on PR trace resolution
Use the documented LangSmith metadata filter syntax
(and(eq(metadata_key,...), eq(metadata_value,...))) instead of
has(metadata, '{...}'), which does not match runs — _list_thread_runs
was silently returning nothing. Bound full-text searches to a 90-day
window so they don't hit LangSmith's large-window rate limit.
Also folds in the best-effort branch->head-sha resolver (dropping the
weighted scoring/threshold + repo/file evidence + GitHub hydration),
sandbox JSON injection, and the admin "Resolve trace" dry-run endpoint.
The IDOR findings are moot: resolve_pr_to_threads/summarize_agent_session
were removed; resolution now runs deterministically from the trusted run
config with no model-controlled pr_url or thread_id.
* fix: scope branch trace search to the repo
Branch names like fix-tests aren't unique across repos (or older PRs) in
a shared tracing project, so an unscoped branch hit could resolve to an
unrelated thread and write its runs into the reviewer sandbox. Require
the repo slug to co-occur with the branch in matched runs; the full head
SHA stays unscoped since it is globally unique. Addresses open-swe review
on PR #1612.
---------
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* feat: include plan links in PR descriptions (#1613)
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* feat: gate workflow pushes with approval (#1614)
* feat: gate workflow pushes with approval
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* fix: preserve proxy refresh test compatibility
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* fix: bind workflow approvals to pushed ref
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
---------
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* feat: recover thread work as patch (#1615)
* feat: recover thread work as patch
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* fix: search sandbox cwd for recovery patches
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
---------
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* fix: omit plan link in PR description when no plan exists (#1618)
Plan links in PR descriptions were always built from the thread id, so
runs that never produced a plan linked to an empty plan-review page.
Now the plan content store is consulted first; the link is only added
when a plan with non-empty markdown actually exists. A transient store
failure degrades gracefully (no link) rather than blocking PR creation.
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* feat: add filter & grouping menu to agents threads sidebar (#1617)
Add a Cursor-style control to the agents sidebar that groups (None/Date/
Status/Project), filters (ownership, status, source, pull request, model,
repo, include-resolved), and compacts the threads list. All client-side over
already-fetched sidebar threads; preferences persist in localStorage.
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* chore: update langsmith sdk to 0.9.3 (#1616)
Co-authored-by: Ramon Nogueira <270434257+ramon-langchain@users.noreply.github.com>
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* feat: clickable shared PR header in git panel and reviews (#1620)
* feat: clickable shared PR header in git panel and reviews
Replace the standalone "View PR" button in the agent git panel with a
clickable PR title, matching the reviews view. Extract a shared PrHeader
component reused by both the git panel and the review main body.
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* refactor: drop PrHeader wrapper, use shared component directly
The review-side PrHeader was just a thin adapter mapping detail -> the
shared component's props. Inline it at the call site and use the shared
PrHeader directly so there's a single component.
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
---------
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* refactor: durable interrupt dispatch + completion webhook (#1621)
* wip(rebuild): core reliability spine
- remove PR-babysitting (ci_autofix + ci_monitor graph + webhook wiring)
- dispatch core: agent/dispatch.py with multitask_strategy=interrupt +
durability=sync + completion webhook; reroute all webhook + plan triggers;
drop the racy in-process lock + is_thread_active busy-check
- completion webhook: agent/completion.py + /webhooks/run-complete loopback
route for failure/timeout replies (idempotent)
Co-authored-by: open-swe[bot]
* feat(rebuild): async tools, reconcile, shared http timeouts, assembly tuning
Parallel batch on top of the reliability spine:
- async-ify all 24 tools (drop asyncio.run; requests->httpx); re-implement the
http_request/fetch_url SSRF + DNS-rebinding defense httpx-natively and harden
the IP check to 'not is_global' (+ IPv4-mapped unwrap)
- reconcile.py: stale pending-run sweep (threads.search -> per-thread runs.list
-> cancel_many), wired into the scheduler graph via task='reconcile'
- shared DEFAULT_HTTP_TIMEOUT (agent/utils/http.py) on every bare
httpx.AsyncClient() across utils/dashboard/webapp/middleware
- run budget: MODEL_CALL_RECURSION_LIMIT 5000->250
- fix stale OpenAI->Anthropic fallback id (claude-opus-4-5 -> 4-8)
- drop redundant custom repair middleware (deepagents auto-adds PatchToolCalls)
- confirm tool-result eviction + summarization auto-wired via backend
- slim system prompt ~8% (full harness-profile rewrite deferred)
Co-authored-by: open-swe[bot]
* feat(rebuild): harness-profile prompt + split webhooks out of webapp
- prompt.py: own the system prompt via a registered harness profile
(OPEN_SWE_SHARED_BASE, kept neutral so the read-only reviewer/analyzer that
share it stay safe), registered across all 4 providers; per-thread values
stay in construct_system_prompt. Assembled main-agent prompt ~6.8k -> ~3.1k
tokens (~55% smaller); de-duped PR/commit/suite/force-push guidance; dropped
ALL-CAPS markers.
- webapp.py 3325 -> 1890 LOC: moved 14 per-source handlers into
agent/webhooks/{linear,slack,github}.py; webapp re-exports them for the
routes + tests; moved handlers reach shared helpers via the webapp namespace
to preserve the test suite's monkeypatch targets.
Full suite: 1168 passing, lint clean.
Co-authored-by: open-swe[bot]
* Restore MODEL_CALL_RECURSION_LIMIT to 5000 for long-running tasks
Reverts the 250 cap from the run-budget change — long-running tasks legitimately
need many model calls. The notify_step_limit_reached safety net still fires if a
run does hit the cap, so runs end with a signal either way.
Co-authored-by: open-swe[bot]
* fix: address PR review (auth, SSRF, interrupted status, redirect headers)
- completion.py: drop `interrupted` from failure statuses — with
multitask_strategy=interrupt a follow-up ends the prior run as interrupted,
which is healthy, not a failure to report. [open-swe]
- /webhooks/run-complete: shared-secret auth — dispatch appends ?token= when
RUN_COMPLETE_WEBHOOK_SECRET is set; route verifies via hmac.compare_digest.
[corridor-security]
- SSRF: extract the URL validator to agent/utils/url_safety.py and apply it
before server-side image fetches in multimodal.fetch_image_block.
[corridor-security]
- http_request: preserve caller headers/extensions across redirect hops instead
of dropping them on the first hop. [open-swe]
Co-authored-by: open-swe[bot]
* chore: remove REBUILD_PLAN.md (planning doc, not needed in the repo)
Co-authored-by: open-swe[bot]
* fix: fail closed on run-complete webhook auth when secret unset
Corridor follow-up: verify_run_complete_token returns False (not True) when
RUN_COMPLETE_WEBHOOK_SECRET is unset, so the public route is never
unauthenticated. Logs a startup warning when the secret is absent, and dispatch
skips registering the webhook when there's no secret (no rejected callbacks).
Co-authored-by: open-swe[bot]
---------
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* feat: restore forced tool call to prevent premature run stops (#1622)
Restore the ensure_no_empty_msg middleware and the always-call-a-tool system-prompt instruction that #1535 removed. When the model emits a message with no tool call (and hasn't already messaged the user or confirmed completion), the middleware re-injects a no_op / confirming_completion tool call so the run continues instead of ending mid-task.
Shipping to test whether it fixes runs that stop halfway through.
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* chore(deps): bump langgraph-checkpoint from 4.1.0 to 4.1.1 (#1619)
Bumps [langgraph-checkpoint](https://github.com/langchain-ai/langgraph) from 4.1.0 to 4.1.1.
- [Release notes](https://github.com/langchain-ai/langgraph/releases)
- [Commits](https://github.com/langchain-ai/langgraph/compare/checkpoint==4.1.0...checkpoint==4.1.1)
---
updated-dependencies:
- dependency-name: langgraph-checkpoint
dependency-version: 4.1.1
dependency-type: indirect
...
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
* fix: post reviewer resolution notes verbatim (#1624)
* fix: post reviewer resolution notes verbatim
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* fix: stabilize dashboard follow-up e2e
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* fix: preserve dashboard attribution in e2e
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* fix: make e2e attribution marker durable
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* fix: only echo found e2e attribution
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* fix: check live dashboard attribution in e2e
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
---------
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* hotfix: stop prompting agent/reviewer to wrap installs in sfw (#1625)
Installs hung when prefixed with sfw inside the sandbox (trace 019f0608
stalled on a pending `sfw npm install` execute, never returned). Strip the
Socket Firewall guidance from the agent and reviewer prompts so installs run
through the project's package manager directly. sfw stays in the Docker image;
nothing invokes it now.
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* fix: make plan view mobile friendly (#1636)
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* fix: fall back to vision model for image threads (#1626)
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* fix: surface Slack thread errors (#1627)
* fix: surface Slack thread errors
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* fix: don't set failure_reply_posted on Slack preprocessing errors
The preprocessing error handler was setting failure_reply_posted=True,
the same idempotency flag handle_run_completion checks to suppress
duplicate run-failure replies. Since preprocessing failures happen
before any run exists but the flag persists on the thread, a subsequent
run failure on the same thread would be silently ignored.
The preprocessing handler already posts its own Slack reply, so the
run-completion idempotency flag should not be set here.
---------
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* chore: avoid recapping Slack replies (#1629)
* chore: avoid recapping Slack replies
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* chore: simplify Slack reply prompt wording
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
---------
Co-authored-by: Ramon Nogueira <270434257+ramon-langchain@users.noreply.github.com>
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* fix: update Slack trace reply on web handoff (#1630)
* fix: update Slack trace reply on web handoff
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* fix: trigger web handoff on dashboard starts
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* fix: format web handoff as contextual fragment
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* fix: preserve trace_message_ts when overwriting Slack run mapping
When store_slack_run_mapping is called without trace_message_ts (e.g. on
follow-up Slack mentions), it was unconditionally overwriting the
thread-level mapping and clobbering the timestamp captured from the
initial trace reply. After that, _notify_slack_web_handoff could not find
the original message, so a subsequent move to Web silently skipped the
Slack trace update.
Now, when trace_message_ts is not passed, the existing thread mapping is
read first and its trace_message_ts is preserved.
* style: ruff format
---------
Co-authored-by: Ramon Nogueira <270434257+ramon-langchain@users.noreply.github.com>
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
Co-authored-by: Johannes du Plessis <51395795+johannes117@users.noreply.github.com>
Co-authored-by: Johannes du Plessis <johannes@langchain.dev>
* fix: pre-bundle shiki/@pierre deps to stop dev dynamic-import failures (#1643)
* fix(ui): pre-bundle shiki/@pierre deps to stop dev dynamic-import failures
shiki lazy-imports a grammar per language and these libs only live inside
lazy route components, so Vite's startup scanner never sees them. They get
discovered on first thread navigation, triggering a dep re-optimize +
force-reload that aborts the in-flight route-chunk import, surfacing as
"Failed to fetch dynamically imported module: .../$threadId.tsx".
Pre-bundle them (and the github themes + common code-block languages) via
optimizeDeps.include so the optimize happens once at startup. Dev-only;
production bundles are unaffected.
* fix: pre-bundle canonical shiki docker/make langs instead of aliases
---------
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* feat: show queued dashboard follow-ups (#1631)
* feat: show queued dashboard follow-ups
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* fix: de-dupe queued follow-ups while streaming
---------
Co-authored-by: Ramon Nogueira <270434257+ramon-langchain@users.noreply.github.com>
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
Co-authored-by: Johannes du Plessis <johannes@langchain.dev>
* feat: notify Slack on plan approval (#1632)
* feat: notify Slack on plan approval
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* fix: post Slack approval notice after successful dispatch
Move the _maybe_post_plan_approved_to_slack call until after
_dispatch_followup succeeds so the Slack thread is not told
implementation is beginning before the LangGraph run is created.
Addresses PR review comment.
---------
Co-authored-by: Ramon Nogueira <270434257+ramon-langchain@users.noreply.github.com>
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
Co-authored-by: Johannes du Plessis <johannes@langchain.dev>
* feat: include Slack channel context in prompts (#1633)
Add cached Slack channel metadata enrichment for Slack-triggered runs so prompts can include channel names and descriptions without duplicate conversations.info calls.\n\nCo-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
Co-authored-by: Ramon Nogueira <270434257+ramon-langchain@users.noreply.github.com>
* chore: keep plan guidance high-level (#1634)
Co-authored-by: Ramon Nogueira <270434257+ramon-langchain@users.noreply.github.com>
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* feat: publish plans from sandbox files (#1635)
* feat: publish plans from sandbox files
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* fix: avoid fixed plan filenames
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* fix: virtualize local sandbox file paths
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* fix: preserve plan_file_path across set_plan_status
set_plan_status was rewriting the content record with only markdown
and status, dropping plan_file_path. After a reject, the owner's
dashboard edit would mirror to a different file than the agent's
original, and the next save_plan could republish the stale file.
Preserve plan_file_path when updating status.
---------
Co-authored-by: Ramon Nogueira <270434257+ramon-langchain@users.noreply.github.com>
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* fix: return to thread after plan approval (#1637)
Co-authored-by: Ramon Nogueira <270434257+ramon-langchain@users.noreply.github.com>
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* feat: add Slack breakout thread tool (#1638)
* feat: add Slack breakout thread tool
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* chore: make fake LLM scripts declarative
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* fix: exclude slack_start_new_thread from plan mode
The breakout tool can dispatch a fresh agent run that starts outside the
current plan-mode state, bypassing the approval flow. Add it to
PLAN_MODE_EXCLUDED_TOOLS so it's hidden alongside the other mutating
tools while planning.
---------
Co-authored-by: Ramon Nogueira <270434257+ramon-langchain@users.noreply.github.com>
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* chore: require bun for ui agent work (#1639)
Co-authored-by: Ramon Nogueira <270434257+ramon-langchain@users.noreply.github.com>
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* fix: request actions read for sandbox logs (#1642)
* fix: request actions read for sandbox logs
Request optional Actions read permission for sandbox proxy tokens, with fallback for installations that have not approved it yet. Update setup docs and prompt guidance for safe GitHub Actions log usage.
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* fix: restore actions:read scope after workflow push
After an approved workflow push, the guard was restoring the proxy with
BASE_RUNTIME_PROXY_TOKEN_PERMISSIONS, which excludes the actions: read
scope this PR adds. Restore with RUNTIME_PROXY_TOKEN_PERMISSIONS (which
includes actions: read) and fall back to BASE if the install hasn't
granted Actions read — mirroring the pattern in _create_sandbox_with_proxy.
Addresses review comment on PR #1642.
---------
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* fix: widen split review diffs (#1647)
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* chore: install missing deps before verification (#1646)
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* chore: switch ui to pnpm (#1645)
* chore: require pnpm for ui agent work
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* chore: switch ui to pnpm
Replace Bun and Yarn lockfiles with pnpm lockfile and update UI/Vercel commands to use pnpm.
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* ci: use corepack for ui pnpm e2e build
Run pnpm through Corepack in the E2E global setup so CI can use the pinned package manager without a separate pnpm install step.
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
---------
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* feat: add Sonnet 5 to model picker (#1651)
* chore: update Sonnet examples to Sonnet 5
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* chore: add Sonnet 5 to model picker
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
---------
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* Remove dead breakout-thread e2e scenario after dropping the tool
The merge resolution deferred upstream's Slack breakout-thread tool
(slack_start_new_thread, #1638) since it depends on the #1621 dispatch
module, but the e2e harness still scripted it. Removing the tool name
from fake_llm.py's _tool_step call left a malformed scenario, crashing
the langgraph-dev web server at import (TypeError: _tool_step() missing
'call_id') and failing Playwright E2E.
Drop the "breakout" script scenario, its _is_breakout_request helper +
ScriptRule, and the corresponding full_flow.spec.ts test.
* Revert upstream pnpm switch; keep bun for the UI build
The merge auto-adopted upstream's pnpm switch (#1645) in tests/e2e/
global-setup.ts and ui/package.json, but our fork builds the UI with
bun (vercel.json + the E2E workflow's setup-bun). That left the
Playwright globalSetup running `corepack pnpm install --frozen-lockfile`
with no pnpm-lock.yaml, failing E2E at UI build time.
Revert global-setup.ts and ui/package.json to the dev (bun) baseline,
drop the merge-added ui/pnpm-lock.yaml, and remove the re-added
ui/AGENTS.md (our fork had deleted it).
* Align plan-review e2e + UI with the HEAD (pre-#1635) backend
The merge left a split plan vertical: the backend save_plan/plan_api are
HEAD (we deferred the editable-plan/sandbox-publish features #1610/#1635/
#1637 per #80), but the plan UI and e2e harness were upstream's. The
fake_llm scenario called save_plan(plan_file_path=...) — upstream's
file-based #1635 contract — while HEAD save_plan takes plan_markdown,
so the plan never saved and PlanReview never rendered (E2E failure on
the plan-review locator).
Pass plan_markdown to save_plan, and revert PlanReview.tsx / plan.ts /
$threadId_.plan.tsx / plan_review.spec.ts to the dev baseline so the
whole plan flow (save -> render -> approve -> implement) is consistent
with the HEAD backend.
---------
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: Johannes du Plessis <johannes@langchain.dev>
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
Co-authored-by: Ramon Nogueira <ramon.nogueira@langchain.dev>
Co-authored-by: Ramon Nogueira <270434257+ramon-langchain@users.noreply.github.com>
Co-authored-by: Caroline di Vittorio <43390382+carolinedivittorio@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Johannes du Plessis <51395795+johannes117@users.noreply.github.com>
Co-authored-by: Ankush Gola <9536492+agola11@users.noreply.github.com>
Co-authored-by: Mukil Loganathan <mukil@langchain.dev>
2026-06-30 16:45:19 -04:00
|
|
|
contentClassName={cn(
|
|
|
|
|
"mx-auto w-full px-6 py-6",
|
|
|
|
|
diffStyle === "split" ? "max-w-none" : "max-w-6xl"
|
|
|
|
|
)}
|
2026-06-23 12:27:51 -07:00
|
|
|
config={DIFF_VIRTUALIZER_CONFIG}
|
|
|
|
|
>
|
|
|
|
|
<div ref={scrollerProbe} aria-hidden className="hidden" />
|
chore: sync upstream/main, defer #1621 modular webhooks (#81)
* chore: bake sfw binary into sandbox image (#1611)
sfw only ships a launcher that fetches its real binary at first run and does a
daily update check against api.github.com/repos/SocketDev/sfw-free. Both fail in
the sandbox (restricted egress; the proxy injects the GitHub App installation
token, which lacks access to that repo), so `sfw yarn install` errors with
"could not fetch its binary". Pin sfw 2.0.6, warm + verify the binary cache at
build, and set SFW_SKIP_UPDATE_CHECK=1 so runs use the baked binary offline.
* feat: editable plan mode + fix review-plan banner overlap (#1610)
* feat: editable plan mode + fix review-plan banner overlap
Lets the thread owner edit the plan markdown by hand from the plan-review
page (Edit -> textarea -> Save) via a new PUT /dashboard/api/plan/{id}
endpoint that re-publishes the plan and mirrors it into the sandbox
plan.md, so approve hands the edited plan to the agent as the source of
truth. Also fixes the collapsed git-panel's floating expand button
covering the "Review plan ->" banner by reserving space for it.
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* fix: abort plan approval when the published plan read fails
get_plan_content() swallowed store errors and returned None, so a
transient failure during approve would still mark the plan approved and
dispatch the generic fallback text — silently dropping an owner's edited
plan. Read the plan strictly (raise_on_error=True) so approval aborts
instead, matching the comment read.
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
---------
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* feat: show message timestamps (#1609)
* feat: show message timestamps
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* fix: suppress fallback message timestamps
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* feat: stable message + tool-call hover timestamps
Stamp a stable client-side arrival time per message and tool call (keyed
by id, persisted to localStorage). Messages render the timestamp inline;
tool rows reveal a dim timestamp chip on hover. Real backend created_at
still takes precedence when present.
* fix: hide client-stamped message timestamps
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
---------
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* feat: add PR trace resolution (#1612)
* feat: add PR trace resolution
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* fix: inject reviewer trace context as JSON
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* fix: address review on PR trace resolution
Use the documented LangSmith metadata filter syntax
(and(eq(metadata_key,...), eq(metadata_value,...))) instead of
has(metadata, '{...}'), which does not match runs — _list_thread_runs
was silently returning nothing. Bound full-text searches to a 90-day
window so they don't hit LangSmith's large-window rate limit.
Also folds in the best-effort branch->head-sha resolver (dropping the
weighted scoring/threshold + repo/file evidence + GitHub hydration),
sandbox JSON injection, and the admin "Resolve trace" dry-run endpoint.
The IDOR findings are moot: resolve_pr_to_threads/summarize_agent_session
were removed; resolution now runs deterministically from the trusted run
config with no model-controlled pr_url or thread_id.
* fix: scope branch trace search to the repo
Branch names like fix-tests aren't unique across repos (or older PRs) in
a shared tracing project, so an unscoped branch hit could resolve to an
unrelated thread and write its runs into the reviewer sandbox. Require
the repo slug to co-occur with the branch in matched runs; the full head
SHA stays unscoped since it is globally unique. Addresses open-swe review
on PR #1612.
---------
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* feat: include plan links in PR descriptions (#1613)
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* feat: gate workflow pushes with approval (#1614)
* feat: gate workflow pushes with approval
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* fix: preserve proxy refresh test compatibility
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* fix: bind workflow approvals to pushed ref
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
---------
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* feat: recover thread work as patch (#1615)
* feat: recover thread work as patch
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* fix: search sandbox cwd for recovery patches
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
---------
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* fix: omit plan link in PR description when no plan exists (#1618)
Plan links in PR descriptions were always built from the thread id, so
runs that never produced a plan linked to an empty plan-review page.
Now the plan content store is consulted first; the link is only added
when a plan with non-empty markdown actually exists. A transient store
failure degrades gracefully (no link) rather than blocking PR creation.
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* feat: add filter & grouping menu to agents threads sidebar (#1617)
Add a Cursor-style control to the agents sidebar that groups (None/Date/
Status/Project), filters (ownership, status, source, pull request, model,
repo, include-resolved), and compacts the threads list. All client-side over
already-fetched sidebar threads; preferences persist in localStorage.
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* chore: update langsmith sdk to 0.9.3 (#1616)
Co-authored-by: Ramon Nogueira <270434257+ramon-langchain@users.noreply.github.com>
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* feat: clickable shared PR header in git panel and reviews (#1620)
* feat: clickable shared PR header in git panel and reviews
Replace the standalone "View PR" button in the agent git panel with a
clickable PR title, matching the reviews view. Extract a shared PrHeader
component reused by both the git panel and the review main body.
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* refactor: drop PrHeader wrapper, use shared component directly
The review-side PrHeader was just a thin adapter mapping detail -> the
shared component's props. Inline it at the call site and use the shared
PrHeader directly so there's a single component.
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
---------
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* refactor: durable interrupt dispatch + completion webhook (#1621)
* wip(rebuild): core reliability spine
- remove PR-babysitting (ci_autofix + ci_monitor graph + webhook wiring)
- dispatch core: agent/dispatch.py with multitask_strategy=interrupt +
durability=sync + completion webhook; reroute all webhook + plan triggers;
drop the racy in-process lock + is_thread_active busy-check
- completion webhook: agent/completion.py + /webhooks/run-complete loopback
route for failure/timeout replies (idempotent)
Co-authored-by: open-swe[bot]
* feat(rebuild): async tools, reconcile, shared http timeouts, assembly tuning
Parallel batch on top of the reliability spine:
- async-ify all 24 tools (drop asyncio.run; requests->httpx); re-implement the
http_request/fetch_url SSRF + DNS-rebinding defense httpx-natively and harden
the IP check to 'not is_global' (+ IPv4-mapped unwrap)
- reconcile.py: stale pending-run sweep (threads.search -> per-thread runs.list
-> cancel_many), wired into the scheduler graph via task='reconcile'
- shared DEFAULT_HTTP_TIMEOUT (agent/utils/http.py) on every bare
httpx.AsyncClient() across utils/dashboard/webapp/middleware
- run budget: MODEL_CALL_RECURSION_LIMIT 5000->250
- fix stale OpenAI->Anthropic fallback id (claude-opus-4-5 -> 4-8)
- drop redundant custom repair middleware (deepagents auto-adds PatchToolCalls)
- confirm tool-result eviction + summarization auto-wired via backend
- slim system prompt ~8% (full harness-profile rewrite deferred)
Co-authored-by: open-swe[bot]
* feat(rebuild): harness-profile prompt + split webhooks out of webapp
- prompt.py: own the system prompt via a registered harness profile
(OPEN_SWE_SHARED_BASE, kept neutral so the read-only reviewer/analyzer that
share it stay safe), registered across all 4 providers; per-thread values
stay in construct_system_prompt. Assembled main-agent prompt ~6.8k -> ~3.1k
tokens (~55% smaller); de-duped PR/commit/suite/force-push guidance; dropped
ALL-CAPS markers.
- webapp.py 3325 -> 1890 LOC: moved 14 per-source handlers into
agent/webhooks/{linear,slack,github}.py; webapp re-exports them for the
routes + tests; moved handlers reach shared helpers via the webapp namespace
to preserve the test suite's monkeypatch targets.
Full suite: 1168 passing, lint clean.
Co-authored-by: open-swe[bot]
* Restore MODEL_CALL_RECURSION_LIMIT to 5000 for long-running tasks
Reverts the 250 cap from the run-budget change — long-running tasks legitimately
need many model calls. The notify_step_limit_reached safety net still fires if a
run does hit the cap, so runs end with a signal either way.
Co-authored-by: open-swe[bot]
* fix: address PR review (auth, SSRF, interrupted status, redirect headers)
- completion.py: drop `interrupted` from failure statuses — with
multitask_strategy=interrupt a follow-up ends the prior run as interrupted,
which is healthy, not a failure to report. [open-swe]
- /webhooks/run-complete: shared-secret auth — dispatch appends ?token= when
RUN_COMPLETE_WEBHOOK_SECRET is set; route verifies via hmac.compare_digest.
[corridor-security]
- SSRF: extract the URL validator to agent/utils/url_safety.py and apply it
before server-side image fetches in multimodal.fetch_image_block.
[corridor-security]
- http_request: preserve caller headers/extensions across redirect hops instead
of dropping them on the first hop. [open-swe]
Co-authored-by: open-swe[bot]
* chore: remove REBUILD_PLAN.md (planning doc, not needed in the repo)
Co-authored-by: open-swe[bot]
* fix: fail closed on run-complete webhook auth when secret unset
Corridor follow-up: verify_run_complete_token returns False (not True) when
RUN_COMPLETE_WEBHOOK_SECRET is unset, so the public route is never
unauthenticated. Logs a startup warning when the secret is absent, and dispatch
skips registering the webhook when there's no secret (no rejected callbacks).
Co-authored-by: open-swe[bot]
---------
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* feat: restore forced tool call to prevent premature run stops (#1622)
Restore the ensure_no_empty_msg middleware and the always-call-a-tool system-prompt instruction that #1535 removed. When the model emits a message with no tool call (and hasn't already messaged the user or confirmed completion), the middleware re-injects a no_op / confirming_completion tool call so the run continues instead of ending mid-task.
Shipping to test whether it fixes runs that stop halfway through.
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* chore(deps): bump langgraph-checkpoint from 4.1.0 to 4.1.1 (#1619)
Bumps [langgraph-checkpoint](https://github.com/langchain-ai/langgraph) from 4.1.0 to 4.1.1.
- [Release notes](https://github.com/langchain-ai/langgraph/releases)
- [Commits](https://github.com/langchain-ai/langgraph/compare/checkpoint==4.1.0...checkpoint==4.1.1)
---
updated-dependencies:
- dependency-name: langgraph-checkpoint
dependency-version: 4.1.1
dependency-type: indirect
...
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
* fix: post reviewer resolution notes verbatim (#1624)
* fix: post reviewer resolution notes verbatim
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* fix: stabilize dashboard follow-up e2e
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* fix: preserve dashboard attribution in e2e
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* fix: make e2e attribution marker durable
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* fix: only echo found e2e attribution
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* fix: check live dashboard attribution in e2e
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
---------
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* hotfix: stop prompting agent/reviewer to wrap installs in sfw (#1625)
Installs hung when prefixed with sfw inside the sandbox (trace 019f0608
stalled on a pending `sfw npm install` execute, never returned). Strip the
Socket Firewall guidance from the agent and reviewer prompts so installs run
through the project's package manager directly. sfw stays in the Docker image;
nothing invokes it now.
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* fix: make plan view mobile friendly (#1636)
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* fix: fall back to vision model for image threads (#1626)
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* fix: surface Slack thread errors (#1627)
* fix: surface Slack thread errors
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* fix: don't set failure_reply_posted on Slack preprocessing errors
The preprocessing error handler was setting failure_reply_posted=True,
the same idempotency flag handle_run_completion checks to suppress
duplicate run-failure replies. Since preprocessing failures happen
before any run exists but the flag persists on the thread, a subsequent
run failure on the same thread would be silently ignored.
The preprocessing handler already posts its own Slack reply, so the
run-completion idempotency flag should not be set here.
---------
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* chore: avoid recapping Slack replies (#1629)
* chore: avoid recapping Slack replies
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* chore: simplify Slack reply prompt wording
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
---------
Co-authored-by: Ramon Nogueira <270434257+ramon-langchain@users.noreply.github.com>
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* fix: update Slack trace reply on web handoff (#1630)
* fix: update Slack trace reply on web handoff
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* fix: trigger web handoff on dashboard starts
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* fix: format web handoff as contextual fragment
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* fix: preserve trace_message_ts when overwriting Slack run mapping
When store_slack_run_mapping is called without trace_message_ts (e.g. on
follow-up Slack mentions), it was unconditionally overwriting the
thread-level mapping and clobbering the timestamp captured from the
initial trace reply. After that, _notify_slack_web_handoff could not find
the original message, so a subsequent move to Web silently skipped the
Slack trace update.
Now, when trace_message_ts is not passed, the existing thread mapping is
read first and its trace_message_ts is preserved.
* style: ruff format
---------
Co-authored-by: Ramon Nogueira <270434257+ramon-langchain@users.noreply.github.com>
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
Co-authored-by: Johannes du Plessis <51395795+johannes117@users.noreply.github.com>
Co-authored-by: Johannes du Plessis <johannes@langchain.dev>
* fix: pre-bundle shiki/@pierre deps to stop dev dynamic-import failures (#1643)
* fix(ui): pre-bundle shiki/@pierre deps to stop dev dynamic-import failures
shiki lazy-imports a grammar per language and these libs only live inside
lazy route components, so Vite's startup scanner never sees them. They get
discovered on first thread navigation, triggering a dep re-optimize +
force-reload that aborts the in-flight route-chunk import, surfacing as
"Failed to fetch dynamically imported module: .../$threadId.tsx".
Pre-bundle them (and the github themes + common code-block languages) via
optimizeDeps.include so the optimize happens once at startup. Dev-only;
production bundles are unaffected.
* fix: pre-bundle canonical shiki docker/make langs instead of aliases
---------
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* feat: show queued dashboard follow-ups (#1631)
* feat: show queued dashboard follow-ups
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* fix: de-dupe queued follow-ups while streaming
---------
Co-authored-by: Ramon Nogueira <270434257+ramon-langchain@users.noreply.github.com>
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
Co-authored-by: Johannes du Plessis <johannes@langchain.dev>
* feat: notify Slack on plan approval (#1632)
* feat: notify Slack on plan approval
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* fix: post Slack approval notice after successful dispatch
Move the _maybe_post_plan_approved_to_slack call until after
_dispatch_followup succeeds so the Slack thread is not told
implementation is beginning before the LangGraph run is created.
Addresses PR review comment.
---------
Co-authored-by: Ramon Nogueira <270434257+ramon-langchain@users.noreply.github.com>
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
Co-authored-by: Johannes du Plessis <johannes@langchain.dev>
* feat: include Slack channel context in prompts (#1633)
Add cached Slack channel metadata enrichment for Slack-triggered runs so prompts can include channel names and descriptions without duplicate conversations.info calls.\n\nCo-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
Co-authored-by: Ramon Nogueira <270434257+ramon-langchain@users.noreply.github.com>
* chore: keep plan guidance high-level (#1634)
Co-authored-by: Ramon Nogueira <270434257+ramon-langchain@users.noreply.github.com>
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* feat: publish plans from sandbox files (#1635)
* feat: publish plans from sandbox files
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* fix: avoid fixed plan filenames
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* fix: virtualize local sandbox file paths
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* fix: preserve plan_file_path across set_plan_status
set_plan_status was rewriting the content record with only markdown
and status, dropping plan_file_path. After a reject, the owner's
dashboard edit would mirror to a different file than the agent's
original, and the next save_plan could republish the stale file.
Preserve plan_file_path when updating status.
---------
Co-authored-by: Ramon Nogueira <270434257+ramon-langchain@users.noreply.github.com>
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* fix: return to thread after plan approval (#1637)
Co-authored-by: Ramon Nogueira <270434257+ramon-langchain@users.noreply.github.com>
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* feat: add Slack breakout thread tool (#1638)
* feat: add Slack breakout thread tool
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* chore: make fake LLM scripts declarative
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* fix: exclude slack_start_new_thread from plan mode
The breakout tool can dispatch a fresh agent run that starts outside the
current plan-mode state, bypassing the approval flow. Add it to
PLAN_MODE_EXCLUDED_TOOLS so it's hidden alongside the other mutating
tools while planning.
---------
Co-authored-by: Ramon Nogueira <270434257+ramon-langchain@users.noreply.github.com>
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* chore: require bun for ui agent work (#1639)
Co-authored-by: Ramon Nogueira <270434257+ramon-langchain@users.noreply.github.com>
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* fix: request actions read for sandbox logs (#1642)
* fix: request actions read for sandbox logs
Request optional Actions read permission for sandbox proxy tokens, with fallback for installations that have not approved it yet. Update setup docs and prompt guidance for safe GitHub Actions log usage.
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* fix: restore actions:read scope after workflow push
After an approved workflow push, the guard was restoring the proxy with
BASE_RUNTIME_PROXY_TOKEN_PERMISSIONS, which excludes the actions: read
scope this PR adds. Restore with RUNTIME_PROXY_TOKEN_PERMISSIONS (which
includes actions: read) and fall back to BASE if the install hasn't
granted Actions read — mirroring the pattern in _create_sandbox_with_proxy.
Addresses review comment on PR #1642.
---------
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* fix: widen split review diffs (#1647)
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* chore: install missing deps before verification (#1646)
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* chore: switch ui to pnpm (#1645)
* chore: require pnpm for ui agent work
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* chore: switch ui to pnpm
Replace Bun and Yarn lockfiles with pnpm lockfile and update UI/Vercel commands to use pnpm.
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* ci: use corepack for ui pnpm e2e build
Run pnpm through Corepack in the E2E global setup so CI can use the pinned package manager without a separate pnpm install step.
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
---------
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* feat: add Sonnet 5 to model picker (#1651)
* chore: update Sonnet examples to Sonnet 5
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* chore: add Sonnet 5 to model picker
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
---------
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
* Remove dead breakout-thread e2e scenario after dropping the tool
The merge resolution deferred upstream's Slack breakout-thread tool
(slack_start_new_thread, #1638) since it depends on the #1621 dispatch
module, but the e2e harness still scripted it. Removing the tool name
from fake_llm.py's _tool_step call left a malformed scenario, crashing
the langgraph-dev web server at import (TypeError: _tool_step() missing
'call_id') and failing Playwright E2E.
Drop the "breakout" script scenario, its _is_breakout_request helper +
ScriptRule, and the corresponding full_flow.spec.ts test.
* Revert upstream pnpm switch; keep bun for the UI build
The merge auto-adopted upstream's pnpm switch (#1645) in tests/e2e/
global-setup.ts and ui/package.json, but our fork builds the UI with
bun (vercel.json + the E2E workflow's setup-bun). That left the
Playwright globalSetup running `corepack pnpm install --frozen-lockfile`
with no pnpm-lock.yaml, failing E2E at UI build time.
Revert global-setup.ts and ui/package.json to the dev (bun) baseline,
drop the merge-added ui/pnpm-lock.yaml, and remove the re-added
ui/AGENTS.md (our fork had deleted it).
* Align plan-review e2e + UI with the HEAD (pre-#1635) backend
The merge left a split plan vertical: the backend save_plan/plan_api are
HEAD (we deferred the editable-plan/sandbox-publish features #1610/#1635/
#1637 per #80), but the plan UI and e2e harness were upstream's. The
fake_llm scenario called save_plan(plan_file_path=...) — upstream's
file-based #1635 contract — while HEAD save_plan takes plan_markdown,
so the plan never saved and PlanReview never rendered (E2E failure on
the plan-review locator).
Pass plan_markdown to save_plan, and revert PlanReview.tsx / plan.ts /
$threadId_.plan.tsx / plan_review.spec.ts to the dev baseline so the
whole plan flow (save -> render -> approve -> implement) is consistent
with the HEAD backend.
---------
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: Johannes du Plessis <johannes@langchain.dev>
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
Co-authored-by: Ramon Nogueira <ramon.nogueira@langchain.dev>
Co-authored-by: Ramon Nogueira <270434257+ramon-langchain@users.noreply.github.com>
Co-authored-by: Caroline di Vittorio <43390382+carolinedivittorio@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Johannes du Plessis <51395795+johannes117@users.noreply.github.com>
Co-authored-by: Ankush Gola <9536492+agola11@users.noreply.github.com>
Co-authored-by: Mukil Loganathan <mukil@langchain.dev>
2026-06-30 16:45:19 -04:00
|
|
|
<PrHeader
|
|
|
|
|
url={detail.url}
|
|
|
|
|
title={detail.pr.title}
|
|
|
|
|
state={detail.pr.state}
|
|
|
|
|
headRef={detail.pr.head_ref}
|
|
|
|
|
baseRef={detail.pr.base_ref}
|
|
|
|
|
author={detail.pr.author?.login}
|
|
|
|
|
stats={{
|
|
|
|
|
changedFiles: detail.pr.changed_files,
|
|
|
|
|
additions: detail.pr.additions,
|
|
|
|
|
deletions: detail.pr.deletions,
|
|
|
|
|
}}
|
|
|
|
|
/>
|
2026-06-23 12:27:51 -07:00
|
|
|
<div className="mt-4 rounded-lg border border-border bg-card p-4">
|
|
|
|
|
{detail.pr.body ? (
|
|
|
|
|
<Markdown
|
|
|
|
|
content={detail.pr.body}
|
|
|
|
|
transformImageUrl={transformPrImage}
|
|
|
|
|
/>
|
|
|
|
|
) : (
|
|
|
|
|
<p className="text-xs text-muted-foreground">
|
|
|
|
|
This PR has no description.
|
|
|
|
|
</p>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="mt-6">
|
|
|
|
|
<div className="mb-2 flex items-center justify-between gap-3">
|
|
|
|
|
<h2 className="text-sm font-medium">Changes</h2>
|
|
|
|
|
<div className="flex items-center gap-3">
|
|
|
|
|
{linesLeft !== null && (
|
|
|
|
|
<span className="text-xs text-muted-foreground">
|
|
|
|
|
{linesLeft === 0
|
|
|
|
|
? "All lines reviewed"
|
|
|
|
|
: `${linesLeft} lines left`}
|
|
|
|
|
</span>
|
|
|
|
|
)}
|
|
|
|
|
{diffFiles && diffFiles.length > 0 && (
|
|
|
|
|
<DiffStyleToggle
|
|
|
|
|
value={diffStyle}
|
|
|
|
|
onChange={setDiffStyle}
|
|
|
|
|
/>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
{!diffFiles ? (
|
|
|
|
|
<Skeleton className="h-64 w-full" />
|
|
|
|
|
) : diffFiles.length === 0 ? (
|
|
|
|
|
<p className="text-xs text-muted-foreground">
|
|
|
|
|
No diff available.
|
|
|
|
|
</p>
|
|
|
|
|
) : view === "ai" && groupedView ? (
|
|
|
|
|
<div className="space-y-6">
|
|
|
|
|
{groupedView.map((group) => (
|
|
|
|
|
<div
|
|
|
|
|
key={group.index}
|
|
|
|
|
ref={(node) => {
|
|
|
|
|
groupRefs.current[group.index] = node
|
|
|
|
|
}}
|
|
|
|
|
className="scroll-mt-4 space-y-3"
|
|
|
|
|
>
|
|
|
|
|
<GroupHeader group={group} />
|
|
|
|
|
{group.files.map(renderFileCard)}
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
) : (
|
|
|
|
|
<div className="space-y-3">
|
|
|
|
|
{diffFiles.map(renderFileCard)}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</Virtualizer>
|
|
|
|
|
</WorkerPoolContextProvider>
|
|
|
|
|
</div>
|
|
|
|
|
</main>
|
|
|
|
|
|
|
|
|
|
{!embedded && (
|
|
|
|
|
<SidePanel
|
|
|
|
|
detail={detail}
|
|
|
|
|
tab={sideTab}
|
|
|
|
|
onTabChange={setSideTab}
|
|
|
|
|
read={read}
|
|
|
|
|
expandedId={expandedId}
|
|
|
|
|
onMarkAllRead={markAllRead}
|
|
|
|
|
onFindingClick={openFromPanel}
|
|
|
|
|
/>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</ExpandedFindingContext.Provider>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function DiffStyleToggle({
|
|
|
|
|
value,
|
|
|
|
|
onChange,
|
|
|
|
|
}: {
|
|
|
|
|
value: DiffStyle
|
|
|
|
|
onChange: (value: DiffStyle) => void
|
|
|
|
|
}) {
|
|
|
|
|
return (
|
|
|
|
|
<div className="flex items-center gap-0.5 rounded-md border border-border p-0.5">
|
|
|
|
|
<DiffStyleButton
|
|
|
|
|
active={value === "unified"}
|
|
|
|
|
label="Unified view"
|
|
|
|
|
onClick={() => onChange("unified")}
|
|
|
|
|
>
|
|
|
|
|
<RowsIcon className="size-3.5" />
|
|
|
|
|
</DiffStyleButton>
|
|
|
|
|
<DiffStyleButton
|
|
|
|
|
active={value === "split"}
|
|
|
|
|
label="Split view"
|
|
|
|
|
onClick={() => onChange("split")}
|
|
|
|
|
>
|
|
|
|
|
<SquareSplitHorizontalIcon className="size-3.5" />
|
|
|
|
|
</DiffStyleButton>
|
|
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function DiffStyleButton({
|
|
|
|
|
active,
|
|
|
|
|
label,
|
|
|
|
|
onClick,
|
|
|
|
|
children,
|
|
|
|
|
}: {
|
|
|
|
|
active: boolean
|
|
|
|
|
label: string
|
|
|
|
|
onClick: () => void
|
|
|
|
|
children: React.ReactNode
|
|
|
|
|
}) {
|
|
|
|
|
return (
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={onClick}
|
|
|
|
|
aria-label={label}
|
|
|
|
|
aria-pressed={active}
|
|
|
|
|
title={label}
|
|
|
|
|
className={cn(
|
|
|
|
|
"flex size-5 items-center justify-center rounded text-muted-foreground transition-colors",
|
|
|
|
|
active ? "bg-muted text-foreground" : "hover:text-foreground"
|
|
|
|
|
)}
|
|
|
|
|
>
|
|
|
|
|
{children}
|
|
|
|
|
</button>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function GroupHeader({ group }: { group: ResolvedGroup }) {
|
|
|
|
|
return (
|
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
<span className="flex size-5 shrink-0 items-center justify-center rounded bg-[var(--ui-panel-2)] text-[11px] font-medium text-muted-foreground">
|
|
|
|
|
{group.index}
|
|
|
|
|
</span>
|
|
|
|
|
<h3 className="min-w-0 truncate text-sm font-medium">{group.title}</h3>
|
|
|
|
|
<span className="flex shrink-0 items-center gap-1.5 font-mono text-[11px]">
|
|
|
|
|
{group.additions > 0 && (
|
|
|
|
|
<span className="text-emerald-500">+{group.additions}</span>
|
|
|
|
|
)}
|
|
|
|
|
{group.deletions > 0 && (
|
|
|
|
|
<span className="text-red-500">-{group.deletions}</span>
|
|
|
|
|
)}
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const FileDiffCard = memo(function FileDiffCard({
|
|
|
|
|
file,
|
|
|
|
|
findings,
|
|
|
|
|
selectedLines,
|
|
|
|
|
viewed,
|
|
|
|
|
onToggleViewed,
|
|
|
|
|
expanded,
|
|
|
|
|
onToggleExpanded,
|
|
|
|
|
onSelectLines,
|
|
|
|
|
onAddToChat,
|
|
|
|
|
registerSection,
|
|
|
|
|
registerDiffInstance,
|
|
|
|
|
diffStyle,
|
feat: inline PR comments in the reviews UI (#1600)
* feat: inline PR comments in the reviews UI
Click the diff gutter "+" on a line to open an inline comment composer
(rendered like the finding card via a Pierre annotation); submitting
posts a real inline PR review comment as the signed-in user through a
new POST /reviews/{owner}/{repo}/{number}/comments. The "+" press-drag →
"Add to Chat" selection path is unchanged.
* feat: GitHub-parity comment box, PR comments dropdown, collapse nav
- Comment composer now mirrors GitHub's box: Write/Preview tabs (markdown
rendered via the existing Markdown component) and a markdown toolbar
(heading, bold, italic, quote, code, link, bulleted/numbered/task list).
- Surface other people's inline PR comments in a Devin-style dropdown in the
review header (search + link to the thread on GitHub). New
GET /reviews/{owner}/{repo}/{number}/comments lists them and flags the
reviewer's own (marker-bearing) comments so they're filtered out.
- Collapse the global nav by default on a review detail page, restoring the
prior preference on leave.
* feat: bigger comment-toolbar icons; open dropdown comments inline
- Enlarge the markdown toolbar glyphs (Phosphor) in the comment composer —
they were rendering at 10px.
- Clicking a comment in the PR comments dropdown now opens it inline in the
diff as a read-only finding-style card (InlineComment), scrolling its line
into view, instead of navigating to GitHub. Falls back to GitHub when the
comment's file/line isn't in the current diff.
* fix: drive "Add to Chat" from native text selection
The gutter "+" is now comment-only; wiring its click to the composer
conflicted with its old double-duty as the drag-to-select handle, which
broke selection → "Add to Chat". Switch to Devin's model: disable Pierre's
interactive line selection and instead map a native text highlight in the
diff to a line range (via the data-line / data-line-type attributes Pierre
stamps on each line, read from the diff's open shadow root) to show the
"Add to Chat" popup. ⌘L and the existing attachment/popup path are unchanged.
* feat: gutter "+" drag selects a range for multi-line comments
Re-enable Pierre's gutter line selection so dragging the "+" down the
gutter comments across a range (click still comments on a single line);
onLineSelectionEnd routes the range to the composer. Native code-text
selection still drives "Add to Chat" — Pierre only line-selects from the
gutter, and onLineSelectionEnd bails when a native text selection is
present, so a code highlight never opens the composer.
* fix: keep the range highlighted while its comment composer is open
Previously opening the composer cleared the selection, so the lines being
commented on lost their highlight. Drive the controlled selection from the
open comment draft's range so the rows stay highlighted until the composer
is closed.
* fix: address PR review — paginate comments, fall back for outdated ones
- list_review_comments now pages through all PR review comments (bounded by
_MAX_REVIEW_COMMENT_PAGES) instead of returning only the first 100, so older
comments still show in the dropdown.
- Surface GitHub's outdated flag (position == null) as is_outdated; opening such
a comment (or one whose line isn't in the diff) now opens it on GitHub instead
of silently rendering nothing, plus a timeout fallback if the annotation never
mounts (e.g. collapsed context).
2026-06-23 16:01:46 -07:00
|
|
|
owner,
|
|
|
|
|
repo,
|
|
|
|
|
prNumber,
|
|
|
|
|
commentDraftRange,
|
|
|
|
|
onStartComment,
|
|
|
|
|
onCloseComment,
|
|
|
|
|
openComment,
|
|
|
|
|
onCloseOpenComment,
|
2026-06-23 12:27:51 -07:00
|
|
|
}: {
|
|
|
|
|
file: ReviewDiffFile
|
|
|
|
|
findings: Array<ReviewFinding>
|
|
|
|
|
selectedLines: SelectedLineRange | null
|
|
|
|
|
viewed: boolean
|
|
|
|
|
onToggleViewed: (path: string) => void
|
|
|
|
|
expanded: boolean
|
|
|
|
|
onToggleExpanded: (path: string) => void
|
|
|
|
|
onSelectLines: (path: string, range: SelectedLineRange | null) => void
|
|
|
|
|
onAddToChat?: (path: string, range: SelectedLineRange) => void
|
|
|
|
|
registerSection: (path: string, node: HTMLDivElement | null) => void
|
|
|
|
|
registerDiffInstance: (
|
|
|
|
|
path: string,
|
|
|
|
|
target: RegisteredDiffInstance | null
|
|
|
|
|
) => void
|
|
|
|
|
diffStyle: DiffStyle
|
feat: inline PR comments in the reviews UI (#1600)
* feat: inline PR comments in the reviews UI
Click the diff gutter "+" on a line to open an inline comment composer
(rendered like the finding card via a Pierre annotation); submitting
posts a real inline PR review comment as the signed-in user through a
new POST /reviews/{owner}/{repo}/{number}/comments. The "+" press-drag →
"Add to Chat" selection path is unchanged.
* feat: GitHub-parity comment box, PR comments dropdown, collapse nav
- Comment composer now mirrors GitHub's box: Write/Preview tabs (markdown
rendered via the existing Markdown component) and a markdown toolbar
(heading, bold, italic, quote, code, link, bulleted/numbered/task list).
- Surface other people's inline PR comments in a Devin-style dropdown in the
review header (search + link to the thread on GitHub). New
GET /reviews/{owner}/{repo}/{number}/comments lists them and flags the
reviewer's own (marker-bearing) comments so they're filtered out.
- Collapse the global nav by default on a review detail page, restoring the
prior preference on leave.
* feat: bigger comment-toolbar icons; open dropdown comments inline
- Enlarge the markdown toolbar glyphs (Phosphor) in the comment composer —
they were rendering at 10px.
- Clicking a comment in the PR comments dropdown now opens it inline in the
diff as a read-only finding-style card (InlineComment), scrolling its line
into view, instead of navigating to GitHub. Falls back to GitHub when the
comment's file/line isn't in the current diff.
* fix: drive "Add to Chat" from native text selection
The gutter "+" is now comment-only; wiring its click to the composer
conflicted with its old double-duty as the drag-to-select handle, which
broke selection → "Add to Chat". Switch to Devin's model: disable Pierre's
interactive line selection and instead map a native text highlight in the
diff to a line range (via the data-line / data-line-type attributes Pierre
stamps on each line, read from the diff's open shadow root) to show the
"Add to Chat" popup. ⌘L and the existing attachment/popup path are unchanged.
* feat: gutter "+" drag selects a range for multi-line comments
Re-enable Pierre's gutter line selection so dragging the "+" down the
gutter comments across a range (click still comments on a single line);
onLineSelectionEnd routes the range to the composer. Native code-text
selection still drives "Add to Chat" — Pierre only line-selects from the
gutter, and onLineSelectionEnd bails when a native text selection is
present, so a code highlight never opens the composer.
* fix: keep the range highlighted while its comment composer is open
Previously opening the composer cleared the selection, so the lines being
commented on lost their highlight. Drive the controlled selection from the
open comment draft's range so the rows stay highlighted until the composer
is closed.
* fix: address PR review — paginate comments, fall back for outdated ones
- list_review_comments now pages through all PR review comments (bounded by
_MAX_REVIEW_COMMENT_PAGES) instead of returning only the first 100, so older
comments still show in the dropdown.
- Surface GitHub's outdated flag (position == null) as is_outdated; opening such
a comment (or one whose line isn't in the diff) now opens it on GitHub instead
of silently rendering nothing, plus a timeout fallback if the annotation never
mounts (e.g. collapsed context).
2026-06-23 16:01:46 -07:00
|
|
|
owner: string
|
|
|
|
|
repo: string
|
|
|
|
|
prNumber: number
|
|
|
|
|
commentDraftRange: SelectedLineRange | null
|
|
|
|
|
onStartComment?: (path: string, range: SelectedLineRange) => void
|
|
|
|
|
onCloseComment: () => void
|
|
|
|
|
openComment: PrReviewComment | null
|
|
|
|
|
onCloseOpenComment?: () => void
|
2026-06-23 12:27:51 -07:00
|
|
|
}) {
|
|
|
|
|
// No chat means no line-selection → "Add to Chat" affordance (embedded view).
|
|
|
|
|
const selectable = Boolean(onAddToChat)
|
feat: inline PR comments in the reviews UI (#1600)
* feat: inline PR comments in the reviews UI
Click the diff gutter "+" on a line to open an inline comment composer
(rendered like the finding card via a Pierre annotation); submitting
posts a real inline PR review comment as the signed-in user through a
new POST /reviews/{owner}/{repo}/{number}/comments. The "+" press-drag →
"Add to Chat" selection path is unchanged.
* feat: GitHub-parity comment box, PR comments dropdown, collapse nav
- Comment composer now mirrors GitHub's box: Write/Preview tabs (markdown
rendered via the existing Markdown component) and a markdown toolbar
(heading, bold, italic, quote, code, link, bulleted/numbered/task list).
- Surface other people's inline PR comments in a Devin-style dropdown in the
review header (search + link to the thread on GitHub). New
GET /reviews/{owner}/{repo}/{number}/comments lists them and flags the
reviewer's own (marker-bearing) comments so they're filtered out.
- Collapse the global nav by default on a review detail page, restoring the
prior preference on leave.
* feat: bigger comment-toolbar icons; open dropdown comments inline
- Enlarge the markdown toolbar glyphs (Phosphor) in the comment composer —
they were rendering at 10px.
- Clicking a comment in the PR comments dropdown now opens it inline in the
diff as a read-only finding-style card (InlineComment), scrolling its line
into view, instead of navigating to GitHub. Falls back to GitHub when the
comment's file/line isn't in the current diff.
* fix: drive "Add to Chat" from native text selection
The gutter "+" is now comment-only; wiring its click to the composer
conflicted with its old double-duty as the drag-to-select handle, which
broke selection → "Add to Chat". Switch to Devin's model: disable Pierre's
interactive line selection and instead map a native text highlight in the
diff to a line range (via the data-line / data-line-type attributes Pierre
stamps on each line, read from the diff's open shadow root) to show the
"Add to Chat" popup. ⌘L and the existing attachment/popup path are unchanged.
* feat: gutter "+" drag selects a range for multi-line comments
Re-enable Pierre's gutter line selection so dragging the "+" down the
gutter comments across a range (click still comments on a single line);
onLineSelectionEnd routes the range to the composer. Native code-text
selection still drives "Add to Chat" — Pierre only line-selects from the
gutter, and onLineSelectionEnd bails when a native text selection is
present, so a code highlight never opens the composer.
* fix: keep the range highlighted while its comment composer is open
Previously opening the composer cleared the selection, so the lines being
commented on lost their highlight. Drive the controlled selection from the
open comment draft's range so the rows stay highlighted until the composer
is closed.
* fix: address PR review — paginate comments, fall back for outdated ones
- list_review_comments now pages through all PR review comments (bounded by
_MAX_REVIEW_COMMENT_PAGES) instead of returning only the first 100, so older
comments still show in the dropdown.
- Surface GitHub's outdated flag (position == null) as is_outdated; opening such
a comment (or one whose line isn't in the diff) now opens it on GitHub instead
of silently rendering nothing, plus a timeout fallback if the annotation never
mounts (e.g. collapsed context).
2026-06-23 16:01:46 -07:00
|
|
|
// Commenting rides the same gutter "+" as selection, so it's available only
|
|
|
|
|
// where the gutter utility is enabled (the full reviews page).
|
|
|
|
|
const commentable = selectable && Boolean(onStartComment)
|
2026-06-23 12:27:51 -07:00
|
|
|
const diffOptions = useDiffOptions(diffStyle)
|
|
|
|
|
const diffWrapperRef = useRef<HTMLDivElement | null>(null)
|
|
|
|
|
const lastPointerRef = useRef<{ x: number; y: number } | null>(null)
|
|
|
|
|
const [popup, setPopup] = useState<{
|
|
|
|
|
range: SelectedLineRange
|
|
|
|
|
x: number
|
|
|
|
|
y: number
|
|
|
|
|
} | null>(null)
|
|
|
|
|
|
feat: inline PR comments in the reviews UI (#1600)
* feat: inline PR comments in the reviews UI
Click the diff gutter "+" on a line to open an inline comment composer
(rendered like the finding card via a Pierre annotation); submitting
posts a real inline PR review comment as the signed-in user through a
new POST /reviews/{owner}/{repo}/{number}/comments. The "+" press-drag →
"Add to Chat" selection path is unchanged.
* feat: GitHub-parity comment box, PR comments dropdown, collapse nav
- Comment composer now mirrors GitHub's box: Write/Preview tabs (markdown
rendered via the existing Markdown component) and a markdown toolbar
(heading, bold, italic, quote, code, link, bulleted/numbered/task list).
- Surface other people's inline PR comments in a Devin-style dropdown in the
review header (search + link to the thread on GitHub). New
GET /reviews/{owner}/{repo}/{number}/comments lists them and flags the
reviewer's own (marker-bearing) comments so they're filtered out.
- Collapse the global nav by default on a review detail page, restoring the
prior preference on leave.
* feat: bigger comment-toolbar icons; open dropdown comments inline
- Enlarge the markdown toolbar glyphs (Phosphor) in the comment composer —
they were rendering at 10px.
- Clicking a comment in the PR comments dropdown now opens it inline in the
diff as a read-only finding-style card (InlineComment), scrolling its line
into view, instead of navigating to GitHub. Falls back to GitHub when the
comment's file/line isn't in the current diff.
* fix: drive "Add to Chat" from native text selection
The gutter "+" is now comment-only; wiring its click to the composer
conflicted with its old double-duty as the drag-to-select handle, which
broke selection → "Add to Chat". Switch to Devin's model: disable Pierre's
interactive line selection and instead map a native text highlight in the
diff to a line range (via the data-line / data-line-type attributes Pierre
stamps on each line, read from the diff's open shadow root) to show the
"Add to Chat" popup. ⌘L and the existing attachment/popup path are unchanged.
* feat: gutter "+" drag selects a range for multi-line comments
Re-enable Pierre's gutter line selection so dragging the "+" down the
gutter comments across a range (click still comments on a single line);
onLineSelectionEnd routes the range to the composer. Native code-text
selection still drives "Add to Chat" — Pierre only line-selects from the
gutter, and onLineSelectionEnd bails when a native text selection is
present, so a code highlight never opens the composer.
* fix: keep the range highlighted while its comment composer is open
Previously opening the composer cleared the selection, so the lines being
commented on lost their highlight. Drive the controlled selection from the
open comment draft's range so the rows stay highlighted until the composer
is closed.
* fix: address PR review — paginate comments, fall back for outdated ones
- list_review_comments now pages through all PR review comments (bounded by
_MAX_REVIEW_COMMENT_PAGES) instead of returning only the first 100, so older
comments still show in the dropdown.
- Surface GitHub's outdated flag (position == null) as is_outdated; opening such
a comment (or one whose line isn't in the diff) now opens it on GitHub instead
of silently rendering nothing, plus a timeout fallback if the annotation never
mounts (e.g. collapsed context).
2026-06-23 16:01:46 -07:00
|
|
|
const findingAnnotations = useMemo<
|
|
|
|
|
Array<DiffLineAnnotation<ReviewAnnotation>>
|
|
|
|
|
>(
|
2026-06-23 12:27:51 -07:00
|
|
|
() =>
|
|
|
|
|
findings
|
|
|
|
|
.filter((finding) => finding.end_line !== null)
|
|
|
|
|
.map((finding) => ({
|
|
|
|
|
side: findingSide(finding),
|
|
|
|
|
lineNumber: finding.end_line as number,
|
feat: inline PR comments in the reviews UI (#1600)
* feat: inline PR comments in the reviews UI
Click the diff gutter "+" on a line to open an inline comment composer
(rendered like the finding card via a Pierre annotation); submitting
posts a real inline PR review comment as the signed-in user through a
new POST /reviews/{owner}/{repo}/{number}/comments. The "+" press-drag →
"Add to Chat" selection path is unchanged.
* feat: GitHub-parity comment box, PR comments dropdown, collapse nav
- Comment composer now mirrors GitHub's box: Write/Preview tabs (markdown
rendered via the existing Markdown component) and a markdown toolbar
(heading, bold, italic, quote, code, link, bulleted/numbered/task list).
- Surface other people's inline PR comments in a Devin-style dropdown in the
review header (search + link to the thread on GitHub). New
GET /reviews/{owner}/{repo}/{number}/comments lists them and flags the
reviewer's own (marker-bearing) comments so they're filtered out.
- Collapse the global nav by default on a review detail page, restoring the
prior preference on leave.
* feat: bigger comment-toolbar icons; open dropdown comments inline
- Enlarge the markdown toolbar glyphs (Phosphor) in the comment composer —
they were rendering at 10px.
- Clicking a comment in the PR comments dropdown now opens it inline in the
diff as a read-only finding-style card (InlineComment), scrolling its line
into view, instead of navigating to GitHub. Falls back to GitHub when the
comment's file/line isn't in the current diff.
* fix: drive "Add to Chat" from native text selection
The gutter "+" is now comment-only; wiring its click to the composer
conflicted with its old double-duty as the drag-to-select handle, which
broke selection → "Add to Chat". Switch to Devin's model: disable Pierre's
interactive line selection and instead map a native text highlight in the
diff to a line range (via the data-line / data-line-type attributes Pierre
stamps on each line, read from the diff's open shadow root) to show the
"Add to Chat" popup. ⌘L and the existing attachment/popup path are unchanged.
* feat: gutter "+" drag selects a range for multi-line comments
Re-enable Pierre's gutter line selection so dragging the "+" down the
gutter comments across a range (click still comments on a single line);
onLineSelectionEnd routes the range to the composer. Native code-text
selection still drives "Add to Chat" — Pierre only line-selects from the
gutter, and onLineSelectionEnd bails when a native text selection is
present, so a code highlight never opens the composer.
* fix: keep the range highlighted while its comment composer is open
Previously opening the composer cleared the selection, so the lines being
commented on lost their highlight. Drive the controlled selection from the
open comment draft's range so the rows stay highlighted until the composer
is closed.
* fix: address PR review — paginate comments, fall back for outdated ones
- list_review_comments now pages through all PR review comments (bounded by
_MAX_REVIEW_COMMENT_PAGES) instead of returning only the first 100, so older
comments still show in the dropdown.
- Surface GitHub's outdated flag (position == null) as is_outdated; opening such
a comment (or one whose line isn't in the diff) now opens it on GitHub instead
of silently rendering nothing, plus a timeout fallback if the annotation never
mounts (e.g. collapsed context).
2026-06-23 16:01:46 -07:00
|
|
|
metadata: { kind: "finding", finding },
|
2026-06-23 12:27:51 -07:00
|
|
|
})),
|
|
|
|
|
[findings]
|
|
|
|
|
)
|
|
|
|
|
|
feat: inline PR comments in the reviews UI (#1600)
* feat: inline PR comments in the reviews UI
Click the diff gutter "+" on a line to open an inline comment composer
(rendered like the finding card via a Pierre annotation); submitting
posts a real inline PR review comment as the signed-in user through a
new POST /reviews/{owner}/{repo}/{number}/comments. The "+" press-drag →
"Add to Chat" selection path is unchanged.
* feat: GitHub-parity comment box, PR comments dropdown, collapse nav
- Comment composer now mirrors GitHub's box: Write/Preview tabs (markdown
rendered via the existing Markdown component) and a markdown toolbar
(heading, bold, italic, quote, code, link, bulleted/numbered/task list).
- Surface other people's inline PR comments in a Devin-style dropdown in the
review header (search + link to the thread on GitHub). New
GET /reviews/{owner}/{repo}/{number}/comments lists them and flags the
reviewer's own (marker-bearing) comments so they're filtered out.
- Collapse the global nav by default on a review detail page, restoring the
prior preference on leave.
* feat: bigger comment-toolbar icons; open dropdown comments inline
- Enlarge the markdown toolbar glyphs (Phosphor) in the comment composer —
they were rendering at 10px.
- Clicking a comment in the PR comments dropdown now opens it inline in the
diff as a read-only finding-style card (InlineComment), scrolling its line
into view, instead of navigating to GitHub. Falls back to GitHub when the
comment's file/line isn't in the current diff.
* fix: drive "Add to Chat" from native text selection
The gutter "+" is now comment-only; wiring its click to the composer
conflicted with its old double-duty as the drag-to-select handle, which
broke selection → "Add to Chat". Switch to Devin's model: disable Pierre's
interactive line selection and instead map a native text highlight in the
diff to a line range (via the data-line / data-line-type attributes Pierre
stamps on each line, read from the diff's open shadow root) to show the
"Add to Chat" popup. ⌘L and the existing attachment/popup path are unchanged.
* feat: gutter "+" drag selects a range for multi-line comments
Re-enable Pierre's gutter line selection so dragging the "+" down the
gutter comments across a range (click still comments on a single line);
onLineSelectionEnd routes the range to the composer. Native code-text
selection still drives "Add to Chat" — Pierre only line-selects from the
gutter, and onLineSelectionEnd bails when a native text selection is
present, so a code highlight never opens the composer.
* fix: keep the range highlighted while its comment composer is open
Previously opening the composer cleared the selection, so the lines being
commented on lost their highlight. Drive the controlled selection from the
open comment draft's range so the rows stay highlighted until the composer
is closed.
* fix: address PR review — paginate comments, fall back for outdated ones
- list_review_comments now pages through all PR review comments (bounded by
_MAX_REVIEW_COMMENT_PAGES) instead of returning only the first 100, so older
comments still show in the dropdown.
- Surface GitHub's outdated flag (position == null) as is_outdated; opening such
a comment (or one whose line isn't in the diff) now opens it on GitHub instead
of silently rendering nothing, plus a timeout fallback if the annotation never
mounts (e.g. collapsed context).
2026-06-23 16:01:46 -07:00
|
|
|
// The open draft composer and an opened existing comment each render inline as
|
|
|
|
|
// one more annotation, anchored to their line on the appropriate side.
|
|
|
|
|
const lineAnnotations = useMemo<
|
|
|
|
|
Array<DiffLineAnnotation<ReviewAnnotation>>
|
|
|
|
|
>(() => {
|
|
|
|
|
const extra: Array<DiffLineAnnotation<ReviewAnnotation>> = []
|
|
|
|
|
if (commentDraftRange) {
|
|
|
|
|
extra.push({
|
|
|
|
|
side:
|
|
|
|
|
commentDraftRange.endSide ?? commentDraftRange.side ?? "additions",
|
|
|
|
|
lineNumber: commentDraftRange.end,
|
|
|
|
|
metadata: {
|
|
|
|
|
kind: "draftComment",
|
|
|
|
|
path: file.path,
|
|
|
|
|
range: commentDraftRange,
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
if (openComment && openComment.line !== null) {
|
|
|
|
|
extra.push({
|
|
|
|
|
side: openComment.side === "LEFT" ? "deletions" : "additions",
|
|
|
|
|
lineNumber: openComment.line,
|
|
|
|
|
metadata: { kind: "comment", comment: openComment },
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
return extra.length > 0
|
|
|
|
|
? [...findingAnnotations, ...extra]
|
|
|
|
|
: findingAnnotations
|
|
|
|
|
}, [findingAnnotations, commentDraftRange, openComment, file.path])
|
|
|
|
|
|
|
|
|
|
// The gutter "+" drives comments: a click comments on one line, and a drag down
|
|
|
|
|
// the gutter comments across a range (Pierre's gutter selection, which needs
|
|
|
|
|
// enableLineSelection). "Add to Chat" instead comes from a native text highlight
|
|
|
|
|
// on the code (handleTextSelection) — Pierre leaves code content user-selectable
|
|
|
|
|
// and only line-selects from the gutter, so the two don't collide. onLineSelectionEnd
|
|
|
|
|
// bails if a native text selection is present, so a code highlight never opens the
|
|
|
|
|
// composer (belt-and-suspenders in case Pierre ever reports a content drag).
|
2026-06-23 12:27:51 -07:00
|
|
|
const cardOptions = useMemo(
|
|
|
|
|
() => ({
|
|
|
|
|
...diffOptions,
|
feat: inline PR comments in the reviews UI (#1600)
* feat: inline PR comments in the reviews UI
Click the diff gutter "+" on a line to open an inline comment composer
(rendered like the finding card via a Pierre annotation); submitting
posts a real inline PR review comment as the signed-in user through a
new POST /reviews/{owner}/{repo}/{number}/comments. The "+" press-drag →
"Add to Chat" selection path is unchanged.
* feat: GitHub-parity comment box, PR comments dropdown, collapse nav
- Comment composer now mirrors GitHub's box: Write/Preview tabs (markdown
rendered via the existing Markdown component) and a markdown toolbar
(heading, bold, italic, quote, code, link, bulleted/numbered/task list).
- Surface other people's inline PR comments in a Devin-style dropdown in the
review header (search + link to the thread on GitHub). New
GET /reviews/{owner}/{repo}/{number}/comments lists them and flags the
reviewer's own (marker-bearing) comments so they're filtered out.
- Collapse the global nav by default on a review detail page, restoring the
prior preference on leave.
* feat: bigger comment-toolbar icons; open dropdown comments inline
- Enlarge the markdown toolbar glyphs (Phosphor) in the comment composer —
they were rendering at 10px.
- Clicking a comment in the PR comments dropdown now opens it inline in the
diff as a read-only finding-style card (InlineComment), scrolling its line
into view, instead of navigating to GitHub. Falls back to GitHub when the
comment's file/line isn't in the current diff.
* fix: drive "Add to Chat" from native text selection
The gutter "+" is now comment-only; wiring its click to the composer
conflicted with its old double-duty as the drag-to-select handle, which
broke selection → "Add to Chat". Switch to Devin's model: disable Pierre's
interactive line selection and instead map a native text highlight in the
diff to a line range (via the data-line / data-line-type attributes Pierre
stamps on each line, read from the diff's open shadow root) to show the
"Add to Chat" popup. ⌘L and the existing attachment/popup path are unchanged.
* feat: gutter "+" drag selects a range for multi-line comments
Re-enable Pierre's gutter line selection so dragging the "+" down the
gutter comments across a range (click still comments on a single line);
onLineSelectionEnd routes the range to the composer. Native code-text
selection still drives "Add to Chat" — Pierre only line-selects from the
gutter, and onLineSelectionEnd bails when a native text selection is
present, so a code highlight never opens the composer.
* fix: keep the range highlighted while its comment composer is open
Previously opening the composer cleared the selection, so the lines being
commented on lost their highlight. Drive the controlled selection from the
open comment draft's range so the rows stay highlighted until the composer
is closed.
* fix: address PR review — paginate comments, fall back for outdated ones
- list_review_comments now pages through all PR review comments (bounded by
_MAX_REVIEW_COMMENT_PAGES) instead of returning only the first 100, so older
comments still show in the dropdown.
- Surface GitHub's outdated flag (position == null) as is_outdated; opening such
a comment (or one whose line isn't in the diff) now opens it on GitHub instead
of silently rendering nothing, plus a timeout fallback if the annotation never
mounts (e.g. collapsed context).
2026-06-23 16:01:46 -07:00
|
|
|
enableLineSelection: commentable,
|
|
|
|
|
enableGutterUtility: commentable,
|
|
|
|
|
onGutterUtilityClick: commentable
|
|
|
|
|
? (range: SelectedLineRange) => onStartComment?.(file.path, range)
|
|
|
|
|
: undefined,
|
|
|
|
|
onLineSelectionChange: commentable
|
|
|
|
|
? (range: SelectedLineRange | null) => onSelectLines(file.path, range)
|
|
|
|
|
: undefined,
|
|
|
|
|
onLineSelectionEnd: commentable
|
|
|
|
|
? (range: SelectedLineRange | null) => {
|
|
|
|
|
if (!range) return
|
|
|
|
|
const host =
|
|
|
|
|
diffWrapperRef.current?.querySelector("diffs-container")
|
|
|
|
|
const native = readDiffSelection(host)
|
|
|
|
|
if (native && !native.isCollapsed && native.rangeCount > 0) return
|
|
|
|
|
onStartComment?.(file.path, range)
|
|
|
|
|
}
|
|
|
|
|
: undefined,
|
2026-06-23 12:27:51 -07:00
|
|
|
onPostRender: (
|
|
|
|
|
node: HTMLElement,
|
feat: inline PR comments in the reviews UI (#1600)
* feat: inline PR comments in the reviews UI
Click the diff gutter "+" on a line to open an inline comment composer
(rendered like the finding card via a Pierre annotation); submitting
posts a real inline PR review comment as the signed-in user through a
new POST /reviews/{owner}/{repo}/{number}/comments. The "+" press-drag →
"Add to Chat" selection path is unchanged.
* feat: GitHub-parity comment box, PR comments dropdown, collapse nav
- Comment composer now mirrors GitHub's box: Write/Preview tabs (markdown
rendered via the existing Markdown component) and a markdown toolbar
(heading, bold, italic, quote, code, link, bulleted/numbered/task list).
- Surface other people's inline PR comments in a Devin-style dropdown in the
review header (search + link to the thread on GitHub). New
GET /reviews/{owner}/{repo}/{number}/comments lists them and flags the
reviewer's own (marker-bearing) comments so they're filtered out.
- Collapse the global nav by default on a review detail page, restoring the
prior preference on leave.
* feat: bigger comment-toolbar icons; open dropdown comments inline
- Enlarge the markdown toolbar glyphs (Phosphor) in the comment composer —
they were rendering at 10px.
- Clicking a comment in the PR comments dropdown now opens it inline in the
diff as a read-only finding-style card (InlineComment), scrolling its line
into view, instead of navigating to GitHub. Falls back to GitHub when the
comment's file/line isn't in the current diff.
* fix: drive "Add to Chat" from native text selection
The gutter "+" is now comment-only; wiring its click to the composer
conflicted with its old double-duty as the drag-to-select handle, which
broke selection → "Add to Chat". Switch to Devin's model: disable Pierre's
interactive line selection and instead map a native text highlight in the
diff to a line range (via the data-line / data-line-type attributes Pierre
stamps on each line, read from the diff's open shadow root) to show the
"Add to Chat" popup. ⌘L and the existing attachment/popup path are unchanged.
* feat: gutter "+" drag selects a range for multi-line comments
Re-enable Pierre's gutter line selection so dragging the "+" down the
gutter comments across a range (click still comments on a single line);
onLineSelectionEnd routes the range to the composer. Native code-text
selection still drives "Add to Chat" — Pierre only line-selects from the
gutter, and onLineSelectionEnd bails when a native text selection is
present, so a code highlight never opens the composer.
* fix: keep the range highlighted while its comment composer is open
Previously opening the composer cleared the selection, so the lines being
commented on lost their highlight. Drive the controlled selection from the
open comment draft's range so the rows stay highlighted until the composer
is closed.
* fix: address PR review — paginate comments, fall back for outdated ones
- list_review_comments now pages through all PR review comments (bounded by
_MAX_REVIEW_COMMENT_PAGES) instead of returning only the first 100, so older
comments still show in the dropdown.
- Surface GitHub's outdated flag (position == null) as is_outdated; opening such
a comment (or one whose line isn't in the diff) now opens it on GitHub instead
of silently rendering nothing, plus a timeout fallback if the annotation never
mounts (e.g. collapsed context).
2026-06-23 16:01:46 -07:00
|
|
|
instance: CoreFileDiff<ReviewAnnotation>
|
2026-06-23 12:27:51 -07:00
|
|
|
) => registerDiffInstance(file.path, { host: node, instance }),
|
|
|
|
|
}),
|
feat: inline PR comments in the reviews UI (#1600)
* feat: inline PR comments in the reviews UI
Click the diff gutter "+" on a line to open an inline comment composer
(rendered like the finding card via a Pierre annotation); submitting
posts a real inline PR review comment as the signed-in user through a
new POST /reviews/{owner}/{repo}/{number}/comments. The "+" press-drag →
"Add to Chat" selection path is unchanged.
* feat: GitHub-parity comment box, PR comments dropdown, collapse nav
- Comment composer now mirrors GitHub's box: Write/Preview tabs (markdown
rendered via the existing Markdown component) and a markdown toolbar
(heading, bold, italic, quote, code, link, bulleted/numbered/task list).
- Surface other people's inline PR comments in a Devin-style dropdown in the
review header (search + link to the thread on GitHub). New
GET /reviews/{owner}/{repo}/{number}/comments lists them and flags the
reviewer's own (marker-bearing) comments so they're filtered out.
- Collapse the global nav by default on a review detail page, restoring the
prior preference on leave.
* feat: bigger comment-toolbar icons; open dropdown comments inline
- Enlarge the markdown toolbar glyphs (Phosphor) in the comment composer —
they were rendering at 10px.
- Clicking a comment in the PR comments dropdown now opens it inline in the
diff as a read-only finding-style card (InlineComment), scrolling its line
into view, instead of navigating to GitHub. Falls back to GitHub when the
comment's file/line isn't in the current diff.
* fix: drive "Add to Chat" from native text selection
The gutter "+" is now comment-only; wiring its click to the composer
conflicted with its old double-duty as the drag-to-select handle, which
broke selection → "Add to Chat". Switch to Devin's model: disable Pierre's
interactive line selection and instead map a native text highlight in the
diff to a line range (via the data-line / data-line-type attributes Pierre
stamps on each line, read from the diff's open shadow root) to show the
"Add to Chat" popup. ⌘L and the existing attachment/popup path are unchanged.
* feat: gutter "+" drag selects a range for multi-line comments
Re-enable Pierre's gutter line selection so dragging the "+" down the
gutter comments across a range (click still comments on a single line);
onLineSelectionEnd routes the range to the composer. Native code-text
selection still drives "Add to Chat" — Pierre only line-selects from the
gutter, and onLineSelectionEnd bails when a native text selection is
present, so a code highlight never opens the composer.
* fix: keep the range highlighted while its comment composer is open
Previously opening the composer cleared the selection, so the lines being
commented on lost their highlight. Drive the controlled selection from the
open comment draft's range so the rows stay highlighted until the composer
is closed.
* fix: address PR review — paginate comments, fall back for outdated ones
- list_review_comments now pages through all PR review comments (bounded by
_MAX_REVIEW_COMMENT_PAGES) instead of returning only the first 100, so older
comments still show in the dropdown.
- Surface GitHub's outdated flag (position == null) as is_outdated; opening such
a comment (or one whose line isn't in the diff) now opens it on GitHub instead
of silently rendering nothing, plus a timeout fallback if the annotation never
mounts (e.g. collapsed context).
2026-06-23 16:01:46 -07:00
|
|
|
[
|
|
|
|
|
diffOptions,
|
|
|
|
|
commentable,
|
|
|
|
|
onStartComment,
|
|
|
|
|
onSelectLines,
|
|
|
|
|
file.path,
|
|
|
|
|
registerDiffInstance,
|
|
|
|
|
]
|
2026-06-23 12:27:51 -07:00
|
|
|
)
|
|
|
|
|
|
feat: inline PR comments in the reviews UI (#1600)
* feat: inline PR comments in the reviews UI
Click the diff gutter "+" on a line to open an inline comment composer
(rendered like the finding card via a Pierre annotation); submitting
posts a real inline PR review comment as the signed-in user through a
new POST /reviews/{owner}/{repo}/{number}/comments. The "+" press-drag →
"Add to Chat" selection path is unchanged.
* feat: GitHub-parity comment box, PR comments dropdown, collapse nav
- Comment composer now mirrors GitHub's box: Write/Preview tabs (markdown
rendered via the existing Markdown component) and a markdown toolbar
(heading, bold, italic, quote, code, link, bulleted/numbered/task list).
- Surface other people's inline PR comments in a Devin-style dropdown in the
review header (search + link to the thread on GitHub). New
GET /reviews/{owner}/{repo}/{number}/comments lists them and flags the
reviewer's own (marker-bearing) comments so they're filtered out.
- Collapse the global nav by default on a review detail page, restoring the
prior preference on leave.
* feat: bigger comment-toolbar icons; open dropdown comments inline
- Enlarge the markdown toolbar glyphs (Phosphor) in the comment composer —
they were rendering at 10px.
- Clicking a comment in the PR comments dropdown now opens it inline in the
diff as a read-only finding-style card (InlineComment), scrolling its line
into view, instead of navigating to GitHub. Falls back to GitHub when the
comment's file/line isn't in the current diff.
* fix: drive "Add to Chat" from native text selection
The gutter "+" is now comment-only; wiring its click to the composer
conflicted with its old double-duty as the drag-to-select handle, which
broke selection → "Add to Chat". Switch to Devin's model: disable Pierre's
interactive line selection and instead map a native text highlight in the
diff to a line range (via the data-line / data-line-type attributes Pierre
stamps on each line, read from the diff's open shadow root) to show the
"Add to Chat" popup. ⌘L and the existing attachment/popup path are unchanged.
* feat: gutter "+" drag selects a range for multi-line comments
Re-enable Pierre's gutter line selection so dragging the "+" down the
gutter comments across a range (click still comments on a single line);
onLineSelectionEnd routes the range to the composer. Native code-text
selection still drives "Add to Chat" — Pierre only line-selects from the
gutter, and onLineSelectionEnd bails when a native text selection is
present, so a code highlight never opens the composer.
* fix: keep the range highlighted while its comment composer is open
Previously opening the composer cleared the selection, so the lines being
commented on lost their highlight. Drive the controlled selection from the
open comment draft's range so the rows stay highlighted until the composer
is closed.
* fix: address PR review — paginate comments, fall back for outdated ones
- list_review_comments now pages through all PR review comments (bounded by
_MAX_REVIEW_COMMENT_PAGES) instead of returning only the first 100, so older
comments still show in the dropdown.
- Surface GitHub's outdated flag (position == null) as is_outdated; opening such
a comment (or one whose line isn't in the diff) now opens it on GitHub instead
of silently rendering nothing, plus a timeout fallback if the annotation never
mounts (e.g. collapsed context).
2026-06-23 16:01:46 -07:00
|
|
|
// On mouse release, turn any native text highlight inside the diff into a line
|
|
|
|
|
// range: highlight rows (controlled selection) + show the "Add to Chat" popup
|
|
|
|
|
// at the cursor. A collapsed selection (plain click) is ignored.
|
|
|
|
|
const handleTextSelection = useCallback(() => {
|
|
|
|
|
if (!selectable) return
|
|
|
|
|
const container = diffWrapperRef.current?.querySelector("diffs-container")
|
|
|
|
|
const range = selectedRangeFromDiff(container)
|
|
|
|
|
if (!range) return
|
|
|
|
|
onSelectLines(file.path, range)
|
|
|
|
|
const pointer = lastPointerRef.current
|
|
|
|
|
if (pointer) setPopup({ range, x: pointer.x, y: pointer.y })
|
|
|
|
|
}, [selectable, file.path, onSelectLines])
|
|
|
|
|
|
2026-06-23 12:27:51 -07:00
|
|
|
const addPopupToChat = useCallback(() => {
|
|
|
|
|
if (popup) onAddToChat?.(file.path, popup.range)
|
|
|
|
|
setPopup(null)
|
feat: inline PR comments in the reviews UI (#1600)
* feat: inline PR comments in the reviews UI
Click the diff gutter "+" on a line to open an inline comment composer
(rendered like the finding card via a Pierre annotation); submitting
posts a real inline PR review comment as the signed-in user through a
new POST /reviews/{owner}/{repo}/{number}/comments. The "+" press-drag →
"Add to Chat" selection path is unchanged.
* feat: GitHub-parity comment box, PR comments dropdown, collapse nav
- Comment composer now mirrors GitHub's box: Write/Preview tabs (markdown
rendered via the existing Markdown component) and a markdown toolbar
(heading, bold, italic, quote, code, link, bulleted/numbered/task list).
- Surface other people's inline PR comments in a Devin-style dropdown in the
review header (search + link to the thread on GitHub). New
GET /reviews/{owner}/{repo}/{number}/comments lists them and flags the
reviewer's own (marker-bearing) comments so they're filtered out.
- Collapse the global nav by default on a review detail page, restoring the
prior preference on leave.
* feat: bigger comment-toolbar icons; open dropdown comments inline
- Enlarge the markdown toolbar glyphs (Phosphor) in the comment composer —
they were rendering at 10px.
- Clicking a comment in the PR comments dropdown now opens it inline in the
diff as a read-only finding-style card (InlineComment), scrolling its line
into view, instead of navigating to GitHub. Falls back to GitHub when the
comment's file/line isn't in the current diff.
* fix: drive "Add to Chat" from native text selection
The gutter "+" is now comment-only; wiring its click to the composer
conflicted with its old double-duty as the drag-to-select handle, which
broke selection → "Add to Chat". Switch to Devin's model: disable Pierre's
interactive line selection and instead map a native text highlight in the
diff to a line range (via the data-line / data-line-type attributes Pierre
stamps on each line, read from the diff's open shadow root) to show the
"Add to Chat" popup. ⌘L and the existing attachment/popup path are unchanged.
* feat: gutter "+" drag selects a range for multi-line comments
Re-enable Pierre's gutter line selection so dragging the "+" down the
gutter comments across a range (click still comments on a single line);
onLineSelectionEnd routes the range to the composer. Native code-text
selection still drives "Add to Chat" — Pierre only line-selects from the
gutter, and onLineSelectionEnd bails when a native text selection is
present, so a code highlight never opens the composer.
* fix: keep the range highlighted while its comment composer is open
Previously opening the composer cleared the selection, so the lines being
commented on lost their highlight. Drive the controlled selection from the
open comment draft's range so the rows stay highlighted until the composer
is closed.
* fix: address PR review — paginate comments, fall back for outdated ones
- list_review_comments now pages through all PR review comments (bounded by
_MAX_REVIEW_COMMENT_PAGES) instead of returning only the first 100, so older
comments still show in the dropdown.
- Surface GitHub's outdated flag (position == null) as is_outdated; opening such
a comment (or one whose line isn't in the diff) now opens it on GitHub instead
of silently rendering nothing, plus a timeout fallback if the annotation never
mounts (e.g. collapsed context).
2026-06-23 16:01:46 -07:00
|
|
|
// Clear the lingering native highlight once added.
|
|
|
|
|
readDiffSelection(
|
|
|
|
|
diffWrapperRef.current?.querySelector("diffs-container")
|
|
|
|
|
)?.removeAllRanges()
|
2026-06-23 12:27:51 -07:00
|
|
|
}, [popup, onAddToChat, file.path])
|
|
|
|
|
|
|
|
|
|
// Drop the popup once the selection clears (e.g. added via ⌘L, or a finding
|
|
|
|
|
// took focus) so it can't add the same range twice.
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (!selectedLines) setPopup(null)
|
|
|
|
|
}, [selectedLines])
|
|
|
|
|
|
feat: inline PR comments in the reviews UI (#1600)
* feat: inline PR comments in the reviews UI
Click the diff gutter "+" on a line to open an inline comment composer
(rendered like the finding card via a Pierre annotation); submitting
posts a real inline PR review comment as the signed-in user through a
new POST /reviews/{owner}/{repo}/{number}/comments. The "+" press-drag →
"Add to Chat" selection path is unchanged.
* feat: GitHub-parity comment box, PR comments dropdown, collapse nav
- Comment composer now mirrors GitHub's box: Write/Preview tabs (markdown
rendered via the existing Markdown component) and a markdown toolbar
(heading, bold, italic, quote, code, link, bulleted/numbered/task list).
- Surface other people's inline PR comments in a Devin-style dropdown in the
review header (search + link to the thread on GitHub). New
GET /reviews/{owner}/{repo}/{number}/comments lists them and flags the
reviewer's own (marker-bearing) comments so they're filtered out.
- Collapse the global nav by default on a review detail page, restoring the
prior preference on leave.
* feat: bigger comment-toolbar icons; open dropdown comments inline
- Enlarge the markdown toolbar glyphs (Phosphor) in the comment composer —
they were rendering at 10px.
- Clicking a comment in the PR comments dropdown now opens it inline in the
diff as a read-only finding-style card (InlineComment), scrolling its line
into view, instead of navigating to GitHub. Falls back to GitHub when the
comment's file/line isn't in the current diff.
* fix: drive "Add to Chat" from native text selection
The gutter "+" is now comment-only; wiring its click to the composer
conflicted with its old double-duty as the drag-to-select handle, which
broke selection → "Add to Chat". Switch to Devin's model: disable Pierre's
interactive line selection and instead map a native text highlight in the
diff to a line range (via the data-line / data-line-type attributes Pierre
stamps on each line, read from the diff's open shadow root) to show the
"Add to Chat" popup. ⌘L and the existing attachment/popup path are unchanged.
* feat: gutter "+" drag selects a range for multi-line comments
Re-enable Pierre's gutter line selection so dragging the "+" down the
gutter comments across a range (click still comments on a single line);
onLineSelectionEnd routes the range to the composer. Native code-text
selection still drives "Add to Chat" — Pierre only line-selects from the
gutter, and onLineSelectionEnd bails when a native text selection is
present, so a code highlight never opens the composer.
* fix: keep the range highlighted while its comment composer is open
Previously opening the composer cleared the selection, so the lines being
commented on lost their highlight. Drive the controlled selection from the
open comment draft's range so the rows stay highlighted until the composer
is closed.
* fix: address PR review — paginate comments, fall back for outdated ones
- list_review_comments now pages through all PR review comments (bounded by
_MAX_REVIEW_COMMENT_PAGES) instead of returning only the first 100, so older
comments still show in the dropdown.
- Surface GitHub's outdated flag (position == null) as is_outdated; opening such
a comment (or one whose line isn't in the diff) now opens it on GitHub instead
of silently rendering nothing, plus a timeout fallback if the annotation never
mounts (e.g. collapsed context).
2026-06-23 16:01:46 -07:00
|
|
|
// Opening a comment draft owns the "+"; never show "Add to Chat" alongside it
|
|
|
|
|
// (a single "+" click can otherwise both open the composer and arm the popup).
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (commentDraftRange) setPopup(null)
|
|
|
|
|
}, [commentDraftRange])
|
|
|
|
|
|
2026-06-23 12:27:51 -07:00
|
|
|
const oldFile = useMemo<FileContents>(
|
|
|
|
|
() => ({
|
|
|
|
|
name: file.path,
|
|
|
|
|
contents: file.originalContent,
|
|
|
|
|
cacheKey: fileContentsCacheKey(file.path, "old", file.originalContent),
|
|
|
|
|
}),
|
|
|
|
|
[file.path, file.originalContent]
|
|
|
|
|
)
|
|
|
|
|
const newFile = useMemo<FileContents>(
|
|
|
|
|
() => ({
|
|
|
|
|
name: file.path,
|
|
|
|
|
contents: file.modifiedContent,
|
|
|
|
|
cacheKey: fileContentsCacheKey(file.path, "new", file.modifiedContent),
|
|
|
|
|
}),
|
|
|
|
|
[file.path, file.modifiedContent]
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const sectionRef = useCallback(
|
|
|
|
|
(node: HTMLDivElement | null) => registerSection(file.path, node),
|
|
|
|
|
[registerSection, file.path]
|
|
|
|
|
)
|
|
|
|
|
useEffect(
|
|
|
|
|
() => () => registerDiffInstance(file.path, null),
|
|
|
|
|
[file.path, registerDiffInstance]
|
|
|
|
|
)
|
|
|
|
|
const renderAnnotation = useCallback(
|
feat: inline PR comments in the reviews UI (#1600)
* feat: inline PR comments in the reviews UI
Click the diff gutter "+" on a line to open an inline comment composer
(rendered like the finding card via a Pierre annotation); submitting
posts a real inline PR review comment as the signed-in user through a
new POST /reviews/{owner}/{repo}/{number}/comments. The "+" press-drag →
"Add to Chat" selection path is unchanged.
* feat: GitHub-parity comment box, PR comments dropdown, collapse nav
- Comment composer now mirrors GitHub's box: Write/Preview tabs (markdown
rendered via the existing Markdown component) and a markdown toolbar
(heading, bold, italic, quote, code, link, bulleted/numbered/task list).
- Surface other people's inline PR comments in a Devin-style dropdown in the
review header (search + link to the thread on GitHub). New
GET /reviews/{owner}/{repo}/{number}/comments lists them and flags the
reviewer's own (marker-bearing) comments so they're filtered out.
- Collapse the global nav by default on a review detail page, restoring the
prior preference on leave.
* feat: bigger comment-toolbar icons; open dropdown comments inline
- Enlarge the markdown toolbar glyphs (Phosphor) in the comment composer —
they were rendering at 10px.
- Clicking a comment in the PR comments dropdown now opens it inline in the
diff as a read-only finding-style card (InlineComment), scrolling its line
into view, instead of navigating to GitHub. Falls back to GitHub when the
comment's file/line isn't in the current diff.
* fix: drive "Add to Chat" from native text selection
The gutter "+" is now comment-only; wiring its click to the composer
conflicted with its old double-duty as the drag-to-select handle, which
broke selection → "Add to Chat". Switch to Devin's model: disable Pierre's
interactive line selection and instead map a native text highlight in the
diff to a line range (via the data-line / data-line-type attributes Pierre
stamps on each line, read from the diff's open shadow root) to show the
"Add to Chat" popup. ⌘L and the existing attachment/popup path are unchanged.
* feat: gutter "+" drag selects a range for multi-line comments
Re-enable Pierre's gutter line selection so dragging the "+" down the
gutter comments across a range (click still comments on a single line);
onLineSelectionEnd routes the range to the composer. Native code-text
selection still drives "Add to Chat" — Pierre only line-selects from the
gutter, and onLineSelectionEnd bails when a native text selection is
present, so a code highlight never opens the composer.
* fix: keep the range highlighted while its comment composer is open
Previously opening the composer cleared the selection, so the lines being
commented on lost their highlight. Drive the controlled selection from the
open comment draft's range so the rows stay highlighted until the composer
is closed.
* fix: address PR review — paginate comments, fall back for outdated ones
- list_review_comments now pages through all PR review comments (bounded by
_MAX_REVIEW_COMMENT_PAGES) instead of returning only the first 100, so older
comments still show in the dropdown.
- Surface GitHub's outdated flag (position == null) as is_outdated; opening such
a comment (or one whose line isn't in the diff) now opens it on GitHub instead
of silently rendering nothing, plus a timeout fallback if the annotation never
mounts (e.g. collapsed context).
2026-06-23 16:01:46 -07:00
|
|
|
(annotation: DiffLineAnnotation<ReviewAnnotation>) => {
|
|
|
|
|
const meta = annotation.metadata
|
|
|
|
|
if (meta.kind === "finding")
|
|
|
|
|
return <InlineFinding finding={meta.finding} />
|
|
|
|
|
if (meta.kind === "comment")
|
|
|
|
|
return (
|
|
|
|
|
<InlineComment
|
|
|
|
|
comment={meta.comment}
|
|
|
|
|
onClose={onCloseOpenComment ?? (() => undefined)}
|
|
|
|
|
/>
|
|
|
|
|
)
|
|
|
|
|
return (
|
|
|
|
|
<CommentComposer
|
|
|
|
|
owner={owner}
|
|
|
|
|
repo={repo}
|
|
|
|
|
prNumber={prNumber}
|
|
|
|
|
path={meta.path}
|
|
|
|
|
range={meta.range}
|
|
|
|
|
onClose={onCloseComment}
|
|
|
|
|
/>
|
|
|
|
|
)
|
|
|
|
|
},
|
|
|
|
|
[owner, repo, prNumber, onCloseComment, onCloseOpenComment]
|
2026-06-23 12:27:51 -07:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div
|
|
|
|
|
ref={sectionRef}
|
|
|
|
|
className="scroll-mt-4 overflow-hidden rounded-lg border border-[var(--ui-border)]"
|
|
|
|
|
>
|
|
|
|
|
<div className="flex items-center gap-2 bg-[var(--ui-panel-2)] px-3 py-2 text-xs">
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={() => onToggleExpanded(file.path)}
|
|
|
|
|
className="inline-flex items-center gap-2 text-left"
|
|
|
|
|
>
|
|
|
|
|
<CaretDownIcon
|
|
|
|
|
className={cn(
|
|
|
|
|
"size-3 transition-transform",
|
|
|
|
|
!expanded && "-rotate-90"
|
|
|
|
|
)}
|
|
|
|
|
/>
|
|
|
|
|
<span className="font-mono font-medium">{file.path}</span>
|
|
|
|
|
</button>
|
|
|
|
|
<span className="flex items-center gap-1.5 font-mono text-[11px]">
|
|
|
|
|
<span className="text-emerald-500">+{file.additions}</span>
|
|
|
|
|
<span className="text-red-500">-{file.deletions}</span>
|
|
|
|
|
</span>
|
|
|
|
|
{findings.length > 0 && (
|
|
|
|
|
<span className="inline-flex items-center gap-1 text-[11px] text-amber-500">
|
|
|
|
|
<FlagIcon className="size-3" />
|
|
|
|
|
{findings.length}
|
|
|
|
|
</span>
|
|
|
|
|
)}
|
|
|
|
|
<label className="ml-auto inline-flex cursor-pointer items-center gap-1.5 text-[11px] text-muted-foreground">
|
|
|
|
|
Mark as viewed
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
role="checkbox"
|
|
|
|
|
aria-checked={viewed}
|
|
|
|
|
onClick={() => onToggleViewed(file.path)}
|
|
|
|
|
className={cn(
|
|
|
|
|
"flex size-4 items-center justify-center rounded border border-border",
|
|
|
|
|
viewed && "bg-foreground text-background"
|
|
|
|
|
)}
|
|
|
|
|
>
|
|
|
|
|
{viewed && <CheckIcon className="size-3" />}
|
|
|
|
|
</button>
|
|
|
|
|
</label>
|
|
|
|
|
</div>
|
|
|
|
|
{expanded &&
|
|
|
|
|
(file.unrenderable ? (
|
|
|
|
|
<div className="bg-[var(--ui-panel)] p-4 text-center text-xs text-[var(--ui-text-dim)]">
|
|
|
|
|
Binary or large file — diff not shown.
|
|
|
|
|
</div>
|
|
|
|
|
) : (
|
|
|
|
|
<div
|
|
|
|
|
ref={diffWrapperRef}
|
|
|
|
|
onPointerUpCapture={(event) => {
|
|
|
|
|
lastPointerRef.current = { x: event.clientX, y: event.clientY }
|
|
|
|
|
}}
|
feat: inline PR comments in the reviews UI (#1600)
* feat: inline PR comments in the reviews UI
Click the diff gutter "+" on a line to open an inline comment composer
(rendered like the finding card via a Pierre annotation); submitting
posts a real inline PR review comment as the signed-in user through a
new POST /reviews/{owner}/{repo}/{number}/comments. The "+" press-drag →
"Add to Chat" selection path is unchanged.
* feat: GitHub-parity comment box, PR comments dropdown, collapse nav
- Comment composer now mirrors GitHub's box: Write/Preview tabs (markdown
rendered via the existing Markdown component) and a markdown toolbar
(heading, bold, italic, quote, code, link, bulleted/numbered/task list).
- Surface other people's inline PR comments in a Devin-style dropdown in the
review header (search + link to the thread on GitHub). New
GET /reviews/{owner}/{repo}/{number}/comments lists them and flags the
reviewer's own (marker-bearing) comments so they're filtered out.
- Collapse the global nav by default on a review detail page, restoring the
prior preference on leave.
* feat: bigger comment-toolbar icons; open dropdown comments inline
- Enlarge the markdown toolbar glyphs (Phosphor) in the comment composer —
they were rendering at 10px.
- Clicking a comment in the PR comments dropdown now opens it inline in the
diff as a read-only finding-style card (InlineComment), scrolling its line
into view, instead of navigating to GitHub. Falls back to GitHub when the
comment's file/line isn't in the current diff.
* fix: drive "Add to Chat" from native text selection
The gutter "+" is now comment-only; wiring its click to the composer
conflicted with its old double-duty as the drag-to-select handle, which
broke selection → "Add to Chat". Switch to Devin's model: disable Pierre's
interactive line selection and instead map a native text highlight in the
diff to a line range (via the data-line / data-line-type attributes Pierre
stamps on each line, read from the diff's open shadow root) to show the
"Add to Chat" popup. ⌘L and the existing attachment/popup path are unchanged.
* feat: gutter "+" drag selects a range for multi-line comments
Re-enable Pierre's gutter line selection so dragging the "+" down the
gutter comments across a range (click still comments on a single line);
onLineSelectionEnd routes the range to the composer. Native code-text
selection still drives "Add to Chat" — Pierre only line-selects from the
gutter, and onLineSelectionEnd bails when a native text selection is
present, so a code highlight never opens the composer.
* fix: keep the range highlighted while its comment composer is open
Previously opening the composer cleared the selection, so the lines being
commented on lost their highlight. Drive the controlled selection from the
open comment draft's range so the rows stay highlighted until the composer
is closed.
* fix: address PR review — paginate comments, fall back for outdated ones
- list_review_comments now pages through all PR review comments (bounded by
_MAX_REVIEW_COMMENT_PAGES) instead of returning only the first 100, so older
comments still show in the dropdown.
- Surface GitHub's outdated flag (position == null) as is_outdated; opening such
a comment (or one whose line isn't in the diff) now opens it on GitHub instead
of silently rendering nothing, plus a timeout fallback if the annotation never
mounts (e.g. collapsed context).
2026-06-23 16:01:46 -07:00
|
|
|
onMouseUp={handleTextSelection}
|
2026-06-23 12:27:51 -07:00
|
|
|
className="overflow-x-auto bg-[var(--ui-panel)] font-mono text-[11px] leading-5"
|
|
|
|
|
>
|
feat: inline PR comments in the reviews UI (#1600)
* feat: inline PR comments in the reviews UI
Click the diff gutter "+" on a line to open an inline comment composer
(rendered like the finding card via a Pierre annotation); submitting
posts a real inline PR review comment as the signed-in user through a
new POST /reviews/{owner}/{repo}/{number}/comments. The "+" press-drag →
"Add to Chat" selection path is unchanged.
* feat: GitHub-parity comment box, PR comments dropdown, collapse nav
- Comment composer now mirrors GitHub's box: Write/Preview tabs (markdown
rendered via the existing Markdown component) and a markdown toolbar
(heading, bold, italic, quote, code, link, bulleted/numbered/task list).
- Surface other people's inline PR comments in a Devin-style dropdown in the
review header (search + link to the thread on GitHub). New
GET /reviews/{owner}/{repo}/{number}/comments lists them and flags the
reviewer's own (marker-bearing) comments so they're filtered out.
- Collapse the global nav by default on a review detail page, restoring the
prior preference on leave.
* feat: bigger comment-toolbar icons; open dropdown comments inline
- Enlarge the markdown toolbar glyphs (Phosphor) in the comment composer —
they were rendering at 10px.
- Clicking a comment in the PR comments dropdown now opens it inline in the
diff as a read-only finding-style card (InlineComment), scrolling its line
into view, instead of navigating to GitHub. Falls back to GitHub when the
comment's file/line isn't in the current diff.
* fix: drive "Add to Chat" from native text selection
The gutter "+" is now comment-only; wiring its click to the composer
conflicted with its old double-duty as the drag-to-select handle, which
broke selection → "Add to Chat". Switch to Devin's model: disable Pierre's
interactive line selection and instead map a native text highlight in the
diff to a line range (via the data-line / data-line-type attributes Pierre
stamps on each line, read from the diff's open shadow root) to show the
"Add to Chat" popup. ⌘L and the existing attachment/popup path are unchanged.
* feat: gutter "+" drag selects a range for multi-line comments
Re-enable Pierre's gutter line selection so dragging the "+" down the
gutter comments across a range (click still comments on a single line);
onLineSelectionEnd routes the range to the composer. Native code-text
selection still drives "Add to Chat" — Pierre only line-selects from the
gutter, and onLineSelectionEnd bails when a native text selection is
present, so a code highlight never opens the composer.
* fix: keep the range highlighted while its comment composer is open
Previously opening the composer cleared the selection, so the lines being
commented on lost their highlight. Drive the controlled selection from the
open comment draft's range so the rows stay highlighted until the composer
is closed.
* fix: address PR review — paginate comments, fall back for outdated ones
- list_review_comments now pages through all PR review comments (bounded by
_MAX_REVIEW_COMMENT_PAGES) instead of returning only the first 100, so older
comments still show in the dropdown.
- Surface GitHub's outdated flag (position == null) as is_outdated; opening such
a comment (or one whose line isn't in the diff) now opens it on GitHub instead
of silently rendering nothing, plus a timeout fallback if the annotation never
mounts (e.g. collapsed context).
2026-06-23 16:01:46 -07:00
|
|
|
<MultiFileDiff<ReviewAnnotation>
|
2026-06-23 12:27:51 -07:00
|
|
|
oldFile={oldFile}
|
|
|
|
|
newFile={newFile}
|
|
|
|
|
options={cardOptions}
|
|
|
|
|
metrics={DIFF_VIRTUAL_METRICS}
|
|
|
|
|
lineAnnotations={lineAnnotations}
|
|
|
|
|
selectedLines={selectedLines}
|
|
|
|
|
renderAnnotation={renderAnnotation}
|
|
|
|
|
/>
|
feat: inline PR comments in the reviews UI (#1600)
* feat: inline PR comments in the reviews UI
Click the diff gutter "+" on a line to open an inline comment composer
(rendered like the finding card via a Pierre annotation); submitting
posts a real inline PR review comment as the signed-in user through a
new POST /reviews/{owner}/{repo}/{number}/comments. The "+" press-drag →
"Add to Chat" selection path is unchanged.
* feat: GitHub-parity comment box, PR comments dropdown, collapse nav
- Comment composer now mirrors GitHub's box: Write/Preview tabs (markdown
rendered via the existing Markdown component) and a markdown toolbar
(heading, bold, italic, quote, code, link, bulleted/numbered/task list).
- Surface other people's inline PR comments in a Devin-style dropdown in the
review header (search + link to the thread on GitHub). New
GET /reviews/{owner}/{repo}/{number}/comments lists them and flags the
reviewer's own (marker-bearing) comments so they're filtered out.
- Collapse the global nav by default on a review detail page, restoring the
prior preference on leave.
* feat: bigger comment-toolbar icons; open dropdown comments inline
- Enlarge the markdown toolbar glyphs (Phosphor) in the comment composer —
they were rendering at 10px.
- Clicking a comment in the PR comments dropdown now opens it inline in the
diff as a read-only finding-style card (InlineComment), scrolling its line
into view, instead of navigating to GitHub. Falls back to GitHub when the
comment's file/line isn't in the current diff.
* fix: drive "Add to Chat" from native text selection
The gutter "+" is now comment-only; wiring its click to the composer
conflicted with its old double-duty as the drag-to-select handle, which
broke selection → "Add to Chat". Switch to Devin's model: disable Pierre's
interactive line selection and instead map a native text highlight in the
diff to a line range (via the data-line / data-line-type attributes Pierre
stamps on each line, read from the diff's open shadow root) to show the
"Add to Chat" popup. ⌘L and the existing attachment/popup path are unchanged.
* feat: gutter "+" drag selects a range for multi-line comments
Re-enable Pierre's gutter line selection so dragging the "+" down the
gutter comments across a range (click still comments on a single line);
onLineSelectionEnd routes the range to the composer. Native code-text
selection still drives "Add to Chat" — Pierre only line-selects from the
gutter, and onLineSelectionEnd bails when a native text selection is
present, so a code highlight never opens the composer.
* fix: keep the range highlighted while its comment composer is open
Previously opening the composer cleared the selection, so the lines being
commented on lost their highlight. Drive the controlled selection from the
open comment draft's range so the rows stay highlighted until the composer
is closed.
* fix: address PR review — paginate comments, fall back for outdated ones
- list_review_comments now pages through all PR review comments (bounded by
_MAX_REVIEW_COMMENT_PAGES) instead of returning only the first 100, so older
comments still show in the dropdown.
- Surface GitHub's outdated flag (position == null) as is_outdated; opening such
a comment (or one whose line isn't in the diff) now opens it on GitHub instead
of silently rendering nothing, plus a timeout fallback if the annotation never
mounts (e.g. collapsed context).
2026-06-23 16:01:46 -07:00
|
|
|
{popup && !commentDraftRange && (
|
2026-06-23 12:27:51 -07:00
|
|
|
<AddToChatPopup
|
|
|
|
|
x={popup.x}
|
|
|
|
|
y={popup.y}
|
|
|
|
|
onAdd={addPopupToChat}
|
|
|
|
|
onDismiss={() => setPopup(null)}
|
|
|
|
|
/>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
function AddToChatPopup({
|
|
|
|
|
x,
|
|
|
|
|
y,
|
|
|
|
|
onAdd,
|
|
|
|
|
onDismiss,
|
|
|
|
|
}: {
|
|
|
|
|
x: number
|
|
|
|
|
y: number
|
|
|
|
|
onAdd: () => void
|
|
|
|
|
onDismiss: () => void
|
|
|
|
|
}) {
|
|
|
|
|
// Positioned fixed at the pointer-release point so it escapes the diff's
|
|
|
|
|
// overflow clipping. Dismiss on Escape, scroll, or any outside pointer-down.
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
const onKeyDown = (event: KeyboardEvent) => {
|
|
|
|
|
if (event.key === "Escape") onDismiss()
|
|
|
|
|
}
|
|
|
|
|
const onPointerDown = (event: PointerEvent) => {
|
|
|
|
|
const target = event.target
|
|
|
|
|
if (target instanceof Element && target.closest("[data-add-to-chat]"))
|
|
|
|
|
return
|
|
|
|
|
onDismiss()
|
|
|
|
|
}
|
|
|
|
|
window.addEventListener("keydown", onKeyDown)
|
|
|
|
|
window.addEventListener("pointerdown", onPointerDown)
|
|
|
|
|
// Capture so it also catches scrolls from the diff scroll container.
|
|
|
|
|
window.addEventListener("scroll", onDismiss, true)
|
|
|
|
|
return () => {
|
|
|
|
|
window.removeEventListener("keydown", onKeyDown)
|
|
|
|
|
window.removeEventListener("pointerdown", onPointerDown)
|
|
|
|
|
window.removeEventListener("scroll", onDismiss, true)
|
|
|
|
|
}
|
|
|
|
|
}, [onDismiss])
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div
|
|
|
|
|
data-add-to-chat
|
|
|
|
|
style={{ position: "fixed", top: y, left: x }}
|
|
|
|
|
className="z-50 -translate-y-[calc(100%+4px)] font-sans"
|
|
|
|
|
>
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={onAdd}
|
|
|
|
|
className="inline-flex items-center gap-1.5 rounded-md border border-border bg-popover px-2 py-1 text-[11px] font-medium text-popover-foreground shadow-md hover:bg-muted"
|
|
|
|
|
>
|
|
|
|
|
Add to Chat
|
|
|
|
|
<kbd className="rounded border border-border px-1 text-[10px] text-muted-foreground">
|
|
|
|
|
⌘L
|
|
|
|
|
</kbd>
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
feat: inline PR comments in the reviews UI (#1600)
* feat: inline PR comments in the reviews UI
Click the diff gutter "+" on a line to open an inline comment composer
(rendered like the finding card via a Pierre annotation); submitting
posts a real inline PR review comment as the signed-in user through a
new POST /reviews/{owner}/{repo}/{number}/comments. The "+" press-drag →
"Add to Chat" selection path is unchanged.
* feat: GitHub-parity comment box, PR comments dropdown, collapse nav
- Comment composer now mirrors GitHub's box: Write/Preview tabs (markdown
rendered via the existing Markdown component) and a markdown toolbar
(heading, bold, italic, quote, code, link, bulleted/numbered/task list).
- Surface other people's inline PR comments in a Devin-style dropdown in the
review header (search + link to the thread on GitHub). New
GET /reviews/{owner}/{repo}/{number}/comments lists them and flags the
reviewer's own (marker-bearing) comments so they're filtered out.
- Collapse the global nav by default on a review detail page, restoring the
prior preference on leave.
* feat: bigger comment-toolbar icons; open dropdown comments inline
- Enlarge the markdown toolbar glyphs (Phosphor) in the comment composer —
they were rendering at 10px.
- Clicking a comment in the PR comments dropdown now opens it inline in the
diff as a read-only finding-style card (InlineComment), scrolling its line
into view, instead of navigating to GitHub. Falls back to GitHub when the
comment's file/line isn't in the current diff.
* fix: drive "Add to Chat" from native text selection
The gutter "+" is now comment-only; wiring its click to the composer
conflicted with its old double-duty as the drag-to-select handle, which
broke selection → "Add to Chat". Switch to Devin's model: disable Pierre's
interactive line selection and instead map a native text highlight in the
diff to a line range (via the data-line / data-line-type attributes Pierre
stamps on each line, read from the diff's open shadow root) to show the
"Add to Chat" popup. ⌘L and the existing attachment/popup path are unchanged.
* feat: gutter "+" drag selects a range for multi-line comments
Re-enable Pierre's gutter line selection so dragging the "+" down the
gutter comments across a range (click still comments on a single line);
onLineSelectionEnd routes the range to the composer. Native code-text
selection still drives "Add to Chat" — Pierre only line-selects from the
gutter, and onLineSelectionEnd bails when a native text selection is
present, so a code highlight never opens the composer.
* fix: keep the range highlighted while its comment composer is open
Previously opening the composer cleared the selection, so the lines being
commented on lost their highlight. Drive the controlled selection from the
open comment draft's range so the rows stay highlighted until the composer
is closed.
* fix: address PR review — paginate comments, fall back for outdated ones
- list_review_comments now pages through all PR review comments (bounded by
_MAX_REVIEW_COMMENT_PAGES) instead of returning only the first 100, so older
comments still show in the dropdown.
- Surface GitHub's outdated flag (position == null) as is_outdated; opening such
a comment (or one whose line isn't in the diff) now opens it on GitHub instead
of silently rendering nothing, plus a timeout fallback if the annotation never
mounts (e.g. collapsed context).
2026-06-23 16:01:46 -07:00
|
|
|
type MarkdownAction =
|
|
|
|
|
| "heading"
|
|
|
|
|
| "bold"
|
|
|
|
|
| "italic"
|
|
|
|
|
| "quote"
|
|
|
|
|
| "code"
|
|
|
|
|
| "link"
|
|
|
|
|
| "ul"
|
|
|
|
|
| "ol"
|
|
|
|
|
| "task"
|
|
|
|
|
|
|
|
|
|
interface EditState {
|
|
|
|
|
value: string
|
|
|
|
|
start: number
|
|
|
|
|
end: number
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Wrap the current selection (or a placeholder when empty) with a marker, e.g.
|
|
|
|
|
// **bold**. Returns the new value and the selection to restore.
|
|
|
|
|
function wrapSelection(
|
|
|
|
|
state: EditState,
|
|
|
|
|
marker: string,
|
|
|
|
|
placeholder: string
|
|
|
|
|
): EditState {
|
|
|
|
|
const selected = state.value.slice(state.start, state.end) || placeholder
|
|
|
|
|
const value =
|
|
|
|
|
state.value.slice(0, state.start) +
|
|
|
|
|
marker +
|
|
|
|
|
selected +
|
|
|
|
|
marker +
|
|
|
|
|
state.value.slice(state.end)
|
|
|
|
|
const start = state.start + marker.length
|
|
|
|
|
return { value, start, end: start + selected.length }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Prefix each line touched by the selection, e.g. "> " for quotes or "1. " for
|
|
|
|
|
// ordered lists (prefix is computed per line so numbering increments).
|
|
|
|
|
function prefixLines(
|
|
|
|
|
state: EditState,
|
|
|
|
|
prefix: (index: number) => string
|
|
|
|
|
): EditState {
|
|
|
|
|
const lineStart = state.value.lastIndexOf("\n", state.start - 1) + 1
|
|
|
|
|
const block = state.value.slice(lineStart, state.end)
|
|
|
|
|
const prefixed = block
|
|
|
|
|
.split("\n")
|
|
|
|
|
.map((line, index) => prefix(index) + line)
|
|
|
|
|
.join("\n")
|
|
|
|
|
const value =
|
|
|
|
|
state.value.slice(0, lineStart) + prefixed + state.value.slice(state.end)
|
|
|
|
|
return { value, start: lineStart, end: lineStart + prefixed.length }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function applyMarkdownAction(
|
|
|
|
|
state: EditState,
|
|
|
|
|
action: MarkdownAction
|
|
|
|
|
): EditState {
|
|
|
|
|
switch (action) {
|
|
|
|
|
case "bold":
|
|
|
|
|
return wrapSelection(state, "**", "bold text")
|
|
|
|
|
case "italic":
|
|
|
|
|
return wrapSelection(state, "_", "italic text")
|
|
|
|
|
case "code":
|
|
|
|
|
return wrapSelection(state, "`", "code")
|
|
|
|
|
case "heading":
|
|
|
|
|
return prefixLines(state, () => "### ")
|
|
|
|
|
case "quote":
|
|
|
|
|
return prefixLines(state, () => "> ")
|
|
|
|
|
case "ul":
|
|
|
|
|
return prefixLines(state, () => "- ")
|
|
|
|
|
case "ol":
|
|
|
|
|
return prefixLines(state, (index) => `${index + 1}. `)
|
|
|
|
|
case "task":
|
|
|
|
|
return prefixLines(state, () => "- [ ] ")
|
|
|
|
|
case "link": {
|
|
|
|
|
const text = state.value.slice(state.start, state.end) || "text"
|
|
|
|
|
const inserted = `[${text}](url)`
|
|
|
|
|
const value =
|
|
|
|
|
state.value.slice(0, state.start) +
|
|
|
|
|
inserted +
|
|
|
|
|
state.value.slice(state.end)
|
|
|
|
|
const urlStart = state.start + text.length + 3
|
|
|
|
|
return { value, start: urlStart, end: urlStart + 3 }
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface ToolbarItem {
|
|
|
|
|
action: MarkdownAction
|
|
|
|
|
label: string
|
|
|
|
|
Icon: Icon
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Grouped to match GitHub's comment toolbar (format group, then list group).
|
|
|
|
|
const MARKDOWN_TOOLBAR: ReadonlyArray<ReadonlyArray<ToolbarItem>> = [
|
|
|
|
|
[
|
|
|
|
|
{ action: "heading", label: "Heading", Icon: TextHIcon },
|
|
|
|
|
{ action: "bold", label: "Bold", Icon: TextBIcon },
|
|
|
|
|
{ action: "italic", label: "Italic", Icon: TextItalicIcon },
|
|
|
|
|
{ action: "quote", label: "Quote", Icon: QuotesIcon },
|
|
|
|
|
{ action: "code", label: "Code", Icon: CodeIcon },
|
|
|
|
|
{ action: "link", label: "Link", Icon: LinkIcon },
|
|
|
|
|
],
|
|
|
|
|
[
|
|
|
|
|
{ action: "ul", label: "Bulleted list", Icon: ListBulletsIcon },
|
|
|
|
|
{ action: "ol", label: "Numbered list", Icon: ListNumbersIcon },
|
|
|
|
|
{ action: "task", label: "Task list", Icon: ListChecksIcon },
|
|
|
|
|
],
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
// The inline comment composer, opened by clicking the gutter "+" on a line.
|
|
|
|
|
// Rendered through the same Pierre annotation portal as InlineFinding, so it sits
|
|
|
|
|
// in place at the line. Mirrors GitHub's stock comment box (Write/Preview tabs +
|
|
|
|
|
// markdown toolbar); submitting posts a real PR review comment as the user.
|
|
|
|
|
function CommentComposer({
|
|
|
|
|
owner,
|
|
|
|
|
repo,
|
|
|
|
|
prNumber,
|
|
|
|
|
path,
|
|
|
|
|
range,
|
|
|
|
|
onClose,
|
|
|
|
|
}: {
|
|
|
|
|
owner: string
|
|
|
|
|
repo: string
|
|
|
|
|
prNumber: number
|
|
|
|
|
path: string
|
|
|
|
|
range: SelectedLineRange
|
|
|
|
|
onClose: () => void
|
|
|
|
|
}) {
|
|
|
|
|
const [value, setValue] = useState("")
|
|
|
|
|
const [mode, setMode] = useState<"write" | "preview">("write")
|
|
|
|
|
const textareaRef = useRef<HTMLTextAreaElement | null>(null)
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
textareaRef.current?.focus()
|
|
|
|
|
}, [])
|
|
|
|
|
const mutation = useMutation({
|
|
|
|
|
mutationFn: (body: string) =>
|
|
|
|
|
api.createReviewComment(
|
|
|
|
|
owner,
|
|
|
|
|
repo,
|
|
|
|
|
prNumber,
|
|
|
|
|
buildCommentPayload(path, range, body)
|
|
|
|
|
),
|
|
|
|
|
})
|
|
|
|
|
const submit = () => {
|
|
|
|
|
const body = value.trim()
|
|
|
|
|
if (!body || mutation.isPending) return
|
|
|
|
|
mutation.mutate(body)
|
|
|
|
|
}
|
|
|
|
|
// Apply a toolbar action to the live textarea selection, then restore the
|
|
|
|
|
// caret/selection on the next frame (after the controlled value re-renders).
|
|
|
|
|
const applyAction = (action: MarkdownAction) => {
|
|
|
|
|
const textarea = textareaRef.current
|
|
|
|
|
if (!textarea) return
|
|
|
|
|
const next = applyMarkdownAction(
|
|
|
|
|
{ value, start: textarea.selectionStart, end: textarea.selectionEnd },
|
|
|
|
|
action
|
|
|
|
|
)
|
|
|
|
|
setValue(next.value)
|
|
|
|
|
requestAnimationFrame(() => {
|
|
|
|
|
textarea.focus()
|
|
|
|
|
textarea.setSelectionRange(next.start, next.end)
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
const posted = mutation.data
|
|
|
|
|
const tabClass = (active: boolean) =>
|
|
|
|
|
cn(
|
|
|
|
|
"rounded px-2 py-0.5 text-[11px]",
|
|
|
|
|
active
|
|
|
|
|
? "bg-[var(--ui-panel-2)] font-medium text-foreground"
|
|
|
|
|
: "text-muted-foreground hover:text-foreground"
|
|
|
|
|
)
|
|
|
|
|
return (
|
|
|
|
|
<div className="px-2 py-1 font-sans">
|
|
|
|
|
<div className="overflow-hidden rounded-md border border-[var(--ui-border)] bg-[var(--ui-surface)]">
|
|
|
|
|
<div className="flex items-center gap-1.5 border-b border-[var(--ui-border)] px-2 py-1 text-[11px]">
|
|
|
|
|
<ChatCircleIcon className="size-3 text-muted-foreground" />
|
|
|
|
|
<span className="font-medium">
|
|
|
|
|
Add a comment on line {commentRangeLabel(range)}
|
|
|
|
|
</span>
|
|
|
|
|
<IconButton
|
|
|
|
|
type="button"
|
|
|
|
|
variant="ghost"
|
|
|
|
|
size="icon-xs"
|
|
|
|
|
aria-label="Close comment"
|
|
|
|
|
className="ml-auto"
|
|
|
|
|
onClick={onClose}
|
|
|
|
|
>
|
|
|
|
|
<XIcon />
|
|
|
|
|
</IconButton>
|
|
|
|
|
</div>
|
|
|
|
|
{posted ? (
|
|
|
|
|
<div className="flex items-center gap-2 px-3 py-2.5 text-[11px] text-muted-foreground">
|
|
|
|
|
<CheckCircleIcon className="size-3.5 text-emerald-500" />
|
|
|
|
|
Comment posted
|
|
|
|
|
<a
|
|
|
|
|
href={posted.html_url}
|
|
|
|
|
target="_blank"
|
|
|
|
|
rel="noreferrer"
|
|
|
|
|
className="inline-flex items-center gap-1 text-foreground hover:underline"
|
|
|
|
|
>
|
|
|
|
|
<IoLogoGithub className="size-3" />
|
|
|
|
|
View on GitHub
|
|
|
|
|
</a>
|
|
|
|
|
</div>
|
|
|
|
|
) : (
|
|
|
|
|
<>
|
|
|
|
|
<div className="flex items-center gap-1 border-b border-[var(--ui-border)] px-1.5 py-1">
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={() => setMode("write")}
|
|
|
|
|
aria-selected={mode === "write"}
|
|
|
|
|
className={tabClass(mode === "write")}
|
|
|
|
|
>
|
|
|
|
|
Write
|
|
|
|
|
</button>
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={() => setMode("preview")}
|
|
|
|
|
aria-selected={mode === "preview"}
|
|
|
|
|
className={tabClass(mode === "preview")}
|
|
|
|
|
>
|
|
|
|
|
Preview
|
|
|
|
|
</button>
|
|
|
|
|
{mode === "write" && (
|
|
|
|
|
<div className="ml-auto flex items-center gap-0.5">
|
|
|
|
|
{MARKDOWN_TOOLBAR.map((group, groupIndex) => (
|
|
|
|
|
<Fragment key={group[0]?.action ?? groupIndex}>
|
|
|
|
|
{groupIndex > 0 && (
|
|
|
|
|
<span className="mx-0.5 h-4 w-px bg-[var(--ui-border)]" />
|
|
|
|
|
)}
|
|
|
|
|
{group.map(({ action, label, Icon }) => (
|
|
|
|
|
<IconButton
|
|
|
|
|
key={action}
|
|
|
|
|
type="button"
|
|
|
|
|
variant="ghost"
|
|
|
|
|
size="icon-sm"
|
|
|
|
|
aria-label={label}
|
|
|
|
|
title={label}
|
|
|
|
|
onMouseDown={(event) => event.preventDefault()}
|
|
|
|
|
onClick={() => applyAction(action)}
|
|
|
|
|
>
|
|
|
|
|
<Icon className="size-4" />
|
|
|
|
|
</IconButton>
|
|
|
|
|
))}
|
|
|
|
|
</Fragment>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
<div className="p-2">
|
|
|
|
|
{mode === "write" ? (
|
|
|
|
|
<Textarea
|
|
|
|
|
ref={textareaRef}
|
|
|
|
|
value={value}
|
|
|
|
|
onChange={(event) => setValue(event.target.value)}
|
|
|
|
|
onKeyDown={(event) => {
|
|
|
|
|
if (
|
|
|
|
|
(event.metaKey || event.ctrlKey) &&
|
|
|
|
|
event.key === "Enter"
|
|
|
|
|
) {
|
|
|
|
|
event.preventDefault()
|
|
|
|
|
submit()
|
|
|
|
|
} else if (event.key === "Escape") {
|
|
|
|
|
event.preventDefault()
|
|
|
|
|
onClose()
|
|
|
|
|
}
|
|
|
|
|
}}
|
|
|
|
|
placeholder="Leave a comment…"
|
|
|
|
|
rows={3}
|
|
|
|
|
className="resize-y text-xs"
|
|
|
|
|
/>
|
|
|
|
|
) : (
|
|
|
|
|
<div className="min-h-16 rounded-md border border-input bg-input/20 px-2 py-2 text-xs">
|
|
|
|
|
{value.trim() ? (
|
|
|
|
|
<Markdown content={value} />
|
|
|
|
|
) : (
|
|
|
|
|
<span className="text-muted-foreground">
|
|
|
|
|
Nothing to preview
|
|
|
|
|
</span>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
{mutation.isError && (
|
|
|
|
|
<p className="mt-1.5 text-[11px] text-destructive">
|
|
|
|
|
{mutation.error instanceof Error
|
|
|
|
|
? mutation.error.message
|
|
|
|
|
: "Failed to post comment"}
|
|
|
|
|
</p>
|
|
|
|
|
)}
|
|
|
|
|
<div className="mt-2 flex items-center justify-end gap-2">
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={onClose}
|
|
|
|
|
className="rounded border border-border px-2 py-1 text-[11px] text-muted-foreground hover:text-foreground"
|
|
|
|
|
>
|
|
|
|
|
Cancel
|
|
|
|
|
</button>
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={submit}
|
|
|
|
|
disabled={!value.trim() || mutation.isPending}
|
|
|
|
|
className="rounded bg-foreground px-2 py-1 text-[11px] font-medium text-background disabled:opacity-50"
|
|
|
|
|
>
|
|
|
|
|
{mutation.isPending ? "Posting…" : "Comment"}
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// An existing PR comment opened from the comments dropdown, rendered inline at
|
|
|
|
|
// its line through the same annotation portal as InlineFinding. Read-only; the
|
|
|
|
|
// node is registered so the dropdown can scroll it into view. Links to the
|
|
|
|
|
// full thread on GitHub.
|
|
|
|
|
function InlineComment({
|
|
|
|
|
comment,
|
|
|
|
|
onClose,
|
|
|
|
|
}: {
|
|
|
|
|
comment: PrReviewComment
|
|
|
|
|
onClose: () => void
|
|
|
|
|
}) {
|
|
|
|
|
const { registerAnnotation } = useExpandedFinding()
|
|
|
|
|
const sideLabel = comment.side === "LEFT" ? "L" : "R"
|
|
|
|
|
return (
|
|
|
|
|
<div
|
|
|
|
|
ref={(node) => registerAnnotation(`comment:${comment.id}`, node)}
|
|
|
|
|
className="px-2 py-1 font-sans"
|
|
|
|
|
>
|
|
|
|
|
<div className="overflow-hidden rounded-md border border-[var(--ui-border)] bg-[var(--ui-surface)]">
|
|
|
|
|
<div className="flex items-center gap-1.5 border-b border-[var(--ui-border)] px-2 py-1 text-[11px]">
|
|
|
|
|
{comment.author_avatar_url ? (
|
|
|
|
|
<img
|
|
|
|
|
src={comment.author_avatar_url}
|
|
|
|
|
alt=""
|
|
|
|
|
className="size-4 shrink-0 rounded-full"
|
|
|
|
|
/>
|
|
|
|
|
) : (
|
|
|
|
|
<span className="size-4 shrink-0 rounded-full bg-muted" />
|
|
|
|
|
)}
|
|
|
|
|
<span className="font-medium">{comment.author}</span>
|
|
|
|
|
{comment.line !== null && (
|
|
|
|
|
<span className="font-mono text-muted-foreground">
|
|
|
|
|
{sideLabel}
|
|
|
|
|
{comment.line}
|
|
|
|
|
</span>
|
|
|
|
|
)}
|
|
|
|
|
<div className="ml-auto flex items-center gap-0.5">
|
|
|
|
|
<a
|
|
|
|
|
href={comment.html_url}
|
|
|
|
|
target="_blank"
|
|
|
|
|
rel="noreferrer"
|
|
|
|
|
aria-label="View on GitHub"
|
|
|
|
|
title="View on GitHub"
|
|
|
|
|
className="inline-flex size-5 items-center justify-center rounded-sm text-muted-foreground hover:text-foreground"
|
|
|
|
|
>
|
|
|
|
|
<IoLogoGithub className="size-3" />
|
|
|
|
|
</a>
|
|
|
|
|
<IconButton
|
|
|
|
|
type="button"
|
|
|
|
|
variant="ghost"
|
|
|
|
|
size="icon-xs"
|
|
|
|
|
aria-label="Close comment"
|
|
|
|
|
onClick={onClose}
|
|
|
|
|
>
|
|
|
|
|
<XIcon />
|
|
|
|
|
</IconButton>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="px-3 py-2.5 text-xs text-muted-foreground">
|
|
|
|
|
<Markdown content={comment.body} />
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-23 12:27:51 -07:00
|
|
|
// The finding rendered inline in the diff (via Pierre's annotation portal). A
|
|
|
|
|
// collapsed header sits at the line; clicking it expands the full details in
|
|
|
|
|
// place. Expand state is shared through context so it survives the annotation
|
|
|
|
|
// remounting as rows window in/out, and so the side panel can drive it.
|
|
|
|
|
function InlineFinding({ finding }: { finding: ReviewFinding }) {
|
|
|
|
|
const { expandedId, reviewUrl, toggle, registerAnnotation } =
|
|
|
|
|
useExpandedFinding()
|
|
|
|
|
const expanded = expandedId === finding.id
|
|
|
|
|
const style = GROUP_STYLES[finding.group]
|
|
|
|
|
const Icon = style.Icon
|
|
|
|
|
return (
|
|
|
|
|
<div
|
|
|
|
|
ref={(node) => registerAnnotation(finding.id, node)}
|
|
|
|
|
className="px-2 py-1 font-sans"
|
|
|
|
|
>
|
|
|
|
|
<div className="overflow-hidden rounded-md border border-[var(--ui-border)] bg-[var(--ui-surface)]">
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={() => toggle(finding)}
|
|
|
|
|
aria-expanded={expanded}
|
|
|
|
|
aria-label={`${expanded ? "Collapse" : "Expand"} finding: ${finding.title}`}
|
|
|
|
|
className="flex w-full items-center gap-1.5 px-2 py-1 text-left text-[11px]"
|
|
|
|
|
>
|
|
|
|
|
<Icon className={cn("size-3 shrink-0", style.className)} />
|
|
|
|
|
<span className={cn("font-medium", style.className)}>
|
|
|
|
|
{style.label}
|
|
|
|
|
</span>
|
|
|
|
|
<span className="min-w-0 flex-1 truncate text-foreground">
|
|
|
|
|
{finding.title}
|
|
|
|
|
</span>
|
|
|
|
|
{finding.outdated && <Badgeish>Outdated</Badgeish>}
|
|
|
|
|
{finding.status !== "open" && <Badgeish>{finding.status}</Badgeish>}
|
|
|
|
|
<CaretDownIcon
|
|
|
|
|
className={cn(
|
|
|
|
|
"size-3 shrink-0 text-muted-foreground transition-transform",
|
|
|
|
|
!expanded && "-rotate-90"
|
|
|
|
|
)}
|
|
|
|
|
/>
|
|
|
|
|
</button>
|
|
|
|
|
{expanded && <FindingDetails finding={finding} reviewUrl={reviewUrl} />}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// The expandable body + actions of a finding, shared by the inline diff
|
|
|
|
|
// annotation and the side-panel row (non-anchored findings).
|
|
|
|
|
function FindingDetails({
|
|
|
|
|
finding,
|
|
|
|
|
reviewUrl,
|
|
|
|
|
}: {
|
|
|
|
|
finding: ReviewFinding
|
|
|
|
|
reviewUrl: string
|
|
|
|
|
}) {
|
|
|
|
|
const [copied, setCopied] = useState(false)
|
|
|
|
|
const githubUrl =
|
|
|
|
|
finding.github_review_comment_id !== null
|
|
|
|
|
? `${reviewUrl}#discussion_r${finding.github_review_comment_id}`
|
|
|
|
|
: null
|
|
|
|
|
|
|
|
|
|
const copy = () => {
|
|
|
|
|
void navigator.clipboard
|
|
|
|
|
.writeText(findingClipboardText(finding))
|
|
|
|
|
.then(() => {
|
|
|
|
|
setCopied(true)
|
|
|
|
|
window.setTimeout(() => setCopied(false), 1500)
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="border-t border-[var(--ui-border)] px-3 py-2.5 font-sans">
|
|
|
|
|
<div className="text-xs text-muted-foreground">
|
|
|
|
|
<Markdown content={finding.description} />
|
|
|
|
|
</div>
|
|
|
|
|
{finding.resolution_note && (
|
|
|
|
|
<p className="mt-2 text-[11px] text-muted-foreground">
|
|
|
|
|
Resolution: {finding.resolution_note}
|
|
|
|
|
</p>
|
|
|
|
|
)}
|
|
|
|
|
<div className="mt-2.5 flex items-center gap-2">
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={copy}
|
|
|
|
|
className="inline-flex items-center gap-1.5 rounded border border-border px-2 py-1 text-[11px] text-muted-foreground hover:text-foreground"
|
|
|
|
|
>
|
|
|
|
|
<CopyIcon className="size-3" />
|
|
|
|
|
{copied ? "Copied" : "Copy"}
|
|
|
|
|
</button>
|
|
|
|
|
{githubUrl && (
|
|
|
|
|
<a
|
|
|
|
|
href={githubUrl}
|
|
|
|
|
target="_blank"
|
|
|
|
|
rel="noreferrer"
|
|
|
|
|
className="inline-flex items-center gap-1.5 rounded border border-border px-2 py-1 text-[11px] text-muted-foreground hover:text-foreground"
|
|
|
|
|
>
|
|
|
|
|
<IoLogoGithub className="size-3" />
|
|
|
|
|
View on GitHub
|
|
|
|
|
</a>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function Badgeish({ children }: { children: React.ReactNode }) {
|
|
|
|
|
return (
|
|
|
|
|
<span className="rounded border border-border px-1.5 py-0.5 text-[10px] text-muted-foreground capitalize">
|
|
|
|
|
{children}
|
|
|
|
|
</span>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const REVIEW_PANEL_STORAGE_WIDTH = "open-swe.review-panel.width"
|
|
|
|
|
const REVIEW_PANEL_DEFAULT_WIDTH = 420
|
|
|
|
|
const REVIEW_PANEL_MIN_WIDTH = 360
|
|
|
|
|
// Keep at least this much room for the PR content column so the panel can grow
|
|
|
|
|
// wide without squeezing the diff/description below a usable width.
|
|
|
|
|
const REVIEW_PANEL_MIN_MAIN_WIDTH = 480
|
|
|
|
|
|
|
|
|
|
function reviewPanelMaxWidth(availableWidth?: number): number {
|
|
|
|
|
if (typeof window === "undefined") return REVIEW_PANEL_DEFAULT_WIDTH
|
|
|
|
|
const available = availableWidth ?? window.innerWidth
|
|
|
|
|
return Math.max(
|
|
|
|
|
REVIEW_PANEL_MIN_WIDTH,
|
|
|
|
|
available - REVIEW_PANEL_MIN_MAIN_WIDTH
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function clampReviewPanelWidth(width: number, availableWidth?: number): number {
|
|
|
|
|
return Math.min(
|
|
|
|
|
reviewPanelMaxWidth(availableWidth),
|
|
|
|
|
Math.max(REVIEW_PANEL_MIN_WIDTH, width)
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function readStoredReviewPanelWidth(): number {
|
|
|
|
|
if (typeof window === "undefined") return REVIEW_PANEL_DEFAULT_WIDTH
|
|
|
|
|
const raw = window.localStorage.getItem(REVIEW_PANEL_STORAGE_WIDTH)
|
|
|
|
|
const parsed = raw ? Number(raw) : NaN
|
|
|
|
|
if (!Number.isFinite(parsed)) return REVIEW_PANEL_DEFAULT_WIDTH
|
|
|
|
|
return clampReviewPanelWidth(parsed)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function ReviewPanelResizeHandle({
|
|
|
|
|
width,
|
|
|
|
|
onResize,
|
|
|
|
|
}: {
|
|
|
|
|
width: number
|
|
|
|
|
onResize: (next: number) => void
|
|
|
|
|
}) {
|
|
|
|
|
const startRef = useRef<{ x: number; width: number } | null>(null)
|
|
|
|
|
const [dragging, setDragging] = useState(false)
|
|
|
|
|
|
|
|
|
|
const onPointerDown = (e: React.PointerEvent<HTMLDivElement>) => {
|
|
|
|
|
e.preventDefault()
|
|
|
|
|
startRef.current = { x: e.clientX, width }
|
|
|
|
|
setDragging(true)
|
|
|
|
|
e.currentTarget.setPointerCapture(e.pointerId)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const onPointerMove = (e: React.PointerEvent<HTMLDivElement>) => {
|
|
|
|
|
if (!startRef.current) return
|
|
|
|
|
onResize(startRef.current.width - (e.clientX - startRef.current.x))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const onPointerUp = (e: React.PointerEvent<HTMLDivElement>) => {
|
|
|
|
|
startRef.current = null
|
|
|
|
|
setDragging(false)
|
|
|
|
|
if (e.currentTarget.hasPointerCapture(e.pointerId)) {
|
|
|
|
|
e.currentTarget.releasePointerCapture(e.pointerId)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (!dragging) return
|
|
|
|
|
const prev = document.body.style.cursor
|
|
|
|
|
document.body.style.cursor = "col-resize"
|
|
|
|
|
return () => {
|
|
|
|
|
document.body.style.cursor = prev
|
|
|
|
|
}
|
|
|
|
|
}, [dragging])
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div
|
|
|
|
|
role="separator"
|
|
|
|
|
aria-orientation="vertical"
|
|
|
|
|
onPointerDown={onPointerDown}
|
|
|
|
|
onPointerMove={onPointerMove}
|
|
|
|
|
onPointerUp={onPointerUp}
|
|
|
|
|
onPointerCancel={onPointerUp}
|
|
|
|
|
className={cn(
|
|
|
|
|
"absolute inset-y-0 left-0 z-20 w-1 cursor-col-resize touch-none select-none",
|
|
|
|
|
"after:absolute after:inset-y-0 after:left-0 after:w-px after:bg-transparent after:transition-colors",
|
|
|
|
|
"hover:after:bg-border",
|
|
|
|
|
dragging && "after:bg-border"
|
|
|
|
|
)}
|
|
|
|
|
/>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function SidePanel({
|
|
|
|
|
detail,
|
|
|
|
|
tab,
|
|
|
|
|
onTabChange,
|
|
|
|
|
read,
|
|
|
|
|
expandedId,
|
|
|
|
|
onMarkAllRead,
|
|
|
|
|
onFindingClick,
|
|
|
|
|
}: {
|
|
|
|
|
detail: ReviewDetail
|
|
|
|
|
tab: SideTab
|
|
|
|
|
onTabChange: (tab: SideTab) => void
|
|
|
|
|
read: Set<string>
|
|
|
|
|
expandedId: string | null
|
|
|
|
|
onMarkAllRead: () => void
|
|
|
|
|
onFindingClick: (finding: ReviewFinding) => void
|
|
|
|
|
}) {
|
|
|
|
|
const qc = useQueryClient()
|
|
|
|
|
const reReview = useMutation({
|
|
|
|
|
mutationFn: () => api.reReview(detail.owner, detail.repo, detail.number),
|
|
|
|
|
onSuccess: () => {
|
|
|
|
|
void qc.invalidateQueries({
|
|
|
|
|
queryKey: ["review", detail.owner, detail.repo, detail.number],
|
|
|
|
|
})
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const panelRef = useRef<HTMLDivElement>(null)
|
|
|
|
|
const [width, setWidthState] = useState(() => readStoredReviewPanelWidth())
|
|
|
|
|
const setWidth = useCallback((next: number) => {
|
|
|
|
|
const available = panelRef.current?.parentElement?.clientWidth
|
|
|
|
|
const clamped = clampReviewPanelWidth(next, available)
|
|
|
|
|
setWidthState(clamped)
|
|
|
|
|
if (typeof window !== "undefined") {
|
|
|
|
|
window.localStorage.setItem(REVIEW_PANEL_STORAGE_WIDTH, String(clamped))
|
|
|
|
|
}
|
|
|
|
|
}, [])
|
|
|
|
|
|
|
|
|
|
// Re-clamp against the real container width on mount and on window resize so
|
|
|
|
|
// the panel can never squeeze the PR content below its minimum.
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (typeof window === "undefined") return
|
|
|
|
|
const reclamp = () => setWidth(width)
|
|
|
|
|
reclamp()
|
|
|
|
|
window.addEventListener("resize", reclamp)
|
|
|
|
|
return () => window.removeEventListener("resize", reclamp)
|
|
|
|
|
}, [setWidth, width])
|
|
|
|
|
|
|
|
|
|
const bugs = detail.findings.filter((f) => f.group === "bug")
|
|
|
|
|
const flags = detail.findings.filter((f) => f.group !== "bug")
|
|
|
|
|
const openBugs = bugs.filter((f) => f.status === "open")
|
|
|
|
|
const openFlags = flags.filter((f) => f.status === "open")
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div
|
|
|
|
|
ref={panelRef}
|
|
|
|
|
style={{ width }}
|
|
|
|
|
className="relative hidden h-full shrink-0 xl:flex"
|
|
|
|
|
>
|
|
|
|
|
<ReviewPanelResizeHandle width={width} onResize={setWidth} />
|
|
|
|
|
<aside className="flex h-full w-full flex-col overflow-y-auto border-l border-border">
|
|
|
|
|
<div className="flex items-center gap-1 border-b border-border px-3 py-2">
|
|
|
|
|
{(
|
|
|
|
|
[
|
|
|
|
|
["info", "Info"],
|
|
|
|
|
["chat", "Chat"],
|
|
|
|
|
] as const
|
|
|
|
|
).map(([id, label]) => (
|
|
|
|
|
<button
|
|
|
|
|
key={id}
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={() => onTabChange(id)}
|
|
|
|
|
className={cn(
|
|
|
|
|
"rounded-md px-2.5 py-1 text-xs transition-colors",
|
|
|
|
|
tab === id
|
|
|
|
|
? "bg-muted font-medium text-foreground"
|
|
|
|
|
: "text-muted-foreground hover:bg-muted/50"
|
|
|
|
|
)}
|
|
|
|
|
>
|
|
|
|
|
{label}
|
|
|
|
|
</button>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{tab === "chat" ? (
|
|
|
|
|
<ReviewChat
|
|
|
|
|
owner={detail.owner}
|
|
|
|
|
repo={detail.repo}
|
|
|
|
|
number={detail.number}
|
|
|
|
|
/>
|
|
|
|
|
) : (
|
|
|
|
|
<div className="divide-y divide-border">
|
|
|
|
|
<section className="px-3 py-3">
|
|
|
|
|
<div className="flex items-center justify-between text-xs">
|
|
|
|
|
<span className="font-medium">
|
|
|
|
|
{detail.status === "running"
|
|
|
|
|
? "PR analysis in progress"
|
|
|
|
|
: detail.status === "error"
|
|
|
|
|
? "PR analysis failed"
|
|
|
|
|
: "PR analysis complete"}
|
|
|
|
|
</span>
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={() => reReview.mutate()}
|
|
|
|
|
disabled={reReview.isPending || detail.status === "running"}
|
|
|
|
|
className="inline-flex items-center gap-1 rounded border border-border px-1.5 py-0.5 text-[11px] text-muted-foreground hover:text-foreground disabled:opacity-50"
|
|
|
|
|
>
|
|
|
|
|
<ArrowClockwiseIcon className="size-3" />
|
|
|
|
|
Re-review
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="mt-2 space-y-1 text-[11px] text-muted-foreground">
|
|
|
|
|
<div>Reviewing commit {detail.head_sha.slice(0, 7) || "—"}</div>
|
|
|
|
|
{detail.watch && <div>Watching for new pushes</div>}
|
|
|
|
|
{reReview.error && (
|
|
|
|
|
<div className="text-destructive">
|
|
|
|
|
{reReview.error.message}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
<FindingSection
|
|
|
|
|
icon={BugBeetleIcon}
|
|
|
|
|
label={`${openBugs.length} Bug${openBugs.length === 1 ? "" : "s"}`}
|
|
|
|
|
emptyLabel="No bugs found."
|
|
|
|
|
findings={bugs}
|
|
|
|
|
read={read}
|
|
|
|
|
expandedId={expandedId}
|
|
|
|
|
reviewUrl={detail.url}
|
|
|
|
|
onFindingClick={onFindingClick}
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
<FindingSection
|
|
|
|
|
icon={FlagIcon}
|
|
|
|
|
label={`${openFlags.length} Flag${openFlags.length === 1 ? "" : "s"}`}
|
|
|
|
|
emptyLabel="No issues found."
|
|
|
|
|
findings={flags}
|
|
|
|
|
read={read}
|
|
|
|
|
expandedId={expandedId}
|
|
|
|
|
reviewUrl={detail.url}
|
|
|
|
|
onFindingClick={onFindingClick}
|
|
|
|
|
action={
|
|
|
|
|
detail.findings.length > 0 ? (
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={onMarkAllRead}
|
|
|
|
|
className="rounded border border-border px-1.5 py-0.5 text-[11px] text-muted-foreground hover:text-foreground"
|
|
|
|
|
>
|
|
|
|
|
Mark all as read
|
|
|
|
|
</button>
|
|
|
|
|
) : null
|
|
|
|
|
}
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
<ChecksSection checks={detail.checks} />
|
|
|
|
|
<PeopleSection
|
|
|
|
|
title="Reviewers"
|
|
|
|
|
people={detail.pr.requested_reviewers}
|
|
|
|
|
/>
|
|
|
|
|
<PeopleSection title="Assignees" people={detail.pr.assignees} />
|
|
|
|
|
<section className="px-3 py-3">
|
|
|
|
|
<h3 className="mb-2 text-xs font-medium">Labels</h3>
|
|
|
|
|
{detail.pr.labels.length === 0 ? (
|
|
|
|
|
<p className="text-[11px] text-muted-foreground">None</p>
|
|
|
|
|
) : (
|
|
|
|
|
<div className="flex flex-wrap gap-1">
|
|
|
|
|
{detail.pr.labels.map((label) => (
|
|
|
|
|
<span
|
|
|
|
|
key={label.name}
|
|
|
|
|
className="rounded-full border border-border px-2 py-0.5 text-[11px]"
|
|
|
|
|
>
|
|
|
|
|
{label.name}
|
|
|
|
|
</span>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</section>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</aside>
|
|
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function FindingSection({
|
|
|
|
|
icon: HeaderIcon,
|
|
|
|
|
label,
|
|
|
|
|
emptyLabel,
|
|
|
|
|
findings,
|
|
|
|
|
read,
|
|
|
|
|
expandedId,
|
|
|
|
|
reviewUrl,
|
|
|
|
|
onFindingClick,
|
|
|
|
|
action,
|
|
|
|
|
}: {
|
|
|
|
|
icon: (typeof GROUP_STYLES)["bug"]["Icon"]
|
|
|
|
|
label: string
|
|
|
|
|
emptyLabel: string
|
|
|
|
|
findings: Array<ReviewFinding>
|
|
|
|
|
read: Set<string>
|
|
|
|
|
expandedId: string | null
|
|
|
|
|
reviewUrl: string
|
|
|
|
|
onFindingClick: (finding: ReviewFinding) => void
|
|
|
|
|
action?: React.ReactNode
|
|
|
|
|
}) {
|
|
|
|
|
const [collapsed, setCollapsed] = useState(false)
|
|
|
|
|
return (
|
|
|
|
|
<section className="px-3 py-3">
|
|
|
|
|
<div className="mb-2 flex items-center justify-between text-xs">
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={() => setCollapsed((v) => !v)}
|
|
|
|
|
className="inline-flex items-center gap-1.5 font-medium"
|
|
|
|
|
>
|
|
|
|
|
<HeaderIcon className="size-3.5" />
|
|
|
|
|
{label}
|
|
|
|
|
<CaretDownIcon
|
|
|
|
|
className={cn(
|
|
|
|
|
"size-3 text-muted-foreground transition-transform",
|
|
|
|
|
collapsed && "-rotate-90"
|
|
|
|
|
)}
|
|
|
|
|
/>
|
|
|
|
|
</button>
|
|
|
|
|
{action}
|
|
|
|
|
</div>
|
|
|
|
|
{!collapsed &&
|
|
|
|
|
(findings.length === 0 ? (
|
|
|
|
|
<p className="text-[11px] text-muted-foreground">{emptyLabel}</p>
|
|
|
|
|
) : (
|
|
|
|
|
<div className="space-y-0.5">
|
|
|
|
|
{findings.map((finding) => {
|
|
|
|
|
const style = GROUP_STYLES[finding.group]
|
|
|
|
|
const Icon = style.Icon
|
|
|
|
|
const isRead = read.has(finding.id)
|
|
|
|
|
const muted = finding.status !== "open" || isRead
|
|
|
|
|
const anchored = isAnchored(finding)
|
|
|
|
|
const expanded = expandedId === finding.id && !anchored
|
|
|
|
|
return (
|
|
|
|
|
<div
|
|
|
|
|
key={finding.id}
|
|
|
|
|
className={cn(
|
|
|
|
|
"rounded-md border border-transparent transition-colors hover:border-border hover:bg-muted/40",
|
|
|
|
|
expanded && "border-border bg-muted/40",
|
|
|
|
|
muted && !expanded && "opacity-50"
|
|
|
|
|
)}
|
|
|
|
|
>
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={() => onFindingClick(finding)}
|
|
|
|
|
aria-expanded={anchored ? undefined : expanded}
|
|
|
|
|
className="block w-full px-2 py-1.5 text-left"
|
|
|
|
|
>
|
|
|
|
|
<span className="flex items-start gap-1.5 text-xs">
|
|
|
|
|
<Icon
|
|
|
|
|
className={cn(
|
|
|
|
|
"mt-0.5 size-3.5 shrink-0",
|
|
|
|
|
style.className
|
|
|
|
|
)}
|
|
|
|
|
/>
|
|
|
|
|
<span className="min-w-0 flex-1">
|
|
|
|
|
<span className="line-clamp-1 font-medium text-foreground">
|
|
|
|
|
{finding.title || finding.description}
|
|
|
|
|
</span>
|
|
|
|
|
<span className="mt-0.5 flex flex-wrap items-center gap-1.5 text-[11px] text-muted-foreground">
|
|
|
|
|
<span className={style.className}>{style.label}</span>
|
|
|
|
|
<span className="truncate font-mono">
|
|
|
|
|
{findingAnchorLabel(finding)}
|
|
|
|
|
</span>
|
|
|
|
|
{finding.outdated && <Badgeish>Outdated</Badgeish>}
|
|
|
|
|
{finding.status !== "open" && (
|
|
|
|
|
<Badgeish>{finding.status}</Badgeish>
|
|
|
|
|
)}
|
|
|
|
|
{isRead && finding.status === "open" && (
|
|
|
|
|
<span>• Read</span>
|
|
|
|
|
)}
|
|
|
|
|
</span>
|
|
|
|
|
</span>
|
|
|
|
|
{!anchored && (
|
|
|
|
|
<CaretDownIcon
|
|
|
|
|
className={cn(
|
|
|
|
|
"mt-0.5 size-3 shrink-0 text-muted-foreground transition-transform",
|
|
|
|
|
!expanded && "-rotate-90"
|
|
|
|
|
)}
|
|
|
|
|
/>
|
|
|
|
|
)}
|
|
|
|
|
</span>
|
|
|
|
|
</button>
|
|
|
|
|
{expanded && (
|
|
|
|
|
<FindingDetails finding={finding} reviewUrl={reviewUrl} />
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
})}
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
</section>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function ChecksSection({ checks }: { checks: Array<ReviewCheckRun> }) {
|
|
|
|
|
return (
|
|
|
|
|
<section className="px-3 py-3">
|
|
|
|
|
<h3 className="mb-2 text-xs font-medium">Checks</h3>
|
|
|
|
|
{checks.length === 0 ? (
|
|
|
|
|
<p className="text-[11px] text-muted-foreground">No checks reported.</p>
|
|
|
|
|
) : (
|
|
|
|
|
<div className="max-h-56 space-y-1 overflow-y-auto">
|
|
|
|
|
{checks.map((check, index) =>
|
|
|
|
|
check.url ? (
|
|
|
|
|
<a
|
|
|
|
|
key={`${check.name}-${index}`}
|
|
|
|
|
href={check.url}
|
|
|
|
|
target="_blank"
|
|
|
|
|
rel="noreferrer"
|
|
|
|
|
className="flex items-center gap-1.5 text-[11px] text-muted-foreground hover:text-foreground"
|
|
|
|
|
>
|
|
|
|
|
<CheckStatusIcon check={check} />
|
|
|
|
|
<span className="truncate">{check.name}</span>
|
|
|
|
|
</a>
|
|
|
|
|
) : (
|
|
|
|
|
<span
|
|
|
|
|
key={`${check.name}-${index}`}
|
|
|
|
|
className="flex items-center gap-1.5 text-[11px] text-muted-foreground"
|
|
|
|
|
>
|
|
|
|
|
<CheckStatusIcon check={check} />
|
|
|
|
|
<span className="truncate">{check.name}</span>
|
|
|
|
|
</span>
|
|
|
|
|
)
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</section>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function CheckStatusIcon({ check }: { check: ReviewCheckRun }) {
|
|
|
|
|
if (check.status !== "completed") {
|
|
|
|
|
return (
|
|
|
|
|
<CircleIcon className="size-3.5 shrink-0 animate-pulse text-amber-500" />
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
if (check.conclusion === "success" || check.conclusion === "neutral") {
|
|
|
|
|
return <CheckCircleIcon className="size-3.5 shrink-0 text-emerald-500" />
|
|
|
|
|
}
|
|
|
|
|
if (check.conclusion === "skipped") {
|
|
|
|
|
return <CircleIcon className="size-3.5 shrink-0 text-muted-foreground" />
|
|
|
|
|
}
|
|
|
|
|
return <XCircleIcon className="size-3.5 shrink-0 text-red-500" />
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function PeopleSection({
|
|
|
|
|
title,
|
|
|
|
|
people,
|
|
|
|
|
}: {
|
|
|
|
|
title: string
|
|
|
|
|
people: Array<ReviewUserRef>
|
|
|
|
|
}) {
|
|
|
|
|
return (
|
|
|
|
|
<section className="px-3 py-3">
|
|
|
|
|
<h3 className="mb-2 text-xs font-medium">{title}</h3>
|
|
|
|
|
{people.length === 0 ? (
|
|
|
|
|
<p className="text-[11px] text-muted-foreground">None</p>
|
|
|
|
|
) : (
|
|
|
|
|
<div className="space-y-1">
|
|
|
|
|
{people.map((person) => (
|
|
|
|
|
<div
|
|
|
|
|
key={person.login}
|
|
|
|
|
className="flex items-center gap-2 text-[11px]"
|
|
|
|
|
>
|
|
|
|
|
{person.avatar_url ? (
|
|
|
|
|
<img
|
|
|
|
|
src={person.avatar_url}
|
|
|
|
|
alt=""
|
|
|
|
|
className="size-4 rounded-full"
|
|
|
|
|
/>
|
|
|
|
|
) : (
|
|
|
|
|
<span className="size-4 rounded-full bg-muted" />
|
|
|
|
|
)}
|
|
|
|
|
{person.login}
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</section>
|
|
|
|
|
)
|
|
|
|
|
}
|