feat: vscode extension (#59)
* feat: add initial VS Code extension plan and implementation tasks * feat(vscode): added initial version of an Openchamber VSCode extension * feat(vscode): enhance VS Code extension with theme integration and session management * feat(vscode): implement connection status handling and overlay in VSCode layout * feat: move extension to secondary sidebar * chore: upgrade @opencode-ai/sdk to 1.0.150 * vscode: editor bridge, file picker, click-to-open in tool parts * vscode: layout session lifecycle, theme sync, typography overrides * ui: compact mode for vscode, model search, autocomplete width fixes * perf: scroll force flag, raf placeholder, git polling backoff * ui: tool output styling, markdown code block fix, gitignore * refactor: update typography handling for VSCode runtime, remove unused styles * docs: update README with VS Code extension details and add extension image * docs: update changelog with new features and performance improvements
This commit is contained in:
committed by
GitHub
parent
610ccf4c62
commit
bb72c0fb0c
@@ -105,7 +105,7 @@ const ToolOutputDialog: React.FC<ToolOutputDialogProps> = ({ popup, onOpenChange
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 min-h-0 rounded-xl border border-border/30 bg-muted/10 overflow-hidden">
|
||||
<div className="h-full max-h-[75vh] overflow-y-auto px-3 pr-4">
|
||||
<div className="tool-output-surface h-full max-h-[75vh] overflow-y-auto px-3 pr-4">
|
||||
{popup.metadata?.input && typeof popup.metadata.input === 'object' &&
|
||||
Object.keys(popup.metadata.input).length > 0 &&
|
||||
popup.metadata?.tool !== 'todowrite' &&
|
||||
@@ -127,45 +127,47 @@ const ToolOutputDialog: React.FC<ToolOutputDialogProps> = ({ popup, onOpenChange
|
||||
: 'Input:'}
|
||||
</div>
|
||||
{meta.tool === 'bash' && getInputValue('command') ? (
|
||||
<div className="bg-muted/30 rounded-xl border border-border/20 mx-3">
|
||||
<div className="tool-input-surface bg-transparent rounded-xl border border-border/20 mx-3">
|
||||
<SyntaxHighlighter
|
||||
style={syntaxTheme}
|
||||
language="bash"
|
||||
PreTag="div"
|
||||
customStyle={toolDisplayStyles.getPopupStyles()}
|
||||
codeTagProps={{ style: { background: 'transparent', backgroundColor: 'transparent', fontSize: 'inherit' } }}
|
||||
wrapLongLines
|
||||
>
|
||||
{getInputValue('command')!}
|
||||
</SyntaxHighlighter>
|
||||
</div>
|
||||
) : meta.tool === 'task' && getInputValue('prompt') ? (
|
||||
<pre
|
||||
className="bg-muted/30 p-3 rounded-xl border border-border/20 font-mono whitespace-pre-wrap text-foreground/90 mx-3"
|
||||
<div
|
||||
className="tool-input-surface bg-transparent rounded-xl border border-border/20 font-mono whitespace-pre-wrap text-foreground/90 mx-3"
|
||||
style={toolDisplayStyles.getPopupStyles()}
|
||||
>
|
||||
{getInputValue('description') ? `Task: ${getInputValue('description')}\n` : ''}
|
||||
{getInputValue('subagent_type') ? `Agent Type: ${getInputValue('subagent_type')}\n` : ''}
|
||||
{`Instructions:\n${getInputValue('prompt')}`}
|
||||
</pre>
|
||||
</div>
|
||||
) : meta.tool === 'write' && getInputValue('content') ? (
|
||||
<div className="bg-muted/30 rounded-xl border border-border/20 mx-3">
|
||||
<div className="tool-input-surface bg-transparent rounded-xl border border-border/20 mx-3">
|
||||
<SyntaxHighlighter
|
||||
style={syntaxTheme}
|
||||
language={getLanguageFromExtension(getInputValue('filePath') || getInputValue('file_path') || '') || 'text'}
|
||||
PreTag="div"
|
||||
customStyle={toolDisplayStyles.getPopupStyles()}
|
||||
codeTagProps={{ style: { background: 'transparent', backgroundColor: 'transparent', fontSize: 'inherit' } }}
|
||||
wrapLongLines
|
||||
>
|
||||
{getInputValue('content')!}
|
||||
</SyntaxHighlighter>
|
||||
</div>
|
||||
) : (
|
||||
<pre
|
||||
className="bg-muted/30 p-3 rounded-xl border border-border/20 font-mono whitespace-pre-wrap text-foreground/90 mx-3"
|
||||
<div
|
||||
className="tool-input-surface bg-transparent rounded-xl border border-border/20 font-mono whitespace-pre-wrap text-foreground/90 mx-3"
|
||||
style={toolDisplayStyles.getPopupStyles()}
|
||||
>
|
||||
{formatInputForDisplay(input, meta.tool as string)}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
@@ -173,7 +175,7 @@ const ToolOutputDialog: React.FC<ToolOutputDialogProps> = ({ popup, onOpenChange
|
||||
|
||||
{popup.isDiff ? (
|
||||
diffViewMode === 'unified' ? (
|
||||
<div className="typography-markdown">
|
||||
<div className="typography-code">
|
||||
{parseDiffToUnified(popup.content).map((hunk, hunkIdx) => (
|
||||
<div key={hunkIdx} className="border-b border-border/20 last:border-b-0">
|
||||
<div
|
||||
@@ -186,7 +188,7 @@ const ToolOutputDialog: React.FC<ToolOutputDialogProps> = ({ popup, onOpenChange
|
||||
<div
|
||||
key={lineIdx}
|
||||
className={cn(
|
||||
'typography-markdown font-mono px-3 py-0.5 flex',
|
||||
'typography-code font-mono px-3 py-0.5 flex',
|
||||
line.type === 'context' && 'bg-transparent',
|
||||
line.type === 'removed' && 'bg-transparent',
|
||||
line.type === 'added' && 'bg-transparent'
|
||||
@@ -223,7 +225,8 @@ const ToolOutputDialog: React.FC<ToolOutputDialogProps> = ({ popup, onOpenChange
|
||||
margin: 0,
|
||||
padding: 0,
|
||||
fontSize: 'inherit',
|
||||
background: 'transparent !important',
|
||||
background: 'transparent',
|
||||
backgroundColor: 'transparent',
|
||||
borderRadius: 0,
|
||||
overflow: 'visible',
|
||||
whiteSpace: 'pre-wrap',
|
||||
@@ -232,7 +235,8 @@ const ToolOutputDialog: React.FC<ToolOutputDialogProps> = ({ popup, onOpenChange
|
||||
}}
|
||||
codeTagProps={{
|
||||
style: {
|
||||
background: 'transparent !important',
|
||||
background: 'transparent',
|
||||
backgroundColor: 'transparent',
|
||||
},
|
||||
}}
|
||||
>
|
||||
@@ -248,7 +252,7 @@ const ToolOutputDialog: React.FC<ToolOutputDialogProps> = ({ popup, onOpenChange
|
||||
))}
|
||||
</div>
|
||||
) : popup.diffHunks ? (
|
||||
<div className="typography-markdown">
|
||||
<div className="typography-code">
|
||||
{popup.diffHunks.map((hunk, hunkIdx) => (
|
||||
<div key={hunkIdx} className="border-b border-border/20 last:border-b-0">
|
||||
<div
|
||||
@@ -261,7 +265,7 @@ const ToolOutputDialog: React.FC<ToolOutputDialogProps> = ({ popup, onOpenChange
|
||||
<div key={lineIdx} className="grid grid-cols-2 divide-x divide-border/20">
|
||||
<div
|
||||
className={cn(
|
||||
'typography-markdown font-mono px-3 py-0.5 overflow-hidden',
|
||||
'typography-code font-mono px-3 py-0.5 overflow-hidden',
|
||||
line.leftLine.type === 'context' && 'bg-transparent',
|
||||
line.leftLine.type === 'empty' && 'bg-transparent'
|
||||
)}
|
||||
@@ -292,7 +296,8 @@ const ToolOutputDialog: React.FC<ToolOutputDialogProps> = ({ popup, onOpenChange
|
||||
margin: 0,
|
||||
padding: 0,
|
||||
fontSize: 'inherit',
|
||||
background: 'transparent !important',
|
||||
background: 'transparent',
|
||||
backgroundColor: 'transparent',
|
||||
borderRadius: 0,
|
||||
overflow: 'visible',
|
||||
whiteSpace: 'pre-wrap',
|
||||
@@ -301,7 +306,8 @@ const ToolOutputDialog: React.FC<ToolOutputDialogProps> = ({ popup, onOpenChange
|
||||
}}
|
||||
codeTagProps={{
|
||||
style: {
|
||||
background: 'transparent !important',
|
||||
background: 'transparent',
|
||||
backgroundColor: 'transparent',
|
||||
},
|
||||
}}
|
||||
>
|
||||
@@ -313,7 +319,7 @@ const ToolOutputDialog: React.FC<ToolOutputDialogProps> = ({ popup, onOpenChange
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
'typography-markdown font-mono px-3 py-0.5 overflow-hidden',
|
||||
'typography-code font-mono px-3 py-0.5 overflow-hidden',
|
||||
line.rightLine.type === 'context' && 'bg-transparent',
|
||||
line.rightLine.type === 'empty' && 'bg-transparent'
|
||||
)}
|
||||
@@ -344,7 +350,8 @@ const ToolOutputDialog: React.FC<ToolOutputDialogProps> = ({ popup, onOpenChange
|
||||
margin: 0,
|
||||
padding: 0,
|
||||
fontSize: 'inherit',
|
||||
background: 'transparent !important',
|
||||
background: 'transparent',
|
||||
backgroundColor: 'transparent',
|
||||
borderRadius: 0,
|
||||
overflow: 'visible',
|
||||
whiteSpace: 'pre-wrap',
|
||||
@@ -353,7 +360,8 @@ const ToolOutputDialog: React.FC<ToolOutputDialogProps> = ({ popup, onOpenChange
|
||||
}}
|
||||
codeTagProps={{
|
||||
style: {
|
||||
background: 'transparent !important',
|
||||
background: 'transparent',
|
||||
backgroundColor: 'transparent',
|
||||
},
|
||||
}}
|
||||
>
|
||||
@@ -402,7 +410,7 @@ const ToolOutputDialog: React.FC<ToolOutputDialogProps> = ({ popup, onOpenChange
|
||||
PreTag="div"
|
||||
wrapLongLines
|
||||
customStyle={toolDisplayStyles.getPopupContainerStyles()}
|
||||
codeTagProps={{ style: { background: 'transparent !important' } }}
|
||||
codeTagProps={{ style: { background: 'transparent', backgroundColor: 'transparent', fontSize: 'inherit' } }}
|
||||
>
|
||||
{popup.content}
|
||||
</SyntaxHighlighter>
|
||||
@@ -417,13 +425,13 @@ const ToolOutputDialog: React.FC<ToolOutputDialogProps> = ({ popup, onOpenChange
|
||||
{popup.content}
|
||||
</pre>
|
||||
)
|
||||
);
|
||||
);
|
||||
}
|
||||
|
||||
if (tool === 'grep') {
|
||||
return (
|
||||
renderGrepOutput(popup.content, isMobile) || (
|
||||
<pre className="typography-markdown bg-muted/30 p-2 rounded-xl border border-border/20 font-mono whitespace-pre-wrap">
|
||||
<pre className="typography-code bg-muted/30 p-2 rounded-xl border border-border/20 font-mono whitespace-pre-wrap">
|
||||
{popup.content}
|
||||
</pre>
|
||||
)
|
||||
@@ -433,7 +441,7 @@ const ToolOutputDialog: React.FC<ToolOutputDialogProps> = ({ popup, onOpenChange
|
||||
if (tool === 'glob') {
|
||||
return (
|
||||
renderGlobOutput(popup.content, isMobile) || (
|
||||
<pre className="typography-markdown bg-muted/30 p-2 rounded-xl border border-border/20 font-mono whitespace-pre-wrap">
|
||||
<pre className="typography-code bg-muted/30 p-2 rounded-xl border border-border/20 font-mono whitespace-pre-wrap">
|
||||
{popup.content}
|
||||
</pre>
|
||||
)
|
||||
@@ -444,7 +452,7 @@ const ToolOutputDialog: React.FC<ToolOutputDialogProps> = ({ popup, onOpenChange
|
||||
return (
|
||||
<div
|
||||
className={tool === 'reasoning' ? "text-muted-foreground/70" : ""}
|
||||
style={{ fontSize: 'var(--text-meta)' }}
|
||||
style={{ fontSize: tool === 'task' ? 'var(--text-code)' : 'var(--text-meta)' }}
|
||||
>
|
||||
<Streamdown mode="static" className="streamdown-content streamdown-tool">
|
||||
{popup.content}
|
||||
@@ -462,7 +470,7 @@ const ToolOutputDialog: React.FC<ToolOutputDialogProps> = ({ popup, onOpenChange
|
||||
PreTag="div"
|
||||
wrapLongLines
|
||||
customStyle={toolDisplayStyles.getPopupContainerStyles()}
|
||||
codeTagProps={{ style: { background: 'transparent !important' } }}
|
||||
codeTagProps={{ style: { background: 'transparent', backgroundColor: 'transparent', fontSize: 'inherit' } }}
|
||||
>
|
||||
{popup.content}
|
||||
</SyntaxHighlighter>
|
||||
@@ -491,7 +499,7 @@ const ToolOutputDialog: React.FC<ToolOutputDialogProps> = ({ popup, onOpenChange
|
||||
const shouldShowLineNumber = !isInfo && (limit === undefined || idx < limit);
|
||||
|
||||
return (
|
||||
<div key={idx} className={`typography-markdown font-mono flex ${isInfo ? 'text-muted-foreground/70 italic' : ''}`}>
|
||||
<div key={idx} className={`typography-code font-mono flex ${isInfo ? 'text-muted-foreground/70 italic' : ''}`}>
|
||||
<span className="text-muted-foreground/60 w-10 flex-shrink-0 text-right pr-4 self-start select-none">
|
||||
{shouldShowLineNumber ? lineNumber : ''}
|
||||
</span>
|
||||
@@ -509,7 +517,8 @@ const ToolOutputDialog: React.FC<ToolOutputDialogProps> = ({ popup, onOpenChange
|
||||
margin: 0,
|
||||
padding: 0,
|
||||
fontSize: 'inherit',
|
||||
background: 'transparent !important',
|
||||
background: 'transparent',
|
||||
backgroundColor: 'transparent',
|
||||
borderRadius: 0,
|
||||
overflow: 'visible',
|
||||
whiteSpace: 'pre-wrap',
|
||||
@@ -518,7 +527,9 @@ const ToolOutputDialog: React.FC<ToolOutputDialogProps> = ({ popup, onOpenChange
|
||||
}}
|
||||
codeTagProps={{
|
||||
style: {
|
||||
background: 'transparent !important',
|
||||
background: 'transparent',
|
||||
backgroundColor: 'transparent',
|
||||
fontSize: 'inherit',
|
||||
},
|
||||
}}
|
||||
>
|
||||
@@ -540,7 +551,7 @@ const ToolOutputDialog: React.FC<ToolOutputDialogProps> = ({ popup, onOpenChange
|
||||
PreTag="div"
|
||||
wrapLongLines
|
||||
customStyle={toolDisplayStyles.getPopupContainerStyles()}
|
||||
codeTagProps={{ style: { background: 'transparent !important' } }}
|
||||
codeTagProps={{ style: { background: 'transparent', backgroundColor: 'transparent', fontSize: 'inherit' } }}
|
||||
>
|
||||
{popup.content}
|
||||
</SyntaxHighlighter>
|
||||
@@ -550,7 +561,7 @@ const ToolOutputDialog: React.FC<ToolOutputDialogProps> = ({ popup, onOpenChange
|
||||
) : (
|
||||
<div className="p-8 text-muted-foreground typography-ui-header">
|
||||
<div className="mb-2">Command completed successfully</div>
|
||||
<div className="typography-markdown">No output was produced</div>
|
||||
<div className="typography-meta">No output was produced</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
|
||||
import React from 'react';
|
||||
import { RuntimeAPIContext } from '@/contexts/runtimeAPIContext';
|
||||
import { RiArrowDownSLine, RiArrowRightSLine, RiFileEditLine, RiFileSearchLine, RiFileTextLine, RiFolder6Line, RiGitBranchLine, RiGlobalLine, RiListCheck3, RiMenuSearchLine, RiPencilLine, RiTerminalBoxLine, RiToolsLine } from '@remixicon/react';
|
||||
import { Streamdown } from 'streamdown';
|
||||
import { cn } from '@/lib/utils';
|
||||
@@ -175,7 +176,7 @@ const ToolScrollableSection: React.FC<ToolScrollableSectionProps> = ({
|
||||
}) => (
|
||||
<ScrollableOverlay
|
||||
outerClassName={cn('w-full min-w-0 flex-none overflow-hidden', maxHeightClass, outerClassName)}
|
||||
className={cn('p-2 rounded-xl w-full min-w-0 border border-border/20 bg-muted/30', className)}
|
||||
className={cn('tool-output-surface p-2 rounded-xl w-full min-w-0 border border-border/20 bg-transparent', className)}
|
||||
disableHorizontal={disableHorizontal}
|
||||
>
|
||||
<div className="w-full min-w-0">
|
||||
@@ -191,7 +192,7 @@ interface DiffPreviewProps {
|
||||
}
|
||||
|
||||
const DiffPreview: React.FC<DiffPreviewProps> = ({ diff, syntaxTheme, input }) => (
|
||||
<div className="typography-meta px-1 pb-1 pt-0 space-y-0">
|
||||
<div className="typography-code px-1 pb-1 pt-0 space-y-0">
|
||||
{parseDiffToUnified(diff).map((hunk, hunkIdx) => (
|
||||
<div key={hunkIdx} className="-mx-1 px-1 border-b border-border/20 last:border-b-0">
|
||||
<div className="bg-muted/20 px-2 py-1 typography-meta font-medium text-muted-foreground border-b border-border/10 break-words -mx-1">
|
||||
@@ -203,7 +204,7 @@ const DiffPreview: React.FC<DiffPreviewProps> = ({ diff, syntaxTheme, input }) =
|
||||
<div
|
||||
key={lineIdx}
|
||||
className={cn(
|
||||
'typography-meta font-mono px-2 py-0.5 flex -mx-2',
|
||||
'typography-code font-mono px-2 py-0.5 flex -mx-2',
|
||||
line.type === 'context' && 'bg-transparent',
|
||||
line.type === 'removed' && 'bg-transparent',
|
||||
line.type === 'added' && 'bg-transparent'
|
||||
@@ -226,23 +227,24 @@ const DiffPreview: React.FC<DiffPreviewProps> = ({ diff, syntaxTheme, input }) =
|
||||
PreTag="div"
|
||||
wrapLines
|
||||
wrapLongLines
|
||||
customStyle={{
|
||||
margin: 0,
|
||||
padding: 0,
|
||||
fontSize: 'inherit',
|
||||
background: 'transparent !important',
|
||||
borderRadius: 0,
|
||||
overflow: 'visible',
|
||||
whiteSpace: 'pre-wrap',
|
||||
wordBreak: 'break-all',
|
||||
overflowWrap: 'anywhere',
|
||||
}}
|
||||
codeTagProps={{
|
||||
style: { background: 'transparent !important' },
|
||||
}}
|
||||
>
|
||||
{line.content}
|
||||
</SyntaxHighlighter>
|
||||
customStyle={{
|
||||
margin: 0,
|
||||
padding: 0,
|
||||
fontSize: 'inherit',
|
||||
background: 'transparent',
|
||||
backgroundColor: 'transparent',
|
||||
borderRadius: 0,
|
||||
overflow: 'visible',
|
||||
whiteSpace: 'pre-wrap',
|
||||
wordBreak: 'break-all',
|
||||
overflowWrap: 'anywhere',
|
||||
}}
|
||||
codeTagProps={{
|
||||
style: { background: 'transparent', backgroundColor: 'transparent', fontSize: 'inherit' },
|
||||
}}
|
||||
>
|
||||
{line.content}
|
||||
</SyntaxHighlighter>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
@@ -273,7 +275,7 @@ const WriteInputPreview: React.FC<WriteInputPreviewProps> = ({ content, syntaxTh
|
||||
</div>
|
||||
<div className="space-y-0">
|
||||
{lines.map((line, lineIdx) => (
|
||||
<div key={lineIdx} className="typography-meta font-mono px-2 py-0.5 flex -mx-1">
|
||||
<div key={lineIdx} className="typography-code font-mono px-2 py-0.5 flex -mx-1">
|
||||
<span className="text-muted-foreground/60 w-8 flex-shrink-0 text-right pr-2 self-start select-none">
|
||||
{lineIdx + 1}
|
||||
</span>
|
||||
@@ -288,7 +290,8 @@ const WriteInputPreview: React.FC<WriteInputPreviewProps> = ({ content, syntaxTh
|
||||
margin: 0,
|
||||
padding: 0,
|
||||
fontSize: 'inherit',
|
||||
background: 'transparent !important',
|
||||
background: 'transparent',
|
||||
backgroundColor: 'transparent',
|
||||
borderRadius: 0,
|
||||
overflow: 'visible',
|
||||
whiteSpace: 'pre-wrap',
|
||||
@@ -296,7 +299,7 @@ const WriteInputPreview: React.FC<WriteInputPreviewProps> = ({ content, syntaxTh
|
||||
overflowWrap: 'anywhere',
|
||||
}}
|
||||
codeTagProps={{
|
||||
style: { background: 'transparent !important' },
|
||||
style: { background: 'transparent', backgroundColor: 'transparent', fontSize: 'inherit' },
|
||||
}}
|
||||
>
|
||||
{line || ' '}
|
||||
@@ -421,7 +424,7 @@ const ToolExpandedContent: React.FC<ToolExpandedContentProps> = ({
|
||||
const listOutput = renderListOutput(outputString, { unstyled: true });
|
||||
return renderScrollableBlock(
|
||||
listOutput ?? (
|
||||
<pre className="typography-meta font-mono whitespace-pre-wrap break-words w-full min-w-0">
|
||||
<pre className="typography-code font-mono whitespace-pre-wrap break-words w-full min-w-0">
|
||||
{outputString}
|
||||
</pre>
|
||||
)
|
||||
@@ -432,7 +435,7 @@ const ToolExpandedContent: React.FC<ToolExpandedContentProps> = ({
|
||||
const grepOutput = renderGrepOutput(outputString, isMobile, { unstyled: true });
|
||||
return renderScrollableBlock(
|
||||
grepOutput ?? (
|
||||
<pre className="typography-meta font-mono whitespace-pre-wrap break-words w-full min-w-0">
|
||||
<pre className="typography-code font-mono whitespace-pre-wrap break-words w-full min-w-0">
|
||||
{outputString}
|
||||
</pre>
|
||||
)
|
||||
@@ -443,7 +446,7 @@ const ToolExpandedContent: React.FC<ToolExpandedContentProps> = ({
|
||||
const globOutput = renderGlobOutput(outputString, isMobile, { unstyled: true });
|
||||
return renderScrollableBlock(
|
||||
globOutput ?? (
|
||||
<pre className="typography-meta font-mono whitespace-pre-wrap break-words w-full min-w-0">
|
||||
<pre className="typography-code font-mono whitespace-pre-wrap break-words w-full min-w-0">
|
||||
{outputString}
|
||||
</pre>
|
||||
)
|
||||
@@ -464,7 +467,7 @@ const ToolExpandedContent: React.FC<ToolExpandedContentProps> = ({
|
||||
const webSearchContent = renderWebSearchOutput(outputString, syntaxTheme, { unstyled: true });
|
||||
return renderScrollableBlock(
|
||||
webSearchContent ?? (
|
||||
<pre className="typography-meta font-mono whitespace-pre-wrap break-words w-full min-w-0">
|
||||
<pre className="typography-code font-mono whitespace-pre-wrap break-words w-full min-w-0">
|
||||
{outputString}
|
||||
</pre>
|
||||
)
|
||||
@@ -497,14 +500,14 @@ const ToolExpandedContent: React.FC<ToolExpandedContentProps> = ({
|
||||
const isInfoMessage = (line: string) => line.trim().startsWith('(');
|
||||
|
||||
return renderScrollableBlock(
|
||||
<div className="typography-meta w-full min-w-0 space-y-1">
|
||||
<div className="typography-code w-full min-w-0 space-y-1">
|
||||
{lines.map((line: string, idx: number) => {
|
||||
const isInfo = isInfoMessage(line);
|
||||
const lineNumber = offset + idx + 1;
|
||||
const shouldShowLineNumber = !isInfo && (limit === undefined || idx < limit);
|
||||
|
||||
return (
|
||||
<div key={idx} className={cn('typography-meta font-mono flex w-full min-w-0', isInfo && 'text-muted-foreground/70 italic')}>
|
||||
<div key={idx} className={cn('typography-code font-mono flex w-full min-w-0', isInfo && 'text-muted-foreground/70 italic')}>
|
||||
<span className="text-muted-foreground/60 w-8 flex-shrink-0 text-right pr-3 self-start select-none">
|
||||
{shouldShowLineNumber ? lineNumber : ''}
|
||||
</span>
|
||||
@@ -522,7 +525,8 @@ const ToolExpandedContent: React.FC<ToolExpandedContentProps> = ({
|
||||
margin: 0,
|
||||
padding: 0,
|
||||
fontSize: 'inherit',
|
||||
background: 'transparent !important',
|
||||
background: 'transparent',
|
||||
backgroundColor: 'transparent',
|
||||
borderRadius: 0,
|
||||
overflow: 'visible',
|
||||
whiteSpace: 'pre-wrap',
|
||||
@@ -531,7 +535,8 @@ const ToolExpandedContent: React.FC<ToolExpandedContentProps> = ({
|
||||
}}
|
||||
codeTagProps={{
|
||||
style: {
|
||||
background: 'transparent !important',
|
||||
background: 'transparent',
|
||||
backgroundColor: 'transparent',
|
||||
},
|
||||
}}
|
||||
>
|
||||
@@ -559,7 +564,8 @@ const ToolExpandedContent: React.FC<ToolExpandedContentProps> = ({
|
||||
}}
|
||||
codeTagProps={{
|
||||
style: {
|
||||
background: 'transparent !important',
|
||||
background: 'transparent',
|
||||
backgroundColor: 'transparent',
|
||||
},
|
||||
}}
|
||||
wrapLongLines
|
||||
@@ -603,10 +609,10 @@ const ToolExpandedContent: React.FC<ToolExpandedContentProps> = ({
|
||||
) : hasInputText ? (
|
||||
<div className="my-1">
|
||||
{renderScrollableBlock(
|
||||
<blockquote className="whitespace-pre-wrap break-words typography-meta italic text-muted-foreground/70">
|
||||
<blockquote className="tool-input-text whitespace-pre-wrap break-words typography-meta italic text-muted-foreground/70">
|
||||
{inputTextContent}
|
||||
</blockquote>,
|
||||
{ maxHeightClass: 'max-h-60' }
|
||||
{ maxHeightClass: 'max-h-60', className: 'tool-input-surface' }
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
@@ -674,10 +680,38 @@ const ToolPart: React.FC<ToolPartProps> = ({ part, isExpanded, onToggle, syntaxT
|
||||
|
||||
const stateWithData = state as ToolStateWithMetadata;
|
||||
const metadata = stateWithData.metadata;
|
||||
const input = stateWithData.input;
|
||||
const diffStats = (part.tool === 'edit' || part.tool === 'multiedit') ? parseDiffStats(metadata) : null;
|
||||
const description = getToolDescription(part, state, isMobile, currentDirectory);
|
||||
const displayName = getToolMetadata(part.tool).displayName;
|
||||
|
||||
const runtime = React.useContext(RuntimeAPIContext);
|
||||
|
||||
const handleMainClick = (e: React.MouseEvent) => {
|
||||
if (!runtime?.editor) {
|
||||
onToggle(part.id);
|
||||
return;
|
||||
}
|
||||
|
||||
let filePath: unknown;
|
||||
if (part.tool === 'edit' || part.tool === 'multiedit') {
|
||||
filePath = input?.filePath || input?.file_path || input?.path || metadata?.filePath || metadata?.file_path || metadata?.path;
|
||||
} else if (['write', 'create', 'file_write', 'read', 'view', 'file_read', 'cat'].includes(part.tool)) {
|
||||
filePath = input?.filePath || input?.file_path || input?.path || metadata?.filePath || metadata?.file_path || metadata?.path;
|
||||
}
|
||||
|
||||
if (typeof filePath === 'string') {
|
||||
e.stopPropagation();
|
||||
let absolutePath = filePath;
|
||||
if (!filePath.startsWith('/')) {
|
||||
absolutePath = currentDirectory.endsWith('/') ? currentDirectory + filePath : currentDirectory + '/' + filePath;
|
||||
}
|
||||
runtime.editor.openFile(absolutePath);
|
||||
} else {
|
||||
onToggle(part.id);
|
||||
}
|
||||
};
|
||||
|
||||
if (!isFinalized) {
|
||||
return null;
|
||||
}
|
||||
@@ -689,11 +723,11 @@ const ToolPart: React.FC<ToolPartProps> = ({ part, isExpanded, onToggle, syntaxT
|
||||
className={cn(
|
||||
'group/tool flex items-center gap-2 pr-2 pl-px py-1.5 rounded-xl cursor-pointer'
|
||||
)}
|
||||
onClick={() => onToggle(part.id)}
|
||||
onClick={handleMainClick}
|
||||
>
|
||||
<div className="flex items-center gap-2 flex-shrink-0">
|
||||
{}
|
||||
<div className="relative h-3.5 w-3.5 flex-shrink-0">
|
||||
<div className="relative h-3.5 w-3.5 flex-shrink-0" onClick={(e) => { e.stopPropagation(); onToggle(part.id); }}>
|
||||
{}
|
||||
<div
|
||||
className={cn(
|
||||
|
||||
@@ -34,6 +34,8 @@ export function WorkingPlaceholder({
|
||||
const fadeTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const resultTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const transitionTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const rafIdRef = useRef<number | null>(null);
|
||||
const lastCheckTimeRef = useRef<number>(0);
|
||||
const lastActiveStatusRef = useRef<string | null>(null);
|
||||
const hasShownActivityRef = useRef<boolean>(false);
|
||||
const wasAbortedRef = useRef<boolean>(false);
|
||||
@@ -207,7 +209,16 @@ export function WorkingPlaceholder({
|
||||
wasAbortedRef.current = false;
|
||||
};
|
||||
|
||||
const checkInterval = setInterval(() => {
|
||||
const CHECK_THROTTLE_MS = 150; // Throttle checks to ~6-7 times per second
|
||||
|
||||
const checkLoop = (timestamp: number) => {
|
||||
// Throttle: skip if less than CHECK_THROTTLE_MS since last check
|
||||
if (timestamp - lastCheckTimeRef.current < CHECK_THROTTLE_MS) {
|
||||
rafIdRef.current = requestAnimationFrame(checkLoop);
|
||||
return;
|
||||
}
|
||||
lastCheckTimeRef.current = timestamp;
|
||||
|
||||
const now = Date.now();
|
||||
const elapsed = now - displayStartTimeRef.current;
|
||||
|
||||
@@ -216,6 +227,7 @@ export function WorkingPlaceholder({
|
||||
const shouldWaitForMinTime = !isDone && statusQueueRef.current.length > 0;
|
||||
|
||||
if (shouldWaitForMinTime && elapsed < MIN_DISPLAY_TIME) {
|
||||
rafIdRef.current = requestAnimationFrame(checkLoop);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -257,14 +269,24 @@ export function WorkingPlaceholder({
|
||||
lastActiveStatusRef.current = null;
|
||||
removalPendingRef.current = false;
|
||||
wasAbortedRef.current = false;
|
||||
rafIdRef.current = requestAnimationFrame(checkLoop);
|
||||
return;
|
||||
}
|
||||
|
||||
startFadeOut(result);
|
||||
}
|
||||
}, 50);
|
||||
|
||||
return () => clearInterval(checkInterval);
|
||||
rafIdRef.current = requestAnimationFrame(checkLoop);
|
||||
};
|
||||
|
||||
rafIdRef.current = requestAnimationFrame(checkLoop);
|
||||
|
||||
return () => {
|
||||
if (rafIdRef.current !== null) {
|
||||
cancelAnimationFrame(rafIdRef.current);
|
||||
rafIdRef.current = null;
|
||||
}
|
||||
};
|
||||
|
||||
}, [isFadingOut]);
|
||||
|
||||
|
||||
@@ -70,7 +70,7 @@ export const renderListOutput = (output: string, options?: { unstyled?: boolean
|
||||
'w-full min-w-0 font-mono space-y-0.5',
|
||||
options?.unstyled ? null : 'p-3 bg-muted/20 rounded-xl border border-border/30'
|
||||
)}
|
||||
style={typography.micro}
|
||||
style={typography.tool.popup}
|
||||
>
|
||||
{items.map((item, idx) => (
|
||||
<div key={idx} className="min-w-0" style={{ paddingLeft: `${item.depth * 20}px` }}>
|
||||
@@ -115,13 +115,14 @@ export const renderGrepOutput = (output: string, isMobile: boolean, options?: {
|
||||
'space-y-2 w-full min-w-0',
|
||||
options?.unstyled ? null : 'p-3 bg-muted/20 rounded-xl border border-border/30'
|
||||
)}
|
||||
style={typography.tool.popup}
|
||||
>
|
||||
<div className="typography-meta text-muted-foreground mb-2">
|
||||
Found {lines.length} match{lines.length !== 1 ? 'es' : ''}
|
||||
</div>
|
||||
{Object.entries(fileGroups).map(([filepath, matches]) => (
|
||||
<div key={filepath} className="space-y-1">
|
||||
<div className={cn('font-medium text-muted-foreground', isMobile ? 'typography-micro' : 'typography-meta')}>
|
||||
<div className={cn('font-medium text-muted-foreground', isMobile ? 'typography-micro' : 'typography-code')}>
|
||||
{filepath}
|
||||
</div>
|
||||
<div className="pl-4 space-y-1">
|
||||
@@ -130,7 +131,7 @@ export const renderGrepOutput = (output: string, isMobile: boolean, options?: {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div key={idx} className={cn('flex items-start gap-2 min-w-0', isMobile ? 'typography-micro' : 'typography-meta')}>
|
||||
<div key={idx} className={cn('flex items-start gap-2 min-w-0', isMobile ? 'typography-micro' : 'typography-code')}>
|
||||
<div className="w-1.5 h-1.5 rounded-full flex-shrink-0 mt-1.5" style={{ backgroundColor: 'var(--status-info)', opacity: 0.6 }} />
|
||||
<div className="flex gap-2 min-w-0 flex-1">
|
||||
{match.lineNum && (
|
||||
@@ -181,18 +182,19 @@ export const renderGlobOutput = (output: string, isMobile: boolean, options?: {
|
||||
'space-y-2 w-full min-w-0',
|
||||
options?.unstyled ? null : 'p-3 bg-muted/20 rounded-xl border border-border/30'
|
||||
)}
|
||||
style={typography.tool.popup}
|
||||
>
|
||||
<div className="typography-meta text-muted-foreground mb-2">
|
||||
Found {paths.length} file{paths.length !== 1 ? 's' : ''}
|
||||
</div>
|
||||
{sortedDirs.map((dir) => (
|
||||
<div key={dir} className="space-y-1">
|
||||
<div className={cn('font-medium text-muted-foreground', isMobile ? 'typography-micro' : 'typography-meta')}>
|
||||
<div className={cn('font-medium text-muted-foreground', isMobile ? 'typography-micro' : 'typography-code')}>
|
||||
{dir}/
|
||||
</div>
|
||||
<div className={cn('pl-4 grid gap-1', isMobile ? 'grid-cols-1' : 'grid-cols-2')}>
|
||||
{groups[dir].sort().map((filename) => (
|
||||
<div key={filename} className={cn('flex items-center gap-2 min-w-0', isMobile ? 'typography-micro' : 'typography-meta')}>
|
||||
<div key={filename} className={cn('flex items-center gap-2 min-w-0', isMobile ? 'typography-micro' : 'typography-code')}>
|
||||
<div className="w-1.5 h-1.5 rounded-full flex-shrink-0" style={{ backgroundColor: 'var(--status-info)', opacity: 0.6 }} />
|
||||
<span className="text-foreground font-mono truncate">{filename}</span>
|
||||
</div>
|
||||
@@ -248,6 +250,7 @@ export const renderTodoOutput = (output: string, options?: { unstyled?: boolean
|
||||
'space-y-3 w-full min-w-0',
|
||||
options?.unstyled ? null : 'p-3 bg-muted/20 rounded-xl border border-border/30'
|
||||
)}
|
||||
style={typography.tool.popup}
|
||||
>
|
||||
<div className="flex gap-4 typography-meta pb-2 border-b border-border/20">
|
||||
<span className="font-medium" style={{ color: 'var(--muted-foreground)' }}>Total: {todos.length}</span>
|
||||
@@ -275,7 +278,7 @@ export const renderTodoOutput = (output: string, options?: { unstyled?: boolean
|
||||
{todosByStatus.in_progress.map((todo, idx) => (
|
||||
<div key={todo.id || idx} className="flex items-start gap-2">
|
||||
{getPriorityDot(todo.priority)}
|
||||
<span className="typography-meta text-foreground flex-1 leading-relaxed">{todo.content}</span>
|
||||
<span className="typography-code text-foreground flex-1 leading-relaxed">{todo.content}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -292,7 +295,7 @@ export const renderTodoOutput = (output: string, options?: { unstyled?: boolean
|
||||
{todosByStatus.pending.map((todo, idx) => (
|
||||
<div key={todo.id || idx} className="flex items-start gap-2">
|
||||
{getPriorityDot(todo.priority)}
|
||||
<span className="typography-meta text-foreground flex-1 leading-relaxed">{todo.content}</span>
|
||||
<span className="typography-code text-foreground flex-1 leading-relaxed">{todo.content}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -309,7 +312,7 @@ export const renderTodoOutput = (output: string, options?: { unstyled?: boolean
|
||||
{todosByStatus.completed.map((todo, idx) => (
|
||||
<div key={todo.id || idx} className="flex items-start gap-2">
|
||||
<RiCheckLine className="w-3 h-3 mt-0.5 flex-shrink-0" style={{ color: 'var(--status-success)', opacity: 0.7 }} />
|
||||
<span className="typography-meta text-foreground flex-1 leading-relaxed">{todo.content}</span>
|
||||
<span className="typography-code text-foreground flex-1 leading-relaxed">{todo.content}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -326,7 +329,7 @@ export const renderTodoOutput = (output: string, options?: { unstyled?: boolean
|
||||
{todosByStatus.cancelled.map((todo, idx) => (
|
||||
<div key={todo.id || idx} className="flex items-start gap-2">
|
||||
<span className="w-3 h-3 text-muted-foreground/50 mt-0.5 flex-shrink-0">×</span>
|
||||
<span className="typography-meta text-muted-foreground/50 line-through flex-1 leading-relaxed">{todo.content}</span>
|
||||
<span className="typography-code text-muted-foreground/50 line-through flex-1 leading-relaxed">{todo.content}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -344,9 +347,10 @@ export const renderWebSearchOutput = (output: string, _syntaxTheme: { [key: stri
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'typography-meta max-w-none w-full min-w-0',
|
||||
'typography-code max-w-none w-full min-w-0',
|
||||
options?.unstyled ? null : 'p-3 bg-muted/20 rounded-xl border border-border/20'
|
||||
)}
|
||||
style={typography.tool.popup}
|
||||
>
|
||||
<Streamdown mode="static" className="streamdown-content streamdown-tool">
|
||||
{output}
|
||||
|
||||
Reference in New Issue
Block a user