fix: improve activity rendering, spacing, and path truncation (#654)
* fix: polish activity tool rows and message spacing Show each tool call as its own row in live and activity views Keep tool descriptions inline with truncation and cleaner read-file paths Tune spacing, todo counts, and bash timing display for clearer chat flow * fix: refine activity tool rows and truncation behavior Make activity collapse behavior consistent across modes and rename default mode label to Expanded Show tool descriptions inline with improved spacing and one-row-per-tool rendering Fix read/edit path truncation to clip from the start while keeping edit filenames highlighted * fix: resolve chat activity lint regressions Remove unused activity render mode state in message body Mark path helper animation arg as intentionally unused to satisfy lint
This commit is contained in:
committed by
GitHub
parent
9605fa5106
commit
b7196e65c9
@@ -33,7 +33,7 @@ import { toast } from '@/components/ui';
|
||||
import { formatTimestampForDisplay } from './timeFormat';
|
||||
import { ToolRevealOnMount } from './parts/ToolRevealOnMount';
|
||||
import { StaticToolRow } from './parts/ProgressiveGroup';
|
||||
import { getStaticGroupToolName, isExpandableTool, isStandaloneTool } from './parts/toolRenderUtils';
|
||||
import { isExpandableTool, isStandaloneTool } from './parts/toolRenderUtils';
|
||||
import TurnActivity from '../components/TurnActivity';
|
||||
|
||||
type SubtaskPartLike = Part & {
|
||||
@@ -699,9 +699,8 @@ const AssistantMessageBody: React.FC<Omit<MessageBodyProps, 'isUser'>> = ({
|
||||
const createSessionFromAssistantMessage = useSessionStore((state) => state.createSessionFromAssistantMessage);
|
||||
const openMultiRunLauncherWithPrompt = useUIStore((state) => state.openMultiRunLauncherWithPrompt);
|
||||
const chatRenderMode = useUIStore((state) => state.chatRenderMode);
|
||||
const activityRenderMode = useUIStore((state) => state.activityRenderMode);
|
||||
const isSortedRenderMode = chatRenderMode === 'sorted';
|
||||
const collapsedPreviewCount = activityRenderMode === 'collapsed' ? 7 : 0;
|
||||
const collapsedPreviewCount = 7;
|
||||
const isLastAssistantInTurn = turnGroupingContext?.isLastAssistantInTurn ?? false;
|
||||
const hasStopFinish = messageFinish === 'stop';
|
||||
|
||||
@@ -1111,24 +1110,25 @@ const AssistantMessageBody: React.FC<Omit<MessageBodyProps, 'isUser'>> = ({
|
||||
return;
|
||||
}
|
||||
rendered.push(
|
||||
<TurnActivity
|
||||
key={`progressive-group-${segment.id}`}
|
||||
parts={visibleSegmentParts}
|
||||
isExpanded={turnGroupingContext.isGroupExpanded === true}
|
||||
collapsedPreviewCount={collapsedPreviewCount}
|
||||
onToggle={toggleActivityGroup}
|
||||
syntaxTheme={syntaxTheme}
|
||||
isMobile={isMobile}
|
||||
expandedTools={expandedTools}
|
||||
onToggleTool={onToggleTool}
|
||||
onShowPopup={onShowPopup}
|
||||
onContentChange={onContentChange}
|
||||
streamPhase={streamPhase}
|
||||
showHeader={true}
|
||||
animateRows={animateActivityRows}
|
||||
animatedToolIds={animatedToolIdsLookup}
|
||||
diffStats={turnGroupingContext.diffStats}
|
||||
/>
|
||||
<div key={`progressive-group-${segment.id}`} className="mb-3">
|
||||
<TurnActivity
|
||||
parts={visibleSegmentParts}
|
||||
isExpanded={turnGroupingContext.isGroupExpanded === true}
|
||||
collapsedPreviewCount={collapsedPreviewCount}
|
||||
onToggle={toggleActivityGroup}
|
||||
syntaxTheme={syntaxTheme}
|
||||
isMobile={isMobile}
|
||||
expandedTools={expandedTools}
|
||||
onToggleTool={onToggleTool}
|
||||
onShowPopup={onShowPopup}
|
||||
onContentChange={onContentChange}
|
||||
streamPhase={streamPhase}
|
||||
showHeader={true}
|
||||
animateRows={animateActivityRows}
|
||||
animatedToolIds={animatedToolIdsLookup}
|
||||
diffStats={turnGroupingContext.diffStats}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -1190,7 +1190,7 @@ const AssistantMessageBody: React.FC<Omit<MessageBodyProps, 'isUser'>> = ({
|
||||
if (partText && partText.trim().length > 0) {
|
||||
rendered.push(
|
||||
<FadeInOnReveal key={`reasoning-${messageId}-${i}`}>
|
||||
<div className="text-sm text-muted-foreground/60 italic leading-relaxed whitespace-pre-wrap">
|
||||
<div className="my-0.5 text-sm text-muted-foreground/60 italic leading-relaxed whitespace-pre-wrap">
|
||||
{partText}
|
||||
</div>
|
||||
</FadeInOnReveal>
|
||||
@@ -1239,40 +1239,28 @@ const AssistantMessageBody: React.FC<Omit<MessageBodyProps, 'isUser'>> = ({
|
||||
continue;
|
||||
}
|
||||
|
||||
// Static tools: group consecutive tools of the same kind into compact rows
|
||||
const groupToolName = getStaticGroupToolName(toolName);
|
||||
const group: ToolPartType[] = [toolPart];
|
||||
let j = i + 1;
|
||||
while (j < visibleParts.length) {
|
||||
const next = visibleParts[j];
|
||||
if (next.type !== 'tool') break;
|
||||
const nextTool = next as ToolPartType;
|
||||
if (!shouldShowTool(nextTool)) { j++; continue; }
|
||||
const nextName = nextTool.tool?.toLowerCase() ?? '';
|
||||
if (getStaticGroupToolName(nextName) !== groupToolName) break;
|
||||
group.push(nextTool);
|
||||
j++;
|
||||
}
|
||||
|
||||
// Static tools: one row per tool call (no grouping)
|
||||
rendered.push(
|
||||
<FadeInOnReveal key={`static-tools-${toolPart.id}`}>
|
||||
<ToolRevealOnMount animate={group.some((candidate) => animatedToolIdsLookup.has(candidate.id))} wipe>
|
||||
<ToolRevealOnMount animate={animatedToolIdsLookup.has(toolPart.id)} wipe>
|
||||
<StaticToolRow
|
||||
toolName={groupToolName}
|
||||
activities={group.map(tp => ({
|
||||
id: tp.id,
|
||||
turnId: '',
|
||||
messageId: messageId,
|
||||
partIndex: 0,
|
||||
part: tp,
|
||||
kind: 'tool' as const,
|
||||
}))}
|
||||
animateTailText={group.some((candidate) => animatedToolIdsLookup.has(candidate.id))}
|
||||
toolName={toolName}
|
||||
activities={[
|
||||
{
|
||||
id: toolPart.id,
|
||||
turnId: '',
|
||||
messageId,
|
||||
partIndex: 0,
|
||||
part: toolPart,
|
||||
kind: 'tool' as const,
|
||||
},
|
||||
]}
|
||||
animateTailText={animatedToolIdsLookup.has(toolPart.id)}
|
||||
/>
|
||||
</ToolRevealOnMount>
|
||||
</FadeInOnReveal>
|
||||
);
|
||||
i = j;
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@@ -88,7 +88,10 @@ const AssistantTextPart: React.FC<AssistantTextPartProps> = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="group/assistant-text relative break-words" key={part.id || `${messageId}-text`}>
|
||||
<div
|
||||
className={`group/assistant-text relative break-words ${chatRenderMode === 'live' ? 'my-1' : ''}`}
|
||||
key={part.id || `${messageId}-text`}
|
||||
>
|
||||
<MarkdownRenderer
|
||||
content={displayTextContent}
|
||||
part={part}
|
||||
|
||||
@@ -14,19 +14,13 @@ import { Text } from '@/components/ui/text';
|
||||
import { FadeInOnReveal } from '../FadeInOnReveal';
|
||||
import { getToolIcon } from './toolPresentation';
|
||||
import { getToolMetadata } from '@/lib/toolHelpers';
|
||||
import { getStaticGroupToolName, isExpandableTool, isStandaloneTool, isStaticTool } from './toolRenderUtils';
|
||||
import { isExpandableTool, isStandaloneTool, isStaticTool } from './toolRenderUtils';
|
||||
import { RuntimeAPIContext } from '@/contexts/runtimeAPIContext';
|
||||
import { useDirectoryStore } from '@/stores/useDirectoryStore';
|
||||
import { useUIStore } from '@/stores/useUIStore';
|
||||
import ReasoningPart from './ReasoningPart';
|
||||
import JustificationBlock from './JustificationBlock';
|
||||
|
||||
interface DiffStats {
|
||||
additions: number;
|
||||
deletions: number;
|
||||
files: number;
|
||||
}
|
||||
|
||||
interface ProgressiveGroupProps {
|
||||
parts: TurnActivityPart[];
|
||||
isExpanded: boolean;
|
||||
@@ -42,121 +36,8 @@ interface ProgressiveGroupProps {
|
||||
showHeader: boolean;
|
||||
animateRows?: boolean;
|
||||
animatedToolIds?: Set<string>;
|
||||
diffStats?: DiffStats;
|
||||
}
|
||||
|
||||
const EDIT_LIKE_TOOL_NAMES = new Set<string>([
|
||||
'edit',
|
||||
'multiedit',
|
||||
'apply_patch',
|
||||
'str_replace',
|
||||
'str_replace_based_edit_tool',
|
||||
]);
|
||||
|
||||
const isEditLikeTool = (toolName: unknown): boolean => {
|
||||
return typeof toolName === 'string' && EDIT_LIKE_TOOL_NAMES.has(toolName.toLowerCase());
|
||||
};
|
||||
|
||||
const parseDiffCounts = (diffText: string): { added: number; removed: number } => {
|
||||
const lines = diffText.split('\n');
|
||||
let added = 0;
|
||||
let removed = 0;
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.startsWith('+') && !line.startsWith('+++')) added += 1;
|
||||
if (line.startsWith('-') && !line.startsWith('---')) removed += 1;
|
||||
}
|
||||
|
||||
return { added, removed };
|
||||
};
|
||||
|
||||
type FileDiffAggregate = { filePath: string; added: number; removed: number };
|
||||
|
||||
const aggregateFileDiffs = (parts: TurnActivityPart[]): FileDiffAggregate[] => {
|
||||
const byPath = new Map<string, { added: number; removed: number }>();
|
||||
|
||||
const addToPath = (filePath: string, added: number, removed: number) => {
|
||||
if (!filePath) return;
|
||||
const current = byPath.get(filePath) ?? { added: 0, removed: 0 };
|
||||
current.added += Math.max(0, added);
|
||||
current.removed += Math.max(0, removed);
|
||||
byPath.set(filePath, current);
|
||||
};
|
||||
|
||||
for (const activity of parts) {
|
||||
if (activity.kind !== 'tool') continue;
|
||||
const toolPart = activity.part as ToolPartType;
|
||||
if (!isEditLikeTool(toolPart.tool)) continue;
|
||||
|
||||
const state = toolPart.state as { metadata?: Record<string, unknown>; input?: Record<string, unknown> } | undefined;
|
||||
const metadata = state?.metadata;
|
||||
const input = state?.input;
|
||||
const files = Array.isArray(metadata?.files) ? metadata?.files : [];
|
||||
|
||||
if (files.length > 0) {
|
||||
for (const file of files) {
|
||||
if (!file || typeof file !== 'object') continue;
|
||||
const record = file as {
|
||||
relativePath?: unknown;
|
||||
filePath?: unknown;
|
||||
path?: unknown;
|
||||
additions?: unknown;
|
||||
deletions?: unknown;
|
||||
diff?: unknown;
|
||||
};
|
||||
|
||||
const filePath =
|
||||
(typeof record.relativePath === 'string' && record.relativePath) ||
|
||||
(typeof record.filePath === 'string' && record.filePath) ||
|
||||
(typeof record.path === 'string' && record.path) ||
|
||||
'';
|
||||
|
||||
const explicitAdditions = typeof record.additions === 'number' ? record.additions : null;
|
||||
const explicitDeletions = typeof record.deletions === 'number' ? record.deletions : null;
|
||||
|
||||
if (explicitAdditions !== null || explicitDeletions !== null) {
|
||||
addToPath(filePath, explicitAdditions ?? 0, explicitDeletions ?? 0);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (typeof record.diff === 'string' && record.diff.trim().length > 0) {
|
||||
const counts = parseDiffCounts(record.diff);
|
||||
addToPath(filePath, counts.added, counts.removed);
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
const fallbackPath =
|
||||
(typeof input?.filePath === 'string' && input.filePath) ||
|
||||
(typeof input?.file_path === 'string' && input.file_path) ||
|
||||
(typeof input?.path === 'string' && input.path) ||
|
||||
'';
|
||||
|
||||
if (typeof metadata?.diff === 'string' && metadata.diff.trim().length > 0) {
|
||||
const counts = parseDiffCounts(metadata.diff);
|
||||
addToPath(fallbackPath || 'Diff', counts.added, counts.removed);
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(byPath.entries())
|
||||
.map(([filePath, counts]) => ({ filePath, added: counts.added, removed: counts.removed }))
|
||||
.filter((entry) => entry.added > 0 || entry.removed > 0)
|
||||
.sort((a, b) => {
|
||||
const aChanges = a.added + a.removed;
|
||||
const bChanges = b.added + b.removed;
|
||||
if (aChanges !== bChanges) return bChanges - aChanges;
|
||||
return a.filePath.localeCompare(b.filePath);
|
||||
});
|
||||
};
|
||||
|
||||
const toDisplayFileName = (filePath: string): string => {
|
||||
const normalized = filePath.replace(/\\/g, '/');
|
||||
const segments = normalized.split('/').filter(Boolean);
|
||||
if (segments.length === 0) return normalized;
|
||||
return segments[segments.length - 1];
|
||||
};
|
||||
|
||||
const isActivityRunning = (activity: TurnActivityPart): boolean => {
|
||||
if (activity.kind !== 'tool') return false;
|
||||
const part = activity.part as ToolPartType;
|
||||
@@ -236,14 +117,11 @@ const toTodoStatusKey = (value: unknown): 'pending' | 'in_progress' | 'completed
|
||||
|
||||
const formatTodoSummary = (todos: unknown[]): string | null => {
|
||||
if (todos.length === 0) {
|
||||
return null;
|
||||
return '0 tasks';
|
||||
}
|
||||
|
||||
let pending = 0;
|
||||
let inProgress = 0;
|
||||
let completed = 0;
|
||||
let cancelled = 0;
|
||||
|
||||
for (const todo of todos) {
|
||||
if (!todo || typeof todo !== 'object') {
|
||||
continue;
|
||||
@@ -254,26 +132,14 @@ const formatTodoSummary = (todos: unknown[]): string | null => {
|
||||
}
|
||||
if (status === 'pending') pending += 1;
|
||||
if (status === 'in_progress') inProgress += 1;
|
||||
if (status === 'completed') completed += 1;
|
||||
if (status === 'cancelled') cancelled += 1;
|
||||
}
|
||||
|
||||
const total = pending + inProgress + completed + cancelled;
|
||||
if (total === 0) {
|
||||
return null;
|
||||
const activeCount = pending + inProgress;
|
||||
if (activeCount === 0) {
|
||||
return '0 tasks';
|
||||
}
|
||||
|
||||
if (completed === total) {
|
||||
return `All ${total} tasks done`;
|
||||
}
|
||||
|
||||
const parts: string[] = [];
|
||||
if (inProgress > 0) parts.push(`${inProgress} in progress`);
|
||||
if (pending > 0) parts.push(`${pending} pending`);
|
||||
if (completed > 0) parts.push(`${completed} done`);
|
||||
if (cancelled > 0) parts.push(`${cancelled} cancelled`);
|
||||
|
||||
return parts.length > 0 ? parts.join(', ') : null;
|
||||
return `${activeCount} ${activeCount === 1 ? 'task' : 'tasks'}`;
|
||||
};
|
||||
|
||||
const getTodoSummaryFromActivity = (activity: TurnActivityPart): string | null => {
|
||||
@@ -343,6 +209,37 @@ const normalizePathValue = (value: string): string => {
|
||||
return trimmed.replace(/\\/g, '/');
|
||||
};
|
||||
|
||||
const trimTrailingSlashes = (value: string): string => {
|
||||
if (value === '/') {
|
||||
return value;
|
||||
}
|
||||
return value.replace(/\/+$/, '');
|
||||
};
|
||||
|
||||
const getRelativePathFromDirectory = (filePath: string, currentDirectory: string): string => {
|
||||
const normalizedPath = trimTrailingSlashes(normalizePathValue(filePath));
|
||||
const normalizedDirectory = trimTrailingSlashes(normalizePathValue(currentDirectory));
|
||||
|
||||
if (!normalizedPath) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (!normalizedDirectory) {
|
||||
return normalizedPath;
|
||||
}
|
||||
|
||||
if (normalizedPath === normalizedDirectory) {
|
||||
return '.';
|
||||
}
|
||||
|
||||
const prefix = `${normalizedDirectory}/`;
|
||||
if (normalizedPath.startsWith(prefix)) {
|
||||
return normalizedPath.slice(prefix.length);
|
||||
}
|
||||
|
||||
return normalizedPath;
|
||||
};
|
||||
|
||||
const resolveAbsolutePath = (currentDirectory: string, filePath: string): string => {
|
||||
const normalizedPath = normalizePathValue(filePath);
|
||||
if (!normalizedPath) {
|
||||
@@ -446,7 +343,7 @@ type AggregatedRow =
|
||||
|
||||
/**
|
||||
* Aggregate sorted activity parts into display rows.
|
||||
* Consecutive static tools of the same type are merged into a single row.
|
||||
* Static tools are rendered as one row per call.
|
||||
* Reasoning/justification become inline text.
|
||||
* Expandable tools (edit, bash, write, question) stay as individual rows.
|
||||
* Unknown tools stay as individual expandable rows (fallback).
|
||||
@@ -487,20 +384,8 @@ const aggregateRows = (parts: TurnActivityPart[]): AggregatedRow[] => {
|
||||
}
|
||||
|
||||
if (isStaticTool(toolName)) {
|
||||
// Aggregate consecutive static tools of the same name
|
||||
const groupedToolName = getStaticGroupToolName(toolName);
|
||||
const group: TurnActivityPart[] = [activity];
|
||||
let j = i + 1;
|
||||
while (j < parts.length) {
|
||||
const next = parts[j];
|
||||
if (next.kind !== 'tool') break;
|
||||
const nextTool = (next.part as ToolPartType).tool?.toLowerCase() ?? '';
|
||||
if (getStaticGroupToolName(nextTool) !== groupedToolName) break;
|
||||
group.push(next);
|
||||
j++;
|
||||
}
|
||||
rows.push({ type: 'tool-static-group', toolName: groupedToolName, activities: group });
|
||||
i = j;
|
||||
rows.push({ type: 'tool-static-group', toolName, activities: [activity] });
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -541,19 +426,20 @@ export const StaticToolRow: React.FC<{
|
||||
}, [activities]);
|
||||
|
||||
const readFileEntries = React.useMemo(() => {
|
||||
if (!isReadGroup) return [] as Array<{ path: string; name: string; offset?: number }>;
|
||||
if (!isReadGroup) return [] as Array<{ path: string; displayPath: string; offset?: number }>;
|
||||
|
||||
const entries: Array<{ path: string; name: string; offset?: number }> = [];
|
||||
const entries: Array<{ path: string; displayPath: string; offset?: number }> = [];
|
||||
for (const activity of activities) {
|
||||
const filePath = getToolFilePath(activity);
|
||||
const fileName = getToolFileName(activity);
|
||||
const offset = getToolReadOffset(activity);
|
||||
if (!filePath || !fileName) continue;
|
||||
if (!filePath) continue;
|
||||
if (entries.some((entry) => entry.path === filePath)) continue;
|
||||
entries.push({ path: filePath, name: fileName, offset });
|
||||
const displayPath = getRelativePathFromDirectory(filePath, currentDirectory);
|
||||
if (!displayPath) continue;
|
||||
entries.push({ path: filePath, displayPath, offset });
|
||||
}
|
||||
return entries;
|
||||
}, [activities, isReadGroup]);
|
||||
}, [activities, currentDirectory, isReadGroup]);
|
||||
|
||||
const handleReadFileClick = React.useCallback((filePath: string, offset?: number) => {
|
||||
const absolutePath = resolveAbsolutePath(currentDirectory, filePath);
|
||||
@@ -575,13 +461,18 @@ export const StaticToolRow: React.FC<{
|
||||
uiStore.openContextFile(contextDirectory, absolutePath);
|
||||
}, [currentDirectory, runtime]);
|
||||
|
||||
const isSearchGroup = toolName.toLowerCase() === 'grep';
|
||||
const isFetchGroup = toolName.toLowerCase() === 'webfetch' || toolName.toLowerCase() === 'fetch' || toolName.toLowerCase() === 'curl' || toolName.toLowerCase() === 'wget';
|
||||
const normalizedToolName = toolName.toLowerCase();
|
||||
const isSearchGroup = normalizedToolName === 'grep'
|
||||
|| normalizedToolName === 'search'
|
||||
|| normalizedToolName === 'find'
|
||||
|| normalizedToolName === 'ripgrep'
|
||||
|| normalizedToolName === 'glob';
|
||||
const isFetchGroup = normalizedToolName === 'webfetch' || normalizedToolName === 'fetch' || normalizedToolName === 'curl' || normalizedToolName === 'wget';
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'flex w-full flex-wrap items-center gap-x-2 gap-y-0.5 pr-2 pl-px py-1.5 rounded-xl'
|
||||
'flex w-full items-center gap-x-1.5 pr-2 pl-px py-1.5 rounded-xl min-w-0'
|
||||
)}
|
||||
>
|
||||
<div className="inline-flex h-5 items-center flex-shrink-0" style={{ color: 'var(--tools-icon)' }}>
|
||||
@@ -606,28 +497,31 @@ export const StaticToolRow: React.FC<{
|
||||
event.stopPropagation();
|
||||
handleReadFileClick(entry.path, entry.offset);
|
||||
}}
|
||||
className="inline-flex items-center gap-1 min-w-0 max-w-full typography-meta leading-5 hover:opacity-90"
|
||||
style={{ color: 'var(--tools-title)' }}
|
||||
title={entry.offset ? `${entry.path}:${entry.offset}` : entry.path}
|
||||
className="inline-flex items-center justify-start gap-1 min-w-0 flex-1 text-left typography-meta leading-5 hover:opacity-90"
|
||||
style={{ color: 'var(--tools-description)' }}
|
||||
title={entry.offset ? `${entry.displayPath}:${entry.offset}` : entry.displayPath}
|
||||
>
|
||||
{showToolFileIcons ? <FileTypeIcon filePath={entry.path} className="h-3.5 w-3.5" /> : null}
|
||||
<Text
|
||||
variant={animateTailText ? 'generate-effect' : undefined}
|
||||
className="min-w-0 max-w-full truncate typography-meta leading-5"
|
||||
style={{ color: 'var(--tools-title)' }}
|
||||
title={entry.path}
|
||||
<span
|
||||
className="min-w-0 flex-1 truncate whitespace-nowrap typography-meta leading-5"
|
||||
style={{
|
||||
color: 'var(--tools-description)',
|
||||
direction: 'rtl',
|
||||
textAlign: 'left',
|
||||
}}
|
||||
title={entry.displayPath}
|
||||
>
|
||||
{entry.name}
|
||||
</Text>
|
||||
{entry.displayPath}
|
||||
</span>
|
||||
</button>
|
||||
))
|
||||
: null}
|
||||
{isSearchGroup && descriptions.length > 0
|
||||
? descriptions.map((desc, index) => (
|
||||
<span key={`${desc}-${index}`} className="inline-flex min-w-0 max-w-full">
|
||||
<span key={`${desc}-${index}`} className="inline-flex min-w-0 flex-1">
|
||||
<Text
|
||||
variant={animateTailText ? 'generate-effect' : undefined}
|
||||
className="min-w-0 max-w-full truncate typography-meta leading-5"
|
||||
className="min-w-0 flex-1 truncate whitespace-nowrap typography-meta leading-5"
|
||||
style={{ color: 'var(--tools-description)' }}
|
||||
title={desc}
|
||||
>
|
||||
@@ -644,8 +538,8 @@ export const StaticToolRow: React.FC<{
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className={cn(
|
||||
'min-w-0 max-w-full underline decoration-[color:var(--status-info)] underline-offset-2 hover:opacity-90',
|
||||
'truncate max-w-[20rem] typography-meta'
|
||||
'min-w-0 flex-1 underline decoration-[color:var(--status-info)] underline-offset-2 hover:opacity-90',
|
||||
'truncate whitespace-nowrap typography-meta'
|
||||
)}
|
||||
style={{ color: 'var(--status-info)' }}
|
||||
title={url}
|
||||
@@ -657,7 +551,7 @@ export const StaticToolRow: React.FC<{
|
||||
{!isReadGroup && !isSearchGroup && !isFetchGroup && descriptions.length > 0 ? (
|
||||
<Text
|
||||
variant={animateTailText ? 'generate-effect' : undefined}
|
||||
className="min-w-0 max-w-full truncate typography-meta leading-5"
|
||||
className="min-w-0 flex-1 truncate whitespace-nowrap typography-meta leading-5"
|
||||
style={{ color: 'var(--tools-description)' }}
|
||||
>
|
||||
{descriptions.join(' ')}
|
||||
@@ -749,16 +643,6 @@ const ProgressiveGroup: React.FC<ProgressiveGroupProps> = ({
|
||||
return rows.slice(-previewCount);
|
||||
}, [isExpanded, previewCount, rows]);
|
||||
|
||||
const toolCount = React.useMemo(
|
||||
() => parts.filter((activity) => activity.kind === 'tool').length,
|
||||
[parts]
|
||||
);
|
||||
|
||||
const aggregatedFileDiffs = React.useMemo(() => aggregateFileDiffs(parts), [parts]);
|
||||
|
||||
const hasToolMetric = toolCount > 0;
|
||||
const showToolFileIcons = useUIStore((state) => state.showToolFileIcons);
|
||||
|
||||
if (shouldRenderRows && rows.length === 0) {
|
||||
return null;
|
||||
}
|
||||
@@ -867,14 +751,14 @@ const ProgressiveGroup: React.FC<ProgressiveGroupProps> = ({
|
||||
if (!showHeader) {
|
||||
return (
|
||||
<FadeInOnReveal>
|
||||
<div className="my-1">{renderedRows}</div>
|
||||
<div className="mt-1 mb-2 space-y-1.5">{renderedRows}</div>
|
||||
</FadeInOnReveal>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<FadeInOnReveal>
|
||||
<div className="my-1">
|
||||
<div className="mt-1 mb-2">
|
||||
<button
|
||||
type="button"
|
||||
className="group/tool flex w-full flex-wrap items-center gap-x-2 gap-y-0.5 pr-2 pl-px py-1.5 rounded-xl text-left"
|
||||
@@ -883,30 +767,16 @@ const ProgressiveGroup: React.FC<ProgressiveGroupProps> = ({
|
||||
<span className="inline-flex h-5 items-center flex-shrink-0" style={{ color: 'var(--tools-icon)' }}>
|
||||
<RiStackLine className="h-3.5 w-3.5" />
|
||||
</span>
|
||||
<span className="typography-meta leading-5 font-medium inline-flex h-5 items-center flex-shrink-0" style={{ color: 'var(--tools-title)' }}>
|
||||
<span
|
||||
className="leading-5 font-semibold inline-flex h-5 items-center flex-shrink-0"
|
||||
style={{
|
||||
color: 'var(--tools-title)',
|
||||
fontSize: '0.9rem',
|
||||
letterSpacing: '0.005em',
|
||||
}}
|
||||
>
|
||||
Activity
|
||||
</span>
|
||||
{hasToolMetric ? (
|
||||
<span className="typography-meta leading-5 text-muted-foreground/80 flex-shrink-0">
|
||||
{toolCount} {toolCount === 1 ? 'tool' : 'tools'}
|
||||
</span>
|
||||
) : null}
|
||||
{aggregatedFileDiffs.map((entry, index) => (
|
||||
<span
|
||||
key={`${entry.filePath}-${index}`}
|
||||
className="inline-flex min-w-0 max-w-full items-center gap-1 typography-meta leading-5 text-muted-foreground/80"
|
||||
>
|
||||
{showToolFileIcons ? <FileTypeIcon filePath={entry.filePath} className="h-3.5 w-3.5" /> : null}
|
||||
<span className={cn('truncate', isMobile ? 'max-w-[9rem]' : 'max-w-[12rem]')} style={{ color: 'var(--tools-description)' }} title={entry.filePath}>
|
||||
{toDisplayFileName(entry.filePath)}
|
||||
</span>
|
||||
<span className="flex-shrink-0 inline-flex items-center gap-0 tabular-nums">
|
||||
<span style={{ color: 'var(--status-success)' }}>+{entry.added}</span>
|
||||
<span style={{ color: 'var(--tools-description)' }}>/</span>
|
||||
<span style={{ color: 'var(--status-error)' }}>-{entry.removed}</span>
|
||||
</span>
|
||||
</span>
|
||||
))}
|
||||
</button>
|
||||
{shouldShowRowsContainer ? (
|
||||
<div className="relative ml-2 pl-3">
|
||||
@@ -924,7 +794,7 @@ const ProgressiveGroup: React.FC<ProgressiveGroupProps> = ({
|
||||
+{previewHiddenCount} more...
|
||||
</button>
|
||||
) : null}
|
||||
<div>{renderedRows}</div>
|
||||
<div className="space-y-1.5">{renderedRows}</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
@@ -978,21 +978,20 @@ const renderPathLikeGitChanges = (path: string, grow = true) => {
|
||||
);
|
||||
};
|
||||
|
||||
const renderAnimatedPathWithIcon = (path: string, animate = true, grow = true, showFileIcons = true) => {
|
||||
const renderAnimatedPathWithIcon = (path: string, _animate = true, grow = true, showFileIcons = true) => {
|
||||
void _animate;
|
||||
const lastSlash = path.lastIndexOf('/');
|
||||
|
||||
if (lastSlash === -1) {
|
||||
return (
|
||||
<span className={cn('min-w-0 inline-flex items-center gap-1', grow && 'flex-1')} title={path}>
|
||||
<span className={cn('min-w-0 inline-flex items-center gap-1 overflow-hidden', grow && 'flex-1')} title={path}>
|
||||
{showFileIcons ? <FileTypeIcon filePath={path} className="h-3.5 w-3.5 flex-shrink-0" /> : null}
|
||||
<Text
|
||||
key={animate ? `path-full-${path}` : undefined}
|
||||
variant={animate ? 'generate-effect' : undefined}
|
||||
className="min-w-0 truncate typography-meta"
|
||||
<span
|
||||
className={cn('min-w-0 truncate whitespace-nowrap typography-meta', grow && 'flex-1')}
|
||||
style={{ color: 'var(--tools-title)' }}
|
||||
>
|
||||
{path}
|
||||
</Text>
|
||||
</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -1004,26 +1003,19 @@ const renderAnimatedPathWithIcon = (path: string, animate = true, grow = true, s
|
||||
<span className={cn('min-w-0 inline-flex items-center gap-1 overflow-hidden', grow && 'flex-1')} title={path}>
|
||||
{showFileIcons ? <FileTypeIcon filePath={path} className="h-3.5 w-3.5 flex-shrink-0" /> : null}
|
||||
<span className={cn('min-w-0 inline-flex items-baseline overflow-hidden typography-meta', grow && 'flex-1')}>
|
||||
<Text
|
||||
key={animate ? `path-dir-${dir}` : undefined}
|
||||
variant={animate ? 'generate-effect' : undefined}
|
||||
className="min-w-0 truncate typography-meta"
|
||||
style={{ color: 'var(--tools-description)', direction: 'rtl', textAlign: 'left' }}
|
||||
<span
|
||||
className="min-w-0 flex-1 truncate whitespace-nowrap"
|
||||
style={{
|
||||
color: 'var(--tools-description)',
|
||||
direction: 'rtl',
|
||||
textAlign: 'left',
|
||||
}}
|
||||
>
|
||||
{dir}
|
||||
</Text>
|
||||
<span className="flex-shrink-0 inline-flex items-baseline">
|
||||
<Text variant={animate ? 'generate-effect' : undefined} className="typography-meta" style={{ color: 'var(--tools-description)' }}>
|
||||
/
|
||||
</Text>
|
||||
<Text
|
||||
key={animate ? `path-name-${name}` : undefined}
|
||||
variant={animate ? 'generate-effect' : undefined}
|
||||
className="typography-meta"
|
||||
style={{ color: 'var(--tools-title)' }}
|
||||
>
|
||||
{name}
|
||||
</Text>
|
||||
</span>
|
||||
<span className="flex-shrink-0" style={{ color: 'var(--tools-description)' }}>/</span>
|
||||
<span className="flex-shrink-0" style={{ color: 'var(--tools-title)' }}>
|
||||
{name}
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
@@ -1763,6 +1755,9 @@ const ToolPart: React.FC<ToolPartProps> = ({
|
||||
|
||||
// Tool title/description — shown inline as context
|
||||
const justificationText = React.useMemo(() => {
|
||||
if (normalizedPartTool === 'bash') {
|
||||
return null;
|
||||
}
|
||||
if (normalizedPartTool === 'apply_patch') {
|
||||
return null;
|
||||
}
|
||||
@@ -1846,15 +1841,15 @@ const ToolPart: React.FC<ToolPartProps> = ({
|
||||
{}
|
||||
<div
|
||||
className={cn(
|
||||
'group/tool flex gap-2 pr-2 pl-px py-1.5 rounded-xl cursor-pointer',
|
||||
isMultiFileApplyPatch ? 'flex-wrap items-start' : 'items-center'
|
||||
)}
|
||||
'group/tool flex gap-1.5 pr-2 pl-px py-2 rounded-xl cursor-pointer',
|
||||
isMultiFileApplyPatch ? 'flex-wrap items-start' : 'items-center'
|
||||
)}
|
||||
onClick={handleMainClick}
|
||||
onKeyDown={handleMainKeyDown}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
>
|
||||
<div className={cn('flex gap-2', isMultiFileApplyPatch ? 'w-full min-w-0 flex-wrap items-center gap-x-2 gap-y-0.5' : 'items-center flex-shrink-0')}>
|
||||
<div className={cn('flex gap-1.5', isMultiFileApplyPatch ? 'w-full min-w-0 flex-wrap items-center gap-x-2 gap-y-0.5' : 'items-center flex-shrink-0')}>
|
||||
{}
|
||||
<div
|
||||
className="relative h-3.5 w-3.5 flex-shrink-0 cursor-pointer"
|
||||
@@ -1893,15 +1888,6 @@ const ToolPart: React.FC<ToolPartProps> = ({
|
||||
>
|
||||
{displayName}
|
||||
</MinDurationShineText>
|
||||
{typeof effectiveTimeStart === 'number' ? (
|
||||
<span className="flex-shrink-0 tabular-nums text-muted-foreground/80 typography-meta">
|
||||
<LiveDuration
|
||||
start={effectiveTimeStart}
|
||||
end={typeof effectiveTimeEnd === 'number' ? effectiveTimeEnd : undefined}
|
||||
active={Boolean(isActive && typeof effectiveTimeEnd !== 'number')}
|
||||
/>
|
||||
</span>
|
||||
) : null}
|
||||
{getMultiFileDescription(metadata, animateTailText, showToolFileIcons)}
|
||||
</>
|
||||
) : (
|
||||
@@ -1917,7 +1903,7 @@ const ToolPart: React.FC<ToolPartProps> = ({
|
||||
{displayName}
|
||||
</MinDurationShineText>
|
||||
</div>
|
||||
{typeof effectiveTimeStart === 'number' ? (
|
||||
{normalizedPartTool === 'bash' && typeof effectiveTimeStart === 'number' ? (
|
||||
<span className="flex-shrink-0 tabular-nums text-muted-foreground/80 typography-meta">
|
||||
<LiveDuration
|
||||
start={effectiveTimeStart}
|
||||
|
||||
@@ -134,7 +134,7 @@ const ACTIVITY_RENDER_MODE_OPTIONS: Option<'collapsed' | 'summary'>[] = [
|
||||
},
|
||||
{
|
||||
id: 'summary',
|
||||
label: 'Summary',
|
||||
label: 'Expanded',
|
||||
description: 'Expand Activity by default.',
|
||||
},
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user