diff --git a/apps/pi-extension/server/serverAnnotate.ts b/apps/pi-extension/server/serverAnnotate.ts index 6934afa5..e2c6e296 100644 --- a/apps/pi-extension/server/serverAnnotate.ts +++ b/apps/pi-extension/server/serverAnnotate.ts @@ -84,10 +84,11 @@ export async function startAnnotateServer(options: { }); } else if (url.pathname === "/api/config" && req.method === "POST") { try { - const body = (await parseBody(req)) as { displayName?: string; diffOptions?: Record }; + const body = (await parseBody(req)) as { displayName?: string; diffOptions?: Record; conventionalComments?: boolean }; const toSave: Record = {}; if (body.displayName !== undefined) toSave.displayName = body.displayName; if (body.diffOptions !== undefined) toSave.diffOptions = body.diffOptions; + if (body.conventionalComments !== undefined) toSave.conventionalComments = body.conventionalComments; if (Object.keys(toSave).length > 0) saveConfig(toSave as Parameters[0]); json(res, { ok: true }); } catch { diff --git a/apps/pi-extension/server/serverPlan.ts b/apps/pi-extension/server/serverPlan.ts index dbda9252..78448877 100644 --- a/apps/pi-extension/server/serverPlan.ts +++ b/apps/pi-extension/server/serverPlan.ts @@ -225,10 +225,11 @@ export async function startPlanReviewServer(options: { } } else if (url.pathname === "/api/config" && req.method === "POST") { try { - const body = (await parseBody(req)) as { displayName?: string; diffOptions?: Record }; + const body = (await parseBody(req)) as { displayName?: string; diffOptions?: Record; conventionalComments?: boolean }; const toSave: Record = {}; if (body.displayName !== undefined) toSave.displayName = body.displayName; if (body.diffOptions !== undefined) toSave.diffOptions = body.diffOptions; + if (body.conventionalComments !== undefined) toSave.conventionalComments = body.conventionalComments; if (Object.keys(toSave).length > 0) saveConfig(toSave as Parameters[0]); json(res, { ok: true }); } catch { diff --git a/apps/pi-extension/server/serverReview.ts b/apps/pi-extension/server/serverReview.ts index 2ea78d9d..c837642f 100644 --- a/apps/pi-extension/server/serverReview.ts +++ b/apps/pi-extension/server/serverReview.ts @@ -565,10 +565,11 @@ export async function startReviewServer(options: { json(res, result); } else if (url.pathname === "/api/config" && req.method === "POST") { try { - const body = (await parseBody(req)) as { displayName?: string; diffOptions?: Record }; + const body = (await parseBody(req)) as { displayName?: string; diffOptions?: Record; conventionalComments?: boolean }; const toSave: Record = {}; if (body.displayName !== undefined) toSave.displayName = body.displayName; if (body.diffOptions !== undefined) toSave.diffOptions = body.diffOptions; + if (body.conventionalComments !== undefined) toSave.conventionalComments = body.conventionalComments; if (Object.keys(toSave).length > 0) saveConfig(toSave as Parameters[0]); json(res, { ok: true }); } catch { diff --git a/packages/review-editor/App.tsx b/packages/review-editor/App.tsx index bf9cbb02..99b14e23 100644 --- a/packages/review-editor/App.tsx +++ b/packages/review-editor/App.tsx @@ -18,7 +18,7 @@ import { getAgentSwitchSettings, getEffectiveAgentName } from '@plannotator/ui/u import { getAIProviderSettings, saveAIProviderSettings, getPreferredModel } from '@plannotator/ui/utils/aiProvider'; import { AISetupDialog } from '@plannotator/ui/components/AISetupDialog'; import { needsAISetup } from '@plannotator/ui/utils/aiSetup'; -import { CodeAnnotation, CodeAnnotationType, SelectedLineRange, TokenAnnotationMeta } from '@plannotator/ui/types'; +import { CodeAnnotation, CodeAnnotationType, SelectedLineRange, TokenAnnotationMeta, ConventionalLabel, ConventionalDecoration } from '@plannotator/ui/types'; import { useResizablePanel } from '@plannotator/ui/hooks/useResizablePanel'; import { useCodeAnnotationDraft } from '@plannotator/ui/hooks/useCodeAnnotationDraft'; import { useGitAdd } from './hooks/useGitAdd'; @@ -36,7 +36,7 @@ import { ReviewHeaderMenu } from './components/ReviewHeaderMenu'; import { ReviewSidebar } from './components/ReviewSidebar'; import { FileTree } from './components/FileTree'; import { DEMO_DIFF } from './demoData'; -import { exportReviewFeedback } from './utils/exportFeedback'; +import { exportReviewFeedback, formatConventionalPrefix } from './utils/exportFeedback'; import { ReviewStateProvider, type ReviewState } from './dock/ReviewStateContext'; import { JobLogsProvider } from './dock/JobLogsContext'; import { reviewPanelComponents } from './dock/reviewPanelComponents'; @@ -688,6 +688,8 @@ const ReviewApp: React.FC = () => { text?: string, suggestedCode?: string, originalCode?: string, + conventionalLabel?: ConventionalLabel, + decorations?: ConventionalDecoration[], tokenMeta?: TokenAnnotationMeta ) => { if (!pendingSelection || !files[activeFileIndex]) return; @@ -714,6 +716,8 @@ const ReviewApp: React.FC = () => { }), createdAt: Date.now(), author: identity, + conventionalLabel, + decorations, }; setAnnotations(prev => [...prev, newAnnotation]); @@ -746,13 +750,18 @@ const ReviewApp: React.FC = () => { id: string, text?: string, suggestedCode?: string, - originalCode?: string + originalCode?: string, + conventionalLabel?: ConventionalLabel | null, + decorations?: ConventionalDecoration[], ) => { const ann = allAnnotationsRef.current.find(a => a.id === id); - const updates = { + const updates: Partial = { ...(text !== undefined && { text }), ...(suggestedCode !== undefined && { suggestedCode }), ...(originalCode !== undefined && { originalCode }), + // null clears the label; undefined means "not provided, keep existing" + ...(conventionalLabel !== undefined && { conventionalLabel: conventionalLabel ?? undefined }), + ...(decorations !== undefined && { decorations }), }; if (ann?.source && externalAnnotations.some(e => e.id === id)) { updateExternalAnnotation(id, updates); @@ -1122,7 +1131,8 @@ const ReviewApp: React.FC = () => { // Inline file comments const fileComments = fileAnnotations.map(ann => { - let commentBody = ann.text ?? ''; + const ccPrefix = formatConventionalPrefix(ann.conventionalLabel, ann.decorations); + let commentBody = ccPrefix + (ann.text ?? ''); if (ann.suggestedCode) { commentBody += `\n\n\`\`\`suggestion\n${ann.suggestedCode}\n\`\`\``; } diff --git a/packages/review-editor/components/AnnotationToolbar.tsx b/packages/review-editor/components/AnnotationToolbar.tsx index 012eb00f..3cfb6c8c 100644 --- a/packages/review-editor/components/AnnotationToolbar.tsx +++ b/packages/review-editor/components/AnnotationToolbar.tsx @@ -5,6 +5,8 @@ import { useTabIndent } from '../hooks/useTabIndent'; import { formatLineRange, formatTokenContext } from '../utils/formatLineRange'; import { AskAIInput } from './AskAIInput'; import { SparklesIcon } from './SparklesIcon'; +import { ConventionalLabelPicker, type LabelDef } from './ConventionalLabelPicker'; +import type { ConventionalLabel, ConventionalDecoration } from '@plannotator/ui/types'; import type { AIChatEntry } from '../hooks/useAIChat'; import { useDraggable } from '@plannotator/ui/hooks/useDraggable'; @@ -23,6 +25,13 @@ interface AnnotationToolbarProps { onSubmit: () => void; onDismiss: () => void; onCancel: () => void; + // Conventional Comments + conventionalCommentsEnabled: boolean; + conventionalLabel: ConventionalLabel | null; + onConventionalLabelChange: (label: ConventionalLabel | null) => void; + decorations: ConventionalDecoration[]; + onDecorationsChange: (decorations: ConventionalDecoration[]) => void; + enabledLabels?: LabelDef[]; // AI props aiAvailable?: boolean; onAskAI?: (question: string) => void; @@ -48,6 +57,12 @@ export const AnnotationToolbar: React.FC = ({ onSubmit, onDismiss, onCancel, + conventionalCommentsEnabled, + conventionalLabel, + onConventionalLabelChange, + decorations, + onDecorationsChange, + enabledLabels, aiAvailable = false, onAskAI, isAILoading = false, @@ -133,6 +148,16 @@ export const AnnotationToolbar: React.FC = ({ + {conventionalCommentsEnabled && ( + + )} +