Resolve frontend warnings final (#1319)

* ##  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<string, JsonValue>;
```

### **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!**
This commit is contained in:
Louis Knight-Webb
2025-11-18 16:22:06 +00:00
committed by GitHub
parent b04672d776
commit 81b92c6ec3
13 changed files with 146 additions and 93 deletions

View File

@@ -105,6 +105,7 @@ fn generate_types_content() -> String {
executors::command::CommandBuilder::decl(), executors::command::CommandBuilder::decl(),
executors::profile::ExecutorProfileId::decl(), executors::profile::ExecutorProfileId::decl(),
executors::profile::ExecutorConfig::decl(), executors::profile::ExecutorConfig::decl(),
executors::profile::ExecutorConfigs::decl(),
executors::executors::BaseAgentCapability::decl(), executors::executors::BaseAgentCapability::decl(),
executors::executors::claude::ClaudeCode::decl(), executors::executors::claude::ClaudeCode::decl(),
executors::executors::gemini::Gemini::decl(), executors::executors::gemini::Gemini::decl(),

View File

@@ -37,7 +37,8 @@ const TaskPanel = ({ task }: TaskPanelProps) => {
const absSec = Math.round(Math.abs(diffMs) / 1000); const absSec = Math.round(Math.abs(diffMs) / 1000);
const rtf = const rtf =
typeof Intl !== 'undefined' && (Intl as any).RelativeTimeFormat typeof Intl !== 'undefined' &&
typeof Intl.RelativeTimeFormat === 'function'
? new Intl.RelativeTimeFormat(undefined, { numeric: 'auto' }) ? new Intl.RelativeTimeFormat(undefined, { numeric: 'auto' })
: null; : null;

View File

@@ -18,13 +18,15 @@ export type Props = Readonly<{
const MAX_VISIBLE_ELEMENTS = 5; const MAX_VISIBLE_ELEMENTS = 5;
const MAX_BADGES = 6; const MAX_BADGES = 6;
type ComponentInfo = ClickedEntry['payload']['components'][number];
// Build component chain from inner-most to outer-most for banner display // Build component chain from inner-most to outer-most for banner display
function buildChainInnerToOuterForBanner(entry: ClickedEntry) { function buildChainInnerToOuterForBanner(entry: ClickedEntry) {
const comps = entry.payload.components ?? []; const comps: ComponentInfo[] = entry.payload.components ?? [];
const s = entry.payload.selected; const s: ComponentInfo = entry.payload.selected;
// Start with selected as innermost, cast to ComponentInfo for uniform handling // Start with selected as innermost
const innerToOuter = [s as any]; const innerToOuter = [s];
// Add components that aren't duplicates // Add components that aren't duplicates
const selectedKey = `${s.name}|${s.pathToSource}|${s.source?.lineNumber}|${s.source?.columnNumber}`; const selectedKey = `${s.name}|${s.pathToSource}|${s.source?.lineNumber}|${s.source?.columnNumber}`;

View File

@@ -18,6 +18,7 @@ import {
import { useMemo, useState } from 'react'; import { useMemo, useState } from 'react';
import type { import type {
BranchStatus, BranchStatus,
Merge,
GitBranch, GitBranch,
TaskAttempt, TaskAttempt,
TaskWithAttemptStatus, TaskWithAttemptStatus,
@@ -102,15 +103,15 @@ function GitOperations({
}; };
const openPR = branchStatus.merges.find( 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( 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( const merges = branchStatus.merges.filter(
(m: any) => (m: Merge) =>
m.type === 'direct' || m.type === 'direct' ||
(m.type === 'pr' && m.pr_info.status === 'merged') (m.type === 'pr' && m.pr_info.status === 'merged')
); );

View File

@@ -8,6 +8,7 @@ import {
$convertToMarkdownString, $convertToMarkdownString,
$convertFromMarkdownString, $convertFromMarkdownString,
TRANSFORMERS, TRANSFORMERS,
type Transformer,
} from '@lexical/markdown'; } from '@lexical/markdown';
import { ImageChipNode, InsertImageChipPlugin } from './wysiwyg/ImageChipNode'; import { ImageChipNode, InsertImageChipPlugin } from './wysiwyg/ImageChipNode';
import { LexicalErrorBoundary } from '@lexical/react/LexicalErrorBoundary'; import { LexicalErrorBoundary } from '@lexical/react/LexicalErrorBoundary';
@@ -62,11 +63,11 @@ export default function WYSIWYGEditor({
// Shared ref to avoid update loops and redundant imports // Shared ref to avoid update loops and redundant imports
const lastMdRef = useRef<string>(''); const lastMdRef = useRef<string>('');
const exportTransformers = useMemo( const exportTransformers: Transformer[] = useMemo(
() => [...TRANSFORMERS, IMAGE_CHIP_EXPORT], () => [...TRANSFORMERS, IMAGE_CHIP_EXPORT],
[] []
); );
const importTransformers = useMemo( const importTransformers: Transformer[] = useMemo(
() => [...TRANSFORMERS, IMAGE_CHIP_IMPORT], () => [...TRANSFORMERS, IMAGE_CHIP_IMPORT],
[] []
); );
@@ -132,7 +133,7 @@ function MarkdownOnChangePlugin({
}: { }: {
onMarkdownChange?: (md: string) => void; onMarkdownChange?: (md: string) => void;
onEditorStateChange?: (s: EditorState) => void; onEditorStateChange?: (s: EditorState) => void;
exportTransformers: any[]; exportTransformers: Transformer[];
lastMdRef: React.MutableRefObject<string>; lastMdRef: React.MutableRefObject<string>;
}) { }) {
const [editor] = useLexicalComposerContext(); const [editor] = useLexicalComposerContext();
@@ -165,7 +166,7 @@ function MarkdownValuePlugin({
lastMdRef, lastMdRef,
}: { }: {
value?: string; value?: string;
importTransformers: any[]; importTransformers: Transformer[];
lastMdRef: React.MutableRefObject<string>; lastMdRef: React.MutableRefObject<string>;
}) { }) {
const [editor] = useLexicalComposerContext(); const [editor] = useLexicalComposerContext();
@@ -188,7 +189,7 @@ function MarkdownDefaultValuePlugin({
lastMdRef, lastMdRef,
}: { }: {
defaultValue: string; defaultValue: string;
importTransformers: any[]; importTransformers: Transformer[];
lastMdRef: React.MutableRefObject<string>; lastMdRef: React.MutableRefObject<string>;
}) { }) {
const [editor] = useLexicalComposerContext(); const [editor] = useLexicalComposerContext();

View File

@@ -88,10 +88,10 @@ export class ImageChipNode extends DecoratorNode<JSX.Element> {
return false; return false;
} }
static importJSON(json: any): ImageChipNode { static importJSON(json: unknown): ImageChipNode {
return new ImageChipNode(json); return new ImageChipNode(json as ImageChipPayload);
} }
exportJSON(): any { exportJSON(): ImageChipPayload & { type: string; version: number } {
return { return {
type: 'image-chip', type: 'image-chip',
version: 1, version: 1,

View File

@@ -1,6 +1,6 @@
import { useCallback, useState } from 'react'; import { useCallback, useState } from 'react';
import { attemptsApi } from '@/lib/api'; import { attemptsApi } from '@/lib/api';
import type { ImageResponse } from 'shared/types'; import type { ImageResponse, CreateFollowUpAttempt } from 'shared/types';
type Args = { type Args = {
attemptId?: string; attemptId?: string;
@@ -57,14 +57,15 @@ export function useFollowUpSend({
: images.length > 0 : images.length > 0
? images.map((img) => img.id) ? images.map((img) => img.id)
: null; : null;
await attemptsApi.followUp(attemptId, { const body: CreateFollowUpAttempt = {
prompt: finalPrompt, prompt: finalPrompt,
variant: selectedVariant, variant: selectedVariant,
image_ids, image_ids,
retry_process_id: null, retry_process_id: null,
force_when_dirty: null, force_when_dirty: null,
perform_git_reset: null, perform_git_reset: null,
} as any); };
await attemptsApi.followUp(attemptId, body);
setMessage(''); setMessage('');
clearComments(); clearComments();
clearClickedElements?.(); clearClickedElements?.();

View File

@@ -1,5 +1,5 @@
import { useMemo } from 'react'; import { useMemo } from 'react';
import type { TodoItem } from 'shared/types'; import type { TodoItem, NormalizedEntry } from 'shared/types';
import type { PatchTypeWithKey } from '@/hooks/useConversationHistory'; import type { PatchTypeWithKey } from '@/hooks/useConversationHistory';
interface UsePinnedTodosResult { interface UsePinnedTodosResult {
@@ -20,7 +20,7 @@ export const usePinnedTodos = (
for (const entry of entries) { for (const entry of entries) {
if (entry.type === 'NORMALIZED_ENTRY' && entry.content) { if (entry.type === 'NORMALIZED_ENTRY' && entry.content) {
const normalizedEntry = entry.content as any; const normalizedEntry = entry.content as NormalizedEntry;
if ( if (
normalizedEntry.entry_type?.type === 'tool_use' && normalizedEntry.entry_type?.type === 'tool_use' &&

View File

@@ -49,10 +49,10 @@ export function useTaskMutations(projectId?: string) {
onSuccess: (createdTask: TaskWithAttemptStatus) => { onSuccess: (createdTask: TaskWithAttemptStatus) => {
invalidateQueries(); invalidateQueries();
// Invalidate parent's relationships cache if this is a subtask // Invalidate parent's relationships cache if this is a subtask
if ((createdTask as any).parent_task_attempt) { if (createdTask.parent_task_attempt) {
queryClient.invalidateQueries({ queryClient.invalidateQueries({
queryKey: taskRelationshipsKeys.byAttempt( queryKey: taskRelationshipsKeys.byAttempt(
(createdTask as any).parent_task_attempt createdTask.parent_task_attempt
), ),
}); });
} }

View File

@@ -1,17 +1,26 @@
import { McpConfig } from 'shared/types'; import type { McpConfig, JsonValue } from 'shared/types';
type JsonObject = Record<string, JsonValue>;
function isJsonObject(v: unknown): v is JsonObject {
return typeof v === 'object' && v !== null && !Array.isArray(v);
}
export class McpConfigStrategyGeneral { export class McpConfigStrategyGeneral {
static createFullConfig(cfg: McpConfig): Record<string, any> { static createFullConfig(cfg: McpConfig): JsonObject {
// create a template with servers filled in at cfg.servers const cloned: JsonValue = JSON.parse(JSON.stringify(cfg.template ?? {}));
const fullConfig = JSON.parse(JSON.stringify(cfg.template)); const fullConfig: JsonObject = isJsonObject(cloned) ? cloned : {};
let current = fullConfig; let current: JsonObject = fullConfig;
for (let i = 0; i < cfg.servers_path.length - 1; i++) { for (let i = 0; i < cfg.servers_path.length - 1; i++) {
const key = cfg.servers_path[i]; const key = cfg.servers_path[i];
if (!current[key]) { const next = isJsonObject(current[key])
current[key] = {}; ? (current[key] as JsonObject)
} : undefined;
current = current[key]; if (!next) current[key] = {};
current = current[key] as JsonObject;
} }
if (cfg.servers_path.length > 0) { if (cfg.servers_path.length > 0) {
const lastKey = cfg.servers_path[cfg.servers_path.length - 1]; const lastKey = cfg.servers_path[cfg.servers_path.length - 1];
current[lastKey] = cfg.servers; current[lastKey] = cfg.servers;
@@ -20,63 +29,83 @@ export class McpConfigStrategyGeneral {
} }
static validateFullConfig( static validateFullConfig(
mcp_config: McpConfig, mcp_config: McpConfig,
full_config: Record<string, any> full_config: JsonValue
): void { ): void {
// Validate using the schema path let current: JsonValue = full_config;
let current = full_config;
for (const key of mcp_config.servers_path) { 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) { if (current === undefined) {
throw new Error( throw new Error(
`Missing required field at path: ${mcp_config.servers_path.join('.')}` `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'); throw new Error('Servers configuration must be an object');
} }
} }
static extractServersForApi( static extractServersForApi(
mcp_config: McpConfig, mcp_config: McpConfig,
full_config: Record<string, any> full_config: JsonValue
): Record<string, any> { ): JsonObject {
// Extract the servers object based on the path let current: JsonValue = full_config;
let current = full_config;
for (const key of mcp_config.servers_path) { 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) { if (current === undefined) {
throw new Error( throw new Error(
`Missing required field at path: ${mcp_config.servers_path.join('.')}` `Missing required field at path: ${mcp_config.servers_path.join('.')}`
); );
} }
} }
if (!isJsonObject(current)) {
throw new Error('Servers configuration must be an object');
}
return current; return current;
} }
static addPreconfiguredToConfig( static addPreconfiguredToConfig(
mcp_config: McpConfig, mcp_config: McpConfig,
existingConfig: Record<string, any>, existingConfig: JsonValue,
serverKey: string serverKey: string
): Record<string, any> { ): JsonObject {
const preconf = mcp_config.preconfigured as Record<string, any>; const preconfVal = mcp_config.preconfigured;
if (!preconf || typeof preconf !== 'object' || !(serverKey in preconf)) { if (!isJsonObject(preconfVal) || !(serverKey in preconfVal)) {
throw new Error(`Unknown preconfigured server '${serverKey}'`); throw new Error(`Unknown preconfigured server '${serverKey}'`);
} }
const updated = JSON.parse(JSON.stringify(existingConfig || {})); const updatedVal: JsonValue = JSON.parse(
let current = updated; 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++) { for (let i = 0; i < mcp_config.servers_path.length - 1; i++) {
const key = mcp_config.servers_path[i]; const key = mcp_config.servers_path[i];
if (!current[key] || typeof current[key] !== 'object') current[key] = {}; const next = isJsonObject(current[key])
current = 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]; const lastKey = mcp_config.servers_path[mcp_config.servers_path.length - 1];
if (!current[lastKey] || typeof current[lastKey] !== 'object') if (!isJsonObject(current[lastKey])) current[lastKey] = {};
current[lastKey] = {}; (current[lastKey] as JsonObject)[serverKey] = preconfVal[serverKey];
current[lastKey][serverKey] = preconf[serverKey];
return updated; return updated;
} }

View File

@@ -6,7 +6,7 @@ import { Card, CardContent } from '@/components/ui/card';
import { AlertTriangle, Plus, X } from 'lucide-react'; import { AlertTriangle, Plus, X } from 'lucide-react';
import { Loader } from '@/components/ui/loader'; import { Loader } from '@/components/ui/loader';
import { tasksApi } from '@/lib/api'; 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 { openTaskForm } from '@/lib/openTaskForm';
import { FeatureShowcaseModal } from '@/components/showcase/FeatureShowcaseModal'; import { FeatureShowcaseModal } from '@/components/showcase/FeatureShowcaseModal';
import { showcases } from '@/config/showcases'; import { showcases } from '@/config/showcases';
@@ -106,10 +106,10 @@ function DiffsPanelContainer({
branchStatus, branchStatus,
branches, branches,
}: { }: {
attempt: any; attempt: TaskAttempt | null;
selectedTask: any; selectedTask: TaskWithAttemptStatus | null;
projectId: string; projectId: string;
branchStatus: any; branchStatus: BranchStatus | null;
branches: GitBranch[]; branches: GitBranch[];
}) { }) {
const { isAttemptRunning } = useAttemptExecution(attempt?.id); const { isAttemptRunning } = useAttemptExecution(attempt?.id);
@@ -994,7 +994,7 @@ export function ProjectTasks() {
attempt={attempt} attempt={attempt}
selectedTask={selectedTask} selectedTask={selectedTask}
projectId={projectId!} projectId={projectId!}
branchStatus={branchStatus} branchStatus={branchStatus ?? null}
branches={branches} branches={branches}
/> />
)} )}

View File

@@ -26,6 +26,9 @@ import { useProfiles } from '@/hooks/useProfiles';
import { useUserSystem } from '@/components/config-provider'; import { useUserSystem } from '@/components/config-provider';
import { CreateConfigurationDialog } from '@/components/dialogs/settings/CreateConfigurationDialog'; import { CreateConfigurationDialog } from '@/components/dialogs/settings/CreateConfigurationDialog';
import { DeleteConfigurationDialog } from '@/components/dialogs/settings/DeleteConfigurationDialog'; import { DeleteConfigurationDialog } from '@/components/dialogs/settings/DeleteConfigurationDialog';
import type { BaseCodingAgent, ExecutorConfigs } from 'shared/types';
type ExecutorsMap = Record<string, Record<string, Record<string, unknown>>>;
export function AgentSettings() { export function AgentSettings() {
const { t } = useTranslation('settings'); const { t } = useTranslation('settings');
@@ -49,10 +52,11 @@ export function AgentSettings() {
// Form-based editor state // Form-based editor state
const [useFormEditor, setUseFormEditor] = useState(true); const [useFormEditor, setUseFormEditor] = useState(true);
const [selectedExecutorType, setSelectedExecutorType] = const [selectedExecutorType, setSelectedExecutorType] =
useState<string>('CLAUDE_CODE'); useState<BaseCodingAgent>('CLAUDE_CODE' as BaseCodingAgent);
const [selectedConfiguration, setSelectedConfiguration] = const [selectedConfiguration, setSelectedConfiguration] =
useState<string>('DEFAULT'); useState<string>('DEFAULT');
const [localParsedProfiles, setLocalParsedProfiles] = useState<any>(null); const [localParsedProfiles, setLocalParsedProfiles] =
useState<ExecutorConfigs | null>(null);
const [isDirty, setIsDirty] = useState(false); const [isDirty, setIsDirty] = useState(false);
// Sync server state to local state when not dirty // Sync server state to local state when not dirty
@@ -77,7 +81,7 @@ export function AgentSettings() {
// Mark profiles as dirty // Mark profiles as dirty
const markDirty = (nextProfiles: unknown) => { const markDirty = (nextProfiles: unknown) => {
setLocalParsedProfiles(nextProfiles); setLocalParsedProfiles(nextProfiles as ExecutorConfigs);
syncRawProfiles(nextProfiles); syncRawProfiles(nextProfiles);
setIsDirty(true); setIsDirty(true);
}; };
@@ -112,10 +116,11 @@ export function AgentSettings() {
) => { ) => {
if (!localParsedProfiles || !localParsedProfiles.executors) return; if (!localParsedProfiles || !localParsedProfiles.executors) return;
const executorsMap =
localParsedProfiles.executors as unknown as ExecutorsMap;
const base = const base =
baseConfig && baseConfig && executorsMap[executorType]?.[baseConfig]?.[executorType]
localParsedProfiles.executors[executorType]?.[baseConfig]?.[executorType] ? executorsMap[executorType][baseConfig][executorType]
? localParsedProfiles.executors[executorType][baseConfig][executorType]
: {}; : {};
const updatedProfiles = { const updatedProfiles = {
@@ -123,7 +128,7 @@ export function AgentSettings() {
executors: { executors: {
...localParsedProfiles.executors, ...localParsedProfiles.executors,
[executorType]: { [executorType]: {
...localParsedProfiles.executors[executorType], ...executorsMap[executorType],
[configName]: { [configName]: {
[executorType]: base, [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 no configurations left, create a blank DEFAULT (should not happen due to check above)
if (Object.keys(remainingConfigs).length === 0) { if (Object.keys(remainingConfigs).length === 0) {
updatedProfiles.executors[selectedExecutorType] = { executorsMap[selectedExecutorType] = {
DEFAULT: { [selectedExecutorType]: {} }, DEFAULT: { [selectedExecutorType]: {} },
}; };
} }
@@ -208,7 +214,7 @@ export function AgentSettings() {
// Select the next available configuration // Select the next available configuration
const nextConfigs = Object.keys( const nextConfigs = Object.keys(
updatedProfiles.executors[selectedExecutorType] executorsMap[selectedExecutorType] || {}
); );
const nextSelected = nextConfigs[0] || 'DEFAULT'; const nextSelected = nextConfigs[0] || 'DEFAULT';
setSelectedConfiguration(nextSelected); setSelectedConfiguration(nextSelected);
@@ -279,13 +285,15 @@ export function AgentSettings() {
) => { ) => {
if (!localParsedProfiles || !localParsedProfiles.executors) return; if (!localParsedProfiles || !localParsedProfiles.executors) return;
const executorsMap =
localParsedProfiles.executors as unknown as ExecutorsMap;
// Update the parsed profiles with the new config // Update the parsed profiles with the new config
const updatedProfiles = { const updatedProfiles = {
...localParsedProfiles, ...localParsedProfiles,
executors: { executors: {
...localParsedProfiles.executors, ...localParsedProfiles.executors,
[executorType]: { [executorType]: {
...localParsedProfiles.executors[executorType], ...executorsMap[executorType],
[configuration]: { [configuration]: {
[executorType]: formData, [executorType]: formData,
}, },
@@ -406,7 +414,7 @@ export function AgentSettings() {
<Select <Select
value={selectedExecutorType} value={selectedExecutorType}
onValueChange={(value) => { onValueChange={(value) => {
setSelectedExecutorType(value); setSelectedExecutorType(value as BaseCodingAgent);
// Reset configuration selection when executor type changes // Reset configuration selection when executor type changes
setSelectedConfiguration('DEFAULT'); setSelectedConfiguration('DEFAULT');
}} }}
@@ -499,29 +507,36 @@ export function AgentSettings() {
</div> </div>
</div> </div>
{localParsedProfiles.executors[selectedExecutorType]?.[ {(() => {
selectedConfiguration const executorsMap =
]?.[selectedExecutorType] && ( localParsedProfiles.executors as unknown as ExecutorsMap;
<ExecutorConfigForm return (
executor={selectedExecutorType as any} !!executorsMap[selectedExecutorType]?.[
value={ selectedConfiguration
localParsedProfiles.executors[selectedExecutorType][ ]?.[selectedExecutorType] && (
selectedConfiguration <ExecutorConfigForm
][selectedExecutorType] || {} executor={selectedExecutorType}
} value={
onChange={(formData) => (executorsMap[selectedExecutorType][
handleExecutorConfigChange( selectedConfiguration
selectedExecutorType, ][selectedExecutorType] as Record<string, unknown>) ||
selectedConfiguration, {}
formData }
) onChange={(formData) =>
} handleExecutorConfigChange(
onSave={handleExecutorConfigSave} selectedExecutorType,
disabled={profilesSaving} selectedConfiguration,
isSaving={profilesSaving} formData
isDirty={isDirty} )
/> }
)} onSave={handleExecutorConfigSave}
disabled={profilesSaving}
isSaving={profilesSaving}
isDirty={isDirty}
/>
)
);
})()}
</div> </div>
) : ( ) : (
// Raw JSON editor // Raw JSON editor

View File

@@ -222,6 +222,8 @@ variant: string | null, };
export type ExecutorConfig = { [key in string]?: { "CLAUDE_CODE": ClaudeCode } | { "AMP": Amp } | { "GEMINI": Gemini } | { "CODEX": Codex } | { "OPENCODE": Opencode } | { "CURSOR_AGENT": CursorAgent } | { "QWEN_CODE": QwenCode } | { "COPILOT": Copilot } }; export type ExecutorConfig = { [key in string]?: { "CLAUDE_CODE": ClaudeCode } | { "AMP": Amp } | { "GEMINI": Gemini } | { "CODEX": Codex } | { "OPENCODE": Opencode } | { "CURSOR_AGENT": CursorAgent } | { "QWEN_CODE": QwenCode } | { "COPILOT": Copilot } };
export type ExecutorConfigs = { executors: { [key in BaseCodingAgent]?: ExecutorConfig }, };
export enum BaseAgentCapability { SESSION_FORK = "SESSION_FORK", SETUP_HELPER = "SETUP_HELPER" } export enum BaseAgentCapability { SESSION_FORK = "SESSION_FORK", SETUP_HELPER = "SETUP_HELPER" }
export type ClaudeCode = { append_prompt: AppendPrompt, claude_code_router?: boolean | null, plan?: boolean | null, approvals?: boolean | null, model?: string | null, dangerously_skip_permissions?: boolean | null, base_command_override?: string | null, additional_params?: Array<string> | null, }; export type ClaudeCode = { append_prompt: AppendPrompt, claude_code_router?: boolean | null, plan?: boolean | null, approvals?: boolean | null, model?: string | null, dangerously_skip_permissions?: boolean | null, base_command_override?: string | null, additional_params?: Array<string> | null, };