init: push source

This commit is contained in:
Mufeed VH
2025-06-19 19:24:01 +05:30
commit 8e76d016d4
136 changed files with 38177 additions and 0 deletions

View File

@@ -0,0 +1,600 @@
import React from "react";
import {
Terminal,
User,
Bot,
AlertCircle,
CheckCircle2
} from "lucide-react";
import { Card, CardContent } from "@/components/ui/card";
import { cn } from "@/lib/utils";
import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm";
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
import { claudeSyntaxTheme } from "@/lib/claudeSyntaxTheme";
import type { ClaudeStreamMessage } from "./AgentExecution";
import {
TodoWidget,
LSWidget,
ReadWidget,
ReadResultWidget,
GlobWidget,
BashWidget,
WriteWidget,
GrepWidget,
EditWidget,
EditResultWidget,
MCPWidget,
CommandWidget,
CommandOutputWidget,
SummaryWidget,
MultiEditWidget,
MultiEditResultWidget,
SystemReminderWidget,
SystemInitializedWidget,
TaskWidget,
LSResultWidget
} from "./ToolWidgets";
interface StreamMessageProps {
message: ClaudeStreamMessage;
className?: string;
streamMessages: ClaudeStreamMessage[];
}
/**
* Component to render a single Claude Code stream message
*/
export const StreamMessage: React.FC<StreamMessageProps> = ({ message, className, streamMessages }) => {
try {
// Skip rendering for meta messages that don't have meaningful content
if (message.isMeta && !message.leafUuid && !message.summary) {
return null;
}
// Handle summary messages
if (message.leafUuid && message.summary && (message as any).type === "summary") {
return <SummaryWidget summary={message.summary} leafUuid={message.leafUuid} />;
}
// System initialization message
if (message.type === "system" && message.subtype === "init") {
return (
<SystemInitializedWidget
sessionId={message.session_id}
model={message.model}
cwd={message.cwd}
tools={message.tools}
/>
);
}
// Assistant message
if (message.type === "assistant" && message.message) {
const msg = message.message;
return (
<Card className={cn("border-primary/20 bg-primary/5", className)}>
<CardContent className="p-4">
<div className="flex items-start gap-3">
<Bot className="h-5 w-5 text-primary mt-0.5" />
<div className="flex-1 space-y-2 min-w-0">
{msg.content && Array.isArray(msg.content) && msg.content.map((content: any, idx: number) => {
// Text content - render as markdown
if (content.type === "text") {
// Ensure we have a string to render
const textContent = typeof content.text === 'string'
? content.text
: (content.text?.text || JSON.stringify(content.text || content));
return (
<div key={idx} className="prose prose-sm dark:prose-invert max-w-none">
<ReactMarkdown
remarkPlugins={[remarkGfm]}
components={{
code({ node, inline, className, children, ...props }: any) {
const match = /language-(\w+)/.exec(className || '');
return !inline && match ? (
<SyntaxHighlighter
style={claudeSyntaxTheme}
language={match[1]}
PreTag="div"
{...props}
>
{String(children).replace(/\n$/, '')}
</SyntaxHighlighter>
) : (
<code className={className} {...props}>
{children}
</code>
);
}
}}
>
{textContent}
</ReactMarkdown>
</div>
);
}
// Tool use - render custom widgets based on tool name
if (content.type === "tool_use") {
const toolName = content.name?.toLowerCase();
const input = content.input;
// Task tool - for sub-agent tasks
if (toolName === "task" && input) {
return <TaskWidget key={idx} description={input.description} prompt={input.prompt} />;
}
// Edit tool
if (toolName === "edit" && input?.file_path) {
return <EditWidget key={idx} {...input} />;
}
// MultiEdit tool
if (toolName === "multiedit" && input?.file_path && input?.edits) {
return <MultiEditWidget key={idx} {...input} />;
}
// MCP tools (starting with mcp__)
if (content.name?.startsWith("mcp__")) {
return <MCPWidget key={idx} toolName={content.name} input={input} />;
}
// TodoWrite tool
if (toolName === "todowrite" && input?.todos) {
return <TodoWidget key={idx} todos={input.todos} />;
}
// LS tool
if (toolName === "ls" && input?.path) {
return <LSWidget key={idx} path={input.path} />;
}
// Read tool
if (toolName === "read" && input?.file_path) {
return <ReadWidget key={idx} filePath={input.file_path} />;
}
// Glob tool
if (toolName === "glob" && input?.pattern) {
return <GlobWidget key={idx} pattern={input.pattern} />;
}
// Bash tool
if (toolName === "bash" && input?.command) {
return <BashWidget key={idx} command={input.command} description={input.description} />;
}
// Write tool
if (toolName === "write" && input?.file_path && input?.content) {
return <WriteWidget key={idx} filePath={input.file_path} content={input.content} />;
}
// Grep tool
if (toolName === "grep" && input?.pattern) {
return <GrepWidget key={idx} pattern={input.pattern} include={input.include} path={input.path} exclude={input.exclude} />;
}
// Default tool display
return (
<div key={idx} className="space-y-2">
<div className="flex items-center gap-2">
<Terminal className="h-4 w-4 text-muted-foreground" />
<span className="text-sm font-medium">
Using tool: <code className="font-mono">{content.name}</code>
</span>
</div>
{content.input && (
<div className="ml-6 p-2 bg-background rounded-md border">
<pre className="text-xs font-mono overflow-x-auto">
{JSON.stringify(content.input, null, 2)}
</pre>
</div>
)}
</div>
);
}
return null;
})}
{msg.usage && (
<div className="text-xs text-muted-foreground mt-2">
Tokens: {msg.usage.input_tokens} in, {msg.usage.output_tokens} out
</div>
)}
</div>
</div>
</CardContent>
</Card>
);
}
// User message
if (message.type === "user" && message.message) {
// Don't render meta messages, which are for system use
if (message.isMeta) return null;
const msg = message.message;
// Skip empty user messages
if (!msg.content || (Array.isArray(msg.content) && msg.content.length === 0)) {
return null;
}
return (
<Card className={cn("border-muted-foreground/20 bg-muted/20", className)}>
<CardContent className="p-4">
<div className="flex items-start gap-3">
<User className="h-5 w-5 text-muted-foreground mt-0.5" />
<div className="flex-1 space-y-2 min-w-0">
{/* Handle content that is a simple string (e.g. from user commands) */}
{typeof msg.content === 'string' && (
(() => {
const contentStr = msg.content as string;
// Check if it's a command message
const commandMatch = contentStr.match(/<command-name>(.+?)<\/command-name>[\s\S]*?<command-message>(.+?)<\/command-message>[\s\S]*?<command-args>(.*?)<\/command-args>/);
if (commandMatch) {
const [, commandName, commandMessage, commandArgs] = commandMatch;
return (
<CommandWidget
commandName={commandName.trim()}
commandMessage={commandMessage.trim()}
commandArgs={commandArgs?.trim()}
/>
);
}
// Check if it's command output
const stdoutMatch = contentStr.match(/<local-command-stdout>([\s\S]*?)<\/local-command-stdout>/);
if (stdoutMatch) {
const [, output] = stdoutMatch;
return <CommandOutputWidget output={output} />;
}
// Otherwise render as plain text
return (
<pre className="text-sm font-mono whitespace-pre-wrap text-muted-foreground">
{contentStr}
</pre>
);
})()
)}
{/* Handle content that is an array of parts */}
{Array.isArray(msg.content) && msg.content.map((content: any, idx: number) => {
// Tool result
if (content.type === "tool_result") {
// Extract the actual content string
let contentText = '';
if (typeof content.content === 'string') {
contentText = content.content;
} else if (content.content && typeof content.content === 'object') {
// Handle object with text property
if (content.content.text) {
contentText = content.content.text;
} else if (Array.isArray(content.content)) {
// Handle array of content blocks
contentText = content.content
.map((c: any) => (typeof c === 'string' ? c : c.text || JSON.stringify(c)))
.join('\n');
} else {
// Fallback to JSON stringify
contentText = JSON.stringify(content.content, null, 2);
}
}
// Check for system-reminder tags
const reminderMatch = contentText.match(/<system-reminder>(.*?)<\/system-reminder>/s);
if (reminderMatch) {
const reminderMessage = reminderMatch[1].trim();
const beforeReminder = contentText.substring(0, reminderMatch.index || 0).trim();
const afterReminder = contentText.substring((reminderMatch.index || 0) + reminderMatch[0].length).trim();
return (
<div key={idx} className="space-y-2">
<div className="flex items-center gap-2">
<CheckCircle2 className="h-4 w-4 text-green-500" />
<span className="text-sm font-medium">Tool Result</span>
</div>
{beforeReminder && (
<div className="ml-6 p-2 bg-background rounded-md border">
<pre className="text-xs font-mono overflow-x-auto whitespace-pre-wrap">
{beforeReminder}
</pre>
</div>
)}
<div className="ml-6">
<SystemReminderWidget message={reminderMessage} />
</div>
{afterReminder && (
<div className="ml-6 p-2 bg-background rounded-md border">
<pre className="text-xs font-mono overflow-x-auto whitespace-pre-wrap">
{afterReminder}
</pre>
</div>
)}
</div>
);
}
// Check if this is an Edit tool result
const isEditResult = contentText.includes("has been updated. Here's the result of running `cat -n`");
if (isEditResult) {
return (
<div key={idx} className="space-y-2">
<div className="flex items-center gap-2">
<CheckCircle2 className="h-4 w-4 text-green-500" />
<span className="text-sm font-medium">Edit Result</span>
</div>
<EditResultWidget content={contentText} />
</div>
);
}
// Check if this is a MultiEdit tool result
const isMultiEditResult = contentText.includes("has been updated with multiple edits") ||
contentText.includes("MultiEdit completed successfully") ||
contentText.includes("Applied multiple edits to");
if (isMultiEditResult) {
return (
<div key={idx} className="space-y-2">
<div className="flex items-center gap-2">
<CheckCircle2 className="h-4 w-4 text-green-500" />
<span className="text-sm font-medium">MultiEdit Result</span>
</div>
<MultiEditResultWidget content={contentText} />
</div>
);
}
// Check if this is an LS tool result (directory tree structure)
const isLSResult = (() => {
if (!content.tool_use_id || typeof contentText !== 'string') return false;
// Check if this result came from an LS tool by looking for the tool call
let isFromLSTool = false;
// Search in previous assistant messages for the matching tool_use
if (streamMessages) {
for (let i = streamMessages.length - 1; i >= 0; i--) {
const prevMsg = streamMessages[i];
// Only check assistant messages
if (prevMsg.type === 'assistant' && prevMsg.message?.content && Array.isArray(prevMsg.message.content)) {
const toolUse = prevMsg.message.content.find((c: any) =>
c.type === 'tool_use' &&
c.id === content.tool_use_id &&
c.name?.toLowerCase() === 'ls'
);
if (toolUse) {
isFromLSTool = true;
break;
}
}
}
}
// Only proceed if this is from an LS tool
if (!isFromLSTool) return false;
// Additional validation: check for tree structure pattern
const lines = contentText.split('\n');
const hasTreeStructure = lines.some(line => /^\s*-\s+/.test(line));
const hasNoteAtEnd = lines.some(line => line.trim().startsWith('NOTE: do any of the files'));
return hasTreeStructure || hasNoteAtEnd;
})();
if (isLSResult) {
return (
<div key={idx} className="space-y-2">
<div className="flex items-center gap-2">
<CheckCircle2 className="h-4 w-4 text-green-500" />
<span className="text-sm font-medium">Directory Contents</span>
</div>
<LSResultWidget content={contentText} />
</div>
);
}
// Check if this is a Read tool result (contains line numbers with arrow separator)
const isReadResult = content.tool_use_id && typeof contentText === 'string' &&
/^\s*\d+→/.test(contentText);
if (isReadResult) {
// Try to find the corresponding Read tool call to get the file path
let filePath: string | undefined;
// Search in previous assistant messages for the matching tool_use
if (streamMessages) {
for (let i = streamMessages.length - 1; i >= 0; i--) {
const prevMsg = streamMessages[i];
// Only check assistant messages
if (prevMsg.type === 'assistant' && prevMsg.message?.content && Array.isArray(prevMsg.message.content)) {
const toolUse = prevMsg.message.content.find((c: any) =>
c.type === 'tool_use' &&
c.id === content.tool_use_id &&
c.name?.toLowerCase() === 'read'
);
if (toolUse?.input?.file_path) {
filePath = toolUse.input.file_path;
break;
}
}
}
}
return (
<div key={idx} className="space-y-2">
<div className="flex items-center gap-2">
<CheckCircle2 className="h-4 w-4 text-green-500" />
<span className="text-sm font-medium">Read Result</span>
</div>
<ReadResultWidget content={contentText} filePath={filePath} />
</div>
);
}
// Handle empty tool results
if (!contentText || contentText.trim() === '') {
return (
<div key={idx} className="space-y-2">
<div className="flex items-center gap-2">
<CheckCircle2 className="h-4 w-4 text-green-500" />
<span className="text-sm font-medium">Tool Result</span>
</div>
<div className="ml-6 p-3 bg-muted/50 rounded-md border text-sm text-muted-foreground italic">
Tool did not return any output
</div>
</div>
);
}
return (
<div key={idx} className="space-y-2">
<div className="flex items-center gap-2">
{content.is_error ? (
<AlertCircle className="h-4 w-4 text-destructive" />
) : (
<CheckCircle2 className="h-4 w-4 text-green-500" />
)}
<span className="text-sm font-medium">Tool Result</span>
</div>
<div className="ml-6 p-2 bg-background rounded-md border">
<pre className="text-xs font-mono overflow-x-auto whitespace-pre-wrap">
{contentText}
</pre>
</div>
</div>
);
}
// Text content
if (content.type === "text") {
// Handle both string and object formats
const textContent = typeof content.text === 'string'
? content.text
: (content.text?.text || JSON.stringify(content.text));
return (
<div key={idx} className="text-sm">
{textContent}
</div>
);
}
return null;
})}
</div>
</div>
</CardContent>
</Card>
);
}
// Result message - render with markdown
if (message.type === "result") {
const isError = message.is_error || message.subtype?.includes("error");
return (
<Card className={cn(
isError ? "border-destructive/20 bg-destructive/5" : "border-green-500/20 bg-green-500/5",
className
)}>
<CardContent className="p-4">
<div className="flex items-start gap-3">
{isError ? (
<AlertCircle className="h-5 w-5 text-destructive mt-0.5" />
) : (
<CheckCircle2 className="h-5 w-5 text-green-500 mt-0.5" />
)}
<div className="flex-1 space-y-2">
<h4 className="font-semibold text-sm">
{isError ? "Execution Failed" : "Execution Complete"}
</h4>
{message.result && (
<div className="prose prose-sm dark:prose-invert max-w-none">
<ReactMarkdown
remarkPlugins={[remarkGfm]}
components={{
code({ node, inline, className, children, ...props }: any) {
const match = /language-(\w+)/.exec(className || '');
return !inline && match ? (
<SyntaxHighlighter
style={claudeSyntaxTheme}
language={match[1]}
PreTag="div"
{...props}
>
{String(children).replace(/\n$/, '')}
</SyntaxHighlighter>
) : (
<code className={className} {...props}>
{children}
</code>
);
}
}}
>
{message.result}
</ReactMarkdown>
</div>
)}
{message.error && (
<div className="text-sm text-destructive">{message.error}</div>
)}
<div className="text-xs text-muted-foreground space-y-1 mt-2">
{message.cost_usd !== undefined && (
<div>Cost: ${message.cost_usd.toFixed(4)} USD</div>
)}
{message.duration_ms !== undefined && (
<div>Duration: {(message.duration_ms / 1000).toFixed(2)}s</div>
)}
{message.num_turns !== undefined && (
<div>Turns: {message.num_turns}</div>
)}
{message.usage && (
<div>
Total tokens: {message.usage.input_tokens + message.usage.output_tokens}
({message.usage.input_tokens} in, {message.usage.output_tokens} out)
</div>
)}
</div>
</div>
</div>
</CardContent>
</Card>
);
}
// Skip rendering if no meaningful content
return null;
} catch (error) {
// If any error occurs during rendering, show a safe error message
console.error("Error rendering stream message:", error, message);
return (
<Card className={cn("border-destructive/20 bg-destructive/5", className)}>
<CardContent className="p-4">
<div className="flex items-start gap-3">
<AlertCircle className="h-5 w-5 text-destructive mt-0.5" />
<div className="flex-1">
<p className="text-sm font-medium">Error rendering message</p>
<p className="text-xs text-muted-foreground mt-1">
{error instanceof Error ? error.message : 'Unknown error'}
</p>
</div>
</div>
</CardContent>
</Card>
);
}
};