Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
108 changes: 72 additions & 36 deletions src/browser/features/ChatInput/AttachedReviewsPanel.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import React from "react";
import { X } from "lucide-react";
import { MessageSquare, X } from "lucide-react";
import { Tooltip, TooltipTrigger, TooltipContent } from "@/browser/components/Tooltip/Tooltip";
import { ChatInputDecoration } from "@/browser/components/ChatPane/ChatInputDecoration";
import { usePersistedState } from "@/browser/hooks/usePersistedState";
import { getAttachedReviewsExpandedKey } from "@/common/constants/storage";
import { ReviewBlockFromData } from "../Shared/ReviewBlock";
import type { Review } from "@/common/types/review";

export interface AttachedReviewsPanelProps {
workspaceId: string;
reviews: Review[];
onDetachAll?: () => void;
onDetach?: (reviewId: string) => void;
Expand All @@ -14,52 +18,84 @@ export interface AttachedReviewsPanelProps {
}

/**
* Displays attached reviews in the chat input area.
* Shows a header with count and "Clear all" button when multiple reviews attached.
* Displays reviews attached to the pending message as a collapsible chat-input
* decoration. Reuses the {@link ChatInputDecoration} primitive so the panel
* reads with the same collapsed chrome as the other composer decorations and so
* a long list of attachments can be tucked away without detaching them. The
* collapsed/expanded intent persists per-workspace; the panel defaults to
* expanded to preserve the prior always-visible behavior. The summary surfaces
* the count, and "Clear all" lives in the expanded body when multiple reviews
* are attached.
*/
export const AttachedReviewsPanel: React.FC<AttachedReviewsPanelProps> = ({
workspaceId,
reviews,
onDetachAll,
onDetach,
onCheck,
onDelete,
onUpdateNote,
}) => {
const [expanded, setExpanded] = usePersistedState(
getAttachedReviewsExpandedKey(workspaceId),
true
);

if (reviews.length === 0) return null;

return (
<div className="border-border max-h-[50vh] space-y-2 overflow-y-auto border-b px-1.5 py-1.5">
{/* Header with count and clear all button */}
<div className="flex items-center justify-between text-xs">
<span className="text-muted font-medium">
{reviews.length} review{reviews.length !== 1 && "s"} attached
</span>
{onDetachAll && reviews.length > 1 && (
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
onClick={onDetachAll}
className="text-muted hover:text-error flex items-center gap-1 text-xs transition-colors"
>
<X className="size-3" />
Clear all
</button>
</TooltipTrigger>
<TooltipContent>Remove all reviews from message</TooltipContent>
</Tooltip>
)}
</div>
{reviews.map((review) => (
<ReviewBlockFromData
key={review.id}
data={review.data}
onComplete={onCheck ? () => onCheck(review.id) : undefined}
onDetach={onDetach ? () => onDetach(review.id) : undefined}
onDelete={onDelete ? () => onDelete(review.id) : undefined}
onEditComment={onUpdateNote ? (newNote) => onUpdateNote(review.id, newNote) : undefined}
/>
))}
</div>
<ChatInputDecoration
expanded={expanded}
onToggle={() => setExpanded(!expanded)}
dataComponent="AttachedReviewsPanel"
// Unlike the decorations stacked above the composer, this panel renders
// inside the chat-input card (which already supplies the gutter), so drop
// the primitive's top border + horizontal padding and keep a bottom
// divider above the textarea, matching the panel's prior placement.
className="border-t-0 border-b px-0"
contentClassName="max-h-[50vh] space-y-2 overflow-y-auto py-1.5"
summary={
<>
<MessageSquare className="size-3.5 text-[var(--color-review-accent)] transition-colors" />
<span className="text-muted group-hover:text-secondary transition-colors">
<span className="font-medium">{reviews.length}</span> review
{reviews.length !== 1 && "s"} attached
</span>
</>
}
renderExpanded={() => (
<>
{onDetachAll && reviews.length > 1 && (
<div className="flex items-center justify-end">
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
onClick={onDetachAll}
className="text-muted hover:text-error flex items-center gap-1 text-xs transition-colors"
>
<X className="size-3" />
Clear all
</button>
</TooltipTrigger>
<TooltipContent>Remove all reviews from message</TooltipContent>
</Tooltip>
</div>
)}
{reviews.map((review) => (
<ReviewBlockFromData
key={review.id}
data={review.data}
onComplete={onCheck ? () => onCheck(review.id) : undefined}
onDetach={onDetach ? () => onDetach(review.id) : undefined}
onDelete={onDelete ? () => onDelete(review.id) : undefined}
onEditComment={
onUpdateNote ? (newNote) => onUpdateNote(review.id, newNote) : undefined
}
/>
))}
</>
)}
/>
);
};
1 change: 1 addition & 0 deletions src/browser/features/ChatInput/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2992,6 +2992,7 @@ const ChatInputInner: React.FC<ChatInputProps> = (props) => {
{/* Hide during send to avoid duplicate display with the sent message */}
{variant === "workspace" && !hideReviewsDuringSend && (
<AttachedReviewsPanel
workspaceId={props.workspaceId}
reviews={reviewPanelItems}
onDetachAll={
reviewOverrideActive
Expand Down
9 changes: 9 additions & 0 deletions src/common/constants/storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,15 @@ export function getPinnedTodoExpandedKey(workspaceId: string): string {
return `pinnedTodoExpanded:${workspaceId}`;
}

/**
* Get the localStorage key for the attached reviews panel expansion state.
* Lets a long list of reviews attached to the pending message be collapsed
* without detaching them. Format: "attachedReviewsExpanded:{workspaceId}"
*/
export function getAttachedReviewsExpandedKey(workspaceId: string): string {
return `attachedReviewsExpanded:${workspaceId}`;
}

/**
* Get the localStorage key for per-workspace transcript auto-expand preferences.
*
Expand Down
Loading