feat(ui): add dynamic window title and sprite-based project/file icons (#529)
* feat(ui): add dynamic titles and sprite-based project/file icons * feat(files): add viewer syntax fallback and tab file icons * fix(files): restore file viewer highlighting and add diff file icons * feat(git): add file icons and async file-viewer syntax fallback * fix(files): force codemirror token colors in file viewer * feat(files): add shiki view mode for file viewer * fix(files): force codemirror parse after programmatic content updates * feat(files): support markdown frontmatter preview * feat(chat): use pierre diffs for tool previews * feat(chat): add configurable beautiful-mermaid rendering * feat(perf): virtualize chat rendering and add react-scan toggle * feat(build): enable React Compiler in Vite React apps * fix(chat): reduce rerenders from tooltips and streamed activity * fix(ui): make MessageList React Compiler safe * chore(ui): batch commit remaining pending ui updates * fix: polish chat and diff preview rendering - Keep Mermaid action buttons fixed while diagram content scrolls - Align Diff All Files headers and match Git-style path truncation - Default chat tool diffs to unified view with lightweight indicators disabled * fix: preserve file tree expansion and delay git action label collapse * fix: refine project icon controls in settings --------- Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com>
This commit is contained in:
committed by
GitHub
co-authored by
Bohdan Triapitsyn
parent
d6b8f28e6f
commit
1d8ff97c95
@@ -2,12 +2,14 @@
|
||||
import React from 'react';
|
||||
import { RuntimeAPIContext } from '@/contexts/runtimeAPIContext';
|
||||
import { RiAiAgentLine, RiArrowDownSLine, RiArrowRightSLine, RiBookLine, RiExternalLinkLine, RiFileEditLine, RiFileList2Line, RiFileSearchLine, RiFileTextLine, RiFolder6Line, RiGitBranchLine, RiGlobalLine, RiListCheck2, RiListCheck3, RiMenuSearchLine, RiPencilLine, RiSurveyLine, RiTaskLine, RiTerminalBoxLine, RiToolsLine } from '@remixicon/react';
|
||||
import { File as PierreFile, PatchDiff } from '@pierre/diffs/react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { SimpleMarkdownRenderer } from '../../MarkdownRenderer';
|
||||
import { getToolMetadata, getLanguageFromExtension, isImageFile, getImageMimeType } from '@/lib/toolHelpers';
|
||||
import type { ToolPart as ToolPartType, ToolState as ToolStateUnion } from '@opencode-ai/sdk/v2';
|
||||
import { toolDisplayStyles } from '@/lib/typography';
|
||||
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
|
||||
import { useOptionalThemeSystem } from '@/contexts/useThemeSystem';
|
||||
import { useDirectoryStore } from '@/stores/useDirectoryStore';
|
||||
import { useSessionStore } from '@/stores/useSessionStore';
|
||||
import { useUIStore } from '@/stores/useUIStore';
|
||||
@@ -15,6 +17,8 @@ import { opencodeClient } from '@/lib/opencode/client';
|
||||
import { ScrollableOverlay } from '@/components/ui/ScrollableOverlay';
|
||||
import type { ContentChangeReason } from '@/hooks/useChatScrollManager';
|
||||
import type { ToolPopupContent } from '../types';
|
||||
import { ensurePierreThemeRegistered } from '@/lib/shiki/appThemeRegistry';
|
||||
import { getDefaultTheme } from '@/lib/theme/themes';
|
||||
|
||||
import {
|
||||
renderListOutput,
|
||||
@@ -22,12 +26,12 @@ import {
|
||||
renderGlobOutput,
|
||||
renderTodoOutput,
|
||||
renderWebSearchOutput,
|
||||
parseDiffToUnified,
|
||||
formatEditOutput,
|
||||
detectLanguageFromOutput,
|
||||
formatInputForDisplay,
|
||||
parseReadToolOutput,
|
||||
} from '../toolRenderers';
|
||||
import { DiffViewToggle, type DiffViewMode } from '../DiffViewToggle';
|
||||
import { VirtualizedCodeBlock, type CodeLine } from './VirtualizedCodeBlock';
|
||||
|
||||
type ToolStateWithMetadata = ToolStateUnion & { metadata?: Record<string, unknown>; input?: Record<string, unknown>; output?: string; error?: string; time?: { start: number; end?: number } };
|
||||
@@ -167,6 +171,40 @@ const getRelativePath = (absolutePath: string, currentDirectory: string, isMobil
|
||||
return absolutePath;
|
||||
};
|
||||
|
||||
const usePierreThemeConfig = () => {
|
||||
const themeSystem = useOptionalThemeSystem();
|
||||
const fallbackLightTheme = React.useMemo(() => getDefaultTheme(false), []);
|
||||
const fallbackDarkTheme = React.useMemo(() => getDefaultTheme(true), []);
|
||||
|
||||
const availableThemes = React.useMemo(
|
||||
() => themeSystem?.availableThemes ?? [fallbackLightTheme, fallbackDarkTheme],
|
||||
[fallbackDarkTheme, fallbackLightTheme, themeSystem?.availableThemes],
|
||||
);
|
||||
const lightThemeId = themeSystem?.lightThemeId ?? fallbackLightTheme.metadata.id;
|
||||
const darkThemeId = themeSystem?.darkThemeId ?? fallbackDarkTheme.metadata.id;
|
||||
|
||||
const lightTheme = React.useMemo(
|
||||
() => availableThemes.find((theme) => theme.metadata.id === lightThemeId) ?? fallbackLightTheme,
|
||||
[availableThemes, fallbackLightTheme, lightThemeId],
|
||||
);
|
||||
const darkTheme = React.useMemo(
|
||||
() => availableThemes.find((theme) => theme.metadata.id === darkThemeId) ?? fallbackDarkTheme,
|
||||
[availableThemes, darkThemeId, fallbackDarkTheme],
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
ensurePierreThemeRegistered(lightTheme);
|
||||
ensurePierreThemeRegistered(darkTheme);
|
||||
}, [darkTheme, lightTheme]);
|
||||
|
||||
const currentVariant = themeSystem?.currentTheme.metadata.variant ?? 'light';
|
||||
|
||||
return {
|
||||
pierreTheme: { light: lightTheme.metadata.id, dark: darkTheme.metadata.id },
|
||||
pierreThemeType: currentVariant === 'dark' ? ('dark' as const) : ('light' as const),
|
||||
};
|
||||
};
|
||||
|
||||
// Parse question tool output: "User has answered your questions: "Q1"="A1", "Q2"="A2". You can now..."
|
||||
const parseQuestionOutput = (output: string): Array<{ question: string; answer: string }> | null => {
|
||||
const match = output.match(/^User has answered your questions:\s*(.+?)\.\s*You can now/s);
|
||||
@@ -627,49 +665,29 @@ const TaskToolSummary: React.FC<{
|
||||
|
||||
interface DiffPreviewProps {
|
||||
diff: string;
|
||||
syntaxTheme: { [key: string]: React.CSSProperties };
|
||||
input?: ToolStateWithMetadata['input'];
|
||||
pierreTheme: { light: string; dark: string };
|
||||
pierreThemeType: 'light' | 'dark';
|
||||
diffViewMode: DiffViewMode;
|
||||
}
|
||||
|
||||
const DiffPreview: React.FC<DiffPreviewProps> = React.memo(({ diff, syntaxTheme, input }) => {
|
||||
const hunks = React.useMemo(() => parseDiffToUnified(diff), [diff]);
|
||||
|
||||
const DiffPreview: React.FC<DiffPreviewProps> = React.memo(({ diff, pierreTheme, pierreThemeType, diffViewMode }) => {
|
||||
return (
|
||||
<div className="typography-code px-1 pb-1 pt-0 space-y-0">
|
||||
{hunks.map((hunk, hunkIdx) => {
|
||||
const lang = getLanguageFromExtension(
|
||||
typeof input?.file_path === 'string' ? input.file_path
|
||||
: typeof input?.filePath === 'string' ? input.filePath
|
||||
: hunk.file
|
||||
) || 'text';
|
||||
|
||||
const codeLines: CodeLine[] = hunk.lines.map((line) => ({
|
||||
text: line.content,
|
||||
lineNumber: line.lineNumber || null,
|
||||
type: line.type as CodeLine['type'],
|
||||
}));
|
||||
|
||||
return (
|
||||
<div key={hunkIdx} className="-mx-1 px-1 last:border-b-0" style={{ borderBottomWidth: '1px', borderBottomColor: 'var(--tools-border)' }}>
|
||||
<div className="bg-muted/20 px-2 py-1 typography-meta font-medium text-muted-foreground break-words -mx-1" style={{ borderBottomWidth: '1px', borderBottomColor: 'var(--tools-border)' }}>
|
||||
{`${hunk.file} (line ${hunk.oldStart})`}
|
||||
</div>
|
||||
<VirtualizedCodeBlock
|
||||
lines={codeLines}
|
||||
language={lang}
|
||||
syntaxTheme={syntaxTheme}
|
||||
maxHeight="50vh"
|
||||
lineStyles={(line) =>
|
||||
line.type === 'removed'
|
||||
? { backgroundColor: 'var(--tools-edit-removed-bg)', color: 'var(--tools-edit-removed)' }
|
||||
: line.type === 'added'
|
||||
? { backgroundColor: 'var(--tools-edit-added-bg)', color: 'var(--tools-edit-added)' }
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<div className="typography-code px-1 pb-1 pt-0">
|
||||
<PatchDiff
|
||||
patch={diff}
|
||||
options={{
|
||||
diffStyle: diffViewMode === 'side-by-side' ? 'split' : 'unified',
|
||||
diffIndicators: 'none',
|
||||
hunkSeparators: 'line-info-basic',
|
||||
lineDiffType: 'none',
|
||||
maxLineDiffLength: 1000,
|
||||
expansionLineCount: 20,
|
||||
overflow: 'wrap',
|
||||
theme: pierreTheme,
|
||||
themeType: pierreThemeType,
|
||||
}}
|
||||
className="block w-full"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
@@ -678,26 +696,25 @@ DiffPreview.displayName = 'DiffPreview';
|
||||
|
||||
interface WriteInputPreviewProps {
|
||||
content: string;
|
||||
syntaxTheme: { [key: string]: React.CSSProperties };
|
||||
filePath?: string;
|
||||
displayPath: string;
|
||||
pierreTheme: { light: string; dark: string };
|
||||
pierreThemeType: 'light' | 'dark';
|
||||
}
|
||||
|
||||
const WriteInputPreview: React.FC<WriteInputPreviewProps> = React.memo(({ content, syntaxTheme, filePath, displayPath }) => {
|
||||
const WriteInputPreview: React.FC<WriteInputPreviewProps> = React.memo(({
|
||||
content,
|
||||
filePath,
|
||||
displayPath,
|
||||
pierreTheme,
|
||||
pierreThemeType,
|
||||
}) => {
|
||||
const language = React.useMemo(
|
||||
() => getLanguageFromExtension(filePath ?? '') || detectLanguageFromOutput(content, 'write', filePath ? { filePath } : undefined),
|
||||
[content, filePath]
|
||||
);
|
||||
|
||||
const codeLines: CodeLine[] = React.useMemo(() => {
|
||||
const rawLines = content.split('\n');
|
||||
return rawLines.map((text, idx) => ({
|
||||
text: text || ' ',
|
||||
lineNumber: idx + 1,
|
||||
}));
|
||||
}, [content]);
|
||||
|
||||
const lineCount = Math.max(codeLines.length, 1);
|
||||
const lineCount = Math.max(content.split('\n').length, 1);
|
||||
const headerLineLabel = lineCount === 1 ? 'line 1' : `lines 1-${lineCount}`;
|
||||
|
||||
return (
|
||||
@@ -705,11 +722,19 @@ const WriteInputPreview: React.FC<WriteInputPreviewProps> = React.memo(({ conten
|
||||
<div className="bg-muted/20 px-2 py-1 typography-meta font-medium text-muted-foreground rounded-lg mb-1" style={{ borderWidth: '1px', borderColor: 'var(--tools-border)' }}>
|
||||
{`${displayPath} (${headerLineLabel})`}
|
||||
</div>
|
||||
<VirtualizedCodeBlock
|
||||
lines={codeLines}
|
||||
language={language || 'text'}
|
||||
syntaxTheme={syntaxTheme}
|
||||
maxHeight="50vh"
|
||||
<PierreFile
|
||||
file={{
|
||||
name: displayPath,
|
||||
contents: content,
|
||||
lang: language || undefined,
|
||||
}}
|
||||
options={{
|
||||
disableFileHeader: true,
|
||||
overflow: 'wrap',
|
||||
theme: pierreTheme,
|
||||
themeType: pierreThemeType,
|
||||
}}
|
||||
className="block w-full"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
@@ -723,6 +748,8 @@ interface ReadToolVirtualizedProps {
|
||||
input?: Record<string, unknown>;
|
||||
syntaxTheme: { [key: string]: React.CSSProperties };
|
||||
toolName: string;
|
||||
pierreTheme: { light: string; dark: string };
|
||||
pierreThemeType: 'light' | 'dark';
|
||||
renderScrollableBlock: (
|
||||
content: React.ReactNode,
|
||||
options?: { maxHeightClass?: string; className?: string; disableHorizontal?: boolean; outerClassName?: string }
|
||||
@@ -734,41 +761,53 @@ const ReadToolVirtualized: React.FC<ReadToolVirtualizedProps> = React.memo(({
|
||||
input,
|
||||
syntaxTheme,
|
||||
toolName,
|
||||
pierreTheme,
|
||||
pierreThemeType,
|
||||
renderScrollableBlock,
|
||||
}) => {
|
||||
const parsedReadOutput = React.useMemo(() => parseReadToolOutput(outputString), [outputString]);
|
||||
const offset = typeof input?.offset === 'number' ? input.offset : 0;
|
||||
|
||||
const codeLines: CodeLine[] = React.useMemo(() => {
|
||||
const hasExplicitLineNumbers = parsedReadOutput.lines.some((line) => line.lineNumber !== null);
|
||||
let fallbackLineCursor = offset;
|
||||
|
||||
return parsedReadOutput.lines.map((line) => {
|
||||
if (line.lineNumber !== null) {
|
||||
fallbackLineCursor = line.lineNumber;
|
||||
}
|
||||
const shouldAssignFallback =
|
||||
parsedReadOutput.type === 'file'
|
||||
&& !hasExplicitLineNumbers
|
||||
&& line.lineNumber === null
|
||||
&& !line.isInfo;
|
||||
const effectiveLineNumber = line.lineNumber ?? (shouldAssignFallback
|
||||
? (fallbackLineCursor += 1)
|
||||
: null);
|
||||
|
||||
return {
|
||||
text: line.text,
|
||||
lineNumber: effectiveLineNumber,
|
||||
isInfo: line.isInfo,
|
||||
};
|
||||
});
|
||||
}, [parsedReadOutput, offset]);
|
||||
|
||||
const language = React.useMemo(() => {
|
||||
const contentForLanguage = parsedReadOutput.lines.map((l) => l.text).join('\n');
|
||||
return detectLanguageFromOutput(contentForLanguage, toolName, input as Record<string, unknown>);
|
||||
}, [parsedReadOutput, toolName, input]);
|
||||
|
||||
const filePath =
|
||||
typeof input?.filePath === 'string'
|
||||
? input.filePath
|
||||
: typeof input?.file_path === 'string'
|
||||
? input.file_path
|
||||
: typeof input?.path === 'string'
|
||||
? input.path
|
||||
: 'read-output';
|
||||
|
||||
const codeLines: CodeLine[] = React.useMemo(() => parsedReadOutput.lines.map((line) => ({
|
||||
text: line.text,
|
||||
lineNumber: line.lineNumber,
|
||||
isInfo: line.isInfo,
|
||||
})), [parsedReadOutput]);
|
||||
|
||||
if (parsedReadOutput.type === 'file') {
|
||||
const fileContent = parsedReadOutput.lines.map((line) => line.text).join('\n');
|
||||
return renderScrollableBlock(
|
||||
<PierreFile
|
||||
file={{
|
||||
name: filePath,
|
||||
contents: fileContent,
|
||||
lang: language || undefined,
|
||||
}}
|
||||
options={{
|
||||
disableFileHeader: true,
|
||||
overflow: 'wrap',
|
||||
theme: pierreTheme,
|
||||
themeType: pierreThemeType,
|
||||
}}
|
||||
className="block w-full"
|
||||
/>,
|
||||
{ className: 'p-1' }
|
||||
) as React.ReactElement;
|
||||
}
|
||||
|
||||
return renderScrollableBlock(
|
||||
<VirtualizedCodeBlock
|
||||
lines={codeLines}
|
||||
@@ -845,6 +884,8 @@ const ToolExpandedContent: React.FC<ToolExpandedContentProps> = React.memo(({
|
||||
hasPrevTool,
|
||||
hasNextTool,
|
||||
}) => {
|
||||
const { pierreTheme, pierreThemeType } = usePierreThemeConfig();
|
||||
const [diffViewMode, setDiffViewMode] = React.useState<DiffViewMode>('unified');
|
||||
const stateWithData = state as ToolStateWithMetadata;
|
||||
const metadata = stateWithData.metadata;
|
||||
const input = stateWithData.input;
|
||||
@@ -892,6 +933,10 @@ const ToolExpandedContent: React.FC<ToolExpandedContentProps> = React.memo(({
|
||||
}, [input, part.tool]);
|
||||
const hasInputText = part.tool !== 'apply_patch' && inputTextContent.trim().length > 0;
|
||||
|
||||
React.useEffect(() => {
|
||||
setDiffViewMode('unified');
|
||||
}, [part.id]);
|
||||
|
||||
const renderScrollableBlock = (
|
||||
content: React.ReactNode,
|
||||
options?: { maxHeightClass?: string; className?: string; disableHorizontal?: boolean; outerClassName?: string }
|
||||
@@ -1042,7 +1087,12 @@ const ToolExpandedContent: React.FC<ToolExpandedContentProps> = React.memo(({
|
||||
|
||||
if ((part.tool === 'edit' || part.tool === 'multiedit' || part.tool === 'apply_patch') && diffContent) {
|
||||
return renderScrollableBlock(
|
||||
<DiffPreview diff={diffContent} syntaxTheme={syntaxTheme} input={input} />,
|
||||
<DiffPreview
|
||||
diff={diffContent}
|
||||
pierreTheme={pierreTheme}
|
||||
pierreThemeType={pierreThemeType}
|
||||
diffViewMode={diffViewMode}
|
||||
/>,
|
||||
{ className: 'p-1' }
|
||||
);
|
||||
}
|
||||
@@ -1054,6 +1104,8 @@ const ToolExpandedContent: React.FC<ToolExpandedContentProps> = React.memo(({
|
||||
input={input}
|
||||
syntaxTheme={syntaxTheme}
|
||||
toolName={part.tool}
|
||||
pierreTheme={pierreTheme}
|
||||
pierreThemeType={pierreThemeType}
|
||||
renderScrollableBlock={renderScrollableBlock}
|
||||
/>;
|
||||
}
|
||||
@@ -1122,9 +1174,10 @@ const ToolExpandedContent: React.FC<ToolExpandedContentProps> = React.memo(({
|
||||
{renderScrollableBlock(
|
||||
<WriteInputPreview
|
||||
content={writeInputContent as string}
|
||||
syntaxTheme={syntaxTheme}
|
||||
filePath={writeFilePath}
|
||||
displayPath={writeDisplayPath ?? 'New file'}
|
||||
pierreTheme={pierreTheme}
|
||||
pierreThemeType={pierreThemeType}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@@ -1141,8 +1194,17 @@ const ToolExpandedContent: React.FC<ToolExpandedContentProps> = React.memo(({
|
||||
|
||||
{part.tool !== 'write' && state.status === 'completed' && 'output' in state && (
|
||||
<div>
|
||||
<div className="typography-meta font-medium text-muted-foreground/80 mb-1">
|
||||
Result:
|
||||
<div className="mb-1 flex items-center justify-between gap-2">
|
||||
<div className="typography-meta font-medium text-muted-foreground/80">
|
||||
Result:
|
||||
</div>
|
||||
{(part.tool === 'edit' || part.tool === 'multiedit' || part.tool === 'apply_patch') && diffContent ? (
|
||||
<DiffViewToggle
|
||||
mode={diffViewMode}
|
||||
onModeChange={setDiffViewMode}
|
||||
className="h-5 w-5 p-0"
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
{renderResultContent()}
|
||||
</div>
|
||||
@@ -1190,49 +1252,53 @@ const ToolPart: React.FC<ToolPartProps> = ({
|
||||
|
||||
|
||||
|
||||
const previousExpandedRef = React.useRef<boolean | undefined>(isExpanded);
|
||||
const shouldNotifyStructuralChange = isFinalized || isTaskTool;
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!isFinalized && !isTaskTool) {
|
||||
if (!shouldNotifyStructuralChange) {
|
||||
return;
|
||||
}
|
||||
if (previousExpandedRef.current === isExpanded) {
|
||||
return;
|
||||
}
|
||||
previousExpandedRef.current = isExpanded;
|
||||
if (typeof isExpanded === 'boolean') {
|
||||
onContentChange?.('structural');
|
||||
}
|
||||
}, [isExpanded, isFinalized, isTaskTool, onContentChange]);
|
||||
}, [isExpanded, onContentChange, shouldNotifyStructuralChange]);
|
||||
|
||||
const stateWithData = state as ToolStateWithMetadata;
|
||||
const metadata = stateWithData.metadata;
|
||||
const input = stateWithData.input;
|
||||
const time = stateWithData.time;
|
||||
|
||||
// Pin start/end so a server-side time reset doesn't reset UI duration.
|
||||
const pinnedTaskTimeRef = React.useRef<{ start?: number; end?: number }>({});
|
||||
const lastPinnedTaskIdRef = React.useRef<string>(part.id);
|
||||
const [pinnedTaskTime, setPinnedTaskTime] = React.useState<{ start?: number; end?: number }>({});
|
||||
|
||||
if (lastPinnedTaskIdRef.current !== part.id) {
|
||||
lastPinnedTaskIdRef.current = part.id;
|
||||
pinnedTaskTimeRef.current = {};
|
||||
}
|
||||
React.useEffect(() => {
|
||||
setPinnedTaskTime({});
|
||||
}, [part.id]);
|
||||
|
||||
if (isTaskTool) {
|
||||
if (typeof time?.start === 'number') {
|
||||
const pinnedStart = pinnedTaskTimeRef.current.start;
|
||||
if (typeof pinnedStart !== 'number' || time.start < pinnedStart) {
|
||||
pinnedTaskTimeRef.current.start = time.start;
|
||||
React.useEffect(() => {
|
||||
if (!isTaskTool) {
|
||||
return;
|
||||
}
|
||||
|
||||
setPinnedTaskTime((prev) => {
|
||||
const next = { ...prev };
|
||||
let changed = false;
|
||||
|
||||
if (typeof time?.start === 'number' && (typeof prev.start !== 'number' || time.start < prev.start)) {
|
||||
next.start = time.start;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
if (typeof time?.end === 'number') {
|
||||
pinnedTaskTimeRef.current.end = time.end;
|
||||
}
|
||||
}
|
||||
|
||||
const effectiveTimeStart = isTaskTool ? (pinnedTaskTimeRef.current.start ?? time?.start) : time?.start;
|
||||
const effectiveTimeEnd = isTaskTool ? (pinnedTaskTimeRef.current.end ?? time?.end) : time?.end;
|
||||
if (typeof time?.end === 'number' && prev.end !== time.end) {
|
||||
next.end = time.end;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
return changed ? next : prev;
|
||||
});
|
||||
}, [isTaskTool, time?.end, time?.start]);
|
||||
|
||||
const effectiveTimeStart = isTaskTool ? (pinnedTaskTime.start ?? time?.start) : time?.start;
|
||||
const effectiveTimeEnd = isTaskTool ? (pinnedTaskTime.end ?? time?.end) : time?.end;
|
||||
|
||||
const taskOutputString = React.useMemo(() => {
|
||||
return typeof stateWithData.output === 'string' ? stateWithData.output : undefined;
|
||||
@@ -1371,7 +1437,7 @@ const ToolPart: React.FC<ToolPartProps> = ({
|
||||
|
||||
const runtime = React.useContext(RuntimeAPIContext);
|
||||
|
||||
const handleMainClick = (e: React.MouseEvent) => {
|
||||
const handleMainClick = (e: { stopPropagation: () => void }) => {
|
||||
if (isTaskTool || !runtime?.editor) {
|
||||
onToggle(part.id);
|
||||
return;
|
||||
@@ -1400,6 +1466,14 @@ const ToolPart: React.FC<ToolPartProps> = ({
|
||||
}
|
||||
};
|
||||
|
||||
const handleMainKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {
|
||||
if (event.key !== 'Enter' && event.key !== ' ') {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
handleMainClick(event);
|
||||
};
|
||||
|
||||
if (!isFinalized && !isTaskTool) {
|
||||
return null;
|
||||
}
|
||||
@@ -1412,10 +1486,18 @@ const ToolPart: React.FC<ToolPartProps> = ({
|
||||
'group/tool flex items-center gap-2 pr-2 pl-px py-1.5 rounded-xl cursor-pointer'
|
||||
)}
|
||||
onClick={handleMainClick}
|
||||
onKeyDown={handleMainKeyDown}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
>
|
||||
<div className="flex items-center gap-2 flex-shrink-0">
|
||||
{}
|
||||
<div className="relative h-3.5 w-3.5 flex-shrink-0" onClick={(e) => { e.stopPropagation(); onToggle(part.id); }}>
|
||||
<button
|
||||
type="button"
|
||||
className="relative h-3.5 w-3.5 flex-shrink-0"
|
||||
onClick={(event) => { event.stopPropagation(); onToggle(part.id); }}
|
||||
aria-label={isExpanded ? 'Collapse tool details' : 'Expand tool details'}
|
||||
>
|
||||
{}
|
||||
<div
|
||||
className={cn(
|
||||
@@ -1438,7 +1520,7 @@ const ToolPart: React.FC<ToolPartProps> = ({
|
||||
>
|
||||
{isExpanded ? <RiArrowDownSLine className="h-3.5 w-3.5" /> : <RiArrowRightSLine className="h-3.5 w-3.5" />}
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
<span
|
||||
className="typography-meta font-medium"
|
||||
style={!isTaskTool && isError ? { color: 'var(--status-error)' } : { color: 'var(--tools-title)' }}
|
||||
|
||||
Reference in New Issue
Block a user