Skip to content

Commit 1903541

Browse files
authored
πŸ€– feat: add review_pane_update tool + Assisted review toggle (#3331)
## Summary Adds two new agent tools β€” `review_pane_update` and `review_pane_get` β€” that let an agent flag specific code regions in the Review pane for the user to review next. The Review pane gains an "Assisted" toggle that filters to those hunks; flagged hunks are always pinned at the top in agent-declared order, the agent's optional comment is rendered above each hunk, and the hunk body is trimmed to just the flagged new-side line range with the surrounding diff hidden behind a "Show N lines …" affordance. Both new tools get purpose-built transcript renderers instead of the generic JSON-dump fallback. The Review tab auto-focuses on agent-flagged hunks and shows a pulsing Sparkles indicator while there's unread agent focus pending. Tool state is persisted under the workspace session dir so `add` semantics survive backend restarts. ## Background When an agent makes a large or risky change, the user often wants to know *where to look first*. Today there's no signal from the agent into the review surface β€” every hunk has equal weight, and a 500-line diff buries the 20 lines that actually matter. This feature gives the agent a first-class way to say "review these regions, in this order, with this context." ## Implementation **Tools (single source of truth in `toolDefinitions.ts`):** - `review_pane_update({ operation: "add" | "replace", hunks: [{ path, comment? }] })` β€” paths use a familiar `path[:start-end]` syntax (`"src/foo.ts"`, `"src/foo.ts:42"`, or `"src/foo.ts:10-20"`, new-side line numbers). `replace` overwrites the set; `add` appends with dedup-by-`path:range` (preferring the latest comment). - `review_pane_get()` β€” returns the current set in declared order so the agent can inspect before adding more. **State model:** - Backend tool state is persisted under `<workspaceSessionDir>/assistedReview.json` (parallel to `todos.json`), with writes serialized via `workspaceFileLocks` and `writeFileAtomic`. The file is unlinked on a clear so a stale file can't shadow a fresh start. `coerceAssistedReviewHunks` survives corrupted JSON per the self-healing doctrine β€” bad data is treated as empty rather than thrown. - `StreamingMessageAggregator.processToolResult` parses each successful `review_pane_update` output and rebuilds `assistedReviewHunks` on the frontend. Because the tool returns the *resulting* list directly, replaying history naturally reconstructs final state on reload. - Exposed via `WorkspaceStore.getAssistedReviewHunks(workspaceId)`, consumed by `ReviewPanel` through `useSyncExternalStore`. **Review pane UI:** - One shared module `src/common/utils/review/assistedReview.ts` owns parsing, matching, and formatting β€” reused by the tool handler, aggregator, and panel. - `applyFrontendFilters` gains an optional `isAssisted` predicate so the existing read/search pipeline is untouched. - `ReviewPanel`'s `filteredHunks` memo applies the existing sort, then partitions into `[assistedBucket, rest]` preserving agent-declared index, so assisted hunks are always pinned first regardless of the toggle. - `ReviewControls` shows an "Assisted ✦ (N)" toggle only when `assistedCount > 0`, keeping the toolbar compact for normal sessions. - `HunkViewer` renders the comment in an accent-bordered row above the diff header; a hunk flagged without a comment gets a subtle left-border accent. **Range trimming + show-more:** - New helper `sliceHunkByNewLineRange` (`src/browser/utils/review/sliceHunkContent.ts`) walks the diff body using the same algorithm as `groupDiffLines`, partitions lines into `before` / `inside` / `after` (attaching `-` lines to the next visible region; trailing `-` lines to the previous), and returns `null` for no-op cases (full coverage, out-of-bounds, pure deletions, pure renames) so the existing single-renderer path still applies. - `HunkViewer` now renders the inside slice always; before/after stay hidden behind a `ContextCollapseIndicator` that gained a `mode: "expand" | "collapse"` prop, so "Show N lines above" and "Collapse N lines above" share the same squiggle pattern. - Agent-flagged hunks override the auto-collapse behavior that kicks in for large hunks / heavy reviews β€” otherwise the assistance signal got buried under "Click to expand". **Chat-transcript renderers:** - `ReviewPaneUpdateToolCall.tsx` (β‰ˆ120 LOC) β€” collapsed header shows the operation (Add / Replace / Cleared) + "N hunks pinned" + rejected count when present. Expanded view lists each path:range with a Sparkles-accented comment row. Prefers the *post-merge* result list so dedup outcomes are visible. - `ReviewPaneGetToolCall.tsx` (β‰ˆ45 LOC) β€” single-line preview ("Inspected N pinned hunks"); no expand affordance because the full list lives in the Review pane. - Both are registered in `TOOL_REGISTRY` (`getToolComponent.ts`); `TOOL_NAME_TO_ICON` maps `review_pane_update` β†’ `Sparkles` and `review_pane_get` β†’ `ScanEye`. **Focus mode + Review tab pizzazz:** - The Assisted toggle now auto-flips ON the first time the agent flags hunks in a session, so the Review pane lands directly on the critical changes instead of the full diff. A ref-guarded latch makes the user's subsequent manual toggles stick; a drop-to-zero (agent cleared its hint set) re-arms the latch so the next batch of flagged hunks gets focus again. - `ReviewStats` gains an `unreadAssisted` field β€” the count of agent-flagged hunks intersecting the current diff that the user hasn't acked. Computed in `ReviewPanel` alongside the existing `readHunkCount` summary and pushed through the unchanged `onStatsChange` callback. - `ReviewTabLabel` renders a Sparkles pill in `--color-review-accent` with `animate-pulse` when `unreadAssisted > 0`, alongside the existing `read/total` badge. Stops pulsing once everything assisted is marked read. Tooltip explains "N agent-flagged hunk(s) pending review." Matching is exact on workspace-relative paths (with `oldPath` fallback for renames), and overlap-based on new-side line numbers (old-side fallback for pure deletions), so the syntax does what an agent would naturally expect. ## Validation - 42 new unit tests: - 12 for the parser/matcher (`assistedReview.test.ts`) - 6 for `applyReviewPaneUpdate` semantics (add/replace, dedup, comment normalization, rejection of malformed filters, clear-via-empty-replace) - 7 for `sliceHunkByNewLineRange` (whole-hunk coverage, out of bounds, pure deletions, context-only split, deletion grouping, empty-inside short-circuit, boundary cases) - 5 for `ReviewTabLabel` pizzazz indicator (presence/absence gating, accent + pulse classes, singular/plural aria-labels, null-stats safety) - 4 for `coerceAssistedReviewHunks` (non-arrays, missing/invalid path, malformed ranges, empty-string comment) - 4 for end-to-end persistence (restart-survives-state, clear-unlinks-file, per-workspace isolation, corrupted-file self-heal) - Full `make test` deltas: +42 passing tests, no new failures. The 21 `GitStatusStore` and 1 `NestedToolRenderer` cross-test interference failures observed in full-suite runs are **pre-existing on baseline `main`** and reproduce without these changes. ## Risks Low. The new state is opt-in and entirely additive: - The Assisted toggle, per-hunk comment row, range trimming, auto-focus latch, and tab pizzazz pill are all conditional on the agent having flagged something, so users who never see the tool fire get the unchanged Review pane and tab strip. - The slicer is a pure transform and returns `null` for any edge case it doesn't fully handle, falling back to the unchanged full-hunk render path. - Ordering changes only when at least one hunk matches β€” the existing file-order / last-edit sort still owns the rest of the list. - The tools only persist a small JSON file under the workspace session dir; no git or other filesystem side effects. - `ContextCollapseIndicator`'s new `mode` prop defaults to `"collapse"`, preserving existing call-site behavior. - The auto-on Assisted effect is ref-latched so it won't churn against user toggles; it only re-arms when the agent clears the hint set entirely. - File reads self-heal from corruption (treated as empty), so a hand-edited / partial-write `assistedReview.json` can't brick the tool. Touched files outside the feature scope: `StreamingMessageAggregator.processToolResult` (added a guarded branch alongside `todo_write`), `applyFrontendFilters` (added an optional predicate), `ContextCollapseIndicator` (additive `mode` prop), and `ReviewStats` (additive `unreadAssisted` field). All are narrowly scoped and covered by existing or new tests. --- _Generated with `mux` β€’ Model: `anthropic:claude-opus-4-7` β€’ Thinking: `max` β€’ Cost: `$26.13`_ <!-- mux-attribution: model=anthropic:claude-opus-4-7 thinking=max costs=26.13 -->
1 parent 7893af4 commit 1903541

30 files changed

Lines changed: 2334 additions & 113 deletions

β€Ždocs/hooks/tools.mdxβ€Ž

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -587,6 +587,18 @@ If a value is too large for the environment, it may be omitted (not set). Mux al
587587

588588
</details>
589589

590+
<details>
591+
<summary>review_pane_update (4)</summary>
592+
593+
| Env var | JSON path | Type | Description |
594+
| -------------------------------------- | ------------------------ | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------- |
595+
| `MUX_TOOL_INPUT_HUNKS_<INDEX>_COMMENT` | `hunks[<INDEX>].comment` | string | Short note (~1 sentence) telling the user what to look at and why. |
596+
| `MUX_TOOL_INPUT_HUNKS_<INDEX>_PATH` | `hunks[<INDEX>].path` | string | Filter in `path[:range]` form, e.g. "src/foo.ts" or "src/foo.ts:42-58". Path is workspace-relative; range uses new-file line numbers (inclusive). |
597+
| `MUX_TOOL_INPUT_HUNKS_COUNT` | `hunks.length` | number | Number of elements in hunks (List of hunks to flag for review.) |
598+
| `MUX_TOOL_INPUT_OPERATION` | `operation` | enum | 'replace' overwrites the assisted set; 'add' appends to it. |
599+
600+
</details>
601+
590602
<details>
591603
<summary>skills_catalog_read (3)</summary>
592604

β€Žsrc/browser/features/RightSidebar/CodeReview/ContextCollapseIndicator.tsxβ€Ž

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,24 +4,35 @@ interface ContextCollapseIndicatorProps {
44
lineCount: number;
55
onCollapse: (e: React.MouseEvent) => void;
66
position: "above" | "below";
7+
/**
8+
* - "collapse" (default): button text reads "Collapse N lines …"; used to
9+
* hide previously-loaded context.
10+
* - "expand": button text reads "Show N lines …"; used to reveal context
11+
* that is currently hidden (Assisted-review trim).
12+
*/
13+
mode?: "collapse" | "expand";
714
}
815

916
/**
10-
* Visual indicator for collapsing expanded context lines.
17+
* Visual indicator for collapsing/expanding context lines.
1118
* Uses the squiggly line pattern established in BashOutputCollapsedIndicator.
12-
* Placed between expanded context and the main hunk content.
19+
* Used between the main hunk content and surrounding context.
1320
*/
1421
export const ContextCollapseIndicator: React.FC<ContextCollapseIndicatorProps> = ({
1522
lineCount,
1623
onCollapse,
1724
position,
25+
mode = "collapse",
1826
}) => {
27+
const verb = mode === "collapse" ? "Collapse" : "Show";
28+
const ariaLabel =
29+
mode === "collapse" ? `Collapse context ${position}` : `Show context ${position}`;
1930
return (
2031
<div className="flex items-center justify-center">
2132
<button
2233
onClick={onCollapse}
2334
className="text-muted hover:bg-background-highlight inline-flex cursor-pointer items-center gap-1 rounded px-1.5 py-px transition-colors"
24-
aria-label={`Collapse context ${position}`}
35+
aria-label={ariaLabel}
2536
>
2637
{/* Squiggly line SVG - horizontal orientation for separator */}
2738
<svg
@@ -41,7 +52,7 @@ export const ContextCollapseIndicator: React.FC<ContextCollapseIndicatorProps> =
4152
/>
4253
</svg>
4354
<span className="text-[10px] font-medium">
44-
Collapse {lineCount} line{lineCount === 1 ? "" : "s"} {position}
55+
{verb} {lineCount} line{lineCount === 1 ? "" : "s"} {position}
4556
</span>
4657
<svg
4758
className="text-border shrink-0"

β€Žsrc/browser/features/RightSidebar/CodeReview/HunkViewer.tsxβ€Ž

Lines changed: 167 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
*/
55

66
import React, { useState, useMemo } from "react";
7-
import { Check, Circle } from "lucide-react";
7+
import { Check, Circle, Sparkles } from "lucide-react";
88
import type { DiffHunk, Review, ReviewNoteData } from "@/common/types/review";
99
import { SelectableDiffRenderer } from "../../Shared/DiffRenderer";
1010
import type { ReviewActionCallbacks } from "../../Shared/InlineReviewNote";
@@ -25,6 +25,7 @@ import { formatRelativeTime } from "@/browser/utils/ui/dateTime";
2525
import { cn } from "@/common/lib/utils";
2626
import { ContextCollapseIndicator } from "./ContextCollapseIndicator";
2727
import { useReadMore } from "./useReadMore";
28+
import { sliceHunkByNewLineRange } from "@/browser/utils/review/sliceHunkContent";
2829

2930
interface HunkViewerProps {
3031
hunk: DiffHunk;
@@ -51,6 +52,25 @@ interface HunkViewerProps {
5152
reviewActions?: ReviewActionCallbacks;
5253
/** Prefer a collapsed default for huge reviews so opening Review doesn't mount every diff line. */
5354
preferCollapsed?: boolean;
55+
/**
56+
* Optional comment from the agent (via `review_pane_update`) explaining
57+
* why this hunk was flagged for review. Rendered above the hunk's header
58+
* row when present.
59+
*/
60+
assistedComment?: string;
61+
/**
62+
* Whether this hunk was flagged by the agent. Used to surface a subtle
63+
* accent indicator on the hunk header even when no comment was provided.
64+
*/
65+
isAssisted?: boolean;
66+
/**
67+
* When set, the hunk body is trimmed to just these new-side line numbers
68+
* (inclusive) by default. Lines before/after the range hide behind a
69+
* "Show N lines …" affordance that reuses the existing context-collapse
70+
* indicator. Pass a stable reference (e.g. memoized lookup) to keep
71+
* `React.memo` working.
72+
*/
73+
visibleNewLineRange?: { start: number; end: number };
5474
}
5575

5676
function renderHighlightedFilePath(
@@ -129,6 +149,9 @@ export const HunkViewer = React.memo<HunkViewerProps>(
129149
reviewActions,
130150
includeUncommitted,
131151
preferCollapsed = false,
152+
assistedComment,
153+
isAssisted = false,
154+
visibleNewLineRange,
132155
}) => {
133156
// Ref for the hunk container to track visibility
134157
const hunkRef = React.useRef<HTMLDivElement>(null);
@@ -192,6 +215,25 @@ export const HunkViewer = React.memo<HunkViewerProps>(
192215
[hunk.filePath, searchConfig]
193216
);
194217

218+
// Assisted-review trim: when the agent flagged a specific new-side range,
219+
// render only those lines by default and hide the surrounding diff behind
220+
// a "Show N lines …" affordance. The slicer returns null when trimming
221+
// would be a no-op (range covers the whole hunk, pure deletions, etc.),
222+
// so the existing single-renderer path still applies in those cases.
223+
const hunkSlice = useMemo(
224+
() => (visibleNewLineRange ? sliceHunkByNewLineRange(hunk, visibleNewLineRange) : null),
225+
[hunk, visibleNewLineRange]
226+
);
227+
const [showSliceBefore, setShowSliceBefore] = useState(false);
228+
const [showSliceAfter, setShowSliceAfter] = useState(false);
229+
// Reset show-more state when the underlying slice changes (e.g. the agent
230+
// updates the assisted range, or the user navigates to a different hunk
231+
// that reuses this memoized component instance).
232+
React.useEffect(() => {
233+
setShowSliceBefore(false);
234+
setShowSliceAfter(false);
235+
}, [hunkSlice]);
236+
195237
// Persist manual expand/collapse state across remounts per workspace
196238
// Maps hunkId -> isExpanded for user's manual preferences
197239
// Enable listener to synchronize updates across all HunkViewer instances
@@ -205,7 +247,13 @@ export const HunkViewer = React.memo<HunkViewerProps>(
205247
const hasManualState = hunkId in expandStateMap;
206248
const manualExpandState = expandStateMap[hunkId];
207249

208-
const shouldAutoExpand = !isRead && !isLargeHunk && (!preferCollapsed || Boolean(isSelected));
250+
// Agent-flagged hunks should default to expanded even when they're "large"
251+
// or in a heavy review where everything else is collapsed β€” otherwise the
252+
// assisted-review focus signal gets buried.
253+
const shouldAutoExpand =
254+
!isRead &&
255+
(!isLargeHunk || Boolean(visibleNewLineRange)) &&
256+
(!preferCollapsed || Boolean(isSelected) || Boolean(visibleNewLineRange));
209257

210258
// Determine initial expand state (priority: manual > read status > size/review scale)
211259
const [isExpanded, setIsExpanded] = useState(() => {
@@ -301,7 +349,25 @@ export const HunkViewer = React.memo<HunkViewerProps>(
301349
tabIndex={0}
302350
data-hunk-id={hunkId}
303351
>
304-
<div className="border-border-light font-monospace flex items-center gap-1.5 border-b px-2 py-1 text-[11px]">
352+
{(assistedComment ?? isAssisted) && (
353+
<div
354+
className="border-review-accent/40 bg-review-accent/5 text-foreground flex items-start gap-2 border-b px-2 py-1.5 text-[11px] leading-[1.4]"
355+
data-testid="hunk-assisted-comment"
356+
>
357+
<Sparkles aria-hidden="true" className="text-review-accent mt-[2px] h-3 w-3 shrink-0" />
358+
{assistedComment ? (
359+
<span className="min-w-0 break-words whitespace-pre-wrap">{assistedComment}</span>
360+
) : (
361+
<span className="text-muted italic">Flagged by agent for review</span>
362+
)}
363+
</div>
364+
)}
365+
<div
366+
className={cn(
367+
"border-border-light font-monospace flex items-center gap-1.5 border-b px-2 py-1 text-[11px]",
368+
isAssisted && !assistedComment && "border-l-review-accent border-l-2"
369+
)}
370+
>
305371
{onToggleRead && (
306372
<Tooltip>
307373
<TooltipTrigger asChild>
@@ -401,28 +467,104 @@ export const HunkViewer = React.memo<HunkViewerProps>(
401467
</>
402468
)}
403469

404-
{/* Original hunk content */}
405-
<SelectableDiffRenderer
406-
content={hunk.content}
407-
filePath={hunk.filePath}
408-
inlineReviews={inlineReviews}
409-
oldStart={hunk.oldStart}
410-
newStart={hunk.newStart}
411-
fontSize="11px"
412-
maxHeight="none"
413-
className="rounded-none border-0 [&>div]:overflow-x-visible"
414-
onReviewNote={onReviewNote}
415-
onLineClick={() => {
416-
const syntheticEvent = {
417-
currentTarget: { dataset: { hunkId } },
418-
} as unknown as React.MouseEvent<HTMLElement>;
419-
onClick?.(syntheticEvent);
420-
}}
421-
searchConfig={searchConfig}
422-
enableHighlighting={isVisible}
423-
onComposingChange={handleComposingChange}
424-
reviewActions={reviewActions}
425-
/>
470+
{/* Original hunk content (sliced when the agent flagged a range). */}
471+
{(() => {
472+
const renderBody = (content: string, oldStart: number, newStart: number) => (
473+
<SelectableDiffRenderer
474+
content={content}
475+
filePath={hunk.filePath}
476+
inlineReviews={inlineReviews}
477+
oldStart={oldStart}
478+
newStart={newStart}
479+
fontSize="11px"
480+
maxHeight="none"
481+
className="rounded-none border-0 [&>div]:overflow-x-visible"
482+
onReviewNote={onReviewNote}
483+
onLineClick={() => {
484+
const syntheticEvent = {
485+
currentTarget: { dataset: { hunkId } },
486+
} as unknown as React.MouseEvent<HTMLElement>;
487+
onClick?.(syntheticEvent);
488+
}}
489+
searchConfig={searchConfig}
490+
enableHighlighting={isVisible}
491+
onComposingChange={handleComposingChange}
492+
reviewActions={reviewActions}
493+
/>
494+
);
495+
496+
if (!hunkSlice) {
497+
return renderBody(hunk.content, hunk.oldStart, hunk.newStart);
498+
}
499+
500+
return (
501+
<>
502+
{hunkSlice.beforeLineCount > 0 &&
503+
(showSliceBefore ? (
504+
<>
505+
{renderBody(
506+
hunkSlice.beforeContent,
507+
hunkSlice.beforeOldStart,
508+
hunkSlice.beforeNewStart
509+
)}
510+
<ContextCollapseIndicator
511+
lineCount={hunkSlice.beforeLineCount}
512+
onCollapse={(e) => {
513+
e.stopPropagation();
514+
setShowSliceBefore(false);
515+
}}
516+
position="above"
517+
mode="collapse"
518+
/>
519+
</>
520+
) : (
521+
<ContextCollapseIndicator
522+
lineCount={hunkSlice.beforeLineCount}
523+
onCollapse={(e) => {
524+
e.stopPropagation();
525+
setShowSliceBefore(true);
526+
}}
527+
position="above"
528+
mode="expand"
529+
/>
530+
))}
531+
{renderBody(
532+
hunkSlice.insideContent,
533+
hunkSlice.insideOldStart,
534+
hunkSlice.insideNewStart
535+
)}
536+
{hunkSlice.afterLineCount > 0 &&
537+
(showSliceAfter ? (
538+
<>
539+
<ContextCollapseIndicator
540+
lineCount={hunkSlice.afterLineCount}
541+
onCollapse={(e) => {
542+
e.stopPropagation();
543+
setShowSliceAfter(false);
544+
}}
545+
position="below"
546+
mode="collapse"
547+
/>
548+
{renderBody(
549+
hunkSlice.afterContent,
550+
hunkSlice.afterOldStart,
551+
hunkSlice.afterNewStart
552+
)}
553+
</>
554+
) : (
555+
<ContextCollapseIndicator
556+
lineCount={hunkSlice.afterLineCount}
557+
onCollapse={(e) => {
558+
e.stopPropagation();
559+
setShowSliceAfter(true);
560+
}}
561+
position="below"
562+
mode="expand"
563+
/>
564+
))}
565+
</>
566+
);
567+
})()}
426568

427569
{/* Expanded content below */}
428570
{downContent && (

β€Žsrc/browser/features/RightSidebar/CodeReview/ReviewControls.tsxβ€Ž

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
*/
44

55
import React from "react";
6-
import { ArrowLeft, Maximize2 } from "lucide-react";
6+
import { ArrowLeft, Maximize2, Sparkles } from "lucide-react";
77
import { usePersistedState } from "@/browser/hooks/usePersistedState";
88
import { useTutorial } from "@/browser/contexts/TutorialContext";
99
import {
@@ -42,6 +42,12 @@ interface ReviewControlsProps {
4242
isImmersive?: boolean;
4343
/** Toggle immersive review mode */
4444
onToggleImmersive?: () => void;
45+
/**
46+
* Number of agent-flagged "Assisted" hunks the agent has pinned via
47+
* the `review_pane_update` tool. When zero, the Assisted toggle is hidden
48+
* so the control bar stays compact for normal review sessions.
49+
*/
50+
assistedCount?: number;
4551
}
4652

4753
export const ReviewControls: React.FC<ReviewControlsProps> = ({
@@ -57,6 +63,7 @@ export const ReviewControls: React.FC<ReviewControlsProps> = ({
5763
lastRefreshFailure,
5864
isImmersive = false,
5965
onToggleImmersive,
66+
assistedCount = 0,
6067
}) => {
6168
// Per-project default base (used for new workspaces in this project)
6269
const [defaultBase, setDefaultBase] = usePersistedState<string>(
@@ -89,6 +96,11 @@ export const ReviewControls: React.FC<ReviewControlsProps> = ({
8996
onFiltersChange((prev) => ({ ...prev, showReadHunks: checked }));
9097
};
9198

99+
const handleAssistedToggle = (e: React.ChangeEvent<HTMLInputElement>) => {
100+
const checked = e.target.checked;
101+
onFiltersChange((prev) => ({ ...prev, assistedOnly: checked }));
102+
};
103+
92104
const handleSortChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
93105
const sortOrder = e.target.value as ReviewSortOrder;
94106
onFiltersChange((prev) => ({ ...prev, sortOrder }));
@@ -186,6 +198,29 @@ export const ReviewControls: React.FC<ReviewControlsProps> = ({
186198
/>
187199
</label>
188200

201+
{assistedCount > 0 && (
202+
<>
203+
<div className="bg-border-light h-3 w-px" />
204+
<TooltipIfPresent
205+
tooltip={`Show only the ${assistedCount} hunk${assistedCount === 1 ? "" : "s"} the agent flagged for review`}
206+
side="bottom"
207+
>
208+
<label className="text-muted hover:text-foreground flex cursor-pointer items-center gap-1 whitespace-nowrap">
209+
<Sparkles aria-hidden="true" className="text-review-accent h-3 w-3 shrink-0" />
210+
<span>Assisted:</span>
211+
<input
212+
type="checkbox"
213+
aria-label="Show only agent-flagged hunks"
214+
checked={filters.assistedOnly}
215+
onChange={handleAssistedToggle}
216+
className="h-3 w-3 cursor-pointer"
217+
/>
218+
<span className="text-dim text-[10px]">({assistedCount})</span>
219+
</label>
220+
</TooltipIfPresent>
221+
</>
222+
)}
223+
189224
<div className="bg-border-light h-3 w-px" />
190225

191226
<label className="text-muted flex items-center gap-1 whitespace-nowrap">

0 commit comments

Comments
Β (0)