From 7c993284dc9893a1bdf888c530b1b293da290da3 Mon Sep 17 00:00:00 2001 From: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com> Date: Sun, 8 Feb 2026 23:45:55 -0800 Subject: [PATCH] fix: refine cli subagent tree alignment and wrapping --- cli/src/components/ChatMessage.test.tsx | 6 +- cli/src/components/SubagentMessage.tsx | 190 ++++++++++++++++++++---- 2 files changed, 162 insertions(+), 34 deletions(-) diff --git a/cli/src/components/ChatMessage.test.tsx b/cli/src/components/ChatMessage.test.tsx index 73dd68445f..639e145845 100644 --- a/cli/src/components/ChatMessage.test.tsx +++ b/cli/src/components/ChatMessage.test.tsx @@ -31,9 +31,9 @@ describe("ChatMessage subagent rendering", () => { const frame = lastFrame() || "" expect(frame).toContain("Cline wants to run subagents") - expect(frame).toContain("├─ Find codebase stats and size") - expect(frame).toContain("├─ Find funny comments and easter eggs") - expect(frame).toContain("└─ Find unusual patterns and history") + expect(frame).toContain("├─ Find codebase stats and size") + expect(frame).toContain("├─ Find funny comments and easter eggs") + expect(frame).toContain("└─ Find unusual patterns and history") }) it("renders subagent progress rows with compact token stats and completion checks", () => { diff --git a/cli/src/components/SubagentMessage.tsx b/cli/src/components/SubagentMessage.tsx index 1dab6a6696..46b0158f12 100644 --- a/cli/src/components/SubagentMessage.tsx +++ b/cli/src/components/SubagentMessage.tsx @@ -3,6 +3,7 @@ import { Box, Text } from "ink" import Spinner from "ink-spinner" import React from "react" import { COLORS } from "../constants/colors" +import { useTerminalSize } from "../hooks/useTerminalSize" import { jsonParseSafe } from "../utils/parser" interface SubagentMessageProps { @@ -11,6 +12,9 @@ interface SubagentMessageProps { mode?: "act" | "plan" } +const TREE_PREFIX_WIDTH = 5 +const MIN_PROMPT_WIDTH = 20 + const DotRow: React.FC<{ children: React.ReactNode; color?: string; flashing?: boolean }> = ({ children, color, @@ -58,9 +62,108 @@ function formatSubagentStats(entry: SubagentStatusItem): string { return `${entry.toolCalls} ${toolUses} · ${tokensUsed} tokens · ${totalCost}` } +function wrapPrompt(text: string, width: number): string[] { + if (!text) { + return [""] + } + + const normalizedWidth = Math.max(1, width) + const wrappedLines: string[] = [] + const paragraphs = text.split("\n") + + for (const paragraph of paragraphs) { + const words = paragraph.trim().split(/\s+/).filter(Boolean) + if (words.length === 0) { + wrappedLines.push("") + continue + } + + let line = "" + for (const word of words) { + if (!line) { + if (word.length <= normalizedWidth) { + line = word + continue + } + + let remaining = word + while (remaining.length > normalizedWidth) { + wrappedLines.push(remaining.slice(0, normalizedWidth)) + remaining = remaining.slice(normalizedWidth) + } + line = remaining + continue + } + + if (line.length + 1 + word.length <= normalizedWidth) { + line = `${line} ${word}` + continue + } + + wrappedLines.push(line) + + if (word.length <= normalizedWidth) { + line = word + continue + } + + let remaining = word + while (remaining.length > normalizedWidth) { + wrappedLines.push(remaining.slice(0, normalizedWidth)) + remaining = remaining.slice(normalizedWidth) + } + line = remaining + } + + if (line) { + wrappedLines.push(line) + } + } + + return wrappedLines.length > 0 ? wrappedLines : [text] +} + +const TreePromptRow: React.FC<{ + prefix: React.ReactNode + continuationPrefix: string + prompt: string + promptWidth: number + color?: string +}> = ({ prefix, continuationPrefix, prompt, promptWidth, color }) => { + const lines = wrapPrompt(prompt, promptWidth) + + return ( + + {lines.map((line, index) => ( + + + {index === 0 ? prefix : {continuationPrefix}} + + + {line} + + + ))} + + ) +} + +const TreeStatsRow: React.FC<{ prefix: string; stats: string }> = ({ prefix, stats }) => ( + + + {prefix} + + + ⎿ {stats} + + +) + export const SubagentMessage: React.FC = ({ message, mode, isStreaming }) => { const { type, ask, say, text, partial } = message const toolColor = mode === "plan" ? "yellow" : COLORS.primaryBlue + const { columns } = useTerminalSize() + const promptWidth = Math.max(MIN_PROMPT_WIDTH, columns - 2 - TREE_PREFIX_WIDTH) if ((type === "ask" && ask === "use_subagents") || say === "use_subagents") { const parsed = text @@ -90,10 +193,16 @@ export const SubagentMessage: React.FC = ({ message, mode, {prompts.map((prompt, index) => { const isLastPrompt = index === prompts.length - 1 const branch = isLastPrompt ? "└─" : "├─" + const continuationPrefix = isLastPrompt ? " " : "│ " return ( - - {branch} {prompt} - + {`${branch} `}} + prompt={prompt} + promptWidth={promptWidth} + /> ) })} @@ -132,19 +241,26 @@ export const SubagentMessage: React.FC = ({ message, mode, {items.map((entry, index) => { const isLastEntry = index === items.length - 1 - const branch = isLastEntry ? "└─ " : "├─ " - const connector = isLastEntry ? " " : "│ " + const branch = isLastEntry ? "└─" : "├─" + const continuationPrefix = isLastEntry ? " " : "│ " const key = `${entry.index}-${index}` if (entry.status === "completed") { return ( - - {branch}✓ {entry.prompt} - - - {connector}⎿ {formatSubagentStats(entry)} - + + {`${branch} `} + + + } + prompt={entry.prompt} + promptWidth={promptWidth} + /> + ) } @@ -152,32 +268,44 @@ export const SubagentMessage: React.FC = ({ message, mode, if (entry.status === "failed") { return ( - - {branch}✗ {entry.prompt} - - - {connector}⎿ {formatSubagentStats(entry)} - + + {`${branch} `} + + + } + prompt={entry.prompt} + promptWidth={promptWidth} + /> + ) } return ( - - {branch} - {entry.status === "running" ? ( - - - - ) : ( - - )} - {entry.prompt} - - - {connector}⎿ {formatSubagentStats(entry)} - + + {branch} + {entry.status === "running" ? ( + + + + ) : ( + + )} + + } + prompt={entry.prompt} + promptWidth={promptWidth} + /> + ) })}