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:
Louis Knight-Webb
2025-09-18 11:22:10 +01:00
committed by GitHub
parent cc66eb96a3
commit a069304f25
6 changed files with 193 additions and 261 deletions

View File

@@ -8,6 +8,7 @@ import {
} from '@virtuoso.dev/message-list';
import { useEffect, useRef, useState } from 'react';
import DisplayConversationEntry from '../NormalizedConversation/DisplayConversationEntry';
import { useEntries } from '@/contexts/EntriesContext';
import {
AddEntryType,
PatchTypeWithKey,
@@ -69,11 +70,13 @@ const ItemContent: VirtuosoMessageListProps<
const VirtualizedList = ({ attempt }: VirtualizedListProps) => {
const [channelData, setChannelData] = useState<ChannelData>(null);
const [loading, setLoading] = useState(true);
const { setEntries, reset } = useEntries();
// When attempt changes, set loading
// When attempt changes, set loading and reset entries
useEffect(() => {
setLoading(true);
}, [attempt.id]);
reset();
}, [attempt.id, reset]);
const onEntriesUpdated = (
newEntries: PatchTypeWithKey[],
@@ -88,6 +91,7 @@ const VirtualizedList = ({ attempt }: VirtualizedListProps) => {
}
setChannelData({ data: newEntries, scrollModifier });
setEntries(newEntries); // Update shared context
if (loading) {
setLoading(newLoading);
}

View File

@@ -19,6 +19,7 @@ import TodoPanel from '@/components/tasks/TodoPanel';
import { TabNavContext } from '@/contexts/TabNavigationContext';
import { ProcessSelectionProvider } from '@/contexts/ProcessSelectionContext';
import { ReviewProvider } from '@/contexts/ReviewProvider';
import { EntriesProvider } from '@/contexts/EntriesContext';
import { AttemptHeaderCard } from './AttemptHeaderCard';
import { inIframe } from '@/vscode/bridge';
import { TaskRelationshipViewer } from './TaskRelationshipViewer';
@@ -117,6 +118,7 @@ export function TaskDetailsPanel({
<TabNavContext.Provider value={{ activeTab, setActiveTab }}>
<ProcessSelectionProvider>
<ReviewProvider>
<EntriesProvider>
{/* Backdrop - only on smaller screens (overlay mode) */}
{!hideBackdrop && (
<div
@@ -160,7 +162,9 @@ export function TaskDetailsPanel({
projectId={projectId}
projectHasDevScript={projectHasDevScript}
forceCreateAttempt={forceCreateAttempt}
onLeaveForceCreateAttempt={onLeaveForceCreateAttempt}
onLeaveForceCreateAttempt={
onLeaveForceCreateAttempt
}
attempts={attempts}
selectedAttempt={selectedAttempt}
setSelectedAttempt={setSelectedAttempt}
@@ -168,7 +172,7 @@ export function TaskDetailsPanel({
/>
{/* Task Breakdown (TODOs) */}
<TodoPanel selectedAttempt={selectedAttempt} />
<TodoPanel />
{/* Task Relationships */}
<TaskRelationshipViewer
@@ -193,7 +197,9 @@ export function TaskDetailsPanel({
{activeTab === 'diffs' ? (
<DiffTab selectedAttempt={selectedAttempt} />
) : activeTab === 'processes' ? (
<ProcessesTab attemptId={selectedAttempt?.id} />
<ProcessesTab
attemptId={selectedAttempt?.id}
/>
) : (
<LogsTab selectedAttempt={selectedAttempt} />
)}
@@ -216,7 +222,9 @@ export function TaskDetailsPanel({
projectId={projectId}
projectHasDevScript={projectHasDevScript}
forceCreateAttempt={forceCreateAttempt}
onLeaveForceCreateAttempt={onLeaveForceCreateAttempt}
onLeaveForceCreateAttempt={
onLeaveForceCreateAttempt
}
attempts={attempts}
selectedAttempt={selectedAttempt}
setSelectedAttempt={setSelectedAttempt}
@@ -252,6 +260,7 @@ export function TaskDetailsPanel({
)}
</div>
</div>
</EntriesProvider>
</ReviewProvider>
</ProcessSelectionProvider>
</TabNavContext.Provider>

View File

@@ -1,10 +1,6 @@
import { useMemo } from 'react';
import { Circle, CircleCheckBig, CircleDotDashed } from 'lucide-react';
import { useProcessesLogs } from '@/hooks/useProcessesLogs';
import { useEntries } from '@/contexts/EntriesContext';
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';
function getStatusIcon(status?: string) {
@@ -16,26 +12,8 @@ function getStatusIcon(status?: string) {
return <Circle aria-hidden className="h-4 w-4 text-muted-foreground" />;
}
interface TodoPanelProps {
selectedAttempt: TaskAttempt | null;
}
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);
export function TodoPanel() {
const { entries } = useEntries();
const { todos } = usePinnedTodos(entries);
// Only show once the agent has created subtasks

View 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;
};

View File

@@ -1,5 +1,6 @@
import { useMemo } from 'react';
import type { TodoItem } from 'shared/types';
import type { PatchTypeWithKey } from '@/hooks/useConversationHistory';
interface UsePinnedTodosResult {
todos: TodoItem[];
@@ -10,14 +11,16 @@ interface UsePinnedTodosResult {
* 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.
*/
export const usePinnedTodos = (entries: any[]): UsePinnedTodosResult => {
export const usePinnedTodos = (
entries: PatchTypeWithKey[]
): UsePinnedTodosResult => {
return useMemo(() => {
let latestTodos: TodoItem[] = [];
let lastUpdatedTime: string | null = null;
for (const entry of entries) {
if (entry.channel === 'normalized' && entry.payload) {
const normalizedEntry = entry.payload as any;
if (entry.type === 'NORMALIZED_ENTRY' && entry.content) {
const normalizedEntry = entry.content as any;
if (
normalizedEntry.entry_type?.type === 'tool_use' &&

View File

@@ -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 };
};