Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 15 additions & 3 deletions client/src/components/Chat/Messages/Content/CodeAnalyze.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useState, useEffect } from 'react';
import { useRecoilValue } from 'recoil';
import { Terminal } from 'lucide-react';
import type { ToolCallPhase } from '~/utils/toolCallPhase';
import { useProgress, useLocalize } from '~/hooks';
import ProgressText from './ProgressText';
import MarkdownLite from './MarkdownLite';
Expand Down Expand Up @@ -46,22 +47,33 @@ export default function CodeAnalyze({
return acc;
}, '');

/**
* The legacy assistants-endpoint card: it never receives run-step metadata,
* so it genuinely has only these two states and maps them directly rather
* than through `resolveToolCallPhase`, which needs signals this card has no
* access to. The announcement and the icon below read this same value.
*/
const phase: ToolCallPhase = progress < 1 ? 'running' : 'completed';

return (
<>
<span className="sr-only" aria-live="polite" aria-atomic="true">
{progress < 1 ? localize('com_ui_analyzing') : localize('com_ui_analyzing_finished')}
{phase === 'running' ? localize('com_ui_analyzing') : localize('com_ui_analyzing_finished')}
</span>
<div className="my-1 flex items-center gap-2.5">
<ProgressText
progress={progress}
phase={phase}
onClick={handleToggleCode}
inProgressText={localize('com_ui_analyzing')}
finishedText={localize('com_ui_analyzing_finished')}
hasInput={!!code.length}
isExpanded={showCode}
icon={
<Terminal
className={cn('size-4 shrink-0 text-text-secondary', progress < 1 && 'animate-pulse')}
className={cn(
'size-4 shrink-0 text-text-secondary',
phase === 'running' && 'animate-pulse',
)}
aria-hidden="true"
/>
}
Expand Down
44 changes: 25 additions & 19 deletions client/src/components/Chat/Messages/Content/Parts/BashCall.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,20 +49,15 @@ export default function BashCall({
const isWritingCommand = !command || !areToolCallArgsComplete(args);
const sandboxStarting = useRecoilValue(sandboxStartingByToolCallId(toolCallId ?? ''));

const { showCode, toggleCode, expandStyle, expandRef, progress, cancelled, hasError, hasOutput } =
useToolCallState(initialProgress, isSubmitting, output, !!command, onExpand, runStepStatus);

const highlighted = useLazyHighlight(command || undefined, 'bash');
const { ref: commandPaneRef, onScroll: onCommandPaneScroll } = useFollowScroll<HTMLDivElement>(
highlighted ?? command,
progress < 1 && !cancelled,
showCode,
);
const outputHasError = useMemo(() => ERROR_PATTERNS.test(output), [output]);
/** A backgrounded call's persisted output stays the dispatch handle until
* the detached run settles and patches it; render a background state
* instead of the handle JSON. Completion arrives live as the status marker
* attachment (also covers stdout-only runs) or as harvested files. */
* attachment (also covers stdout-only runs) or as harvested files.
*
* Resolved before the phase, which folds `backgroundFailed` in: the
* detached task's outcome is this card's outcome, and the dispatch step's
* own output cannot express it. */
const backgroundHandle = useMemo(() => parseBackgroundHandle(output), [output]);
const { fileAttachments, backgroundStatus } = useMemo(
() => splitBackgroundAttachments(attachments, toolCallId),
Expand All @@ -77,6 +72,23 @@ export default function BashCall({
)
: null;

const { showCode, toggleCode, expandStyle, expandRef, phase, hasOutput } = useToolCallState({
initialProgress,
isSubmitting,
output,
hasInput: !!command,
onExpand,
runStepStatus,
extraError: backgroundFailed,
});

const highlighted = useLazyHighlight(command || undefined, 'bash');
const { ref: commandPaneRef, onScroll: onCommandPaneScroll } = useFollowScroll<HTMLDivElement>(
highlighted ?? command,
phase === 'running',
showCode,
);

const [isCopied, setIsCopied] = useState(false);
const timerRef = useRef<ReturnType<typeof setTimeout>>();
useEffect(() => () => clearTimeout(timerRef.current), []);
Expand Down Expand Up @@ -110,11 +122,11 @@ export default function BashCall({
<>
<div className="relative my-1.5 flex h-5 shrink-0 items-center gap-2.5">
<ProgressText
progress={progress}
phase={phase}
onClick={toggleCode}
inProgressText={inProgressText}
finishedText={
cancelled
phase === 'cancelled'
? localize('com_ui_cancelled')
: (backgroundFinishedText ?? intent ?? localize('com_ui_command_finished'))
}
Expand All @@ -127,23 +139,17 @@ export default function BashCall({
durationMs={
backgroundHandle == null && backgrounded !== true ? runStepDurationMs : undefined
}
errorSuffix={
(hasError && !cancelled) || backgroundFailed
? localize('com_ui_tool_failed')
: undefined
}
icon={
<LangIcon
lang="bash"
className={cn(
'size-4 shrink-0 text-text-secondary',
progress < 1 && !cancelled && !hasError && 'animate-pulse',
phase === 'running' && 'animate-pulse',
)}
/>
}
hasInput={!!command || hasOutput}
isExpanded={showCode}
error={cancelled}
/>
</div>
<div style={expandStyle}>
Expand Down
44 changes: 25 additions & 19 deletions client/src/components/Chat/Messages/Content/Parts/ExecuteCode.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -87,20 +87,15 @@ export default function ExecuteCode({
const intent = useToolCallIntent(args);
const sandboxStarting = useRecoilValue(sandboxStartingByToolCallId(toolCallId ?? ''));

const { showCode, toggleCode, expandStyle, expandRef, progress, cancelled, hasError, hasOutput } =
useToolCallState(initialProgress, isSubmitting, output, !!code, onExpand, runStepStatus);

const highlighted = useLazyHighlight(code, lang);
const { ref: codePaneRef, onScroll: onCodePaneScroll } = useFollowScroll<HTMLPreElement>(
highlighted ?? code ?? '',
progress < 1 && !cancelled,
showCode,
);
const outputHasError = useMemo(() => ERROR_PATTERNS.test(output), [output]);
/** A backgrounded call's persisted output stays the dispatch handle until
* the detached run settles and patches it; render a background state
* instead of the handle JSON. Completion arrives live as the status marker
* attachment (also covers stdout-only runs) or as harvested files. */
* attachment (also covers stdout-only runs) or as harvested files.
*
* Resolved before the phase, which folds `backgroundFailed` in: the
* detached task's outcome is this card's outcome, and the dispatch step's
* own output cannot express it. */
const backgroundHandle = useMemo(() => parseBackgroundHandle(output), [output]);
const { fileAttachments, backgroundStatus } = useMemo(
() => splitBackgroundAttachments(attachments, toolCallId),
Expand All @@ -115,18 +110,35 @@ export default function ExecuteCode({
)
: null;

const { showCode, toggleCode, expandStyle, expandRef, phase, hasOutput } = useToolCallState({
initialProgress,
isSubmitting,
output,
hasInput: !!code,
onExpand,
runStepStatus,
extraError: backgroundFailed,
});

const highlighted = useLazyHighlight(code, lang);
const { ref: codePaneRef, onScroll: onCodePaneScroll } = useFollowScroll<HTMLPreElement>(
highlighted ?? code ?? '',
phase === 'running',
showCode,
);

return (
<>
<div className="relative my-1.5 flex h-5 shrink-0 items-center gap-2.5">
<ProgressText
progress={progress}
phase={phase}
onClick={toggleCode}
inProgressText={
intent ??
(sandboxStarting ? localize('com_ui_sandbox_starting') : localize('com_ui_analyzing'))
}
finishedText={
cancelled
phase === 'cancelled'
? localize('com_ui_cancelled')
: (backgroundFinishedText ?? intent ?? localize('com_ui_analyzing_finished'))
}
Expand All @@ -139,23 +151,17 @@ export default function ExecuteCode({
durationMs={
backgroundHandle == null && backgrounded !== true ? runStepDurationMs : undefined
}
errorSuffix={
(hasError && !cancelled) || backgroundFailed
? localize('com_ui_tool_failed')
: undefined
}
icon={
<SquareTerminal
className={cn(
'size-4 shrink-0 text-text-secondary',
progress < 1 && !cancelled && !hasError && 'animate-pulse',
phase === 'running' && 'animate-pulse',
)}
aria-hidden="true"
/>
}
hasInput={!!code?.length}
isExpanded={showCode}
error={cancelled}
/>
</div>
<div style={expandStyle}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -152,20 +152,19 @@ export default function FileAuthoringCall({
previewLang = fileLang;
}

const { showCode, toggleCode, expandStyle, expandRef, progress, cancelled, hasError } =
useToolCallState(
initialProgress,
isSubmitting,
output,
!!filePath || !!preview,
onExpand,
runStepStatus,
);
const { showCode, toggleCode, expandStyle, expandRef, phase } = useToolCallState({
initialProgress,
isSubmitting,
output,
hasInput: !!filePath || !!preview,
onExpand,
runStepStatus,
});

const highlighted = useLazyHighlight(preview || undefined, previewLang);
const { ref: previewPaneRef, onScroll: onPreviewPaneScroll } = useFollowScroll<HTMLPreElement>(
highlighted ?? preview,
progress < 1 && !cancelled,
phase === 'running',
showCode,
);
const Icon = isCreate && !overwrote ? FilePlus2 : FilePenLine;
Expand All @@ -179,7 +178,7 @@ export default function FileAuthoringCall({
<>
<div className="relative my-1.5 flex h-5 shrink-0 items-center gap-2.5">
<ProgressText
progress={progress}
phase={phase}
onClick={toggleCode}
inProgressText={
intent ??
Expand All @@ -188,24 +187,22 @@ export default function FileAuthoringCall({
})
}
finishedText={
cancelled
phase === 'cancelled'
? localize('com_ui_cancelled')
: (intent ?? localize(finishedKey, { 0: fileName }))
}
durationMs={runStepDurationMs}
errorSuffix={hasError && !cancelled ? localize('com_ui_tool_failed') : undefined}
icon={
<Icon
className={cn(
'size-4 shrink-0 text-text-secondary',
progress < 1 && !cancelled && !hasError && 'animate-pulse',
phase === 'running' && 'animate-pulse',
)}
aria-hidden="true"
/>
}
hasInput={!!filePath || !!preview}
isExpanded={showCode}
error={cancelled}
/>
</div>
<div style={expandStyle}>
Expand All @@ -226,7 +223,7 @@ export default function FileAuthoringCall({
<pre
className={cn(
'max-h-[300px] overflow-auto whitespace-pre-wrap break-words border-t border-border-light px-3 py-2.5 font-mono text-xs',
hasError ? 'text-status-error' : 'text-text-primary',
phase === 'failed' ? 'text-status-error' : 'text-text-primary',
)}
>
{output}
Expand Down
18 changes: 11 additions & 7 deletions client/src/components/Chat/Messages/Content/Parts/ReadFileCall.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -89,37 +89,41 @@ export default function ReadFileCall({
const fileName = filePath.split('/').pop() || filePath;
const lang = useMemo(() => langFromPath(filePath), [filePath]);

const { showCode, toggleCode, expandStyle, expandRef, progress, cancelled, hasError, hasOutput } =
useToolCallState(initialProgress, isSubmitting, output, !!filePath, onExpand, runStepStatus);
const { showCode, toggleCode, expandStyle, expandRef, phase, hasOutput } = useToolCallState({
initialProgress,
isSubmitting,
output,
hasInput: !!filePath,
onExpand,
runStepStatus,
});

const highlighted = useLazyHighlight(hasOutput ? output : undefined, lang);

return (
<>
<div className="relative my-1.5 flex h-5 shrink-0 items-center gap-2.5">
<ProgressText
progress={progress}
phase={phase}
onClick={toggleCode}
inProgressText={intent ?? localize('com_ui_reading_file', { 0: fileName })}
finishedText={
cancelled
phase === 'cancelled'
? localize('com_ui_cancelled')
: (intent ?? localize('com_ui_read_file', { 0: fileName }))
}
durationMs={runStepDurationMs}
errorSuffix={hasError && !cancelled ? localize('com_ui_tool_failed') : undefined}
icon={
<FileText
className={cn(
'size-4 shrink-0 text-text-secondary',
progress < 1 && !cancelled && !hasError && 'animate-pulse',
phase === 'running' && 'animate-pulse',
)}
aria-hidden="true"
/>
}
hasInput={!!filePath || hasOutput}
isExpanded={showCode}
error={cancelled}
/>
</div>
<div style={expandStyle}>
Expand Down
18 changes: 11 additions & 7 deletions client/src/components/Chat/Messages/Content/Parts/SkillCall.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,35 +35,39 @@ export default function SkillCall({
const skillName = useMemo(() => parseJsonField(args, 'skillName'), [args]);
const intent = useToolCallIntent(args);

const { showCode, toggleCode, expandStyle, expandRef, progress, cancelled, hasError, hasOutput } =
useToolCallState(initialProgress, isSubmitting, output, !!skillName, onExpand, runStepStatus);
const { showCode, toggleCode, expandStyle, expandRef, phase, hasOutput } = useToolCallState({
initialProgress,
isSubmitting,
output,
hasInput: !!skillName,
onExpand,
runStepStatus,
});

return (
<>
<div className="relative my-1.5 flex h-5 shrink-0 items-center gap-2.5">
<ProgressText
progress={progress}
phase={phase}
onClick={toggleCode}
inProgressText={intent ?? localize('com_ui_skill_running', { 0: skillName })}
finishedText={
cancelled
phase === 'cancelled'
? localize('com_ui_cancelled')
: (intent ?? localize('com_ui_skill_finished', { 0: skillName }))
}
durationMs={runStepDurationMs}
errorSuffix={hasError && !cancelled ? localize('com_ui_tool_failed') : undefined}
icon={
<ScrollText
className={cn(
'size-4 shrink-0 text-text-secondary',
progress < 1 && !cancelled && !hasError && 'animate-pulse',
phase === 'running' && 'animate-pulse',
)}
aria-hidden="true"
/>
}
hasInput={!!skillName || hasOutput}
isExpanded={showCode}
error={cancelled}
/>
</div>
<div style={expandStyle}>
Expand Down
Loading
Loading