Skip to content
Merged
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
15 changes: 15 additions & 0 deletions apps/opencode-plugin/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}`;
Expand Down
77 changes: 50 additions & 27 deletions packages/editor/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -300,6 +301,7 @@ const App: React.FC = () => {
const [blocks, setBlocks] = useState<Block[]>([]);
const [showExport, setShowExport] = useState(false);
const [showFeedbackPrompt, setShowFeedbackPrompt] = useState(false);
const [showClaudeCodeWarning, setShowClaudeCodeWarning] = useState(false);
const [isPanelOpen, setIsPanelOpen] = useState(true);
const [editorMode, setEditorMode] = useState<EditorMode>('selection');
const [taterMode, setTaterMode] = useState(() => {
Expand Down Expand Up @@ -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 = {
Expand All @@ -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' },
Expand Down Expand Up @@ -575,7 +582,14 @@ const App: React.FC = () => {

<div className="relative group/approve">
<button
onClick={handleApprove}
onClick={() => {
// Show warning for Claude Code users with annotations
if (origin === 'claude-code' && annotations.length > 0) {
setShowClaudeCodeWarning(true);
} else {
handleApprove();
}
}}
disabled={isSubmitting}
className={`px-2 py-1 md:px-2.5 rounded-md text-xs font-medium transition-all ${
isSubmitting
Expand Down Expand Up @@ -681,31 +695,40 @@ const App: React.FC = () => {
/>

{/* Feedback prompt dialog */}
{showFeedbackPrompt && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-background/80 backdrop-blur-sm p-4">
<div className="bg-card border border-border rounded-xl w-full max-w-sm shadow-2xl p-6">
<div className="flex items-center gap-3 mb-4">
<div className="w-10 h-10 rounded-full bg-accent/20 flex items-center justify-center">
<svg className="w-5 h-5 text-accent" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<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" />
</svg>
</div>
<h3 className="font-semibold">Add Annotations First</h3>
</div>
<p className="text-sm text-muted-foreground mb-6">
To provide feedback, select text in the plan and add annotations. Claude will use your annotations to revise the plan.
</p>
<div className="flex justify-end">
<button
onClick={() => setShowFeedbackPrompt(false)}
className="px-4 py-2 rounded-md text-sm font-medium bg-primary text-primary-foreground hover:opacity-90 transition-opacity"
>
Got it
</button>
</div>
</div>
</div>
)}
<ConfirmDialog
isOpen={showFeedbackPrompt}
onClose={() => 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 */}
<ConfirmDialog
isOpen={showClaudeCodeWarning}
onClose={() => 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 <strong>Deny with Feedback</strong> instead.
<br /><br />
Want this feature? Upvote these issues:
<br />
<a href="https://github.com/anthropics/claude-code/issues/16001" target="_blank" rel="noopener noreferrer" className="text-primary hover:underline">#16001</a>
{' · '}
<a href="https://github.com/anthropics/claude-code/issues/15755" target="_blank" rel="noopener noreferrer" className="text-primary hover:underline">#15755</a>
</>
}
confirmText="Approve Anyway"
cancelText="Cancel"
variant="warning"
showCancel
/>

{/* Completion overlay - shown after approve/deny */}
{submitted && (
Expand Down
11 changes: 9 additions & 2 deletions packages/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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 });
}

Expand Down
100 changes: 100 additions & 0 deletions packages/ui/components/ConfirmDialog.tsx
Original file line number Diff line number Diff line change
@@ -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<ConfirmDialogProps> = ({
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: (
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<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" />
</svg>
),
warning: (
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<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" />
</svg>
),
};

return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-background/80 backdrop-blur-sm p-4">
<div className="bg-card border border-border rounded-xl w-full max-w-sm shadow-2xl p-6">
<div className="flex items-center gap-3 mb-4">
<div className={`w-10 h-10 rounded-full flex items-center justify-center ${iconColors[variant]}`}>
{icons[variant]}
</div>
<h3 className="font-semibold">{title}</h3>
</div>
<div className="text-sm text-muted-foreground mb-2">
{message}
</div>
{subMessage && (
<div className="text-xs text-muted-foreground mb-6">
{subMessage}
</div>
)}
{!subMessage && <div className="mb-4" />}
<div className="flex justify-end gap-2">
{showCancel && (
<button
onClick={onClose}
className="px-4 py-2 rounded-md text-sm font-medium bg-muted text-muted-foreground hover:bg-muted/80 transition-opacity"
>
{cancelText}
</button>
)}
<button
onClick={() => {
if (onConfirm) {
onConfirm();
} else {
onClose();
}
}}
className={`px-4 py-2 rounded-md text-sm font-medium transition-opacity ${buttonColors[variant]}`}
>
{confirmText}
</button>
</div>
</div>
</div>
);
};