Files
openchamber/packages/ui/src/components/chat/message/toolRenderers.tsx
T
Bohdan Triapitsyn c22e1cbb15 refactor(chat): remove dead syntaxTheme plumbing
The Prism syntaxTheme prop is no longer read after code highlighting moved to
the Shiki worker. Remove the now-dead prop threading and its source.

- Drop syntaxTheme from interfaces, destructures, prop passes, and React.memo
  comparators across ChatMessage, MessageBody, ProgressiveGroup, ToolPart,
  TurnActivity, ToolOutputDialog, and ChatInput.
- Drop the unused _syntaxTheme param from renderWebSearchOutput.
- Remove the dead generateSyntaxTheme usages (ChatMessage memo, PlanView
  unassigned memo) and delete the now-unimported syntaxThemeGenerator module.
2026-06-16 01:07:43 +03:00

816 lines
32 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { cn } from '@/lib/utils';
import { typography } from '@/lib/typography';
import { formatToolInput, detectToolOutputLanguage } from '@/lib/toolHelpers';
import { SimpleMarkdownRenderer } from '../MarkdownRenderer';
import { Icon } from "@/components/icon/Icon";
const cleanOutput = (output: string) => {
let cleaned = output.replace(/^<file>\s*\n?/, '').replace(/\n?<\/file>\s*$/, '');
cleaned = cleaned.replace(/^\s*\d{5}\|\s?/gm, '');
return cleaned.trim();
};
export const hasLspDiagnostics = (output: string): boolean => {
if (!output) return false;
return output.includes('<diagnostics')
|| output.includes('<file_diagnostics>')
|| output.includes('LSP errors detected')
|| output.includes('This file has errors');
};
const stripLspDiagnostics = (output: string): string => {
if (!output) return '';
return output
.replace(/\n{0,2}LSP errors detected[\s\S]*?<diagnostics[^>]*>[\s\S]*?<\/diagnostics>/g, '')
.replace(/\n{0,2}This file has errors[\s\S]*?<\/file_diagnostics>/g, '')
.replace(/<diagnostics[^>]*>[\s\S]*?<\/diagnostics>/g, '')
.replace(/<file_diagnostics>[\s\S]*?<\/file_diagnostics>/g, '')
.trim();
};
const formatInputForDisplay = (input: Record<string, unknown>, toolName?: string) => {
if (!input || typeof input !== 'object') {
return String(input);
}
return formatToolInput(input, toolName || '');
};
const getPatchText = (value: unknown): string | undefined => {
if (typeof value === 'string') {
const trimmed = value.trim();
return trimmed.length > 0 ? trimmed : undefined;
}
if (value && typeof value === 'object') {
const patch = (value as { patch?: unknown }).patch;
if (typeof patch === 'string') {
const trimmed = patch.trim();
return trimmed.length > 0 ? trimmed : undefined;
}
}
return undefined;
};
const getToolMetadataPatch = (metadata?: Record<string, unknown>): string | undefined => {
if (!metadata || typeof metadata !== 'object') {
return undefined;
}
const topLevelPatch = getPatchText((metadata as { patch?: unknown }).patch) ?? getPatchText(metadata.diff);
if (topLevelPatch) {
return topLevelPatch;
}
const files = Array.isArray((metadata as { files?: unknown }).files) ? (metadata as { files: unknown[] }).files : [];
for (const file of files) {
if (!file || typeof file !== 'object') {
continue;
}
const patch = getPatchText((file as { patch?: unknown }).patch) ?? getPatchText((file as { diff?: unknown }).diff);
if (patch) {
return patch;
}
}
return undefined;
};
export const tryParseJsonOutput = (output: string): { data: unknown; isJson: boolean } => {
if (!output || typeof output !== 'string') {
return { data: null, isJson: false };
}
const trimmed = output.trim();
if (!trimmed.startsWith('{') && !trimmed.startsWith('[')) {
return { data: null, isJson: false };
}
if (!trimmed.endsWith('}') && !trimmed.endsWith(']')) {
return { data: null, isJson: false };
}
if (trimmed.length < 2) {
return { data: null, isJson: false };
}
try {
const parsed = JSON.parse(trimmed);
if (parsed !== null && typeof parsed === 'object') {
return { data: parsed, isJson: true };
}
return { data: null, isJson: false };
} catch {
return { data: null, isJson: false };
}
};
export const formatEditOutput = (output: string, toolName: string, metadata?: Record<string, unknown>): string => {
let cleaned = cleanOutput(output);
if ((toolName === 'edit' || toolName === 'multiedit' || toolName === 'write' || toolName === 'apply_patch') && hasLspDiagnostics(cleaned)) {
cleaned = stripLspDiagnostics(cleaned);
}
if ((toolName === 'edit' || toolName === 'multiedit' || toolName === 'apply_patch') && cleaned.trim().length === 0) {
const diff = getToolMetadataPatch(metadata);
if (diff) {
return diff;
}
}
return cleaned;
};
export interface ParsedReadOutputLine {
text: string;
lineNumber: number | null;
isInfo: boolean;
}
export interface ParsedReadToolOutput {
type: 'file' | 'directory' | 'unknown';
lines: ParsedReadOutputLine[];
}
export const parseReadToolOutput = (output: string): ParsedReadToolOutput => {
const typeMatch = output.match(/<type>(file|directory)<\/type>/i);
const detectedType = (typeMatch?.[1]?.toLowerCase() ?? 'unknown') as ParsedReadToolOutput['type'];
const contentMatch = output.match(/<content>([\s\S]*?)<\/content>/i);
const rawContent = contentMatch?.[1] ?? output;
const normalizedContent = rawContent.replace(/\r\n/g, '\n');
const rawLines = normalizedContent.split('\n');
const isTruncationInfoLine = (text: string): boolean => {
return /\(\s*File has more lines\..*offset.*\)/i.test(text.trim());
};
const parsedLines = rawLines.map((line): ParsedReadOutputLine => {
const trimmed = line.trim();
const isInfo = (trimmed.startsWith('(') && trimmed.endsWith(')')) || isTruncationInfoLine(trimmed);
if (detectedType !== 'directory') {
const numberedMatch = line.match(/^(\d+):\s?(.*)$/);
if (numberedMatch) {
const numberedText = numberedMatch[2];
const numberedTrimmed = numberedText.trim();
const numberedIsInfo =
(numberedTrimmed.startsWith('(') && numberedTrimmed.endsWith(')'))
|| isTruncationInfoLine(numberedTrimmed);
return {
lineNumber: numberedIsInfo ? null : Number(numberedMatch[1]),
text: numberedText,
isInfo: numberedIsInfo,
};
}
}
return {
lineNumber: null,
text: line,
isInfo,
};
});
const lines = parsedLines.filter((line, index, arr) => {
if (line.text.trim().length > 0) {
return true;
}
const prev = arr[index - 1];
const next = arr[index + 1];
const adjacentToInfo = Boolean(prev?.isInfo || next?.isInfo);
const hasNumber = line.lineNumber !== null;
// Drop numbered blank lines wrapped around helper/info rows.
if (adjacentToInfo && hasNumber) {
return false;
}
return true;
});
return {
type: detectedType,
lines,
};
};
export const renderListOutput = (output: string, options?: { unstyled?: boolean }) => {
try {
const lines = output.trim().split('\n').filter(Boolean);
if (lines.length === 0) return null;
const items: Array<{ name: string; depth: number; isFile: boolean }> = [];
lines.forEach((line) => {
const match = line.match(/^(\s*)(.+)$/);
if (match) {
const [, spaces, name] = match;
const depth = Math.floor(spaces.length / 2);
const isFile = !name.endsWith('/');
items.push({
name: name.replace(/\/$/, ''),
depth,
isFile,
});
}
});
return (
<div
className={cn(
'w-full min-w-0 font-mono space-y-0.5',
options?.unstyled ? null : 'p-3 bg-muted/20 rounded-xl border border-border/30'
)}
style={typography.tool.popup}
>
{items.map((item, idx) => (
<div key={idx} className="min-w-0" style={{ paddingLeft: `${item.depth * 20}px` }}>
{item.isFile ? (
<span className="text-foreground/90 block truncate">{item.name}</span>
) : (
<span className="font-semibold text-foreground block truncate">{item.name}/</span>
)}
</div>
))}
</div>
);
} catch {
return null;
}
};
const GREP_DOT_STYLE = { backgroundColor: 'var(--status-info)', opacity: 0.6 };
export const renderGrepOutput = (output: string, isMobile: boolean, options?: { unstyled?: boolean }) => {
try {
const lines = output.trim().split('\n').filter(Boolean);
if (lines.length === 0) return null;
const fileGroups: Record<string, Array<{ lineNum: string; content: string }>> = {};
lines.forEach((line) => {
const match = line.match(/^(.+?):(\d+):(.*)$/) || line.match(/^(.+?):(.*)$/);
if (match) {
const [, filepath, lineNumOrContent, content] = match;
const lineNum = content !== undefined ? lineNumOrContent : '';
const actualContent = content !== undefined ? content : lineNumOrContent;
if (!fileGroups[filepath]) {
fileGroups[filepath] = [];
}
fileGroups[filepath].push({ lineNum, content: actualContent });
}
});
return (
<div
className={cn(
'space-y-2 w-full min-w-0',
options?.unstyled ? null : 'p-3 bg-muted/20 rounded-xl border border-border/30'
)}
style={typography.tool.popup}
>
<div className="typography-meta text-muted-foreground mb-2">
Found {lines.length} match{lines.length !== 1 ? 'es' : ''}
</div>
{Object.entries(fileGroups).map(([filepath, matches]) => (
<div key={filepath} className="space-y-1">
<div className={cn('font-medium text-muted-foreground', isMobile ? 'typography-micro' : 'typography-code')}>
{filepath}
</div>
<div className="pl-4 space-y-1">
{matches.map((match, idx) => {
if (!match.lineNum && !match.content) {
return null;
}
return (
<div key={idx} className={cn('flex items-start gap-2 min-w-0', isMobile ? 'typography-micro' : 'typography-code')}>
<div className="w-1.5 h-1.5 rounded-full flex-shrink-0 mt-1.5" style={GREP_DOT_STYLE} />
<div className="flex gap-2 min-w-0 flex-1">
{match.lineNum && (
<span className="text-muted-foreground font-mono whitespace-nowrap">
Line {match.lineNum}:
</span>
)}
<span className="text-foreground font-mono break-words flex-1">
{match.content || '\u00A0'}
</span>
</div>
</div>
);
})}
</div>
</div>
))}
</div>
);
} catch {
return null;
}
};
const GLOB_DOT_STYLE = { backgroundColor: 'var(--status-info)', opacity: 0.6 };
export const renderGlobOutput = (output: string, isMobile: boolean, options?: { unstyled?: boolean }) => {
try {
const paths = output.trim().split('\n').filter(Boolean);
if (paths.length === 0) return null;
const groups: Record<string, string[]> = {};
paths.forEach((path) => {
const lastSlash = path.lastIndexOf('/');
const dir = lastSlash > 0 ? path.substring(0, lastSlash) : '/';
const filename = lastSlash >= 0 ? path.substring(lastSlash + 1) : path;
if (!groups[dir]) {
groups[dir] = [];
}
groups[dir].push(filename);
});
const sortedDirs = Object.keys(groups).sort();
return (
<div
className={cn(
'space-y-2 w-full min-w-0',
options?.unstyled ? null : 'p-3 bg-muted/20 rounded-xl border border-border/30'
)}
style={typography.tool.popup}
>
<div className="typography-meta text-muted-foreground mb-2">
Found {paths.length} file{paths.length !== 1 ? 's' : ''}
</div>
{sortedDirs.map((dir) => (
<div key={dir} className="space-y-1">
<div className={cn('font-medium text-muted-foreground', isMobile ? 'typography-micro' : 'typography-code')}>
{dir}/
</div>
<div className={cn('pl-4 grid gap-1', isMobile ? 'grid-cols-1' : 'grid-cols-2')}>
{groups[dir].sort().map((filename) => (
<div key={filename} className={cn('flex items-center gap-2 min-w-0', isMobile ? 'typography-micro' : 'typography-code')}>
<div className="w-1.5 h-1.5 rounded-full flex-shrink-0" style={GLOB_DOT_STYLE} />
<span className="text-foreground font-mono truncate">{filename}</span>
</div>
))}
</div>
</div>
))}
</div>
);
} catch {
return null;
}
};
type Todo = {
id?: string;
content: string;
status: 'in_progress' | 'pending' | 'completed' | 'cancelled';
priority?: 'high' | 'medium' | 'low';
};
export const renderTodoOutput = (
output: string,
labels: {
total: string;
inProgress: string;
pending: string;
completed: string;
cancelled: string;
},
options?: { unstyled?: boolean },
) => {
try {
const todos = JSON.parse(output) as Todo[];
if (!Array.isArray(todos)) {
return null;
}
const todosByStatus = todos.reduce((acc, t) => {
const status = t.status as keyof typeof acc;
if (status in acc) acc[status].push(t);
return acc;
}, { in_progress: [] as Todo[], pending: [] as Todo[], completed: [] as Todo[], cancelled: [] as Todo[] });
const getPriorityDot = (priority?: string) => {
const baseClasses = 'w-2 h-2 rounded-full flex-shrink-0 mt-1';
switch (priority) {
case 'high':
return <div className={baseClasses} style={{ backgroundColor: 'var(--status-error)' }} />;
case 'medium':
return <div className={baseClasses} style={{ backgroundColor: 'var(--primary)' }} />;
case 'low':
return <div className={baseClasses} style={{ backgroundColor: 'var(--status-info)' }} />;
default:
return <div className={baseClasses} style={{ backgroundColor: 'var(--muted-foreground)', opacity: 0.5 }} />;
}
};
return (
<div
className={cn(
'space-y-3 w-full min-w-0',
options?.unstyled ? null : 'p-3 bg-muted/20 rounded-xl border border-border/30'
)}
style={typography.tool.popup}
>
<div className="flex gap-4 typography-meta pb-2 border-b border-border/20">
<span className="font-medium" style={{ color: 'var(--muted-foreground)' }}>{labels.total}: {todos.length}</span>
{todosByStatus.in_progress.length > 0 && (
<span className="font-medium" style={{ color: 'var(--foreground)' }}>{labels.inProgress}: {todosByStatus.in_progress.length}</span>
)}
{todosByStatus.pending.length > 0 && (
<span style={{ color: 'var(--muted-foreground)' }}>{labels.pending}: {todosByStatus.pending.length}</span>
)}
{todosByStatus.completed.length > 0 && (
<span style={{ color: 'var(--status-success)' }}>{labels.completed}: {todosByStatus.completed.length}</span>
)}
{todosByStatus.cancelled.length > 0 && (
<span style={{ color: 'var(--muted-foreground)', opacity: 0.5 }}>{labels.cancelled}: {todosByStatus.cancelled.length}</span>
)}
</div>
{todosByStatus.in_progress.length > 0 && (
<div className="space-y-2">
<div className="flex items-center gap-2">
<div className="w-2 h-2 rounded-full animate-pulse" style={{ backgroundColor: 'var(--foreground)' }} />
<span className="typography-meta font-semibold text-foreground uppercase tracking-wide">{labels.inProgress}</span>
</div>
<div className="space-y-1.5 pl-4">
{todosByStatus.in_progress.map((todo, idx) => (
<div key={todo.id || idx} className="flex items-start gap-2">
{getPriorityDot(todo.priority)}
<span className="typography-code text-foreground flex-1 leading-relaxed">{todo.content}</span>
</div>
))}
</div>
</div>
)}
{todosByStatus.pending.length > 0 && (
<div className="space-y-2">
<div className="flex items-center gap-2">
<div className="w-2 h-2 rounded-full bg-muted-foreground/50" />
<span className="typography-meta font-semibold text-muted-foreground uppercase tracking-wide">{labels.pending}</span>
</div>
<div className="space-y-1.5 pl-4">
{todosByStatus.pending.map((todo, idx) => (
<div key={todo.id || idx} className="flex items-start gap-2">
{getPriorityDot(todo.priority)}
<span className="typography-code text-foreground flex-1 leading-relaxed">{todo.content}</span>
</div>
))}
</div>
</div>
)}
{todosByStatus.completed.length > 0 && (
<div className="space-y-2">
<div className="flex items-center gap-2">
<Icon name="check" className="w-3 h-3" style={{ color: 'var(--status-success)' }}/>
<span className="typography-meta font-semibold uppercase tracking-wide" style={{ color: 'var(--status-success)' }}>{labels.completed}</span>
</div>
<div className="space-y-1.5 pl-4">
{todosByStatus.completed.map((todo, idx) => (
<div key={todo.id || idx} className="flex items-start gap-2">
<Icon name="check" className="w-3 h-3 mt-0.5 flex-shrink-0" style={{ color: 'var(--status-success)', opacity: 0.7 }}/>
<span className="typography-code text-foreground flex-1 leading-relaxed">{todo.content}</span>
</div>
))}
</div>
</div>
)}
{todosByStatus.cancelled.length > 0 && (
<div className="space-y-2">
<div className="flex items-center gap-2">
<span className="w-3 h-3 text-muted-foreground/50">×</span>
<span className="typography-meta font-semibold text-muted-foreground/50 uppercase tracking-wide">{labels.cancelled}</span>
</div>
<div className="space-y-1.5 pl-4">
{todosByStatus.cancelled.map((todo, idx) => (
<div key={todo.id || idx} className="flex items-start gap-2">
<span className="w-3 h-3 text-muted-foreground/50 mt-0.5 flex-shrink-0">×</span>
<span className="typography-code text-muted-foreground/50 line-through flex-1 leading-relaxed">{todo.content}</span>
</div>
))}
</div>
</div>
)}
</div>
);
} catch {
return null;
}
};
export const renderWebSearchOutput = (output: string, options?: { unstyled?: boolean }) => {
try {
return (
<div
className={cn(
'typography-code max-w-none w-full min-w-0',
options?.unstyled ? null : 'p-3 bg-muted/20 rounded-xl border border-border/20'
)}
style={typography.tool.popup}
>
<SimpleMarkdownRenderer content={output} variant="tool" />
</div>
);
} catch {
return null;
}
};
export type DiffLineType = 'context' | 'added' | 'removed';
export interface UnifiedDiffLine {
type: DiffLineType;
lineNumber: number | null;
content: string;
}
export interface UnifiedDiffHunk {
file: string;
oldStart: number;
newStart: number;
lines: UnifiedDiffLine[];
}
export interface SideBySideDiffLine {
leftLine: { type: 'context' | 'removed' | 'empty'; lineNumber: number | null; content: string };
rightLine: { type: 'context' | 'added' | 'empty'; lineNumber: number | null; content: string };
}
export interface SideBySideDiffHunk {
file: string;
oldStart: number;
newStart: number;
lines: SideBySideDiffLine[];
}
export const parseDiffToUnified = (diffText: string): UnifiedDiffHunk[] => {
const lines = diffText.split('\n');
let currentFile = '';
const hunks: UnifiedDiffHunk[] = [];
let i = 0;
while (i < lines.length) {
const line = lines[i];
if (line.startsWith('Index:') || line.startsWith('===') || line.startsWith('---') || line.startsWith('+++')) {
if (line.startsWith('Index:')) {
currentFile = line.split(' ')[1].split('/').pop() || 'file';
}
i++;
continue;
}
if (line.startsWith('@@')) {
const match = line.match(/@@ -(\d+),\d+ \+(\d+),\d+ @@/);
const oldStart = match ? parseInt(match[1]) : 0;
const newStart = match ? parseInt(match[2]) : 0;
const unifiedLines: UnifiedDiffLine[] = [];
let oldLineNum = oldStart;
let newLineNum = newStart;
let j = i + 1;
while (j < lines.length && !lines[j].startsWith('@@') && !lines[j].startsWith('Index:')) {
const contentLine = lines[j];
if (contentLine.startsWith('+')) {
unifiedLines.push({ type: 'added', lineNumber: newLineNum, content: contentLine.substring(1) });
newLineNum++;
} else if (contentLine.startsWith('-')) {
unifiedLines.push({ type: 'removed', lineNumber: oldLineNum, content: contentLine.substring(1) });
oldLineNum++;
} else if (contentLine.startsWith(' ')) {
unifiedLines.push({ type: 'context', lineNumber: newLineNum, content: contentLine.substring(1) });
oldLineNum++;
newLineNum++;
}
j++;
}
hunks.push({
file: currentFile,
oldStart,
newStart,
lines: unifiedLines,
});
i = j;
continue;
}
i++;
}
return hunks;
};
export const parseDiffToLines = (diffText: string): SideBySideDiffHunk[] => {
const lines = diffText.split('\n');
let currentFile = '';
const hunks: SideBySideDiffHunk[] = [];
let i = 0;
while (i < lines.length) {
const line = lines[i];
if (line.startsWith('Index:') || line.startsWith('===') || line.startsWith('---') || line.startsWith('+++')) {
if (line.startsWith('Index:')) {
currentFile = line.split(' ')[1].split('/').pop() || 'file';
}
i++;
continue;
}
if (line.startsWith('@@')) {
const match = line.match(/@@ -(\d+),\d+ \+(\d+),\d+ @@/);
const oldStart = match ? parseInt(match[1]) : 0;
const newStart = match ? parseInt(match[2]) : 0;
const changes: Array<{
type: 'context' | 'added' | 'removed';
content: string;
oldLine?: number;
newLine?: number;
}> = [];
let oldLineNum = oldStart;
let newLineNum = newStart;
let j = i + 1;
while (j < lines.length && !lines[j].startsWith('@@') && !lines[j].startsWith('Index:')) {
const contentLine = lines[j];
if (contentLine.startsWith('+')) {
changes.push({ type: 'added', content: contentLine.substring(1), newLine: newLineNum });
newLineNum++;
} else if (contentLine.startsWith('-')) {
changes.push({ type: 'removed', content: contentLine.substring(1), oldLine: oldLineNum });
oldLineNum++;
} else if (contentLine.startsWith(' ')) {
changes.push({
type: 'context',
content: contentLine.substring(1),
oldLine: oldLineNum,
newLine: newLineNum,
});
oldLineNum++;
newLineNum++;
}
j++;
}
const alignedLines: Array<{
leftLine: { type: 'context' | 'removed' | 'empty'; lineNumber: number | null; content: string };
rightLine: { type: 'context' | 'added' | 'empty'; lineNumber: number | null; content: string };
}> = [];
const leftSide: Array<{ type: 'context' | 'removed'; lineNumber: number; content: string }> = [];
const rightSide: Array<{ type: 'context' | 'added'; lineNumber: number; content: string }> = [];
changes.forEach((change) => {
if (change.type === 'context') {
leftSide.push({ type: 'context', lineNumber: change.oldLine!, content: change.content });
rightSide.push({ type: 'context', lineNumber: change.newLine!, content: change.content });
} else if (change.type === 'removed') {
leftSide.push({ type: 'removed', lineNumber: change.oldLine!, content: change.content });
} else if (change.type === 'added') {
rightSide.push({ type: 'added', lineNumber: change.newLine!, content: change.content });
}
});
const alignmentPoints: Array<{ leftIdx: number; rightIdx: number }> = [];
leftSide.forEach((leftItem, leftIdx) => {
if (leftItem.type === 'context') {
const rightIdx = rightSide.findIndex((rightItem, rIdx) =>
rightItem.type === 'context' &&
rightItem.content === leftItem.content &&
!alignmentPoints.some((ap) => ap.rightIdx === rIdx)
);
if (rightIdx >= 0) {
alignmentPoints.push({ leftIdx, rightIdx });
}
}
});
alignmentPoints.sort((a, b) => a.leftIdx - b.leftIdx);
let leftIdx = 0;
let rightIdx = 0;
let alignIdx = 0;
while (leftIdx < leftSide.length || rightIdx < rightSide.length) {
const nextAlign = alignIdx < alignmentPoints.length ? alignmentPoints[alignIdx] : null;
if (nextAlign && leftIdx === nextAlign.leftIdx && rightIdx === nextAlign.rightIdx) {
const leftItem = leftSide[leftIdx];
const rightItem = rightSide[rightIdx];
alignedLines.push({
leftLine: {
type: 'context',
lineNumber: leftItem.lineNumber,
content: leftItem.content,
},
rightLine: {
type: 'context',
lineNumber: rightItem.lineNumber,
content: rightItem.content,
},
});
leftIdx++;
rightIdx++;
alignIdx++;
} else {
const needProcessLeft = leftIdx < leftSide.length && (!nextAlign || leftIdx < nextAlign.leftIdx);
const needProcessRight = rightIdx < rightSide.length && (!nextAlign || rightIdx < nextAlign.rightIdx);
if (needProcessLeft && needProcessRight) {
const leftItem = leftSide[leftIdx];
const rightItem = rightSide[rightIdx];
alignedLines.push({
leftLine: {
type: leftItem.type,
lineNumber: leftItem.lineNumber,
content: leftItem.content,
},
rightLine: {
type: rightItem.type,
lineNumber: rightItem.lineNumber,
content: rightItem.content,
},
});
leftIdx++;
rightIdx++;
} else if (needProcessLeft) {
const leftItem = leftSide[leftIdx];
alignedLines.push({
leftLine: {
type: leftItem.type,
lineNumber: leftItem.lineNumber,
content: leftItem.content,
},
rightLine: {
type: 'empty',
lineNumber: null,
content: '',
},
});
leftIdx++;
} else if (needProcessRight) {
const rightItem = rightSide[rightIdx];
alignedLines.push({
leftLine: {
type: 'empty',
lineNumber: null,
content: '',
},
rightLine: {
type: rightItem.type,
lineNumber: rightItem.lineNumber,
content: rightItem.content,
},
});
rightIdx++;
} else {
break;
}
}
}
hunks.push({
file: currentFile,
oldStart,
newStart,
lines: alignedLines,
});
i = j;
continue;
}
i++;
}
return hunks;
};
export const detectLanguageFromOutput = (output: string, toolName: string, input?: Record<string, unknown>) => {
return detectToolOutputLanguage(toolName, output, input);
};
export { formatInputForDisplay };