diff --git a/bun.lock b/bun.lock index f297be44..858d55ef 100644 --- a/bun.lock +++ b/bun.lock @@ -1,6 +1,5 @@ { "lockfileVersion": 1, - "configVersion": 0, "workspaces": { "": { "name": "plannotator", diff --git a/packages/ui/components/AnnotationToolbar.tsx b/packages/ui/components/AnnotationToolbar.tsx index 9fb2f1dc..f6d10616 100644 --- a/packages/ui/components/AnnotationToolbar.tsx +++ b/packages/ui/components/AnnotationToolbar.tsx @@ -1,8 +1,9 @@ -import React, { useState, useEffect, useRef, useMemo } from "react"; +import React, { useState, useEffect, useRef, useMemo, useCallback } from "react"; import { AnnotationType } from "../types"; import { createPortal } from "react-dom"; import { useDismissOnOutsideAndEscape } from "../hooks/useDismissOnOutsideAndEscape"; -import { type QuickLabel, getQuickLabels, getLabelColors } from "../utils/quickLabels"; +import { type QuickLabel, getQuickLabels } from "../utils/quickLabels"; +import { FloatingQuickLabelPicker } from "./FloatingQuickLabelPicker"; type PositionMode = 'center-above' | 'top-right'; @@ -50,6 +51,7 @@ export const AnnotationToolbar: React.FC = ({ const [copied, setCopied] = useState(false); const [showQuickLabels, setShowQuickLabels] = useState(false); const toolbarRef = useRef(null); + const zapButtonRef = useRef(null); const quickLabels = useMemo(() => getQuickLabels(), []); const handleCopy = async () => { @@ -101,21 +103,26 @@ export const AnnotationToolbar: React.FC = ({ }; }, [element, positionMode, closeOnScrollOut, onClose]); - // Type-to-comment + Alt+N quick label shortcuts + // Type-to-comment + Alt+N / bare digit quick label shortcuts useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if (e.isComposing) return; if (isEditableElement(e.target) || isEditableElement(document.activeElement)) return; + + // When picker is open, let FloatingQuickLabelPicker own all keyboard input + if (showQuickLabels) return; + if (e.key === "Escape") { - setShowQuickLabels(false); onClose(); return; } - // Alt+1..8: apply quick label - if (e.altKey && e.code >= 'Digit1' && e.code <= 'Digit8') { + // Alt+N applies quick label (picker closed) + const isDigit = (e.code >= 'Digit1' && e.code <= 'Digit9') || e.code === 'Digit0'; + if (isDigit && !e.ctrlKey && !e.metaKey && e.altKey) { e.preventDefault(); - const index = parseInt(e.code.slice(5), 10) - 1; + const digit = parseInt(e.code.slice(5), 10); + const index = digit === 0 ? 9 : digit - 1; if (index < quickLabels.length) { onQuickLabel?.(quickLabels[index]); } @@ -131,10 +138,10 @@ export const AnnotationToolbar: React.FC = ({ window.addEventListener("keydown", handleKeyDown); return () => window.removeEventListener("keydown", handleKeyDown); - }, [onClose, onRequestComment, onQuickLabel, quickLabels]); + }, [onClose, onRequestComment, onQuickLabel, quickLabels, showQuickLabels]); useDismissOnOutsideAndEscape({ - enabled: true, + enabled: !showQuickLabels, ref: toolbarRef, onDismiss: onClose, }); @@ -202,23 +209,25 @@ export const AnnotationToolbar: React.FC = ({ className="text-accent hover:bg-accent/10" /> {onQuickLabel && ( -
+ <> setShowQuickLabels(prev => !prev)} icon={} label="Quick label" className={showQuickLabels ? "text-amber-500 bg-amber-500/10" : "text-amber-500 hover:bg-amber-500/10"} /> - {showQuickLabels && ( - { setShowQuickLabels(false); onQuickLabel(label); }} + onDismiss={() => setShowQuickLabels(false)} /> )} -
+ )}
= ({ ); }; -// Quick Label Dropdown -const QuickLabelDropdown: React.FC<{ - labels: QuickLabel[]; - onSelect: (label: QuickLabel) => void; -}> = ({ labels, onSelect }) => { - const isMac = navigator.platform?.includes('Mac'); - const altKey = isMac ? '⌥' : 'Alt+'; - - return ( -
e.stopPropagation()} - > -
Quick Labels
-
- {labels.map((label, index) => { - const colors = getLabelColors(label.color); - return ( - - ); - })} -
-
- ); -}; - // Icons const CopyIcon = () => ( @@ -309,17 +279,18 @@ const CloseIcon = () => ( ); -const ToolbarButton: React.FC<{ +const ToolbarButton = React.forwardRef void; icon: React.ReactNode; label: string; className: string; -}> = ({ onClick, icon, label, className }) => ( +}>(({ onClick, icon, label, className }, ref) => ( -); +)); diff --git a/packages/ui/components/AnnotationToolstrip.tsx b/packages/ui/components/AnnotationToolstrip.tsx index 41dc08de..e94018be 100644 --- a/packages/ui/components/AnnotationToolstrip.tsx +++ b/packages/ui/components/AnnotationToolstrip.tsx @@ -104,6 +104,18 @@ export const AnnotationToolstrip: React.FC = ({ } /> + onModeChange('quickLabel')} + label="Label" + color="warning" + mounted={mounted} + icon={ + + + + } + />
{/* Help */} @@ -203,6 +215,11 @@ const colorStyles = { hover: 'text-destructive/80 bg-destructive/8', inactive: 'text-muted-foreground hover:text-foreground', }, + warning: { + active: 'bg-background text-foreground shadow-sm', + hover: 'text-amber-500/80 bg-amber-500/8', + inactive: 'text-muted-foreground hover:text-foreground', + }, } as const; type ButtonColor = keyof typeof colorStyles; diff --git a/packages/ui/components/FloatingQuickLabelPicker.tsx b/packages/ui/components/FloatingQuickLabelPicker.tsx new file mode 100644 index 00000000..f64f1e1c --- /dev/null +++ b/packages/ui/components/FloatingQuickLabelPicker.tsx @@ -0,0 +1,143 @@ +import React, { useState, useEffect, useRef, useMemo } from 'react'; +import { createPortal } from 'react-dom'; +import { type QuickLabel, getQuickLabels } from '../utils/quickLabels'; +import { QuickLabelDropdown } from './QuickLabelDropdown'; + +interface FloatingQuickLabelPickerProps { + anchorEl: HTMLElement; + /** Mouse coordinates at the moment of selection — picker appears here */ + cursorHint?: { x: number; y: number }; + onSelect: (label: QuickLabel) => void; + onDismiss: () => void; +} + +const PICKER_WIDTH = 192; +const GAP = 6; +const VIEWPORT_PADDING = 12; + +function computePosition( + anchorEl: HTMLElement, + cursorHint?: { x: number; y: number }, +): { top: number; left: number; flipAbove: boolean } { + const rect = anchorEl.getBoundingClientRect(); + + // Vertical: use anchor rect for above/below decision + placement + const spaceBelow = window.innerHeight - rect.bottom; + const flipAbove = spaceBelow < 220; + const top = flipAbove ? rect.top - GAP : rect.bottom + GAP; + + // Horizontal: prefer cursor x, fallback to anchor right edge + let left: number; + if (cursorHint) { + // Anchor left edge of picker at cursor x, nudge left slightly so + // the first row's text is directly under the pointer + left = cursorHint.x - 28; + } else { + // Fallback: right edge of anchor (where selection likely ended) + left = rect.right - PICKER_WIDTH / 2; + } + + // Clamp to viewport + left = Math.max(VIEWPORT_PADDING, Math.min(left, window.innerWidth - PICKER_WIDTH - VIEWPORT_PADDING)); + + return { top, left, flipAbove }; +} + +export const FloatingQuickLabelPicker: React.FC = ({ + anchorEl, + cursorHint, + onSelect, + onDismiss, +}) => { + const [position, setPosition] = useState<{ top: number; left: number; flipAbove: boolean } | null>(null); + const ref = useRef(null); + const quickLabels = useMemo(() => getQuickLabels(), []); + + // Position tracking + useEffect(() => { + const update = () => setPosition(computePosition(anchorEl, cursorHint)); + update(); + window.addEventListener('scroll', update, true); + window.addEventListener('resize', update); + return () => { + window.removeEventListener('scroll', update, true); + window.removeEventListener('resize', update); + }; + }, [anchorEl, cursorHint]); + + // Keyboard: 1-9/0 or Alt+1-9/0 to apply label, Escape to dismiss + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === 'Escape') { + e.preventDefault(); + onDismiss(); + return; + } + // Accept bare digit or Alt+digit — picker is open so digits mean labels + const isDigit = (e.code >= 'Digit1' && e.code <= 'Digit9') || e.code === 'Digit0'; + if (isDigit && !e.ctrlKey && !e.metaKey) { + e.preventDefault(); + const digit = parseInt(e.code.slice(5), 10); + const index = digit === 0 ? 9 : digit - 1; + if (index < quickLabels.length) { + onSelect(quickLabels[index]); + } + } + }; + window.addEventListener('keydown', handleKeyDown); + return () => window.removeEventListener('keydown', handleKeyDown); + }, [onDismiss, onSelect, quickLabels]); + + // Click outside to dismiss + useEffect(() => { + const handlePointerDown = (e: PointerEvent) => { + if (ref.current && !ref.current.contains(e.target as Node)) { + onDismiss(); + } + }; + // Defer to avoid catching the triggering click + const timer = setTimeout(() => { + document.addEventListener('pointerdown', handlePointerDown, true); + }, 0); + return () => { + clearTimeout(timer); + document.removeEventListener('pointerdown', handlePointerDown, true); + }; + }, [onDismiss]); + + if (!position) return null; + + const animName = position.flipAbove ? 'qlp-in-above' : 'qlp-in-below'; + + return createPortal( +
e.stopPropagation()} + > + + +
+ +
+
, + document.body + ); +}; diff --git a/packages/ui/components/KeyboardShortcuts.tsx b/packages/ui/components/KeyboardShortcuts.tsx index 713ed278..29acf831 100644 --- a/packages/ui/components/KeyboardShortcuts.tsx +++ b/packages/ui/components/KeyboardShortcuts.tsx @@ -94,7 +94,7 @@ const planShortcuts: ShortcutSection[] = [ title: 'Annotations', shortcuts: [ { keys: ['a-z'], desc: 'Start typing comment', hint: 'When the annotation toolbar is open, any letter key opens the comment editor with that character' }, - { keys: [alt, '1-8'], desc: 'Apply quick label', hint: 'When the toolbar is open, instantly applies the Nth preset label as an annotation' }, + { keys: [alt, '1-0'], desc: 'Apply quick label', hint: 'Instantly applies the Nth preset label (0 = 10th). When the label picker is open, bare digits also work.' }, { keys: [mod, enter], desc: 'Submit comment' }, { keys: [mod, 'C'], desc: 'Copy selected text' }, { keys: ['Esc'], desc: 'Close toolbar / Cancel' }, diff --git a/packages/ui/components/QuickLabelDropdown.tsx b/packages/ui/components/QuickLabelDropdown.tsx new file mode 100644 index 00000000..6dcc3539 --- /dev/null +++ b/packages/ui/components/QuickLabelDropdown.tsx @@ -0,0 +1,63 @@ +import React from 'react'; +import { type QuickLabel, getLabelColors } from '../utils/quickLabels'; + +/** + * Shared vertical label list used by both FloatingQuickLabelPicker + * and the AnnotationToolbar's inline dropdown. + * + * Context-menu style: single column, colored accent bars, full-width rows. + */ +export const QuickLabelDropdown: React.FC<{ + labels: QuickLabel[]; + onSelect: (label: QuickLabel) => void; + /** Enable staggered row entrance animation */ + animate?: boolean; +}> = ({ labels, onSelect, animate = false }) => { + return ( +
e.stopPropagation()}> + {animate && ( + + )} + {labels.map((label, index) => { + const colors = getLabelColors(label.color); + return ( + + ); + })} +
+ ); +}; diff --git a/packages/ui/components/Settings.tsx b/packages/ui/components/Settings.tsx index 0d713b07..ce16f65a 100644 --- a/packages/ui/components/Settings.tsx +++ b/packages/ui/components/Settings.tsx @@ -83,6 +83,8 @@ export const Settings: React.FC = ({ taterMode, onTaterModeChange const [autoCloseDelay, setAutoCloseDelayState] = useState('off'); const [defaultNotesApp, setDefaultNotesApp] = useState('ask'); const [quickLabelsState, setQuickLabelsState] = useState([]); + const [editingTipIndex, setEditingTipIndex] = useState(null); + const [editingTipValue, setEditingTipValue] = useState(''); // Fetch available agents for OpenCode const { agents: availableAgents, validateAgent, getAgentWarning } = useAgents(origin ?? null); @@ -776,57 +778,138 @@ export const Settings: React.FC = ({ taterMode, onTaterModeChange +
{quickLabelsState.map((label, index) => { const colors = getLabelColors(label.color); + const hasTip = !!label.tip; + const isEditingTip = editingTipIndex === index; return ( -
- {label.emoji} - { - const updated = [...quickLabelsState]; - updated[index] = { - ...label, - text: e.target.value, - id: e.target.value.toLowerCase().replace(/\s+/g, '-').replace(/[^a-z0-9-]/g, ''), - }; - setQuickLabelsState(updated); - saveQuickLabels(updated); - }} - className="flex-1 px-2 py-1 bg-background/80 rounded text-xs focus:outline-none focus:ring-1 focus:ring-primary/50" - /> - - - {index < 8 ? `${navigator.platform?.includes('Mac') ? '⌥' : 'Alt+'}${index + 1}` : ''} - - +
+ {/* Main row */} +
+ {label.emoji} + { + const updated = [...quickLabelsState]; + updated[index] = { + ...label, + text: e.target.value, + id: e.target.value.toLowerCase().replace(/\s+/g, '-').replace(/[^a-z0-9-]/g, ''), + }; + setQuickLabelsState(updated); + saveQuickLabels(updated); + }} + className="flex-1 px-2 py-1 bg-background/80 rounded text-xs focus:outline-none focus:ring-1 focus:ring-primary/50" + /> + {/* Tip indicator button */} + + + + {index < 10 ? `${navigator.platform?.includes('Mac') ? '⌥' : 'Alt+'}${index === 9 ? '0' : index + 1}` : ''} + + +
+ {/* Tip editor — slides open below the row */} + {isEditingTip && ( +
+ + + + setEditingTipValue(e.target.value)} + onKeyDown={(e) => { + if (e.key === 'Enter') { + const updated = [...quickLabelsState]; + updated[index] = { ...label, tip: editingTipValue || undefined }; + setQuickLabelsState(updated); + saveQuickLabels(updated); + setEditingTipIndex(null); + } + if (e.key === 'Escape') setEditingTipIndex(null); + }} + placeholder="AI instruction tip..." + className="flex-1 px-2 py-1 bg-background/60 rounded text-[10px] text-muted-foreground placeholder:text-muted-foreground/30 focus:outline-none focus:ring-1 focus:ring-primary/50" + autoFocus + onFocus={(e) => { e.target.setSelectionRange(0, 0); e.target.scrollLeft = 0; }} + /> + +
+ )}
); })} @@ -852,7 +935,7 @@ export const Settings: React.FC = ({ taterMode, onTaterModeChange )}
- Use {navigator.platform?.includes('Mac') ? '⌥' : 'Alt+'}1 through {navigator.platform?.includes('Mac') ? '⌥' : 'Alt+'}8 when the annotation toolbar is visible to apply a label instantly. + Use {navigator.platform?.includes('Mac') ? '⌥' : 'Alt+'}1 through {navigator.platform?.includes('Mac') ? '⌥' : 'Alt+'}0 when the annotation toolbar is visible to apply a label instantly.
)} diff --git a/packages/ui/components/Viewer.tsx b/packages/ui/components/Viewer.tsx index 116302f1..39e35a92 100644 --- a/packages/ui/components/Viewer.tsx +++ b/packages/ui/components/Viewer.tsx @@ -6,6 +6,7 @@ import 'highlight.js/styles/github-dark.css'; import { Block, Annotation, AnnotationType, EditorMode, type InputMethod, type ImageAttachment } from '../types'; import { Frontmatter } from '../utils/parser'; import { AnnotationToolbar } from './AnnotationToolbar'; +import { FloatingQuickLabelPicker } from './FloatingQuickLabelPicker'; // Debug error boundary to catch silent toolbar crashes class ToolbarErrorBoundary extends React.Component< @@ -166,6 +167,13 @@ export const Viewer = forwardRef(({ source?: any; codeBlock?: { block: Block; element: HTMLElement }; } | null>(null); + const [quickLabelPicker, setQuickLabelPicker] = useState<{ + anchorEl: HTMLElement; + cursorHint?: { x: number; y: number }; + source?: any; + codeBlock?: { block: Block; element: HTMLElement }; + } | null>(null); + const lastMousePosRef = useRef<{ x: number; y: number }>({ x: 0, y: 0 }); const hoverTimeoutRef = useRef(null); const stickySentinelRef = useRef(null); const [isStuck, setIsStuck] = useState(false); @@ -177,6 +185,11 @@ export const Viewer = forwardRef(({ // In pinpoint mode, apply code block annotation based on current editor mode if (modeRef.current === 'redline') { applyCodeBlockAnnotation(blockId, codeEl, AnnotationType.DELETION); + } else if (modeRef.current === 'quickLabel') { + setQuickLabelPicker({ + anchorEl: element, + codeBlock: { block: blocks.find(b => b.id === blockId)!, element }, + }); } else { // Show comment popover anchored to the code block setCommentPopover({ @@ -192,7 +205,7 @@ export const Viewer = forwardRef(({ containerRef, highlighterRef, inputMethod, - enabled: !toolbarState && !commentPopover && !(isPlanDiffActive ?? false), + enabled: !toolbarState && !commentPopover && !quickLabelPicker && !(isPlanDiffActive ?? false), onCodeBlockClick: handlePinpointCodeBlockClick, }); @@ -251,6 +264,7 @@ export const Viewer = forwardRef(({ text?: string, images?: ImageAttachment[], isQuickLabel?: boolean, + quickLabelTip?: string, ) => { const doms = highlighter.getDoms(source.id); let blockId = ''; @@ -284,6 +298,7 @@ export const Viewer = forwardRef(({ endMeta: source.endMeta, images, ...(isQuickLabel ? { isQuickLabel: true } : {}), + ...(quickLabelTip ? { quickLabelTip } : {}), }; if (type === AnnotationType.DELETION) { @@ -530,6 +545,13 @@ export const Viewer = forwardRef(({ } }), [findTextInDOM, onSelectAnnotation]); + // Track last mouse position for cursor-anchored quick label picker + useEffect(() => { + const track = (e: MouseEvent) => { lastMousePosRef.current = { x: e.clientX, y: e.clientY }; }; + document.addEventListener('mouseup', track, true); + return () => document.removeEventListener('mouseup', track, true); + }, []); + useEffect(() => { if (!containerRef.current) return; @@ -553,6 +575,7 @@ export const Viewer = forwardRef(({ pendingSourceRef.current = null; } setCommentPopover(null); + setQuickLabelPicker(null); if (modeRef.current === 'redline') { // Auto-delete in redline mode @@ -567,6 +590,14 @@ export const Viewer = forwardRef(({ isGlobal: false, source, }); + } else if (modeRef.current === 'quickLabel') { + // Quick Label mode - show floating label picker directly + pendingSourceRef.current = source; + setQuickLabelPicker({ + anchorEl: doms[0] as HTMLElement, + cursorHint: lastMousePosRef.current, + source, + }); } else { // Selection mode - show toolbar menu const selectionText = source.text; @@ -697,13 +728,45 @@ export const Viewer = forwardRef(({ createAnnotationFromSource( highlighter, toolbarState.source, AnnotationType.COMMENT, - `${label.emoji} ${label.text}`, undefined, true + `${label.emoji} ${label.text}`, undefined, true, label.tip ); pendingSourceRef.current = null; setToolbarState(null); window.getSelection()?.removeAllRanges(); }; + const handleFloatingQuickLabel = useCallback((label: QuickLabel) => { + if (!quickLabelPicker) return; + + if (quickLabelPicker.source && highlighterRef.current) { + createAnnotationFromSource( + highlighterRef.current, quickLabelPicker.source, AnnotationType.COMMENT, + `${label.emoji} ${label.text}`, undefined, true, label.tip + ); + pendingSourceRef.current = null; + } else if (quickLabelPicker.codeBlock) { + const codeEl = quickLabelPicker.codeBlock.element.querySelector('code'); + if (codeEl) { + applyCodeBlockAnnotation( + quickLabelPicker.codeBlock.block.id, codeEl, AnnotationType.COMMENT, + `${label.emoji} ${label.text}`, undefined, true, label.tip + ); + } + } + + setQuickLabelPicker(null); + window.getSelection()?.removeAllRanges(); + }, [quickLabelPicker]); + + const handleQuickLabelPickerDismiss = useCallback(() => { + if (quickLabelPicker?.source && highlighterRef.current) { + highlighterRef.current.remove(quickLabelPicker.source.id); + pendingSourceRef.current = null; + } + setQuickLabelPicker(null); + window.getSelection()?.removeAllRanges(); + }, [quickLabelPicker]); + const handleToolbarClose = () => { if (toolbarState && highlighterRef.current) { highlighterRef.current.remove(toolbarState.source.id); @@ -720,6 +783,7 @@ export const Viewer = forwardRef(({ text?: string, images?: ImageAttachment[], isQuickLabel?: boolean, + quickLabelTip?: string, ) => { const id = `codeblock-${Date.now()}`; const codeText = codeEl.textContent || ''; @@ -744,6 +808,7 @@ export const Viewer = forwardRef(({ author: getIdentity(), images, ...(isQuickLabel ? { isQuickLabel: true } : {}), + ...(quickLabelTip ? { quickLabelTip } : {}), }; justCreatedIdRef.current = newAnnotation.id; @@ -765,7 +830,7 @@ export const Viewer = forwardRef(({ if (!codeEl) return; applyCodeBlockAnnotation( hoveredCodeBlock.block.id, codeEl, AnnotationType.COMMENT, - `${label.emoji} ${label.text}`, undefined, true + `${label.emoji} ${label.text}`, undefined, true, label.tip ); setHoveredCodeBlock(null); }; @@ -1082,6 +1147,16 @@ export const Viewer = forwardRef(({ onClose={handleCommentClose} /> )} + + {/* Quick Label floating picker (quickLabel mode) */} + {quickLabelPicker && ( + + )} {/* Image lightbox */} diff --git a/packages/ui/hooks/useInputMethodSwitch.ts b/packages/ui/hooks/useInputMethodSwitch.ts index 6721fe64..9b7b891a 100644 --- a/packages/ui/hooks/useInputMethodSwitch.ts +++ b/packages/ui/hooks/useInputMethodSwitch.ts @@ -36,6 +36,8 @@ export function useInputMethodSwitch( // Don't interfere when user is typing const tag = (e.target as HTMLElement)?.tagName; if (tag === 'INPUT' || tag === 'TEXTAREA' || (e.target as HTMLElement)?.isContentEditable) return; + // Don't interfere when a quick label picker is open (it uses Alt+N shortcuts) + if (document.querySelector('[data-quick-label-picker]')) return; if (stateRef.current === 'idle') { // First press — switch immediately diff --git a/packages/ui/types.ts b/packages/ui/types.ts index 4b71fde4..e3f7cd75 100644 --- a/packages/ui/types.ts +++ b/packages/ui/types.ts @@ -6,7 +6,7 @@ export enum AnnotationType { GLOBAL_COMMENT = 'GLOBAL_COMMENT', } -export type EditorMode = 'selection' | 'comment' | 'redline'; +export type EditorMode = 'selection' | 'comment' | 'redline' | 'quickLabel'; export type InputMethod = 'drag' | 'pinpoint'; @@ -27,6 +27,7 @@ export interface Annotation { author?: string; // Tater identity for collaborative sharing images?: ImageAttachment[]; // Attached images with human-readable names isQuickLabel?: boolean; // true if created via quick label chip + quickLabelTip?: string; // optional instruction tip from the label definition // web-highlighter metadata for cross-element selections startMeta?: { parentTagName: string; diff --git a/packages/ui/utils/editorMode.ts b/packages/ui/utils/editorMode.ts index d5baac0d..40b95f60 100644 --- a/packages/ui/utils/editorMode.ts +++ b/packages/ui/utils/editorMode.ts @@ -17,7 +17,7 @@ const DEFAULT_MODE: EditorMode = 'selection'; */ export function getEditorMode(): EditorMode { const stored = storage.getItem(STORAGE_KEY); - if (stored === 'selection' || stored === 'comment' || stored === 'redline') { + if (stored === 'selection' || stored === 'comment' || stored === 'redline' || stored === 'quickLabel') { return stored; } return DEFAULT_MODE; diff --git a/packages/ui/utils/parser.ts b/packages/ui/utils/parser.ts index 14fe91ff..80c6cb7a 100644 --- a/packages/ui/utils/parser.ts +++ b/packages/ui/utils/parser.ts @@ -298,6 +298,9 @@ export const exportAnnotations = (blocks: Block[], annotations: any[], globalAtt case 'COMMENT': if (ann.isQuickLabel) { output += `[${ann.text}] Feedback on: "${ann.originalText}"\n`; + if (ann.quickLabelTip) { + output += `> ${ann.quickLabelTip}\n`; + } } else { output += `Feedback on: "${ann.originalText}"\n`; output += `> ${ann.text}\n`; diff --git a/packages/ui/utils/quickLabels.ts b/packages/ui/utils/quickLabels.ts index b231d6ee..44f78a60 100644 --- a/packages/ui/utils/quickLabels.ts +++ b/packages/ui/utils/quickLabels.ts @@ -14,6 +14,7 @@ export interface QuickLabel { emoji: string; // single emoji e.g. "🧪" text: string; // display text e.g. "Needs tests" color: string; // key into LABEL_COLOR_MAP + tip?: string; // optional instruction injected into feedback for the agent } /** Inline styles for label colors (avoids Tailwind dynamic class purging) */ @@ -26,17 +27,21 @@ export const LABEL_COLOR_MAP: Record