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
6 changes: 6 additions & 0 deletions packages/core/config-types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
export type DefaultDiffType = 'since-base' | 'uncommitted' | 'unstaged' | 'staged' | 'merge-base' | 'all';
export type DiffLineBgIntensity = 'subtle' | 'normal' | 'strong';

/**
* Which keystroke submits a text composer (comment editors, AI chat inputs).
* 'enter' additionally frees Mod+Enter for Ask AI on composers that offer it.
*/
export type ComposerSubmitKey = 'mod-enter' | 'enter';

export interface DiffOptions {
diffStyle?: 'split' | 'unified';
overflow?: 'scroll' | 'wrap';
Expand Down
14 changes: 7 additions & 7 deletions packages/review-editor/components/AITab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { CountBadge } from './CountBadge';
import { CopyButton } from './CopyButton';
import { PermissionCard } from './PermissionCard';
import { AIConfigBar } from './AIConfigBar';
import { submitHint } from '@plannotator/ui/utils/platform';
import { useComposerKeys, useComposerSubmitHint } from '@plannotator/ui/hooks/useComposerKeys';
import { OverlayScrollArea } from '@plannotator/ui/components/OverlayScrollArea';
import type { AIProviderOption } from '@plannotator/ui/utils/aiProvider';

Expand Down Expand Up @@ -294,6 +294,11 @@ const GeneralInput: React.FC<{
onStop?: () => void;
}> = ({ value, onChange, onSubmit, disabled, isStreaming, onStop }) => {
const textareaRef = useRef<HTMLTextAreaElement>(null);
const handleKeyDown = useComposerKeys({
onSubmit,
canSubmit: !disabled && value.trim().length > 0,
});
const submitHint = useComposerSubmitHint();

const autoResize = useCallback(() => {
const el = textareaRef.current;
Expand All @@ -317,12 +322,7 @@ const GeneralInput: React.FC<{
className="flex-1 px-2.5 py-1.5 bg-muted rounded-md text-xs text-foreground placeholder:text-muted-foreground/50 resize-none focus:outline-none focus:ring-1 focus:ring-primary/50 leading-relaxed"
style={{ maxHeight: 120 }}
disabled={disabled}
onKeyDown={(e) => {
if (e.key === 'Enter' && (e.metaKey || e.ctrlKey) && !e.nativeEvent.isComposing && !disabled) {
e.preventDefault();
onSubmit();
}
}}
onKeyDown={handleKeyDown}
/>
{isStreaming && onStop ? (
<button
Expand Down
17 changes: 10 additions & 7 deletions packages/review-editor/components/AnnotationToolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { ConventionalLabelPicker, type LabelDef } from './ConventionalLabelPicke
import type { ConventionalLabel, ConventionalDecoration } from '@plannotator/ui/types';
import type { AIChatEntry } from '../hooks/useAIChat';
import { useDraggable } from '@plannotator/ui/hooks/useDraggable';
import { useComposerKeys } from '@plannotator/ui/hooks/useComposerKeys';

interface AnnotationToolbarProps {
toolbarState: ToolbarState;
Expand Down Expand Up @@ -105,6 +106,14 @@ export const AnnotationToolbar: React.FC<AnnotationToolbarProps> = ({
onCancel(); // close the whole toolbar
};

const askAIFromComment = aiAvailable && !isEditing && !!onAskAI && commentText.trim().length > 0;
const handleCommentKeyDown = useComposerKeys({
onSubmit,
onAskAI: askAIFromComment ? handleAskAIClick : undefined,
onCancel: () => onDismiss(),
canSubmit: commentText.trim().length > 0 || suggestedCode.trim().length > 0,
});

const content = (
<div
ref={toolbarRef}
Expand Down Expand Up @@ -180,13 +189,7 @@ export const AnnotationToolbar: React.FC<AnnotationToolbarProps> = ({
className="w-full min-h-[4.5rem] max-h-[calc(100vh-16rem)] px-3 py-2 bg-muted rounded-lg text-xs leading-6 resize-y border-0 focus:outline-none focus:ring-1 focus:ring-primary/50 placeholder:text-muted-foreground"
rows={3}
autoFocus
onKeyDown={(e) => {
if (e.key === 'Escape') {
onDismiss();
} else if (e.key === 'Enter' && (e.metaKey || e.ctrlKey) && !e.nativeEvent.isComposing) {
onSubmit();
}
}}
onKeyDown={handleCommentKeyDown}
/>

{/* Optional suggested code section */}
Expand Down
17 changes: 9 additions & 8 deletions packages/review-editor/components/AskAIInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React, { useState } from 'react';
import { formatLineRange } from '../utils/formatLineRange';
import { SparklesIcon } from '@plannotator/ui/components/SparklesIcon';
import type { AIChatEntry } from '../hooks/useAIChat';
import { submitHint } from '@plannotator/ui/utils/platform';
import { useComposerKeys, useComposerSubmitHint } from '@plannotator/ui/hooks/useComposerKeys';

interface AskAIInputProps {
lineStart: number;
Expand Down Expand Up @@ -41,6 +41,13 @@ export const AskAIInput: React.FC<AskAIInputProps> = ({
setQuestion('');
};

const handleKeyDown = useComposerKeys({
onSubmit: handleSubmit,
onCancel: () => onCancel(),
canSubmit: !isLoading && question.trim().length > 0,
});
const submitHint = useComposerSubmitHint();

return (
<div className="w-80">
<div className="flex items-center justify-between mb-2" {...dragHandleProps}>
Expand All @@ -67,13 +74,7 @@ export const AskAIInput: React.FC<AskAIInputProps> = ({
rows={2}
autoFocus
disabled={isLoading}
onKeyDown={(e) => {
if (e.key === 'Escape') {
onCancel();
} else if (e.key === 'Enter' && (e.metaKey || e.ctrlKey) && !e.nativeEvent.isComposing) {
handleSubmit();
}
}}
onKeyDown={handleKeyDown}
/>

<div className="flex items-center gap-2 mt-2">
Expand Down
32 changes: 23 additions & 9 deletions packages/review-editor/components/ExpandedCommentDialog.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import React, { useRef } from 'react';
import { Dialog } from '@base-ui/react/dialog';
import { SparklesIcon } from '@plannotator/ui/components/SparklesIcon';
import { useReviewAnnotationToolbarShortcuts } from '@plannotator/ui/shortcuts';
import { matchesShortcutBinding, useReviewAnnotationToolbarShortcuts } from '@plannotator/ui/shortcuts';
import { useComposerKeys } from '@plannotator/ui/hooks/useComposerKeys';

interface ExpandedCommentDialogProps {
title: string;
Expand Down Expand Up @@ -33,17 +34,11 @@ export const ExpandedCommentDialog: React.FC<ExpandedCommentDialogProps> = ({
const askAIEnabled = aiAvailable && !!onAskAI && commentText.trim().length > 0;
const submitLabel = isEditing ? 'Update' : 'Add Comment';

// Submit/Ask AI run off the popup's own keydown (see handleKeyDown below) so
// they follow the composer keymap; Escape stays document-level.
useReviewAnnotationToolbarShortcuts({
target: 'document',
handlers: {
submitComment: {
when: (event) => canSubmit && !event.isComposing && event.target instanceof Node && !!dialogRef.current?.contains(event.target),
handle: (event) => {
event.preventDefault();
event.stopPropagation();
onSubmit();
},
},
cancel: {
when: (event) => event.target instanceof Node && !!dialogRef.current?.contains(event.target),
handle: (event) => {
Expand All @@ -60,6 +55,24 @@ export const ExpandedCommentDialog: React.FC<ExpandedCommentDialogProps> = ({
onAskAI?.(commentText.trim());
};

const composerKeys = useComposerKeys({
onSubmit,
onAskAI: askAIEnabled ? handleAskAI : undefined,
canSubmit,
});

// Bound to the popup rather than the textarea so Mod+Enter keeps working
// wherever focus sits inside the dialog.
const handleKeyDown = (event: React.KeyboardEvent<HTMLElement>) => {
const isModEnter = matchesShortcutBinding(event.nativeEvent, 'Mod+Enter');
// The textarea gets the whole keymap. Elsewhere only Mod+Enter runs: under
// "Enter sends", bare Enter on a focused button must press that button.
if (event.target === textareaRef.current || isModEnter) composerKeys(event);
// Mod+Enter must never reach the window listener that submits the whole
// review while this dialog is open, even when the composer declined it.
if (isModEnter) event.stopPropagation();
};

return (
<Dialog.Root
open
Expand All @@ -80,6 +93,7 @@ export const ExpandedCommentDialog: React.FC<ExpandedCommentDialogProps> = ({
return textarea;
}}
finalFocus={false}
onKeyDown={handleKeyDown}
className="fixed left-1/2 top-1/2 z-[2000] w-[calc(100vw-2rem)] max-w-2xl h-[min(36rem,85dvh)] max-h-[calc(100dvh-2rem)] -translate-x-1/2 -translate-y-1/2 overflow-hidden bg-popover border border-border rounded-xl shadow-2xl flex flex-col"
>
<div className="shrink-0 flex items-center justify-between px-4 py-3 border-b border-border/50">
Expand Down
12 changes: 8 additions & 4 deletions packages/review-editor/components/FileCommentBanner.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React, { useLayoutEffect, useMemo, useRef, useState } from 'react';
import type { CodeAnnotation } from '@plannotator/ui/types';
import { sanitizeBlockHtml } from '@plannotator/ui/utils/sanitizeHtml';
import { useComposerKeys } from '@plannotator/ui/hooks/useComposerKeys';
import { CommentMeta } from './CommentMeta';
import { CommentActions } from './CommentActions';
import { FileNameChip } from './FileNameChip';
Expand Down Expand Up @@ -71,6 +72,12 @@ export const FileCommentCard: React.FC<{
setIsEditing(false);
};

const handleEditKeyDown = useComposerKeys({
onSubmit: saveEdit,
onCancel: (e) => { e.preventDefault(); setIsEditing(false); },
canSubmit: draft.trim().length > 0,
});

return (
<div
className={`review-comment group${isSelected ? ' is-selected' : ''}`}
Expand Down Expand Up @@ -108,10 +115,7 @@ export const FileCommentCard: React.FC<{
autoFocus
value={draft}
onChange={(e) => setDraft(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Escape') { e.preventDefault(); setIsEditing(false); }
else if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) { e.preventDefault(); saveEdit(); }
}}
onKeyDown={handleEditKeyDown}
className="w-full min-h-[80px] resize-y rounded border border-border bg-background p-2 text-xs leading-relaxed focus:outline-none focus:ring-1 focus:ring-primary/40"
placeholder="File comment (markdown supported)…"
/>
Expand Down
33 changes: 17 additions & 16 deletions packages/ui/components/AnnotationPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { AnnotationType, type Annotation, type Block, type CodeAnnotation, type
import { isCurrentUser } from '../utils/identity';
import { ImageThumbnail } from './ImageThumbnail';
import { EditorAnnotationCard } from './EditorAnnotationCard';
import { useComposerKeys } from '../hooks/useComposerKeys';
import { useIsMobile } from '../hooks/useIsMobile';
import { OverlayScrollArea } from './OverlayScrollArea';
import { Button } from './ui/button';
Expand Down Expand Up @@ -468,15 +469,14 @@ const AnnotationCard: React.FC<{
setIsEditing(false);
};

const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (e.key === 'Enter' && (e.metaKey || e.ctrlKey) && !e.nativeEvent.isComposing) {
e.preventDefault();
handleSaveEdit();
} else if (e.key === 'Escape') {
const handleKeyDown = useComposerKeys({
onSubmit: handleSaveEdit,
onCancel: (e) => {
e.preventDefault();
handleCancelEdit();
}
};
},
canSubmit: editText.trim().length > 0,
});

const typeColor = TYPE_COLOR[annotation.type] ?? 'text-muted-foreground';
const typeLabel = TYPE_LABEL[annotation.type] ?? 'Note';
Expand Down Expand Up @@ -656,6 +656,15 @@ const CodeAnnotationCard: React.FC<{
setEditText(annotation.text || '');
};

const handleEditKeyDown = useComposerKeys({
onSubmit: handleSaveEdit,
onCancel: (e) => {
e.preventDefault();
handleCancelEdit();
},
canSubmit: editText.trim().length > 0,
});

return (
<div
data-annotation-id={annotation.id}
Expand Down Expand Up @@ -720,15 +729,7 @@ const CodeAnnotationCard: React.FC<{
ref={textareaRef}
value={editText}
onChange={(e) => setEditText(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter' && (e.metaKey || e.ctrlKey) && !e.nativeEvent.isComposing) {
e.preventDefault();
handleSaveEdit();
} else if (e.key === 'Escape') {
e.preventDefault();
handleCancelEdit();
}
}}
onKeyDown={handleEditKeyDown}
placeholder="Add your comment..."
aria-label="Annotation comment"
className="w-full resize-none rounded-lg border border-border/50 bg-card px-2.5 py-2 text-base leading-relaxed text-foreground outline-none transition-colors placeholder:text-muted-foreground/50 focus:border-primary/40 focus:ring-1 focus:ring-primary/20"
Expand Down
22 changes: 12 additions & 10 deletions packages/ui/components/CodeFilePopout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { PopoutDialog } from './PopoutDialog';
import { useTheme } from './ThemeProvider';
import { CommentPopover } from './CommentPopover';
import { ImageThumbnail } from './ImageThumbnail';
import { useComposerKeys } from '../hooks/useComposerKeys';
import type { CodeAnnotation, ImageAttachment } from '../types';

export interface CodeFileAnnotationInput {
Expand Down Expand Up @@ -161,6 +162,16 @@ const CodeInlineAnnotation: React.FC<{
setIsEditing(false);
};

const handleEditKeyDown = useComposerKeys({
onSubmit: save,
onCancel: (e) => {
e.preventDefault();
setIsEditing(false);
setEditText(annotation.text ?? '');
},
canSubmit: editText.trim().length > 0,
});

return (
<div
data-code-annotation-id={annotation.id}
Expand Down Expand Up @@ -221,16 +232,7 @@ const CodeInlineAnnotation: React.FC<{
value={editText}
onChange={(e) => setEditText(e.target.value)}
onClick={(e) => e.stopPropagation()}
onKeyDown={(e) => {
if (e.key === 'Escape') {
e.preventDefault();
setIsEditing(false);
setEditText(annotation.text ?? '');
} else if (e.key === 'Enter' && (e.metaKey || e.ctrlKey) && !e.nativeEvent.isComposing) {
e.preventDefault();
save();
}
}}
onKeyDown={handleEditKeyDown}
rows={Math.min(editText.split('\n').length + 1, 8)}
className="w-full resize-none rounded border border-border bg-background px-2 py-1.5 text-xs text-foreground focus:outline-none focus:ring-2 focus:ring-primary/40"
/>
Expand Down
34 changes: 19 additions & 15 deletions packages/ui/components/CommentPopover.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React, { useState, useEffect, useRef, useCallback } from 'react';
import { createPortal } from 'react-dom';
import type { ImageAttachment } from '../types';
import { AttachmentsButton } from './AttachmentsButton';
import { submitHint } from '../utils/platform';
import { useComposerAskAIHint, useComposerKeys, useComposerSubmitHint } from '../hooks/useComposerKeys';
import { useDraggable } from '../hooks/useDraggable';
import { SparklesIcon } from './SparklesIcon';
import { hasUnsavedCommentContent } from '../utils/commentContent';
Expand Down Expand Up @@ -235,21 +235,14 @@ export const CommentPopover: React.FC<CommentPopoverProps> = ({
onClose();
}, [allowImages, askAIContext, contextText, draftKey, isGlobal, onAskAI, onClose, onDraftChange, text]);

const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (e.key === 'Escape') {
e.stopPropagation();
if (mode === 'dialog') {
setMode('popover');
} else {
onClose();
}
return;
}
if (e.key === 'Enter' && (e.metaKey || e.ctrlKey) && !e.nativeEvent.isComposing) {
e.preventDefault();
handleSubmit();
const handleCancelKey = useCallback((e: React.KeyboardEvent<HTMLElement>) => {
e.stopPropagation();
if (mode === 'dialog') {
setMode('popover');
} else {
onClose();
}
};
}, [mode, onClose]);

const headerLabel = isGlobal
? 'Global Comment'
Expand All @@ -262,6 +255,15 @@ export const CommentPopover: React.FC<CommentPopoverProps> = ({
(allowEmptySubmit && initialText.trim().length > 0);
const canAskAI = !!onAskAI && !askAIDisabled && text.trim().length > 0;

const handleKeyDown = useComposerKeys({
onSubmit: handleSubmit,
onAskAI: canAskAI ? handleAskAI : undefined,
onCancel: handleCancelKey,
canSubmit,
});
const submitHint = useComposerSubmitHint();
const askAIHint = useComposerAskAIHint();

if (mode === 'dialog') {
return createPortal(
<div data-comment-popover="true" className="fixed inset-0 z-[100] flex items-center justify-center p-4">
Expand Down Expand Up @@ -340,6 +342,7 @@ export const CommentPopover: React.FC<CommentPopoverProps> = ({
>
<SparklesIcon className="w-3 h-3" />
Ask AI
{askAIHint && <span className="text-[10px] font-normal opacity-60">{askAIHint}</span>}
</button>
)}
</div>
Expand Down Expand Up @@ -462,6 +465,7 @@ export const CommentPopover: React.FC<CommentPopoverProps> = ({
>
<SparklesIcon className="w-3 h-3" />
Ask AI
{askAIHint && <span className="text-[10px] font-normal opacity-60">{askAIHint}</span>}
</button>
)}
</div>
Expand Down
Loading