diff --git a/gui/src/components/UnifiedTerminal/UnifiedTerminal.tsx b/gui/src/components/UnifiedTerminal/UnifiedTerminal.tsx
new file mode 100644
index 000000000..00e34114b
--- /dev/null
+++ b/gui/src/components/UnifiedTerminal/UnifiedTerminal.tsx
@@ -0,0 +1,555 @@
+import { ChevronDownIcon } from "@heroicons/react/24/outline";
+import Anser, { AnserJsonEntry } from "anser";
+import { ToolCallState } from "core";
+import { escapeCarriageReturn } from "escape-carriage";
+import { useMemo, useState } from "react";
+import styled, { keyframes } from "styled-components";
+import { useAppDispatch } from "../../redux/hooks";
+import { moveTerminalProcessToBackground } from "../../redux/thunks/moveTerminalProcessToBackground";
+import { getFontSize } from "../../util";
+import {
+ defaultBorderRadius,
+ vscBackground,
+ vscEditorBackground,
+ vscForeground,
+} from "../index";
+import { CopyButton } from "../StyledMarkdownPreview/StepContainerPreToolbar/CopyButton";
+import { RunInTerminalButton } from "../StyledMarkdownPreview/StepContainerPreToolbar/RunInTerminalButton";
+import { ButtonContent, SpoilerButton } from "../ui/SpoilerButton";
+
+const blinkCursor = keyframes`
+ 0%, 50% { opacity: 1; }
+ 51%, 100% { opacity: 0; }
+`;
+
+const BlinkingCursor = styled.span`
+ &::after {
+ content: "█";
+ animation: ${blinkCursor} 1s infinite;
+ color: ${vscForeground};
+ }
+`;
+
+const AnsiSpan = styled.span<{
+ bg?: string;
+ fg?: string;
+ decoration?: string;
+}>`
+ ${({ bg }) => bg && `background-color: rgb(${bg});`}
+ ${({ fg }) => fg && `color: rgb(${fg});`}
+ ${({ decoration }) => {
+ switch (decoration) {
+ case "bold":
+ return "font-weight: bold;";
+ case "dim":
+ return "opacity: 0.5;";
+ case "italic":
+ return "font-style: italic;";
+ case "hidden":
+ return "visibility: hidden;";
+ case "strikethrough":
+ return "text-decoration: line-through;";
+ case "underline":
+ return "text-decoration: underline;";
+ case "blink":
+ return "text-decoration: blink;";
+ default:
+ return "";
+ }
+ }}
+`;
+
+const AnsiLink = styled.a`
+ color: var(--vscode-textLink-foreground, #3794ff);
+ text-decoration: none;
+ &:hover {
+ text-decoration: underline;
+ }
+`;
+
+const StyledTerminalContainer = styled.div<{
+ fontSize?: number;
+ bgColor: string;
+}>`
+ background-color: ${(props) => props.bgColor};
+ font-family:
+ var(--vscode-font-family),
+ system-ui,
+ -apple-system,
+ BlinkMacSystemFont,
+ "Segoe UI",
+ Roboto,
+ Oxygen,
+ Ubuntu,
+ Cantarell,
+ "Open Sans",
+ "Helvetica Neue",
+ sans-serif;
+ font-size: ${(props) => props.fontSize || getFontSize()}px;
+ color: ${vscForeground};
+ line-height: 1.5;
+
+ > *:last-child {
+ margin-bottom: 0;
+ }
+`;
+
+const TerminalContent = styled.div`
+ pre {
+ white-space: pre-wrap;
+ background-color: ${vscEditorBackground};
+ border-radius: ${defaultBorderRadius};
+ border: 1px solid
+ var(--vscode-editorWidget-border, rgba(127, 127, 127, 0.3));
+ max-width: calc(100vw - 24px);
+ overflow-x: scroll;
+ overflow-y: hidden;
+ padding: 8px;
+ margin: 0;
+ }
+
+ code {
+ span.line:empty {
+ display: none;
+ }
+ word-wrap: break-word;
+ border-radius: ${defaultBorderRadius};
+ background-color: ${vscEditorBackground};
+ font-size: ${getFontSize() - 2}px;
+ font-family: var(--vscode-editor-font-family);
+ }
+
+ code:not(pre > code) {
+ font-family: var(--vscode-editor-font-family);
+ color: var(--vscode-input-placeholderForeground);
+ }
+`;
+
+const ToolbarContainer = styled.div<{ isExpanded: boolean }>`
+ font-size: ${getFontSize() - 2}px;
+`;
+
+const CommandLine = styled.div`
+ padding-bottom: 0.5rem;
+ color: var(--vscode-terminal-ansiGreen, #0dbc79);
+`;
+
+function ansiToJSON(
+ input: string,
+ use_classes: boolean = false,
+): AnserJsonEntry[] {
+ input = escapeCarriageReturn(fixBackspace(input));
+ return Anser.ansiToJson(input, {
+ json: true,
+ remove_empty: true,
+ use_classes,
+ });
+}
+
+function createClass(bundle: AnserJsonEntry): string | null {
+ let classNames: string = "";
+
+ if (bundle.bg) {
+ classNames += `${bundle.bg}-bg `;
+ }
+ if (bundle.fg) {
+ classNames += `${bundle.fg}-fg `;
+ }
+ if (bundle.decoration) {
+ classNames += `ansi-${bundle.decoration} `;
+ }
+
+ if (classNames === "") {
+ return null;
+ }
+
+ classNames = classNames.substring(0, classNames.length - 1);
+ return classNames;
+}
+
+function convertBundleIntoReact(
+ linkify: boolean,
+ useClasses: boolean,
+ bundle: AnserJsonEntry,
+ key: number,
+): JSX.Element {
+ const className = useClasses ? createClass(bundle) : null;
+ const decorationProp = bundle.decoration
+ ? String(bundle.decoration)
+ : undefined;
+
+ if (!linkify) {
+ return (
+
+ {bundle.content}
+
+ );
+ }
+
+ const content: React.ReactNode[] = [];
+ const linkRegex =
+ /(\s|^)(https?:\/\/(?:www\.|(?!www))[^\s.]+\.[^\s]{2,}|www\.[^\s]+\.[^\s]{2,})/g;
+
+ let index = 0;
+ let match: RegExpExecArray | null;
+ while ((match = linkRegex.exec(bundle.content)) !== null) {
+ const [, pre, url] = match;
+
+ const startIndex = match.index + pre.length;
+ if (startIndex > index) {
+ content.push(bundle.content.substring(index, startIndex));
+ }
+
+ const href = url.startsWith("www.") ? `http://${url}` : url;
+
+ content.push(
+
+ {url}
+ ,
+ );
+
+ index = linkRegex.lastIndex;
+ }
+
+ if (index < bundle.content.length) {
+ content.push(bundle.content.substring(index));
+ }
+
+ return (
+
+ {content}
+
+ );
+}
+
+function fixBackspace(txt: string) {
+ let tmp = txt;
+ do {
+ txt = tmp;
+ tmp = txt.replace(/[^\n]\x08/gm, "");
+ } while (tmp.length < txt.length);
+ return txt;
+}
+
+function AnsiRenderer({
+ children,
+ linkify = false,
+}: {
+ children?: string;
+ linkify?: boolean;
+}) {
+ const ansiContent = ansiToJSON(children ?? "", false).map((bundle, i) =>
+ convertBundleIntoReact(linkify, false, bundle, i),
+ );
+
+ return <>{ansiContent}>;
+}
+
+interface StatusIconProps {
+ status: "running" | "completed" | "failed" | "background";
+}
+
+function StatusIcon({ status }: StatusIconProps) {
+ const getStatusColor = () => {
+ switch (status) {
+ case "running":
+ return "bg-success";
+ case "completed":
+ return "bg-success";
+ case "background":
+ return "bg-accent";
+ case "failed":
+ return "bg-error";
+ default:
+ return "bg-success";
+ }
+ };
+
+ return (
+
+ );
+}
+
+interface IndicatorOnlyProps {
+ hiddenLinesCount: number;
+ isExpanded: boolean;
+ onToggle: () => void;
+}
+
+function IndicatorOnly({
+ hiddenLinesCount,
+ isExpanded,
+ onToggle,
+}: IndicatorOnlyProps) {
+ return (
+
+
+
+
+ {isExpanded ? "Collapse" : `+${hiddenLinesCount} more lines`}
+
+
+
+
+
+ );
+}
+
+interface CollapsibleOutputContainerProps {
+ limitedContent: string;
+ fullContent: string;
+ isExpanded: boolean;
+ onToggle: () => void;
+}
+
+function CollapsibleOutputContainer({
+ limitedContent,
+ fullContent,
+ isExpanded,
+ onToggle,
+}: CollapsibleOutputContainerProps) {
+ return (
+
+ {/* Gradient overlay when collapsed */}
+ {!isExpanded && (
+
+ )}
+
+
+
+
+ {isExpanded ? fullContent : limitedContent}
+
+
+
+
+ );
+}
+
+interface UnifiedTerminalCommandProps {
+ command: string;
+ output?: string;
+ status?: "running" | "completed" | "failed" | "background";
+ statusMessage?: string;
+ toolCallState?: ToolCallState;
+ toolCallId?: string;
+ displayLines?: number;
+}
+
+export function UnifiedTerminalCommand({
+ command,
+ output = "",
+ status = "completed",
+ statusMessage = "",
+ toolCallState,
+ toolCallId,
+ displayLines = 15,
+}: UnifiedTerminalCommandProps) {
+ const dispatch = useAppDispatch();
+ const [isExpanded, setIsExpanded] = useState(true);
+ const [outputExpanded, setOutputExpanded] = useState(false);
+
+ // Determine running state
+ const isRunning = toolCallState?.status === "calling" || status === "running";
+ const hasOutput = output.length > 0;
+
+ // Process terminal content for line limiting
+ const processedTerminalContent = useMemo(() => {
+ if (!output) {
+ return {
+ fullContent: "",
+ limitedContent: "",
+ totalLines: 0,
+ isLimited: false,
+ hiddenLinesCount: 0,
+ };
+ }
+
+ const lines = output.split("\n");
+ const totalLines = lines.length;
+
+ if (totalLines > displayLines) {
+ const lastLines = lines.slice(-displayLines);
+ return {
+ fullContent: output,
+ limitedContent: lastLines.join("\n"),
+ totalLines,
+ isLimited: true,
+ hiddenLinesCount: totalLines - displayLines,
+ };
+ }
+
+ return {
+ fullContent: output,
+ limitedContent: output,
+ totalLines,
+ isLimited: false,
+ hiddenLinesCount: 0,
+ };
+ }, [output, displayLines]);
+
+ // Determine status type
+ let statusType: "running" | "completed" | "failed" | "background" = status;
+ if (isRunning) {
+ statusType = "running";
+ } else if (statusMessage?.includes("failed")) {
+ statusType = "failed";
+ } else if (statusMessage?.includes("background")) {
+ statusType = "background";
+ }
+
+ const handleMoveToBackground = () => {
+ if (toolCallId) {
+ void dispatch(
+ moveTerminalProcessToBackground({
+ toolCallId,
+ }),
+ );
+ }
+ };
+
+ // Create combined content for copying (command + output)
+ const copyContent = useMemo(() => {
+ let content = `$ ${command}`;
+ if (hasOutput) {
+ content += `\n\n${output}`;
+ }
+ return content;
+ }, [command, output, hasOutput]);
+
+ return (
+
+
+ {/* Toolbar */}
+
+
+ setIsExpanded(!isExpanded)}
+ className={`text-lightgray h-3.5 w-3.5 flex-shrink-0 cursor-pointer hover:brightness-125 ${
+ isExpanded ? "rotate-0" : "-rotate-90"
+ }`}
+ />
+ Terminal
+
+
+
+ {!isRunning && (
+
+
+
+
+ )}
+
+
+
+ {/* Content */}
+ {isExpanded && (
+
+
+
+ {/* Command is always visible */}
+
+ $ {command}
+
+
+ {/* Running state with cursor */}
+ {isRunning && !hasOutput && (
+
+
+
+ )}
+
+ {/* Output with optional collapsible functionality */}
+ {hasOutput && (
+
+ {/* Expand/Collapse indicator positioned between command and output */}
+ {processedTerminalContent.isLimited && (
+
setOutputExpanded(!outputExpanded)}
+ />
+ )}
+
+
+ {processedTerminalContent.isLimited ? (
+
setOutputExpanded(!outputExpanded)}
+ />
+ ) : (
+
+
+ {processedTerminalContent.fullContent}
+
+
+ )}
+
+
+ )}
+
+
+
+ )}
+
+ {/* Status information */}
+ {(statusMessage || isRunning) && (
+
+ )}
+
+
+ );
+}
diff --git a/gui/src/components/ansiTerminal/Ansi.tsx b/gui/src/components/ansiTerminal/Ansi.tsx
deleted file mode 100644
index 35d4b0a43..000000000
--- a/gui/src/components/ansiTerminal/Ansi.tsx
+++ /dev/null
@@ -1,279 +0,0 @@
-import Anser, { AnserJsonEntry } from "anser";
-import { escapeCarriageReturn } from "escape-carriage";
-import * as React from "react";
-import styled from "styled-components";
-import {
- defaultBorderRadius,
- vscBackground,
- vscEditorBackground,
- vscForeground,
-} from "../../components";
-import { getFontSize } from "../../util";
-
-const AnsiSpan = styled.span<{
- bg?: string;
- fg?: string;
- decoration?: string;
-}>`
- ${({ bg }) => bg && `background-color: rgb(${bg});`}
- ${({ fg }) => fg && `color: rgb(${fg});`}
- ${({ decoration }) => {
- switch (decoration) {
- case "bold":
- return "font-weight: bold;";
- case "dim":
- return "opacity: 0.5;";
- case "italic":
- return "font-style: italic;";
- case "hidden":
- return "visibility: hidden;";
- case "strikethrough":
- return "text-decoration: line-through;";
- case "underline":
- return "text-decoration: underline;";
- case "blink":
- return "text-decoration: blink;";
- default:
- return "";
- }
- }}
-`;
-
-const AnsiLink = styled.a`
- color: var(--vscode-textLink-foreground, #3794ff);
- text-decoration: none;
- &:hover {
- text-decoration: underline;
- }
-`;
-
-// Using the same styled component structure as StyledMarkdown
-const StyledAnsi = styled.div<{
- fontSize?: number;
- whiteSpace: string;
- bgColor: string;
-}>`
- pre {
- white-space: ${(props) => props.whiteSpace};
- background-color: ${vscEditorBackground};
- border-radius: ${defaultBorderRadius};
- border: 1px solid
- var(--vscode-editorWidget-border, rgba(127, 127, 127, 0.3));
- max-width: calc(100vw - 24px);
- overflow-x: scroll;
- overflow-y: hidden;
- padding: 8px;
- }
-
- code {
- span.line:empty {
- display: none;
- }
- word-wrap: break-word;
- border-radius: ${defaultBorderRadius};
- background-color: ${vscEditorBackground};
- font-size: ${getFontSize() - 2}px;
- font-family: var(--vscode-editor-font-family);
- }
-
- code:not(pre > code) {
- font-family: var(--vscode-editor-font-family);
- color: var(--vscode-input-placeholderForeground);
- }
-
- background-color: ${(props) => props.bgColor};
- font-family:
- var(--vscode-font-family),
- system-ui,
- -apple-system,
- BlinkMacSystemFont,
- "Segoe UI",
- Roboto,
- Oxygen,
- Ubuntu,
- Cantarell,
- "Open Sans",
- "Helvetica Neue",
- sans-serif;
- font-size: ${(props) => props.fontSize || getFontSize()}px;
- padding-left: 8px;
- padding-right: 8px;
- color: ${vscForeground};
- line-height: 1.5;
-
- > *:last-child {
- margin-bottom: 0;
- }
-`;
-
-/**
- * Converts ANSI strings into JSON output.
- * @name ansiToJSON
- * @function
- * @param {String} input The input string.
- * @param {boolean} use_classes If `true`, HTML classes will be appended
- * to the HTML output.
- * @return {Array} The parsed input.
- */
-function ansiToJSON(
- input: string,
- use_classes: boolean = false,
-): AnserJsonEntry[] {
- input = escapeCarriageReturn(fixBackspace(input));
- return Anser.ansiToJson(input, {
- json: true,
- remove_empty: true,
- use_classes,
- });
-}
-
-/**
- * Create a class string.
- * @name createClass
- * @function
- * @param {AnserJsonEntry} bundle
- * @return {String} class name(s)
- */
-function createClass(bundle: AnserJsonEntry): string | null {
- let classNames: string = "";
-
- if (bundle.bg) {
- classNames += `${bundle.bg}-bg `;
- }
- if (bundle.fg) {
- classNames += `${bundle.fg}-fg `;
- }
- if (bundle.decoration) {
- classNames += `ansi-${bundle.decoration} `;
- }
-
- if (classNames === "") {
- return null;
- }
-
- classNames = classNames.substring(0, classNames.length - 1);
- return classNames;
-}
-
-/**
- * Converts an Anser bundle into a React Node.
- * @param linkify whether links should be converting into clickable anchor tags.
- * @param useClasses should render the span with a class instead of style.
- * @param bundle Anser output.
- * @param key
- */
-
-function convertBundleIntoReact(
- linkify: boolean,
- useClasses: boolean,
- bundle: AnserJsonEntry,
- key: number,
-): JSX.Element {
- const className = useClasses ? createClass(bundle) : null;
- // Convert bundle.decoration to string or undefined (not null) to match the prop type
- const decorationProp = bundle.decoration
- ? String(bundle.decoration)
- : undefined;
-
- if (!linkify) {
- return (
-
- {bundle.content}
-
- );
- }
-
- const content: React.ReactNode[] = [];
- const linkRegex =
- /(\s|^)(https?:\/\/(?:www\.|(?!www))[^\s.]+\.[^\s]{2,}|www\.[^\s]+\.[^\s]{2,})/g;
-
- let index = 0;
- let match: RegExpExecArray | null;
- while ((match = linkRegex.exec(bundle.content)) !== null) {
- const [, pre, url] = match;
-
- const startIndex = match.index + pre.length;
- if (startIndex > index) {
- content.push(bundle.content.substring(index, startIndex));
- }
-
- // Make sure the href we generate from the link is fully qualified. We assume http
- // if it starts with a www because many sites don't support https
- const href = url.startsWith("www.") ? `http://${url}` : url;
-
- content.push(
-
- {url}
- ,
- );
-
- index = linkRegex.lastIndex;
- }
-
- if (index < bundle.content.length) {
- content.push(bundle.content.substring(index));
- }
-
- return (
-
- {content}
-
- );
-}
-
-declare interface Props {
- children?: string;
- linkify?: boolean;
- className?: string;
- useClasses?: boolean;
-}
-
-export default function Ansi(props: Props): JSX.Element {
- const { className, useClasses, children, linkify } = props;
-
- // Create the ANSI content
- const ansiContent = ansiToJSON(children ?? "", useClasses ?? false).map(
- (bundle, i) =>
- convertBundleIntoReact(linkify ?? false, useClasses ?? false, bundle, i),
- );
-
- return (
-
-
- {ansiContent}
-
-
- );
-}
-
-// This is copied from the Jupyter Classic source code
-// notebook/static/base/js/utils.js to handle \b in a way
-// that is **compatible with Jupyter classic**. One can
-// argue that this behavior is questionable:
-// https://stackoverflow.com/questions/55440152/multiple-b-doesnt-work-as-expected-in-jupyter#
-function fixBackspace(txt: string) {
- let tmp = txt;
- do {
- txt = tmp;
- // Cancel out anything-but-newline followed by backspace
- tmp = txt.replace(/[^\n]\x08/gm, "");
- } while (tmp.length < txt.length);
- return txt;
-}
diff --git a/gui/src/components/mainInput/belowMainInput/ThinkingBlockPeek.tsx b/gui/src/components/mainInput/belowMainInput/ThinkingBlockPeek.tsx
index ba423bbc4..3dea83d0e 100644
--- a/gui/src/components/mainInput/belowMainInput/ThinkingBlockPeek.tsx
+++ b/gui/src/components/mainInput/belowMainInput/ThinkingBlockPeek.tsx
@@ -5,38 +5,10 @@ import { ChatHistoryItem } from "core";
import { useEffect, useState } from "react";
import styled from "styled-components";
-import { defaultBorderRadius, lightGray, vscBackground } from "../..";
-import { getFontSize } from "../../../util";
+import { vscBackground } from "../..";
import { AnimatedEllipsis } from "../../AnimatedEllipsis";
import StyledMarkdownPreview from "../../StyledMarkdownPreview";
-
-const SpoilerButton = styled.div`
- background-color: ${vscBackground};
- width: fit-content;
- margin: 8px 6px 0px 2px;
- font-size: ${getFontSize() - 2}px;
- border: 0.5px solid ${lightGray};
- border-radius: ${defaultBorderRadius};
- padding: 4px 8px;
- color: ${lightGray};
- cursor: pointer;
- box-shadow:
- 0 4px 6px rgba(0, 0, 0, 0.1),
- 0 1px 3px rgba(0, 0, 0, 0.08);
- transition: box-shadow 0.3s ease;
-
- &:hover {
- box-shadow:
- 0 6px 8px rgba(0, 0, 0, 0.15),
- 0 3px 6px rgba(0, 0, 0, 0.1);
- }
-`;
-
-const ButtonContent = styled.div`
- display: flex;
- align-items: center;
- gap: 6px;
-`;
+import { ButtonContent, SpoilerButton } from "../../ui/SpoilerButton";
const ThinkingTextContainer = styled.span`
display: inline-block;
diff --git a/gui/src/components/ui/SpoilerButton.tsx b/gui/src/components/ui/SpoilerButton.tsx
new file mode 100644
index 000000000..66b74bca9
--- /dev/null
+++ b/gui/src/components/ui/SpoilerButton.tsx
@@ -0,0 +1,31 @@
+import styled from "styled-components";
+import { defaultBorderRadius, lightGray, vscBackground } from "../index";
+import { getFontSize } from "../../util";
+
+export const SpoilerButton = styled.div`
+ background-color: ${vscBackground};
+ width: fit-content;
+ margin: 8px 6px 0px 2px;
+ font-size: ${getFontSize() - 2}px;
+ border: 0.5px solid ${lightGray};
+ border-radius: ${defaultBorderRadius};
+ padding: 4px 8px;
+ color: ${lightGray};
+ cursor: pointer;
+ box-shadow:
+ 0 4px 6px rgba(0, 0, 0, 0.1),
+ 0 1px 3px rgba(0, 0, 0, 0.08);
+ transition: box-shadow 0.3s ease;
+
+ &:hover {
+ box-shadow:
+ 0 6px 8px rgba(0, 0, 0, 0.15),
+ 0 3px 6px rgba(0, 0, 0, 0.1);
+ }
+`;
+
+export const ButtonContent = styled.div`
+ display: flex;
+ align-items: center;
+ gap: 6px;
+`;
diff --git a/gui/src/pages/gui/ToolCallDiv/RunTerminalCommand.tsx b/gui/src/pages/gui/ToolCallDiv/RunTerminalCommand.tsx
index dfdcfb5ca..5975e4db9 100644
--- a/gui/src/pages/gui/ToolCallDiv/RunTerminalCommand.tsx
+++ b/gui/src/pages/gui/ToolCallDiv/RunTerminalCommand.tsx
@@ -1,10 +1,5 @@
import { ToolCallState } from "core";
-import { useMemo } from "react";
-import Ansi from "../../../components/ansiTerminal/Ansi";
-import StyledMarkdownPreview from "../../../components/StyledMarkdownPreview";
-import { useAppDispatch } from "../../../redux/hooks";
-import { moveTerminalProcessToBackground } from "../../../redux/thunks/moveTerminalProcessToBackground";
-import { TerminalCollapsibleContainer } from "./TerminalCollapsibleContainer";
+import { UnifiedTerminalCommand } from "../../../components/UnifiedTerminal/UnifiedTerminal";
interface RunTerminalCommandToolCallProps {
command: string;
@@ -12,38 +7,7 @@ interface RunTerminalCommandToolCallProps {
toolCallId: string | undefined;
}
-interface StatusIconProps {
- status: "running" | "completed" | "failed" | "background";
-}
-
-function StatusIcon({ status }: StatusIconProps) {
- const getStatusColor = () => {
- switch (status) {
- case "running":
- return "bg-success";
- case "completed":
- return "bg-success";
- case "background":
- return "bg-accent";
- case "failed":
- return "bg-error";
- default:
- return "bg-success";
- }
- };
-
- return (
-
- );
-}
-
export function RunTerminalCommand(props: RunTerminalCommandToolCallProps) {
- const dispatch = useAppDispatch();
-
// Find the terminal output from context items if available
const terminalItem = props.toolCallState.output?.find(
(item) => item.name === "Terminal",
@@ -52,43 +16,6 @@ export function RunTerminalCommand(props: RunTerminalCommandToolCallProps) {
const terminalContent = terminalItem?.content || "";
const statusMessage = terminalItem?.status || "";
const isRunning = props.toolCallState.status === "calling";
- const hasOutput = terminalContent.length > 0;
-
- const displayLines = 15;
-
- // Process terminal content for line limiting
- const processedTerminalContent = useMemo(() => {
- if (!terminalContent)
- return {
- fullContent: "",
- limitedContent: "",
- totalLines: 0,
- isLimited: false,
- hiddenLinesCount: 0,
- };
-
- const lines = terminalContent.split("\n");
- const totalLines = lines.length;
-
- if (totalLines > displayLines) {
- const lastTwentyLines = lines.slice(-displayLines);
- return {
- fullContent: terminalContent,
- limitedContent: lastTwentyLines.join("\n"),
- totalLines,
- isLimited: true,
- hiddenLinesCount: totalLines - displayLines,
- };
- }
-
- return {
- fullContent: terminalContent,
- limitedContent: terminalContent,
- totalLines,
- isLimited: false,
- hiddenLinesCount: 0,
- };
- }, [terminalContent]);
// Determine status type
let statusType: "running" | "completed" | "failed" | "background" =
@@ -102,55 +29,13 @@ export function RunTerminalCommand(props: RunTerminalCommandToolCallProps) {
}
return (
-
- {/* Command */}
-
-
- {/* Terminal output with ANSI support */}
- {isRunning && !hasOutput && (
-
Waiting for output...
- )}
- {hasOutput && (
-
{processedTerminalContent.limitedContent}
- }
- expandedContent={{processedTerminalContent.fullContent}}
- />
- )}
-
- {/* Status information */}
- {(statusMessage || isRunning) && (
-
- )}
-
+
);
}