feat(composer): context chip previews with inline comment editing

Hovering (or tapping) a context chip opens a stacked preview of its
pending items above the composer: numbered entries with a muted header
band, the captured selection, and the user's comment, which can be
edited in place (save/cancel) or removed before sending. The chips
component now subscribes to the draft store itself; the per-kind count
plumbing in ChatInput is gone.
This commit is contained in:
Bohdan Triapitsyn
2026-08-24 00:16:26 +03:00
parent c3f89e0106
commit 538b532fb1
13 changed files with 388 additions and 190 deletions
@@ -752,55 +752,8 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({
[inlineDraftKey]
)
);
const draftSourceKey = useInlineCommentDraftStore(
React.useCallback(
(state) => {
const drafts = inlineDraftKey ? (state.drafts[inlineDraftKey] ?? []) : [];
let previewAnnotation = 0;
let review = 0;
let terminal = 0;
let prComment = 0;
let prCheck = 0;
let chatQuote = 0;
for (const draft of drafts) {
if (draft.source === 'preview-annotation') previewAnnotation += 1;
else if (draft.source === 'terminal') terminal += 1;
else if (draft.source === 'pr-comment') prComment += 1;
else if (draft.source === 'pr-check') prCheck += 1;
else if (draft.source === 'chat-quote') chatQuote += 1;
else review += 1;
}
return `${previewAnnotation}:${review}:${terminal}:${prComment}:${prCheck}:${chatQuote}`;
},
[inlineDraftKey]
)
);
const consumeDrafts = useInlineCommentDraftStore((state) => state.consumeDrafts);
const removeInlineCommentDraft = useInlineCommentDraftStore((state) => state.removeDraft);
const hasDrafts = draftCount > 0;
const [previewAnnotationCount, reviewCount, terminalContextCount, prCommentCount, prCheckCount, chatQuoteCount] = draftSourceKey.split(':').map((entry) => Number(entry) || 0);
const terminalContextDrafts = terminalContextCount > 0
? (inlineDraftKey ? useInlineCommentDraftStore.getState().drafts[inlineDraftKey] ?? [] : []).filter((draft) => draft.source === 'terminal')
: [];
const removePreviewDrafts = React.useCallback((source: 'preview-annotation' | 'pr-comment' | 'pr-check' | 'chat-quote') => {
if (!inlineDraftTarget) return;
const drafts = useInlineCommentDraftStore.getState().getDrafts(inlineDraftTarget);
for (const draft of drafts) {
if (draft.source === source) {
removeInlineCommentDraft(inlineDraftTarget, draft.id);
}
}
}, [inlineDraftTarget, removeInlineCommentDraft]);
// Review comments are the inline-comment drafts that aren't preview sources.
const removeReviewDrafts = React.useCallback(() => {
if (!inlineDraftTarget) return;
const drafts = useInlineCommentDraftStore.getState().getDrafts(inlineDraftTarget);
for (const draft of drafts) {
if (draft.source !== 'preview-annotation' && draft.source !== 'terminal' && draft.source !== 'pr-comment' && draft.source !== 'pr-check' && draft.source !== 'chat-quote') {
removeInlineCommentDraft(inlineDraftTarget, draft.id);
}
}
}, [inlineDraftTarget, removeInlineCommentDraft]);
// User message history for up/down arrow navigation.
// Keep this on a narrow hook instead of full session message records.
@@ -2661,16 +2614,7 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({
<AutoReviewBanner />
{hasDrafts ? (
<ComposerContextChips
terminalDrafts={terminalContextDrafts}
reviewCount={reviewCount}
prCommentCount={prCommentCount}
prCheckCount={prCheckCount}
previewAnnotationCount={previewAnnotationCount}
chatQuoteCount={chatQuoteCount}
draftTarget={inlineDraftTarget}
onRemoveDraft={removeInlineCommentDraft}
onRemoveReviewDrafts={removeReviewDrafts}
onRemovePreviewDrafts={removePreviewDrafts}
colors={currentTheme.colors}
/>
) : null}