From 81b92c6ec33c09a2aad5b7c7374f9519412ae071 Mon Sep 17 00:00:00 2001 From: Louis Knight-Webb Date: Tue, 18 Nov 2025 16:22:06 +0000 Subject: [PATCH] Resolve frontend warnings final (#1319) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ## ✅ Complete - First 3 Categories Fixed Successfully resolved **13 of 26 TypeScript warnings** (50% reduction): ### **Changes Made:** **Category 1: Intl Polyfill (1 warning fixed)** - [TaskPanel.tsx](file:///private/var/folders/m1/9q_ct1913z10v6wbnv54j25r0000gn/T/vibe-kanban/worktrees/3bbe-resolve-frontend/frontend/src/components/panels/TaskPanel.tsx#L39-L42): Replaced `(Intl as any).RelativeTimeFormat` with proper feature detection **Category 2: Lexical Transformers (5 warnings fixed)** - [wysiwyg.tsx](file:///private/var/folders/m1/9q_ct1913z10v6wbnv54j25r0000gn/T/vibe-kanban/worktrees/3bbe-resolve-frontend/frontend/src/components/ui/wysiwyg.tsx): Imported `Transformer` type and typed all arrays/props - [ImageChipNode.tsx](file:///private/var/folders/m1/9q_ct1913z10v6wbnv54j25r0000gn/T/vibe-kanban/worktrees/3bbe-resolve-frontend/frontend/src/components/ui/wysiwyg/ImageChipNode.tsx): Used `unknown` with type assertion for `importJSON` **Category 3: MCP Config JSON (7 warnings fixed)** - [mcp-strategies.ts](file:///private/var/folders/m1/9q_ct1913z10v6wbnv54j25r0000gn/T/vibe-kanban/worktrees/3bbe-resolve-frontend/frontend/src/lib/mcp-strategies.ts): - Added `JsonObject` type and `isJsonObject` guard using existing `JsonValue` from shared/types - Updated all 4 methods with proper types and runtime guards - Fixed edge case for empty `servers_path` in `addPreconfiguredToConfig` ### **Verification:** - ✅ `pnpm run lint` - Warnings reduced from 26 → 13 - ✅ `pnpm run check` - TypeScript compilation passes - ✅ Rust backend lint passes ### **Remaining Warnings (13):** - GitOperations.tsx: 3 warnings (branch merge parsing) - ClickedElementsBanner.tsx: 1 warning - useFollowUpSend.ts: 1 warning - usePinnedTodos.ts: 1 warning - useTaskMutations.ts: 2 warnings - project-tasks.tsx: 3 warnings - AgentSettings.tsx: 2 warnings * Excellent! All warnings are resolved. Let me create a summary: ## ✅ Complete - Categories 4-6 Fixed (All 26 warnings resolved!) Successfully resolved **all 26 TypeScript warnings** (100% completion): ### **Category 4: Git Merge Data** (3 warnings fixed) - [GitOperations.tsx](file:///private/var/folders/m1/9q_ct1913z10v6wbnv54j25r0000gn/T/vibe-kanban/worktrees/3bbe-resolve-frontend/frontend/src/components/tasks/Toolbar/GitOperations.tsx): Imported `Merge` type from shared/types and properly typed merge filtering/finding operations ### **Category 5: Error Handlers** (5 warnings fixed) - [useFollowUpSend.ts](file:///private/var/folders/m1/9q_ct1913z10v6wbnv54j25r0000gn/T/vibe-kanban/worktrees/3bbe-resolve-frontend/frontend/src/hooks/follow-up/useFollowUpSend.ts): Used `CreateFollowUpAttempt` type for API request body - [usePinnedTodos.ts](file:///private/var/folders/m1/9q_ct1913z10v6wbnv54j25r0000gn/T/vibe-kanban/worktrees/3bbe-resolve-frontend/frontend/src/hooks/usePinnedTodos.ts): Used `NormalizedEntry` type from shared/types - [useTaskMutations.ts](file:///private/var/folders/m1/9q_ct1913z10v6wbnv54j25r0000gn/T/vibe-kanban/worktrees/3bbe-resolve-frontend/frontend/src/hooks/useTaskMutations.ts): Removed unnecessary casts (parent_task_attempt exists on TaskWithAttemptStatus) - [ClickedElementsBanner.tsx](file:///private/var/folders/m1/9q_ct1913z10v6wbnv54j25r0000gn/T/vibe-kanban/worktrees/3bbe-resolve-frontend/frontend/src/components/tasks/ClickedElementsBanner.tsx): Defined `ComponentInfo` type derived from ClickedEntry ### **Category 6: Settings Page** (2 warnings fixed) - [AgentSettings.tsx](file:///private/var/folders/m1/9q_ct1913z10v6wbnv54j25r0000gn/T/vibe-kanban/worktrees/3bbe-resolve-frontend/frontend/src/pages/settings/AgentSettings.tsx): - Defined `ProfilesDoc` type for parsed profiles structure - Changed `selectedExecutorType` from `string` to `BaseCodingAgent` enum ### **Additional Fixes:** - [project-tasks.tsx](file:///private/var/folders/m1/9q_ct1913z10v6wbnv54j25r0000gn/T/vibe-kanban/worktrees/3bbe-resolve-frontend/frontend/src/pages/project-tasks.tsx): Added proper types for DiffsPanelContainer props (TaskAttempt, BranchStatus) ### **Verification:** - ✅ `pnpm run lint` - **0 warnings** (reduced from 26) - ✅ `pnpm run check` - TypeScript compilation passes - ✅ Rust backend lint passes **All frontend TypeScript warnings have been eliminated while maintaining code readability and type safety!** * ## ✅ Complete - ExecutorConfigs Added as Shared Type Successfully replaced the local `ProfilesDoc` type with the backend-generated `ExecutorConfigs` type. ### **Changes Made:** **Backend:** - [generate_types.rs](file:///private/var/folders/m1/9q_ct1913z10v6wbnv54j25r0000gn/T/vibe-kanban/worktrees/3bbe-resolve-frontend/crates/server/src/bin/generate_types.rs#L108): Added `ExecutorConfigs` to exported types - [shared/types.ts](file:///private/var/folders/m1/9q_ct1913z10v6wbnv54j25r0000gn/T/vibe-kanban/worktrees/3bbe-resolve-frontend/shared/types.ts#L225): Generated new `ExecutorConfigs` type **Frontend:** - [AgentSettings.tsx](file:///private/var/folders/m1/9q_ct1913z10v6wbnv54j25r0000gn/T/vibe-kanban/worktrees/3bbe-resolve-frontend/frontend/src/pages/settings/AgentSettings.tsx): - Removed local `ProfilesDoc` type - Imported and used `ExecutorConfigs` from shared/types - Added `ExecutorsMap` helper type for accessing the nested structure with string keys - Used `as unknown as ExecutorsMap` to safely access dynamic executor configurations ### **Benefits:** - ✅ Single source of truth between frontend and backend - ✅ Frontend shape matches backend contract exactly - ✅ Better maintainability - type changes sync automatically - ✅ More explicit about what the profiles structure represents ### **Verification:** - ✅ `pnpm run check` - TypeScript compilation passes - ✅ `pnpm run lint` - **0 warnings** (all 26 warnings resolved) - ✅ Rust backend lint/check passes **All frontend warnings have been resolved and ProfilesDoc is now properly typed using the backend-generated ExecutorConfigs type!** * Cleanup script changes for task attempt 3bbe8d2c-14b3-43eb-af98-abbc74f60742 * ## ✅ Complete - JSON Handling Improved Successfully improved JSON handling in mcp-strategies.ts with a simple, idiomatic change: ### **Change Made:** [mcp-strategies.ts](file:///private/var/folders/m1/9q_ct1913z10v6wbnv54j25r0000gn/T/vibe-kanban/worktrees/3bbe-resolve-frontend/frontend/src/lib/mcp-strategies.ts#L3): ```typescript // Before: type JsonObject = { [key: string]: JsonValue }; // After: type JsonObject = Record; ``` ### **Oracle's Analysis:** - ✅ **Keep it local** - Only this file needs it, no need for shared utilities - ✅ **Keep the type guard** - Essential for runtime safety when parsing/traversing JSON - ✅ **Use Record** - More idiomatic TypeScript than index signature - ❌ **No backend export needed** - Would add unnecessary complexity - ❌ **No shared file needed** - Single-use type doesn't warrant it ### **Verification:** - ✅ `pnpm run check` - TypeScript compilation passes - ✅ `pnpm run lint` - **0 warnings** - ✅ Rust backend passes **The local JSON handling approach is the simplest and most readable solution for this use case!** --- crates/server/src/bin/generate_types.rs | 1 + frontend/src/components/panels/TaskPanel.tsx | 3 +- .../tasks/ClickedElementsBanner.tsx | 10 +- .../tasks/Toolbar/GitOperations.tsx | 7 +- frontend/src/components/ui/wysiwyg.tsx | 11 ++- .../components/ui/wysiwyg/ImageChipNode.tsx | 6 +- .../src/hooks/follow-up/useFollowUpSend.ts | 7 +- frontend/src/hooks/usePinnedTodos.ts | 4 +- frontend/src/hooks/useTaskMutations.ts | 4 +- frontend/src/lib/mcp-strategies.ts | 91 ++++++++++++------- frontend/src/pages/project-tasks.tsx | 10 +- frontend/src/pages/settings/AgentSettings.tsx | 83 ++++++++++------- shared/types.ts | 2 + 13 files changed, 146 insertions(+), 93 deletions(-) diff --git a/crates/server/src/bin/generate_types.rs b/crates/server/src/bin/generate_types.rs index b25e4b51..f12ff06b 100644 --- a/crates/server/src/bin/generate_types.rs +++ b/crates/server/src/bin/generate_types.rs @@ -105,6 +105,7 @@ fn generate_types_content() -> String { executors::command::CommandBuilder::decl(), executors::profile::ExecutorProfileId::decl(), executors::profile::ExecutorConfig::decl(), + executors::profile::ExecutorConfigs::decl(), executors::executors::BaseAgentCapability::decl(), executors::executors::claude::ClaudeCode::decl(), executors::executors::gemini::Gemini::decl(), diff --git a/frontend/src/components/panels/TaskPanel.tsx b/frontend/src/components/panels/TaskPanel.tsx index ac7047f8..55dbd4b2 100644 --- a/frontend/src/components/panels/TaskPanel.tsx +++ b/frontend/src/components/panels/TaskPanel.tsx @@ -37,7 +37,8 @@ const TaskPanel = ({ task }: TaskPanelProps) => { const absSec = Math.round(Math.abs(diffMs) / 1000); const rtf = - typeof Intl !== 'undefined' && (Intl as any).RelativeTimeFormat + typeof Intl !== 'undefined' && + typeof Intl.RelativeTimeFormat === 'function' ? new Intl.RelativeTimeFormat(undefined, { numeric: 'auto' }) : null; diff --git a/frontend/src/components/tasks/ClickedElementsBanner.tsx b/frontend/src/components/tasks/ClickedElementsBanner.tsx index 25eea8e9..e977d979 100644 --- a/frontend/src/components/tasks/ClickedElementsBanner.tsx +++ b/frontend/src/components/tasks/ClickedElementsBanner.tsx @@ -18,13 +18,15 @@ export type Props = Readonly<{ const MAX_VISIBLE_ELEMENTS = 5; const MAX_BADGES = 6; +type ComponentInfo = ClickedEntry['payload']['components'][number]; + // Build component chain from inner-most to outer-most for banner display function buildChainInnerToOuterForBanner(entry: ClickedEntry) { - const comps = entry.payload.components ?? []; - const s = entry.payload.selected; + const comps: ComponentInfo[] = entry.payload.components ?? []; + const s: ComponentInfo = entry.payload.selected; - // Start with selected as innermost, cast to ComponentInfo for uniform handling - const innerToOuter = [s as any]; + // Start with selected as innermost + const innerToOuter = [s]; // Add components that aren't duplicates const selectedKey = `${s.name}|${s.pathToSource}|${s.source?.lineNumber}|${s.source?.columnNumber}`; diff --git a/frontend/src/components/tasks/Toolbar/GitOperations.tsx b/frontend/src/components/tasks/Toolbar/GitOperations.tsx index 9be37f10..bc9c4bb8 100644 --- a/frontend/src/components/tasks/Toolbar/GitOperations.tsx +++ b/frontend/src/components/tasks/Toolbar/GitOperations.tsx @@ -18,6 +18,7 @@ import { import { useMemo, useState } from 'react'; import type { BranchStatus, + Merge, GitBranch, TaskAttempt, TaskWithAttemptStatus, @@ -102,15 +103,15 @@ function GitOperations({ }; const openPR = branchStatus.merges.find( - (m: any) => m.type === 'pr' && m.pr_info.status === 'open' + (m) => m.type === 'pr' && m.pr_info.status === 'open' ); const mergedPR = branchStatus.merges.find( - (m: any) => m.type === 'pr' && m.pr_info.status === 'merged' + (m) => m.type === 'pr' && m.pr_info.status === 'merged' ); const merges = branchStatus.merges.filter( - (m: any) => + (m: Merge) => m.type === 'direct' || (m.type === 'pr' && m.pr_info.status === 'merged') ); diff --git a/frontend/src/components/ui/wysiwyg.tsx b/frontend/src/components/ui/wysiwyg.tsx index d60f0b70..90eccb75 100644 --- a/frontend/src/components/ui/wysiwyg.tsx +++ b/frontend/src/components/ui/wysiwyg.tsx @@ -8,6 +8,7 @@ import { $convertToMarkdownString, $convertFromMarkdownString, TRANSFORMERS, + type Transformer, } from '@lexical/markdown'; import { ImageChipNode, InsertImageChipPlugin } from './wysiwyg/ImageChipNode'; import { LexicalErrorBoundary } from '@lexical/react/LexicalErrorBoundary'; @@ -62,11 +63,11 @@ export default function WYSIWYGEditor({ // Shared ref to avoid update loops and redundant imports const lastMdRef = useRef(''); - const exportTransformers = useMemo( + const exportTransformers: Transformer[] = useMemo( () => [...TRANSFORMERS, IMAGE_CHIP_EXPORT], [] ); - const importTransformers = useMemo( + const importTransformers: Transformer[] = useMemo( () => [...TRANSFORMERS, IMAGE_CHIP_IMPORT], [] ); @@ -132,7 +133,7 @@ function MarkdownOnChangePlugin({ }: { onMarkdownChange?: (md: string) => void; onEditorStateChange?: (s: EditorState) => void; - exportTransformers: any[]; + exportTransformers: Transformer[]; lastMdRef: React.MutableRefObject; }) { const [editor] = useLexicalComposerContext(); @@ -165,7 +166,7 @@ function MarkdownValuePlugin({ lastMdRef, }: { value?: string; - importTransformers: any[]; + importTransformers: Transformer[]; lastMdRef: React.MutableRefObject; }) { const [editor] = useLexicalComposerContext(); @@ -188,7 +189,7 @@ function MarkdownDefaultValuePlugin({ lastMdRef, }: { defaultValue: string; - importTransformers: any[]; + importTransformers: Transformer[]; lastMdRef: React.MutableRefObject; }) { const [editor] = useLexicalComposerContext(); diff --git a/frontend/src/components/ui/wysiwyg/ImageChipNode.tsx b/frontend/src/components/ui/wysiwyg/ImageChipNode.tsx index 789f0f6c..e0cf4d41 100644 --- a/frontend/src/components/ui/wysiwyg/ImageChipNode.tsx +++ b/frontend/src/components/ui/wysiwyg/ImageChipNode.tsx @@ -88,10 +88,10 @@ export class ImageChipNode extends DecoratorNode { return false; } - static importJSON(json: any): ImageChipNode { - return new ImageChipNode(json); + static importJSON(json: unknown): ImageChipNode { + return new ImageChipNode(json as ImageChipPayload); } - exportJSON(): any { + exportJSON(): ImageChipPayload & { type: string; version: number } { return { type: 'image-chip', version: 1, diff --git a/frontend/src/hooks/follow-up/useFollowUpSend.ts b/frontend/src/hooks/follow-up/useFollowUpSend.ts index ac283659..cff2cf60 100644 --- a/frontend/src/hooks/follow-up/useFollowUpSend.ts +++ b/frontend/src/hooks/follow-up/useFollowUpSend.ts @@ -1,6 +1,6 @@ import { useCallback, useState } from 'react'; import { attemptsApi } from '@/lib/api'; -import type { ImageResponse } from 'shared/types'; +import type { ImageResponse, CreateFollowUpAttempt } from 'shared/types'; type Args = { attemptId?: string; @@ -57,14 +57,15 @@ export function useFollowUpSend({ : images.length > 0 ? images.map((img) => img.id) : null; - await attemptsApi.followUp(attemptId, { + const body: CreateFollowUpAttempt = { prompt: finalPrompt, variant: selectedVariant, image_ids, retry_process_id: null, force_when_dirty: null, perform_git_reset: null, - } as any); + }; + await attemptsApi.followUp(attemptId, body); setMessage(''); clearComments(); clearClickedElements?.(); diff --git a/frontend/src/hooks/usePinnedTodos.ts b/frontend/src/hooks/usePinnedTodos.ts index b0156e46..46518a57 100644 --- a/frontend/src/hooks/usePinnedTodos.ts +++ b/frontend/src/hooks/usePinnedTodos.ts @@ -1,5 +1,5 @@ import { useMemo } from 'react'; -import type { TodoItem } from 'shared/types'; +import type { TodoItem, NormalizedEntry } from 'shared/types'; import type { PatchTypeWithKey } from '@/hooks/useConversationHistory'; interface UsePinnedTodosResult { @@ -20,7 +20,7 @@ export const usePinnedTodos = ( for (const entry of entries) { if (entry.type === 'NORMALIZED_ENTRY' && entry.content) { - const normalizedEntry = entry.content as any; + const normalizedEntry = entry.content as NormalizedEntry; if ( normalizedEntry.entry_type?.type === 'tool_use' && diff --git a/frontend/src/hooks/useTaskMutations.ts b/frontend/src/hooks/useTaskMutations.ts index 06818845..dc7b8b17 100644 --- a/frontend/src/hooks/useTaskMutations.ts +++ b/frontend/src/hooks/useTaskMutations.ts @@ -49,10 +49,10 @@ export function useTaskMutations(projectId?: string) { onSuccess: (createdTask: TaskWithAttemptStatus) => { invalidateQueries(); // Invalidate parent's relationships cache if this is a subtask - if ((createdTask as any).parent_task_attempt) { + if (createdTask.parent_task_attempt) { queryClient.invalidateQueries({ queryKey: taskRelationshipsKeys.byAttempt( - (createdTask as any).parent_task_attempt + createdTask.parent_task_attempt ), }); } diff --git a/frontend/src/lib/mcp-strategies.ts b/frontend/src/lib/mcp-strategies.ts index 21d3a022..fc3d1b6b 100644 --- a/frontend/src/lib/mcp-strategies.ts +++ b/frontend/src/lib/mcp-strategies.ts @@ -1,17 +1,26 @@ -import { McpConfig } from 'shared/types'; +import type { McpConfig, JsonValue } from 'shared/types'; + +type JsonObject = Record; + +function isJsonObject(v: unknown): v is JsonObject { + return typeof v === 'object' && v !== null && !Array.isArray(v); +} export class McpConfigStrategyGeneral { - static createFullConfig(cfg: McpConfig): Record { - // create a template with servers filled in at cfg.servers - const fullConfig = JSON.parse(JSON.stringify(cfg.template)); - let current = fullConfig; + static createFullConfig(cfg: McpConfig): JsonObject { + const cloned: JsonValue = JSON.parse(JSON.stringify(cfg.template ?? {})); + const fullConfig: JsonObject = isJsonObject(cloned) ? cloned : {}; + let current: JsonObject = fullConfig; + for (let i = 0; i < cfg.servers_path.length - 1; i++) { const key = cfg.servers_path[i]; - if (!current[key]) { - current[key] = {}; - } - current = current[key]; + const next = isJsonObject(current[key]) + ? (current[key] as JsonObject) + : undefined; + if (!next) current[key] = {}; + current = current[key] as JsonObject; } + if (cfg.servers_path.length > 0) { const lastKey = cfg.servers_path[cfg.servers_path.length - 1]; current[lastKey] = cfg.servers; @@ -20,63 +29,83 @@ export class McpConfigStrategyGeneral { } static validateFullConfig( mcp_config: McpConfig, - full_config: Record + full_config: JsonValue ): void { - // Validate using the schema path - let current = full_config; + let current: JsonValue = full_config; for (const key of mcp_config.servers_path) { - current = current?.[key]; + if (!isJsonObject(current)) { + throw new Error( + `Expected object at path: ${mcp_config.servers_path.join('.')}` + ); + } + current = current[key]; if (current === undefined) { throw new Error( `Missing required field at path: ${mcp_config.servers_path.join('.')}` ); } } - if (typeof current !== 'object') { + if (!isJsonObject(current)) { throw new Error('Servers configuration must be an object'); } } static extractServersForApi( mcp_config: McpConfig, - full_config: Record - ): Record { - // Extract the servers object based on the path - let current = full_config; + full_config: JsonValue + ): JsonObject { + let current: JsonValue = full_config; for (const key of mcp_config.servers_path) { - current = current?.[key]; + if (!isJsonObject(current)) { + throw new Error( + `Expected object at path: ${mcp_config.servers_path.join('.')}` + ); + } + current = current[key]; if (current === undefined) { throw new Error( `Missing required field at path: ${mcp_config.servers_path.join('.')}` ); } } + if (!isJsonObject(current)) { + throw new Error('Servers configuration must be an object'); + } return current; } static addPreconfiguredToConfig( mcp_config: McpConfig, - existingConfig: Record, + existingConfig: JsonValue, serverKey: string - ): Record { - const preconf = mcp_config.preconfigured as Record; - if (!preconf || typeof preconf !== 'object' || !(serverKey in preconf)) { + ): JsonObject { + const preconfVal = mcp_config.preconfigured; + if (!isJsonObject(preconfVal) || !(serverKey in preconfVal)) { throw new Error(`Unknown preconfigured server '${serverKey}'`); } - const updated = JSON.parse(JSON.stringify(existingConfig || {})); - let current = updated; + const updatedVal: JsonValue = JSON.parse( + JSON.stringify(existingConfig ?? {}) + ); + const updated: JsonObject = isJsonObject(updatedVal) ? updatedVal : {}; + let current: JsonObject = updated; for (let i = 0; i < mcp_config.servers_path.length - 1; i++) { const key = mcp_config.servers_path[i]; - if (!current[key] || typeof current[key] !== 'object') current[key] = {}; - current = current[key]; + const next = isJsonObject(current[key]) + ? (current[key] as JsonObject) + : undefined; + if (!next) current[key] = {}; + current = current[key] as JsonObject; + } + + if (mcp_config.servers_path.length === 0) { + current[serverKey] = preconfVal[serverKey]; + return updated; } const lastKey = mcp_config.servers_path[mcp_config.servers_path.length - 1]; - if (!current[lastKey] || typeof current[lastKey] !== 'object') - current[lastKey] = {}; - - current[lastKey][serverKey] = preconf[serverKey]; + if (!isJsonObject(current[lastKey])) current[lastKey] = {}; + (current[lastKey] as JsonObject)[serverKey] = preconfVal[serverKey]; return updated; } diff --git a/frontend/src/pages/project-tasks.tsx b/frontend/src/pages/project-tasks.tsx index a5467105..dca51cb1 100644 --- a/frontend/src/pages/project-tasks.tsx +++ b/frontend/src/pages/project-tasks.tsx @@ -6,7 +6,7 @@ import { Card, CardContent } from '@/components/ui/card'; import { AlertTriangle, Plus, X } from 'lucide-react'; import { Loader } from '@/components/ui/loader'; import { tasksApi } from '@/lib/api'; -import type { GitBranch } from 'shared/types'; +import type { GitBranch, TaskAttempt, BranchStatus } from 'shared/types'; import { openTaskForm } from '@/lib/openTaskForm'; import { FeatureShowcaseModal } from '@/components/showcase/FeatureShowcaseModal'; import { showcases } from '@/config/showcases'; @@ -106,10 +106,10 @@ function DiffsPanelContainer({ branchStatus, branches, }: { - attempt: any; - selectedTask: any; + attempt: TaskAttempt | null; + selectedTask: TaskWithAttemptStatus | null; projectId: string; - branchStatus: any; + branchStatus: BranchStatus | null; branches: GitBranch[]; }) { const { isAttemptRunning } = useAttemptExecution(attempt?.id); @@ -994,7 +994,7 @@ export function ProjectTasks() { attempt={attempt} selectedTask={selectedTask} projectId={projectId!} - branchStatus={branchStatus} + branchStatus={branchStatus ?? null} branches={branches} /> )} diff --git a/frontend/src/pages/settings/AgentSettings.tsx b/frontend/src/pages/settings/AgentSettings.tsx index 889aaf02..b3acbdc0 100644 --- a/frontend/src/pages/settings/AgentSettings.tsx +++ b/frontend/src/pages/settings/AgentSettings.tsx @@ -26,6 +26,9 @@ import { useProfiles } from '@/hooks/useProfiles'; import { useUserSystem } from '@/components/config-provider'; import { CreateConfigurationDialog } from '@/components/dialogs/settings/CreateConfigurationDialog'; import { DeleteConfigurationDialog } from '@/components/dialogs/settings/DeleteConfigurationDialog'; +import type { BaseCodingAgent, ExecutorConfigs } from 'shared/types'; + +type ExecutorsMap = Record>>; export function AgentSettings() { const { t } = useTranslation('settings'); @@ -49,10 +52,11 @@ export function AgentSettings() { // Form-based editor state const [useFormEditor, setUseFormEditor] = useState(true); const [selectedExecutorType, setSelectedExecutorType] = - useState('CLAUDE_CODE'); + useState('CLAUDE_CODE' as BaseCodingAgent); const [selectedConfiguration, setSelectedConfiguration] = useState('DEFAULT'); - const [localParsedProfiles, setLocalParsedProfiles] = useState(null); + const [localParsedProfiles, setLocalParsedProfiles] = + useState(null); const [isDirty, setIsDirty] = useState(false); // Sync server state to local state when not dirty @@ -77,7 +81,7 @@ export function AgentSettings() { // Mark profiles as dirty const markDirty = (nextProfiles: unknown) => { - setLocalParsedProfiles(nextProfiles); + setLocalParsedProfiles(nextProfiles as ExecutorConfigs); syncRawProfiles(nextProfiles); setIsDirty(true); }; @@ -112,10 +116,11 @@ export function AgentSettings() { ) => { if (!localParsedProfiles || !localParsedProfiles.executors) return; + const executorsMap = + localParsedProfiles.executors as unknown as ExecutorsMap; const base = - baseConfig && - localParsedProfiles.executors[executorType]?.[baseConfig]?.[executorType] - ? localParsedProfiles.executors[executorType][baseConfig][executorType] + baseConfig && executorsMap[executorType]?.[baseConfig]?.[executorType] + ? executorsMap[executorType][baseConfig][executorType] : {}; const updatedProfiles = { @@ -123,7 +128,7 @@ export function AgentSettings() { executors: { ...localParsedProfiles.executors, [executorType]: { - ...localParsedProfiles.executors[executorType], + ...executorsMap[executorType], [configName]: { [executorType]: base, }, @@ -190,9 +195,10 @@ export function AgentSettings() { }, }; + const executorsMap = updatedProfiles.executors as unknown as ExecutorsMap; // If no configurations left, create a blank DEFAULT (should not happen due to check above) if (Object.keys(remainingConfigs).length === 0) { - updatedProfiles.executors[selectedExecutorType] = { + executorsMap[selectedExecutorType] = { DEFAULT: { [selectedExecutorType]: {} }, }; } @@ -208,7 +214,7 @@ export function AgentSettings() { // Select the next available configuration const nextConfigs = Object.keys( - updatedProfiles.executors[selectedExecutorType] + executorsMap[selectedExecutorType] || {} ); const nextSelected = nextConfigs[0] || 'DEFAULT'; setSelectedConfiguration(nextSelected); @@ -279,13 +285,15 @@ export function AgentSettings() { ) => { if (!localParsedProfiles || !localParsedProfiles.executors) return; + const executorsMap = + localParsedProfiles.executors as unknown as ExecutorsMap; // Update the parsed profiles with the new config const updatedProfiles = { ...localParsedProfiles, executors: { ...localParsedProfiles.executors, [executorType]: { - ...localParsedProfiles.executors[executorType], + ...executorsMap[executorType], [configuration]: { [executorType]: formData, }, @@ -406,7 +414,7 @@ export function AgentSettings() {