diff --git a/apps/hook/server/index.ts b/apps/hook/server/index.ts
index 797cc6e9..265ba0ff 100644
--- a/apps/hook/server/index.ts
+++ b/apps/hook/server/index.ts
@@ -431,7 +431,9 @@ if (args[0] === "sessions") {
server.stop();
// Output feedback (captured by slash command)
- if (result.approved) {
+ if (result.exit) {
+ console.log("Review session closed without feedback.");
+ } else if (result.approved) {
console.log("Code review completed — no changes requested.");
} else {
console.log(result.feedback);
@@ -553,7 +555,11 @@ if (args[0] === "sessions") {
server.stop();
// Output feedback (captured by slash command)
- console.log(result.feedback || "No feedback provided.");
+ if (result.exit) {
+ console.log("Annotation session closed without feedback.");
+ } else {
+ console.log(result.feedback || "No feedback provided.");
+ }
process.exit(0);
} else if (args[0] === "annotate-last" || args[0] === "last") {
@@ -667,7 +673,11 @@ if (args[0] === "sessions") {
server.stop();
- console.log(result.feedback || "No feedback provided.");
+ if (result.exit) {
+ console.log("Annotation session closed without feedback.");
+ } else {
+ console.log(result.feedback || "No feedback provided.");
+ }
process.exit(0);
} else if (args[0] === "archive") {
@@ -852,7 +862,11 @@ if (args[0] === "sessions") {
await Bun.sleep(1500);
server.stop();
- console.log(result.feedback || "No feedback provided.");
+ if (result.exit) {
+ console.log("Annotation session closed without feedback.");
+ } else {
+ console.log(result.feedback || "No feedback provided.");
+ }
process.exit(0);
} else if (args[0] === "improve-context") {
diff --git a/apps/opencode-plugin/commands.ts b/apps/opencode-plugin/commands.ts
index c2829f3c..2218c271 100644
--- a/apps/opencode-plugin/commands.ts
+++ b/apps/opencode-plugin/commands.ts
@@ -103,6 +103,10 @@ export async function handleReviewCommand(
await Bun.sleep(1500);
server.stop();
+ if (result.exit) {
+ return;
+ }
+
if (result.feedback) {
// @ts-ignore - Event properties contain sessionID
const sessionId = event.properties?.sessionID;
@@ -181,6 +185,10 @@ export async function handleAnnotateCommand(
await Bun.sleep(1500);
server.stop();
+ if (result.exit) {
+ return;
+ }
+
if (result.feedback) {
// @ts-ignore - Event properties contain sessionID
const sessionId = event.properties?.sessionID;
@@ -266,6 +274,10 @@ export async function handleAnnotateLastCommand(
await Bun.sleep(1500);
server.stop();
+ if (result.exit) {
+ return null;
+ }
+
return result.feedback || null;
}
diff --git a/apps/pi-extension/index.ts b/apps/pi-extension/index.ts
index a3c62040..38ed6cfe 100644
--- a/apps/pi-extension/index.ts
+++ b/apps/pi-extension/index.ts
@@ -347,7 +347,9 @@ export default function plannotator(pi: ExtensionAPI): void {
const prUrl = args?.trim() || undefined;
const isPRReview = prUrl?.startsWith("http://") || prUrl?.startsWith("https://");
const result = await openCodeReview(ctx, { prUrl });
- if (result.feedback) {
+ if (result.exit) {
+ ctx.ui.notify("Code review session closed.", "info");
+ } else if (result.feedback) {
if (result.approved) {
pi.sendUserMessage(
`# Code Review\n\nCode review completed — no changes requested.`,
@@ -424,7 +426,9 @@ export default function plannotator(pi: ExtensionAPI): void {
try {
const result = await openMarkdownAnnotation(ctx, absolutePath, markdown, mode ?? "annotate", folderPath);
- if (result.feedback) {
+ if (result.exit) {
+ ctx.ui.notify("Annotation session closed.", "info");
+ } else if (result.feedback) {
const header = isFolder
? `# Markdown Annotations\n\nFolder: ${absolutePath}\n\n`
: `# Markdown Annotations\n\nFile: ${absolutePath}\n\n`;
@@ -464,7 +468,9 @@ export default function plannotator(pi: ExtensionAPI): void {
try {
const result = await openLastMessageAnnotation(ctx, lastText);
- if (result.feedback) {
+ if (result.exit) {
+ ctx.ui.notify("Annotation session closed.", "info");
+ } else if (result.feedback) {
pi.sendUserMessage(
`# Message Annotations\n\n${result.feedback}\n\nPlease address the annotation feedback above.`,
);
diff --git a/apps/pi-extension/plannotator-browser.ts b/apps/pi-extension/plannotator-browser.ts
index 5b69fd18..6c6548fb 100644
--- a/apps/pi-extension/plannotator-browser.ts
+++ b/apps/pi-extension/plannotator-browser.ts
@@ -165,7 +165,7 @@ export async function openPlanReviewBrowser(
export async function openCodeReview(
ctx: ExtensionContext,
options: { cwd?: string; defaultBranch?: string; diffType?: DiffType; prUrl?: string } = {},
-): Promise<{ approved: boolean; feedback?: string; annotations?: unknown[]; agentSwitch?: string }> {
+): Promise<{ approved: boolean; feedback?: string; annotations?: unknown[]; agentSwitch?: string; exit?: boolean }> {
if (!ctx.hasUI || !reviewHtmlContent) {
throw new Error("Plannotator code review browser is unavailable in this session.");
}
@@ -367,7 +367,7 @@ export async function openMarkdownAnnotation(
markdown: string,
mode: AnnotateMode,
folderPath?: string,
-): Promise<{ feedback: string }> {
+): Promise<{ feedback: string; exit?: boolean }> {
if (!ctx.hasUI || !planHtmlContent) {
throw new Error("Plannotator annotation browser is unavailable in this session.");
}
@@ -402,7 +402,7 @@ export async function openMarkdownAnnotation(
export async function openLastMessageAnnotation(
ctx: ExtensionContext,
lastText: string,
-): Promise<{ feedback: string }> {
+): Promise<{ feedback: string; exit?: boolean }> {
return openMarkdownAnnotation(ctx, "last-message", lastText, "annotate-last");
}
diff --git a/apps/pi-extension/server.test.ts b/apps/pi-extension/server.test.ts
index 763b3fb6..007ac539 100644
--- a/apps/pi-extension/server.test.ts
+++ b/apps/pi-extension/server.test.ts
@@ -214,6 +214,43 @@ describe("pi review server", () => {
}
});
+ test("exit endpoint resolves decision with exit flag", async () => {
+ const homeDir = makeTempDir("plannotator-pi-home-");
+ const repoDir = initRepo();
+ process.env.HOME = homeDir;
+ process.chdir(repoDir);
+ process.env.PLANNOTATOR_PORT = String(await reservePort());
+
+ const gitContext = await getGitContext();
+ const diff = await runGitDiff("uncommitted", gitContext.defaultBranch);
+
+ const server = await startReviewServer({
+ rawPatch: diff.patch,
+ gitRef: diff.label,
+ error: diff.error,
+ diffType: "uncommitted",
+ gitContext,
+ origin: "pi",
+ htmlContent: "
review",
+ });
+
+ try {
+ const exitResponse = await fetch(`${server.url}/api/exit`, { method: "POST" });
+ expect(exitResponse.status).toBe(200);
+ expect(await exitResponse.json()).toEqual({ ok: true });
+
+ await expect(server.waitForDecision()).resolves.toEqual({
+ exit: true,
+ approved: false,
+ feedback: "",
+ annotations: [],
+ agentSwitch: undefined,
+ });
+ } finally {
+ server.stop();
+ }
+ });
+
test("git-add endpoint stages and unstages files in review mode", async () => {
const homeDir = makeTempDir("plannotator-pi-home-");
const repoDir = initRepo();
diff --git a/apps/pi-extension/server/serverAnnotate.ts b/apps/pi-extension/server/serverAnnotate.ts
index c2b96e34..7699aa12 100644
--- a/apps/pi-extension/server/serverAnnotate.ts
+++ b/apps/pi-extension/server/serverAnnotate.ts
@@ -28,7 +28,7 @@ export interface AnnotateServerResult {
port: number;
portSource: "env" | "remote-default" | "random";
url: string;
- waitForDecision: () => Promise<{ feedback: string; annotations: unknown[] }>;
+ waitForDecision: () => Promise<{ feedback: string; annotations: unknown[]; exit?: boolean }>;
stop: () => void;
}
@@ -54,10 +54,12 @@ export async function startAnnotateServer(options: {
let resolveDecision!: (result: {
feedback: string;
annotations: unknown[];
+ exit?: boolean;
}) => void;
const decisionPromise = new Promise<{
feedback: string;
annotations: unknown[];
+ exit?: boolean;
}>((r) => {
resolveDecision = r;
});
@@ -122,6 +124,10 @@ export async function startAnnotateServer(options: {
handleFileBrowserRequest(res, url);
} else if (url.pathname === "/favicon.svg") {
handleFavicon(res);
+ } else if (url.pathname === "/api/exit" && req.method === "POST") {
+ deleteDraft(draftKey);
+ resolveDecision({ feedback: "", annotations: [], exit: true });
+ json(res, { ok: true });
} else if (url.pathname === "/api/feedback" && req.method === "POST") {
try {
const body = await parseBody(req);
diff --git a/apps/pi-extension/server/serverReview.ts b/apps/pi-extension/server/serverReview.ts
index 6d2c6939..c69180aa 100644
--- a/apps/pi-extension/server/serverReview.ts
+++ b/apps/pi-extension/server/serverReview.ts
@@ -95,6 +95,7 @@ export interface ReviewServerResult {
feedback: string;
annotations: unknown[];
agentSwitch?: string;
+ exit?: boolean;
}>;
stop: () => void;
}
@@ -285,12 +286,14 @@ export async function startReviewServer(options: {
feedback: string;
annotations: unknown[];
agentSwitch?: string;
+ exit?: boolean;
}) => void;
const decisionPromise = new Promise<{
approved: boolean;
feedback: string;
annotations: unknown[];
agentSwitch?: string;
+ exit?: boolean;
}>((r) => {
resolveDecision = r;
});
@@ -680,6 +683,10 @@ export async function startReviewServer(options: {
return;
}
json(res, { error: "Not found" }, 404);
+ } else if (url.pathname === "/api/exit" && req.method === "POST") {
+ deleteDraft(draftKey);
+ resolveDecision({ approved: false, feedback: '', annotations: [], exit: true });
+ json(res, { ok: true });
} else if (url.pathname === "/api/feedback" && req.method === "POST") {
try {
const body = await parseBody(req);
diff --git a/bun.lock b/bun.lock
index c9d35859..786f3433 100644
--- a/bun.lock
+++ b/bun.lock
@@ -1,5 +1,6 @@
{
"lockfileVersion": 1,
+ "configVersion": 0,
"workspaces": {
"": {
"name": "plannotator",
@@ -62,7 +63,7 @@
},
"apps/opencode-plugin": {
"name": "@plannotator/opencode",
- "version": "0.17.3",
+ "version": "0.17.7",
"dependencies": {
"@opencode-ai/plugin": "^1.1.10",
},
@@ -84,7 +85,7 @@
},
"apps/pi-extension": {
"name": "@plannotator/pi-extension",
- "version": "0.17.3",
+ "version": "0.17.7",
"peerDependencies": {
"@mariozechner/pi-coding-agent": ">=0.53.0",
},
@@ -170,7 +171,7 @@
},
"packages/server": {
"name": "@plannotator/server",
- "version": "0.17.3",
+ "version": "0.17.7",
"dependencies": {
"@plannotator/ai": "workspace:*",
"@plannotator/shared": "workspace:*",
diff --git a/packages/editor/App.tsx b/packages/editor/App.tsx
index 5499be10..ca75dd06 100644
--- a/packages/editor/App.tsx
+++ b/packages/editor/App.tsx
@@ -1,4 +1,4 @@
-import React, { useState, useEffect, useLayoutEffect, useMemo, useRef } from 'react';
+import React, { useState, useEffect, useLayoutEffect, useMemo, useRef, useCallback } from 'react';
import { type Origin, getAgentName } from '@plannotator/shared/agents';
import { parseMarkdownToBlocks, exportAnnotations, exportLinkedDocAnnotations, exportEditorAnnotations, extractFrontmatter, wrapFeedbackForAgent, Frontmatter } from '@plannotator/ui/utils/parser';
import { Viewer, ViewerHandle } from '@plannotator/ui/components/Viewer';
@@ -13,7 +13,7 @@ import { StickyHeaderLane } from '@plannotator/ui/components/StickyHeaderLane';
import { TaterSpriteRunning } from '@plannotator/ui/components/TaterSpriteRunning';
import { TaterSpritePullup } from '@plannotator/ui/components/TaterSpritePullup';
import { Settings } from '@plannotator/ui/components/Settings';
-import { FeedbackButton, ApproveButton } from '@plannotator/ui/components/ToolbarButtons';
+import { FeedbackButton, ApproveButton, ExitButton } from '@plannotator/ui/components/ToolbarButtons';
import { useSharing } from '@plannotator/ui/hooks/useSharing';
import { getCallbackConfig, CallbackAction, executeCallback, type ToastPayload } from '@plannotator/ui/utils/callback';
import { useAgents } from '@plannotator/ui/hooks/useAgents';
@@ -84,6 +84,7 @@ const App: React.FC = () => {
const [showImport, setShowImport] = useState(false);
const [showFeedbackPrompt, setShowFeedbackPrompt] = useState(false);
const [showClaudeCodeWarning, setShowClaudeCodeWarning] = useState(false);
+ const [showExitWarning, setShowExitWarning] = useState(false);
const [showAgentWarning, setShowAgentWarning] = useState(false);
const [agentWarningMessage, setAgentWarningMessage] = useState('');
const [isPanelOpen, setIsPanelOpen] = useState(() => window.innerWidth >= 768);
@@ -136,7 +137,8 @@ const App: React.FC = () => {
const [imageBaseDir, setImageBaseDir] = useState(undefined);
const [isLoading, setIsLoading] = useState(true);
const [isSubmitting, setIsSubmitting] = useState(false);
- const [submitted, setSubmitted] = useState<'approved' | 'denied' | null>(null);
+ const [isExiting, setIsExiting] = useState(false);
+ const [submitted, setSubmitted] = useState<'approved' | 'denied' | 'exited' | null>(null);
const [pendingPasteImage, setPendingPasteImage] = useState<{ file: File; blobUrl: string; initialName: string } | null>(null);
const [showPermissionModeSetup, setShowPermissionModeSetup] = useState(false);
const [permissionMode, setPermissionMode] = useState('bypassPermissions');
@@ -865,6 +867,21 @@ const App: React.FC = () => {
}
};
+ // Exit annotation session without sending feedback
+ const handleAnnotateExit = useCallback(async () => {
+ setIsExiting(true);
+ try {
+ const res = await fetch('/api/exit', { method: 'POST' });
+ if (res.ok) {
+ setSubmitted('exited');
+ } else {
+ throw new Error('Failed to exit');
+ }
+ } catch {
+ setIsExiting(false);
+ }
+ }, []);
+
// Global keyboard shortcuts (Cmd/Ctrl+Enter to submit)
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
@@ -877,10 +894,10 @@ const App: React.FC = () => {
// Don't intercept if any modal is open
if (showExport || showImport || showFeedbackPrompt || showClaudeCodeWarning ||
- showAgentWarning || showPermissionModeSetup || pendingPasteImage) return;
+ showExitWarning || showAgentWarning || showPermissionModeSetup || pendingPasteImage) return;
- // Don't intercept if already submitted or submitting
- if (submitted || isSubmitting) return;
+ // Don't intercept if already submitted, submitting, or exiting
+ if (submitted || isSubmitting || isExiting) return;
// Don't intercept in demo/share mode (no API)
if (!isApiMode) return;
@@ -920,9 +937,9 @@ const App: React.FC = () => {
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [
- showExport, showImport, showFeedbackPrompt, showClaudeCodeWarning, showAgentWarning,
+ showExport, showImport, showFeedbackPrompt, showClaudeCodeWarning, showExitWarning, showAgentWarning,
showPermissionModeSetup, pendingPasteImage,
- submitted, isSubmitting, isApiMode, linkedDocHook.isActive, annotations.length, externalAnnotations.length, annotateMode,
+ submitted, isSubmitting, isExiting, isApiMode, linkedDocHook.isActive, annotations.length, externalAnnotations.length, annotateMode,
origin, getAgentWarning,
]);
@@ -1153,7 +1170,7 @@ const App: React.FC = () => {
if (tag === 'INPUT' || tag === 'TEXTAREA') return;
if (showExport || showFeedbackPrompt || showClaudeCodeWarning ||
- showAgentWarning || showPermissionModeSetup || pendingPasteImage) return;
+ showExitWarning || showAgentWarning || showPermissionModeSetup || pendingPasteImage) return;
if (submitted || !isApiMode) return;
@@ -1181,7 +1198,7 @@ const App: React.FC = () => {
window.addEventListener('keydown', handleSaveShortcut);
return () => window.removeEventListener('keydown', handleSaveShortcut);
}, [
- showExport, showFeedbackPrompt, showClaudeCodeWarning, showAgentWarning,
+ showExport, showFeedbackPrompt, showClaudeCodeWarning, showExitWarning, showAgentWarning,
showPermissionModeSetup, pendingPasteImage,
submitted, isApiMode, markdown, annotationsOutput,
]);
@@ -1195,7 +1212,7 @@ const App: React.FC = () => {
if (tag === 'INPUT' || tag === 'TEXTAREA') return;
if (showExport || showFeedbackPrompt || showClaudeCodeWarning ||
- showAgentWarning || showPermissionModeSetup || pendingPasteImage) return;
+ showExitWarning || showAgentWarning || showPermissionModeSetup || pendingPasteImage) return;
if (submitted) return;
@@ -1206,7 +1223,7 @@ const App: React.FC = () => {
window.addEventListener('keydown', handlePrintShortcut);
return () => window.removeEventListener('keydown', handlePrintShortcut);
}, [
- showExport, showFeedbackPrompt, showClaudeCodeWarning, showAgentWarning,
+ showExport, showFeedbackPrompt, showClaudeCodeWarning, showExitWarning, showAgentWarning,
showPermissionModeSetup, pendingPasteImage, submitted,
]);
@@ -1278,27 +1295,44 @@ const App: React.FC = () => {
{isApiMode && (!linkedDocHook.isActive || annotateMode) && !archive.archiveMode && (
<>
- {
- if (annotateMode) {
- handleAnnotateFeedback();
- return;
- }
- const docAnnotations = linkedDocHook.getDocAnnotations();
- const hasDocAnnotations = Array.from(docAnnotations.values()).some(
- (d) => d.annotations.length > 0 || d.globalAttachments.length > 0
- );
- if (allAnnotations.length === 0 && editorAnnotations.length === 0 && !hasDocAnnotations) {
- setShowFeedbackPrompt(true);
- } else {
- handleDeny();
- }
- }}
- disabled={isSubmitting}
- isLoading={isSubmitting}
- label={annotateMode ? (allAnnotations.length > 0 || editorAnnotations.length > 0 || linkedDocHook.docAnnotationCount > 0 ? 'Send Annotations' : 'Done') : 'Send Feedback'}
- title={annotateMode ? (allAnnotations.length > 0 || editorAnnotations.length > 0 || linkedDocHook.docAnnotationCount > 0 ? 'Send Annotations' : 'Done') : 'Send Feedback'}
- />
+ {annotateMode ? (
+ // Annotate mode: Close always visible, Send Annotations when annotations exist
+ <>
+ (allAnnotations.length > 0 || editorAnnotations.length > 0 || linkedDocHook.docAnnotationCount > 0 || globalAttachments.length > 0) ? setShowExitWarning(true) : handleAnnotateExit()}
+ disabled={isSubmitting || isExiting}
+ isLoading={isExiting}
+ />
+ {(allAnnotations.length > 0 || editorAnnotations.length > 0 || linkedDocHook.docAnnotationCount > 0 || globalAttachments.length > 0) && (
+
+ )}
+ >
+ ) : (
+ // Plan mode: Send Feedback
+ {
+ const docAnnotations = linkedDocHook.getDocAnnotations();
+ const hasDocAnnotations = Array.from(docAnnotations.values()).some(
+ (d) => d.annotations.length > 0 || d.globalAttachments.length > 0
+ );
+ if (allAnnotations.length === 0 && editorAnnotations.length === 0 && !hasDocAnnotations) {
+ setShowFeedbackPrompt(true);
+ } else {
+ handleDeny();
+ }
+ }}
+ disabled={isSubmitting}
+ isLoading={isSubmitting}
+ label="Send Feedback"
+ title="Send Feedback"
+ />
+ )}
{!annotateMode &&
{
showCancel
/>
+ {/* Exit with annotations warning dialog */}
+ setShowExitWarning(false)}
+ onConfirm={() => {
+ setShowExitWarning(false);
+ handleAnnotateExit();
+ }}
+ title="Annotations Won't Be Sent"
+ message={<>You have {allAnnotations.length + editorAnnotations.length + linkedDocHook.docAnnotationCount + globalAttachments.length} annotation{(allAnnotations.length + editorAnnotations.length + linkedDocHook.docAnnotationCount + globalAttachments.length) !== 1 ? 's' : ''} that will be lost if you close.>}
+ subMessage="To send your annotations, use Send Annotations instead."
+ confirmText="Close Anyway"
+ cancelText="Cancel"
+ variant="warning"
+ showCancel
+ />
+
{/* OpenCode agent not found warning dialog */}
{
{/* Completion overlay - shown after approve/deny */}
diff --git a/packages/review-editor/App.tsx b/packages/review-editor/App.tsx
index a752d628..15f1ab6d 100644
--- a/packages/review-editor/App.tsx
+++ b/packages/review-editor/App.tsx
@@ -3,7 +3,8 @@ import { type Origin, getAgentName } from '@plannotator/shared/agents';
import { ThemeProvider, useTheme } from '@plannotator/ui/components/ThemeProvider';
import { ConfirmDialog } from '@plannotator/ui/components/ConfirmDialog';
import { Settings } from '@plannotator/ui/components/Settings';
-import { FeedbackButton, ApproveButton } from '@plannotator/ui/components/ToolbarButtons';
+import { FeedbackButton, ApproveButton, ExitButton } from '@plannotator/ui/components/ToolbarButtons';
+import { AgentReviewActions } from './components/AgentReviewActions';
import { UpdateBanner } from '@plannotator/ui/components/UpdateBanner';
import { storage } from '@plannotator/ui/utils/storage';
import { CompletionOverlay } from '@plannotator/ui/components/CompletionOverlay';
@@ -158,8 +159,10 @@ const ReviewApp: React.FC = () => {
const [diffError, setDiffError] = useState(null);
const [isSendingFeedback, setIsSendingFeedback] = useState(false);
const [isApproving, setIsApproving] = useState(false);
- const [submitted, setSubmitted] = useState<'approved' | 'feedback' | false>(false);
+ const [isExiting, setIsExiting] = useState(false);
+ const [submitted, setSubmitted] = useState<'approved' | 'feedback' | 'exited' | false>(false);
const [showApproveWarning, setShowApproveWarning] = useState(false);
+ const [showExitWarning, setShowExitWarning] = useState(false);
const [sharingEnabled, setSharingEnabled] = useState(true);
const [repoInfo, setRepoInfo] = useState<{ display: string; branch?: string } | null>(null);
@@ -1089,6 +1092,22 @@ const ReviewApp: React.FC = () => {
}
}, [totalAnnotationCount, feedbackMarkdown, allAnnotations]);
+ // Exit review session without sending any feedback
+ const handleExit = useCallback(async () => {
+ setIsExiting(true);
+ try {
+ const res = await fetch('/api/exit', { method: 'POST' });
+ if (res.ok) {
+ setSubmitted('exited');
+ } else {
+ throw new Error('Failed to exit');
+ }
+ } catch (error) {
+ console.error('Failed to exit review:', error);
+ setIsExiting(false);
+ }
+ }, []);
+
// Approve without feedback (LGTM)
const handleApprove = useCallback(async () => {
setIsApproving(true);
@@ -1278,8 +1297,8 @@ const ReviewApp: React.FC = () => {
const tag = (e.target as HTMLElement)?.tagName;
if (tag === 'INPUT' || tag === 'TEXTAREA') return;
- if (showExportModal || showNoAnnotationsDialog || showApproveWarning) return;
- if (submitted || isSendingFeedback || isApproving || isPlatformActioning) return;
+ if (showExportModal || showNoAnnotationsDialog || showApproveWarning || showExitWarning) return;
+ if (submitted || isSendingFeedback || isApproving || isExiting || isPlatformActioning) return;
if (!origin) return; // Demo mode
e.preventDefault();
@@ -1307,9 +1326,9 @@ const ReviewApp: React.FC = () => {
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [
- showExportModal, showNoAnnotationsDialog, showApproveWarning,
+ showExportModal, showNoAnnotationsDialog, showApproveWarning, showExitWarning,
platformCommentDialog, platformGeneralComment,
- submitted, isSendingFeedback, isApproving, isPlatformActioning,
+ submitted, isSendingFeedback, isApproving, isExiting, isPlatformActioning,
origin, platformMode, platformUser, prMetadata, totalAnnotationCount,
handleApprove, handleSendFeedback, handlePlatformAction
]);
@@ -1499,73 +1518,67 @@ const ReviewApp: React.FC = () => {
)}
- {/* Send Feedback button — always the same label */}
- {
- if (platformMode) {
- setPlatformGeneralComment('');
- setPlatformCommentDialog({ action: 'comment' });
- } else {
- handleSendFeedback();
- }
- }}
- disabled={
- isSendingFeedback || isApproving || isPlatformActioning ||
- (!platformMode && totalAnnotationCount === 0)
- }
- isLoading={isSendingFeedback || isPlatformActioning}
- muted={!platformMode && totalAnnotationCount === 0 && !isSendingFeedback && !isApproving && !isPlatformActioning}
- label={platformMode ? 'Post Comments' : 'Send Feedback'}
- shortLabel={platformMode ? 'Post' : 'Send'}
- loadingLabel={platformMode ? 'Posting...' : 'Sending...'}
- shortLoadingLabel={platformMode ? 'Posting...' : 'Sending...'}
- title={!platformMode && totalAnnotationCount === 0 ? "Add annotations to send feedback" : "Send feedback"}
- />
-
- {/* Approve button — always the same label */}
-
-
{
- if (platformMode) {
- if (platformUser && prMetadata?.author === platformUser) return;
+ {/* Agent mode: Close/SendFeedback flip + Approve */}
+ {!platformMode ? (
+ totalAnnotationCount > 0 ? setShowApproveWarning(true) : handleApprove()}
+ onExit={() => totalAnnotationCount > 0 ? setShowExitWarning(true) : handleExit()}
+ />
+ ) : (
+ <>
+ {/* Platform mode: Close + Post Comments + Approve */}
+ totalAnnotationCount > 0 ? setShowExitWarning(true) : handleExit()}
+ disabled={isSendingFeedback || isApproving || isExiting || isPlatformActioning}
+ isLoading={isExiting}
+ />
+ {
setPlatformGeneralComment('');
- setPlatformCommentDialog({ action: 'approve' });
- } else {
- if (totalAnnotationCount > 0) {
- setShowApproveWarning(true);
- } else {
- handleApprove();
+ setPlatformCommentDialog({ action: 'comment' });
+ }}
+ disabled={isSendingFeedback || isApproving || isPlatformActioning}
+ isLoading={isSendingFeedback || isPlatformActioning}
+ label="Post Comments"
+ shortLabel="Post"
+ loadingLabel="Posting..."
+ shortLoadingLabel="Posting..."
+ title="Send feedback"
+ />
+
+
{
+ if (platformUser && prMetadata?.author === platformUser) return;
+ setPlatformGeneralComment('');
+ setPlatformCommentDialog({ action: 'approve' });
+ }}
+ disabled={
+ isSendingFeedback || isApproving || isPlatformActioning ||
+ (!!platformUser && prMetadata?.author === platformUser)
}
- }
- }}
- disabled={
- isSendingFeedback || isApproving || isPlatformActioning ||
- (platformMode && !!platformUser && prMetadata?.author === platformUser)
- }
- isLoading={isApproving}
- dimmed={!platformMode && totalAnnotationCount > 0}
- muted={platformMode && !!platformUser && prMetadata?.author === platformUser && !isSendingFeedback && !isApproving && !isPlatformActioning}
- title={
- platformMode && platformUser && prMetadata?.author === platformUser
- ? `You can't approve your own ${mrLabel}`
- : "Approve - no changes needed"
- }
- />
- {/* Tooltip: own PR warning OR annotations-lost warning */}
- {platformMode && platformUser && prMetadata?.author === platformUser ? (
-
-
-
- You can't approve your own {mrLabel === 'MR' ? 'merge request' : 'pull request'} on {platformLabel}.
-
- ) : !platformMode && totalAnnotationCount > 0 ? (
-
-
-
- Your {totalAnnotationCount} annotation{totalAnnotationCount !== 1 ? 's' : ''} won't be sent if you approve.
+ isLoading={isApproving}
+ muted={!!platformUser && prMetadata?.author === platformUser && !isSendingFeedback && !isApproving && !isPlatformActioning}
+ title={
+ platformUser && prMetadata?.author === platformUser
+ ? `You can't approve your own ${mrLabel}`
+ : "Approve - no changes needed"
+ }
+ />
+ {platformUser && prMetadata?.author === platformUser && (
+
+
+
+ You can't approve your own {mrLabel === 'MR' ? 'merge request' : 'pull request'} on {platformLabel}.
+
+ )}
- ) : null}
-
+ >
+ )}
>
) : (
);
+
+interface ExitButtonProps {
+ onClick: () => void;
+ disabled?: boolean;
+ isLoading?: boolean;
+ title?: string;
+}
+
+export const ExitButton: React.FC = ({
+ onClick,
+ disabled = false,
+ isLoading = false,
+ title = 'Close session without sending feedback',
+}) => (
+
+);