feat: add plan viewer with browser preview (LET-7645) (#1089)

Co-authored-by: Letta <noreply@letta.com>
This commit is contained in:
Charles Packer
2026-02-21 13:39:22 -08:00
committed by GitHub
parent 5ad7094a26
commit 5d8a832c00
7 changed files with 502 additions and 3 deletions

View File

@@ -11791,6 +11791,18 @@ Plan file path: ${planFilePath}`;
"project")
}
showPreview={showApprovalPreview}
planContent={
currentApproval.toolName === "ExitPlanMode"
? _readPlanFile()
: undefined
}
planFilePath={
currentApproval.toolName === "ExitPlanMode"
? (permissionMode.getPlanFilePath() ??
undefined)
: undefined
}
agentName={agentName ?? undefined}
/>
) : ln.kind === "user" ? (
<UserMessage line={ln} prompt={statusLine.prompt} />
@@ -11875,6 +11887,17 @@ Plan file path: ${planFilePath}`;
: (currentApprovalContext?.defaultScope ?? "project")
}
showPreview={showApprovalPreview}
planContent={
currentApproval.toolName === "ExitPlanMode"
? _readPlanFile()
: undefined
}
planFilePath={
currentApproval.toolName === "ExitPlanMode"
? (permissionMode.getPlanFilePath() ?? undefined)
: undefined
}
agentName={agentName ?? undefined}
/>
</Box>
)}

View File

@@ -84,6 +84,11 @@ type Props = {
// External data for FileEdit approvals
precomputedDiff?: AdvancedDiffSuccess;
allDiffs?: Map<string, AdvancedDiffSuccess>;
// Plan viewer data (for ExitPlanMode 'o' key)
planContent?: string;
planFilePath?: string;
agentName?: string;
};
// Parse bash info from approval args
@@ -217,6 +222,9 @@ export const ApprovalSwitch = memo(
allDiffs,
showPreview = true,
defaultScope = "project",
planContent,
planFilePath,
agentName,
}: Props) => {
const toolName = approval.toolName;
@@ -229,6 +237,9 @@ export const ApprovalSwitch = memo(
onKeepPlanning={onPlanKeepPlanning}
onCancel={onCancel ?? (() => {})}
isFocused={isFocused}
planContent={planContent}
planFilePath={planFilePath}
agentName={agentName}
/>
);
}

View File

@@ -1,5 +1,6 @@
import { Box, useInput } from "ink";
import { memo, useState } from "react";
import { memo, useCallback, useState } from "react";
import { generateAndOpenPlanViewer } from "../../web/generate-plan-viewer";
import { useProgressIndicator } from "../hooks/useProgressIndicator";
import { useTerminalWidth } from "../hooks/useTerminalWidth";
import { useTextInputCursor } from "../hooks/useTextInputCursor";
@@ -12,6 +13,9 @@ type Props = {
onKeepPlanning: (reason: string) => void;
onCancel: () => void; // For CTRL-C to queue denial (like other approval screens)
isFocused?: boolean;
planContent?: string;
planFilePath?: string;
agentName?: string;
};
/**
@@ -32,8 +36,12 @@ export const StaticPlanApproval = memo(
onKeepPlanning,
onCancel,
isFocused = true,
planContent,
planFilePath,
agentName,
}: Props) => {
const [selectedOption, setSelectedOption] = useState(0);
const [browserStatus, setBrowserStatus] = useState("");
const {
text: customReason,
cursorPos,
@@ -43,6 +51,24 @@ export const StaticPlanApproval = memo(
const columns = useTerminalWidth();
useProgressIndicator();
const openInBrowser = useCallback(() => {
if (!planContent || !planFilePath) return;
setBrowserStatus("Opening in browser...");
generateAndOpenPlanViewer(planContent, planFilePath, { agentName })
.then((result) => {
setBrowserStatus(
result.opened
? "Opened in browser"
: `Run: open ${result.filePath}`,
);
setTimeout(() => setBrowserStatus(""), 5000);
})
.catch(() => {
setBrowserStatus("Failed to open browser");
setTimeout(() => setBrowserStatus(""), 5000);
});
}, [planContent, planFilePath, agentName]);
const customOptionIndex = 2;
const maxOptionIndex = customOptionIndex;
const isOnCustomOption = selectedOption === customOptionIndex;
@@ -59,6 +85,16 @@ export const StaticPlanApproval = memo(
return;
}
// O: open plan in browser (only when not typing in custom field)
if (
(input === "o" || input === "O") &&
!isOnCustomOption &&
planContent
) {
openInBrowser();
return;
}
// Arrow navigation always works
if (key.upArrow) {
setSelectedOption((prev) => Math.max(0, prev - 1));
@@ -117,11 +153,12 @@ export const StaticPlanApproval = memo(
);
// Hint text based on state
const browserHint = planContent ? " · O open in browser" : "";
const hintText = isOnCustomOption
? customReason
? "Enter to submit · Esc to clear"
: "Type feedback · Esc to cancel"
: "Enter to select · Esc to cancel";
: `Enter to select${browserHint} · Esc to cancel`;
return (
<Box flexDirection="column">
@@ -206,7 +243,7 @@ export const StaticPlanApproval = memo(
{/* Hint */}
<Box marginTop={1}>
<Text dimColor>{hintText}</Text>
<Text dimColor>{browserStatus || hintText}</Text>
</Box>
</Box>
);