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
32 changes: 22 additions & 10 deletions app/components/ComputerSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,14 @@ import {
type ChatStatus,
} from "@/types/chat";

const SHELL_ACTION_LABELS: Record<string, string> = {
exec: "Running command",
send: "Writing to terminal",
wait: "Waiting for completion",
kill: "Terminating process",
view: "Reading terminal",
};

interface ComputerSidebarProps {
sidebarOpen: boolean;
sidebarContent: SidebarContent | null;
Expand Down Expand Up @@ -175,13 +183,16 @@ export const ComputerSidebarBase: React.FC<ComputerSidebarProps> = ({
};
return actionMap[sidebarContent.action || "reading"];
} else if (isTerminal) {
return sidebarContent.isExecuting
? "Executing command"
: "Command executed";
// Use shellAction if available for accurate action text
if (sidebarContent.shellAction) {
return (
SHELL_ACTION_LABELS[sidebarContent.shellAction] ?? "Executing command"
);
}
// Fallback for legacy terminal entries without shellAction
return "Executing command";
} else if (isPython) {
return sidebarContent.isExecuting
? "Executing Python"
: "Python executed";
return "Executing Python";
}
return "Unknown action";
};
Expand Down Expand Up @@ -212,6 +223,7 @@ export const ComputerSidebarBase: React.FC<ComputerSidebarProps> = ({
if (isFile) {
return sidebarContent.path.split("/").pop() || sidebarContent.path;
} else if (isTerminal) {
// If it's a shell tool call, just show the session ID (command field)
return sidebarContent.command;
} else if (isPython) {
return sidebarContent.code.replace(/\n/g, " ");
Expand Down Expand Up @@ -289,10 +301,9 @@ export const ComputerSidebarBase: React.FC<ComputerSidebarProps> = ({
{/* Title - far left */}
<div className="flex items-center gap-2">
{isTerminal ? (
<Terminal
size={14}
className="text-muted-foreground flex-shrink-0"
/>
<span className="text-muted-foreground text-sm font-mono truncate max-w-[200px]">
{sidebarContent.sessionName || "Shell"}
</span>
) : isPython ? (
<Code2
size={14}
Expand Down Expand Up @@ -385,6 +396,7 @@ export const ComputerSidebarBase: React.FC<ComputerSidebarProps> = ({
output={sidebarContent.output}
isExecuting={sidebarContent.isExecuting}
isBackground={sidebarContent.isBackground}
showContentOnly={sidebarContent.showContentOnly}
status={
sidebarContent.isExecuting ? "streaming" : "ready"
}
Expand Down
4 changes: 4 additions & 0 deletions app/components/MessagePartHandler.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { UIMessage } from "@ai-sdk/react";
import { MemoizedMarkdown } from "./MemoizedMarkdown";
import { FileToolsHandler } from "./tools/FileToolsHandler";
import { TerminalToolHandler } from "./tools/TerminalToolHandler";
import { ShellToolHandler } from "./tools/ShellToolHandler";
import { HttpRequestToolHandler } from "./tools/HttpRequestToolHandler";
import { PythonToolHandler } from "./tools/PythonToolHandler";
import { WebToolHandler } from "./tools/WebToolHandler";
Expand Down Expand Up @@ -87,6 +88,9 @@ export const MessagePartHandler = ({
<TerminalToolHandler message={message} part={part} status={status} />
);

case "tool-shell":
return <ShellToolHandler message={message} part={part} status={status} />;

case "tool-http_request":
return (
<HttpRequestToolHandler message={message} part={part} status={status} />
Expand Down
12 changes: 10 additions & 2 deletions app/components/TerminalCodeBlock.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ interface TerminalCodeBlockProps {
isExecuting?: boolean;
status?: "ready" | "submitted" | "streaming" | "error";
isBackground?: boolean;
showContentOnly?: boolean;
variant?: "default" | "sidebar";
wrap?: boolean;
}
Expand Down Expand Up @@ -172,7 +173,7 @@ const AnsiCodeBlock = ({
return (
<div className="px-4 py-4 text-muted-foreground">
<Shimmer>
{isStreaming ? "Processing output..." : "Rendering output..."}
{isStreaming ? "Processing output" : "Rendering output"}
</Shimmer>
</div>
);
Expand All @@ -198,6 +199,7 @@ export const TerminalCodeBlock = ({
isExecuting = false,
status,
isBackground = false,
showContentOnly = false,
variant = "default",
wrap = false,
}: TerminalCodeBlockProps) => {
Expand All @@ -209,7 +211,13 @@ export const TerminalCodeBlock = ({
}, [wrap]);

// Combine command and output for full terminal session
const terminalContent = output ? `$ ${command}\n${output}` : `$ ${command}`;
// For views/non-exec actions, show raw output only
const terminalContent =
showContentOnly && output
? output
: output
? `$ ${command}\n${output}`
: `$ ${command}`;
const displayContent = output || "";

// For non-sidebar variant, keep the original terminal look
Expand Down
39 changes: 19 additions & 20 deletions app/components/tools/FileToolsHandler.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { UIMessage } from "@ai-sdk/react";
import ToolBlock from "@/components/ui/tool-block";
import { FilePlus, FileText, FilePen, FileMinus } from "lucide-react";
import { useGlobalState } from "../../contexts/GlobalState";
import type { ChatStatus } from "@/types";
import type { ChatStatus, SidebarContent } from "@/types";
import { isSidebarFile } from "@/types/chat";

interface DiffDataPart {
Expand All @@ -20,15 +20,20 @@ interface FileToolsHandlerProps {
message: UIMessage;
part: any;
status: ChatStatus;
// Optional: pass openSidebar to make handler context-agnostic
externalOpenSidebar?: (content: SidebarContent) => void;
}

export const FileToolsHandler = ({
message,
part,
status,
externalOpenSidebar,
}: FileToolsHandlerProps) => {
const { openSidebar, updateSidebarContent, sidebarContent, sidebarOpen } =
useGlobalState();
const globalState = useGlobalState();
// Use external openSidebar if provided, otherwise use from GlobalState
const openSidebar = externalOpenSidebar ?? globalState.openSidebar;
const { updateSidebarContent, sidebarContent, sidebarOpen } = globalState;

// Track the last streamed content to avoid unnecessary updates
const lastStreamedContentRef = useRef<string | null>(null);
Expand All @@ -45,7 +50,10 @@ export const FileToolsHandler = ({
}, [part.type, part.input]);

// Update sidebar content as write_file content streams in
// Only applies when using GlobalState (not external openSidebar)
useEffect(() => {
// Skip if using external openSidebar (read-only mode)
if (externalOpenSidebar) return;
// Only update for write_file tool during streaming
if (part.type !== "tool-write_file") return;
if (part.state !== "input-streaming" && part.state !== "input-available")
Expand Down Expand Up @@ -77,6 +85,7 @@ export const FileToolsHandler = ({
sidebarOpen,
sidebarContent,
updateSidebarContent,
externalOpenSidebar,
]);

// Reset tracking refs when tool completes or changes
Expand Down Expand Up @@ -182,7 +191,7 @@ export const FileToolsHandler = ({
<ToolBlock
key={toolCallId}
icon={<FileText />}
action="Read"
action="Reading"
target={`${readInput.target_file}${getFileRange()}`}
isClickable={true}
onClick={handleOpenInSidebar}
Expand Down Expand Up @@ -272,7 +281,7 @@ export const FileToolsHandler = ({
<ToolBlock
key={toolCallId}
icon={<FilePlus />}
action="Successfully wrote"
action="Writing to"
target={writeInput.file_path}
isClickable={true}
onClick={() => {
Expand Down Expand Up @@ -332,14 +341,12 @@ export const FileToolsHandler = ({
) : null;
case "output-available": {
if (!deleteInput) return null;
const deleteOutput = output as { result: string };
const isSuccess = deleteOutput.result.includes("Successfully deleted");

return (
<ToolBlock
key={toolCallId}
icon={<FileMinus />}
action={isSuccess ? "Successfully deleted" : "Failed to delete"}
action="Deleting"
target={deleteInput.target_file}
/>
);
Expand Down Expand Up @@ -385,8 +392,6 @@ export const FileToolsHandler = ({
case "output-available": {
if (!searchReplaceInput) return null;
const searchReplaceOutput = output as { result: string };
const isSuccess =
searchReplaceOutput.result.includes("Successfully made");

const handleOpenInSidebar = () => {
// Use diff data from stream if available (not persisted across reloads)
Expand All @@ -412,7 +417,9 @@ export const FileToolsHandler = ({
<ToolBlock
key={toolCallId}
icon={<FilePen />}
action={isSuccess ? "Successfully edited" : "Failed to edit"}
action={
searchReplaceInput?.replace_all ? "Replacing all in" : "Editing"
}
target={searchReplaceInput.file_path}
isClickable={true}
onClick={handleOpenInSidebar}
Expand Down Expand Up @@ -464,20 +471,12 @@ export const FileToolsHandler = ({
) : null;
case "output-available": {
if (!multiEditInput) return null;
const multiEditOutput = output as { result: string };
const isSuccess = multiEditOutput.result.includes(
"Successfully applied",
);

return (
<ToolBlock
key={toolCallId}
icon={<FilePen />}
action={
isSuccess
? `Successfully applied ${multiEditInput.edits.length} edits`
: "Failed to apply edits"
}
action={`Making ${multiEditInput.edits.length} edits to`}
target={multiEditInput.file_path}
/>
);
Expand Down
9 changes: 3 additions & 6 deletions app/components/tools/GetTerminalFilesHandler.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,23 +52,20 @@ export const GetTerminalFilesHandler = ({
<ToolBlock
key={toolCallId}
icon={<FileDown />}
action={status === "streaming" ? "Sharing" : "Shared"}
action="Sharing"
target={getFileNames(filesInput?.files || [])}
isShimmer={status === "streaming"}
/>
);

case "output-available": {
// Support both new (files) and legacy (fileUrls) formats
const fileCount =
filesOutput?.files?.length || filesOutput?.fileUrls?.length || 0;
const fileNames = getFileNames(filesInput?.files || []);

return (
<ToolBlock
key={toolCallId}
icon={<FileDown />}
action={`Shared ${fileCount} file${fileCount !== 1 ? "s" : ""}`}
action="Sharing"
target={fileNames}
/>
);
Expand All @@ -79,7 +76,7 @@ export const GetTerminalFilesHandler = ({
<ToolBlock
key={toolCallId}
icon={<FileDown />}
action="Failed to share"
action="Sharing"
target={getFileNames(filesInput?.files || [])}
/>
);
Expand Down
6 changes: 2 additions & 4 deletions app/components/tools/HttpRequestToolHandler.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -117,9 +117,7 @@ export const HttpRequestToolHandler = ({
// Determine action text based on state
const getActionText = (): string => {
if (state === "input-streaming") return "Preparing request";
if (isExecuting) return "Requesting";
if (httpOutput?.error) return "Request failed";
return "Requested";
return "Requesting";
};

switch (state) {
Expand Down Expand Up @@ -162,7 +160,7 @@ export const HttpRequestToolHandler = ({
<ToolBlock
key={toolCallId}
icon={<Globe />}
action="Request failed"
action={getActionText()}
target={displayCommand}
isClickable={true}
onClick={handleOpenInSidebar}
Expand Down
34 changes: 3 additions & 31 deletions app/components/tools/MatchToolHandler.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,36 +37,8 @@ export const MatchToolHandler = ({ part, status }: MatchToolHandlerProps) => {
return matchInput.scope;
};

// Parse the output to get a summary label
const getResultLabel = (outputText: string) => {
if (outputText.startsWith("Found ")) {
// Extract "Found X file(s)" or "Found X match(es)"
const match = outputText.match(/^Found (\d+) (file|match)/);
if (match) {
const count = parseInt(match[1], 10);
const type = match[2];
if (type === "file") {
return `Found ${count} file${count === 1 ? "" : "s"}`;
}
return `Found ${count} match${count === 1 ? "" : "es"}`;
}
}
if (outputText.startsWith("No files found")) {
return "No files found";
}
if (outputText.startsWith("No matches found")) {
return "No matches found";
}
if (outputText.startsWith("Search timed out")) {
return "Search timed out";
}
if (
outputText.startsWith("Error:") ||
outputText.startsWith("Search failed")
) {
return "Search failed";
}
return isGlob ? "Search complete" : "Search complete";
const getResultLabel = () => {
return isGlob ? "Finding files" : "Searching";
};

switch (state) {
Expand Down Expand Up @@ -113,7 +85,7 @@ export const MatchToolHandler = ({ part, status }: MatchToolHandlerProps) => {
<ToolBlock
key={toolCallId}
icon={<FolderSearch />}
action={getResultLabel(outputText)}
action={getResultLabel()}
target={getTarget()}
isClickable={true}
onClick={handleOpenInSidebar}
Expand Down
17 changes: 2 additions & 15 deletions app/components/tools/MemoryToolHandler.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,19 +26,6 @@ export const MemoryToolHandler = ({ part, status }: MemoryToolHandlerProps) => {
};

const getActionText = (action?: string) => {
switch (action) {
case "create":
return "Created memory";
case "update":
return "Updated memory";
case "delete":
return "Deleted memory";
default:
return "Updated memory";
}
};

const getStreamingActionText = (action?: string) => {
switch (action) {
case "create":
return "Creating memory";
Expand Down Expand Up @@ -113,7 +100,7 @@ export const MemoryToolHandler = ({ part, status }: MemoryToolHandlerProps) => {
<ToolBlock
key={toolCallId}
icon={<NotebookPen />}
action={getStreamingActionText(memoryInput.action)}
action={getActionText(memoryInput.action)}
isShimmer={true}
/>
) : null;
Expand All @@ -123,7 +110,7 @@ export const MemoryToolHandler = ({ part, status }: MemoryToolHandlerProps) => {
<ToolBlock
key={toolCallId}
icon={<NotebookPen />}
action={getStreamingActionText(memoryInput.action)}
action={getActionText(memoryInput.action)}
target={memoryInput.title}
isShimmer={true}
/>
Expand Down
Loading