Skip to content
Merged
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
13 changes: 12 additions & 1 deletion src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,17 @@ const App: React.FC<{ router?: boolean }> = ({ router = true }) => {
}
};
}

// Check if it is an ATIF trajectory (steps array)
if (data.steps && Array.isArray(data.steps)) {
console.log('Detected ATIF trajectory format - using trajectory viewer');
return {
content: {
trajectoryData: data.steps,
fileType: 'trajectory'
}
};
}

// If it's not in a recognized format, return as is
console.log('Unknown format, passing as trajectory data');
Expand Down Expand Up @@ -829,4 +840,4 @@ const App: React.FC<{ router?: boolean }> = ({ router = true }) => {
return router ? <BrowserRouter>{content}</BrowserRouter> : content;
};

export default App;
export default App;
59 changes: 59 additions & 0 deletions src/components/share/__tests__/trajectory-list.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { render, screen } from '@testing-library/react';
import { describe, expect, it } from 'vitest';
import { TrajectoryList } from '../trajectory-list';

describe('TrajectoryList', () => {
it('renders ATIF steps with visible headers and content', () => {
render(
<TrajectoryList
trajectory={[
{
step_id: 1,
timestamp: '2026-09-14T02:40:44.835462',
source: 'system',
message: 'System line 1\nSystem line 2\nSystem line 3\nSystem line 4\nSystem line 5\nSystem line 6'
},
{
step_id: 2,
timestamp: '2026-09-14T02:40:44.835462',
source: 'agent',
message: '',
tool_calls: [{ function_name: 'terminal', arguments: { command: 'command 1\ncommand 2\ncommand 3\ncommand 4\ncommand 5\ncommand 6\ncommand 7\ncommand 8\ncommand 9\ncommand 10\ncommand 11' } }],
observation: { results: [{ content: 'file 1\nfile 2\nfile 3\nfile 4\nfile 5\nfile 6\nfile 7\nfile 8\nfile 9\nfile 10\nfile 11' }] }
},
{
step_id: 3,
timestamp: '2026-09-14T02:40:44.835462',
source: 'agent',
message: '',
tool_calls: [{ function_name: 'think', arguments: { thought: 'I should inspect the next file.' } }],
observation: { results: [{ content: 'Your thought has been logged.' }] }
},
{
step_id: 4,
timestamp: '2026-09-14T02:40:44.835462',
source: 'agent',
message: '',
tool_calls: [{ function_name: 'file_editor', arguments: { command: 'view', path: '/app/data/README.md' } }],
observation: { results: [{ content: 'README contents' }] }
}
] as any}
/>
);

expect(screen.getByText('System step #1')).toBeInTheDocument();
expect(screen.getByText('Agent step #2')).toBeInTheDocument();
expect(document.body.textContent).toContain('System line 1');
expect(document.body.textContent).not.toContain('System line 6');
expect(screen.getAllByRole('button', { name: 'Show more' })).toHaveLength(3);
expect(document.body.textContent).toContain('terminal');
expect(document.body.textContent).toContain('file 1');
expect(screen.getByText('think')).toBeInTheDocument();
expect(screen.getByText('I should inspect the next file.')).toBeInTheDocument();
expect(screen.getByText('Agent step #4')).toBeInTheDocument();
expect(screen.getByText('Operation:')).toBeInTheDocument();
expect(screen.getByText('view')).toBeInTheDocument();
expect(screen.getByText('/app/data/README.md')).toBeInTheDocument();
expect(screen.getByText('README contents')).toBeInTheDocument();
});
});
46 changes: 46 additions & 0 deletions src/components/share/expandable-content.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import React, { useState } from 'react';
import { CSyntaxHighlighter } from '../syntax-highlighter';

interface ExpandableContentProps {
content: string;
maxLines: number;
language?: string;
renderContent?: (content: string) => React.ReactNode;
}

export const ExpandableContent: React.FC<ExpandableContentProps> = ({
content,
maxLines,
language,
renderContent
}) => {
const [isExpanded, setIsExpanded] = useState(false);
const lines = content.split('\n');
const isExpandable = lines.length > maxLines;
const displayedContent = isExpanded ? content : lines.slice(0, maxLines).join('\n');
const renderedContent = renderContent
? renderContent(displayedContent)
: language
? <CSyntaxHighlighter language={language}>{displayedContent}</CSyntaxHighlighter>
: <pre className="whitespace-pre-wrap text-sm text-gray-700 dark:text-gray-300">{displayedContent}</pre>;

return (
<div className="space-y-1">
{renderedContent}
{isExpandable && (
<div className="flex justify-center">
<button
type="button"
onClick={(event) => {
event.stopPropagation();
setIsExpanded(!isExpanded);
}}
className="text-xs text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-300 px-3 py-1 rounded-md bg-blue-50 dark:bg-blue-900/20 hover:bg-blue-100 dark:hover:bg-blue-900/30 transition-colors"
>
{isExpanded ? 'Show less' : 'Show more'}
</button>
</div>
)}
</div>
);
};
17 changes: 9 additions & 8 deletions src/components/share/trajectory-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ interface TrajectoryTempateProps {
className?: React.HTMLAttributes<HTMLDivElement>["className"];
originalJson?: any;
defaultCollapsed?: boolean;
collapsedPreview?: React.ReactNode;
timestamp?: string;
}

Expand All @@ -15,7 +16,7 @@ interface TrajectoryCardType extends React.FC<TrajectoryTempateProps> {
Body: React.FC<TrajectoryCardBodyProps>;
}

export const TrajectoryCard: TrajectoryCardType = ({ children, className, originalJson, defaultCollapsed = false, timestamp }) => {
export const TrajectoryCard: TrajectoryCardType = ({ children, className, originalJson, defaultCollapsed = false, collapsedPreview, timestamp }) => {
const [isModalOpen, setIsModalOpen] = useState(false);
const [isCollapsed, setIsCollapsed] = useState(defaultCollapsed);

Expand Down Expand Up @@ -67,12 +68,12 @@ export const TrajectoryCard: TrajectoryCardType = ({ children, className, origin
)}

{/* Render body only if not collapsed, otherwise show hint */}
{!isCollapsed ? (
body
) : (
<div className="px-2 py-1 text-[10px] text-gray-500 dark:text-gray-400 italic">
Content collapsed. Click the arrow icon in the top right to expand.
</div>
{!isCollapsed ? body : (
collapsedPreview || (
<div className="px-2 py-1 text-[10px] text-gray-500 dark:text-gray-400 italic">
Content collapsed. Click the arrow icon in the top right to expand.
</div>
)
)}

{originalJson && (
Expand Down Expand Up @@ -139,4 +140,4 @@ const TrajectoryCardBody: React.FC<TrajectoryCardBodyProps> = ({ children }) =>
};

TrajectoryCard.Header = TrajectoryCardHeader;
TrajectoryCard.Body = TrajectoryCardBody;
TrajectoryCard.Body = TrajectoryCardBody;
145 changes: 145 additions & 0 deletions src/components/share/trajectory-list-items/atif-step.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import React from 'react';
import { CMarkdown } from '../../markdown';
import { CSyntaxHighlighter } from '../../syntax-highlighter';
import { ExpandableContent } from '../expandable-content';
import { TrajectoryCard } from '../trajectory-card';

interface AtifToolCall {
function_name?: string;
arguments?: unknown;
tool_call_id?: string;
}

interface AtifObservationResult {
content?: unknown;
source_call_id?: string;
}

export interface AtifStep {
step_id?: number;
timestamp?: string;
source?: string;
message?: string;
model_name?: string;
tool_calls?: AtifToolCall[];
observation?: {
results?: AtifObservationResult[];
};
}

interface AtifStepProps {
step: AtifStep;
}

const sourceStyles: Record<string, string> = {
system: 'bg-slate-100 dark:bg-slate-800/50 text-slate-800 dark:text-slate-100',
user: 'bg-blue-100 dark:bg-blue-800/50 text-blue-800 dark:text-blue-100',
agent: 'bg-amber-100 dark:bg-amber-800/50 text-amber-800 dark:text-amber-100'
};

const getArgument = (toolCall: AtifToolCall, name: string): unknown => {
if (!toolCall.arguments || typeof toolCall.arguments !== 'object') return undefined;
return (toolCall.arguments as Record<string, unknown>)[name];
};

const renderFileEditorArguments = (toolCall: AtifToolCall) => {
const command = getArgument(toolCall, 'command');
const path = getArgument(toolCall, 'path');
const patch = getArgument(toolCall, 'patch');
const fileText = getArgument(toolCall, 'file_text');

return (
<div className="p-2 space-y-1 text-xs text-gray-700 dark:text-gray-300">
{typeof command === 'string' && (
<div><span className="text-gray-500 dark:text-gray-400">Operation:</span> <code>{command}</code></div>
)}
{typeof path === 'string' && (
<div><span className="text-gray-500 dark:text-gray-400">Path:</span> <code className="break-all">{path}</code></div>
)}
{typeof patch === 'string' && <ExpandableContent content={patch} maxLines={10} language="diff" />}
{typeof fileText === 'string' && <ExpandableContent content={fileText} maxLines={10} language="text" />}
</div>
);
};

export const AtifStepComponent: React.FC<AtifStepProps> = ({ step }) => {
const source = step.source || 'unknown';
const toolCalls = Array.isArray(step.tool_calls) ? step.tool_calls : [];
const results = Array.isArray(step.observation?.results) ? step.observation.results : [];
const message = typeof step.message === 'string' ? step.message : '';
const headerSource = `${source.charAt(0).toUpperCase()}${source.slice(1)}`;

return (
<TrajectoryCard
className="bg-gray-50 dark:bg-gray-900/20 border border-gray-200 dark:border-gray-700"
originalJson={step}
timestamp={step.timestamp}
>
<TrajectoryCard.Header className={sourceStyles[source] || sourceStyles.agent}>
{headerSource} step{step.step_id !== undefined ? ` #${step.step_id}` : ''}
{step.model_name && <span className="ml-2 font-normal opacity-75">({step.model_name})</span>}
</TrajectoryCard.Header>
<TrajectoryCard.Body>
{message && (
<div className="text-sm text-gray-700 dark:text-gray-300">
{source === 'system' ? (
<ExpandableContent content={message} maxLines={5} renderContent={(content) => <CMarkdown>{content}</CMarkdown>} />
) : <CMarkdown>{message}</CMarkdown>}
</div>
)}

{toolCalls.length > 0 && (
<div className="space-y-2">
<div className="text-xs font-medium text-gray-500 dark:text-gray-400">
Tool calls ({toolCalls.length})
</div>
{toolCalls.map((toolCall, index) => (
<div key={toolCall.tool_call_id || index} className="rounded border border-amber-200 dark:border-amber-800/60 overflow-hidden">
<div className="px-2 py-1 bg-amber-50 dark:bg-amber-900/20 text-xs font-medium text-amber-800 dark:text-amber-200">
{toolCall.function_name || 'Unnamed tool'}
</div>
{toolCall.function_name === 'file_editor' ? (
renderFileEditorArguments(toolCall)
) : toolCall.function_name === 'think' && typeof getArgument(toolCall, 'thought') === 'string' ? (
<div className="p-2 text-sm text-gray-700 dark:text-gray-300">
<ExpandableContent
content={getArgument(toolCall, 'thought') as string}
maxLines={10}
renderContent={(content) => <CMarkdown>{content}</CMarkdown>}
/>
</div>
) : toolCall.function_name === 'terminal' && typeof getArgument(toolCall, 'command') === 'string' ? (
<ExpandableContent content={getArgument(toolCall, 'command') as string} maxLines={10} language="shell" />
) : toolCall.arguments !== undefined && (
<CSyntaxHighlighter language="json">
{JSON.stringify(toolCall.arguments, null, 2)}
</CSyntaxHighlighter>
)}
</div>
))}
</div>
)}

{results.length > 0 && (
<div className="space-y-2">
<div className="text-xs font-medium text-gray-500 dark:text-gray-400">
{toolCalls.some(toolCall => toolCall.function_name === 'file_editor') ? 'File editor output' : 'Tool output'} ({results.length})
</div>
{results.map((result, index) => (
<ExpandableContent
key={result.source_call_id || index}
content={typeof result.content === 'string' ? result.content : JSON.stringify(result.content, null, 2)}
maxLines={10}
language="text"
/>
))}
</div>
)}

{!message && toolCalls.length === 0 && results.length === 0 && (
<div className="text-sm text-gray-500 dark:text-gray-400">No content in this step.</div>
)}
</TrajectoryCard.Body>
</TrajectoryCard>
);
};
Loading
Loading