Batch A: Fix high-priority 'any' types in core files (vibe-kanban) (#1310)

* 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
<ToolCallCard
  entryType={toolEntry}
  entryContent={entry.content}
  defaultExpanded={defaultExpanded}
  statusAppearance={statusAppearance}
  forceExpanded={isPendingApproval}
  linkifyUrls={isGithubCliSetup}
/>
```

### After (Simple with unified entry):
```typescript
// Just pass the entry - ToolCallCard handles everything
<ToolCallCard
  entry={entry}
  expansionKey={expansionKey}
  forceExpanded={isPendingApproval}
/>
```

### 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
This commit is contained in:
Louis Knight-Webb
2025-11-17 22:19:44 +00:00
committed by GitHub
parent 5e7742da2a
commit 870a166c38
7 changed files with 71 additions and 73 deletions

View File

@@ -6,6 +6,8 @@ import {
TaskAttempt, TaskAttempt,
ToolStatus, ToolStatus,
type NormalizedEntryType, type NormalizedEntryType,
type TaskWithAttemptStatus,
type JsonValue,
} from 'shared/types.ts'; } from 'shared/types.ts';
import type { ProcessStartPayload } from '@/types/logs'; import type { ProcessStartPayload } from '@/types/logs';
import FileChangeRenderer from './FileChangeRenderer'; import FileChangeRenderer from './FileChangeRenderer';
@@ -39,11 +41,10 @@ type Props = {
diffDeletable?: boolean; diffDeletable?: boolean;
executionProcessId?: string; executionProcessId?: string;
taskAttempt?: TaskAttempt; taskAttempt?: TaskAttempt;
task?: any; task?: TaskWithAttemptStatus;
}; };
type FileEditAction = Extract<ActionType, { action: 'file_edit' }>; type FileEditAction = Extract<ActionType, { action: 'file_edit' }>;
type JsonValue = any;
const renderJson = (v: JsonValue) => ( const renderJson = (v: JsonValue) => (
<pre className="whitespace-pre-wrap">{JSON.stringify(v, null, 2)}</pre> <pre className="whitespace-pre-wrap">{JSON.stringify(v, null, 2)}</pre>
@@ -419,62 +420,56 @@ const PlanPresentationCard: React.FC<{
}; };
const ToolCallCard: React.FC<{ const ToolCallCard: React.FC<{
entryType?: Extract<NormalizedEntryType, { type: 'tool_use' }>; entry: NormalizedEntry | ProcessStartPayload;
action?: any;
expansionKey: string; expansionKey: string;
content?: string;
entryContent?: string;
highlighted?: boolean;
defaultExpanded?: boolean;
statusAppearance?: ToolStatusAppearance;
forceExpanded?: boolean; forceExpanded?: boolean;
linkifyUrls?: boolean; }> = ({ entry, expansionKey, forceExpanded = false }) => {
}> = ({
entryType,
action,
expansionKey,
content,
entryContent,
defaultExpanded = false,
forceExpanded = false,
linkifyUrls = false,
}) => {
const { t } = useTranslation('common'); 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( const [expanded, toggle] = useExpandable(
`tool-entry:${expansionKey}`, `tool-entry:${expansionKey}`,
defaultExpanded defaultExpanded
); );
const effectiveExpanded = forceExpanded || expanded; const effectiveExpanded = forceExpanded || expanded;
const label = // Extract action details
at?.action === 'command_run' const actionType = entryType?.action_type;
? 'Ran' const isCommand = actionType?.action === 'command_run';
: entryType?.tool_name || at?.tool_name || 'Tool'; 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 isSingleLine = inlineText !== '' && !/\r?\n/.test(inlineText);
const showInlineSummary = isSingleLine; const showInlineSummary = isSingleLine;
const hasArgs = at?.action === 'tool' && !!at?.arguments; // Command details
const hasResult = at?.action === 'tool' && !!at?.result; const commandResult = isCommand ? actionType.result : null;
const output = commandResult?.output ?? null;
const output: string | null = isCommand ? (at?.result?.output ?? null) : null;
let argsText: string | null = null; let argsText: string | null = null;
if (isCommand) { if (isCommand) {
const fromArgs = const fromArgs =
typeof at?.arguments === 'string' typeof actionType.command === 'string' ? actionType.command : '';
? at.arguments const fallback = inlineText;
: at?.arguments != null
? JSON.stringify(at.arguments, null, 2)
: '';
const fallback = (entryContent || content || '').trim();
argsText = (fromArgs || fallback).trim(); argsText = (fromArgs || fallback).trim();
} }
// Tool details
const hasArgs = isTool && !!actionType.arguments;
const hasResult = isTool && !!actionType.result;
const hasExpandableDetails = isCommand const hasExpandableDetails = isCommand
? Boolean(argsText) || Boolean(output) ? Boolean(argsText) || Boolean(output)
: hasArgs || hasResult; : hasArgs || hasResult;
@@ -497,6 +492,7 @@ const ToolCallCard: React.FC<{
const headerClassName = cn( const headerClassName = cn(
'w-full flex items-center gap-1.5 text-left text-secondary-foreground' 'w-full flex items-center gap-1.5 text-left text-secondary-foreground'
); );
return ( return (
<div className="inline-block w-full flex flex-col gap-4"> <div className="inline-block w-full flex flex-col gap-4">
<HeaderWrapper {...headerProps} className={headerClassName}> <HeaderWrapper {...headerProps} className={headerClassName}>
@@ -539,26 +535,26 @@ const ToolCallCard: React.FC<{
</> </>
) : ( ) : (
<> <>
{entryType?.action_type.action === 'tool' && ( {isTool && actionType && (
<> <>
<div className="font-normal uppercase bg-background border-b border-dashed px-2 py-1"> <div className="font-normal uppercase bg-background border-b border-dashed px-2 py-1">
{t('conversation.args')} {t('conversation.args')}
</div> </div>
<div className="px-2 py-1"> <div className="px-2 py-1">
{renderJson(entryType.action_type.arguments)} {renderJson(actionType.arguments)}
</div> </div>
<div className="font-normal uppercase bg-background border-y border-dashed px-2 py-1"> <div className="font-normal uppercase bg-background border-y border-dashed px-2 py-1">
{t('conversation.result')} {t('conversation.result')}
</div> </div>
<div className="px-2 py-1"> <div className="px-2 py-1">
{entryType.action_type.result?.type.type === 'markdown' && {actionType.result?.type.type === 'markdown' &&
entryType.action_type.result.value && ( actionType.result.value && (
<MarkdownRenderer <MarkdownRenderer
content={entryType.action_type.result.value?.toString()} content={actionType.result.value?.toString()}
/> />
)} )}
{entryType.action_type.result?.type.type === 'json' && {actionType.result?.type.type === 'json' &&
renderJson(entryType.action_type.result.value)} renderJson(actionType.result.value)}
</div> </div>
</> </>
)} )}
@@ -624,14 +620,9 @@ function DisplayConversationEntry({
const greyed = isProcessGreyed(executionProcessId); const greyed = isProcessGreyed(executionProcessId);
if (isProcessStart(entry)) { if (isProcessStart(entry)) {
const toolAction: any = entry.action ?? null;
return ( return (
<div className={greyed ? 'opacity-50 pointer-events-none' : undefined}> <div className={greyed ? 'opacity-50 pointer-events-none' : undefined}>
<ToolCallCard <ToolCallCard entry={entry} expansionKey={expansionKey} />
action={toolAction}
expansionKey={expansionKey}
content={toolAction?.message ?? toolAction?.summary ?? undefined}
/>
</div> </div>
); );
} }
@@ -691,9 +682,7 @@ function DisplayConversationEntry({
const isPlanPresentation = const isPlanPresentation =
toolEntry.action_type.action === 'plan_presentation'; toolEntry.action_type.action === 'plan_presentation';
const isPendingApproval = status.status === 'pending_approval'; const isPendingApproval = status.status === 'pending_approval';
const isGithubCliSetup = toolEntry.tool_name === 'GitHub CLI Setup Script'; const defaultExpanded = isPendingApproval || isPlanPresentation;
const defaultExpanded =
isPendingApproval || isPlanPresentation || isGithubCliSetup;
const body = (() => { const body = (() => {
if (isFileEdit(toolEntry.action_type)) { if (isFileEdit(toolEntry.action_type)) {
@@ -728,13 +717,9 @@ function DisplayConversationEntry({
return ( return (
<ToolCallCard <ToolCallCard
entryType={toolEntry} entry={entry}
expansionKey={expansionKey} expansionKey={expansionKey}
entryContent={entry.content}
defaultExpanded={defaultExpanded}
statusAppearance={statusAppearance}
forceExpanded={isPendingApproval} forceExpanded={isPendingApproval}
linkifyUrls={isGithubCliSetup}
/> />
); );
})(); })();

View File

@@ -24,7 +24,11 @@ import { getIdeName } from '@/components/ide/IdeIcon';
import { useProject } from '@/contexts/project-context'; import { useProject } from '@/contexts/project-context';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { attemptsApi } from '@/lib/api'; import { attemptsApi } from '@/lib/api';
import { BaseAgentCapability, type BaseCodingAgent } from 'shared/types'; import {
BaseAgentCapability,
type BaseCodingAgent,
type TaskWithAttemptStatus,
} from 'shared/types';
import { import {
Tooltip, Tooltip,
TooltipContent, TooltipContent,
@@ -37,7 +41,7 @@ type NextActionCardProps = {
containerRef?: string | null; containerRef?: string | null;
failed: boolean; failed: boolean;
execution_processes: number; execution_processes: number;
task?: any; task?: TaskWithAttemptStatus;
needsSetup?: boolean; needsSetup?: boolean;
}; };

View File

@@ -265,9 +265,11 @@ const PendingApprovalEntry = ({
}); });
setHasResponded(true); setHasResponded(true);
clear(); clear();
} catch (e: any) { } catch (e: unknown) {
console.error('Approval respond failed:', e); 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 { } finally {
setIsResponding(false); setIsResponding(false);
} }

View File

@@ -52,8 +52,8 @@ export const useLogStream = (processId: string): UseLogStreamResult => {
// Handle different message types based on LogMsg enum // Handle different message types based on LogMsg enum
if ('JsonPatch' in data) { if ('JsonPatch' in data) {
const patches = data.JsonPatch; const patches = data.JsonPatch as Array<{ value?: PatchType }>;
patches.forEach((patch: any) => { patches.forEach((patch) => {
const value = patch?.value; const value = patch?.value;
if (!value || !value.type) return; if (!value || !value.type) return;

View File

@@ -3,7 +3,11 @@ import { useCallback, useState } from 'react';
import { useAttemptExecution } from '@/hooks/useAttemptExecution'; import { useAttemptExecution } from '@/hooks/useAttemptExecution';
import { useBranchStatus } from '@/hooks/useBranchStatus'; import { useBranchStatus } from '@/hooks/useBranchStatus';
import { attemptsApi, executionProcessesApi } from '@/lib/api'; 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. * Reusable hook to retry a process given its executionProcessId and a new prompt.
@@ -64,13 +68,14 @@ export function useProcessRetry(attempt: TaskAttempt | undefined) {
try { try {
const details = const details =
await executionProcessesApi.getDetails(executionProcessId); await executionProcessesApi.getDetails(executionProcessId);
const typ: any = details?.executor_action?.typ as any; const typ: ExecutorActionType | undefined =
details?.executor_action?.typ;
if ( if (
typ && typ &&
(typ.type === 'CodingAgentInitialRequest' || (typ.type === 'CodingAgentInitialRequest' ||
typ.type === 'CodingAgentFollowUpRequest') typ.type === 'CodingAgentFollowUpRequest')
) { ) {
variant = (typ.executor_profile_id?.variant as string | null) ?? null; variant = typ.executor_profile_id?.variant ?? null;
} }
} catch { } catch {
/* ignore */ /* ignore */
@@ -83,7 +88,7 @@ export function useProcessRetry(attempt: TaskAttempt | undefined) {
prompt: newPrompt, prompt: newPrompt,
variant, variant,
image_ids: [], image_ids: [],
version: null as any, version: null,
}); });
} finally { } finally {
setBusy(false); setBusy(false);

View File

@@ -1,11 +1,12 @@
import { useMemo } from 'react'; import { useMemo } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { profilesApi } from '@/lib/api'; import { profilesApi } from '@/lib/api';
import type { JsonValue } from 'shared/types';
export type UseProfilesReturn = { export type UseProfilesReturn = {
// data // data
profilesContent: string; profilesContent: string;
parsedProfiles: any | null; parsedProfiles: JsonValue | null;
profilesPath: string; profilesPath: string;
// status // status
@@ -33,8 +34,9 @@ export function useProfiles(): UseProfilesReturn {
mutationFn: (content: string) => profilesApi.save(content), mutationFn: (content: string) => profilesApi.save(content),
onSuccess: (_, content) => { onSuccess: (_, content) => {
// Optimistically update cache with new content // Optimistically update cache with new content
queryClient.setQueryData(['profiles'], (old: any) => queryClient.setQueryData<{ content: string; path: string }>(
old ? { ...old, content } : old ['profiles'],
(old) => (old ? { ...old, content } : old)
); );
}, },
}); });

View File

@@ -274,7 +274,7 @@ export const projectsApi = {
id: string, id: string,
editorType?: EditorType editorType?: EditorType
): Promise<OpenEditorResponse> => { ): Promise<OpenEditorResponse> => {
const requestBody: any = {}; const requestBody: { editor_type?: EditorType } = {};
if (editorType) requestBody.editor_type = editorType; if (editorType) requestBody.editor_type = editorType;
const response = await makeRequest(`/api/projects/${id}/open-editor`, { const response = await makeRequest(`/api/projects/${id}/open-editor`, {