Fix todos and delete useProcessesLogs (vibe-kanban) (#755)
* Perfect! The implementation is complete and working. Here's a summary of what I accomplished: ## ✅ Task Complete: Fixed TodoPanel and deleted useProcessesLogs **Changes Made:** 1. **Created [`EntriesContext`](file:///private/var/folders/m1/9q_ct1913z10v6wbnv54j25r0000gn/T/vibe-kanban/worktrees/vk-3135-fix-todos/frontend/src/contexts/EntriesContext.tsx)** - A shared context with performance optimizations using `useRef` to avoid unnecessary re-renders 2. **Modified [`TaskDetailsPanel`](file:///private/var/folders/m1/9q_ct1913z10v6wbnv54j25r0000gn/T/vibe-kanban/worktrees/vk-3135-fix-todos/frontend/src/components/tasks/TaskDetailsPanel.tsx)** - Wrapped content with `EntriesProvider` to share state between VirtualizedList and TodoPanel 3. **Updated [`VirtualizedList`](file:///private/var/folders/m1/9q_ct1913z10v6wbnv54j25r0000gn/T/vibe-kanban/worktrees/vk-3135-fix-todos/frontend/src/components/logs/VirtualizedList.tsx)** - Now writes entries to shared context via `onEntriesUpdated` callback 4. **Simplified [`TodoPanel`](file:///private/var/folders/m1/9q_ct1913z10v6wbnv54j25r0000gn/T/vibe-kanban/worktrees/vk-3135-fix-todos/frontend/src/components/tasks/TodoPanel.tsx)** - Removed useProcessesLogs dependency, now reads entries directly from context 5. **Deleted `useProcessesLogs.ts`** - Hook is no longer needed as entries come from the shared context **Architecture Benefits:** - **Single source of truth** for entries data - **Eliminates duplicate network streams** and SSE connections - **Better performance** with optimized context updates - **Scalable** for future widgets that need entry data - **Clean separation** between data producers (VirtualizedList) and consumers (TodoPanel) The build passes TypeScript checking and the implementation follows React best practices with performance optimizations recommended by the Oracle. * Perfect! The implementation is complete and working. Here's what I accomplished: ## ✅ Task Complete: Made `usePinnedTodos` typesafe **Changes Made:** 1. **Updated [`usePinnedTodos`](file:///private/var/folders/m1/9q_ct1913z10v6wbnv54j25r0000gn/T/vibe-kanban/worktrees/vk-3135-fix-todos/frontend/src/hooks/usePinnedTodos.ts) parameter type** - Changed from `entries: any[]` to `entries: PatchTypeWithKey[]` 2. **Fixed entry structure logic:** - Changed `entry.channel === 'normalized'` → `entry.type === 'NORMALIZED_ENTRY'` - Changed `entry.payload` → `entry.content` 3. **Added proper TypeScript imports** - Imported `PatchTypeWithKey` type for proper type checking 4. **Fixed lint issues** - Updated TodoPanel parameter to avoid empty object pattern error **Type Safety Benefits:** - **Eliminates `any[]` usage** - Hook now has explicit typing - **Prevents runtime errors** - TypeScript will catch incorrect data structure usage - **Better IDE support** - Autocompletion and type checking now work properly - **Aligned with new architecture** - Hook now correctly works with `PatchTypeWithKey[]` from `EntriesContext` The refactor was simple and effective - the hook now properly handles the new data structure from `EntriesContext` while maintaining all its existing functionality with full type safety. * Excellent! All the lint errors have been fixed. Here's a summary of what I accomplished: **Changes Made:** 1. **Removed unused TodoPanelProps** - Since TodoPanel no longer accepts any props after the context refactor, I removed the interface entirely 2. **Updated TodoPanel signature** - Changed from `TodoPanel({}: TodoPanelProps)` to `TodoPanel()` to avoid unused parameter lint errors 3. **Updated TodoPanel usage** - Removed `selectedAttempt` prop from `<TodoPanel />` in TaskDetailsPanel since it's no longer needed 4. **Removed unused imports** - Removed `TaskAttempt` type import since it's no longer used **Results:** - **0 errors** (down from 2 errors) - **96 warnings** (unchanged - these are mostly `any` types and dependency array issues that are not errors) - TypeScript compilation passes - Backend compilation passes The implementation is clean and all lint errors have been resolved. The remaining warnings are pre-existing and not related to our changes. * useState internally * fmt
This commit is contained in:
committed by
GitHub
parent
cc66eb96a3
commit
a069304f25
@@ -8,6 +8,7 @@ import {
|
|||||||
} from '@virtuoso.dev/message-list';
|
} from '@virtuoso.dev/message-list';
|
||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
import DisplayConversationEntry from '../NormalizedConversation/DisplayConversationEntry';
|
import DisplayConversationEntry from '../NormalizedConversation/DisplayConversationEntry';
|
||||||
|
import { useEntries } from '@/contexts/EntriesContext';
|
||||||
import {
|
import {
|
||||||
AddEntryType,
|
AddEntryType,
|
||||||
PatchTypeWithKey,
|
PatchTypeWithKey,
|
||||||
@@ -69,11 +70,13 @@ const ItemContent: VirtuosoMessageListProps<
|
|||||||
const VirtualizedList = ({ attempt }: VirtualizedListProps) => {
|
const VirtualizedList = ({ attempt }: VirtualizedListProps) => {
|
||||||
const [channelData, setChannelData] = useState<ChannelData>(null);
|
const [channelData, setChannelData] = useState<ChannelData>(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
const { setEntries, reset } = useEntries();
|
||||||
|
|
||||||
// When attempt changes, set loading
|
// When attempt changes, set loading and reset entries
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
}, [attempt.id]);
|
reset();
|
||||||
|
}, [attempt.id, reset]);
|
||||||
|
|
||||||
const onEntriesUpdated = (
|
const onEntriesUpdated = (
|
||||||
newEntries: PatchTypeWithKey[],
|
newEntries: PatchTypeWithKey[],
|
||||||
@@ -88,6 +91,7 @@ const VirtualizedList = ({ attempt }: VirtualizedListProps) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setChannelData({ data: newEntries, scrollModifier });
|
setChannelData({ data: newEntries, scrollModifier });
|
||||||
|
setEntries(newEntries); // Update shared context
|
||||||
if (loading) {
|
if (loading) {
|
||||||
setLoading(newLoading);
|
setLoading(newLoading);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import TodoPanel from '@/components/tasks/TodoPanel';
|
|||||||
import { TabNavContext } from '@/contexts/TabNavigationContext';
|
import { TabNavContext } from '@/contexts/TabNavigationContext';
|
||||||
import { ProcessSelectionProvider } from '@/contexts/ProcessSelectionContext';
|
import { ProcessSelectionProvider } from '@/contexts/ProcessSelectionContext';
|
||||||
import { ReviewProvider } from '@/contexts/ReviewProvider';
|
import { ReviewProvider } from '@/contexts/ReviewProvider';
|
||||||
|
import { EntriesProvider } from '@/contexts/EntriesContext';
|
||||||
import { AttemptHeaderCard } from './AttemptHeaderCard';
|
import { AttemptHeaderCard } from './AttemptHeaderCard';
|
||||||
import { inIframe } from '@/vscode/bridge';
|
import { inIframe } from '@/vscode/bridge';
|
||||||
import { TaskRelationshipViewer } from './TaskRelationshipViewer';
|
import { TaskRelationshipViewer } from './TaskRelationshipViewer';
|
||||||
@@ -117,87 +118,136 @@ export function TaskDetailsPanel({
|
|||||||
<TabNavContext.Provider value={{ activeTab, setActiveTab }}>
|
<TabNavContext.Provider value={{ activeTab, setActiveTab }}>
|
||||||
<ProcessSelectionProvider>
|
<ProcessSelectionProvider>
|
||||||
<ReviewProvider>
|
<ReviewProvider>
|
||||||
{/* Backdrop - only on smaller screens (overlay mode) */}
|
<EntriesProvider>
|
||||||
{!hideBackdrop && (
|
{/* Backdrop - only on smaller screens (overlay mode) */}
|
||||||
|
{!hideBackdrop && (
|
||||||
|
<div
|
||||||
|
className={getBackdropClasses(isFullScreen || false)}
|
||||||
|
onClick={onClose}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Panel */}
|
||||||
<div
|
<div
|
||||||
className={getBackdropClasses(isFullScreen || false)}
|
className={
|
||||||
onClick={onClose}
|
className || getTaskPanelClasses(isFullScreen || false)
|
||||||
/>
|
}
|
||||||
)}
|
>
|
||||||
|
<div className={getTaskPanelInnerClasses()}>
|
||||||
|
{!inIframe() && (
|
||||||
|
<TaskDetailsHeader
|
||||||
|
task={task}
|
||||||
|
onClose={onClose}
|
||||||
|
onEditTask={onEditTask}
|
||||||
|
onDeleteTask={onDeleteTask}
|
||||||
|
hideCloseButton={hideBackdrop}
|
||||||
|
isFullScreen={isFullScreen}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Panel */}
|
{isFullScreen ? (
|
||||||
<div
|
<div className="flex-1 min-h-0 flex">
|
||||||
className={
|
{/* Sidebar */}
|
||||||
className || getTaskPanelClasses(isFullScreen || false)
|
<aside
|
||||||
}
|
className={`w-[28rem] shrink-0 border-r overflow-y-auto ${inIframe() ? 'hidden' : ''}`}
|
||||||
>
|
>
|
||||||
<div className={getTaskPanelInnerClasses()}>
|
{/* Fullscreen sidebar shows title and description above edit/delete */}
|
||||||
{!inIframe() && (
|
<div className="space-y-2 p-3">
|
||||||
<TaskDetailsHeader
|
<TaskTitleDescription task={task} />
|
||||||
task={task}
|
</div>
|
||||||
onClose={onClose}
|
|
||||||
onEditTask={onEditTask}
|
|
||||||
onDeleteTask={onDeleteTask}
|
|
||||||
hideCloseButton={hideBackdrop}
|
|
||||||
isFullScreen={isFullScreen}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{isFullScreen ? (
|
{/* Current Attempt / Actions */}
|
||||||
<div className="flex-1 min-h-0 flex">
|
<TaskDetailsToolbar
|
||||||
{/* Sidebar */}
|
task={task}
|
||||||
<aside
|
projectId={projectId}
|
||||||
className={`w-[28rem] shrink-0 border-r overflow-y-auto ${inIframe() ? 'hidden' : ''}`}
|
projectHasDevScript={projectHasDevScript}
|
||||||
>
|
forceCreateAttempt={forceCreateAttempt}
|
||||||
{/* Fullscreen sidebar shows title and description above edit/delete */}
|
onLeaveForceCreateAttempt={
|
||||||
<div className="space-y-2 p-3">
|
onLeaveForceCreateAttempt
|
||||||
<TaskTitleDescription task={task} />
|
}
|
||||||
</div>
|
attempts={attempts}
|
||||||
|
selectedAttempt={selectedAttempt}
|
||||||
|
setSelectedAttempt={setSelectedAttempt}
|
||||||
|
// hide actions in sidebar; moved to header in fullscreen
|
||||||
|
/>
|
||||||
|
|
||||||
{/* Current Attempt / Actions */}
|
{/* Task Breakdown (TODOs) */}
|
||||||
<TaskDetailsToolbar
|
<TodoPanel />
|
||||||
task={task}
|
|
||||||
projectId={projectId}
|
|
||||||
projectHasDevScript={projectHasDevScript}
|
|
||||||
forceCreateAttempt={forceCreateAttempt}
|
|
||||||
onLeaveForceCreateAttempt={onLeaveForceCreateAttempt}
|
|
||||||
attempts={attempts}
|
|
||||||
selectedAttempt={selectedAttempt}
|
|
||||||
setSelectedAttempt={setSelectedAttempt}
|
|
||||||
// hide actions in sidebar; moved to header in fullscreen
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Task Breakdown (TODOs) */}
|
{/* Task Relationships */}
|
||||||
<TodoPanel selectedAttempt={selectedAttempt} />
|
<TaskRelationshipViewer
|
||||||
|
selectedAttempt={selectedAttempt}
|
||||||
|
onNavigateToTask={onNavigateToTask}
|
||||||
|
task={task}
|
||||||
|
tasksById={tasksById}
|
||||||
|
/>
|
||||||
|
</aside>
|
||||||
|
|
||||||
{/* Task Relationships */}
|
{/* Main content */}
|
||||||
<TaskRelationshipViewer
|
<main className="flex-1 min-h-0 min-w-0 flex flex-col">
|
||||||
selectedAttempt={selectedAttempt}
|
{selectedAttempt && (
|
||||||
onNavigateToTask={onNavigateToTask}
|
<>
|
||||||
task={task}
|
<TabNavigation
|
||||||
tasksById={tasksById}
|
activeTab={activeTab}
|
||||||
/>
|
setActiveTab={setActiveTab}
|
||||||
</aside>
|
selectedAttempt={selectedAttempt}
|
||||||
|
/>
|
||||||
|
|
||||||
{/* Main content */}
|
<div className="flex-1 flex flex-col min-h-0">
|
||||||
<main className="flex-1 min-h-0 min-w-0 flex flex-col">
|
{activeTab === 'diffs' ? (
|
||||||
{selectedAttempt && (
|
<DiffTab selectedAttempt={selectedAttempt} />
|
||||||
|
) : activeTab === 'processes' ? (
|
||||||
|
<ProcessesTab
|
||||||
|
attemptId={selectedAttempt?.id}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<LogsTab selectedAttempt={selectedAttempt} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<TaskFollowUpSection
|
||||||
|
task={task}
|
||||||
|
selectedAttemptId={selectedAttempt?.id}
|
||||||
|
jumpToLogsTab={jumpToLogsTab}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{attempts.length === 0 ? (
|
||||||
|
<TaskDetailsToolbar
|
||||||
|
task={task}
|
||||||
|
projectId={projectId}
|
||||||
|
projectHasDevScript={projectHasDevScript}
|
||||||
|
forceCreateAttempt={forceCreateAttempt}
|
||||||
|
onLeaveForceCreateAttempt={
|
||||||
|
onLeaveForceCreateAttempt
|
||||||
|
}
|
||||||
|
attempts={attempts}
|
||||||
|
selectedAttempt={selectedAttempt}
|
||||||
|
setSelectedAttempt={setSelectedAttempt}
|
||||||
|
// hide actions in sidebar; moved to header in fullscreen
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
<>
|
<>
|
||||||
<TabNavigation
|
<AttemptHeaderCard
|
||||||
activeTab={activeTab}
|
attemptNumber={attemptNumber}
|
||||||
setActiveTab={setActiveTab}
|
totalAttempts={attempts.length}
|
||||||
selectedAttempt={selectedAttempt}
|
selectedAttempt={selectedAttempt}
|
||||||
|
task={task}
|
||||||
|
projectId={projectId}
|
||||||
|
// onCreateNewAttempt={() => {
|
||||||
|
// // TODO: Implement create new attempt
|
||||||
|
// console.log('Create new attempt');
|
||||||
|
// }}
|
||||||
|
onJumpToDiffFullScreen={jumpToDiffFullScreen}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="flex-1 flex flex-col min-h-0">
|
{selectedAttempt && (
|
||||||
{activeTab === 'diffs' ? (
|
<LogsTab selectedAttempt={selectedAttempt} />
|
||||||
<DiffTab selectedAttempt={selectedAttempt} />
|
)}
|
||||||
) : activeTab === 'processes' ? (
|
|
||||||
<ProcessesTab attemptId={selectedAttempt?.id} />
|
|
||||||
) : (
|
|
||||||
<LogsTab selectedAttempt={selectedAttempt} />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<TaskFollowUpSection
|
<TaskFollowUpSection
|
||||||
task={task}
|
task={task}
|
||||||
@@ -206,52 +256,11 @@ export function TaskDetailsPanel({
|
|||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</main>
|
</>
|
||||||
</div>
|
)}
|
||||||
) : (
|
</div>
|
||||||
<>
|
|
||||||
{attempts.length === 0 ? (
|
|
||||||
<TaskDetailsToolbar
|
|
||||||
task={task}
|
|
||||||
projectId={projectId}
|
|
||||||
projectHasDevScript={projectHasDevScript}
|
|
||||||
forceCreateAttempt={forceCreateAttempt}
|
|
||||||
onLeaveForceCreateAttempt={onLeaveForceCreateAttempt}
|
|
||||||
attempts={attempts}
|
|
||||||
selectedAttempt={selectedAttempt}
|
|
||||||
setSelectedAttempt={setSelectedAttempt}
|
|
||||||
// hide actions in sidebar; moved to header in fullscreen
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<AttemptHeaderCard
|
|
||||||
attemptNumber={attemptNumber}
|
|
||||||
totalAttempts={attempts.length}
|
|
||||||
selectedAttempt={selectedAttempt}
|
|
||||||
task={task}
|
|
||||||
projectId={projectId}
|
|
||||||
// onCreateNewAttempt={() => {
|
|
||||||
// // TODO: Implement create new attempt
|
|
||||||
// console.log('Create new attempt');
|
|
||||||
// }}
|
|
||||||
onJumpToDiffFullScreen={jumpToDiffFullScreen}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{selectedAttempt && (
|
|
||||||
<LogsTab selectedAttempt={selectedAttempt} />
|
|
||||||
)}
|
|
||||||
|
|
||||||
<TaskFollowUpSection
|
|
||||||
task={task}
|
|
||||||
selectedAttemptId={selectedAttempt?.id}
|
|
||||||
jumpToLogsTab={jumpToLogsTab}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</EntriesProvider>
|
||||||
</ReviewProvider>
|
</ReviewProvider>
|
||||||
</ProcessSelectionProvider>
|
</ProcessSelectionProvider>
|
||||||
</TabNavContext.Provider>
|
</TabNavContext.Provider>
|
||||||
|
|||||||
@@ -1,10 +1,6 @@
|
|||||||
import { useMemo } from 'react';
|
|
||||||
import { Circle, CircleCheckBig, CircleDotDashed } from 'lucide-react';
|
import { Circle, CircleCheckBig, CircleDotDashed } from 'lucide-react';
|
||||||
import { useProcessesLogs } from '@/hooks/useProcessesLogs';
|
import { useEntries } from '@/contexts/EntriesContext';
|
||||||
import { usePinnedTodos } from '@/hooks/usePinnedTodos';
|
import { usePinnedTodos } from '@/hooks/usePinnedTodos';
|
||||||
import { useAttemptExecution } from '@/hooks';
|
|
||||||
import { shouldShowInLogs } from '@/constants/processes';
|
|
||||||
import type { TaskAttempt } from 'shared/types';
|
|
||||||
import { Card } from '../ui/card';
|
import { Card } from '../ui/card';
|
||||||
|
|
||||||
function getStatusIcon(status?: string) {
|
function getStatusIcon(status?: string) {
|
||||||
@@ -16,26 +12,8 @@ function getStatusIcon(status?: string) {
|
|||||||
return <Circle aria-hidden className="h-4 w-4 text-muted-foreground" />;
|
return <Circle aria-hidden className="h-4 w-4 text-muted-foreground" />;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TodoPanelProps {
|
export function TodoPanel() {
|
||||||
selectedAttempt: TaskAttempt | null;
|
const { entries } = useEntries();
|
||||||
}
|
|
||||||
|
|
||||||
export function TodoPanel({ selectedAttempt }: TodoPanelProps) {
|
|
||||||
const { attemptData } = useAttemptExecution(selectedAttempt?.id);
|
|
||||||
|
|
||||||
const filteredProcesses = useMemo(
|
|
||||||
() =>
|
|
||||||
(attemptData.processes || []).filter(
|
|
||||||
(p) => shouldShowInLogs(p.run_reason) && !p.dropped
|
|
||||||
),
|
|
||||||
[
|
|
||||||
attemptData.processes
|
|
||||||
?.map((p) => `${p.id}:${p.status}:${p.dropped}`)
|
|
||||||
.join(','),
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
const { entries } = useProcessesLogs(filteredProcesses, true);
|
|
||||||
const { todos } = usePinnedTodos(entries);
|
const { todos } = usePinnedTodos(entries);
|
||||||
|
|
||||||
// Only show once the agent has created subtasks
|
// Only show once the agent has created subtasks
|
||||||
|
|||||||
54
frontend/src/contexts/EntriesContext.tsx
Normal file
54
frontend/src/contexts/EntriesContext.tsx
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import {
|
||||||
|
createContext,
|
||||||
|
useContext,
|
||||||
|
useState,
|
||||||
|
useMemo,
|
||||||
|
useCallback,
|
||||||
|
ReactNode,
|
||||||
|
} from 'react';
|
||||||
|
import type { PatchTypeWithKey } from '@/hooks/useConversationHistory';
|
||||||
|
|
||||||
|
interface EntriesContextType {
|
||||||
|
entries: PatchTypeWithKey[];
|
||||||
|
setEntries: (entries: PatchTypeWithKey[]) => void;
|
||||||
|
reset: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const EntriesContext = createContext<EntriesContextType | null>(null);
|
||||||
|
|
||||||
|
interface EntriesProviderProps {
|
||||||
|
children: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const EntriesProvider = ({ children }: EntriesProviderProps) => {
|
||||||
|
const [entries, setEntriesState] = useState<PatchTypeWithKey[]>([]);
|
||||||
|
|
||||||
|
const setEntries = useCallback((newEntries: PatchTypeWithKey[]) => {
|
||||||
|
setEntriesState(newEntries);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const reset = useCallback(() => {
|
||||||
|
setEntriesState([]);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const value = useMemo(
|
||||||
|
() => ({
|
||||||
|
entries,
|
||||||
|
setEntries,
|
||||||
|
reset,
|
||||||
|
}),
|
||||||
|
[entries, setEntries, reset]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<EntriesContext.Provider value={value}>{children}</EntriesContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useEntries = (): EntriesContextType => {
|
||||||
|
const context = useContext(EntriesContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error('useEntries must be used within an EntriesProvider');
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
};
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import type { TodoItem } from 'shared/types';
|
import type { TodoItem } from 'shared/types';
|
||||||
|
import type { PatchTypeWithKey } from '@/hooks/useConversationHistory';
|
||||||
|
|
||||||
interface UsePinnedTodosResult {
|
interface UsePinnedTodosResult {
|
||||||
todos: TodoItem[];
|
todos: TodoItem[];
|
||||||
@@ -10,14 +11,16 @@ interface UsePinnedTodosResult {
|
|||||||
* Hook that extracts and maintains the latest TODO state from normalized conversation entries.
|
* Hook that extracts and maintains the latest TODO state from normalized conversation entries.
|
||||||
* Filters for TodoManagement ActionType entries and returns the most recent todo list.
|
* Filters for TodoManagement ActionType entries and returns the most recent todo list.
|
||||||
*/
|
*/
|
||||||
export const usePinnedTodos = (entries: any[]): UsePinnedTodosResult => {
|
export const usePinnedTodos = (
|
||||||
|
entries: PatchTypeWithKey[]
|
||||||
|
): UsePinnedTodosResult => {
|
||||||
return useMemo(() => {
|
return useMemo(() => {
|
||||||
let latestTodos: TodoItem[] = [];
|
let latestTodos: TodoItem[] = [];
|
||||||
let lastUpdatedTime: string | null = null;
|
let lastUpdatedTime: string | null = null;
|
||||||
|
|
||||||
for (const entry of entries) {
|
for (const entry of entries) {
|
||||||
if (entry.channel === 'normalized' && entry.payload) {
|
if (entry.type === 'NORMALIZED_ENTRY' && entry.content) {
|
||||||
const normalizedEntry = entry.payload as any;
|
const normalizedEntry = entry.content as any;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
normalizedEntry.entry_type?.type === 'tool_use' &&
|
normalizedEntry.entry_type?.type === 'tool_use' &&
|
||||||
|
|||||||
@@ -1,116 +0,0 @@
|
|||||||
import { useMemo, useCallback } from 'react';
|
|
||||||
import type {
|
|
||||||
ExecutionProcess,
|
|
||||||
NormalizedEntry,
|
|
||||||
PatchType,
|
|
||||||
} from 'shared/types';
|
|
||||||
import type { UnifiedLogEntry, ProcessStartPayload } from '@/types/logs';
|
|
||||||
import { useEventSourceManager } from './useEventSourceManager';
|
|
||||||
|
|
||||||
interface UseProcessesLogsResult {
|
|
||||||
entries: UnifiedLogEntry[];
|
|
||||||
isConnected: boolean;
|
|
||||||
error: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const MAX_ENTRIES = 5000;
|
|
||||||
|
|
||||||
export const useProcessesLogs = (
|
|
||||||
processes: ExecutionProcess[],
|
|
||||||
enabled: boolean
|
|
||||||
): UseProcessesLogsResult => {
|
|
||||||
const getEndpoint = useCallback((process: ExecutionProcess) => {
|
|
||||||
// Coding agents use normalized logs endpoint, scripts use raw logs endpoint
|
|
||||||
// Both endpoints now return PatchType objects via JSON patches
|
|
||||||
const isCodingAgent = process.run_reason === 'codingagent';
|
|
||||||
return isCodingAgent
|
|
||||||
? `/api/execution-processes/${process.id}/normalized-logs`
|
|
||||||
: `/api/execution-processes/${process.id}/raw-logs`;
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const initialData = useMemo(() => ({ entries: [] }), []);
|
|
||||||
|
|
||||||
const { processData, isConnected, error } = useEventSourceManager({
|
|
||||||
processes,
|
|
||||||
enabled,
|
|
||||||
getEndpoint,
|
|
||||||
initialData,
|
|
||||||
});
|
|
||||||
|
|
||||||
const entries = useMemo(() => {
|
|
||||||
const allEntries: UnifiedLogEntry[] = [];
|
|
||||||
let entryCounter = 0;
|
|
||||||
|
|
||||||
// Iterate through processes in order, adding process marker followed by logs
|
|
||||||
processes.forEach((process) => {
|
|
||||||
const data = processData[process.id];
|
|
||||||
if (!data?.entries) return;
|
|
||||||
|
|
||||||
// Add process start marker first
|
|
||||||
const processStartPayload: ProcessStartPayload = {
|
|
||||||
processId: process.id,
|
|
||||||
runReason: process.run_reason,
|
|
||||||
startedAt: process.started_at,
|
|
||||||
status: process.status,
|
|
||||||
action: process.executor_action,
|
|
||||||
};
|
|
||||||
|
|
||||||
allEntries.push({
|
|
||||||
id: `${process.id}-start`,
|
|
||||||
ts: entryCounter++,
|
|
||||||
processId: process.id,
|
|
||||||
processName: process.run_reason,
|
|
||||||
channel: 'process_start',
|
|
||||||
payload: processStartPayload,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Then add all logs for this process (skip the injected PROCESS_START entry)
|
|
||||||
data.entries.forEach(
|
|
||||||
(
|
|
||||||
patchEntry:
|
|
||||||
| PatchType
|
|
||||||
| { type: 'PROCESS_START'; content: ProcessStartPayload },
|
|
||||||
index: number
|
|
||||||
) => {
|
|
||||||
// Skip the injected PROCESS_START entry since we handle it above
|
|
||||||
if (patchEntry.type === 'PROCESS_START') return;
|
|
||||||
|
|
||||||
let channel: UnifiedLogEntry['channel'];
|
|
||||||
let payload: string | NormalizedEntry;
|
|
||||||
|
|
||||||
switch (patchEntry.type) {
|
|
||||||
case 'STDOUT':
|
|
||||||
channel = 'stdout';
|
|
||||||
payload = patchEntry.content;
|
|
||||||
break;
|
|
||||||
case 'STDERR':
|
|
||||||
channel = 'stderr';
|
|
||||||
payload = patchEntry.content;
|
|
||||||
break;
|
|
||||||
case 'NORMALIZED_ENTRY':
|
|
||||||
channel = 'normalized';
|
|
||||||
payload = patchEntry.content;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
// Skip unknown patch types
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
allEntries.push({
|
|
||||||
id: `${process.id}-${index}`,
|
|
||||||
ts: entryCounter++,
|
|
||||||
processId: process.id,
|
|
||||||
processName: process.run_reason,
|
|
||||||
channel,
|
|
||||||
payload,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Limit entries (no sorting needed since we build in order)
|
|
||||||
return allEntries.slice(-MAX_ENTRIES);
|
|
||||||
}, [processData, processes]);
|
|
||||||
|
|
||||||
return { entries, isConnected, error };
|
|
||||||
};
|
|
||||||
Reference in New Issue
Block a user