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:
committed by
GitHub
parent
b04672d776
commit
81b92c6ec3
@@ -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(),
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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}`;
|
||||||
|
|||||||
@@ -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')
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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?.();
|
||||||
|
|||||||
@@ -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' &&
|
||||||
|
|||||||
@@ -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
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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, };
|
||||||
|
|||||||
Reference in New Issue
Block a user