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
5 changes: 5 additions & 0 deletions components/AgentChat/ActivityFeed.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
import { Loader } from '@/components/ui/Loader';
import { cn } from '@/utils/styles';
import { MarkdownMessage } from './MarkdownMessage';
import { CodeExecutionPanel } from './CodeExecutionPanel';
import { answerRevealKey, isRevealable, narrationRevealKey } from '@/hooks/useTextReveal';
import type {
ActivityCallStatus,
Expand All @@ -45,6 +46,7 @@ const TOOL_ICONS: Record<string, ComponentType<{ className?: string }>> = {
get_author_works: BookOpen,
get_work_fulltext: FileSearch,
code_execution: SquareTerminal,
bash_code_execution: SquareTerminal,
};

/**
Expand Down Expand Up @@ -170,6 +172,9 @@ function ToolCallRow({ call }: { readonly call: ChatToolCallActivity }) {
{call.detail && (
<p className="mt-1.5 break-words leading-relaxed text-gray-500">{call.detail}</p>
)}
{call.code_execution && (
<CodeExecutionPanel execution={call.code_execution} tool={call.tool} />
)}
{call.sources && call.sources.length > 0 && <SourceLinks sources={call.sources} />}
</div>
</div>
Expand Down
104 changes: 104 additions & 0 deletions components/AgentChat/CodeExecutionPanel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
'use client';

import { useId, useState } from 'react';
import { ChevronDown, ChevronRight } from 'lucide-react';
import type { ChatCodeExecution } from '@/types/agentChat';

function previewLabel(hasCode: boolean, hasOutput: boolean): string {
if (hasCode && hasOutput) return 'Code and output';
if (hasCode) return 'Code';
if (hasOutput) return 'Output';
return 'Execution details';
}

function TextPreview({
label,
text,
truncated,
}: {
readonly label: string;
readonly text: string;
readonly truncated?: boolean;
}) {
return (
<div>
<p className="mb-1.5 text-xs font-medium text-gray-600">{label}</p>
{/* Treat code and stdout as literal text, including HTML and Markdown. */}
<pre
// Keyboard scrolling needs explicit focusability in browsers such as Safari.
tabIndex={0}

Check warning on line 29 in components/AgentChat/CodeExecutionPanel.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

`tabIndex` should only be declared on interactive elements.

See more on https://sonarcloud.io/project/issues?id=ResearchHub_web&issues=AaCmTtfL-ykPmY755U2u&open=AaCmTtfL-ykPmY755U2u&pullRequest=1108
role="region"
aria-label={label}
className="max-h-64 overflow-auto whitespace-pre rounded-md bg-gray-50 p-3 text-xs leading-relaxed text-gray-800 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500"
>

Check warning on line 33 in components/AgentChat/CodeExecutionPanel.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use <section aria-label=...>, or <section aria-labelledby=...> instead of the "region" role to ensure accessibility across all devices.

See more on https://sonarcloud.io/project/issues?id=ResearchHub_web&issues=AaCmUsUn1v8MDSs8M64B&open=AaCmUsUn1v8MDSs8M64B&pullRequest=1108
<code>{text}</code>
</pre>
{truncated && <p className="mt-1 text-xs text-gray-500">{label} preview truncated.</p>}
</div>
);
}

/** A compact disclosure shared by live and historical activity on both chat surfaces. */
export function CodeExecutionPanel({
execution,
tool,
}: {
readonly execution: ChatCodeExecution;
readonly tool: string;
}) {
const [expanded, setExpanded] = useState(false);
const panelId = useId();
const hasCode = Boolean(execution.code);
const hasOutput = Boolean(execution.output);
const hasReturnCode = execution.return_code != null;
const hasOutputCount = execution.output_count != null;

if (!hasCode && !hasOutput && !hasReturnCode && !hasOutputCount) return null;

const label = previewLabel(hasCode, hasOutput);

return (
<div className="mt-2">
<button
type="button"
aria-expanded={expanded}
aria-controls={panelId}
onClick={() => setExpanded(!expanded)}
className="flex items-center gap-1 rounded py-1 text-xs font-medium text-gray-500 transition-colors hover:text-gray-800 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500"
>
{expanded ? (
<ChevronDown className="h-3.5 w-3.5 shrink-0" aria-hidden="true" />
) : (
<ChevronRight className="h-3.5 w-3.5 shrink-0" aria-hidden="true" />
)}
{label}
</button>
<div id={panelId} hidden={!expanded} className="mt-2 min-w-0 space-y-3">
{hasCode && (
<TextPreview
label={tool === 'bash_code_execution' ? 'Command' : 'Code'}
text={execution.code!}
truncated={execution.code_truncated}
/>
)}
{hasOutput && (
<TextPreview
label="Output"
text={execution.output!}
truncated={execution.output_truncated}
/>
)}
{(hasReturnCode || hasOutputCount) && (
<div className="flex flex-wrap gap-x-3 gap-y-1 text-xs text-gray-500">
{hasReturnCode && <span>Exit code: {execution.return_code}</span>}
{hasOutputCount && (
<span>
{execution.output_count} output {execution.output_count === 1 ? 'item' : 'items'}
</span>
)}
</div>
)}
</div>
</div>
);
}
17 changes: 16 additions & 1 deletion types/agentChat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,19 @@ export interface ChatThinkingActivity {
at: string;
}

/** Selected public previews only; raw provider payloads are never part of the feed. */
export interface ChatCodeExecution {
/** Plain-text code or shell command, bounded by the backend to 12,000 characters. */
code?: string;
code_truncated?: boolean;
/** Readable stdout, bounded to 2,000 characters. Omitted for encrypted output. */
output?: string;
output_truncated?: boolean;
return_code?: number;
/** Number of output items; their private file identifiers are not exposed. */
output_count?: number;
}

export interface ChatToolCallActivity {
type: 'tool_call';
/** Machine name (e.g. `web_search`). Only used to pick an icon — new tools appear without notice. */
Expand All @@ -59,8 +72,10 @@ export interface ChatToolCallActivity {
status: ActivityCallStatus;
started_at: string | null;
finished_at: string | null;
/** Optional query/name behind the call, ≤200 chars. */
/** Optional query/name behind the call or a public code execution outcome summary. */
detail?: string | null;
/** Optional code execution preview, available on both assistant and notebook calls. */
code_execution?: ChatCodeExecution | null;
/** Present only on a succeeded `edit_note`: the note version the agent produced. */
note_version_id?: number | null;
/** Present only on a succeeded `create_note` (assistant surface): the note it made. */
Expand Down
Loading