From 870a166c3801bbd4f836fe6318a2fbb001b5ffde Mon Sep 17 00:00:00 2001 From: Louis Knight-Webb Date: Mon, 17 Nov 2025 22:19:44 +0000 Subject: [PATCH] Batch A: Fix high-priority 'any' types in core files (vibe-kanban) (#1310) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Perfect! All type checks pass. Here's a summary of the completed fixes: ## ✅ Batch A Complete: Fixed all 'any' types in 7 core files ### Summary of Changes **1. useLogStream.ts** - Typed WebSocket patches - `patches.forEach((patch: any)` → `const patches = data.JsonPatch as Array<{ value?: PatchType }>` **2. api.ts** - Typed request body - `const requestBody: any = {}` → `const requestBody: { editor_type?: EditorType } = {}` **3. NextActionCard.tsx** - Used proper domain type - `task?: any` → `task?: TaskWithAttemptStatus` **4. PendingApprovalEntry.tsx** - Safe error handling - `catch (e: any)` → `catch (e: unknown)` with proper narrowing **5. useProfiles.ts** - Typed JSON values - `parsedProfiles: any | null` → `parsedProfiles: JsonValue | null` - `queryClient.setQueryData(['profiles'], (old: any)` → typed with `<{ content: string; path: string }>` **6. useProcessRetry.ts** - Typed executor actions - `const typ: any = details?.executor_action?.typ as any` → `const typ: ExecutorActionType | undefined` - Removed unnecessary `as any` cast on version field **7. DisplayConversationEntry.tsx** - Created ActionLike helper type - Removed local `type JsonValue = any` - Imported `JsonValue` and `TaskWithAttemptStatus` from shared/types - Created minimal `ActionLike` type for ToolCallCard compatibility - Typed all component props and variables properly All changes passed frontend and backend type checks ✓ * Cleanup script changes for task attempt 1b9207c2-6291-4bea-b06d-b4702f99f9ee * Perfect! Updated the `ActionLike` type to use `CommandRunResult` from shared types, which is cleaner and more aligned with the domain model. All type checks pass ✓ * Excellent! All checks pass. Let me create a summary of what we accomplished: ## ✅ Refactor Complete: Simplified ToolCallCard by Passing Full Entry ### Summary of Changes **Removed the ActionLike workaround** and made the code significantly simpler and more readable: ### Before (Complex with ActionLike bridge): ```typescript // Helper type to bridge ExecutorAction and ActionType type ActionLike = Partial<{ action: string; arguments: JsonValue; result: CommandRunResult; tool_name: string; message: string; summary: string; }>; // Multiple props with conversions at call sites ``` ### After (Simple with unified entry): ```typescript // Just pass the entry - ToolCallCard handles everything ``` ### Key Improvements 1. **Simpler Props**: Changed from 8+ props to just 3 (`entry`, `expansionKey`, `forceExpanded`) 2. **Removed ActionLike**: Eliminated the bridge type that was papering over type differences 3. **Centralized Logic**: All normalization logic now lives inside ToolCallCard where it belongs 4. **Type Safety**: Uses proper type narrowing (`'entry_type' in entry`) instead of type casts 5. **Better Readability**: Call sites are now trivial - just pass the entry ### Files Changed - `DisplayConversationEntry.tsx`: - Removed ActionLike helper type - Updated ToolCallCard component to accept unified entry prop - Simplified both call sites (ProcessStartPayload and NormalizedEntry) - Moved linkifyUrls, defaultExpanded logic into ToolCallCard All type checks pass ✓ * Cleanup script changes for task attempt 1b9207c2-6291-4bea-b06d-b4702f99f9ee --- .../DisplayConversationEntry.tsx | 103 ++++++++---------- .../NormalizedConversation/NextActionCard.tsx | 8 +- .../PendingApprovalEntry.tsx | 6 +- frontend/src/hooks/useLogStream.ts | 4 +- frontend/src/hooks/useProcessRetry.ts | 13 ++- frontend/src/hooks/useProfiles.ts | 8 +- frontend/src/lib/api.ts | 2 +- 7 files changed, 71 insertions(+), 73 deletions(-) diff --git a/frontend/src/components/NormalizedConversation/DisplayConversationEntry.tsx b/frontend/src/components/NormalizedConversation/DisplayConversationEntry.tsx index e44499a2..57cfce0a 100644 --- a/frontend/src/components/NormalizedConversation/DisplayConversationEntry.tsx +++ b/frontend/src/components/NormalizedConversation/DisplayConversationEntry.tsx @@ -6,6 +6,8 @@ import { TaskAttempt, ToolStatus, type NormalizedEntryType, + type TaskWithAttemptStatus, + type JsonValue, } from 'shared/types.ts'; import type { ProcessStartPayload } from '@/types/logs'; import FileChangeRenderer from './FileChangeRenderer'; @@ -39,11 +41,10 @@ type Props = { diffDeletable?: boolean; executionProcessId?: string; taskAttempt?: TaskAttempt; - task?: any; + task?: TaskWithAttemptStatus; }; type FileEditAction = Extract; -type JsonValue = any; const renderJson = (v: JsonValue) => (
{JSON.stringify(v, null, 2)}
@@ -419,62 +420,56 @@ const PlanPresentationCard: React.FC<{ }; const ToolCallCard: React.FC<{ - entryType?: Extract; - action?: any; + entry: NormalizedEntry | ProcessStartPayload; expansionKey: string; - content?: string; - entryContent?: string; - highlighted?: boolean; - defaultExpanded?: boolean; - statusAppearance?: ToolStatusAppearance; forceExpanded?: boolean; - linkifyUrls?: boolean; -}> = ({ - entryType, - action, - expansionKey, - content, - entryContent, - defaultExpanded = false, - forceExpanded = false, - linkifyUrls = false, -}) => { +}> = ({ entry, expansionKey, forceExpanded = false }) => { const { t } = useTranslation('common'); - const at: any = entryType?.action_type || action; + + // Determine if this is a NormalizedEntry with tool_use + const isNormalizedEntry = 'entry_type' in entry; + const entryType = + isNormalizedEntry && entry.entry_type.type === 'tool_use' + ? entry.entry_type + : undefined; + + // Compute defaults from entry + const linkifyUrls = entryType?.tool_name === 'GitHub CLI Setup Script'; + const defaultExpanded = linkifyUrls; + const [expanded, toggle] = useExpandable( `tool-entry:${expansionKey}`, defaultExpanded ); const effectiveExpanded = forceExpanded || expanded; - const label = - at?.action === 'command_run' - ? 'Ran' - : entryType?.tool_name || at?.tool_name || 'Tool'; + // Extract action details + const actionType = entryType?.action_type; + const isCommand = actionType?.action === 'command_run'; + const isTool = actionType?.action === 'tool'; - const isCommand = at?.action === 'command_run'; + // Label and content + const label = isCommand ? 'Ran' : entryType?.tool_name || 'Tool'; - const inlineText = (entryContent || content || '').trim(); + const inlineText = isNormalizedEntry ? entry.content.trim() : ''; const isSingleLine = inlineText !== '' && !/\r?\n/.test(inlineText); const showInlineSummary = isSingleLine; - const hasArgs = at?.action === 'tool' && !!at?.arguments; - const hasResult = at?.action === 'tool' && !!at?.result; - - const output: string | null = isCommand ? (at?.result?.output ?? null) : null; + // Command details + const commandResult = isCommand ? actionType.result : null; + const output = commandResult?.output ?? null; let argsText: string | null = null; if (isCommand) { const fromArgs = - typeof at?.arguments === 'string' - ? at.arguments - : at?.arguments != null - ? JSON.stringify(at.arguments, null, 2) - : ''; - - const fallback = (entryContent || content || '').trim(); + typeof actionType.command === 'string' ? actionType.command : ''; + const fallback = inlineText; argsText = (fromArgs || fallback).trim(); } + // Tool details + const hasArgs = isTool && !!actionType.arguments; + const hasResult = isTool && !!actionType.result; + const hasExpandableDetails = isCommand ? Boolean(argsText) || Boolean(output) : hasArgs || hasResult; @@ -497,6 +492,7 @@ const ToolCallCard: React.FC<{ const headerClassName = cn( 'w-full flex items-center gap-1.5 text-left text-secondary-foreground' ); + return (
@@ -539,26 +535,26 @@ const ToolCallCard: React.FC<{ ) : ( <> - {entryType?.action_type.action === 'tool' && ( + {isTool && actionType && ( <>
{t('conversation.args')}
- {renderJson(entryType.action_type.arguments)} + {renderJson(actionType.arguments)}
{t('conversation.result')}
- {entryType.action_type.result?.type.type === 'markdown' && - entryType.action_type.result.value && ( + {actionType.result?.type.type === 'markdown' && + actionType.result.value && ( )} - {entryType.action_type.result?.type.type === 'json' && - renderJson(entryType.action_type.result.value)} + {actionType.result?.type.type === 'json' && + renderJson(actionType.result.value)}
)} @@ -624,14 +620,9 @@ function DisplayConversationEntry({ const greyed = isProcessGreyed(executionProcessId); if (isProcessStart(entry)) { - const toolAction: any = entry.action ?? null; return (
- +
); } @@ -691,9 +682,7 @@ function DisplayConversationEntry({ const isPlanPresentation = toolEntry.action_type.action === 'plan_presentation'; const isPendingApproval = status.status === 'pending_approval'; - const isGithubCliSetup = toolEntry.tool_name === 'GitHub CLI Setup Script'; - const defaultExpanded = - isPendingApproval || isPlanPresentation || isGithubCliSetup; + const defaultExpanded = isPendingApproval || isPlanPresentation; const body = (() => { if (isFileEdit(toolEntry.action_type)) { @@ -728,13 +717,9 @@ function DisplayConversationEntry({ return ( ); })(); diff --git a/frontend/src/components/NormalizedConversation/NextActionCard.tsx b/frontend/src/components/NormalizedConversation/NextActionCard.tsx index a6cd70cf..c3ff3e31 100644 --- a/frontend/src/components/NormalizedConversation/NextActionCard.tsx +++ b/frontend/src/components/NormalizedConversation/NextActionCard.tsx @@ -24,7 +24,11 @@ import { getIdeName } from '@/components/ide/IdeIcon'; import { useProject } from '@/contexts/project-context'; import { useQuery } from '@tanstack/react-query'; import { attemptsApi } from '@/lib/api'; -import { BaseAgentCapability, type BaseCodingAgent } from 'shared/types'; +import { + BaseAgentCapability, + type BaseCodingAgent, + type TaskWithAttemptStatus, +} from 'shared/types'; import { Tooltip, TooltipContent, @@ -37,7 +41,7 @@ type NextActionCardProps = { containerRef?: string | null; failed: boolean; execution_processes: number; - task?: any; + task?: TaskWithAttemptStatus; needsSetup?: boolean; }; diff --git a/frontend/src/components/NormalizedConversation/PendingApprovalEntry.tsx b/frontend/src/components/NormalizedConversation/PendingApprovalEntry.tsx index 95be537d..80b76684 100644 --- a/frontend/src/components/NormalizedConversation/PendingApprovalEntry.tsx +++ b/frontend/src/components/NormalizedConversation/PendingApprovalEntry.tsx @@ -265,9 +265,11 @@ const PendingApprovalEntry = ({ }); setHasResponded(true); clear(); - } catch (e: any) { + } catch (e: unknown) { console.error('Approval respond failed:', e); - setError(e?.message || 'Failed to send response'); + const errorMessage = + e instanceof Error ? e.message : 'Failed to send response'; + setError(errorMessage); } finally { setIsResponding(false); } diff --git a/frontend/src/hooks/useLogStream.ts b/frontend/src/hooks/useLogStream.ts index fad0c798..c9fdc4f0 100644 --- a/frontend/src/hooks/useLogStream.ts +++ b/frontend/src/hooks/useLogStream.ts @@ -52,8 +52,8 @@ export const useLogStream = (processId: string): UseLogStreamResult => { // Handle different message types based on LogMsg enum if ('JsonPatch' in data) { - const patches = data.JsonPatch; - patches.forEach((patch: any) => { + const patches = data.JsonPatch as Array<{ value?: PatchType }>; + patches.forEach((patch) => { const value = patch?.value; if (!value || !value.type) return; diff --git a/frontend/src/hooks/useProcessRetry.ts b/frontend/src/hooks/useProcessRetry.ts index 37837a9a..272fd736 100644 --- a/frontend/src/hooks/useProcessRetry.ts +++ b/frontend/src/hooks/useProcessRetry.ts @@ -3,7 +3,11 @@ import { useCallback, useState } from 'react'; import { useAttemptExecution } from '@/hooks/useAttemptExecution'; import { useBranchStatus } from '@/hooks/useBranchStatus'; import { attemptsApi, executionProcessesApi } from '@/lib/api'; -import type { ExecutionProcess, TaskAttempt } from 'shared/types'; +import type { + ExecutionProcess, + TaskAttempt, + ExecutorActionType, +} from 'shared/types'; /** * Reusable hook to retry a process given its executionProcessId and a new prompt. @@ -64,13 +68,14 @@ export function useProcessRetry(attempt: TaskAttempt | undefined) { try { const details = await executionProcessesApi.getDetails(executionProcessId); - const typ: any = details?.executor_action?.typ as any; + const typ: ExecutorActionType | undefined = + details?.executor_action?.typ; if ( typ && (typ.type === 'CodingAgentInitialRequest' || typ.type === 'CodingAgentFollowUpRequest') ) { - variant = (typ.executor_profile_id?.variant as string | null) ?? null; + variant = typ.executor_profile_id?.variant ?? null; } } catch { /* ignore */ @@ -83,7 +88,7 @@ export function useProcessRetry(attempt: TaskAttempt | undefined) { prompt: newPrompt, variant, image_ids: [], - version: null as any, + version: null, }); } finally { setBusy(false); diff --git a/frontend/src/hooks/useProfiles.ts b/frontend/src/hooks/useProfiles.ts index c8859a4e..9fbf07bb 100644 --- a/frontend/src/hooks/useProfiles.ts +++ b/frontend/src/hooks/useProfiles.ts @@ -1,11 +1,12 @@ import { useMemo } from 'react'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { profilesApi } from '@/lib/api'; +import type { JsonValue } from 'shared/types'; export type UseProfilesReturn = { // data profilesContent: string; - parsedProfiles: any | null; + parsedProfiles: JsonValue | null; profilesPath: string; // status @@ -33,8 +34,9 @@ export function useProfiles(): UseProfilesReturn { mutationFn: (content: string) => profilesApi.save(content), onSuccess: (_, content) => { // Optimistically update cache with new content - queryClient.setQueryData(['profiles'], (old: any) => - old ? { ...old, content } : old + queryClient.setQueryData<{ content: string; path: string }>( + ['profiles'], + (old) => (old ? { ...old, content } : old) ); }, }); diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index f01dbf6f..4c2f128c 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -274,7 +274,7 @@ export const projectsApi = { id: string, editorType?: EditorType ): Promise => { - const requestBody: any = {}; + const requestBody: { editor_type?: EditorType } = {}; if (editorType) requestBody.editor_type = editorType; const response = await makeRequest(`/api/projects/${id}/open-editor`, {