322
src/cli/components/ApprovalPreview.tsx
Normal file
322
src/cli/components/ApprovalPreview.tsx
Normal file
@@ -0,0 +1,322 @@
|
||||
import { Box, Text } from "ink";
|
||||
import { memo } from "react";
|
||||
import type { AdvancedDiffSuccess } from "../helpers/diff";
|
||||
import { parsePatchOperations } from "../helpers/formatArgsDisplay";
|
||||
import { useTerminalWidth } from "../hooks/useTerminalWidth";
|
||||
import { AdvancedDiffRenderer } from "./AdvancedDiffRenderer";
|
||||
import { colors } from "./colors";
|
||||
import { BashPreview } from "./previews/BashPreview";
|
||||
import { PlanPreview } from "./previews/PlanPreview";
|
||||
|
||||
const SOLID_LINE = "─";
|
||||
const DOTTED_LINE = "╌";
|
||||
|
||||
type Props = {
|
||||
toolName: string;
|
||||
toolArgs: string;
|
||||
precomputedDiff?: AdvancedDiffSuccess;
|
||||
allDiffs?: Map<string, AdvancedDiffSuccess>;
|
||||
planContent?: string;
|
||||
planFilePath?: string;
|
||||
toolCallId?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get a human-readable header for file edit tools
|
||||
*/
|
||||
function getFileEditHeader(toolName: string, toolArgs: string): string {
|
||||
const t = toolName.toLowerCase();
|
||||
|
||||
try {
|
||||
const args = JSON.parse(toolArgs);
|
||||
|
||||
// Handle patch tools
|
||||
if (t === "apply_patch" || t === "applypatch") {
|
||||
if (args.input) {
|
||||
const operations = parsePatchOperations(args.input);
|
||||
if (operations.length > 1) {
|
||||
return `Apply patch to ${operations.length} files?`;
|
||||
} else if (operations.length === 1) {
|
||||
const op = operations[0];
|
||||
if (op) {
|
||||
const { relative } = require("node:path");
|
||||
const cwd = process.cwd();
|
||||
const relPath = relative(cwd, op.path);
|
||||
const displayPath = relPath.startsWith("..") ? op.path : relPath;
|
||||
|
||||
if (op.kind === "add") return `Write to ${displayPath}?`;
|
||||
if (op.kind === "update") return `Update ${displayPath}?`;
|
||||
if (op.kind === "delete") return `Delete ${displayPath}?`;
|
||||
}
|
||||
}
|
||||
}
|
||||
return "Apply patch?";
|
||||
}
|
||||
|
||||
// Handle single-file edit/write tools
|
||||
const filePath = args.file_path || "";
|
||||
const { relative } = require("node:path");
|
||||
const cwd = process.cwd();
|
||||
const relPath = relative(cwd, filePath);
|
||||
const displayPath = relPath.startsWith("..") ? filePath : relPath;
|
||||
|
||||
if (
|
||||
t === "write" ||
|
||||
t === "write_file" ||
|
||||
t === "writefile" ||
|
||||
t === "write_file_gemini" ||
|
||||
t === "writefilegemini"
|
||||
) {
|
||||
const { existsSync } = require("node:fs");
|
||||
try {
|
||||
if (existsSync(filePath)) {
|
||||
return `Overwrite ${displayPath}?`;
|
||||
}
|
||||
} catch {
|
||||
// Ignore
|
||||
}
|
||||
return `Write to ${displayPath}?`;
|
||||
}
|
||||
|
||||
if (
|
||||
t === "edit" ||
|
||||
t === "str_replace_editor" ||
|
||||
t === "str_replace_based_edit_tool"
|
||||
) {
|
||||
return `Update ${displayPath}?`;
|
||||
}
|
||||
|
||||
if (t === "multi_edit" || t === "multiedit") {
|
||||
return `Apply edits to ${displayPath}?`;
|
||||
}
|
||||
} catch {
|
||||
// Fall through
|
||||
}
|
||||
|
||||
return `${toolName} requires approval`;
|
||||
}
|
||||
|
||||
/**
|
||||
* ApprovalPreview - Renders the preview content for an eagerly-committed approval
|
||||
*
|
||||
* This component renders the "preview" part of an approval that was committed
|
||||
* early to enable flicker-free approval UI. It ensures visual parity with
|
||||
* what the inline approval components show.
|
||||
*/
|
||||
export const ApprovalPreview = memo(
|
||||
({
|
||||
toolName,
|
||||
toolArgs,
|
||||
precomputedDiff,
|
||||
allDiffs,
|
||||
planContent,
|
||||
toolCallId,
|
||||
}: Props) => {
|
||||
const columns = useTerminalWidth();
|
||||
const solidLine = SOLID_LINE.repeat(Math.max(columns - 2, 10));
|
||||
const dottedLine = DOTTED_LINE.repeat(Math.max(columns - 2, 10));
|
||||
|
||||
// ExitPlanMode: Use PlanPreview component
|
||||
if (toolName === "ExitPlanMode" && planContent) {
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
<PlanPreview plan={planContent} />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
// Bash/Shell: Use BashPreview component
|
||||
if (
|
||||
toolName === "Bash" ||
|
||||
toolName === "shell" ||
|
||||
toolName === "Shell" ||
|
||||
toolName === "shell_command"
|
||||
) {
|
||||
try {
|
||||
const args = JSON.parse(toolArgs);
|
||||
const command =
|
||||
typeof args.command === "string"
|
||||
? args.command
|
||||
: Array.isArray(args.command)
|
||||
? args.command.join(" ")
|
||||
: "";
|
||||
const description = args.description || args.justification || "";
|
||||
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
<BashPreview command={command} description={description} />
|
||||
</Box>
|
||||
);
|
||||
} catch {
|
||||
// Fall through to generic
|
||||
}
|
||||
}
|
||||
|
||||
// File Edit tools: Render diff preview
|
||||
if (
|
||||
toolName === "Edit" ||
|
||||
toolName === "MultiEdit" ||
|
||||
toolName === "Write" ||
|
||||
toolName === "str_replace_editor" ||
|
||||
toolName === "str_replace_based_edit_tool" ||
|
||||
toolName === "apply_patch" ||
|
||||
toolName === "ApplyPatch"
|
||||
) {
|
||||
const headerText = getFileEditHeader(toolName, toolArgs);
|
||||
|
||||
try {
|
||||
const args = JSON.parse(toolArgs);
|
||||
|
||||
// Handle patch tools (can have multiple files)
|
||||
if (
|
||||
args.input &&
|
||||
(toolName === "apply_patch" || toolName === "ApplyPatch")
|
||||
) {
|
||||
const operations = parsePatchOperations(args.input);
|
||||
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
<Text dimColor>{solidLine}</Text>
|
||||
<Text bold color={colors.approval.header}>
|
||||
{headerText}
|
||||
</Text>
|
||||
<Text dimColor>{dottedLine}</Text>
|
||||
|
||||
<Box flexDirection="column">
|
||||
{operations.map((op, idx) => {
|
||||
const { relative } = require("node:path");
|
||||
const cwd = process.cwd();
|
||||
const relPath = relative(cwd, op.path);
|
||||
const displayPath = relPath.startsWith("..")
|
||||
? op.path
|
||||
: relPath;
|
||||
|
||||
const diffKey = toolCallId
|
||||
? `${toolCallId}:${op.path}`
|
||||
: undefined;
|
||||
const opDiff =
|
||||
diffKey && allDiffs ? allDiffs.get(diffKey) : undefined;
|
||||
|
||||
if (op.kind === "add") {
|
||||
return (
|
||||
<Box key={`patch-add-${op.path}`} flexDirection="column">
|
||||
{idx > 0 && <Box height={1} />}
|
||||
<Text dimColor>{displayPath}</Text>
|
||||
<AdvancedDiffRenderer
|
||||
precomputed={opDiff}
|
||||
kind="write"
|
||||
filePath={op.path}
|
||||
content={op.content}
|
||||
showHeader={false}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
if (op.kind === "update") {
|
||||
return (
|
||||
<Box
|
||||
key={`patch-update-${op.path}`}
|
||||
flexDirection="column"
|
||||
>
|
||||
{idx > 0 && <Box height={1} />}
|
||||
<Text dimColor>{displayPath}</Text>
|
||||
<AdvancedDiffRenderer
|
||||
precomputed={opDiff}
|
||||
kind="edit"
|
||||
filePath={op.path}
|
||||
oldString={op.oldString}
|
||||
newString={op.newString}
|
||||
showHeader={false}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
if (op.kind === "delete") {
|
||||
return (
|
||||
<Box key={`patch-delete-${op.path}`}>
|
||||
{idx > 0 && <Box height={1} />}
|
||||
<Text>
|
||||
Delete <Text bold>{displayPath}</Text>
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
})}
|
||||
</Box>
|
||||
|
||||
<Text dimColor>{dottedLine}</Text>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
// Single file edit/write
|
||||
const filePath = args.file_path || "";
|
||||
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
<Text dimColor>{solidLine}</Text>
|
||||
<Text bold color={colors.approval.header}>
|
||||
{headerText}
|
||||
</Text>
|
||||
<Text dimColor>{dottedLine}</Text>
|
||||
|
||||
{/* Write */}
|
||||
{args.content !== undefined && (
|
||||
<AdvancedDiffRenderer
|
||||
precomputed={precomputedDiff}
|
||||
kind="write"
|
||||
filePath={filePath}
|
||||
content={args.content}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Multi-edit */}
|
||||
{args.edits && Array.isArray(args.edits) && (
|
||||
<AdvancedDiffRenderer
|
||||
precomputed={precomputedDiff}
|
||||
kind="multi_edit"
|
||||
filePath={filePath}
|
||||
edits={args.edits.map(
|
||||
(e: { old_string?: string; new_string?: string }) => ({
|
||||
old_string: e.old_string || "",
|
||||
new_string: e.new_string || "",
|
||||
}),
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Single edit */}
|
||||
{args.old_string !== undefined && !args.edits && (
|
||||
<AdvancedDiffRenderer
|
||||
precomputed={precomputedDiff}
|
||||
kind="edit"
|
||||
filePath={filePath}
|
||||
oldString={args.old_string || ""}
|
||||
newString={args.new_string || ""}
|
||||
replaceAll={args.replace_all}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Text dimColor>{dottedLine}</Text>
|
||||
</Box>
|
||||
);
|
||||
} catch {
|
||||
// Fall through to generic
|
||||
}
|
||||
}
|
||||
|
||||
// Generic fallback
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
<Text dimColor>{solidLine}</Text>
|
||||
<Text bold color={colors.approval.header}>
|
||||
{toolName} requires approval
|
||||
</Text>
|
||||
<Text dimColor>{dottedLine}</Text>
|
||||
</Box>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
ApprovalPreview.displayName = "ApprovalPreview";
|
||||
199
src/cli/components/StaticPlanApproval.tsx
Normal file
199
src/cli/components/StaticPlanApproval.tsx
Normal file
@@ -0,0 +1,199 @@
|
||||
import { Box, Text, useInput } from "ink";
|
||||
import { memo, useState } from "react";
|
||||
import { useTerminalWidth } from "../hooks/useTerminalWidth";
|
||||
import { colors } from "./colors";
|
||||
|
||||
type Props = {
|
||||
onApprove: () => void;
|
||||
onApproveAndAcceptEdits: () => void;
|
||||
onKeepPlanning: (reason: string) => void;
|
||||
isFocused?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* StaticPlanApproval - Options-only plan approval component
|
||||
*
|
||||
* This component renders ONLY the approval options (no plan preview).
|
||||
* The plan preview is committed separately to the Static area via the
|
||||
* eager commit pattern, which keeps this component small (~8 lines)
|
||||
* and flicker-free.
|
||||
*
|
||||
* The plan prop was removed because the plan is rendered in the Static
|
||||
* area by ApprovalPreview, not here.
|
||||
*/
|
||||
export const StaticPlanApproval = memo(
|
||||
({
|
||||
onApprove,
|
||||
onApproveAndAcceptEdits,
|
||||
onKeepPlanning,
|
||||
isFocused = true,
|
||||
}: Props) => {
|
||||
const [selectedOption, setSelectedOption] = useState(0);
|
||||
const [customReason, setCustomReason] = useState("");
|
||||
const columns = useTerminalWidth();
|
||||
|
||||
const customOptionIndex = 2;
|
||||
const maxOptionIndex = customOptionIndex;
|
||||
const isOnCustomOption = selectedOption === customOptionIndex;
|
||||
const customOptionPlaceholder =
|
||||
"Type here to tell Letta Code what to change";
|
||||
|
||||
useInput(
|
||||
(input, key) => {
|
||||
if (!isFocused) return;
|
||||
|
||||
// CTRL-C: keep planning with cancel message
|
||||
if (key.ctrl && input === "c") {
|
||||
onKeepPlanning("User pressed CTRL-C to cancel");
|
||||
return;
|
||||
}
|
||||
|
||||
// Arrow navigation always works
|
||||
if (key.upArrow) {
|
||||
setSelectedOption((prev) => Math.max(0, prev - 1));
|
||||
return;
|
||||
}
|
||||
if (key.downArrow) {
|
||||
setSelectedOption((prev) => Math.min(maxOptionIndex, prev + 1));
|
||||
return;
|
||||
}
|
||||
|
||||
// When on custom input option
|
||||
if (isOnCustomOption) {
|
||||
if (key.return) {
|
||||
if (customReason.trim()) {
|
||||
onKeepPlanning(customReason.trim());
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (key.escape) {
|
||||
if (customReason) {
|
||||
setCustomReason("");
|
||||
} else {
|
||||
onKeepPlanning("User cancelled");
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (key.backspace || key.delete) {
|
||||
setCustomReason((prev) => prev.slice(0, -1));
|
||||
return;
|
||||
}
|
||||
if (input && !key.ctrl && !key.meta && input.length === 1) {
|
||||
setCustomReason((prev) => prev + input);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// When on regular options
|
||||
if (key.return) {
|
||||
if (selectedOption === 0) {
|
||||
onApproveAndAcceptEdits();
|
||||
} else if (selectedOption === 1) {
|
||||
onApprove();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (key.escape) {
|
||||
onKeepPlanning("User cancelled");
|
||||
}
|
||||
},
|
||||
{ isActive: isFocused },
|
||||
);
|
||||
|
||||
// Hint text based on state
|
||||
const hintText = isOnCustomOption
|
||||
? customReason
|
||||
? "Enter to submit · Esc to clear"
|
||||
: "Type feedback · Esc to cancel"
|
||||
: "Enter to select · Esc to cancel";
|
||||
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
{/* Question */}
|
||||
<Box>
|
||||
<Text>Would you like to proceed?</Text>
|
||||
</Box>
|
||||
|
||||
{/* Options */}
|
||||
<Box marginTop={1} flexDirection="column">
|
||||
{/* Option 1: Yes, and auto-accept edits */}
|
||||
<Box flexDirection="row">
|
||||
<Box width={5} flexShrink={0}>
|
||||
<Text
|
||||
color={
|
||||
selectedOption === 0 ? colors.approval.header : undefined
|
||||
}
|
||||
>
|
||||
{selectedOption === 0 ? "❯" : " "} 1.
|
||||
</Text>
|
||||
</Box>
|
||||
<Box flexGrow={1} width={Math.max(0, columns - 5)}>
|
||||
<Text
|
||||
wrap="wrap"
|
||||
color={
|
||||
selectedOption === 0 ? colors.approval.header : undefined
|
||||
}
|
||||
>
|
||||
Yes, and auto-accept edits
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Option 2: Yes, and manually approve edits */}
|
||||
<Box flexDirection="row">
|
||||
<Box width={5} flexShrink={0}>
|
||||
<Text
|
||||
color={
|
||||
selectedOption === 1 ? colors.approval.header : undefined
|
||||
}
|
||||
>
|
||||
{selectedOption === 1 ? "❯" : " "} 2.
|
||||
</Text>
|
||||
</Box>
|
||||
<Box flexGrow={1} width={Math.max(0, columns - 5)}>
|
||||
<Text
|
||||
wrap="wrap"
|
||||
color={
|
||||
selectedOption === 1 ? colors.approval.header : undefined
|
||||
}
|
||||
>
|
||||
Yes, and manually approve edits
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Option 3: Custom input */}
|
||||
<Box flexDirection="row">
|
||||
<Box width={5} flexShrink={0}>
|
||||
<Text
|
||||
color={isOnCustomOption ? colors.approval.header : undefined}
|
||||
>
|
||||
{isOnCustomOption ? "❯" : " "} 3.
|
||||
</Text>
|
||||
</Box>
|
||||
<Box flexGrow={1} width={Math.max(0, columns - 5)}>
|
||||
{customReason ? (
|
||||
<Text wrap="wrap">
|
||||
{customReason}
|
||||
{isOnCustomOption && "█"}
|
||||
</Text>
|
||||
) : (
|
||||
<Text wrap="wrap" dimColor>
|
||||
{customOptionPlaceholder}
|
||||
{isOnCustomOption && "█"}
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Hint */}
|
||||
<Box marginTop={1}>
|
||||
<Text dimColor>{hintText}</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
StaticPlanApproval.displayName = "StaticPlanApproval";
|
||||
@@ -1,4 +1,5 @@
|
||||
import { existsSync, readFileSync } from "node:fs";
|
||||
// existsSync, readFileSync removed - no longer needed since plan content
|
||||
// is shown via StaticPlanApproval during approval, not in tool result
|
||||
import { Box, Text } from "ink";
|
||||
import { memo } from "react";
|
||||
import { INTERRUPTED_BY_USER } from "../../constants";
|
||||
@@ -324,40 +325,25 @@ export const ToolCallMessage = memo(
|
||||
// Fall through to regular handling if parsing fails
|
||||
}
|
||||
|
||||
// Check if this is ExitPlanMode - show plan content (faded) instead of simple message
|
||||
// Check if this is ExitPlanMode - just show path, not plan content
|
||||
// The plan content was already shown during approval via StaticPlanApproval
|
||||
// (rendered via Ink's <Static> and is visible in terminal scrollback)
|
||||
if (rawName === "ExitPlanMode" && line.resultOk !== false) {
|
||||
// Read plan file path from ref (captured before plan mode was exited)
|
||||
const planFilePath = lastPlanFilePath;
|
||||
let planContent = "";
|
||||
|
||||
if (planFilePath && existsSync(planFilePath)) {
|
||||
try {
|
||||
planContent = readFileSync(planFilePath, "utf-8");
|
||||
} catch {
|
||||
// Fall through to default
|
||||
}
|
||||
}
|
||||
|
||||
if (planContent) {
|
||||
if (planFilePath) {
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
{/* Plan file path */}
|
||||
<Box flexDirection="row">
|
||||
<Box width={prefixWidth} flexShrink={0}>
|
||||
<Text>{prefix}</Text>
|
||||
</Box>
|
||||
<Box flexGrow={1} width={contentWidth}>
|
||||
<Text dimColor>Plan saved to: {planFilePath}</Text>
|
||||
</Box>
|
||||
<Box flexDirection="row">
|
||||
<Box width={prefixWidth} flexShrink={0}>
|
||||
<Text>{prefix}</Text>
|
||||
</Box>
|
||||
{/* Plan content (faded) - indent to align with content column */}
|
||||
<Box paddingLeft={prefixWidth}>
|
||||
<MarkdownDisplay text={planContent} dimColor={true} />
|
||||
<Box flexGrow={1} width={contentWidth}>
|
||||
<Text dimColor>Plan saved to: {planFilePath}</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
// Fall through to default if no plan content
|
||||
// Fall through to default if no plan path
|
||||
}
|
||||
|
||||
// Check if this is a file edit tool - show diff instead of success message
|
||||
|
||||
45
src/cli/components/previews/BashPreview.tsx
Normal file
45
src/cli/components/previews/BashPreview.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
import { Box, Text } from "ink";
|
||||
import { memo } from "react";
|
||||
import { useTerminalWidth } from "../../hooks/useTerminalWidth";
|
||||
import { colors } from "../colors";
|
||||
|
||||
const SOLID_LINE = "─";
|
||||
|
||||
type Props = {
|
||||
command: string;
|
||||
description?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* BashPreview - Renders the bash command preview (no interactive options)
|
||||
*
|
||||
* Used by:
|
||||
* - InlineBashApproval for memoized content
|
||||
* - Static area for eagerly-committed command previews
|
||||
*/
|
||||
export const BashPreview = memo(({ command, description }: Props) => {
|
||||
const columns = useTerminalWidth();
|
||||
const solidLine = SOLID_LINE.repeat(Math.max(columns - 2, 10));
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Top solid line */}
|
||||
<Text dimColor>{solidLine}</Text>
|
||||
|
||||
{/* Header */}
|
||||
<Text bold color={colors.approval.header}>
|
||||
Run this command?
|
||||
</Text>
|
||||
|
||||
<Box height={1} />
|
||||
|
||||
{/* Command preview */}
|
||||
<Box paddingLeft={2} flexDirection="column">
|
||||
<Text>{command}</Text>
|
||||
{description && <Text dimColor>{description}</Text>}
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
BashPreview.displayName = "BashPreview";
|
||||
48
src/cli/components/previews/PlanPreview.tsx
Normal file
48
src/cli/components/previews/PlanPreview.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
import { Text } from "ink";
|
||||
import { memo } from "react";
|
||||
import { useTerminalWidth } from "../../hooks/useTerminalWidth";
|
||||
import { colors } from "../colors";
|
||||
import { MarkdownDisplay } from "../MarkdownDisplay";
|
||||
|
||||
const SOLID_LINE = "─";
|
||||
const DOTTED_LINE = "╌";
|
||||
|
||||
type Props = {
|
||||
plan: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* PlanPreview - Renders the plan content preview (no interactive options)
|
||||
*
|
||||
* Used by:
|
||||
* - InlinePlanApproval/StaticPlanApproval for memoized content
|
||||
* - Static area for eagerly-committed plan previews
|
||||
*/
|
||||
export const PlanPreview = memo(({ plan }: Props) => {
|
||||
const columns = useTerminalWidth();
|
||||
const solidLine = SOLID_LINE.repeat(Math.max(columns - 2, 10));
|
||||
const dottedLine = DOTTED_LINE.repeat(Math.max(columns - 2, 10));
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Top solid line */}
|
||||
<Text dimColor>{solidLine}</Text>
|
||||
|
||||
{/* Header */}
|
||||
<Text bold color={colors.approval.header}>
|
||||
Ready to code? Here is your plan:
|
||||
</Text>
|
||||
|
||||
{/* Dotted separator before plan content */}
|
||||
<Text dimColor>{dottedLine}</Text>
|
||||
|
||||
{/* Plan content */}
|
||||
<MarkdownDisplay text={plan} />
|
||||
|
||||
{/* Dotted separator after plan content */}
|
||||
<Text dimColor>{dottedLine}</Text>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
PlanPreview.displayName = "PlanPreview";
|
||||
Reference in New Issue
Block a user