Skip to content

Commit 052eaf8

Browse files
authored
Merge pull request #34 from backnotprop/feature/approve-with-feedback
Add approve with notes for OpenCode + Claude Code warning dialog
2 parents 373e1d8 + f41894a commit 052eaf8

4 files changed

Lines changed: 174 additions & 29 deletions

File tree

apps/opencode-plugin/index.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,21 @@ Do NOT proceed with implementation until your plan is approved.
9696
// Silently fail if session is busy
9797
}
9898

99+
// If user approved with annotations, include them as notes for implementation
100+
if (result.feedback) {
101+
return `Plan approved with notes!
102+
103+
Plan Summary: ${args.summary}
104+
105+
## Implementation Notes
106+
107+
The user approved your plan but added the following notes to consider during implementation:
108+
109+
${result.feedback}
110+
111+
Proceed with implementation, incorporating these notes where applicable.`;
112+
}
113+
99114
return `Plan approved!
100115
101116
Plan Summary: ${args.summary}`;

packages/editor/App.tsx

Lines changed: 50 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { parseMarkdownToBlocks, exportDiff } from '@plannotator/ui/utils/parser'
33
import { Viewer, ViewerHandle } from '@plannotator/ui/components/Viewer';
44
import { AnnotationPanel } from '@plannotator/ui/components/AnnotationPanel';
55
import { ExportModal } from '@plannotator/ui/components/ExportModal';
6+
import { ConfirmDialog } from '@plannotator/ui/components/ConfirmDialog';
67
import { Annotation, Block, EditorMode } from '@plannotator/ui/types';
78
import { ThemeProvider } from '@plannotator/ui/components/ThemeProvider';
89
import { ModeToggle } from '@plannotator/ui/components/ModeToggle';
@@ -300,6 +301,7 @@ const App: React.FC = () => {
300301
const [blocks, setBlocks] = useState<Block[]>([]);
301302
const [showExport, setShowExport] = useState(false);
302303
const [showFeedbackPrompt, setShowFeedbackPrompt] = useState(false);
304+
const [showClaudeCodeWarning, setShowClaudeCodeWarning] = useState(false);
303305
const [isPanelOpen, setIsPanelOpen] = useState(true);
304306
const [editorMode, setEditorMode] = useState<EditorMode>('selection');
305307
const [taterMode, setTaterMode] = useState(() => {
@@ -446,7 +448,7 @@ const App: React.FC = () => {
446448
const bearSettings = getBearSettings();
447449

448450
// Build request body - include integrations if enabled
449-
const body: { obsidian?: object; bear?: object } = {};
451+
const body: { obsidian?: object; bear?: object; feedback?: string } = {};
450452

451453
if (obsidianSettings.enabled && obsidianSettings.vaultPath) {
452454
body.obsidian = {
@@ -460,6 +462,11 @@ const App: React.FC = () => {
460462
body.bear = { plan: markdown };
461463
}
462464

465+
// Include annotations as feedback if any exist (for OpenCode "approve with notes")
466+
if (annotations.length > 0 || globalAttachments.length > 0) {
467+
body.feedback = diffOutput;
468+
}
469+
463470
await fetch('/api/approve', {
464471
method: 'POST',
465472
headers: { 'Content-Type': 'application/json' },
@@ -575,7 +582,14 @@ const App: React.FC = () => {
575582

576583
<div className="relative group/approve">
577584
<button
578-
onClick={handleApprove}
585+
onClick={() => {
586+
// Show warning for Claude Code users with annotations
587+
if (origin === 'claude-code' && annotations.length > 0) {
588+
setShowClaudeCodeWarning(true);
589+
} else {
590+
handleApprove();
591+
}
592+
}}
579593
disabled={isSubmitting}
580594
className={`px-2 py-1 md:px-2.5 rounded-md text-xs font-medium transition-all ${
581595
isSubmitting
@@ -681,31 +695,40 @@ const App: React.FC = () => {
681695
/>
682696

683697
{/* Feedback prompt dialog */}
684-
{showFeedbackPrompt && (
685-
<div className="fixed inset-0 z-50 flex items-center justify-center bg-background/80 backdrop-blur-sm p-4">
686-
<div className="bg-card border border-border rounded-xl w-full max-w-sm shadow-2xl p-6">
687-
<div className="flex items-center gap-3 mb-4">
688-
<div className="w-10 h-10 rounded-full bg-accent/20 flex items-center justify-center">
689-
<svg className="w-5 h-5 text-accent" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
690-
<path strokeLinecap="round" strokeLinejoin="round" d="M7 8h10M7 12h4m1 8l-4-4H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-3l-4 4z" />
691-
</svg>
692-
</div>
693-
<h3 className="font-semibold">Add Annotations First</h3>
694-
</div>
695-
<p className="text-sm text-muted-foreground mb-6">
696-
To provide feedback, select text in the plan and add annotations. Claude will use your annotations to revise the plan.
697-
</p>
698-
<div className="flex justify-end">
699-
<button
700-
onClick={() => setShowFeedbackPrompt(false)}
701-
className="px-4 py-2 rounded-md text-sm font-medium bg-primary text-primary-foreground hover:opacity-90 transition-opacity"
702-
>
703-
Got it
704-
</button>
705-
</div>
706-
</div>
707-
</div>
708-
)}
698+
<ConfirmDialog
699+
isOpen={showFeedbackPrompt}
700+
onClose={() => setShowFeedbackPrompt(false)}
701+
title="Add Annotations First"
702+
message="To provide feedback, select text in the plan and add annotations. Claude will use your annotations to revise the plan."
703+
variant="info"
704+
/>
705+
706+
{/* Claude Code annotation warning dialog */}
707+
<ConfirmDialog
708+
isOpen={showClaudeCodeWarning}
709+
onClose={() => setShowClaudeCodeWarning(false)}
710+
onConfirm={() => {
711+
setShowClaudeCodeWarning(false);
712+
handleApprove();
713+
}}
714+
title="Annotations Won't Be Sent"
715+
message={<>Claude Code doesn't yet support feedback on approval. Your {annotations.length} annotation{annotations.length !== 1 ? 's' : ''} will be lost.</>}
716+
subMessage={
717+
<>
718+
To send feedback, use <strong>Deny with Feedback</strong> instead.
719+
<br /><br />
720+
Want this feature? Upvote these issues:
721+
<br />
722+
<a href="https://github.com/anthropics/claude-code/issues/16001" target="_blank" rel="noopener noreferrer" className="text-primary hover:underline">#16001</a>
723+
{' · '}
724+
<a href="https://github.com/anthropics/claude-code/issues/15755" target="_blank" rel="noopener noreferrer" className="text-primary hover:underline">#15755</a>
725+
</>
726+
}
727+
confirmText="Approve Anyway"
728+
cancelText="Cancel"
729+
variant="warning"
730+
showCancel
731+
/>
709732

710733
{/* Completion overlay - shown after approve/deny */}
711734
{submitted && (

packages/server/index.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -145,13 +145,20 @@ export async function startPlannotatorServer(
145145

146146
// API: Approve plan
147147
if (url.pathname === "/api/approve" && req.method === "POST") {
148-
// Check for note integrations
148+
// Check for note integrations and optional feedback
149+
let feedback: string | undefined;
149150
try {
150151
const body = (await req.json().catch(() => ({}))) as {
151152
obsidian?: ObsidianConfig;
152153
bear?: BearConfig;
154+
feedback?: string;
153155
};
154156

157+
// Capture feedback if provided (for "approve with notes")
158+
if (body.feedback) {
159+
feedback = body.feedback;
160+
}
161+
155162
// Obsidian integration
156163
if (body.obsidian?.vaultPath && body.obsidian?.plan) {
157164
const result = await saveToObsidian(body.obsidian);
@@ -176,7 +183,7 @@ export async function startPlannotatorServer(
176183
console.error(`[Integration] Error:`, err);
177184
}
178185

179-
resolveDecision({ approved: true });
186+
resolveDecision({ approved: true, feedback });
180187
return Response.json({ ok: true });
181188
}
182189

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
/**
2+
* Reusable confirmation dialog component
3+
*/
4+
5+
import React from 'react';
6+
7+
export interface ConfirmDialogProps {
8+
isOpen: boolean;
9+
onClose: () => void;
10+
onConfirm?: () => void;
11+
title: string;
12+
message: React.ReactNode;
13+
subMessage?: React.ReactNode;
14+
confirmText?: string;
15+
cancelText?: string;
16+
variant?: 'info' | 'warning';
17+
showCancel?: boolean;
18+
}
19+
20+
export const ConfirmDialog: React.FC<ConfirmDialogProps> = ({
21+
isOpen,
22+
onClose,
23+
onConfirm,
24+
title,
25+
message,
26+
subMessage,
27+
confirmText = 'Got it',
28+
cancelText = 'Cancel',
29+
variant = 'info',
30+
showCancel = false,
31+
}) => {
32+
if (!isOpen) return null;
33+
34+
const iconColors = {
35+
info: 'bg-accent/20 text-accent',
36+
warning: 'bg-yellow-500/20 text-yellow-500',
37+
};
38+
39+
const buttonColors = {
40+
info: 'bg-primary text-primary-foreground hover:opacity-90',
41+
warning: 'bg-yellow-600 text-white hover:bg-yellow-500',
42+
};
43+
44+
const icons = {
45+
info: (
46+
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
47+
<path strokeLinecap="round" strokeLinejoin="round" d="M7 8h10M7 12h4m1 8l-4-4H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-3l-4 4z" />
48+
</svg>
49+
),
50+
warning: (
51+
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
52+
<path strokeLinecap="round" strokeLinejoin="round" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
53+
</svg>
54+
),
55+
};
56+
57+
return (
58+
<div className="fixed inset-0 z-50 flex items-center justify-center bg-background/80 backdrop-blur-sm p-4">
59+
<div className="bg-card border border-border rounded-xl w-full max-w-sm shadow-2xl p-6">
60+
<div className="flex items-center gap-3 mb-4">
61+
<div className={`w-10 h-10 rounded-full flex items-center justify-center ${iconColors[variant]}`}>
62+
{icons[variant]}
63+
</div>
64+
<h3 className="font-semibold">{title}</h3>
65+
</div>
66+
<div className="text-sm text-muted-foreground mb-2">
67+
{message}
68+
</div>
69+
{subMessage && (
70+
<div className="text-xs text-muted-foreground mb-6">
71+
{subMessage}
72+
</div>
73+
)}
74+
{!subMessage && <div className="mb-4" />}
75+
<div className="flex justify-end gap-2">
76+
{showCancel && (
77+
<button
78+
onClick={onClose}
79+
className="px-4 py-2 rounded-md text-sm font-medium bg-muted text-muted-foreground hover:bg-muted/80 transition-opacity"
80+
>
81+
{cancelText}
82+
</button>
83+
)}
84+
<button
85+
onClick={() => {
86+
if (onConfirm) {
87+
onConfirm();
88+
} else {
89+
onClose();
90+
}
91+
}}
92+
className={`px-4 py-2 rounded-md text-sm font-medium transition-opacity ${buttonColors[variant]}`}
93+
>
94+
{confirmText}
95+
</button>
96+
</div>
97+
</div>
98+
</div>
99+
);
100+
};

0 commit comments

Comments
 (0)