Skip to content
Closed
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
9 changes: 8 additions & 1 deletion src/renderer/components/chat/items/LinkedToolItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
getToolContextTokens,
getToolStatus,
getToolSummary,
hasBashContent,
hasEditContent,
hasReadContent,
hasSkillInstructions,
Expand All @@ -31,6 +32,7 @@ import { Wrench } from 'lucide-react';
import { BaseItem, StatusDot } from './BaseItem';
import { formatDuration } from './baseItemHelpers';
import {
BashToolViewer,
DefaultToolViewer,
EditToolViewer,
ReadToolViewer,
Expand Down Expand Up @@ -139,7 +141,9 @@ export const LinkedToolItem: React.FC<LinkedToolItemProps> = ({
const useWriteViewer =
linkedTool.name === 'Write' && hasWriteContent(linkedTool) && !linkedTool.result?.isError;
const useSkillViewer = linkedTool.name === 'Skill' && hasSkillInstructions(linkedTool);
const useDefaultViewer = !useReadViewer && !useEditViewer && !useWriteViewer && !useSkillViewer;
const useBashViewer = linkedTool.name === 'Bash' && hasBashContent(linkedTool);
const useDefaultViewer =
!useReadViewer && !useEditViewer && !useWriteViewer && !useSkillViewer && !useBashViewer;

// Check if we should show error display for Read/Write tools
const showReadError = linkedTool.name === 'Read' && linkedTool.result?.isError;
Expand Down Expand Up @@ -177,6 +181,9 @@ export const LinkedToolItem: React.FC<LinkedToolItemProps> = ({
{/* Skill tool with instructions */}
{useSkillViewer && <SkillToolViewer linkedTool={linkedTool} />}

{/* Bash tool with syntax-highlighted command */}
{useBashViewer && <BashToolViewer linkedTool={linkedTool} status={status} />}

{/* Default rendering for other tools */}
{useDefaultViewer && <DefaultToolViewer linkedTool={linkedTool} status={status} />}

Expand Down
69 changes: 69 additions & 0 deletions src/renderer/components/chat/items/linkedTool/BashToolViewer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/**
* BashToolViewer
*
* Renders Bash tool calls with a clean command display with copy button,
* and collapsible output section.
*/

import React from 'react';

import { CopyButton } from '@renderer/components/common/CopyButton';
import { COLOR_TEXT, COLOR_TEXT_MUTED } from '@renderer/constants/cssVariables';

import { type ItemStatus } from '../BaseItem';

import { CollapsibleOutputSection } from './CollapsibleOutputSection';
import { extractOutputText, renderOutput } from './renderHelpers';

import type { LinkedToolItem } from '@renderer/types/groups';

interface BashToolViewerProps {
linkedTool: LinkedToolItem;
status: ItemStatus;
}

export const BashToolViewer: React.FC<BashToolViewerProps> = ({ linkedTool, status }) => {
const command = (linkedTool.input.command as string) || '';
const description = linkedTool.input.description as string | undefined;

// Extract output text for copy button
const outputText =
!linkedTool.isOrphaned && linkedTool.result
? extractOutputText(linkedTool.result.content)
: '';

return (
<>
{/* Input Section — Command with copy button */}
<div>
<div className="mb-1 text-xs" style={{ color: 'var(--tool-item-muted)' }}>
Input
</div>
<div
className="group relative max-h-96 overflow-auto rounded p-3 font-mono text-xs"
style={{
backgroundColor: 'var(--code-bg)',
border: '1px solid var(--code-border)',
}}
>
{description && (
<div className="mb-2 text-xs" style={{ color: COLOR_TEXT_MUTED }}>
{description}
</div>
)}
<code className="whitespace-pre-wrap break-all" style={{ color: COLOR_TEXT }}>
{command}
</code>
<CopyButton text={command} />
</div>
</div>

{/* Output Section — Collapsible with copy button */}
{!linkedTool.isOrphaned && linkedTool.result && (
<CollapsibleOutputSection status={status} copyText={outputText}>
{renderOutput(linkedTool.result.content)}
</CollapsibleOutputSection>
)}
</>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/**
* CollapsibleOutputSection
*
* Reusable component that wraps tool output in a collapsed-by-default section.
* Shows a clickable header with label, StatusDot, and chevron toggle.
*/

import React, { useState } from 'react';

import { CopyButton } from '@renderer/components/common/CopyButton';
import { ChevronDown, ChevronRight } from 'lucide-react';

import { type ItemStatus, StatusDot } from '../BaseItem';

interface CollapsibleOutputSectionProps {
status: ItemStatus;
children: React.ReactNode;
/** Label shown in the header (default: "Output") */
label?: string;
/** Text content for the copy button (when provided, shows a copy button) */
copyText?: string;
}

export const CollapsibleOutputSection: React.FC<CollapsibleOutputSectionProps> = ({
status,
children,
label = 'Output',
copyText,
}) => {
const [isExpanded, setIsExpanded] = useState(false);

return (
<div>
<button
type="button"
className="mb-1 flex items-center gap-2 text-xs"
style={{
color: 'var(--tool-item-muted)',
background: 'none',
border: 'none',
padding: 0,
cursor: 'pointer',
}}
onClick={() => setIsExpanded((prev) => !prev)}
Comment on lines +34 to +44

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The inline styles for the button (e.g., color, background, border, padding, cursor) are static and could be moved to a CSS class. This would improve maintainability, promote consistency across components, and separate concerns between structure and styling. Consider defining these styles in a dedicated CSS module or using a utility-first CSS framework if available.

>
{isExpanded ? <ChevronDown className="size-3" /> : <ChevronRight className="size-3" />}
{label}
<StatusDot status={status} />
</button>
{isExpanded && (
<div
className="group relative max-h-96 overflow-auto rounded p-3 font-mono text-xs"
style={{
backgroundColor: 'var(--code-bg)',
border: '1px solid var(--code-border)',
color:
status === 'error'
? 'var(--tool-result-error-text)'
: 'var(--color-text-secondary)',
}}
>
{children}
{copyText && <CopyButton text={copyText} />}
</div>
)}
</div>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@

import React from 'react';

import { type ItemStatus, StatusDot } from '../BaseItem';
import { type ItemStatus } from '../BaseItem';

import { CollapsibleOutputSection } from './CollapsibleOutputSection';
import { renderInput, renderOutput } from './renderHelpers';

import type { LinkedToolItem } from '@renderer/types/groups';
Expand Down Expand Up @@ -37,30 +38,11 @@ export const DefaultToolViewer: React.FC<DefaultToolViewerProps> = ({ linkedTool
</div>
</div>

{/* Output Section */}
{/* Output Section — Collapsed by default */}
{!linkedTool.isOrphaned && linkedTool.result && (
<div>
<div
className="mb-1 flex items-center gap-2 text-xs"
style={{ color: 'var(--tool-item-muted)' }}
>
Output
<StatusDot status={status} />
</div>
<div
className="max-h-96 overflow-auto rounded p-3 font-mono text-xs"
style={{
backgroundColor: 'var(--code-bg)',
border: '1px solid var(--code-border)',
color:
status === 'error'
? 'var(--tool-result-error-text)'
: 'var(--color-text-secondary)',
}}
>
{renderOutput(linkedTool.result.content)}
</div>
</div>
<CollapsibleOutputSection status={status}>
{renderOutput(linkedTool.result.content)}
</CollapsibleOutputSection>
)}
</>
);
Expand Down
57 changes: 50 additions & 7 deletions src/renderer/components/chat/items/linkedTool/ReadToolViewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

import React from 'react';

import { CodeBlockViewer } from '@renderer/components/chat/viewers';
import { CodeBlockViewer, MarkdownViewer } from '@renderer/components/chat/viewers';

import type { LinkedToolItem } from '@renderer/types/groups';

Expand Down Expand Up @@ -54,12 +54,55 @@ export const ReadToolViewer: React.FC<ReadToolViewerProps> = ({ linkedTool }) =>
? startLine + limit - 1
: undefined;

const isMarkdownFile = /\.mdx?$/i.test(filePath);
const [viewMode, setViewMode] = React.useState<'code' | 'preview'>('code');

React.useEffect(() => {
setViewMode(isMarkdownFile ? 'preview' : 'code');
}, [isMarkdownFile]);

return (
<CodeBlockViewer
fileName={filePath}
content={content}
startLine={startLine}
endLine={endLine}
/>
<div className="space-y-2">
{isMarkdownFile && (
<div className="flex items-center justify-end gap-1">
<button
type="button"
onClick={() => setViewMode('code')}
aria-pressed={viewMode === 'code'}
className="rounded px-2 py-1 text-xs transition-colors"
style={{
backgroundColor: viewMode === 'code' ? 'var(--tag-bg)' : 'transparent',
color: viewMode === 'code' ? 'var(--tag-text)' : 'var(--color-text-muted)',
border: '1px solid var(--tag-border)',
Comment on lines +73 to +76

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The inline styles for the 'Code' button are static and could be extracted into a CSS class. This would improve maintainability and consistency with other UI elements.

}}
>
Code
</button>
<button
type="button"
onClick={() => setViewMode('preview')}
aria-pressed={viewMode === 'preview'}
className="rounded px-2 py-1 text-xs transition-colors"
style={{
backgroundColor: viewMode === 'preview' ? 'var(--tag-bg)' : 'transparent',
color: viewMode === 'preview' ? 'var(--tag-text)' : 'var(--color-text-muted)',
border: '1px solid var(--tag-border)',
}}
Comment on lines +87 to +90

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The inline styles for the 'Preview' button are static and could be extracted into a CSS class. This would improve maintainability and consistency with other UI elements.

>
Preview
</button>
</div>
)}
{isMarkdownFile && viewMode === 'preview' ? (
<MarkdownViewer content={content} label="Markdown Preview" copyable />
) : (
<CodeBlockViewer
fileName={filePath}
content={content}
startLine={startLine}
endLine={endLine}
/>
)}
</div>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ export const WriteToolViewer: React.FC<WriteToolViewerProps> = ({ linkedTool })
const isMarkdownFile = /\.mdx?$/i.test(filePath);
const [viewMode, setViewMode] = React.useState<'code' | 'preview'>('code');

React.useEffect(() => {
setViewMode(isMarkdownFile ? 'preview' : 'code');
}, [isMarkdownFile]);

return (
<div className="space-y-2">
<div className="mb-1 text-xs text-zinc-500">
Expand Down
2 changes: 2 additions & 0 deletions src/renderer/components/chat/items/linkedTool/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
* Exports all specialized tool viewer components.
*/

export { BashToolViewer } from './BashToolViewer';
export { CollapsibleOutputSection } from './CollapsibleOutputSection';
export { DefaultToolViewer } from './DefaultToolViewer';
export { EditToolViewer } from './EditToolViewer';
export { ReadToolViewer } from './ReadToolViewer';
Expand Down
Loading