From f41894add3de34a1d68fd3305bc7f7e1ef089af5 Mon Sep 17 00:00:00 2001 From: Michael Ramos Date: Tue, 6 Jan 2026 18:26:11 -0800 Subject: [PATCH] Add "approve with notes" for OpenCode + Claude Code warning dialog MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Features: - OpenCode: annotations are sent on approval as implementation notes - Claude Code: confirmation dialog warns annotations won't be sent - Dialog includes links to upvote Claude Code issues #16001 #15755 Refactor: - New reusable ConfirmDialog component Closes #30 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- apps/opencode-plugin/index.ts | 15 ++++ packages/editor/App.tsx | 77 +++++++++++------ packages/server/index.ts | 11 ++- packages/ui/components/ConfirmDialog.tsx | 100 +++++++++++++++++++++++ 4 files changed, 174 insertions(+), 29 deletions(-) create mode 100644 packages/ui/components/ConfirmDialog.tsx diff --git a/apps/opencode-plugin/index.ts b/apps/opencode-plugin/index.ts index 68bb998de..91a881af0 100644 --- a/apps/opencode-plugin/index.ts +++ b/apps/opencode-plugin/index.ts @@ -96,6 +96,21 @@ Do NOT proceed with implementation until your plan is approved. // Silently fail if session is busy } + // If user approved with annotations, include them as notes for implementation + if (result.feedback) { + return `Plan approved with notes! + +Plan Summary: ${args.summary} + +## Implementation Notes + +The user approved your plan but added the following notes to consider during implementation: + +${result.feedback} + +Proceed with implementation, incorporating these notes where applicable.`; + } + return `Plan approved! Plan Summary: ${args.summary}`; diff --git a/packages/editor/App.tsx b/packages/editor/App.tsx index 3463b9fe7..829bc188b 100644 --- a/packages/editor/App.tsx +++ b/packages/editor/App.tsx @@ -3,6 +3,7 @@ import { parseMarkdownToBlocks, exportDiff } from '@plannotator/ui/utils/parser' import { Viewer, ViewerHandle } from '@plannotator/ui/components/Viewer'; import { AnnotationPanel } from '@plannotator/ui/components/AnnotationPanel'; import { ExportModal } from '@plannotator/ui/components/ExportModal'; +import { ConfirmDialog } from '@plannotator/ui/components/ConfirmDialog'; import { Annotation, Block, EditorMode } from '@plannotator/ui/types'; import { ThemeProvider } from '@plannotator/ui/components/ThemeProvider'; import { ModeToggle } from '@plannotator/ui/components/ModeToggle'; @@ -300,6 +301,7 @@ const App: React.FC = () => { const [blocks, setBlocks] = useState([]); const [showExport, setShowExport] = useState(false); const [showFeedbackPrompt, setShowFeedbackPrompt] = useState(false); + const [showClaudeCodeWarning, setShowClaudeCodeWarning] = useState(false); const [isPanelOpen, setIsPanelOpen] = useState(true); const [editorMode, setEditorMode] = useState('selection'); const [taterMode, setTaterMode] = useState(() => { @@ -446,7 +448,7 @@ const App: React.FC = () => { const bearSettings = getBearSettings(); // Build request body - include integrations if enabled - const body: { obsidian?: object; bear?: object } = {}; + const body: { obsidian?: object; bear?: object; feedback?: string } = {}; if (obsidianSettings.enabled && obsidianSettings.vaultPath) { body.obsidian = { @@ -460,6 +462,11 @@ const App: React.FC = () => { body.bear = { plan: markdown }; } + // Include annotations as feedback if any exist (for OpenCode "approve with notes") + if (annotations.length > 0 || globalAttachments.length > 0) { + body.feedback = diffOutput; + } + await fetch('/api/approve', { method: 'POST', headers: { 'Content-Type': 'application/json' }, @@ -575,7 +582,14 @@ const App: React.FC = () => {
-
- - - )} + setShowFeedbackPrompt(false)} + title="Add Annotations First" + message="To provide feedback, select text in the plan and add annotations. Claude will use your annotations to revise the plan." + variant="info" + /> + + {/* Claude Code annotation warning dialog */} + setShowClaudeCodeWarning(false)} + onConfirm={() => { + setShowClaudeCodeWarning(false); + handleApprove(); + }} + title="Annotations Won't Be Sent" + message={<>Claude Code doesn't yet support feedback on approval. Your {annotations.length} annotation{annotations.length !== 1 ? 's' : ''} will be lost.} + subMessage={ + <> + To send feedback, use Deny with Feedback instead. +

+ Want this feature? Upvote these issues: +
+ #16001 + {' · '} + #15755 + + } + confirmText="Approve Anyway" + cancelText="Cancel" + variant="warning" + showCancel + /> {/* Completion overlay - shown after approve/deny */} {submitted && ( diff --git a/packages/server/index.ts b/packages/server/index.ts index 15600a49d..c8c2948de 100644 --- a/packages/server/index.ts +++ b/packages/server/index.ts @@ -145,13 +145,20 @@ export async function startPlannotatorServer( // API: Approve plan if (url.pathname === "/api/approve" && req.method === "POST") { - // Check for note integrations + // Check for note integrations and optional feedback + let feedback: string | undefined; try { const body = (await req.json().catch(() => ({}))) as { obsidian?: ObsidianConfig; bear?: BearConfig; + feedback?: string; }; + // Capture feedback if provided (for "approve with notes") + if (body.feedback) { + feedback = body.feedback; + } + // Obsidian integration if (body.obsidian?.vaultPath && body.obsidian?.plan) { const result = await saveToObsidian(body.obsidian); @@ -176,7 +183,7 @@ export async function startPlannotatorServer( console.error(`[Integration] Error:`, err); } - resolveDecision({ approved: true }); + resolveDecision({ approved: true, feedback }); return Response.json({ ok: true }); } diff --git a/packages/ui/components/ConfirmDialog.tsx b/packages/ui/components/ConfirmDialog.tsx new file mode 100644 index 000000000..1d0868618 --- /dev/null +++ b/packages/ui/components/ConfirmDialog.tsx @@ -0,0 +1,100 @@ +/** + * Reusable confirmation dialog component + */ + +import React from 'react'; + +export interface ConfirmDialogProps { + isOpen: boolean; + onClose: () => void; + onConfirm?: () => void; + title: string; + message: React.ReactNode; + subMessage?: React.ReactNode; + confirmText?: string; + cancelText?: string; + variant?: 'info' | 'warning'; + showCancel?: boolean; +} + +export const ConfirmDialog: React.FC = ({ + isOpen, + onClose, + onConfirm, + title, + message, + subMessage, + confirmText = 'Got it', + cancelText = 'Cancel', + variant = 'info', + showCancel = false, +}) => { + if (!isOpen) return null; + + const iconColors = { + info: 'bg-accent/20 text-accent', + warning: 'bg-yellow-500/20 text-yellow-500', + }; + + const buttonColors = { + info: 'bg-primary text-primary-foreground hover:opacity-90', + warning: 'bg-yellow-600 text-white hover:bg-yellow-500', + }; + + const icons = { + info: ( + + + + ), + warning: ( + + + + ), + }; + + return ( +
+
+
+
+ {icons[variant]} +
+

{title}

+
+
+ {message} +
+ {subMessage && ( +
+ {subMessage} +
+ )} + {!subMessage &&
} +
+ {showCancel && ( + + )} + +
+
+
+ ); +};