feat: add plan viewer with browser preview (LET-7645) (#1089)
Co-authored-by: Letta <noreply@letta.com>
This commit is contained in:
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user