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:
Louis Knight-Webb
2025-08-19 14:19:50 +01:00
committed by GitHub
parent 85cc1d6211
commit b868d1917e
9 changed files with 155 additions and 22 deletions

View File

@@ -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",

View File

@@ -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",

View 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;

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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} />

View File

@@ -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"
/>
);
};

View File

@@ -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;
}
}