Raw logs should render ANSI (vibe-kanban) (#520)
* Perfect! Let me summarize what has been successfully implemented: ## ✅ Implementation Complete I've successfully implemented a **unified log rendering architecture** with ANSI support across the entire application: ### 🎯 What Was Accomplished 1. **✅ Added `fancy-ansi` dependency** - React 18 compatible ANSI rendering library 2. **✅ Created `RawLogText` component** - Single, unified component for all raw log rendering 3. **✅ Added ANSI color CSS classes** - Consistent theming with Tailwind classes 4. **✅ Refactored all log rendering components**: - **StdoutEntry.tsx** - Now uses RawLogText with `channel="stdout"` - **StderrEntry.tsx** - Now uses RawLogText with `channel="stderr"` - **ProcessLogsViewer.tsx** - Replaced `formatLogLine()` with RawLogText - **ProcessCard.tsx** - Updated raw log display to use RawLogText 5. **✅ Added comprehensive tests** - Unit tests for the RawLogText component 6. **✅ Verified build success** - All TypeScript checks pass ### 🌟 Key Features - **ANSI Escape Code Rendering**: Raw logs like `[0m[1][0m [32m[1mVITE[22m v5.4.19[39m` now render with proper colors - **Smart STDERR Handling**: Only applies red fallback color when no ANSI codes are present - **Flexible HTML Elements**: Supports both `<div>` and `<span>` rendering - **Performance Optimized**: Uses `memo()` and efficient ANSI detection - **Consistent Styling**: Single source of truth for all log rendering with proper whitespace preservation ### 📁 Files Modified - `frontend/package.json` - Added `fancy-ansi` dependency - `frontend/src/components/common/RawLogText.tsx` - **New unified component** - `frontend/src/styles/index.css` - Added ANSI color CSS classes - `frontend/src/components/logs/StdoutEntry.tsx` - Refactored to use RawLogText - `frontend/src/components/logs/StderrEntry.tsx` - Refactored to use RawLogText - `frontend/src/components/tasks/TaskDetails/ProcessLogsViewer.tsx` - Refactored to use RawLogText - `frontend/src/components/tasks/TaskDetails/ProcessCard.tsx` - Refactored to use RawLogText - `frontend/src/components/common/__tests__/RawLogText.test.tsx` - **New test file** The implementation follows all the best practices identified by the Oracle and provides a robust, maintainable solution for ANSI log rendering across the entire application. Raw logs will now display with proper colors, formatting, and styling instead of showing escape codes as plain text. * Cleanup script changes for task attempt 287442b9-2ddf-4f29-9b91-ddc18e4a96b7 * lockfile * delete test
This commit is contained in:
committed by
GitHub
parent
85cc1d6211
commit
b868d1917e
16
frontend/package-lock.json
generated
16
frontend/package-lock.json
generated
@@ -34,6 +34,7 @@
|
||||
"click-to-react-component": "^1.1.2",
|
||||
"clsx": "^2.0.0",
|
||||
"diff": "^8.0.2",
|
||||
"fancy-ansi": "^0.1.3",
|
||||
"lucide-react": "^0.539.0",
|
||||
"react": "^18.2.0",
|
||||
"react-diff-viewer-continued": "^3.4.0",
|
||||
@@ -4166,6 +4167,12 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/escape-html": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
|
||||
"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/escape-string-regexp": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
|
||||
@@ -4471,6 +4478,15 @@
|
||||
"integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fancy-ansi": {
|
||||
"version": "0.1.3",
|
||||
"resolved": "https://registry.npmjs.org/fancy-ansi/-/fancy-ansi-0.1.3.tgz",
|
||||
"integrity": "sha512-tRQVTo5jjdSIiydqgzIIEZpKddzSsfGLsSVt6vWdjVm7fbvDTiQkyoPu6Z3dIPlAM4OZk0jP5jmTCX4G8WGgBw==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"escape-html": "^1.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/fast-deep-equal": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
"click-to-react-component": "^1.1.2",
|
||||
"clsx": "^2.0.0",
|
||||
"diff": "^8.0.2",
|
||||
"fancy-ansi": "^0.1.3",
|
||||
"lucide-react": "^0.539.0",
|
||||
"react": "^18.2.0",
|
||||
"react-diff-viewer-continued": "^3.4.0",
|
||||
|
||||
40
frontend/src/components/common/RawLogText.tsx
Normal file
40
frontend/src/components/common/RawLogText.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import { memo } from 'react';
|
||||
import { AnsiHtml } from 'fancy-ansi/react';
|
||||
import { hasAnsi } from 'fancy-ansi';
|
||||
import { clsx } from 'clsx';
|
||||
|
||||
interface RawLogTextProps {
|
||||
content: string;
|
||||
channel?: 'stdout' | 'stderr';
|
||||
as?: 'div' | 'span';
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const RawLogText = memo(
|
||||
({
|
||||
content,
|
||||
channel = 'stdout',
|
||||
as: Component = 'div',
|
||||
className,
|
||||
}: RawLogTextProps) => {
|
||||
// Only apply stderr fallback color when no ANSI codes are present
|
||||
const hasAnsiCodes = hasAnsi(content);
|
||||
const shouldApplyStderrFallback = channel === 'stderr' && !hasAnsiCodes;
|
||||
|
||||
return (
|
||||
<Component
|
||||
className={clsx(
|
||||
'font-mono text-xs break-all whitespace-pre-wrap',
|
||||
shouldApplyStderrFallback && 'text-red-600',
|
||||
className
|
||||
)}
|
||||
>
|
||||
<AnsiHtml text={content} />
|
||||
</Component>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
RawLogText.displayName = 'RawLogText';
|
||||
|
||||
export default RawLogText;
|
||||
@@ -1,11 +1,13 @@
|
||||
import RawLogText from '@/components/common/RawLogText';
|
||||
|
||||
interface StderrEntryProps {
|
||||
content: string;
|
||||
}
|
||||
|
||||
function StderrEntry({ content }: StderrEntryProps) {
|
||||
return (
|
||||
<div className="flex gap-2 text-xs font-mono px-4">
|
||||
<span className="text-red-600 break-all">{content}</span>
|
||||
<div className="flex gap-2 px-4">
|
||||
<RawLogText content={content} channel="stderr" as="span" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import RawLogText from '@/components/common/RawLogText';
|
||||
|
||||
interface StdoutEntryProps {
|
||||
content: string;
|
||||
}
|
||||
|
||||
function StdoutEntry({ content }: StdoutEntryProps) {
|
||||
return (
|
||||
<div className="flex gap-2 text-xs font-mono px-4">
|
||||
<span className="break-all">{content}</span>
|
||||
<div className="flex gap-2 px-4">
|
||||
<RawLogText content={content} channel="stdout" as="span" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import type { ExecutionProcessStatus, ExecutionProcess } from 'shared/types';
|
||||
import { useLogStream } from '@/hooks/useLogStream';
|
||||
import { useProcessConversation } from '@/hooks/useProcessConversation';
|
||||
import DisplayConversationEntry from '@/components/NormalizedConversation/DisplayConversationEntry';
|
||||
import RawLogText from '@/components/common/RawLogText';
|
||||
|
||||
interface ProcessCardProps {
|
||||
process: ExecutionProcess;
|
||||
@@ -173,17 +174,12 @@ function ProcessCard({ process }: ProcessCardProps) {
|
||||
<div className="text-gray-400">No logs available...</div>
|
||||
) : (
|
||||
logs.map((logEntry, index) => (
|
||||
<div key={index} className="break-all">
|
||||
{logEntry.type === 'STDERR' ? (
|
||||
<span className="text-destructive">
|
||||
{logEntry.content}
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-foreground">
|
||||
{logEntry.content}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<RawLogText
|
||||
key={index}
|
||||
content={logEntry.content}
|
||||
channel={logEntry.type === 'STDERR' ? 'stderr' : 'stdout'}
|
||||
as="div"
|
||||
/>
|
||||
))
|
||||
)}
|
||||
<div ref={logEndRef} />
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useEffect, useRef, useState } from 'react';
|
||||
import { Virtuoso, VirtuosoHandle } from 'react-virtuoso';
|
||||
import { AlertCircle } from 'lucide-react';
|
||||
import { useLogStream } from '@/hooks/useLogStream';
|
||||
import RawLogText from '@/components/common/RawLogText';
|
||||
import type { PatchType } from 'shared/types';
|
||||
|
||||
type LogEntry = Extract<PatchType, { type: 'STDOUT' } | { type: 'STDERR' }>;
|
||||
@@ -53,14 +54,13 @@ export default function ProcessLogsViewer({
|
||||
}, [logs.length, atBottom, logs]);
|
||||
|
||||
const formatLogLine = (entry: LogEntry, index: number) => {
|
||||
let className = 'text-sm font-mono px-4 py-1 whitespace-pre-wrap';
|
||||
className +=
|
||||
entry.type === 'STDERR' ? ' text-destructive' : ' text-foreground';
|
||||
|
||||
return (
|
||||
<div key={index} className={className}>
|
||||
{entry.content}
|
||||
</div>
|
||||
<RawLogText
|
||||
key={index}
|
||||
content={entry.content}
|
||||
channel={entry.type === 'STDERR' ? 'stderr' : 'stdout'}
|
||||
className="text-sm px-4 py-1"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -291,3 +291,64 @@
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
|
||||
/* ANSI color classes for fancy-ansi */
|
||||
@layer components {
|
||||
.ansi-red {
|
||||
@apply text-red-500;
|
||||
}
|
||||
.ansi-green {
|
||||
@apply text-green-500;
|
||||
}
|
||||
.ansi-yellow {
|
||||
@apply text-yellow-500;
|
||||
}
|
||||
.ansi-blue {
|
||||
@apply text-blue-500;
|
||||
}
|
||||
.ansi-magenta {
|
||||
@apply text-purple-500;
|
||||
}
|
||||
.ansi-cyan {
|
||||
@apply text-cyan-500;
|
||||
}
|
||||
.ansi-white {
|
||||
@apply text-white;
|
||||
}
|
||||
.ansi-black {
|
||||
@apply text-black;
|
||||
}
|
||||
.ansi-bright-red {
|
||||
@apply text-red-400;
|
||||
}
|
||||
.ansi-bright-green {
|
||||
@apply text-green-400;
|
||||
}
|
||||
.ansi-bright-yellow {
|
||||
@apply text-yellow-400;
|
||||
}
|
||||
.ansi-bright-blue {
|
||||
@apply text-blue-400;
|
||||
}
|
||||
.ansi-bright-magenta {
|
||||
@apply text-purple-400;
|
||||
}
|
||||
.ansi-bright-cyan {
|
||||
@apply text-cyan-400;
|
||||
}
|
||||
.ansi-bright-white {
|
||||
@apply text-gray-200;
|
||||
}
|
||||
.ansi-bright-black {
|
||||
@apply text-gray-700;
|
||||
}
|
||||
.ansi-bold {
|
||||
@apply font-bold;
|
||||
}
|
||||
.ansi-italic {
|
||||
@apply italic;
|
||||
}
|
||||
.ansi-underline {
|
||||
@apply underline;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user