feat: split view, add-to-chat, virtualization + scroll/grouping perf (#1574)
* feat: reviews page split view, add-to-chat, virtualization + perf
- Virtualize the diff (Pierre Virtualizer + worker pool), mirroring the agent
chat panel, so large PRs window rows instead of materializing every line.
- Split chat and diff into independent scroll containers and make chat
auto-scroll fully contained, so typing/streaming no longer moves the diff.
- Memoize FileDiffCard with stable callbacks so focusing a finding re-renders
only the affected card.
- Add a persisted unified/split diff toggle.
- Add highlight-to-chat: select lines (drag / shift-click) + gutter "+" to drop
a file:line snippet into the chat composer.
- Rebuild sidebar group rows: whole card scrolls to the group (incl. the
expanded explanation), Read explanation stays a separate toggle, memoized.
* feat: add-to-chat uses attachment pills + selection popup / ⌘L
Replace the raw-snippet injection with a Cursor-style flow:
- Selecting lines shows a floating "Add to Chat ⌘L" popup at the pointer; ⌘L
adds the current selection without it. Removes the auto-adding gutter "+".
- "Add to chat" now creates a removable attachment pill in the composer (and a
pill in the sent message bubble) instead of pasting raw text. The code is
still serialized into the message content so the model receives it as context.
* feat: restore gutter + drag-handle for line selection
Re-enable Pierre's gutter '+' as a click-and-drag line selector (with the
highlight growing as you drag) — the affordance that was lost when the
auto-adding gutter button was removed. It no longer auto-adds: the commit flows
through onLineSelected to the 'Add to Chat' popup / ⌘L.
* fix: live selection highlight while dragging + reposition Add to Chat popup
- Feed onLineSelectionChange into the controlled selection so rows highlight
live as you drag, not just on release (Pierre only paints the controlled
selection when the prop updates). Popup now fires on onLineSelectionEnd.
- Anchor the popup's bottom-left to the drag handle (drop horizontal centering)
so it no longer overlaps the '+' button.
* fix: anchor Add to Chat popup to gutter handle + click-away to unselect
- Position the popup from the gutter '+' handle's rect (in the diff shadow DOM,
placed on the selection's bottom line) instead of the pointer-release point,
which landed inconsistently. Falls back to the pointer if not found.
- Clear the line selection (and popup) on any outside pointer-down.
* fix: sidebar-collapse header overlap + PR review comments
- Lift useSidebarLayout to AgentsShell (single source), share collapsed via
context, and pad the reviews header left when the sidebar is collapsed so the
fixed collapse toggle no longer overlaps the header content.
- add-to-chat: collect each diff side separately so a selection that spans a
deletion->addition no longer pastes wrong-file lines (PR comment).
- chat: clear attachments after sending via a suggested prompt, so an attached
snippet isn't silently resent on the next message (PR comment).
* fix: anchored finding card positions to the right of the diff again
The virtualization refactor moved the card inside the main-width Virtualizer
scroller, so it clamped over the diff. Render it in the outer container as a
viewport-fixed card clamped to window width (right gutter / over the side panel,
like prod) and track the finding as the diff scrolls (rAF-throttled), hiding
when the finding scrolls out of view.
2026-06-18 16:54:07 -07:00
|
|
|
import {
|
|
|
|
|
createContext,
|
|
|
|
|
useCallback,
|
|
|
|
|
useContext,
|
|
|
|
|
useEffect,
|
|
|
|
|
useLayoutEffect,
|
|
|
|
|
useMemo,
|
|
|
|
|
useRef,
|
|
|
|
|
useState,
|
|
|
|
|
} from "react"
|
feat: chat with your PR on the review page (#1534)
* feat: chat with your PR on the review page
Add a sandbox-less `chat` graph that answers questions about a single PR
from its diff, the published review findings, and read-only GitHub access.
- agent/chat.py: deepagents graph, no sandbox (default StateBackend, file
mutation + execute tools excluded). PR context is seeded as virtual files
under /pr/; a repo-scoped App token is resolved in-graph.
- tools: read_repo_file, search_repo_code, list_review_findings.
- dashboard/review_chat_api.py + routes: per-user chat thread, LangGraph
stream/commands/state/history proxy pinned to the chat assistant, seeds
diff/findings/overview on first run. Gated by repo access.
- UI: Chat tab wired to a chat-scoped StreamProvider (replaces Coming Soon).
* feat: admin setting for review-chat default model
Add a 'Open SWE Review Chat' default to team settings (default_chat_model /
default_chat_reasoning_effort). get_team_default_model("chat") inherits the
Agent default when unset; the chat graph resolves through it. Admin RolePicker
gains an 'Agent default' inherit option that clears the override.
* feat: multi-conversation review chat (tabs, new chat, history)
Replace the single per-PR chat thread with multiple per-user conversations:
- threads minted client-side; first message persists with a title derived
from the prompt.
- list + delete endpoints; chat panel gets a tab strip (history), new-chat
(+), close (x), refresh, an intro greeting, and suggested prompts.
- get_review_chat now returns availability only (ids are client-minted).
* ui fixes
* ui: review-chat history dropdown, full-width AI replies, resizable side panel
* fix(review-chat): enforce per-user thread ownership on proxy endpoints; reseed PR context on head change
---------
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
2026-06-15 17:17:30 -07:00
|
|
|
import { StreamProvider, useStreamContext } from "@langchain/react"
|
|
|
|
|
import { overrideFetchImplementation } from "@langchain/langgraph-sdk"
|
|
|
|
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
|
|
|
|
|
import {
|
|
|
|
|
ArrowClockwiseIcon,
|
|
|
|
|
ArrowUpIcon,
|
|
|
|
|
CheckIcon,
|
feat: split view, add-to-chat, virtualization + scroll/grouping perf (#1574)
* feat: reviews page split view, add-to-chat, virtualization + perf
- Virtualize the diff (Pierre Virtualizer + worker pool), mirroring the agent
chat panel, so large PRs window rows instead of materializing every line.
- Split chat and diff into independent scroll containers and make chat
auto-scroll fully contained, so typing/streaming no longer moves the diff.
- Memoize FileDiffCard with stable callbacks so focusing a finding re-renders
only the affected card.
- Add a persisted unified/split diff toggle.
- Add highlight-to-chat: select lines (drag / shift-click) + gutter "+" to drop
a file:line snippet into the chat composer.
- Rebuild sidebar group rows: whole card scrolls to the group (incl. the
expanded explanation), Read explanation stays a separate toggle, memoized.
* feat: add-to-chat uses attachment pills + selection popup / ⌘L
Replace the raw-snippet injection with a Cursor-style flow:
- Selecting lines shows a floating "Add to Chat ⌘L" popup at the pointer; ⌘L
adds the current selection without it. Removes the auto-adding gutter "+".
- "Add to chat" now creates a removable attachment pill in the composer (and a
pill in the sent message bubble) instead of pasting raw text. The code is
still serialized into the message content so the model receives it as context.
* feat: restore gutter + drag-handle for line selection
Re-enable Pierre's gutter '+' as a click-and-drag line selector (with the
highlight growing as you drag) — the affordance that was lost when the
auto-adding gutter button was removed. It no longer auto-adds: the commit flows
through onLineSelected to the 'Add to Chat' popup / ⌘L.
* fix: live selection highlight while dragging + reposition Add to Chat popup
- Feed onLineSelectionChange into the controlled selection so rows highlight
live as you drag, not just on release (Pierre only paints the controlled
selection when the prop updates). Popup now fires on onLineSelectionEnd.
- Anchor the popup's bottom-left to the drag handle (drop horizontal centering)
so it no longer overlaps the '+' button.
* fix: anchor Add to Chat popup to gutter handle + click-away to unselect
- Position the popup from the gutter '+' handle's rect (in the diff shadow DOM,
placed on the selection's bottom line) instead of the pointer-release point,
which landed inconsistently. Falls back to the pointer if not found.
- Clear the line selection (and popup) on any outside pointer-down.
* fix: sidebar-collapse header overlap + PR review comments
- Lift useSidebarLayout to AgentsShell (single source), share collapsed via
context, and pad the reviews header left when the sidebar is collapsed so the
fixed collapse toggle no longer overlaps the header content.
- add-to-chat: collect each diff side separately so a selection that spans a
deletion->addition no longer pastes wrong-file lines (PR comment).
- chat: clear attachments after sending via a suggested prompt, so an attached
snippet isn't silently resent on the next message (PR comment).
* fix: anchored finding card positions to the right of the diff again
The virtualization refactor moved the card inside the main-width Virtualizer
scroller, so it clamped over the diff. Render it in the outer container as a
viewport-fixed card clamped to window width (right gutter / over the side panel,
like prod) and track the finding as the diff scrolls (rAF-throttled), hiding
when the finding scrolls out of view.
2026-06-18 16:54:07 -07:00
|
|
|
CodeIcon,
|
feat: chat with your PR on the review page (#1534)
* feat: chat with your PR on the review page
Add a sandbox-less `chat` graph that answers questions about a single PR
from its diff, the published review findings, and read-only GitHub access.
- agent/chat.py: deepagents graph, no sandbox (default StateBackend, file
mutation + execute tools excluded). PR context is seeded as virtual files
under /pr/; a repo-scoped App token is resolved in-graph.
- tools: read_repo_file, search_repo_code, list_review_findings.
- dashboard/review_chat_api.py + routes: per-user chat thread, LangGraph
stream/commands/state/history proxy pinned to the chat assistant, seeds
diff/findings/overview on first run. Gated by repo access.
- UI: Chat tab wired to a chat-scoped StreamProvider (replaces Coming Soon).
* feat: admin setting for review-chat default model
Add a 'Open SWE Review Chat' default to team settings (default_chat_model /
default_chat_reasoning_effort). get_team_default_model("chat") inherits the
Agent default when unset; the chat graph resolves through it. Admin RolePicker
gains an 'Agent default' inherit option that clears the override.
* feat: multi-conversation review chat (tabs, new chat, history)
Replace the single per-PR chat thread with multiple per-user conversations:
- threads minted client-side; first message persists with a title derived
from the prompt.
- list + delete endpoints; chat panel gets a tab strip (history), new-chat
(+), close (x), refresh, an intro greeting, and suggested prompts.
- get_review_chat now returns availability only (ids are client-minted).
* ui fixes
* ui: review-chat history dropdown, full-width AI replies, resizable side panel
* fix(review-chat): enforce per-user thread ownership on proxy endpoints; reseed PR context on head change
---------
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
2026-06-15 17:17:30 -07:00
|
|
|
PlusIcon,
|
|
|
|
|
SparkleIcon,
|
|
|
|
|
TrashIcon,
|
|
|
|
|
XIcon,
|
|
|
|
|
} from "@phosphor-icons/react"
|
|
|
|
|
import { Menu } from "@base-ui/react/menu"
|
|
|
|
|
import type { BaseMessage } from "@langchain/core/messages"
|
|
|
|
|
|
|
|
|
|
import type { ReviewChatThread } from "@/lib/api"
|
|
|
|
|
import { Markdown } from "@/components/agents/ported"
|
|
|
|
|
import { IconButton } from "@/components/ui/button"
|
|
|
|
|
import { Textarea } from "@/components/ui/textarea"
|
|
|
|
|
import { Skeleton } from "@/components/ui/skeleton"
|
|
|
|
|
import { api, reviewChatApiBase } from "@/lib/api"
|
|
|
|
|
import { cn } from "@/lib/utils"
|
|
|
|
|
|
feat: split view, add-to-chat, virtualization + scroll/grouping perf (#1574)
* feat: reviews page split view, add-to-chat, virtualization + perf
- Virtualize the diff (Pierre Virtualizer + worker pool), mirroring the agent
chat panel, so large PRs window rows instead of materializing every line.
- Split chat and diff into independent scroll containers and make chat
auto-scroll fully contained, so typing/streaming no longer moves the diff.
- Memoize FileDiffCard with stable callbacks so focusing a finding re-renders
only the affected card.
- Add a persisted unified/split diff toggle.
- Add highlight-to-chat: select lines (drag / shift-click) + gutter "+" to drop
a file:line snippet into the chat composer.
- Rebuild sidebar group rows: whole card scrolls to the group (incl. the
expanded explanation), Read explanation stays a separate toggle, memoized.
* feat: add-to-chat uses attachment pills + selection popup / ⌘L
Replace the raw-snippet injection with a Cursor-style flow:
- Selecting lines shows a floating "Add to Chat ⌘L" popup at the pointer; ⌘L
adds the current selection without it. Removes the auto-adding gutter "+".
- "Add to chat" now creates a removable attachment pill in the composer (and a
pill in the sent message bubble) instead of pasting raw text. The code is
still serialized into the message content so the model receives it as context.
* feat: restore gutter + drag-handle for line selection
Re-enable Pierre's gutter '+' as a click-and-drag line selector (with the
highlight growing as you drag) — the affordance that was lost when the
auto-adding gutter button was removed. It no longer auto-adds: the commit flows
through onLineSelected to the 'Add to Chat' popup / ⌘L.
* fix: live selection highlight while dragging + reposition Add to Chat popup
- Feed onLineSelectionChange into the controlled selection so rows highlight
live as you drag, not just on release (Pierre only paints the controlled
selection when the prop updates). Popup now fires on onLineSelectionEnd.
- Anchor the popup's bottom-left to the drag handle (drop horizontal centering)
so it no longer overlaps the '+' button.
* fix: anchor Add to Chat popup to gutter handle + click-away to unselect
- Position the popup from the gutter '+' handle's rect (in the diff shadow DOM,
placed on the selection's bottom line) instead of the pointer-release point,
which landed inconsistently. Falls back to the pointer if not found.
- Clear the line selection (and popup) on any outside pointer-down.
* fix: sidebar-collapse header overlap + PR review comments
- Lift useSidebarLayout to AgentsShell (single source), share collapsed via
context, and pad the reviews header left when the sidebar is collapsed so the
fixed collapse toggle no longer overlaps the header content.
- add-to-chat: collect each diff side separately so a selection that spans a
deletion->addition no longer pastes wrong-file lines (PR comment).
- chat: clear attachments after sending via a suggested prompt, so an attached
snippet isn't silently resent on the next message (PR comment).
* fix: anchored finding card positions to the right of the diff again
The virtualization refactor moved the card inside the main-width Virtualizer
scroller, so it clamped over the diff. Render it in the outer container as a
viewport-fixed card clamped to window width (right gutter / over the side panel,
like prod) and track the finding as the diff scrolls (rAF-throttled), hiding
when the finding scrolls out of view.
2026-06-18 16:54:07 -07:00
|
|
|
// --- Composer bridge ---------------------------------------------------------
|
|
|
|
|
//
|
|
|
|
|
// Lets the diff column (a separate subtree) attach a code reference to the
|
|
|
|
|
// active chat composer. The reference shows as a removable pill; the code is
|
|
|
|
|
// only serialized into the message text on send. The chat tab may not be
|
|
|
|
|
// mounted when "add to chat" fires, so attachments are stashed until ChatBody
|
|
|
|
|
// registers its sink.
|
|
|
|
|
|
|
|
|
|
export interface ChatAttachment {
|
|
|
|
|
id: string
|
|
|
|
|
path: string
|
|
|
|
|
// e.g. "R35-37" / "L12" — the side+line range shown after the filename.
|
|
|
|
|
lineLabel: string
|
|
|
|
|
language: string
|
|
|
|
|
snippet: string
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface ReviewChatComposer {
|
|
|
|
|
addAttachment: (attachment: ChatAttachment) => void
|
|
|
|
|
registerSink: (fn: ((attachment: ChatAttachment) => void) | null) => void
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const ReviewChatComposerContext = createContext<ReviewChatComposer | null>(null)
|
|
|
|
|
|
|
|
|
|
export function ReviewChatComposerProvider({
|
|
|
|
|
children,
|
|
|
|
|
}: {
|
|
|
|
|
children: React.ReactNode
|
|
|
|
|
}) {
|
|
|
|
|
const sinkRef = useRef<((attachment: ChatAttachment) => void) | null>(null)
|
|
|
|
|
const pendingRef = useRef<Array<ChatAttachment>>([])
|
|
|
|
|
const value = useMemo<ReviewChatComposer>(
|
|
|
|
|
() => ({
|
|
|
|
|
addAttachment: (attachment) => {
|
|
|
|
|
if (sinkRef.current) sinkRef.current(attachment)
|
|
|
|
|
else pendingRef.current.push(attachment)
|
|
|
|
|
},
|
|
|
|
|
registerSink: (fn) => {
|
|
|
|
|
sinkRef.current = fn
|
|
|
|
|
if (fn && pendingRef.current.length > 0) {
|
|
|
|
|
for (const attachment of pendingRef.current) fn(attachment)
|
|
|
|
|
pendingRef.current = []
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
}),
|
|
|
|
|
[]
|
|
|
|
|
)
|
|
|
|
|
return (
|
|
|
|
|
<ReviewChatComposerContext.Provider value={value}>
|
|
|
|
|
{children}
|
|
|
|
|
</ReviewChatComposerContext.Provider>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function useReviewChatComposer(): ReviewChatComposer | null {
|
|
|
|
|
return useContext(ReviewChatComposerContext)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function attachmentBasename(path: string): string {
|
|
|
|
|
const idx = path.lastIndexOf("/")
|
|
|
|
|
return idx === -1 ? path : path.slice(idx + 1)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function attachmentPillLabel(attachment: ChatAttachment): string {
|
|
|
|
|
return `${attachmentBasename(attachment.path)}:${attachment.lineLabel}`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Serialize attachments as fenced code blocks ahead of the prose so the model
|
|
|
|
|
// receives the code as context. The UI renders pills instead (see parse below).
|
|
|
|
|
function serializeMessage(text: string, attachments: Array<ChatAttachment>): string {
|
|
|
|
|
const blocks = attachments.map(
|
|
|
|
|
(a) => `\`${a.path}:${a.lineLabel}\`\n\`\`\`${a.language}\n${a.snippet}\n\`\`\``
|
|
|
|
|
)
|
|
|
|
|
return [...blocks, text.trim()].filter(Boolean).join("\n\n")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface ParsedAttachment {
|
|
|
|
|
label: string
|
|
|
|
|
language: string
|
|
|
|
|
code: string
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Pull the leading attachment blocks (which serializeMessage always writes
|
|
|
|
|
// first) back out of a sent message so the bubble can render them as pills.
|
|
|
|
|
function parseUserMessage(content: string): {
|
|
|
|
|
attachments: Array<ParsedAttachment>
|
|
|
|
|
text: string
|
|
|
|
|
} {
|
|
|
|
|
const attachments: Array<ParsedAttachment> = []
|
|
|
|
|
let rest = content
|
|
|
|
|
const re = /^`([^`\n]+)`\n```([\w.-]*)\n([\s\S]*?)\n```\n*/
|
|
|
|
|
let match = re.exec(rest)
|
|
|
|
|
while (match && match.index === 0) {
|
|
|
|
|
const loc = match[1] ?? ""
|
|
|
|
|
const slash = loc.lastIndexOf("/")
|
|
|
|
|
attachments.push({
|
|
|
|
|
label: slash === -1 ? loc : loc.slice(slash + 1),
|
|
|
|
|
language: match[2] ?? "",
|
|
|
|
|
code: match[3] ?? "",
|
|
|
|
|
})
|
|
|
|
|
rest = rest.slice(match[0].length)
|
|
|
|
|
match = re.exec(rest)
|
|
|
|
|
}
|
|
|
|
|
return { attachments, text: rest.trim() }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function AttachmentPill({
|
|
|
|
|
label,
|
|
|
|
|
onRemove,
|
|
|
|
|
}: {
|
|
|
|
|
label: string
|
|
|
|
|
onRemove?: () => void
|
|
|
|
|
}) {
|
|
|
|
|
return (
|
|
|
|
|
<span className="inline-flex max-w-[200px] items-center gap-1 rounded-md border border-border bg-muted/60 py-0.5 pl-1.5 pr-1 text-[11px] text-foreground">
|
|
|
|
|
<CodeIcon className="size-3 shrink-0 text-muted-foreground" />
|
|
|
|
|
<span className="truncate font-mono">{label}</span>
|
|
|
|
|
{onRemove && (
|
|
|
|
|
<IconButton
|
|
|
|
|
type="button"
|
|
|
|
|
variant="ghost"
|
|
|
|
|
size="icon-xs"
|
|
|
|
|
aria-label="Remove attachment"
|
|
|
|
|
onClick={onRemove}
|
|
|
|
|
>
|
|
|
|
|
<XIcon />
|
|
|
|
|
</IconButton>
|
|
|
|
|
)}
|
|
|
|
|
</span>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
feat: chat with your PR on the review page (#1534)
* feat: chat with your PR on the review page
Add a sandbox-less `chat` graph that answers questions about a single PR
from its diff, the published review findings, and read-only GitHub access.
- agent/chat.py: deepagents graph, no sandbox (default StateBackend, file
mutation + execute tools excluded). PR context is seeded as virtual files
under /pr/; a repo-scoped App token is resolved in-graph.
- tools: read_repo_file, search_repo_code, list_review_findings.
- dashboard/review_chat_api.py + routes: per-user chat thread, LangGraph
stream/commands/state/history proxy pinned to the chat assistant, seeds
diff/findings/overview on first run. Gated by repo access.
- UI: Chat tab wired to a chat-scoped StreamProvider (replaces Coming Soon).
* feat: admin setting for review-chat default model
Add a 'Open SWE Review Chat' default to team settings (default_chat_model /
default_chat_reasoning_effort). get_team_default_model("chat") inherits the
Agent default when unset; the chat graph resolves through it. Admin RolePicker
gains an 'Agent default' inherit option that clears the override.
* feat: multi-conversation review chat (tabs, new chat, history)
Replace the single per-PR chat thread with multiple per-user conversations:
- threads minted client-side; first message persists with a title derived
from the prompt.
- list + delete endpoints; chat panel gets a tab strip (history), new-chat
(+), close (x), refresh, an intro greeting, and suggested prompts.
- get_review_chat now returns availability only (ids are client-minted).
* ui fixes
* ui: review-chat history dropdown, full-width AI replies, resizable side panel
* fix(review-chat): enforce per-user thread ownership on proxy endpoints; reseed PR context on head change
---------
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
2026-06-15 17:17:30 -07:00
|
|
|
const dashboardFetch: typeof fetch = (input, init) =>
|
|
|
|
|
fetch(input, { ...init, credentials: "include" })
|
|
|
|
|
|
|
|
|
|
// The SDK's internal Client issues some reads (getState, history) outside the
|
|
|
|
|
// transport's fetch; without this they drop the session cookie cross-origin.
|
|
|
|
|
overrideFetchImplementation(dashboardFetch)
|
|
|
|
|
|
|
|
|
|
const SUGGESTED_PROMPTS = [
|
|
|
|
|
"Summarize the changes in this PR",
|
|
|
|
|
"Walk me through the review findings",
|
|
|
|
|
"What are the riskiest parts of this change?",
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
// Kept in sync with the backend's `_derive_title` so the optimistic tab label
|
|
|
|
|
// matches the title the server persists for the thread.
|
|
|
|
|
const DEFAULT_TITLE = "New chat"
|
|
|
|
|
const TITLE_MAX_CHARS = 60
|
|
|
|
|
|
|
|
|
|
function deriveTitle(text: string): string {
|
|
|
|
|
const flattened = text.trim().split(/\s+/).join(" ")
|
|
|
|
|
return flattened ? flattened.slice(0, TITLE_MAX_CHARS) : DEFAULT_TITLE
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function messageType(message: BaseMessage): string {
|
|
|
|
|
const candidate = message as unknown as {
|
|
|
|
|
getType?: () => string
|
|
|
|
|
type?: string
|
|
|
|
|
role?: string
|
|
|
|
|
}
|
|
|
|
|
return candidate.getType?.() ?? candidate.type ?? candidate.role ?? "ai"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function messageText(content: BaseMessage["content"]): string {
|
|
|
|
|
if (typeof content === "string") return content
|
|
|
|
|
if (!Array.isArray(content)) return ""
|
|
|
|
|
return content
|
|
|
|
|
.map((block) => {
|
|
|
|
|
if (typeof block === "string") return block
|
|
|
|
|
if (typeof block === "object" && "text" in block) {
|
|
|
|
|
const text = (block as { text?: unknown }).text
|
|
|
|
|
return typeof text === "string" ? text : ""
|
|
|
|
|
}
|
|
|
|
|
return ""
|
|
|
|
|
})
|
|
|
|
|
.filter(Boolean)
|
|
|
|
|
.join("\n")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// --- Conversation store ------------------------------------------------------
|
|
|
|
|
//
|
|
|
|
|
// The client owns the conversation list. Each conversation is a client-minted
|
|
|
|
|
// thread id; the thread is created server-side lazily on its first message.
|
|
|
|
|
// The server thread list is only used to recover conversations on a fresh
|
|
|
|
|
// browser and to reconcile titles — it is never the sole source for the tabs,
|
|
|
|
|
// so an empty/lagging server response can no longer make open chats vanish.
|
|
|
|
|
|
|
|
|
|
interface Conversation {
|
|
|
|
|
id: string
|
|
|
|
|
title: string
|
|
|
|
|
createdAt: number
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface ChatState {
|
|
|
|
|
conversations: Array<Conversation>
|
|
|
|
|
activeId: string
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function storageKey(owner: string, repo: string, number: number): string {
|
|
|
|
|
return `osw:review-chat:${owner}/${repo}/${number}`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function newDraft(): Conversation {
|
|
|
|
|
return { id: crypto.randomUUID(), title: DEFAULT_TITLE, createdAt: Date.now() }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function isConversation(value: unknown): value is Conversation {
|
|
|
|
|
if (!value || typeof value !== "object") return false
|
|
|
|
|
const candidate = value as Record<string, unknown>
|
|
|
|
|
return (
|
|
|
|
|
typeof candidate.id === "string" &&
|
|
|
|
|
typeof candidate.title === "string" &&
|
|
|
|
|
typeof candidate.createdAt === "number"
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function loadState(key: string): ChatState {
|
|
|
|
|
if (typeof window !== "undefined") {
|
|
|
|
|
try {
|
|
|
|
|
const raw = window.localStorage.getItem(key)
|
|
|
|
|
if (raw) {
|
|
|
|
|
const parsed: unknown = JSON.parse(raw)
|
|
|
|
|
const source = (parsed ?? {}) as Record<string, unknown>
|
|
|
|
|
const conversations = Array.isArray(source.conversations)
|
|
|
|
|
? source.conversations.filter(isConversation)
|
|
|
|
|
: []
|
|
|
|
|
const first = conversations[0]
|
|
|
|
|
if (first) {
|
|
|
|
|
const activeId =
|
|
|
|
|
typeof source.activeId === "string" &&
|
|
|
|
|
conversations.some((c) => c.id === source.activeId)
|
|
|
|
|
? source.activeId
|
|
|
|
|
: first.id
|
|
|
|
|
return { conversations, activeId }
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} catch {
|
|
|
|
|
/* fall through to a fresh draft */
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
const draft = newDraft()
|
|
|
|
|
return { conversations: [draft], activeId: draft.id }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function saveState(key: string, state: ChatState): void {
|
|
|
|
|
if (typeof window === "undefined") return
|
|
|
|
|
try {
|
|
|
|
|
window.localStorage.setItem(key, JSON.stringify(state))
|
|
|
|
|
} catch {
|
|
|
|
|
/* ignore quota / availability errors */
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function useConversations(key: string) {
|
|
|
|
|
const [state, setState] = useState<ChatState>(() => loadState(key))
|
|
|
|
|
|
|
|
|
|
const update = useCallback(
|
|
|
|
|
(fn: (prev: ChatState) => ChatState) => {
|
|
|
|
|
setState((prev) => {
|
|
|
|
|
const next = fn(prev)
|
|
|
|
|
if (next === prev) return prev
|
|
|
|
|
saveState(key, next)
|
|
|
|
|
return next
|
|
|
|
|
})
|
|
|
|
|
},
|
|
|
|
|
[key],
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const select = useCallback(
|
|
|
|
|
(conversation: Conversation) => {
|
|
|
|
|
update((prev) => ({
|
|
|
|
|
conversations: prev.conversations.some((c) => c.id === conversation.id)
|
|
|
|
|
? prev.conversations
|
|
|
|
|
: [...prev.conversations, conversation],
|
|
|
|
|
activeId: conversation.id,
|
|
|
|
|
}))
|
|
|
|
|
},
|
|
|
|
|
[update],
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const newChat = useCallback(() => {
|
|
|
|
|
update((prev) => {
|
|
|
|
|
const active = prev.conversations.find((c) => c.id === prev.activeId)
|
|
|
|
|
// Reuse a pristine, never-sent draft instead of stacking empty tabs.
|
|
|
|
|
if (active && active.title === DEFAULT_TITLE) return prev
|
|
|
|
|
const draft = newDraft()
|
|
|
|
|
return { conversations: [...prev.conversations, draft], activeId: draft.id }
|
|
|
|
|
})
|
|
|
|
|
}, [update])
|
|
|
|
|
|
|
|
|
|
// Names a conversation from its first message; later messages don't rename it.
|
|
|
|
|
const nameConversation = useCallback(
|
|
|
|
|
(id: string, title: string) => {
|
|
|
|
|
update((prev) => {
|
|
|
|
|
const current = prev.conversations.find((c) => c.id === id)
|
|
|
|
|
if (!current || current.title !== DEFAULT_TITLE) return prev
|
|
|
|
|
return {
|
|
|
|
|
...prev,
|
|
|
|
|
conversations: prev.conversations.map((c) =>
|
|
|
|
|
c.id === id ? { ...c, title } : c,
|
|
|
|
|
),
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
},
|
|
|
|
|
[update],
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const close = useCallback(
|
|
|
|
|
(id: string) => {
|
|
|
|
|
update((prev) => {
|
|
|
|
|
const index = prev.conversations.findIndex((c) => c.id === id)
|
|
|
|
|
if (index === -1) return prev
|
|
|
|
|
const remaining = prev.conversations.filter((c) => c.id !== id)
|
|
|
|
|
const fallback = remaining[Math.max(0, index - 1)]
|
|
|
|
|
if (!fallback) {
|
|
|
|
|
const draft = newDraft()
|
|
|
|
|
return { conversations: [draft], activeId: draft.id }
|
|
|
|
|
}
|
|
|
|
|
return {
|
|
|
|
|
conversations: remaining,
|
|
|
|
|
activeId: prev.activeId === id ? fallback.id : prev.activeId,
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
},
|
|
|
|
|
[update],
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
return { ...state, select, newChat, nameConversation, close }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// --- View --------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
function EmptyState({ onPick }: { onPick: (prompt: string) => void }) {
|
|
|
|
|
return (
|
|
|
|
|
<div className="flex flex-1 flex-col gap-4 p-4">
|
2026-06-16 14:56:11 -07:00
|
|
|
<p className="text-[13px] text-foreground">
|
feat: chat with your PR on the review page (#1534)
* feat: chat with your PR on the review page
Add a sandbox-less `chat` graph that answers questions about a single PR
from its diff, the published review findings, and read-only GitHub access.
- agent/chat.py: deepagents graph, no sandbox (default StateBackend, file
mutation + execute tools excluded). PR context is seeded as virtual files
under /pr/; a repo-scoped App token is resolved in-graph.
- tools: read_repo_file, search_repo_code, list_review_findings.
- dashboard/review_chat_api.py + routes: per-user chat thread, LangGraph
stream/commands/state/history proxy pinned to the chat assistant, seeds
diff/findings/overview on first run. Gated by repo access.
- UI: Chat tab wired to a chat-scoped StreamProvider (replaces Coming Soon).
* feat: admin setting for review-chat default model
Add a 'Open SWE Review Chat' default to team settings (default_chat_model /
default_chat_reasoning_effort). get_team_default_model("chat") inherits the
Agent default when unset; the chat graph resolves through it. Admin RolePicker
gains an 'Agent default' inherit option that clears the override.
* feat: multi-conversation review chat (tabs, new chat, history)
Replace the single per-PR chat thread with multiple per-user conversations:
- threads minted client-side; first message persists with a title derived
from the prompt.
- list + delete endpoints; chat panel gets a tab strip (history), new-chat
(+), close (x), refresh, an intro greeting, and suggested prompts.
- get_review_chat now returns availability only (ids are client-minted).
* ui fixes
* ui: review-chat history dropdown, full-width AI replies, resizable side panel
* fix(review-chat): enforce per-user thread ownership on proxy endpoints; reseed PR context on head change
---------
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
2026-06-15 17:17:30 -07:00
|
|
|
I've reviewed this PR. Ask me about the diff, the findings, or the
|
|
|
|
|
surrounding code — I have read-only access to the repository.
|
|
|
|
|
</p>
|
|
|
|
|
<div className="flex flex-col gap-1.5">
|
|
|
|
|
<span className="text-xs font-medium text-muted-foreground">
|
|
|
|
|
Suggested prompts
|
|
|
|
|
</span>
|
|
|
|
|
{SUGGESTED_PROMPTS.map((prompt) => (
|
|
|
|
|
<button
|
|
|
|
|
key={prompt}
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={() => onPick(prompt)}
|
2026-06-16 14:56:11 -07:00
|
|
|
className="flex items-center gap-2 rounded-md px-2 py-1.5 text-left text-[13px] text-foreground hover:bg-muted/60"
|
feat: chat with your PR on the review page (#1534)
* feat: chat with your PR on the review page
Add a sandbox-less `chat` graph that answers questions about a single PR
from its diff, the published review findings, and read-only GitHub access.
- agent/chat.py: deepagents graph, no sandbox (default StateBackend, file
mutation + execute tools excluded). PR context is seeded as virtual files
under /pr/; a repo-scoped App token is resolved in-graph.
- tools: read_repo_file, search_repo_code, list_review_findings.
- dashboard/review_chat_api.py + routes: per-user chat thread, LangGraph
stream/commands/state/history proxy pinned to the chat assistant, seeds
diff/findings/overview on first run. Gated by repo access.
- UI: Chat tab wired to a chat-scoped StreamProvider (replaces Coming Soon).
* feat: admin setting for review-chat default model
Add a 'Open SWE Review Chat' default to team settings (default_chat_model /
default_chat_reasoning_effort). get_team_default_model("chat") inherits the
Agent default when unset; the chat graph resolves through it. Admin RolePicker
gains an 'Agent default' inherit option that clears the override.
* feat: multi-conversation review chat (tabs, new chat, history)
Replace the single per-PR chat thread with multiple per-user conversations:
- threads minted client-side; first message persists with a title derived
from the prompt.
- list + delete endpoints; chat panel gets a tab strip (history), new-chat
(+), close (x), refresh, an intro greeting, and suggested prompts.
- get_review_chat now returns availability only (ids are client-minted).
* ui fixes
* ui: review-chat history dropdown, full-width AI replies, resizable side panel
* fix(review-chat): enforce per-user thread ownership on proxy endpoints; reseed PR context on head change
---------
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
2026-06-15 17:17:30 -07:00
|
|
|
>
|
|
|
|
|
<SparkleIcon className="size-4 shrink-0 text-muted-foreground" />
|
|
|
|
|
{prompt}
|
|
|
|
|
</button>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function LoadingState() {
|
|
|
|
|
return (
|
|
|
|
|
<div className="flex flex-1 flex-col gap-4 p-4">
|
|
|
|
|
<div className="flex justify-end">
|
|
|
|
|
<Skeleton className="h-12 w-2/5 rounded-lg" />
|
|
|
|
|
</div>
|
|
|
|
|
<div className="flex flex-col gap-2">
|
|
|
|
|
<Skeleton className="h-4 w-4/5" />
|
|
|
|
|
<Skeleton className="h-4 w-3/5" />
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function ChatBody({
|
|
|
|
|
onUserSend,
|
|
|
|
|
expectsHistory,
|
|
|
|
|
}: {
|
|
|
|
|
onUserSend: (text: string) => void
|
|
|
|
|
expectsHistory: boolean
|
|
|
|
|
}) {
|
feat: split view, add-to-chat, virtualization + scroll/grouping perf (#1574)
* feat: reviews page split view, add-to-chat, virtualization + perf
- Virtualize the diff (Pierre Virtualizer + worker pool), mirroring the agent
chat panel, so large PRs window rows instead of materializing every line.
- Split chat and diff into independent scroll containers and make chat
auto-scroll fully contained, so typing/streaming no longer moves the diff.
- Memoize FileDiffCard with stable callbacks so focusing a finding re-renders
only the affected card.
- Add a persisted unified/split diff toggle.
- Add highlight-to-chat: select lines (drag / shift-click) + gutter "+" to drop
a file:line snippet into the chat composer.
- Rebuild sidebar group rows: whole card scrolls to the group (incl. the
expanded explanation), Read explanation stays a separate toggle, memoized.
* feat: add-to-chat uses attachment pills + selection popup / ⌘L
Replace the raw-snippet injection with a Cursor-style flow:
- Selecting lines shows a floating "Add to Chat ⌘L" popup at the pointer; ⌘L
adds the current selection without it. Removes the auto-adding gutter "+".
- "Add to chat" now creates a removable attachment pill in the composer (and a
pill in the sent message bubble) instead of pasting raw text. The code is
still serialized into the message content so the model receives it as context.
* feat: restore gutter + drag-handle for line selection
Re-enable Pierre's gutter '+' as a click-and-drag line selector (with the
highlight growing as you drag) — the affordance that was lost when the
auto-adding gutter button was removed. It no longer auto-adds: the commit flows
through onLineSelected to the 'Add to Chat' popup / ⌘L.
* fix: live selection highlight while dragging + reposition Add to Chat popup
- Feed onLineSelectionChange into the controlled selection so rows highlight
live as you drag, not just on release (Pierre only paints the controlled
selection when the prop updates). Popup now fires on onLineSelectionEnd.
- Anchor the popup's bottom-left to the drag handle (drop horizontal centering)
so it no longer overlaps the '+' button.
* fix: anchor Add to Chat popup to gutter handle + click-away to unselect
- Position the popup from the gutter '+' handle's rect (in the diff shadow DOM,
placed on the selection's bottom line) instead of the pointer-release point,
which landed inconsistently. Falls back to the pointer if not found.
- Clear the line selection (and popup) on any outside pointer-down.
* fix: sidebar-collapse header overlap + PR review comments
- Lift useSidebarLayout to AgentsShell (single source), share collapsed via
context, and pad the reviews header left when the sidebar is collapsed so the
fixed collapse toggle no longer overlaps the header content.
- add-to-chat: collect each diff side separately so a selection that spans a
deletion->addition no longer pastes wrong-file lines (PR comment).
- chat: clear attachments after sending via a suggested prompt, so an attached
snippet isn't silently resent on the next message (PR comment).
* fix: anchored finding card positions to the right of the diff again
The virtualization refactor moved the card inside the main-width Virtualizer
scroller, so it clamped over the diff. Render it in the outer container as a
viewport-fixed card clamped to window width (right gutter / over the side panel,
like prod) and track the finding as the diff scrolls (rAF-throttled), hiding
when the finding scrolls out of view.
2026-06-18 16:54:07 -07:00
|
|
|
const composer = useReviewChatComposer()
|
feat: chat with your PR on the review page (#1534)
* feat: chat with your PR on the review page
Add a sandbox-less `chat` graph that answers questions about a single PR
from its diff, the published review findings, and read-only GitHub access.
- agent/chat.py: deepagents graph, no sandbox (default StateBackend, file
mutation + execute tools excluded). PR context is seeded as virtual files
under /pr/; a repo-scoped App token is resolved in-graph.
- tools: read_repo_file, search_repo_code, list_review_findings.
- dashboard/review_chat_api.py + routes: per-user chat thread, LangGraph
stream/commands/state/history proxy pinned to the chat assistant, seeds
diff/findings/overview on first run. Gated by repo access.
- UI: Chat tab wired to a chat-scoped StreamProvider (replaces Coming Soon).
* feat: admin setting for review-chat default model
Add a 'Open SWE Review Chat' default to team settings (default_chat_model /
default_chat_reasoning_effort). get_team_default_model("chat") inherits the
Agent default when unset; the chat graph resolves through it. Admin RolePicker
gains an 'Agent default' inherit option that clears the override.
* feat: multi-conversation review chat (tabs, new chat, history)
Replace the single per-PR chat thread with multiple per-user conversations:
- threads minted client-side; first message persists with a title derived
from the prompt.
- list + delete endpoints; chat panel gets a tab strip (history), new-chat
(+), close (x), refresh, an intro greeting, and suggested prompts.
- get_review_chat now returns availability only (ids are client-minted).
* ui fixes
* ui: review-chat history dropdown, full-width AI replies, resizable side panel
* fix(review-chat): enforce per-user thread ownership on proxy endpoints; reseed PR context on head change
---------
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
2026-06-15 17:17:30 -07:00
|
|
|
const stream = useStreamContext()
|
|
|
|
|
const [value, setValue] = useState("")
|
feat: split view, add-to-chat, virtualization + scroll/grouping perf (#1574)
* feat: reviews page split view, add-to-chat, virtualization + perf
- Virtualize the diff (Pierre Virtualizer + worker pool), mirroring the agent
chat panel, so large PRs window rows instead of materializing every line.
- Split chat and diff into independent scroll containers and make chat
auto-scroll fully contained, so typing/streaming no longer moves the diff.
- Memoize FileDiffCard with stable callbacks so focusing a finding re-renders
only the affected card.
- Add a persisted unified/split diff toggle.
- Add highlight-to-chat: select lines (drag / shift-click) + gutter "+" to drop
a file:line snippet into the chat composer.
- Rebuild sidebar group rows: whole card scrolls to the group (incl. the
expanded explanation), Read explanation stays a separate toggle, memoized.
* feat: add-to-chat uses attachment pills + selection popup / ⌘L
Replace the raw-snippet injection with a Cursor-style flow:
- Selecting lines shows a floating "Add to Chat ⌘L" popup at the pointer; ⌘L
adds the current selection without it. Removes the auto-adding gutter "+".
- "Add to chat" now creates a removable attachment pill in the composer (and a
pill in the sent message bubble) instead of pasting raw text. The code is
still serialized into the message content so the model receives it as context.
* feat: restore gutter + drag-handle for line selection
Re-enable Pierre's gutter '+' as a click-and-drag line selector (with the
highlight growing as you drag) — the affordance that was lost when the
auto-adding gutter button was removed. It no longer auto-adds: the commit flows
through onLineSelected to the 'Add to Chat' popup / ⌘L.
* fix: live selection highlight while dragging + reposition Add to Chat popup
- Feed onLineSelectionChange into the controlled selection so rows highlight
live as you drag, not just on release (Pierre only paints the controlled
selection when the prop updates). Popup now fires on onLineSelectionEnd.
- Anchor the popup's bottom-left to the drag handle (drop horizontal centering)
so it no longer overlaps the '+' button.
* fix: anchor Add to Chat popup to gutter handle + click-away to unselect
- Position the popup from the gutter '+' handle's rect (in the diff shadow DOM,
placed on the selection's bottom line) instead of the pointer-release point,
which landed inconsistently. Falls back to the pointer if not found.
- Clear the line selection (and popup) on any outside pointer-down.
* fix: sidebar-collapse header overlap + PR review comments
- Lift useSidebarLayout to AgentsShell (single source), share collapsed via
context, and pad the reviews header left when the sidebar is collapsed so the
fixed collapse toggle no longer overlaps the header content.
- add-to-chat: collect each diff side separately so a selection that spans a
deletion->addition no longer pastes wrong-file lines (PR comment).
- chat: clear attachments after sending via a suggested prompt, so an attached
snippet isn't silently resent on the next message (PR comment).
* fix: anchored finding card positions to the right of the diff again
The virtualization refactor moved the card inside the main-width Virtualizer
scroller, so it clamped over the diff. Render it in the outer container as a
viewport-fixed card clamped to window width (right gutter / over the side panel,
like prod) and track the finding as the diff scrolls (rAF-throttled), hiding
when the finding scrolls out of view.
2026-06-18 16:54:07 -07:00
|
|
|
const [attachments, setAttachments] = useState<Array<ChatAttachment>>([])
|
|
|
|
|
const scrollRef = useRef<HTMLDivElement>(null)
|
|
|
|
|
const autoScrollRef = useRef(true)
|
|
|
|
|
const prevTopRef = useRef(0)
|
feat: chat with your PR on the review page (#1534)
* feat: chat with your PR on the review page
Add a sandbox-less `chat` graph that answers questions about a single PR
from its diff, the published review findings, and read-only GitHub access.
- agent/chat.py: deepagents graph, no sandbox (default StateBackend, file
mutation + execute tools excluded). PR context is seeded as virtual files
under /pr/; a repo-scoped App token is resolved in-graph.
- tools: read_repo_file, search_repo_code, list_review_findings.
- dashboard/review_chat_api.py + routes: per-user chat thread, LangGraph
stream/commands/state/history proxy pinned to the chat assistant, seeds
diff/findings/overview on first run. Gated by repo access.
- UI: Chat tab wired to a chat-scoped StreamProvider (replaces Coming Soon).
* feat: admin setting for review-chat default model
Add a 'Open SWE Review Chat' default to team settings (default_chat_model /
default_chat_reasoning_effort). get_team_default_model("chat") inherits the
Agent default when unset; the chat graph resolves through it. Admin RolePicker
gains an 'Agent default' inherit option that clears the override.
* feat: multi-conversation review chat (tabs, new chat, history)
Replace the single per-PR chat thread with multiple per-user conversations:
- threads minted client-side; first message persists with a title derived
from the prompt.
- list + delete endpoints; chat panel gets a tab strip (history), new-chat
(+), close (x), refresh, an intro greeting, and suggested prompts.
- get_review_chat now returns availability only (ids are client-minted).
* ui fixes
* ui: review-chat history dropdown, full-width AI replies, resizable side panel
* fix(review-chat): enforce per-user thread ownership on proxy endpoints; reseed PR context on head change
---------
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
2026-06-15 17:17:30 -07:00
|
|
|
const messages = stream.messages
|
|
|
|
|
const busy = stream.isLoading
|
|
|
|
|
// True during the one-time getState hydration when switching to / loading an
|
|
|
|
|
// existing thread, before its messages have arrived.
|
|
|
|
|
const hydrating = stream.isThreadLoading
|
|
|
|
|
|
feat: split view, add-to-chat, virtualization + scroll/grouping perf (#1574)
* feat: reviews page split view, add-to-chat, virtualization + perf
- Virtualize the diff (Pierre Virtualizer + worker pool), mirroring the agent
chat panel, so large PRs window rows instead of materializing every line.
- Split chat and diff into independent scroll containers and make chat
auto-scroll fully contained, so typing/streaming no longer moves the diff.
- Memoize FileDiffCard with stable callbacks so focusing a finding re-renders
only the affected card.
- Add a persisted unified/split diff toggle.
- Add highlight-to-chat: select lines (drag / shift-click) + gutter "+" to drop
a file:line snippet into the chat composer.
- Rebuild sidebar group rows: whole card scrolls to the group (incl. the
expanded explanation), Read explanation stays a separate toggle, memoized.
* feat: add-to-chat uses attachment pills + selection popup / ⌘L
Replace the raw-snippet injection with a Cursor-style flow:
- Selecting lines shows a floating "Add to Chat ⌘L" popup at the pointer; ⌘L
adds the current selection without it. Removes the auto-adding gutter "+".
- "Add to chat" now creates a removable attachment pill in the composer (and a
pill in the sent message bubble) instead of pasting raw text. The code is
still serialized into the message content so the model receives it as context.
* feat: restore gutter + drag-handle for line selection
Re-enable Pierre's gutter '+' as a click-and-drag line selector (with the
highlight growing as you drag) — the affordance that was lost when the
auto-adding gutter button was removed. It no longer auto-adds: the commit flows
through onLineSelected to the 'Add to Chat' popup / ⌘L.
* fix: live selection highlight while dragging + reposition Add to Chat popup
- Feed onLineSelectionChange into the controlled selection so rows highlight
live as you drag, not just on release (Pierre only paints the controlled
selection when the prop updates). Popup now fires on onLineSelectionEnd.
- Anchor the popup's bottom-left to the drag handle (drop horizontal centering)
so it no longer overlaps the '+' button.
* fix: anchor Add to Chat popup to gutter handle + click-away to unselect
- Position the popup from the gutter '+' handle's rect (in the diff shadow DOM,
placed on the selection's bottom line) instead of the pointer-release point,
which landed inconsistently. Falls back to the pointer if not found.
- Clear the line selection (and popup) on any outside pointer-down.
* fix: sidebar-collapse header overlap + PR review comments
- Lift useSidebarLayout to AgentsShell (single source), share collapsed via
context, and pad the reviews header left when the sidebar is collapsed so the
fixed collapse toggle no longer overlaps the header content.
- add-to-chat: collect each diff side separately so a selection that spans a
deletion->addition no longer pastes wrong-file lines (PR comment).
- chat: clear attachments after sending via a suggested prompt, so an attached
snippet isn't silently resent on the next message (PR comment).
* fix: anchored finding card positions to the right of the diff again
The virtualization refactor moved the card inside the main-width Virtualizer
scroller, so it clamped over the diff. Render it in the outer container as a
viewport-fixed card clamped to window width (right gutter / over the side panel,
like prod) and track the finding as the diff scrolls (rAF-throttled), hiding
when the finding scrolls out of view.
2026-06-18 16:54:07 -07:00
|
|
|
// Receive "add to chat" attachments from the diff column as composer pills.
|
feat: chat with your PR on the review page (#1534)
* feat: chat with your PR on the review page
Add a sandbox-less `chat` graph that answers questions about a single PR
from its diff, the published review findings, and read-only GitHub access.
- agent/chat.py: deepagents graph, no sandbox (default StateBackend, file
mutation + execute tools excluded). PR context is seeded as virtual files
under /pr/; a repo-scoped App token is resolved in-graph.
- tools: read_repo_file, search_repo_code, list_review_findings.
- dashboard/review_chat_api.py + routes: per-user chat thread, LangGraph
stream/commands/state/history proxy pinned to the chat assistant, seeds
diff/findings/overview on first run. Gated by repo access.
- UI: Chat tab wired to a chat-scoped StreamProvider (replaces Coming Soon).
* feat: admin setting for review-chat default model
Add a 'Open SWE Review Chat' default to team settings (default_chat_model /
default_chat_reasoning_effort). get_team_default_model("chat") inherits the
Agent default when unset; the chat graph resolves through it. Admin RolePicker
gains an 'Agent default' inherit option that clears the override.
* feat: multi-conversation review chat (tabs, new chat, history)
Replace the single per-PR chat thread with multiple per-user conversations:
- threads minted client-side; first message persists with a title derived
from the prompt.
- list + delete endpoints; chat panel gets a tab strip (history), new-chat
(+), close (x), refresh, an intro greeting, and suggested prompts.
- get_review_chat now returns availability only (ids are client-minted).
* ui fixes
* ui: review-chat history dropdown, full-width AI replies, resizable side panel
* fix(review-chat): enforce per-user thread ownership on proxy endpoints; reseed PR context on head change
---------
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
2026-06-15 17:17:30 -07:00
|
|
|
useEffect(() => {
|
feat: split view, add-to-chat, virtualization + scroll/grouping perf (#1574)
* feat: reviews page split view, add-to-chat, virtualization + perf
- Virtualize the diff (Pierre Virtualizer + worker pool), mirroring the agent
chat panel, so large PRs window rows instead of materializing every line.
- Split chat and diff into independent scroll containers and make chat
auto-scroll fully contained, so typing/streaming no longer moves the diff.
- Memoize FileDiffCard with stable callbacks so focusing a finding re-renders
only the affected card.
- Add a persisted unified/split diff toggle.
- Add highlight-to-chat: select lines (drag / shift-click) + gutter "+" to drop
a file:line snippet into the chat composer.
- Rebuild sidebar group rows: whole card scrolls to the group (incl. the
expanded explanation), Read explanation stays a separate toggle, memoized.
* feat: add-to-chat uses attachment pills + selection popup / ⌘L
Replace the raw-snippet injection with a Cursor-style flow:
- Selecting lines shows a floating "Add to Chat ⌘L" popup at the pointer; ⌘L
adds the current selection without it. Removes the auto-adding gutter "+".
- "Add to chat" now creates a removable attachment pill in the composer (and a
pill in the sent message bubble) instead of pasting raw text. The code is
still serialized into the message content so the model receives it as context.
* feat: restore gutter + drag-handle for line selection
Re-enable Pierre's gutter '+' as a click-and-drag line selector (with the
highlight growing as you drag) — the affordance that was lost when the
auto-adding gutter button was removed. It no longer auto-adds: the commit flows
through onLineSelected to the 'Add to Chat' popup / ⌘L.
* fix: live selection highlight while dragging + reposition Add to Chat popup
- Feed onLineSelectionChange into the controlled selection so rows highlight
live as you drag, not just on release (Pierre only paints the controlled
selection when the prop updates). Popup now fires on onLineSelectionEnd.
- Anchor the popup's bottom-left to the drag handle (drop horizontal centering)
so it no longer overlaps the '+' button.
* fix: anchor Add to Chat popup to gutter handle + click-away to unselect
- Position the popup from the gutter '+' handle's rect (in the diff shadow DOM,
placed on the selection's bottom line) instead of the pointer-release point,
which landed inconsistently. Falls back to the pointer if not found.
- Clear the line selection (and popup) on any outside pointer-down.
* fix: sidebar-collapse header overlap + PR review comments
- Lift useSidebarLayout to AgentsShell (single source), share collapsed via
context, and pad the reviews header left when the sidebar is collapsed so the
fixed collapse toggle no longer overlaps the header content.
- add-to-chat: collect each diff side separately so a selection that spans a
deletion->addition no longer pastes wrong-file lines (PR comment).
- chat: clear attachments after sending via a suggested prompt, so an attached
snippet isn't silently resent on the next message (PR comment).
* fix: anchored finding card positions to the right of the diff again
The virtualization refactor moved the card inside the main-width Virtualizer
scroller, so it clamped over the diff. Render it in the outer container as a
viewport-fixed card clamped to window width (right gutter / over the side panel,
like prod) and track the finding as the diff scrolls (rAF-throttled), hiding
when the finding scrolls out of view.
2026-06-18 16:54:07 -07:00
|
|
|
if (!composer) return
|
|
|
|
|
composer.registerSink((attachment) =>
|
|
|
|
|
setAttachments((prev) =>
|
|
|
|
|
prev.some((a) => a.id === attachment.id) ? prev : [...prev, attachment]
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
return () => composer.registerSink(null)
|
|
|
|
|
}, [composer])
|
|
|
|
|
|
|
|
|
|
const removeAttachment = useCallback((id: string) => {
|
|
|
|
|
setAttachments((prev) => prev.filter((a) => a.id !== id))
|
|
|
|
|
}, [])
|
feat: chat with your PR on the review page (#1534)
* feat: chat with your PR on the review page
Add a sandbox-less `chat` graph that answers questions about a single PR
from its diff, the published review findings, and read-only GitHub access.
- agent/chat.py: deepagents graph, no sandbox (default StateBackend, file
mutation + execute tools excluded). PR context is seeded as virtual files
under /pr/; a repo-scoped App token is resolved in-graph.
- tools: read_repo_file, search_repo_code, list_review_findings.
- dashboard/review_chat_api.py + routes: per-user chat thread, LangGraph
stream/commands/state/history proxy pinned to the chat assistant, seeds
diff/findings/overview on first run. Gated by repo access.
- UI: Chat tab wired to a chat-scoped StreamProvider (replaces Coming Soon).
* feat: admin setting for review-chat default model
Add a 'Open SWE Review Chat' default to team settings (default_chat_model /
default_chat_reasoning_effort). get_team_default_model("chat") inherits the
Agent default when unset; the chat graph resolves through it. Admin RolePicker
gains an 'Agent default' inherit option that clears the override.
* feat: multi-conversation review chat (tabs, new chat, history)
Replace the single per-PR chat thread with multiple per-user conversations:
- threads minted client-side; first message persists with a title derived
from the prompt.
- list + delete endpoints; chat panel gets a tab strip (history), new-chat
(+), close (x), refresh, an intro greeting, and suggested prompts.
- get_review_chat now returns availability only (ids are client-minted).
* ui fixes
* ui: review-chat history dropdown, full-width AI replies, resizable side panel
* fix(review-chat): enforce per-user thread ownership on proxy endpoints; reseed PR context on head change
---------
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
2026-06-15 17:17:30 -07:00
|
|
|
|
|
|
|
|
const send = useCallback(
|
feat: split view, add-to-chat, virtualization + scroll/grouping perf (#1574)
* feat: reviews page split view, add-to-chat, virtualization + perf
- Virtualize the diff (Pierre Virtualizer + worker pool), mirroring the agent
chat panel, so large PRs window rows instead of materializing every line.
- Split chat and diff into independent scroll containers and make chat
auto-scroll fully contained, so typing/streaming no longer moves the diff.
- Memoize FileDiffCard with stable callbacks so focusing a finding re-renders
only the affected card.
- Add a persisted unified/split diff toggle.
- Add highlight-to-chat: select lines (drag / shift-click) + gutter "+" to drop
a file:line snippet into the chat composer.
- Rebuild sidebar group rows: whole card scrolls to the group (incl. the
expanded explanation), Read explanation stays a separate toggle, memoized.
* feat: add-to-chat uses attachment pills + selection popup / ⌘L
Replace the raw-snippet injection with a Cursor-style flow:
- Selecting lines shows a floating "Add to Chat ⌘L" popup at the pointer; ⌘L
adds the current selection without it. Removes the auto-adding gutter "+".
- "Add to chat" now creates a removable attachment pill in the composer (and a
pill in the sent message bubble) instead of pasting raw text. The code is
still serialized into the message content so the model receives it as context.
* feat: restore gutter + drag-handle for line selection
Re-enable Pierre's gutter '+' as a click-and-drag line selector (with the
highlight growing as you drag) — the affordance that was lost when the
auto-adding gutter button was removed. It no longer auto-adds: the commit flows
through onLineSelected to the 'Add to Chat' popup / ⌘L.
* fix: live selection highlight while dragging + reposition Add to Chat popup
- Feed onLineSelectionChange into the controlled selection so rows highlight
live as you drag, not just on release (Pierre only paints the controlled
selection when the prop updates). Popup now fires on onLineSelectionEnd.
- Anchor the popup's bottom-left to the drag handle (drop horizontal centering)
so it no longer overlaps the '+' button.
* fix: anchor Add to Chat popup to gutter handle + click-away to unselect
- Position the popup from the gutter '+' handle's rect (in the diff shadow DOM,
placed on the selection's bottom line) instead of the pointer-release point,
which landed inconsistently. Falls back to the pointer if not found.
- Clear the line selection (and popup) on any outside pointer-down.
* fix: sidebar-collapse header overlap + PR review comments
- Lift useSidebarLayout to AgentsShell (single source), share collapsed via
context, and pad the reviews header left when the sidebar is collapsed so the
fixed collapse toggle no longer overlaps the header content.
- add-to-chat: collect each diff side separately so a selection that spans a
deletion->addition no longer pastes wrong-file lines (PR comment).
- chat: clear attachments after sending via a suggested prompt, so an attached
snippet isn't silently resent on the next message (PR comment).
* fix: anchored finding card positions to the right of the diff again
The virtualization refactor moved the card inside the main-width Virtualizer
scroller, so it clamped over the diff. Render it in the outer container as a
viewport-fixed card clamped to window width (right gutter / over the side panel,
like prod) and track the finding as the diff scrolls (rAF-throttled), hiding
when the finding scrolls out of view.
2026-06-18 16:54:07 -07:00
|
|
|
(text: string, atts: Array<ChatAttachment>) => {
|
feat: chat with your PR on the review page (#1534)
* feat: chat with your PR on the review page
Add a sandbox-less `chat` graph that answers questions about a single PR
from its diff, the published review findings, and read-only GitHub access.
- agent/chat.py: deepagents graph, no sandbox (default StateBackend, file
mutation + execute tools excluded). PR context is seeded as virtual files
under /pr/; a repo-scoped App token is resolved in-graph.
- tools: read_repo_file, search_repo_code, list_review_findings.
- dashboard/review_chat_api.py + routes: per-user chat thread, LangGraph
stream/commands/state/history proxy pinned to the chat assistant, seeds
diff/findings/overview on first run. Gated by repo access.
- UI: Chat tab wired to a chat-scoped StreamProvider (replaces Coming Soon).
* feat: admin setting for review-chat default model
Add a 'Open SWE Review Chat' default to team settings (default_chat_model /
default_chat_reasoning_effort). get_team_default_model("chat") inherits the
Agent default when unset; the chat graph resolves through it. Admin RolePicker
gains an 'Agent default' inherit option that clears the override.
* feat: multi-conversation review chat (tabs, new chat, history)
Replace the single per-PR chat thread with multiple per-user conversations:
- threads minted client-side; first message persists with a title derived
from the prompt.
- list + delete endpoints; chat panel gets a tab strip (history), new-chat
(+), close (x), refresh, an intro greeting, and suggested prompts.
- get_review_chat now returns availability only (ids are client-minted).
* ui fixes
* ui: review-chat history dropdown, full-width AI replies, resizable side panel
* fix(review-chat): enforce per-user thread ownership on proxy endpoints; reseed PR context on head change
---------
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
2026-06-15 17:17:30 -07:00
|
|
|
const trimmed = text.trim()
|
feat: split view, add-to-chat, virtualization + scroll/grouping perf (#1574)
* feat: reviews page split view, add-to-chat, virtualization + perf
- Virtualize the diff (Pierre Virtualizer + worker pool), mirroring the agent
chat panel, so large PRs window rows instead of materializing every line.
- Split chat and diff into independent scroll containers and make chat
auto-scroll fully contained, so typing/streaming no longer moves the diff.
- Memoize FileDiffCard with stable callbacks so focusing a finding re-renders
only the affected card.
- Add a persisted unified/split diff toggle.
- Add highlight-to-chat: select lines (drag / shift-click) + gutter "+" to drop
a file:line snippet into the chat composer.
- Rebuild sidebar group rows: whole card scrolls to the group (incl. the
expanded explanation), Read explanation stays a separate toggle, memoized.
* feat: add-to-chat uses attachment pills + selection popup / ⌘L
Replace the raw-snippet injection with a Cursor-style flow:
- Selecting lines shows a floating "Add to Chat ⌘L" popup at the pointer; ⌘L
adds the current selection without it. Removes the auto-adding gutter "+".
- "Add to chat" now creates a removable attachment pill in the composer (and a
pill in the sent message bubble) instead of pasting raw text. The code is
still serialized into the message content so the model receives it as context.
* feat: restore gutter + drag-handle for line selection
Re-enable Pierre's gutter '+' as a click-and-drag line selector (with the
highlight growing as you drag) — the affordance that was lost when the
auto-adding gutter button was removed. It no longer auto-adds: the commit flows
through onLineSelected to the 'Add to Chat' popup / ⌘L.
* fix: live selection highlight while dragging + reposition Add to Chat popup
- Feed onLineSelectionChange into the controlled selection so rows highlight
live as you drag, not just on release (Pierre only paints the controlled
selection when the prop updates). Popup now fires on onLineSelectionEnd.
- Anchor the popup's bottom-left to the drag handle (drop horizontal centering)
so it no longer overlaps the '+' button.
* fix: anchor Add to Chat popup to gutter handle + click-away to unselect
- Position the popup from the gutter '+' handle's rect (in the diff shadow DOM,
placed on the selection's bottom line) instead of the pointer-release point,
which landed inconsistently. Falls back to the pointer if not found.
- Clear the line selection (and popup) on any outside pointer-down.
* fix: sidebar-collapse header overlap + PR review comments
- Lift useSidebarLayout to AgentsShell (single source), share collapsed via
context, and pad the reviews header left when the sidebar is collapsed so the
fixed collapse toggle no longer overlaps the header content.
- add-to-chat: collect each diff side separately so a selection that spans a
deletion->addition no longer pastes wrong-file lines (PR comment).
- chat: clear attachments after sending via a suggested prompt, so an attached
snippet isn't silently resent on the next message (PR comment).
* fix: anchored finding card positions to the right of the diff again
The virtualization refactor moved the card inside the main-width Virtualizer
scroller, so it clamped over the diff. Render it in the outer container as a
viewport-fixed card clamped to window width (right gutter / over the side panel,
like prod) and track the finding as the diff scrolls (rAF-throttled), hiding
when the finding scrolls out of view.
2026-06-18 16:54:07 -07:00
|
|
|
const first = atts[0]
|
|
|
|
|
if ((!trimmed && !first) || busy) return
|
|
|
|
|
const content = serializeMessage(trimmed, atts)
|
|
|
|
|
onUserSend(trimmed || (first ? attachmentPillLabel(first) : ""))
|
|
|
|
|
void stream.submit({ messages: [{ type: "human", content }] })
|
feat: chat with your PR on the review page (#1534)
* feat: chat with your PR on the review page
Add a sandbox-less `chat` graph that answers questions about a single PR
from its diff, the published review findings, and read-only GitHub access.
- agent/chat.py: deepagents graph, no sandbox (default StateBackend, file
mutation + execute tools excluded). PR context is seeded as virtual files
under /pr/; a repo-scoped App token is resolved in-graph.
- tools: read_repo_file, search_repo_code, list_review_findings.
- dashboard/review_chat_api.py + routes: per-user chat thread, LangGraph
stream/commands/state/history proxy pinned to the chat assistant, seeds
diff/findings/overview on first run. Gated by repo access.
- UI: Chat tab wired to a chat-scoped StreamProvider (replaces Coming Soon).
* feat: admin setting for review-chat default model
Add a 'Open SWE Review Chat' default to team settings (default_chat_model /
default_chat_reasoning_effort). get_team_default_model("chat") inherits the
Agent default when unset; the chat graph resolves through it. Admin RolePicker
gains an 'Agent default' inherit option that clears the override.
* feat: multi-conversation review chat (tabs, new chat, history)
Replace the single per-PR chat thread with multiple per-user conversations:
- threads minted client-side; first message persists with a title derived
from the prompt.
- list + delete endpoints; chat panel gets a tab strip (history), new-chat
(+), close (x), refresh, an intro greeting, and suggested prompts.
- get_review_chat now returns availability only (ids are client-minted).
* ui fixes
* ui: review-chat history dropdown, full-width AI replies, resizable side panel
* fix(review-chat): enforce per-user thread ownership on proxy endpoints; reseed PR context on head change
---------
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
2026-06-15 17:17:30 -07:00
|
|
|
},
|
|
|
|
|
[busy, stream, onUserSend],
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const visible = messages.filter((message) => {
|
|
|
|
|
const type = messageType(message)
|
|
|
|
|
if (type !== "human" && type !== "ai") return false
|
|
|
|
|
return messageText(message.content).trim().length > 0
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const submitComposer = () => {
|
feat: split view, add-to-chat, virtualization + scroll/grouping perf (#1574)
* feat: reviews page split view, add-to-chat, virtualization + perf
- Virtualize the diff (Pierre Virtualizer + worker pool), mirroring the agent
chat panel, so large PRs window rows instead of materializing every line.
- Split chat and diff into independent scroll containers and make chat
auto-scroll fully contained, so typing/streaming no longer moves the diff.
- Memoize FileDiffCard with stable callbacks so focusing a finding re-renders
only the affected card.
- Add a persisted unified/split diff toggle.
- Add highlight-to-chat: select lines (drag / shift-click) + gutter "+" to drop
a file:line snippet into the chat composer.
- Rebuild sidebar group rows: whole card scrolls to the group (incl. the
expanded explanation), Read explanation stays a separate toggle, memoized.
* feat: add-to-chat uses attachment pills + selection popup / ⌘L
Replace the raw-snippet injection with a Cursor-style flow:
- Selecting lines shows a floating "Add to Chat ⌘L" popup at the pointer; ⌘L
adds the current selection without it. Removes the auto-adding gutter "+".
- "Add to chat" now creates a removable attachment pill in the composer (and a
pill in the sent message bubble) instead of pasting raw text. The code is
still serialized into the message content so the model receives it as context.
* feat: restore gutter + drag-handle for line selection
Re-enable Pierre's gutter '+' as a click-and-drag line selector (with the
highlight growing as you drag) — the affordance that was lost when the
auto-adding gutter button was removed. It no longer auto-adds: the commit flows
through onLineSelected to the 'Add to Chat' popup / ⌘L.
* fix: live selection highlight while dragging + reposition Add to Chat popup
- Feed onLineSelectionChange into the controlled selection so rows highlight
live as you drag, not just on release (Pierre only paints the controlled
selection when the prop updates). Popup now fires on onLineSelectionEnd.
- Anchor the popup's bottom-left to the drag handle (drop horizontal centering)
so it no longer overlaps the '+' button.
* fix: anchor Add to Chat popup to gutter handle + click-away to unselect
- Position the popup from the gutter '+' handle's rect (in the diff shadow DOM,
placed on the selection's bottom line) instead of the pointer-release point,
which landed inconsistently. Falls back to the pointer if not found.
- Clear the line selection (and popup) on any outside pointer-down.
* fix: sidebar-collapse header overlap + PR review comments
- Lift useSidebarLayout to AgentsShell (single source), share collapsed via
context, and pad the reviews header left when the sidebar is collapsed so the
fixed collapse toggle no longer overlaps the header content.
- add-to-chat: collect each diff side separately so a selection that spans a
deletion->addition no longer pastes wrong-file lines (PR comment).
- chat: clear attachments after sending via a suggested prompt, so an attached
snippet isn't silently resent on the next message (PR comment).
* fix: anchored finding card positions to the right of the diff again
The virtualization refactor moved the card inside the main-width Virtualizer
scroller, so it clamped over the diff. Render it in the outer container as a
viewport-fixed card clamped to window width (right gutter / over the side panel,
like prod) and track the finding as the diff scrolls (rAF-throttled), hiding
when the finding scrolls out of view.
2026-06-18 16:54:07 -07:00
|
|
|
send(value, attachments)
|
feat: chat with your PR on the review page (#1534)
* feat: chat with your PR on the review page
Add a sandbox-less `chat` graph that answers questions about a single PR
from its diff, the published review findings, and read-only GitHub access.
- agent/chat.py: deepagents graph, no sandbox (default StateBackend, file
mutation + execute tools excluded). PR context is seeded as virtual files
under /pr/; a repo-scoped App token is resolved in-graph.
- tools: read_repo_file, search_repo_code, list_review_findings.
- dashboard/review_chat_api.py + routes: per-user chat thread, LangGraph
stream/commands/state/history proxy pinned to the chat assistant, seeds
diff/findings/overview on first run. Gated by repo access.
- UI: Chat tab wired to a chat-scoped StreamProvider (replaces Coming Soon).
* feat: admin setting for review-chat default model
Add a 'Open SWE Review Chat' default to team settings (default_chat_model /
default_chat_reasoning_effort). get_team_default_model("chat") inherits the
Agent default when unset; the chat graph resolves through it. Admin RolePicker
gains an 'Agent default' inherit option that clears the override.
* feat: multi-conversation review chat (tabs, new chat, history)
Replace the single per-PR chat thread with multiple per-user conversations:
- threads minted client-side; first message persists with a title derived
from the prompt.
- list + delete endpoints; chat panel gets a tab strip (history), new-chat
(+), close (x), refresh, an intro greeting, and suggested prompts.
- get_review_chat now returns availability only (ids are client-minted).
* ui fixes
* ui: review-chat history dropdown, full-width AI replies, resizable side panel
* fix(review-chat): enforce per-user thread ownership on proxy endpoints; reseed PR context on head change
---------
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
2026-06-15 17:17:30 -07:00
|
|
|
setValue("")
|
feat: split view, add-to-chat, virtualization + scroll/grouping perf (#1574)
* feat: reviews page split view, add-to-chat, virtualization + perf
- Virtualize the diff (Pierre Virtualizer + worker pool), mirroring the agent
chat panel, so large PRs window rows instead of materializing every line.
- Split chat and diff into independent scroll containers and make chat
auto-scroll fully contained, so typing/streaming no longer moves the diff.
- Memoize FileDiffCard with stable callbacks so focusing a finding re-renders
only the affected card.
- Add a persisted unified/split diff toggle.
- Add highlight-to-chat: select lines (drag / shift-click) + gutter "+" to drop
a file:line snippet into the chat composer.
- Rebuild sidebar group rows: whole card scrolls to the group (incl. the
expanded explanation), Read explanation stays a separate toggle, memoized.
* feat: add-to-chat uses attachment pills + selection popup / ⌘L
Replace the raw-snippet injection with a Cursor-style flow:
- Selecting lines shows a floating "Add to Chat ⌘L" popup at the pointer; ⌘L
adds the current selection without it. Removes the auto-adding gutter "+".
- "Add to chat" now creates a removable attachment pill in the composer (and a
pill in the sent message bubble) instead of pasting raw text. The code is
still serialized into the message content so the model receives it as context.
* feat: restore gutter + drag-handle for line selection
Re-enable Pierre's gutter '+' as a click-and-drag line selector (with the
highlight growing as you drag) — the affordance that was lost when the
auto-adding gutter button was removed. It no longer auto-adds: the commit flows
through onLineSelected to the 'Add to Chat' popup / ⌘L.
* fix: live selection highlight while dragging + reposition Add to Chat popup
- Feed onLineSelectionChange into the controlled selection so rows highlight
live as you drag, not just on release (Pierre only paints the controlled
selection when the prop updates). Popup now fires on onLineSelectionEnd.
- Anchor the popup's bottom-left to the drag handle (drop horizontal centering)
so it no longer overlaps the '+' button.
* fix: anchor Add to Chat popup to gutter handle + click-away to unselect
- Position the popup from the gutter '+' handle's rect (in the diff shadow DOM,
placed on the selection's bottom line) instead of the pointer-release point,
which landed inconsistently. Falls back to the pointer if not found.
- Clear the line selection (and popup) on any outside pointer-down.
* fix: sidebar-collapse header overlap + PR review comments
- Lift useSidebarLayout to AgentsShell (single source), share collapsed via
context, and pad the reviews header left when the sidebar is collapsed so the
fixed collapse toggle no longer overlaps the header content.
- add-to-chat: collect each diff side separately so a selection that spans a
deletion->addition no longer pastes wrong-file lines (PR comment).
- chat: clear attachments after sending via a suggested prompt, so an attached
snippet isn't silently resent on the next message (PR comment).
* fix: anchored finding card positions to the right of the diff again
The virtualization refactor moved the card inside the main-width Virtualizer
scroller, so it clamped over the diff. Render it in the outer container as a
viewport-fixed card clamped to window width (right gutter / over the side panel,
like prod) and track the finding as the diff scrolls (rAF-throttled), hiding
when the finding scrolls out of view.
2026-06-18 16:54:07 -07:00
|
|
|
setAttachments([])
|
feat: chat with your PR on the review page (#1534)
* feat: chat with your PR on the review page
Add a sandbox-less `chat` graph that answers questions about a single PR
from its diff, the published review findings, and read-only GitHub access.
- agent/chat.py: deepagents graph, no sandbox (default StateBackend, file
mutation + execute tools excluded). PR context is seeded as virtual files
under /pr/; a repo-scoped App token is resolved in-graph.
- tools: read_repo_file, search_repo_code, list_review_findings.
- dashboard/review_chat_api.py + routes: per-user chat thread, LangGraph
stream/commands/state/history proxy pinned to the chat assistant, seeds
diff/findings/overview on first run. Gated by repo access.
- UI: Chat tab wired to a chat-scoped StreamProvider (replaces Coming Soon).
* feat: admin setting for review-chat default model
Add a 'Open SWE Review Chat' default to team settings (default_chat_model /
default_chat_reasoning_effort). get_team_default_model("chat") inherits the
Agent default when unset; the chat graph resolves through it. Admin RolePicker
gains an 'Agent default' inherit option that clears the override.
* feat: multi-conversation review chat (tabs, new chat, history)
Replace the single per-PR chat thread with multiple per-user conversations:
- threads minted client-side; first message persists with a title derived
from the prompt.
- list + delete endpoints; chat panel gets a tab strip (history), new-chat
(+), close (x), refresh, an intro greeting, and suggested prompts.
- get_review_chat now returns availability only (ids are client-minted).
* ui fixes
* ui: review-chat history dropdown, full-width AI replies, resizable side panel
* fix(review-chat): enforce per-user thread ownership on proxy endpoints; reseed PR context on head change
---------
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
2026-06-15 17:17:30 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Show the loading placeholder (not the empty/intro state) while an existing
|
|
|
|
|
// conversation hydrates, so a chat with messages never flashes its greeting.
|
|
|
|
|
const showEmpty = visible.length === 0 && !busy
|
|
|
|
|
const showLoading = showEmpty && hydrating && expectsHistory
|
feat: split view, add-to-chat, virtualization + scroll/grouping perf (#1574)
* feat: reviews page split view, add-to-chat, virtualization + perf
- Virtualize the diff (Pierre Virtualizer + worker pool), mirroring the agent
chat panel, so large PRs window rows instead of materializing every line.
- Split chat and diff into independent scroll containers and make chat
auto-scroll fully contained, so typing/streaming no longer moves the diff.
- Memoize FileDiffCard with stable callbacks so focusing a finding re-renders
only the affected card.
- Add a persisted unified/split diff toggle.
- Add highlight-to-chat: select lines (drag / shift-click) + gutter "+" to drop
a file:line snippet into the chat composer.
- Rebuild sidebar group rows: whole card scrolls to the group (incl. the
expanded explanation), Read explanation stays a separate toggle, memoized.
* feat: add-to-chat uses attachment pills + selection popup / ⌘L
Replace the raw-snippet injection with a Cursor-style flow:
- Selecting lines shows a floating "Add to Chat ⌘L" popup at the pointer; ⌘L
adds the current selection without it. Removes the auto-adding gutter "+".
- "Add to chat" now creates a removable attachment pill in the composer (and a
pill in the sent message bubble) instead of pasting raw text. The code is
still serialized into the message content so the model receives it as context.
* feat: restore gutter + drag-handle for line selection
Re-enable Pierre's gutter '+' as a click-and-drag line selector (with the
highlight growing as you drag) — the affordance that was lost when the
auto-adding gutter button was removed. It no longer auto-adds: the commit flows
through onLineSelected to the 'Add to Chat' popup / ⌘L.
* fix: live selection highlight while dragging + reposition Add to Chat popup
- Feed onLineSelectionChange into the controlled selection so rows highlight
live as you drag, not just on release (Pierre only paints the controlled
selection when the prop updates). Popup now fires on onLineSelectionEnd.
- Anchor the popup's bottom-left to the drag handle (drop horizontal centering)
so it no longer overlaps the '+' button.
* fix: anchor Add to Chat popup to gutter handle + click-away to unselect
- Position the popup from the gutter '+' handle's rect (in the diff shadow DOM,
placed on the selection's bottom line) instead of the pointer-release point,
which landed inconsistently. Falls back to the pointer if not found.
- Clear the line selection (and popup) on any outside pointer-down.
* fix: sidebar-collapse header overlap + PR review comments
- Lift useSidebarLayout to AgentsShell (single source), share collapsed via
context, and pad the reviews header left when the sidebar is collapsed so the
fixed collapse toggle no longer overlaps the header content.
- add-to-chat: collect each diff side separately so a selection that spans a
deletion->addition no longer pastes wrong-file lines (PR comment).
- chat: clear attachments after sending via a suggested prompt, so an attached
snippet isn't silently resent on the next message (PR comment).
* fix: anchored finding card positions to the right of the diff again
The virtualization refactor moved the card inside the main-width Virtualizer
scroller, so it clamped over the diff. Render it in the outer container as a
viewport-fixed card clamped to window width (right gutter / over the side panel,
like prod) and track the finding as the diff scrolls (rAF-throttled), hiding
when the finding scrolls out of view.
2026-06-18 16:54:07 -07:00
|
|
|
const showMessages = !showEmpty && !showLoading
|
|
|
|
|
|
|
|
|
|
// Auto-scroll is fully contained to the messages list (scrollTop assignment,
|
|
|
|
|
// never scrollIntoView) so it can't bubble up and move the diff column. We
|
|
|
|
|
// pause auto-scroll when the user scrolls up and resume when near the bottom.
|
|
|
|
|
useLayoutEffect(() => {
|
|
|
|
|
if (!showMessages) return
|
|
|
|
|
const el = scrollRef.current
|
|
|
|
|
if (!el || !autoScrollRef.current) return
|
|
|
|
|
el.scrollTop = el.scrollHeight
|
|
|
|
|
prevTopRef.current = el.scrollTop
|
|
|
|
|
}, [showMessages, messages, busy])
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (!showMessages) return
|
|
|
|
|
const el = scrollRef.current
|
|
|
|
|
if (!el) return
|
|
|
|
|
const onScroll = () => {
|
|
|
|
|
const top = el.scrollTop
|
|
|
|
|
const nearBottom = el.scrollHeight - top - el.clientHeight <= 24
|
|
|
|
|
if (top < prevTopRef.current - 1) autoScrollRef.current = false
|
|
|
|
|
else if (nearBottom) autoScrollRef.current = true
|
|
|
|
|
prevTopRef.current = top
|
|
|
|
|
}
|
|
|
|
|
el.addEventListener("scroll", onScroll, { passive: true })
|
|
|
|
|
return () => el.removeEventListener("scroll", onScroll)
|
|
|
|
|
}, [showMessages])
|
feat: chat with your PR on the review page (#1534)
* feat: chat with your PR on the review page
Add a sandbox-less `chat` graph that answers questions about a single PR
from its diff, the published review findings, and read-only GitHub access.
- agent/chat.py: deepagents graph, no sandbox (default StateBackend, file
mutation + execute tools excluded). PR context is seeded as virtual files
under /pr/; a repo-scoped App token is resolved in-graph.
- tools: read_repo_file, search_repo_code, list_review_findings.
- dashboard/review_chat_api.py + routes: per-user chat thread, LangGraph
stream/commands/state/history proxy pinned to the chat assistant, seeds
diff/findings/overview on first run. Gated by repo access.
- UI: Chat tab wired to a chat-scoped StreamProvider (replaces Coming Soon).
* feat: admin setting for review-chat default model
Add a 'Open SWE Review Chat' default to team settings (default_chat_model /
default_chat_reasoning_effort). get_team_default_model("chat") inherits the
Agent default when unset; the chat graph resolves through it. Admin RolePicker
gains an 'Agent default' inherit option that clears the override.
* feat: multi-conversation review chat (tabs, new chat, history)
Replace the single per-PR chat thread with multiple per-user conversations:
- threads minted client-side; first message persists with a title derived
from the prompt.
- list + delete endpoints; chat panel gets a tab strip (history), new-chat
(+), close (x), refresh, an intro greeting, and suggested prompts.
- get_review_chat now returns availability only (ids are client-minted).
* ui fixes
* ui: review-chat history dropdown, full-width AI replies, resizable side panel
* fix(review-chat): enforce per-user thread ownership on proxy endpoints; reseed PR context on head change
---------
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
2026-06-15 17:17:30 -07:00
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="flex flex-1 flex-col overflow-hidden">
|
|
|
|
|
{showLoading ? (
|
|
|
|
|
<LoadingState />
|
|
|
|
|
) : showEmpty ? (
|
feat: split view, add-to-chat, virtualization + scroll/grouping perf (#1574)
* feat: reviews page split view, add-to-chat, virtualization + perf
- Virtualize the diff (Pierre Virtualizer + worker pool), mirroring the agent
chat panel, so large PRs window rows instead of materializing every line.
- Split chat and diff into independent scroll containers and make chat
auto-scroll fully contained, so typing/streaming no longer moves the diff.
- Memoize FileDiffCard with stable callbacks so focusing a finding re-renders
only the affected card.
- Add a persisted unified/split diff toggle.
- Add highlight-to-chat: select lines (drag / shift-click) + gutter "+" to drop
a file:line snippet into the chat composer.
- Rebuild sidebar group rows: whole card scrolls to the group (incl. the
expanded explanation), Read explanation stays a separate toggle, memoized.
* feat: add-to-chat uses attachment pills + selection popup / ⌘L
Replace the raw-snippet injection with a Cursor-style flow:
- Selecting lines shows a floating "Add to Chat ⌘L" popup at the pointer; ⌘L
adds the current selection without it. Removes the auto-adding gutter "+".
- "Add to chat" now creates a removable attachment pill in the composer (and a
pill in the sent message bubble) instead of pasting raw text. The code is
still serialized into the message content so the model receives it as context.
* feat: restore gutter + drag-handle for line selection
Re-enable Pierre's gutter '+' as a click-and-drag line selector (with the
highlight growing as you drag) — the affordance that was lost when the
auto-adding gutter button was removed. It no longer auto-adds: the commit flows
through onLineSelected to the 'Add to Chat' popup / ⌘L.
* fix: live selection highlight while dragging + reposition Add to Chat popup
- Feed onLineSelectionChange into the controlled selection so rows highlight
live as you drag, not just on release (Pierre only paints the controlled
selection when the prop updates). Popup now fires on onLineSelectionEnd.
- Anchor the popup's bottom-left to the drag handle (drop horizontal centering)
so it no longer overlaps the '+' button.
* fix: anchor Add to Chat popup to gutter handle + click-away to unselect
- Position the popup from the gutter '+' handle's rect (in the diff shadow DOM,
placed on the selection's bottom line) instead of the pointer-release point,
which landed inconsistently. Falls back to the pointer if not found.
- Clear the line selection (and popup) on any outside pointer-down.
* fix: sidebar-collapse header overlap + PR review comments
- Lift useSidebarLayout to AgentsShell (single source), share collapsed via
context, and pad the reviews header left when the sidebar is collapsed so the
fixed collapse toggle no longer overlaps the header content.
- add-to-chat: collect each diff side separately so a selection that spans a
deletion->addition no longer pastes wrong-file lines (PR comment).
- chat: clear attachments after sending via a suggested prompt, so an attached
snippet isn't silently resent on the next message (PR comment).
* fix: anchored finding card positions to the right of the diff again
The virtualization refactor moved the card inside the main-width Virtualizer
scroller, so it clamped over the diff. Render it in the outer container as a
viewport-fixed card clamped to window width (right gutter / over the side panel,
like prod) and track the finding as the diff scrolls (rAF-throttled), hiding
when the finding scrolls out of view.
2026-06-18 16:54:07 -07:00
|
|
|
<EmptyState
|
|
|
|
|
onPick={(prompt) => {
|
|
|
|
|
send(prompt, attachments)
|
|
|
|
|
setAttachments([])
|
|
|
|
|
}}
|
|
|
|
|
/>
|
feat: chat with your PR on the review page (#1534)
* feat: chat with your PR on the review page
Add a sandbox-less `chat` graph that answers questions about a single PR
from its diff, the published review findings, and read-only GitHub access.
- agent/chat.py: deepagents graph, no sandbox (default StateBackend, file
mutation + execute tools excluded). PR context is seeded as virtual files
under /pr/; a repo-scoped App token is resolved in-graph.
- tools: read_repo_file, search_repo_code, list_review_findings.
- dashboard/review_chat_api.py + routes: per-user chat thread, LangGraph
stream/commands/state/history proxy pinned to the chat assistant, seeds
diff/findings/overview on first run. Gated by repo access.
- UI: Chat tab wired to a chat-scoped StreamProvider (replaces Coming Soon).
* feat: admin setting for review-chat default model
Add a 'Open SWE Review Chat' default to team settings (default_chat_model /
default_chat_reasoning_effort). get_team_default_model("chat") inherits the
Agent default when unset; the chat graph resolves through it. Admin RolePicker
gains an 'Agent default' inherit option that clears the override.
* feat: multi-conversation review chat (tabs, new chat, history)
Replace the single per-PR chat thread with multiple per-user conversations:
- threads minted client-side; first message persists with a title derived
from the prompt.
- list + delete endpoints; chat panel gets a tab strip (history), new-chat
(+), close (x), refresh, an intro greeting, and suggested prompts.
- get_review_chat now returns availability only (ids are client-minted).
* ui fixes
* ui: review-chat history dropdown, full-width AI replies, resizable side panel
* fix(review-chat): enforce per-user thread ownership on proxy endpoints; reseed PR context on head change
---------
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
2026-06-15 17:17:30 -07:00
|
|
|
) : (
|
feat: split view, add-to-chat, virtualization + scroll/grouping perf (#1574)
* feat: reviews page split view, add-to-chat, virtualization + perf
- Virtualize the diff (Pierre Virtualizer + worker pool), mirroring the agent
chat panel, so large PRs window rows instead of materializing every line.
- Split chat and diff into independent scroll containers and make chat
auto-scroll fully contained, so typing/streaming no longer moves the diff.
- Memoize FileDiffCard with stable callbacks so focusing a finding re-renders
only the affected card.
- Add a persisted unified/split diff toggle.
- Add highlight-to-chat: select lines (drag / shift-click) + gutter "+" to drop
a file:line snippet into the chat composer.
- Rebuild sidebar group rows: whole card scrolls to the group (incl. the
expanded explanation), Read explanation stays a separate toggle, memoized.
* feat: add-to-chat uses attachment pills + selection popup / ⌘L
Replace the raw-snippet injection with a Cursor-style flow:
- Selecting lines shows a floating "Add to Chat ⌘L" popup at the pointer; ⌘L
adds the current selection without it. Removes the auto-adding gutter "+".
- "Add to chat" now creates a removable attachment pill in the composer (and a
pill in the sent message bubble) instead of pasting raw text. The code is
still serialized into the message content so the model receives it as context.
* feat: restore gutter + drag-handle for line selection
Re-enable Pierre's gutter '+' as a click-and-drag line selector (with the
highlight growing as you drag) — the affordance that was lost when the
auto-adding gutter button was removed. It no longer auto-adds: the commit flows
through onLineSelected to the 'Add to Chat' popup / ⌘L.
* fix: live selection highlight while dragging + reposition Add to Chat popup
- Feed onLineSelectionChange into the controlled selection so rows highlight
live as you drag, not just on release (Pierre only paints the controlled
selection when the prop updates). Popup now fires on onLineSelectionEnd.
- Anchor the popup's bottom-left to the drag handle (drop horizontal centering)
so it no longer overlaps the '+' button.
* fix: anchor Add to Chat popup to gutter handle + click-away to unselect
- Position the popup from the gutter '+' handle's rect (in the diff shadow DOM,
placed on the selection's bottom line) instead of the pointer-release point,
which landed inconsistently. Falls back to the pointer if not found.
- Clear the line selection (and popup) on any outside pointer-down.
* fix: sidebar-collapse header overlap + PR review comments
- Lift useSidebarLayout to AgentsShell (single source), share collapsed via
context, and pad the reviews header left when the sidebar is collapsed so the
fixed collapse toggle no longer overlaps the header content.
- add-to-chat: collect each diff side separately so a selection that spans a
deletion->addition no longer pastes wrong-file lines (PR comment).
- chat: clear attachments after sending via a suggested prompt, so an attached
snippet isn't silently resent on the next message (PR comment).
* fix: anchored finding card positions to the right of the diff again
The virtualization refactor moved the card inside the main-width Virtualizer
scroller, so it clamped over the diff. Render it in the outer container as a
viewport-fixed card clamped to window width (right gutter / over the side panel,
like prod) and track the finding as the diff scrolls (rAF-throttled), hiding
when the finding scrolls out of view.
2026-06-18 16:54:07 -07:00
|
|
|
<div
|
|
|
|
|
ref={scrollRef}
|
|
|
|
|
className="flex flex-1 flex-col gap-4 overflow-y-auto p-4"
|
|
|
|
|
>
|
feat: chat with your PR on the review page (#1534)
* feat: chat with your PR on the review page
Add a sandbox-less `chat` graph that answers questions about a single PR
from its diff, the published review findings, and read-only GitHub access.
- agent/chat.py: deepagents graph, no sandbox (default StateBackend, file
mutation + execute tools excluded). PR context is seeded as virtual files
under /pr/; a repo-scoped App token is resolved in-graph.
- tools: read_repo_file, search_repo_code, list_review_findings.
- dashboard/review_chat_api.py + routes: per-user chat thread, LangGraph
stream/commands/state/history proxy pinned to the chat assistant, seeds
diff/findings/overview on first run. Gated by repo access.
- UI: Chat tab wired to a chat-scoped StreamProvider (replaces Coming Soon).
* feat: admin setting for review-chat default model
Add a 'Open SWE Review Chat' default to team settings (default_chat_model /
default_chat_reasoning_effort). get_team_default_model("chat") inherits the
Agent default when unset; the chat graph resolves through it. Admin RolePicker
gains an 'Agent default' inherit option that clears the override.
* feat: multi-conversation review chat (tabs, new chat, history)
Replace the single per-PR chat thread with multiple per-user conversations:
- threads minted client-side; first message persists with a title derived
from the prompt.
- list + delete endpoints; chat panel gets a tab strip (history), new-chat
(+), close (x), refresh, an intro greeting, and suggested prompts.
- get_review_chat now returns availability only (ids are client-minted).
* ui fixes
* ui: review-chat history dropdown, full-width AI replies, resizable side panel
* fix(review-chat): enforce per-user thread ownership on proxy endpoints; reseed PR context on head change
---------
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
2026-06-15 17:17:30 -07:00
|
|
|
{visible.map((message, index) => {
|
|
|
|
|
const isUser = messageType(message) === "human"
|
feat: split view, add-to-chat, virtualization + scroll/grouping perf (#1574)
* feat: reviews page split view, add-to-chat, virtualization + perf
- Virtualize the diff (Pierre Virtualizer + worker pool), mirroring the agent
chat panel, so large PRs window rows instead of materializing every line.
- Split chat and diff into independent scroll containers and make chat
auto-scroll fully contained, so typing/streaming no longer moves the diff.
- Memoize FileDiffCard with stable callbacks so focusing a finding re-renders
only the affected card.
- Add a persisted unified/split diff toggle.
- Add highlight-to-chat: select lines (drag / shift-click) + gutter "+" to drop
a file:line snippet into the chat composer.
- Rebuild sidebar group rows: whole card scrolls to the group (incl. the
expanded explanation), Read explanation stays a separate toggle, memoized.
* feat: add-to-chat uses attachment pills + selection popup / ⌘L
Replace the raw-snippet injection with a Cursor-style flow:
- Selecting lines shows a floating "Add to Chat ⌘L" popup at the pointer; ⌘L
adds the current selection without it. Removes the auto-adding gutter "+".
- "Add to chat" now creates a removable attachment pill in the composer (and a
pill in the sent message bubble) instead of pasting raw text. The code is
still serialized into the message content so the model receives it as context.
* feat: restore gutter + drag-handle for line selection
Re-enable Pierre's gutter '+' as a click-and-drag line selector (with the
highlight growing as you drag) — the affordance that was lost when the
auto-adding gutter button was removed. It no longer auto-adds: the commit flows
through onLineSelected to the 'Add to Chat' popup / ⌘L.
* fix: live selection highlight while dragging + reposition Add to Chat popup
- Feed onLineSelectionChange into the controlled selection so rows highlight
live as you drag, not just on release (Pierre only paints the controlled
selection when the prop updates). Popup now fires on onLineSelectionEnd.
- Anchor the popup's bottom-left to the drag handle (drop horizontal centering)
so it no longer overlaps the '+' button.
* fix: anchor Add to Chat popup to gutter handle + click-away to unselect
- Position the popup from the gutter '+' handle's rect (in the diff shadow DOM,
placed on the selection's bottom line) instead of the pointer-release point,
which landed inconsistently. Falls back to the pointer if not found.
- Clear the line selection (and popup) on any outside pointer-down.
* fix: sidebar-collapse header overlap + PR review comments
- Lift useSidebarLayout to AgentsShell (single source), share collapsed via
context, and pad the reviews header left when the sidebar is collapsed so the
fixed collapse toggle no longer overlaps the header content.
- add-to-chat: collect each diff side separately so a selection that spans a
deletion->addition no longer pastes wrong-file lines (PR comment).
- chat: clear attachments after sending via a suggested prompt, so an attached
snippet isn't silently resent on the next message (PR comment).
* fix: anchored finding card positions to the right of the diff again
The virtualization refactor moved the card inside the main-width Virtualizer
scroller, so it clamped over the diff. Render it in the outer container as a
viewport-fixed card clamped to window width (right gutter / over the side panel,
like prod) and track the finding as the diff scrolls (rAF-throttled), hiding
when the finding scrolls out of view.
2026-06-18 16:54:07 -07:00
|
|
|
if (!isUser) {
|
|
|
|
|
return (
|
|
|
|
|
<div key={message.id ?? index} className="flex justify-start">
|
|
|
|
|
<div className="w-full text-[13px] text-foreground">
|
|
|
|
|
<Markdown content={messageText(message.content)} />
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
const parsed = parseUserMessage(messageText(message.content))
|
feat: chat with your PR on the review page (#1534)
* feat: chat with your PR on the review page
Add a sandbox-less `chat` graph that answers questions about a single PR
from its diff, the published review findings, and read-only GitHub access.
- agent/chat.py: deepagents graph, no sandbox (default StateBackend, file
mutation + execute tools excluded). PR context is seeded as virtual files
under /pr/; a repo-scoped App token is resolved in-graph.
- tools: read_repo_file, search_repo_code, list_review_findings.
- dashboard/review_chat_api.py + routes: per-user chat thread, LangGraph
stream/commands/state/history proxy pinned to the chat assistant, seeds
diff/findings/overview on first run. Gated by repo access.
- UI: Chat tab wired to a chat-scoped StreamProvider (replaces Coming Soon).
* feat: admin setting for review-chat default model
Add a 'Open SWE Review Chat' default to team settings (default_chat_model /
default_chat_reasoning_effort). get_team_default_model("chat") inherits the
Agent default when unset; the chat graph resolves through it. Admin RolePicker
gains an 'Agent default' inherit option that clears the override.
* feat: multi-conversation review chat (tabs, new chat, history)
Replace the single per-PR chat thread with multiple per-user conversations:
- threads minted client-side; first message persists with a title derived
from the prompt.
- list + delete endpoints; chat panel gets a tab strip (history), new-chat
(+), close (x), refresh, an intro greeting, and suggested prompts.
- get_review_chat now returns availability only (ids are client-minted).
* ui fixes
* ui: review-chat history dropdown, full-width AI replies, resizable side panel
* fix(review-chat): enforce per-user thread ownership on proxy endpoints; reseed PR context on head change
---------
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
2026-06-15 17:17:30 -07:00
|
|
|
return (
|
feat: split view, add-to-chat, virtualization + scroll/grouping perf (#1574)
* feat: reviews page split view, add-to-chat, virtualization + perf
- Virtualize the diff (Pierre Virtualizer + worker pool), mirroring the agent
chat panel, so large PRs window rows instead of materializing every line.
- Split chat and diff into independent scroll containers and make chat
auto-scroll fully contained, so typing/streaming no longer moves the diff.
- Memoize FileDiffCard with stable callbacks so focusing a finding re-renders
only the affected card.
- Add a persisted unified/split diff toggle.
- Add highlight-to-chat: select lines (drag / shift-click) + gutter "+" to drop
a file:line snippet into the chat composer.
- Rebuild sidebar group rows: whole card scrolls to the group (incl. the
expanded explanation), Read explanation stays a separate toggle, memoized.
* feat: add-to-chat uses attachment pills + selection popup / ⌘L
Replace the raw-snippet injection with a Cursor-style flow:
- Selecting lines shows a floating "Add to Chat ⌘L" popup at the pointer; ⌘L
adds the current selection without it. Removes the auto-adding gutter "+".
- "Add to chat" now creates a removable attachment pill in the composer (and a
pill in the sent message bubble) instead of pasting raw text. The code is
still serialized into the message content so the model receives it as context.
* feat: restore gutter + drag-handle for line selection
Re-enable Pierre's gutter '+' as a click-and-drag line selector (with the
highlight growing as you drag) — the affordance that was lost when the
auto-adding gutter button was removed. It no longer auto-adds: the commit flows
through onLineSelected to the 'Add to Chat' popup / ⌘L.
* fix: live selection highlight while dragging + reposition Add to Chat popup
- Feed onLineSelectionChange into the controlled selection so rows highlight
live as you drag, not just on release (Pierre only paints the controlled
selection when the prop updates). Popup now fires on onLineSelectionEnd.
- Anchor the popup's bottom-left to the drag handle (drop horizontal centering)
so it no longer overlaps the '+' button.
* fix: anchor Add to Chat popup to gutter handle + click-away to unselect
- Position the popup from the gutter '+' handle's rect (in the diff shadow DOM,
placed on the selection's bottom line) instead of the pointer-release point,
which landed inconsistently. Falls back to the pointer if not found.
- Clear the line selection (and popup) on any outside pointer-down.
* fix: sidebar-collapse header overlap + PR review comments
- Lift useSidebarLayout to AgentsShell (single source), share collapsed via
context, and pad the reviews header left when the sidebar is collapsed so the
fixed collapse toggle no longer overlaps the header content.
- add-to-chat: collect each diff side separately so a selection that spans a
deletion->addition no longer pastes wrong-file lines (PR comment).
- chat: clear attachments after sending via a suggested prompt, so an attached
snippet isn't silently resent on the next message (PR comment).
* fix: anchored finding card positions to the right of the diff again
The virtualization refactor moved the card inside the main-width Virtualizer
scroller, so it clamped over the diff. Render it in the outer container as a
viewport-fixed card clamped to window width (right gutter / over the side panel,
like prod) and track the finding as the diff scrolls (rAF-throttled), hiding
when the finding scrolls out of view.
2026-06-18 16:54:07 -07:00
|
|
|
<div key={message.id ?? index} className="flex justify-end">
|
|
|
|
|
<div className="flex max-w-[85%] flex-col items-end gap-1.5">
|
|
|
|
|
{parsed.attachments.length > 0 && (
|
|
|
|
|
<div className="flex flex-wrap justify-end gap-1">
|
|
|
|
|
{parsed.attachments.map((attachment, i) => (
|
|
|
|
|
<AttachmentPill key={i} label={attachment.label} />
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
feat: chat with your PR on the review page (#1534)
* feat: chat with your PR on the review page
Add a sandbox-less `chat` graph that answers questions about a single PR
from its diff, the published review findings, and read-only GitHub access.
- agent/chat.py: deepagents graph, no sandbox (default StateBackend, file
mutation + execute tools excluded). PR context is seeded as virtual files
under /pr/; a repo-scoped App token is resolved in-graph.
- tools: read_repo_file, search_repo_code, list_review_findings.
- dashboard/review_chat_api.py + routes: per-user chat thread, LangGraph
stream/commands/state/history proxy pinned to the chat assistant, seeds
diff/findings/overview on first run. Gated by repo access.
- UI: Chat tab wired to a chat-scoped StreamProvider (replaces Coming Soon).
* feat: admin setting for review-chat default model
Add a 'Open SWE Review Chat' default to team settings (default_chat_model /
default_chat_reasoning_effort). get_team_default_model("chat") inherits the
Agent default when unset; the chat graph resolves through it. Admin RolePicker
gains an 'Agent default' inherit option that clears the override.
* feat: multi-conversation review chat (tabs, new chat, history)
Replace the single per-PR chat thread with multiple per-user conversations:
- threads minted client-side; first message persists with a title derived
from the prompt.
- list + delete endpoints; chat panel gets a tab strip (history), new-chat
(+), close (x), refresh, an intro greeting, and suggested prompts.
- get_review_chat now returns availability only (ids are client-minted).
* ui fixes
* ui: review-chat history dropdown, full-width AI replies, resizable side panel
* fix(review-chat): enforce per-user thread ownership on proxy endpoints; reseed PR context on head change
---------
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
2026-06-15 17:17:30 -07:00
|
|
|
)}
|
feat: split view, add-to-chat, virtualization + scroll/grouping perf (#1574)
* feat: reviews page split view, add-to-chat, virtualization + perf
- Virtualize the diff (Pierre Virtualizer + worker pool), mirroring the agent
chat panel, so large PRs window rows instead of materializing every line.
- Split chat and diff into independent scroll containers and make chat
auto-scroll fully contained, so typing/streaming no longer moves the diff.
- Memoize FileDiffCard with stable callbacks so focusing a finding re-renders
only the affected card.
- Add a persisted unified/split diff toggle.
- Add highlight-to-chat: select lines (drag / shift-click) + gutter "+" to drop
a file:line snippet into the chat composer.
- Rebuild sidebar group rows: whole card scrolls to the group (incl. the
expanded explanation), Read explanation stays a separate toggle, memoized.
* feat: add-to-chat uses attachment pills + selection popup / ⌘L
Replace the raw-snippet injection with a Cursor-style flow:
- Selecting lines shows a floating "Add to Chat ⌘L" popup at the pointer; ⌘L
adds the current selection without it. Removes the auto-adding gutter "+".
- "Add to chat" now creates a removable attachment pill in the composer (and a
pill in the sent message bubble) instead of pasting raw text. The code is
still serialized into the message content so the model receives it as context.
* feat: restore gutter + drag-handle for line selection
Re-enable Pierre's gutter '+' as a click-and-drag line selector (with the
highlight growing as you drag) — the affordance that was lost when the
auto-adding gutter button was removed. It no longer auto-adds: the commit flows
through onLineSelected to the 'Add to Chat' popup / ⌘L.
* fix: live selection highlight while dragging + reposition Add to Chat popup
- Feed onLineSelectionChange into the controlled selection so rows highlight
live as you drag, not just on release (Pierre only paints the controlled
selection when the prop updates). Popup now fires on onLineSelectionEnd.
- Anchor the popup's bottom-left to the drag handle (drop horizontal centering)
so it no longer overlaps the '+' button.
* fix: anchor Add to Chat popup to gutter handle + click-away to unselect
- Position the popup from the gutter '+' handle's rect (in the diff shadow DOM,
placed on the selection's bottom line) instead of the pointer-release point,
which landed inconsistently. Falls back to the pointer if not found.
- Clear the line selection (and popup) on any outside pointer-down.
* fix: sidebar-collapse header overlap + PR review comments
- Lift useSidebarLayout to AgentsShell (single source), share collapsed via
context, and pad the reviews header left when the sidebar is collapsed so the
fixed collapse toggle no longer overlaps the header content.
- add-to-chat: collect each diff side separately so a selection that spans a
deletion->addition no longer pastes wrong-file lines (PR comment).
- chat: clear attachments after sending via a suggested prompt, so an attached
snippet isn't silently resent on the next message (PR comment).
* fix: anchored finding card positions to the right of the diff again
The virtualization refactor moved the card inside the main-width Virtualizer
scroller, so it clamped over the diff. Render it in the outer container as a
viewport-fixed card clamped to window width (right gutter / over the side panel,
like prod) and track the finding as the diff scrolls (rAF-throttled), hiding
when the finding scrolls out of view.
2026-06-18 16:54:07 -07:00
|
|
|
{parsed.text && (
|
|
|
|
|
<span className="rounded-lg bg-muted px-3 py-2 text-[13px] whitespace-pre-wrap text-foreground">
|
|
|
|
|
{parsed.text}
|
feat: chat with your PR on the review page (#1534)
* feat: chat with your PR on the review page
Add a sandbox-less `chat` graph that answers questions about a single PR
from its diff, the published review findings, and read-only GitHub access.
- agent/chat.py: deepagents graph, no sandbox (default StateBackend, file
mutation + execute tools excluded). PR context is seeded as virtual files
under /pr/; a repo-scoped App token is resolved in-graph.
- tools: read_repo_file, search_repo_code, list_review_findings.
- dashboard/review_chat_api.py + routes: per-user chat thread, LangGraph
stream/commands/state/history proxy pinned to the chat assistant, seeds
diff/findings/overview on first run. Gated by repo access.
- UI: Chat tab wired to a chat-scoped StreamProvider (replaces Coming Soon).
* feat: admin setting for review-chat default model
Add a 'Open SWE Review Chat' default to team settings (default_chat_model /
default_chat_reasoning_effort). get_team_default_model("chat") inherits the
Agent default when unset; the chat graph resolves through it. Admin RolePicker
gains an 'Agent default' inherit option that clears the override.
* feat: multi-conversation review chat (tabs, new chat, history)
Replace the single per-PR chat thread with multiple per-user conversations:
- threads minted client-side; first message persists with a title derived
from the prompt.
- list + delete endpoints; chat panel gets a tab strip (history), new-chat
(+), close (x), refresh, an intro greeting, and suggested prompts.
- get_review_chat now returns availability only (ids are client-minted).
* ui fixes
* ui: review-chat history dropdown, full-width AI replies, resizable side panel
* fix(review-chat): enforce per-user thread ownership on proxy endpoints; reseed PR context on head change
---------
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
2026-06-15 17:17:30 -07:00
|
|
|
</span>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
})}
|
|
|
|
|
{busy && (
|
|
|
|
|
<div className="flex justify-start">
|
2026-06-16 14:56:11 -07:00
|
|
|
<div className="px-3 py-2 text-xs text-muted-foreground">
|
feat: chat with your PR on the review page (#1534)
* feat: chat with your PR on the review page
Add a sandbox-less `chat` graph that answers questions about a single PR
from its diff, the published review findings, and read-only GitHub access.
- agent/chat.py: deepagents graph, no sandbox (default StateBackend, file
mutation + execute tools excluded). PR context is seeded as virtual files
under /pr/; a repo-scoped App token is resolved in-graph.
- tools: read_repo_file, search_repo_code, list_review_findings.
- dashboard/review_chat_api.py + routes: per-user chat thread, LangGraph
stream/commands/state/history proxy pinned to the chat assistant, seeds
diff/findings/overview on first run. Gated by repo access.
- UI: Chat tab wired to a chat-scoped StreamProvider (replaces Coming Soon).
* feat: admin setting for review-chat default model
Add a 'Open SWE Review Chat' default to team settings (default_chat_model /
default_chat_reasoning_effort). get_team_default_model("chat") inherits the
Agent default when unset; the chat graph resolves through it. Admin RolePicker
gains an 'Agent default' inherit option that clears the override.
* feat: multi-conversation review chat (tabs, new chat, history)
Replace the single per-PR chat thread with multiple per-user conversations:
- threads minted client-side; first message persists with a title derived
from the prompt.
- list + delete endpoints; chat panel gets a tab strip (history), new-chat
(+), close (x), refresh, an intro greeting, and suggested prompts.
- get_review_chat now returns availability only (ids are client-minted).
* ui fixes
* ui: review-chat history dropdown, full-width AI replies, resizable side panel
* fix(review-chat): enforce per-user thread ownership on proxy endpoints; reseed PR context on head change
---------
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
2026-06-15 17:17:30 -07:00
|
|
|
Thinking…
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
<div className="p-3">
|
feat: split view, add-to-chat, virtualization + scroll/grouping perf (#1574)
* feat: reviews page split view, add-to-chat, virtualization + perf
- Virtualize the diff (Pierre Virtualizer + worker pool), mirroring the agent
chat panel, so large PRs window rows instead of materializing every line.
- Split chat and diff into independent scroll containers and make chat
auto-scroll fully contained, so typing/streaming no longer moves the diff.
- Memoize FileDiffCard with stable callbacks so focusing a finding re-renders
only the affected card.
- Add a persisted unified/split diff toggle.
- Add highlight-to-chat: select lines (drag / shift-click) + gutter "+" to drop
a file:line snippet into the chat composer.
- Rebuild sidebar group rows: whole card scrolls to the group (incl. the
expanded explanation), Read explanation stays a separate toggle, memoized.
* feat: add-to-chat uses attachment pills + selection popup / ⌘L
Replace the raw-snippet injection with a Cursor-style flow:
- Selecting lines shows a floating "Add to Chat ⌘L" popup at the pointer; ⌘L
adds the current selection without it. Removes the auto-adding gutter "+".
- "Add to chat" now creates a removable attachment pill in the composer (and a
pill in the sent message bubble) instead of pasting raw text. The code is
still serialized into the message content so the model receives it as context.
* feat: restore gutter + drag-handle for line selection
Re-enable Pierre's gutter '+' as a click-and-drag line selector (with the
highlight growing as you drag) — the affordance that was lost when the
auto-adding gutter button was removed. It no longer auto-adds: the commit flows
through onLineSelected to the 'Add to Chat' popup / ⌘L.
* fix: live selection highlight while dragging + reposition Add to Chat popup
- Feed onLineSelectionChange into the controlled selection so rows highlight
live as you drag, not just on release (Pierre only paints the controlled
selection when the prop updates). Popup now fires on onLineSelectionEnd.
- Anchor the popup's bottom-left to the drag handle (drop horizontal centering)
so it no longer overlaps the '+' button.
* fix: anchor Add to Chat popup to gutter handle + click-away to unselect
- Position the popup from the gutter '+' handle's rect (in the diff shadow DOM,
placed on the selection's bottom line) instead of the pointer-release point,
which landed inconsistently. Falls back to the pointer if not found.
- Clear the line selection (and popup) on any outside pointer-down.
* fix: sidebar-collapse header overlap + PR review comments
- Lift useSidebarLayout to AgentsShell (single source), share collapsed via
context, and pad the reviews header left when the sidebar is collapsed so the
fixed collapse toggle no longer overlaps the header content.
- add-to-chat: collect each diff side separately so a selection that spans a
deletion->addition no longer pastes wrong-file lines (PR comment).
- chat: clear attachments after sending via a suggested prompt, so an attached
snippet isn't silently resent on the next message (PR comment).
* fix: anchored finding card positions to the right of the diff again
The virtualization refactor moved the card inside the main-width Virtualizer
scroller, so it clamped over the diff. Render it in the outer container as a
viewport-fixed card clamped to window width (right gutter / over the side panel,
like prod) and track the finding as the diff scrolls (rAF-throttled), hiding
when the finding scrolls out of view.
2026-06-18 16:54:07 -07:00
|
|
|
<div className="flex flex-col gap-1.5 rounded-2xl border border-border bg-background px-1.5 py-1.5 transition-colors focus-within:border-ring/60">
|
|
|
|
|
{attachments.length > 0 && (
|
|
|
|
|
<div className="flex flex-wrap gap-1 pl-2 pt-0.5">
|
|
|
|
|
{attachments.map((attachment) => (
|
|
|
|
|
<AttachmentPill
|
|
|
|
|
key={attachment.id}
|
|
|
|
|
label={attachmentPillLabel(attachment)}
|
|
|
|
|
onRemove={() => removeAttachment(attachment.id)}
|
|
|
|
|
/>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
<div className="flex items-end gap-2 pl-2">
|
|
|
|
|
<Textarea
|
|
|
|
|
value={value}
|
|
|
|
|
onChange={(event) => setValue(event.target.value)}
|
|
|
|
|
onKeyDown={(event) => {
|
|
|
|
|
if (event.key === "Enter" && !event.shiftKey) {
|
|
|
|
|
event.preventDefault()
|
|
|
|
|
submitComposer()
|
|
|
|
|
}
|
|
|
|
|
}}
|
|
|
|
|
placeholder="Ask anything about this PR…"
|
|
|
|
|
rows={1}
|
|
|
|
|
className="max-h-40 min-h-7 flex-1 resize-none rounded-none border-0 bg-transparent px-0 py-1 shadow-none focus-visible:border-transparent focus-visible:ring-0"
|
|
|
|
|
/>
|
|
|
|
|
<IconButton
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={submitComposer}
|
|
|
|
|
disabled={(!value.trim() && attachments.length === 0) || busy}
|
|
|
|
|
aria-label="Send message"
|
|
|
|
|
className="rounded-full"
|
|
|
|
|
>
|
|
|
|
|
<ArrowUpIcon className="size-4" />
|
|
|
|
|
</IconButton>
|
|
|
|
|
</div>
|
feat: chat with your PR on the review page (#1534)
* feat: chat with your PR on the review page
Add a sandbox-less `chat` graph that answers questions about a single PR
from its diff, the published review findings, and read-only GitHub access.
- agent/chat.py: deepagents graph, no sandbox (default StateBackend, file
mutation + execute tools excluded). PR context is seeded as virtual files
under /pr/; a repo-scoped App token is resolved in-graph.
- tools: read_repo_file, search_repo_code, list_review_findings.
- dashboard/review_chat_api.py + routes: per-user chat thread, LangGraph
stream/commands/state/history proxy pinned to the chat assistant, seeds
diff/findings/overview on first run. Gated by repo access.
- UI: Chat tab wired to a chat-scoped StreamProvider (replaces Coming Soon).
* feat: admin setting for review-chat default model
Add a 'Open SWE Review Chat' default to team settings (default_chat_model /
default_chat_reasoning_effort). get_team_default_model("chat") inherits the
Agent default when unset; the chat graph resolves through it. Admin RolePicker
gains an 'Agent default' inherit option that clears the override.
* feat: multi-conversation review chat (tabs, new chat, history)
Replace the single per-PR chat thread with multiple per-user conversations:
- threads minted client-side; first message persists with a title derived
from the prompt.
- list + delete endpoints; chat panel gets a tab strip (history), new-chat
(+), close (x), refresh, an intro greeting, and suggested prompts.
- get_review_chat now returns availability only (ids are client-minted).
* ui fixes
* ui: review-chat history dropdown, full-width AI replies, resizable side panel
* fix(review-chat): enforce per-user thread ownership on proxy endpoints; reseed PR context on head change
---------
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
2026-06-15 17:17:30 -07:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function ChatPanel({
|
|
|
|
|
owner,
|
|
|
|
|
repo,
|
|
|
|
|
number,
|
|
|
|
|
assistantId,
|
|
|
|
|
}: {
|
|
|
|
|
owner: string
|
|
|
|
|
repo: string
|
|
|
|
|
number: number
|
|
|
|
|
assistantId: string
|
|
|
|
|
}) {
|
|
|
|
|
const qc = useQueryClient()
|
|
|
|
|
const threadsKey = useMemo(
|
|
|
|
|
() => ["review-chat-threads", owner, repo, number] as const,
|
|
|
|
|
[owner, repo, number],
|
|
|
|
|
)
|
|
|
|
|
const { conversations, activeId, select, newChat, nameConversation, close } =
|
|
|
|
|
useConversations(storageKey(owner, repo, number))
|
|
|
|
|
|
|
|
|
|
const threadsQuery = useQuery({
|
|
|
|
|
queryKey: threadsKey,
|
|
|
|
|
queryFn: () => api.listReviewChatThreads(owner, repo, number),
|
|
|
|
|
})
|
|
|
|
|
const serverThreads = useMemo(
|
|
|
|
|
() => threadsQuery.data?.threads ?? [],
|
|
|
|
|
[threadsQuery.data],
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const invalidateThreads = useCallback(() => {
|
|
|
|
|
void qc.invalidateQueries({ queryKey: threadsKey })
|
|
|
|
|
}, [qc, threadsKey])
|
|
|
|
|
|
|
|
|
|
const deleteThread = useMutation({
|
|
|
|
|
mutationFn: (id: string) =>
|
|
|
|
|
api.deleteReviewChatThread(owner, repo, number, id),
|
|
|
|
|
onMutate: (id: string) => {
|
|
|
|
|
qc.setQueryData<{ threads: Array<ReviewChatThread> }>(threadsKey, (old) =>
|
|
|
|
|
old ? { threads: old.threads.filter((t) => t.thread_id !== id) } : old,
|
|
|
|
|
)
|
|
|
|
|
},
|
|
|
|
|
onSettled: invalidateThreads,
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// Open tabs are the client's conversations (titles reconciled from the
|
|
|
|
|
// server). The history dropdown is the full set — open tabs plus every other
|
|
|
|
|
// conversation the server knows about (e.g. recovered on a fresh browser) —
|
|
|
|
|
// newest first, so past chats are reachable without cluttering the tab strip.
|
|
|
|
|
const { openTabs, historyItems } = useMemo(() => {
|
|
|
|
|
const byId = new Map(conversations.map((c) => [c.id, { ...c }]))
|
|
|
|
|
for (const thread of serverThreads) {
|
|
|
|
|
const existing = byId.get(thread.thread_id)
|
|
|
|
|
if (existing) {
|
|
|
|
|
if (thread.title && thread.title !== DEFAULT_TITLE) {
|
|
|
|
|
existing.title = thread.title
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
byId.set(thread.thread_id, {
|
|
|
|
|
id: thread.thread_id,
|
|
|
|
|
title: thread.title || DEFAULT_TITLE,
|
|
|
|
|
createdAt: thread.updated_at ? Date.parse(thread.updated_at) || 0 : 0,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return {
|
|
|
|
|
openTabs: conversations.map((c) => byId.get(c.id) ?? c),
|
|
|
|
|
historyItems: [...byId.values()].sort((a, b) => b.createdAt - a.createdAt),
|
|
|
|
|
}
|
|
|
|
|
}, [conversations, serverThreads])
|
|
|
|
|
|
|
|
|
|
const handleClose = useCallback(
|
|
|
|
|
(id: string) => {
|
|
|
|
|
if (serverThreads.some((t) => t.thread_id === id)) {
|
|
|
|
|
deleteThread.mutate(id)
|
|
|
|
|
}
|
|
|
|
|
close(id)
|
|
|
|
|
},
|
|
|
|
|
[serverThreads, deleteThread, close],
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const handleUserSend = useCallback(
|
|
|
|
|
(text: string) => {
|
|
|
|
|
nameConversation(activeId, deriveTitle(text))
|
|
|
|
|
},
|
|
|
|
|
[nameConversation, activeId],
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// Whether the active conversation should already have messages: it exists
|
|
|
|
|
// server-side, or it's been named by a sent message. Drives the hydration
|
|
|
|
|
// placeholder so an existing chat never flashes the intro state on load.
|
|
|
|
|
const expectsHistory = useMemo(() => {
|
|
|
|
|
if (serverThreads.some((t) => t.thread_id === activeId)) return true
|
|
|
|
|
const active = conversations.find((c) => c.id === activeId)
|
|
|
|
|
return active !== undefined && active.title !== DEFAULT_TITLE
|
|
|
|
|
}, [serverThreads, conversations, activeId])
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="flex h-full flex-1 flex-col overflow-hidden">
|
|
|
|
|
<div className="flex items-center gap-1 border-b border-border px-2 py-1.5">
|
|
|
|
|
<div className="flex flex-1 items-center gap-1 overflow-x-auto">
|
|
|
|
|
{openTabs.map((tab) => (
|
|
|
|
|
<div
|
|
|
|
|
key={tab.id}
|
|
|
|
|
className={cn(
|
|
|
|
|
"flex shrink-0 items-center gap-1 rounded-md py-1 pl-2 pr-1 text-xs",
|
|
|
|
|
tab.id === activeId
|
|
|
|
|
? "bg-muted text-foreground"
|
|
|
|
|
: "text-muted-foreground hover:bg-muted/50",
|
|
|
|
|
)}
|
|
|
|
|
>
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={() => select(tab)}
|
|
|
|
|
className="max-w-[140px] truncate"
|
|
|
|
|
>
|
|
|
|
|
{tab.title}
|
|
|
|
|
</button>
|
|
|
|
|
<IconButton
|
|
|
|
|
type="button"
|
|
|
|
|
variant="ghost"
|
|
|
|
|
size="icon-xs"
|
|
|
|
|
aria-label="Close chat"
|
|
|
|
|
onClick={() => handleClose(tab.id)}
|
|
|
|
|
>
|
|
|
|
|
<XIcon />
|
|
|
|
|
</IconButton>
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
<IconButton
|
|
|
|
|
type="button"
|
|
|
|
|
variant="ghost"
|
|
|
|
|
size="icon-sm"
|
|
|
|
|
aria-label="New chat"
|
|
|
|
|
onClick={newChat}
|
|
|
|
|
>
|
|
|
|
|
<PlusIcon />
|
|
|
|
|
</IconButton>
|
|
|
|
|
<Menu.Root
|
|
|
|
|
onOpenChange={(open) => {
|
|
|
|
|
if (open) void threadsQuery.refetch()
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<Menu.Trigger
|
|
|
|
|
render={
|
|
|
|
|
<IconButton
|
|
|
|
|
type="button"
|
|
|
|
|
variant="ghost"
|
|
|
|
|
size="icon-sm"
|
|
|
|
|
aria-label="Chat history"
|
|
|
|
|
>
|
|
|
|
|
<ArrowClockwiseIcon />
|
|
|
|
|
</IconButton>
|
|
|
|
|
}
|
|
|
|
|
/>
|
|
|
|
|
<Menu.Portal>
|
|
|
|
|
<Menu.Positioner align="end" sideOffset={6} className="z-50">
|
|
|
|
|
<Menu.Popup className="origin-(--transform-origin) max-h-80 w-64 overflow-y-auto rounded-lg bg-popover p-1 text-popover-foreground shadow-md ring-1 ring-foreground/10 outline-none">
|
|
|
|
|
{historyItems.length === 0 ? (
|
|
|
|
|
<div className="px-2 py-1.5 text-xs text-muted-foreground">
|
|
|
|
|
No conversations yet
|
|
|
|
|
</div>
|
|
|
|
|
) : (
|
|
|
|
|
historyItems.map((item) => (
|
|
|
|
|
<div
|
|
|
|
|
key={item.id}
|
|
|
|
|
className="group/hist relative flex items-stretch"
|
|
|
|
|
>
|
|
|
|
|
<Menu.Item
|
|
|
|
|
onClick={() => select(item)}
|
|
|
|
|
className="flex flex-1 cursor-default items-center gap-2 rounded-md py-1.5 pl-2 pr-8 text-xs outline-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground"
|
|
|
|
|
>
|
|
|
|
|
{item.id === activeId ? (
|
|
|
|
|
<CheckIcon className="size-3.5 shrink-0" />
|
|
|
|
|
) : (
|
|
|
|
|
<span className="size-3.5 shrink-0" />
|
|
|
|
|
)}
|
|
|
|
|
<span className="flex-1 truncate text-left">
|
|
|
|
|
{item.title}
|
|
|
|
|
</span>
|
|
|
|
|
</Menu.Item>
|
|
|
|
|
<IconButton
|
|
|
|
|
type="button"
|
|
|
|
|
variant="ghost"
|
|
|
|
|
size="icon-xs"
|
|
|
|
|
aria-label="Delete chat"
|
|
|
|
|
className="absolute right-1 top-1/2 -translate-y-1/2 opacity-0 group-hover/hist:opacity-100"
|
|
|
|
|
onClick={() => handleClose(item.id)}
|
|
|
|
|
>
|
|
|
|
|
<TrashIcon />
|
|
|
|
|
</IconButton>
|
|
|
|
|
</div>
|
|
|
|
|
))
|
|
|
|
|
)}
|
|
|
|
|
</Menu.Popup>
|
|
|
|
|
</Menu.Positioner>
|
|
|
|
|
</Menu.Portal>
|
|
|
|
|
</Menu.Root>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<StreamProvider
|
|
|
|
|
key={activeId}
|
|
|
|
|
apiUrl={reviewChatApiBase(owner, repo, number)}
|
|
|
|
|
assistantId={assistantId}
|
|
|
|
|
fetch={dashboardFetch}
|
|
|
|
|
threadId={activeId}
|
|
|
|
|
onCompleted={invalidateThreads}
|
|
|
|
|
>
|
|
|
|
|
<ChatBody onUserSend={handleUserSend} expectsHistory={expectsHistory} />
|
|
|
|
|
</StreamProvider>
|
|
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function ReviewChat({
|
|
|
|
|
owner,
|
|
|
|
|
repo,
|
|
|
|
|
number,
|
|
|
|
|
}: {
|
|
|
|
|
owner: string
|
|
|
|
|
repo: string
|
|
|
|
|
number: number
|
|
|
|
|
}) {
|
|
|
|
|
const meta = useQuery({
|
|
|
|
|
queryKey: ["review-chat", owner, repo, number],
|
|
|
|
|
queryFn: () => api.getReviewChat(owner, repo, number),
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
if (meta.isPending) {
|
|
|
|
|
return (
|
|
|
|
|
<div className="flex flex-1 flex-col gap-3 p-4">
|
|
|
|
|
<Skeleton className="h-4 w-2/3" />
|
|
|
|
|
<Skeleton className="h-4 w-1/2" />
|
|
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (meta.isError || !meta.data.available) {
|
|
|
|
|
return (
|
|
|
|
|
<div className="flex flex-1 items-center justify-center p-6 text-center text-xs text-muted-foreground">
|
|
|
|
|
Chat becomes available once the review has finished running.
|
|
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<ChatPanel
|
|
|
|
|
key={`${owner}/${repo}/${number}`}
|
|
|
|
|
owner={owner}
|
|
|
|
|
repo={repo}
|
|
|
|
|
number={number}
|
|
|
|
|
assistantId={meta.data.assistant_id}
|
|
|
|
|
/>
|
|
|
|
|
)
|
|
|
|
|
}
|