Introduce inline comment in plan and diff panel (#277)

* feat(chat): support inline comment drafts in chat input

Add per-session inline comment draft tracking
Append inline drafts to outgoing messages when queuing
Consume drafts after sending to clear state

* feat: add actions menu to PierreDiffViewer

Add a dropdown actions menu in the diff viewer with More and Delete options
Enable inline comment drafting support and selection synchronization to prevent loops
Replace previous action icon with contextual actions in the header

* feat(plan): enable inline comment drafting in PlanView

Add inline comment draft support with addDraft, removeDraft and drafts state
Compute session key for drafts based on currentSessionId and draft state
Prepare dropdown and toast utilities for draft actions in PlanView

* feat: add inline comment formatting utilities

Introduce formatInlineCommentDraft to render inline comments for diff and plan views
Add appendInlineComments to attach comments to existing messages with proper separation
Provide hasInlineComments to detect inline comment blocks in text

* feat: add inline comment draft store

Persist drafts per session key for inline comments
Add, remove, and clear drafts with unique IDs per session
Consume drafts to move them out of store after submission
This commit is contained in:
Nelson Pires
2026-02-03 20:05:01 +02:00
committed by GitHub
parent 0e35e7f46b
commit c119d89084
5 changed files with 660 additions and 229 deletions
+72 -5
View File
@@ -12,6 +12,8 @@ import { useConfigStore } from '@/stores/useConfigStore';
import { useUIStore } from '@/stores/useUIStore';
import { useMessageQueueStore, type QueuedMessage } from '@/stores/messageQueueStore';
import type { AttachedFile } from '@/stores/types/sessionTypes';
import { useInlineCommentDraftStore } from '@/stores/useInlineCommentDraftStore';
import { appendInlineComments } from '@/lib/messages/inlineComments';
import { AttachedFilesList } from './FileAttachment';
import { QueuedMessageChips } from './QueuedMessageChips';
import { FileMentionAutocomplete, type FileMentionHandle } from './FileMentionAutocomplete';
@@ -112,6 +114,20 @@ export const ChatInput: React.FC<ChatInputProps> = ({ onOpenSettings, scrollToBo
const addToQueue = useMessageQueueStore((state) => state.addToQueue);
const clearQueue = useMessageQueueStore((state) => state.clearQueue);
// Inline comment drafts
const draftCount = useInlineCommentDraftStore(
React.useCallback(
(state) => {
const sessionKey = currentSessionId ?? (newSessionDraftOpen ? 'draft' : '');
if (!sessionKey) return 0;
return (state.drafts[sessionKey] ?? []).length;
},
[currentSessionId, newSessionDraftOpen]
)
);
const consumeDrafts = useInlineCommentDraftStore((state) => state.consumeDrafts);
const hasDrafts = draftCount > 0;
// Session activity for auto-send on idle
const { phase: sessionPhase } = useCurrentSessionActivity();
const prevSessionPhaseRef = React.useRef(sessionPhase);
@@ -223,7 +239,7 @@ export const ChatInput: React.FC<ChatInputProps> = ({ onOpenSettings, scrollToBo
}
}, [pendingInputText, consumePendingInputText]);
const hasContent = message.trim() || attachedFiles.length > 0;
const hasContent = message.trim() || attachedFiles.length > 0 || hasDrafts;
const hasQueuedMessages = queuedMessages.length > 0;
const canSend = hasContent || hasQueuedMessages;
@@ -233,7 +249,16 @@ export const ChatInput: React.FC<ChatInputProps> = ({ onOpenSettings, scrollToBo
const handleQueueMessage = React.useCallback(() => {
if (!hasContent || !currentSessionId) return;
const messageToQueue = message.replace(/^\n+|\n+$/g, '');
// Get and consume drafts for this session
const sessionKey = currentSessionId;
const drafts = consumeDrafts(sessionKey);
// Build message with appended drafts
let messageToQueue = message.replace(/^\n+|\n+$/g, '');
if (drafts.length > 0) {
messageToQueue = appendInlineComments(messageToQueue, drafts);
}
const attachmentsToQueue = attachedFiles.map((file) => ({ ...file }));
addToQueue(currentSessionId, {
@@ -250,7 +275,7 @@ export const ChatInput: React.FC<ChatInputProps> = ({ onOpenSettings, scrollToBo
if (!isMobile) {
textareaRef.current?.focus();
}
}, [hasContent, currentSessionId, message, attachedFiles, addToQueue, clearAttachedFiles, isMobile]);
}, [hasContent, currentSessionId, message, attachedFiles, addToQueue, clearAttachedFiles, isMobile, consumeDrafts]);
const handleSubmit = async (e?: React.FormEvent) => {
e?.preventDefault();
@@ -317,6 +342,28 @@ export const ChatInput: React.FC<ChatInputProps> = ({ onOpenSettings, scrollToBo
}
}
// Get session key for drafts (use currentSessionId or 'draft' for new sessions)
const sessionKey = currentSessionId ?? (newSessionDraftOpen ? 'draft' : null);
let drafts: import('@/stores/useInlineCommentDraftStore').InlineCommentDraft[] = [];
if (sessionKey) {
drafts = consumeDrafts(sessionKey);
}
// Append drafts to the message if any exist
if (drafts.length > 0) {
if (queuedMessages.length === 0) {
// No queue - append to primary text
primaryText = appendInlineComments(primaryText, drafts);
} else if (additionalParts.length > 0) {
// Has queue with additional parts - append to the last part (current input)
const lastPart = additionalParts[additionalParts.length - 1];
lastPart.text = appendInlineComments(lastPart.text, drafts);
} else {
// Has queue but no additional parts yet (shouldn't happen with hasContent check, but handle it)
primaryText = appendInlineComments(primaryText, drafts);
}
}
if (!primaryText && additionalParts.length === 0) return;
// Clear queue and input
@@ -1373,14 +1420,34 @@ export const ChatInput: React.FC<ChatInputProps> = ({ onOpenSettings, scrollToBo
</div>
)}
<AttachedFilesList />
<QueuedMessageChips
<QueuedMessageChips
onEditMessage={(content) => {
setMessage(content);
setTimeout(() => {
textareaRef.current?.focus();
}, 0);
}}
}}
/>
{/* Review comments chip */}
{hasDrafts && (
<div className="pb-2">
<div
className="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-xl border"
style={{
backgroundColor: currentTheme?.colors?.surface?.elevated,
borderColor: currentTheme?.colors?.interactive?.border,
}}
>
<span className="text-xs font-medium text-muted-foreground">Review comments:</span>
<span
className="text-xs font-semibold"
style={{ color: currentTheme?.colors?.status?.info }}
>
{draftCount}
</span>
</div>
</div>
)}
<div
className={cn(
"flex flex-col relative overflow-visible",