Files
vibe-kanban/frontend/src/components/tasks/TaskDetails/Conversation.tsx
Anastasiia Solop 0d3a7a18f8 Refactor TaskDetailsToolbar and LogsPanel, improve performance (#136)
* separate CreatePRDialog from TaskDetailsToolbar

* separate CreateAttempt from TaskDetailsToolbar

* separate CurrentAttempt from TaskDetailsToolbar

* refactor logs panel and diffs

* split big context, add callbacks and memo, check prev state before update for big polled values
2025-07-11 19:27:33 +02:00

144 lines
4.3 KiB
TypeScript

import { NormalizedConversationViewer } from '@/components/tasks/TaskDetails/NormalizedConversationViewer.tsx';
import {
useCallback,
useContext,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import { TaskAttemptDataContext } from '@/components/context/taskDetailsContext.ts';
type Props = {
conversationUpdateTrigger: number;
handleConversationUpdate: () => void;
};
function Conversation({
conversationUpdateTrigger,
handleConversationUpdate,
}: Props) {
const { attemptData } = useContext(TaskAttemptDataContext);
const [shouldAutoScrollLogs, setShouldAutoScrollLogs] = useState(true);
const scrollContainerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (shouldAutoScrollLogs && scrollContainerRef.current) {
scrollContainerRef.current.scrollTop =
scrollContainerRef.current.scrollHeight;
}
}, [
attemptData.activities,
attemptData.processes,
conversationUpdateTrigger,
shouldAutoScrollLogs,
]);
const handleLogsScroll = useCallback(() => {
if (scrollContainerRef.current) {
const { scrollTop, scrollHeight, clientHeight } =
scrollContainerRef.current;
const isAtBottom = scrollTop + clientHeight >= scrollHeight - 5;
if (isAtBottom && !shouldAutoScrollLogs) {
setShouldAutoScrollLogs(true);
} else if (!isAtBottom && shouldAutoScrollLogs) {
setShouldAutoScrollLogs(false);
}
}
}, [shouldAutoScrollLogs]);
const mainCodingAgentProcess = useMemo(() => {
let mainCAProcess = Object.values(attemptData.runningProcessDetails).find(
(process) =>
process.process_type === 'codingagent' && process.command === 'executor'
);
if (!mainCAProcess) {
const mainCodingAgentSummary = attemptData.processes.find(
(process) =>
process.process_type === 'codingagent' &&
process.command === 'executor'
);
if (mainCodingAgentSummary) {
mainCAProcess = Object.values(attemptData.runningProcessDetails).find(
(process) => process.id === mainCodingAgentSummary.id
);
if (!mainCAProcess) {
mainCAProcess = {
...mainCodingAgentSummary,
stdout: null,
stderr: null,
} as any;
}
}
}
return mainCAProcess;
}, [attemptData.processes, attemptData.runningProcessDetails]);
const followUpProcesses = useMemo(() => {
return attemptData.processes
.filter(
(process) =>
process.process_type === 'codingagent' &&
process.command === 'followup_executor'
)
.map((summary) => {
const detailedProcess = Object.values(
attemptData.runningProcessDetails
).find((process) => process.id === summary.id);
return (
detailedProcess ||
({
...summary,
stdout: null,
stderr: null,
} as any)
);
});
}, [attemptData.processes, attemptData.runningProcessDetails]);
return (
<div
ref={scrollContainerRef}
onScroll={handleLogsScroll}
className="h-full overflow-y-auto"
>
{mainCodingAgentProcess || followUpProcesses.length > 0 ? (
<div className="space-y-8">
{mainCodingAgentProcess && (
<div className="space-y-6">
<NormalizedConversationViewer
executionProcess={mainCodingAgentProcess}
onConversationUpdate={handleConversationUpdate}
diffDeletable
/>
</div>
)}
{followUpProcesses.map((followUpProcess) => (
<div key={followUpProcess.id}>
<div className="border-t border-border mb-8"></div>
<NormalizedConversationViewer
executionProcess={followUpProcess}
onConversationUpdate={handleConversationUpdate}
diffDeletable
/>
</div>
))}
</div>
) : (
<div className="text-center py-8 text-muted-foreground">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500 mx-auto mb-4"></div>
<p className="text-lg font-semibold mb-2">Coding Agent Starting</p>
<p>Initializing conversation...</p>
</div>
)}
</div>
);
}
export default Conversation;