Skip to content
Open
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
27 changes: 21 additions & 6 deletions components/VercelChat/MessageParts.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {
ToolUIPart,
UIMessage,
isToolOrDynamicToolUIPart,
getToolOrDynamicToolName,
UIMessagePart,
UIDataTypes,
UITools,
Expand All @@ -11,6 +12,8 @@ import { cn } from "@/lib/utils";
import ViewingMessage from "./ViewingMessage";
import EditingMessage from "./EditingMessage";
import { getToolCallComponent, getToolResultComponent } from "./ToolComponents";
import { ToolCall } from "./tools/agent/ToolCall";
import { isAgentToolName } from "./tools/agent/renderTool";
import MessageFileViewer from "./message-file-viewer";
import { EnhancedReasoning } from "@/components/reasoning/EnhancedReasoning";
import { Actions, Action } from "@/components/actions";
Expand Down Expand Up @@ -71,7 +74,7 @@ export function MessageParts({ message, mode, setMode }: MessagePartsProps) {
{
"justify-start": message.role === "assistant",
"justify-end": message.role === "user",
}
},
)}
>
{message.role === "user" && (
Expand Down Expand Up @@ -106,14 +109,26 @@ export function MessageParts({ message, mode, setMode }: MessagePartsProps) {
}

if (isToolOrDynamicToolUIPart(part)) {
const { state } = part as ToolUIPart;
if (state !== "output-available") {
return getToolCallComponent(part as ToolUIPart);
const toolPart = part as ToolUIPart;
if (isAgentToolName(getToolOrDynamicToolName(toolPart))) {
return (
<ToolCall
key={key}
part={toolPart}
isStreaming={
status === "streaming" &&
partIndex === message.parts.length - 1
}
/>
Comment on lines +115 to +122

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Wire approval buttons before rendering them

When an agent tool enters approval-requested, the new ToolLayout renders Approve/Deny buttons, but this call site never supplies onApprove or onDeny (and the chat context does not expose a tool-result submission handler), so clicking either button is a no-op. This blocks workflows that request approval for bash/write/edit/etc. because the user sees controls but cannot resume the tool call.

Useful? React with 👍 / 👎.

);
}
if (toolPart.state !== "output-available") {
return getToolCallComponent(toolPart);
} else {
return getToolResultComponent(part as ToolUIPart);
return getToolResultComponent(toolPart);
}
}
}
},
)}
</div>
);
Expand Down
36 changes: 36 additions & 0 deletions components/VercelChat/tools/agent/ApprovalButtons.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
"use client";

import { Button } from "@/components/ui/button";

export type ApprovalButtonsProps = {
approvalId: string;
onApprove?: (id: string) => void;
onDeny?: (id: string, reason?: string) => void;
};

export function ApprovalButtons({
approvalId,
onApprove,
onDeny,
}: ApprovalButtonsProps) {
return (
<div className="mt-3 flex items-center gap-2 pl-5">
<Button
size="sm"
variant="outline"
className="h-7 border-green-600 text-green-600 hover:bg-green-600 hover:text-white"
onClick={() => onApprove?.(approvalId)}
>
Approve
</Button>
<Button
size="sm"
variant="outline"
className="h-7 border-red-600 text-red-600 hover:bg-red-600 hover:text-white"
onClick={() => onDeny?.(approvalId)}
>
Deny
</Button>
</div>
);
}
84 changes: 84 additions & 0 deletions components/VercelChat/tools/agent/AskUserQuestionRenderer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
"use client";

import { MessageCircleQuestion } from "lucide-react";
import { ToolLayout } from "./ToolLayout";
import type { ToolRendererProps } from "./renderTool";

type Question = { question?: string };
type AskInput = { questions?: Question[] };
type AskOutput = {
declined?: boolean;
answers?: Record<string, string | string[]> | null;
};

export function AskUserQuestionRenderer({ part, state }: ToolRendererProps) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: AskUserQuestionRenderer does not destructure or forward onApprove/onDeny to ToolLayout. In an approval-pending state, the rendered approval buttons will be non-functional.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At components/VercelChat/tools/agent/AskUserQuestionRenderer.tsx, line 14:

<comment>`AskUserQuestionRenderer` does not destructure or forward `onApprove`/`onDeny` to `ToolLayout`. In an approval-pending state, the rendered approval buttons will be non-functional.</comment>

<file context>
@@ -0,0 +1,84 @@
+  answers?: Record<string, string | string[]> | null;
+};
+
+export function AskUserQuestionRenderer({ part, state }: ToolRendererProps) {
+  const input = part.input as AskInput | undefined;
+  const output =
</file context>

const input = part.input as AskInput | undefined;
const output =
part.state === "output-available"
? (part.output as AskOutput | undefined)
: undefined;
const questions = input?.questions ?? [];

const isWaitingForInput = part.state === "input-available";
const isStreaming = part.state === "input-streaming";
const hasOutput = part.state === "output-available";
const isDeclined = hasOutput && output?.declined === true;
const hasAnswers = hasOutput && output?.answers != null;

const summary = isStreaming
? "Generating questions"
: isWaitingForInput
? "Waiting for user input"
: isDeclined
? "User declined to answer"
: hasAnswers
? "Answered"
: state.denied
? "Cancelled"
: "Questions";

const questionCount = questions.length;
const meta =
questionCount > 0
? `${questionCount} question${questionCount === 1 ? "" : "s"}`
: undefined;

const answers = output?.answers;
const expandedContent =
hasAnswers && answers ? (
<div className="space-y-2">
{questions.map((q) => {
if (!q?.question) return null;
const answer = answers[q.question];
const answerStr = Array.isArray(answer)
? answer.join(", ")
: (answer ?? "(not answered)");
return (
<div key={q.question} className="space-y-0.5">
<p className="text-sm text-foreground">{q.question}</p>
<p className="text-sm text-muted-foreground">
<span className="text-green-500">&rarr;</span> {answerStr}
</p>
</div>
);
})}
</div>
) : undefined;

const displayState = isWaitingForInput
? { ...state, interrupted: false }
: state;

return (
<ToolLayout
name="Ask user"
summary={summary}
meta={meta}
state={displayState}
icon={<MessageCircleQuestion className="h-3.5 w-3.5" />}
nameClassName={state.denied || isDeclined ? "text-red-500" : undefined}
expandedContent={expandedContent}
defaultExpanded={false}
/>
);
Comment on lines +14 to +83

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Pass approval callbacks through this renderer to preserve approval workflow behavior.

Line 14 and Line 73 omit onApprove/onDeny. In approval-pending states, this renderer would render without actionable approve/deny handlers.

Suggested fix
-export function AskUserQuestionRenderer({ part, state }: ToolRendererProps) {
+export function AskUserQuestionRenderer({
+  part,
+  state,
+  onApprove,
+  onDeny,
+}: ToolRendererProps) {
@@
   return (
     <ToolLayout
       name="Ask user"
       summary={summary}
       meta={meta}
       state={displayState}
       icon={<MessageCircleQuestion className="h-3.5 w-3.5" />}
       nameClassName={state.denied || isDeclined ? "text-red-500" : undefined}
       expandedContent={expandedContent}
       defaultExpanded={false}
+      onApprove={onApprove}
+      onDeny={onDeny}
     />
   );
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export function AskUserQuestionRenderer({ part, state }: ToolRendererProps) {
const input = part.input as AskInput | undefined;
const output =
part.state === "output-available"
? (part.output as AskOutput | undefined)
: undefined;
const questions = input?.questions ?? [];
const isWaitingForInput = part.state === "input-available";
const isStreaming = part.state === "input-streaming";
const hasOutput = part.state === "output-available";
const isDeclined = hasOutput && output?.declined === true;
const hasAnswers = hasOutput && output?.answers != null;
const summary = isStreaming
? "Generating questions"
: isWaitingForInput
? "Waiting for user input"
: isDeclined
? "User declined to answer"
: hasAnswers
? "Answered"
: state.denied
? "Cancelled"
: "Questions";
const questionCount = questions.length;
const meta =
questionCount > 0
? `${questionCount} question${questionCount === 1 ? "" : "s"}`
: undefined;
const answers = output?.answers;
const expandedContent =
hasAnswers && answers ? (
<div className="space-y-2">
{questions.map((q) => {
if (!q?.question) return null;
const answer = answers[q.question];
const answerStr = Array.isArray(answer)
? answer.join(", ")
: (answer ?? "(not answered)");
return (
<div key={q.question} className="space-y-0.5">
<p className="text-sm text-foreground">{q.question}</p>
<p className="text-sm text-muted-foreground">
<span className="text-green-500">&rarr;</span> {answerStr}
</p>
</div>
);
})}
</div>
) : undefined;
const displayState = isWaitingForInput
? { ...state, interrupted: false }
: state;
return (
<ToolLayout
name="Ask user"
summary={summary}
meta={meta}
state={displayState}
icon={<MessageCircleQuestion className="h-3.5 w-3.5" />}
nameClassName={state.denied || isDeclined ? "text-red-500" : undefined}
expandedContent={expandedContent}
defaultExpanded={false}
/>
);
export function AskUserQuestionRenderer({
part,
state,
onApprove,
onDeny,
}: ToolRendererProps) {
const input = part.input as AskInput | undefined;
const output =
part.state === "output-available"
? (part.output as AskOutput | undefined)
: undefined;
const questions = input?.questions ?? [];
const isWaitingForInput = part.state === "input-available";
const isStreaming = part.state === "input-streaming";
const hasOutput = part.state === "output-available";
const isDeclined = hasOutput && output?.declined === true;
const hasAnswers = hasOutput && output?.answers != null;
const summary = isStreaming
? "Generating questions"
: isWaitingForInput
? "Waiting for user input"
: isDeclined
? "User declined to answer"
: hasAnswers
? "Answered"
: state.denied
? "Cancelled"
: "Questions";
const questionCount = questions.length;
const meta =
questionCount > 0
? `${questionCount} question${questionCount === 1 ? "" : "s"}`
: undefined;
const answers = output?.answers;
const expandedContent =
hasAnswers && answers ? (
<div className="space-y-2">
{questions.map((q) => {
if (!q?.question) return null;
const answer = answers[q.question];
const answerStr = Array.isArray(answer)
? answer.join(", ")
: (answer ?? "(not answered)");
return (
<div key={q.question} className="space-y-0.5">
<p className="text-sm text-foreground">{q.question}</p>
<p className="text-sm text-muted-foreground">
<span className="text-green-500">&rarr;</span> {answerStr}
</p>
</div>
);
})}
</div>
) : undefined;
const displayState = isWaitingForInput
? { ...state, interrupted: false }
: state;
return (
<ToolLayout
name="Ask user"
summary={summary}
meta={meta}
state={displayState}
icon={<MessageCircleQuestion className="h-3.5 w-3.5" />}
nameClassName={state.denied || isDeclined ? "text-red-500" : undefined}
expandedContent={expandedContent}
defaultExpanded={false}
onApprove={onApprove}
onDeny={onDeny}
/>
);
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/VercelChat/tools/agent/AskUserQuestionRenderer.tsx` around lines
14 - 83, AskUserQuestionRenderer currently fails to pass approval handlers into
the ToolLayout when rendering approval-pending states, so wire the approve/deny
callbacks from the renderer props through to ToolLayout: locate the
AskUserQuestionRenderer function (props `part`, `state` of type
ToolRendererProps) and add the onApprove and onDeny handlers (from state or
props that carry approval callbacks) to the ToolLayout call (same place where
name, summary, meta, state, icon, nameClassName, expandedContent are passed) so
approval and denial actions are preserved in the UI.

}
85 changes: 85 additions & 0 deletions components/VercelChat/tools/agent/BashRenderer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
"use client";

import { Terminal } from "lucide-react";
import { ToolLayout } from "./ToolLayout";
import type { ToolRendererProps } from "./renderTool";

type BashInput = { command?: string; cwd?: string; detached?: boolean };
type BashOutput = {
success?: boolean;
exitCode?: number | null;
stdout?: string;
stderr?: string;
};

export function BashRenderer({
part,
state,
onApprove,
onDeny,
}: ToolRendererProps) {
const input = part.input as BashInput | undefined;
const command = String(input?.command ?? "");
const isDetached = input?.detached === true;

const output =
part.state === "output-available"
? (part.output as BashOutput | undefined)
: undefined;
const exitCode = output?.exitCode;
const stdout = output?.stdout;
const stderr = output?.stderr;
const hasOutput = Boolean(stdout || stderr);
const toolFailed = output?.success === false;
const isError =
toolFailed || (typeof exitCode === "number" && exitCode !== 0);

const combinedOutput = [stdout, stderr].filter(Boolean).join("\n").trim();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Preserve raw bash output; avoid trimming terminal text.

Line 37 uses .trim(), which removes leading/trailing whitespace from stdout/stderr. That can alter meaningful command output (indentation, spacing, final newline cues) in the terminal preview.

Suggested fix
-  const combinedOutput = [stdout, stderr].filter(Boolean).join("\n").trim();
+  const combinedOutput = [stdout, stderr]
+    .filter((chunk): chunk is string => typeof chunk === "string")
+    .join("\n");

Also applies to: 60-61, 66-67

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/VercelChat/tools/agent/BashRenderer.tsx` at line 37, The code
currently constructs combined bash output using .trim(), which strips
leading/trailing whitespace and final newlines; update the logic in
BashRenderer.tsx (the combinedOutput creation and the similar occurrences around
lines where combinedOutput is built/used) to stop calling .trim() so
stdout/stderr are preserved exactly (keep the .filter(Boolean) and .join("\n")
but remove the trailing .trim()). Apply the same change to the other occurrences
referenced (the blocks around lines 60-61 and 66-67) so all terminal previews
preserve raw output including indentation and final newline cues.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: .trim() on combined stdout/stderr can strip meaningful leading/trailing whitespace from command output (e.g., indentation or newline cues). Consider removing the trim to preserve raw terminal output fidelity.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At components/VercelChat/tools/agent/BashRenderer.tsx, line 37:

<comment>`.trim()` on combined stdout/stderr can strip meaningful leading/trailing whitespace from command output (e.g., indentation or newline cues). Consider removing the trim to preserve raw terminal output fidelity.</comment>

<file context>
@@ -0,0 +1,85 @@
+  const isError =
+    toolFailed || (typeof exitCode === "number" && exitCode !== 0);
+
+  const combinedOutput = [stdout, stderr].filter(Boolean).join("\n").trim();
+  const hasExpandableContent = part.state === "output-available" || isDetached;
+
</file context>

const hasExpandableContent = part.state === "output-available" || isDetached;

// When bash errors, route through ToolLayout's standard error UI.
const mergedState =
isError && !state.error
? { ...state, error: `Exit code ${exitCode ?? "unknown"}` }
: state;

const meta = isDetached ? (
<span className="rounded bg-blue-500/15 px-1.5 py-0.5 text-[11px] font-medium text-blue-500">
detached
</span>
) : undefined;

const errorMetaContent =
isError && exitCode !== undefined && exitCode !== null
? `exit ${exitCode}`
: undefined;

const expandedContent = hasExpandableContent ? (
isError ? (
hasOutput ? (
<pre className="max-h-64 overflow-auto whitespace-pre-wrap rounded-md border border-red-500/20 bg-red-500/5 px-3 py-2 font-mono text-xs leading-relaxed text-red-400">
{combinedOutput}
</pre>
) : undefined
) : (
<pre className="max-h-64 overflow-auto whitespace-pre-wrap rounded-md border border-border bg-muted/50 p-3 font-mono text-xs leading-relaxed text-muted-foreground">
{hasOutput ? combinedOutput : "(No output)"}
</pre>
)
) : undefined;

return (
<ToolLayout
name="Bash"
summary={command || "..."}
summaryClassName="font-mono"
meta={meta}
errorMeta={errorMetaContent}
state={mergedState}
icon={<Terminal className="h-3.5 w-3.5" />}
expandedContent={expandedContent}
onApprove={onApprove}
onDeny={onDeny}
/>
);
}
119 changes: 119 additions & 0 deletions components/VercelChat/tools/agent/EditRenderer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
"use client";

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Custom agent: Code Structure and Size Limits for Readability and Single Responsibility

Newly added EditRenderer.tsx exceeds the 100-line file size limit (119 lines).

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At components/VercelChat/tools/agent/EditRenderer.tsx, line 1:

<comment>Newly added EditRenderer.tsx exceeds the 100-line file size limit (119 lines).</comment>

<file context>
@@ -0,0 +1,119 @@
+"use client";
+
+import { Pencil } from "lucide-react";
</file context>


import { Pencil } from "lucide-react";
import { useMemo } from "react";
import { ToolLayout } from "./ToolLayout";
import { FileNamePill } from "./FileNamePill";
import type { ToolRendererProps } from "./renderTool";

type EditInput = { filePath?: string; oldString?: string; newString?: string };
type EditOutput = { success?: boolean; error?: string };

export function EditRenderer({
part,
state,
onApprove,
onDeny,
}: ToolRendererProps) {
const input = part.input as EditInput | undefined;
const filePath = input?.filePath ?? "...";
const oldString = input?.oldString ?? "";
const newString = input?.newString ?? "";

const { additions, removals } = useMemo(() => {
const oldLines = oldString.split("\n");
const newLines = newString.split("\n");

const oldCounts = new Map<string, number>();
for (const line of oldLines) {
oldCounts.set(line, (oldCounts.get(line) ?? 0) + 1);
}
const newCounts = new Map<string, number>();
for (const line of newLines) {
newCounts.set(line, (newCounts.get(line) ?? 0) + 1);
}

let add = 0;
for (const [line, count] of newCounts) {
add += Math.max(0, count - (oldCounts.get(line) ?? 0));
}
let remove = 0;
for (const [line, count] of oldCounts) {
remove += Math.max(0, count - (newCounts.get(line) ?? 0));
}
return { additions: add, removals: remove };
}, [oldString, newString]);

const output =
part.state === "output-available"
? (part.output as EditOutput | undefined)
: undefined;
const outputError =
output?.success === false ? (output?.error ?? "Edit failed") : undefined;

const mergedState = outputError
? { ...state, error: state.error ?? outputError }
: state;

const showDiff =
mergedState.approvalRequested ||
(!mergedState.running && !mergedState.error && !mergedState.denied);

const expandedContent =
showDiff && !mergedState.denied && (oldString || newString) ? (
<div className="max-h-96 overflow-auto rounded-md border border-border bg-muted/50 p-3 font-mono text-xs leading-relaxed">
{oldString

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Edit preview marks unchanged text as removed/added, producing a misleading diff.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At components/VercelChat/tools/agent/EditRenderer.tsx, line 65:

<comment>Edit preview marks unchanged text as removed/added, producing a misleading diff.</comment>

<file context>
@@ -0,0 +1,119 @@
+  const expandedContent =
+    showDiff && !mergedState.denied && (oldString || newString) ? (
+      <div className="max-h-96 overflow-auto rounded-md border border-border bg-muted/50 p-3 font-mono text-xs leading-relaxed">
+        {oldString
+          .split("\n")
+          .filter((_, i, arr) => !(arr.length === 1 && arr[0] === ""))
</file context>

.split("\n")
.filter((_, i, arr) => !(arr.length === 1 && arr[0] === ""))
.map((line, i) => (
<div key={`old-${i}`} className="whitespace-pre-wrap text-red-400">
<span className="select-none text-red-500">- </span>
{line}
</div>
))}
Comment on lines +65 to +73

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Optimize filter logic—move outside map.

The filter condition !(arr.length === 1 && arr[0] === "") is evaluated for every line during iteration, but it checks properties of the entire array (arr). This is inefficient and confusing.

Filter the array once before mapping:

♻️ Proposed refactor
-        {oldString
-          .split("\n")
-          .filter((_, i, arr) => !(arr.length === 1 && arr[0] === ""))
-          .map((line, i) => (
+        {(() => {
+          const oldLines = oldString.split("\n");
+          if (oldLines.length === 1 && oldLines[0] === "") return null;
+          return oldLines.map((line, i) => (
            <div key={`old-${i}`} className="whitespace-pre-wrap text-red-400">
              <span className="select-none text-red-500">- </span>
              {line}
            </div>
-          ))}
+          ));
+        })()}

Apply the same pattern to the newString block on lines 74-85.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{oldString
.split("\n")
.filter((_, i, arr) => !(arr.length === 1 && arr[0] === ""))
.map((line, i) => (
<div key={`old-${i}`} className="whitespace-pre-wrap text-red-400">
<span className="select-none text-red-500">- </span>
{line}
</div>
))}
{oldString.split("\n").length === 1 && oldString.split("\n")[0] === ""
? null
: oldString.split("\n").map((line, i) => (
<div key={`old-${i}`} className="whitespace-pre-wrap text-red-400">
<span className="select-none text-red-500">- </span>
{line}
</div>
))}
Suggested change
{oldString
.split("\n")
.filter((_, i, arr) => !(arr.length === 1 && arr[0] === ""))
.map((line, i) => (
<div key={`old-${i}`} className="whitespace-pre-wrap text-red-400">
<span className="select-none text-red-500">- </span>
{line}
</div>
))}
{(() => {
const oldLines = oldString.split("\n");
if (oldLines.length === 1 && oldLines[0] === "") return null;
return oldLines.map((line, i) => (
<div key={`old-${i}`} className="whitespace-pre-wrap text-red-400">
<span className="select-none text-red-500">- </span>
{line}
</div>
));
})()}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/VercelChat/tools/agent/EditRenderer.tsx` around lines 65 - 73, The
`oldString` rendering in `EditRenderer` is doing an array-wide empty check
inside the `filter` callback for every line, which is unnecessary and hard to
read. Refactor the `oldString` block so the empty-string handling happens once
before `map`, using a precomputed filtered array or equivalent logic, and apply
the same cleanup to the matching `newString` rendering block in `EditRenderer`
so both branches share the same simpler pattern.

{newString
.split("\n")
.filter((_, i, arr) => !(arr.length === 1 && arr[0] === ""))
.map((line, i) => (
<div
key={`new-${i}`}
className="whitespace-pre-wrap text-green-400"
>
<span className="select-none text-green-500">+ </span>
{line}
</div>
))}
</div>
) : undefined;

const meta =
showDiff && !mergedState.denied ? (
<span className="inline-flex items-center gap-1.5">
<span className="text-green-500">+{additions}</span>
<span className="text-red-500">-{removals}</span>
</span>
) : undefined;

return (
<ToolLayout
name="Update"
icon={<Pencil className="h-3.5 w-3.5" />}
summary={
filePath === "..." ? (
filePath
) : (
<FileNamePill
filePath={filePath}
error={Boolean(mergedState.error)}
/>
)
}
meta={meta}
errorMeta={mergedState.error ? "failed" : undefined}
state={mergedState}
expandedContent={expandedContent}
onApprove={onApprove}
onDeny={onDeny}
/>
);
}
Loading
Loading