fix: Address code review feedback and fix lint errors

This commit is contained in:
gsxdsm
2026-02-17 00:13:38 -08:00
parent b9653d6338
commit a09a2c76ae
15 changed files with 183 additions and 165 deletions

View File

@@ -0,0 +1,74 @@
import { defineConfig, globalIgnores } from 'eslint/config';
import js from '@eslint/js';
import ts from '@typescript-eslint/eslint-plugin';
import tsParser from '@typescript-eslint/parser';
const eslintConfig = defineConfig([
js.configs.recommended,
{
files: ['**/*.ts'],
languageOptions: {
parser: tsParser,
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
},
globals: {
// Node.js globals
console: 'readonly',
process: 'readonly',
Buffer: 'readonly',
__dirname: 'readonly',
__filename: 'readonly',
URL: 'readonly',
URLSearchParams: 'readonly',
AbortController: 'readonly',
AbortSignal: 'readonly',
fetch: 'readonly',
Response: 'readonly',
Request: 'readonly',
Headers: 'readonly',
FormData: 'readonly',
RequestInit: 'readonly',
// Timers
setTimeout: 'readonly',
setInterval: 'readonly',
clearTimeout: 'readonly',
clearInterval: 'readonly',
setImmediate: 'readonly',
clearImmediate: 'readonly',
queueMicrotask: 'readonly',
// Node.js types
NodeJS: 'readonly',
},
},
plugins: {
'@typescript-eslint': ts,
},
rules: {
...ts.configs.recommended.rules,
'@typescript-eslint/no-unused-vars': [
'warn',
{
argsIgnorePattern: '^_',
varsIgnorePattern: '^_',
caughtErrorsIgnorePattern: '^_',
ignoreRestSiblings: true,
},
],
'@typescript-eslint/no-explicit-any': 'warn',
// Server code frequently works with terminal output containing ANSI escape codes
'no-control-regex': 'off',
'@typescript-eslint/ban-ts-comment': [
'error',
{
'ts-nocheck': 'allow-with-description',
minimumDescriptionLength: 10,
},
],
},
},
globalIgnores(['dist/**', 'node_modules/**']),
]);
export default eslintConfig;

View File

@@ -37,7 +37,7 @@ export function createCheckoutBranchHandler() {
} }
// Validate branch name (basic validation) // Validate branch name (basic validation)
const invalidChars = /[\s~^:?*\[\\]/; const invalidChars = /[\s~^:?*[\\]/;
if (invalidChars.test(branchName)) { if (invalidChars.test(branchName)) {
res.status(400).json({ res.status(400).json({
success: false, success: false,

View File

@@ -125,19 +125,14 @@ export function createOpenInEditorHandler() {
`Failed to open in editor, falling back to file manager: ${getErrorMessage(editorError)}` `Failed to open in editor, falling back to file manager: ${getErrorMessage(editorError)}`
); );
try { const result = await openInFileManager(worktreePath);
const result = await openInFileManager(worktreePath); res.json({
res.json({ success: true,
success: true, result: {
result: { message: `Opened ${worktreePath} in ${result.editorName}`,
message: `Opened ${worktreePath} in ${result.editorName}`, editorName: result.editorName,
editorName: result.editorName, },
}, });
});
} catch (fallbackError) {
// Both editor and file manager failed
throw fallbackError;
}
} }
} catch (error) { } catch (error) {
logError(error, 'Open in editor failed'); logError(error, 'Open in editor failed');

View File

@@ -662,7 +662,7 @@ export class ClaudeUsageService {
resetTime = this.parseResetTime(resetText, type); resetTime = this.parseResetTime(resetText, type);
// Strip timezone like "(Asia/Dubai)" from the display text // Strip timezone like "(Asia/Dubai)" from the display text
resetText = resetText.replace(/\s*\([A-Za-z_\/]+\)\s*$/, '').trim(); resetText = resetText.replace(/\s*\([A-Za-z_/]+\)\s*$/, '').trim();
} }
return { percentage: percentage ?? 0, resetTime, resetText }; return { percentage: percentage ?? 0, resetTime, resetText };

View File

@@ -124,7 +124,7 @@ class DevServerService {
/(?:Local|Network):\s+(https?:\/\/[^\s]+)/i, // Vite format /(?:Local|Network):\s+(https?:\/\/[^\s]+)/i, // Vite format
/(?:ready|started server).*?(?:url:\s*)?(https?:\/\/[^\s,]+)/i, // Next.js format /(?:ready|started server).*?(?:url:\s*)?(https?:\/\/[^\s,]+)/i, // Next.js format
/(https?:\/\/(?:localhost|127\.0\.0\.1|\[::\]):\d+)/i, // Generic localhost URL /(https?:\/\/(?:localhost|127\.0\.0\.1|\[::\]):\d+)/i, // Generic localhost URL
/(https?:\/\/[^\s<>"{}|\\^`\[\]]+)/i, // Any HTTP(S) URL /(https?:\/\/[^\s<>"{}|\\^`[\]]+)/i, // Any HTTP(S) URL
]; ];
for (const pattern of urlPatterns) { for (const pattern of urlPatterns) {

View File

@@ -888,7 +888,7 @@ ${contextSection}${existingWorkSection}`;
for (const line of lines) { for (const line of lines) {
// Check for numbered items or markdown headers // Check for numbered items or markdown headers
const titleMatch = line.match(/^(?:\d+[\.\)]\s*\*{0,2}|#{1,3}\s+)(.+)/); const titleMatch = line.match(/^(?:\d+[.)]\s*\*{0,2}|#{1,3}\s+)(.+)/);
if (titleMatch) { if (titleMatch) {
// Save previous suggestion // Save previous suggestion

View File

@@ -119,7 +119,15 @@ const eslintConfig = defineConfig([
}, },
rules: { rules: {
...ts.configs.recommended.rules, ...ts.configs.recommended.rules,
'@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_' }], '@typescript-eslint/no-unused-vars': [
'warn',
{
argsIgnorePattern: '^_',
varsIgnorePattern: '^_',
caughtErrorsIgnorePattern: '^_',
ignoreRestSiblings: true,
},
],
'@typescript-eslint/no-explicit-any': 'warn', '@typescript-eslint/no-explicit-any': 'warn',
'@typescript-eslint/ban-ts-comment': [ '@typescript-eslint/ban-ts-comment': [
'error', 'error',

View File

@@ -1545,7 +1545,8 @@ export function BoardView() {
setSpawnParentFeature(feature); setSpawnParentFeature(feature);
setShowAddDialog(true); setShowAddDialog(true);
}} }}
onDuplicate={handleDuplicateFeature} onDuplicate={(feature) => handleDuplicateFeature(feature, false)}
onDuplicateAsChild={(feature) => handleDuplicateFeature(feature, true)}
featuresWithContext={featuresWithContext} featuresWithContext={featuresWithContext}
runningAutoTasks={runningAutoTasksAllWorktrees} runningAutoTasks={runningAutoTasksAllWorktrees}
onArchiveAllVerified={() => setShowArchiveAllVerifiedDialog(true)} onArchiveAllVerified={() => setShowArchiveAllVerifiedDialog(true)}

View File

@@ -31,6 +31,49 @@ import { formatModelName, DEFAULT_MODEL } from '@/lib/agent-context-parser';
import { DeleteConfirmDialog } from '@/components/ui/delete-confirm-dialog'; import { DeleteConfirmDialog } from '@/components/ui/delete-confirm-dialog';
import { getProviderIconForModel } from '@/components/ui/provider-icon'; import { getProviderIconForModel } from '@/components/ui/provider-icon';
function DuplicateMenuItems({
onDuplicate,
onDuplicateAsChild,
}: {
onDuplicate?: () => void;
onDuplicateAsChild?: () => void;
}) {
if (!onDuplicate) return null;
return (
<DropdownMenuSub>
<div className="flex items-center">
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
onDuplicate();
}}
className="text-xs flex-1 pr-0 rounded-r-none"
>
<Copy className="w-3 h-3 mr-2" />
Duplicate
</DropdownMenuItem>
{onDuplicateAsChild && (
<DropdownMenuSubTrigger className="text-xs px-1 rounded-l-none border-l border-border/30 h-8" />
)}
</div>
{onDuplicateAsChild && (
<DropdownMenuSubContent>
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
onDuplicateAsChild();
}}
className="text-xs"
>
<GitFork className="w-3 h-3 mr-2" />
Duplicate as Child
</DropdownMenuItem>
</DropdownMenuSubContent>
)}
</DropdownMenuSub>
);
}
interface CardHeaderProps { interface CardHeaderProps {
feature: Feature; feature: Feature;
isDraggable: boolean; isDraggable: boolean;
@@ -122,39 +165,10 @@ export const CardHeaderSection = memo(function CardHeaderSection({
<GitFork className="w-3 h-3 mr-2" /> <GitFork className="w-3 h-3 mr-2" />
Spawn Sub-Task Spawn Sub-Task
</DropdownMenuItem> </DropdownMenuItem>
{onDuplicate && ( <DuplicateMenuItems
<DropdownMenuSub> onDuplicate={onDuplicate}
<div className="flex items-center"> onDuplicateAsChild={onDuplicateAsChild}
<DropdownMenuItem />
onClick={(e) => {
e.stopPropagation();
onDuplicate();
}}
className="text-xs flex-1 pr-0 rounded-r-none"
>
<Copy className="w-3 h-3 mr-2" />
Duplicate
</DropdownMenuItem>
{onDuplicateAsChild && (
<DropdownMenuSubTrigger className="text-xs px-1 rounded-l-none border-l border-border/30 h-8" />
)}
</div>
{onDuplicateAsChild && (
<DropdownMenuSubContent>
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
onDuplicateAsChild();
}}
className="text-xs"
>
<GitFork className="w-3 h-3 mr-2" />
Duplicate as Child
</DropdownMenuItem>
</DropdownMenuSubContent>
)}
</DropdownMenuSub>
)}
{/* Model info in dropdown */} {/* Model info in dropdown */}
{(() => { {(() => {
const ProviderIcon = getProviderIconForModel(feature.model); const ProviderIcon = getProviderIconForModel(feature.model);
@@ -217,39 +231,10 @@ export const CardHeaderSection = memo(function CardHeaderSection({
</Button> </Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-40"> <DropdownMenuContent align="end" className="w-40">
{onDuplicate && ( <DuplicateMenuItems
<DropdownMenuSub> onDuplicate={onDuplicate}
<div className="flex items-center"> onDuplicateAsChild={onDuplicateAsChild}
<DropdownMenuItem />
onClick={(e) => {
e.stopPropagation();
onDuplicate();
}}
className="text-xs flex-1 pr-0 rounded-r-none"
>
<Copy className="w-3 h-3 mr-2" />
Duplicate
</DropdownMenuItem>
{onDuplicateAsChild && (
<DropdownMenuSubTrigger className="text-xs px-1 rounded-l-none border-l border-border/30 h-8" />
)}
</div>
{onDuplicateAsChild && (
<DropdownMenuSubContent>
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
onDuplicateAsChild();
}}
className="text-xs"
>
<GitFork className="w-3 h-3 mr-2" />
Duplicate as Child
</DropdownMenuItem>
</DropdownMenuSubContent>
)}
</DropdownMenuSub>
)}
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
</div> </div>
@@ -337,39 +322,10 @@ export const CardHeaderSection = memo(function CardHeaderSection({
<GitFork className="w-3 h-3 mr-2" /> <GitFork className="w-3 h-3 mr-2" />
Spawn Sub-Task Spawn Sub-Task
</DropdownMenuItem> </DropdownMenuItem>
{onDuplicate && ( <DuplicateMenuItems
<DropdownMenuSub> onDuplicate={onDuplicate}
<div className="flex items-center"> onDuplicateAsChild={onDuplicateAsChild}
<DropdownMenuItem />
onClick={(e) => {
e.stopPropagation();
onDuplicate();
}}
className="text-xs flex-1 pr-0 rounded-r-none"
>
<Copy className="w-3 h-3 mr-2" />
Duplicate
</DropdownMenuItem>
{onDuplicateAsChild && (
<DropdownMenuSubTrigger className="text-xs px-1 rounded-l-none border-l border-border/30 h-8" />
)}
</div>
{onDuplicateAsChild && (
<DropdownMenuSubContent>
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
onDuplicateAsChild();
}}
className="text-xs"
>
<GitFork className="w-3 h-3 mr-2" />
Duplicate as Child
</DropdownMenuItem>
</DropdownMenuSubContent>
)}
</DropdownMenuSub>
)}
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
</div> </div>
@@ -440,39 +396,10 @@ export const CardHeaderSection = memo(function CardHeaderSection({
<GitFork className="w-3 h-3 mr-2" /> <GitFork className="w-3 h-3 mr-2" />
Spawn Sub-Task Spawn Sub-Task
</DropdownMenuItem> </DropdownMenuItem>
{onDuplicate && ( <DuplicateMenuItems
<DropdownMenuSub> onDuplicate={onDuplicate}
<div className="flex items-center"> onDuplicateAsChild={onDuplicateAsChild}
<DropdownMenuItem />
onClick={(e) => {
e.stopPropagation();
onDuplicate();
}}
className="text-xs flex-1 pr-0 rounded-r-none"
>
<Copy className="w-3 h-3 mr-2" />
Duplicate
</DropdownMenuItem>
{onDuplicateAsChild && (
<DropdownMenuSubTrigger className="text-xs px-1 rounded-l-none border-l border-border/30 h-8" />
)}
</div>
{onDuplicateAsChild && (
<DropdownMenuSubContent>
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
onDuplicateAsChild();
}}
className="text-xs"
>
<GitFork className="w-3 h-3 mr-2" />
Duplicate as Child
</DropdownMenuItem>
</DropdownMenuSubContent>
)}
</DropdownMenuSub>
)}
{/* Model info in dropdown */} {/* Model info in dropdown */}
{(() => { {(() => {
const ProviderIcon = getProviderIconForModel(feature.model); const ProviderIcon = getProviderIconForModel(feature.model);

View File

@@ -85,6 +85,11 @@ export function useBoardPersistence({ currentProject }: UseBoardPersistenceProps
throw new Error('Features API not available'); throw new Error('Features API not available');
} }
// Capture previous cache snapshot for synchronous rollback on error
const previousFeatures = queryClient.getQueryData<Feature[]>(
queryKeys.features.all(currentProject.path)
);
// Optimistically add to React Query cache for immediate board refresh // Optimistically add to React Query cache for immediate board refresh
queryClient.setQueryData<Feature[]>( queryClient.setQueryData<Feature[]>(
queryKeys.features.all(currentProject.path), queryKeys.features.all(currentProject.path),
@@ -95,6 +100,16 @@ export function useBoardPersistence({ currentProject }: UseBoardPersistenceProps
const result = await api.features.create(currentProject.path, feature as ApiFeature); const result = await api.features.create(currentProject.path, feature as ApiFeature);
if (result.success && result.feature) { if (result.success && result.feature) {
updateFeature(result.feature.id, result.feature as Partial<Feature>); updateFeature(result.feature.id, result.feature as Partial<Feature>);
// Update cache with server-confirmed feature before invalidating
queryClient.setQueryData<Feature[]>(
queryKeys.features.all(currentProject.path),
(features) => {
if (!features) return features;
return features.map((f) =>
f.id === result.feature!.id ? { ...f, ...(result.feature as Feature) } : f
);
}
);
} else if (!result.success) { } else if (!result.success) {
throw new Error(result.error || 'Failed to create feature on server'); throw new Error(result.error || 'Failed to create feature on server');
} }
@@ -104,7 +119,10 @@ export function useBoardPersistence({ currentProject }: UseBoardPersistenceProps
}); });
} catch (error) { } catch (error) {
logger.error('Failed to persist feature creation:', error); logger.error('Failed to persist feature creation:', error);
// Rollback optimistic update on error // Rollback optimistic update synchronously on error
if (previousFeatures) {
queryClient.setQueryData(queryKeys.features.all(currentProject.path), previousFeatures);
}
queryClient.invalidateQueries({ queryClient.invalidateQueries({
queryKey: queryKeys.features.all(currentProject.path), queryKey: queryKeys.features.all(currentProject.path),
}); });
@@ -131,7 +149,6 @@ export function useBoardPersistence({ currentProject }: UseBoardPersistenceProps
try { try {
const api = getElectronAPI(); const api = getElectronAPI();
if (!api.features) { if (!api.features) {
logger.error('Features API not available');
// Rollback optimistic deletion since we can't persist // Rollback optimistic deletion since we can't persist
if (previousFeatures) { if (previousFeatures) {
queryClient.setQueryData(queryKeys.features.all(currentProject.path), previousFeatures); queryClient.setQueryData(queryKeys.features.all(currentProject.path), previousFeatures);
@@ -139,7 +156,7 @@ export function useBoardPersistence({ currentProject }: UseBoardPersistenceProps
queryClient.invalidateQueries({ queryClient.invalidateQueries({
queryKey: queryKeys.features.all(currentProject.path), queryKey: queryKeys.features.all(currentProject.path),
}); });
return; throw new Error('Features API not available');
} }
await api.features.delete(currentProject.path, featureId); await api.features.delete(currentProject.path, featureId);

View File

@@ -46,7 +46,8 @@ interface KanbanBoardProps {
onViewPlan: (feature: Feature) => void; onViewPlan: (feature: Feature) => void;
onApprovePlan: (feature: Feature) => void; onApprovePlan: (feature: Feature) => void;
onSpawnTask?: (feature: Feature) => void; onSpawnTask?: (feature: Feature) => void;
onDuplicate?: (feature: Feature, asChild: boolean) => void; onDuplicate?: (feature: Feature) => void;
onDuplicateAsChild?: (feature: Feature) => void;
featuresWithContext: Set<string>; featuresWithContext: Set<string>;
runningAutoTasks: string[]; runningAutoTasks: string[];
onArchiveAllVerified: () => void; onArchiveAllVerified: () => void;
@@ -284,6 +285,7 @@ export function KanbanBoard({
onApprovePlan, onApprovePlan,
onSpawnTask, onSpawnTask,
onDuplicate, onDuplicate,
onDuplicateAsChild,
featuresWithContext, featuresWithContext,
runningAutoTasks, runningAutoTasks,
onArchiveAllVerified, onArchiveAllVerified,
@@ -571,8 +573,8 @@ export function KanbanBoard({
onViewPlan={() => onViewPlan(feature)} onViewPlan={() => onViewPlan(feature)}
onApprovePlan={() => onApprovePlan(feature)} onApprovePlan={() => onApprovePlan(feature)}
onSpawnTask={() => onSpawnTask?.(feature)} onSpawnTask={() => onSpawnTask?.(feature)}
onDuplicate={() => onDuplicate?.(feature, false)} onDuplicate={() => onDuplicate?.(feature)}
onDuplicateAsChild={() => onDuplicate?.(feature, true)} onDuplicateAsChild={() => onDuplicateAsChild?.(feature)}
hasContext={featuresWithContext.has(feature.id)} hasContext={featuresWithContext.has(feature.id)}
isCurrentAutoTask={runningAutoTasks.includes(feature.id)} isCurrentAutoTask={runningAutoTasks.includes(feature.id)}
shortcutKey={shortcutKey} shortcutKey={shortcutKey}
@@ -615,8 +617,8 @@ export function KanbanBoard({
onViewPlan={() => onViewPlan(feature)} onViewPlan={() => onViewPlan(feature)}
onApprovePlan={() => onApprovePlan(feature)} onApprovePlan={() => onApprovePlan(feature)}
onSpawnTask={() => onSpawnTask?.(feature)} onSpawnTask={() => onSpawnTask?.(feature)}
onDuplicate={() => onDuplicate?.(feature, false)} onDuplicate={() => onDuplicate?.(feature)}
onDuplicateAsChild={() => onDuplicate?.(feature, true)} onDuplicateAsChild={() => onDuplicateAsChild?.(feature)}
hasContext={featuresWithContext.has(feature.id)} hasContext={featuresWithContext.has(feature.id)}
isCurrentAutoTask={runningAutoTasks.includes(feature.id)} isCurrentAutoTask={runningAutoTasks.includes(feature.id)}
shortcutKey={shortcutKey} shortcutKey={shortcutKey}

View File

@@ -32,7 +32,6 @@ function featureToInternal(feature: Feature): FeatureWithId {
} }
function internalToFeature(internal: FeatureWithId): Feature { function internalToFeature(internal: FeatureWithId): Feature {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { _id, _locationIds, ...feature } = internal; const { _id, _locationIds, ...feature } = internal;
return feature; return feature;
} }

View File

@@ -27,7 +27,6 @@ function phaseToInternal(phase: RoadmapPhase): PhaseWithId {
} }
function internalToPhase(internal: PhaseWithId): RoadmapPhase { function internalToPhase(internal: PhaseWithId): RoadmapPhase {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { _id, ...phase } = internal; const { _id, ...phase } = internal;
return phase; return phase;
} }

View File

@@ -1062,7 +1062,6 @@ if (typeof window !== 'undefined') {
} }
// Mock API for development/fallback when no backend is available // Mock API for development/fallback when no backend is available
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const _getMockElectronAPI = (): ElectronAPI => { const _getMockElectronAPI = (): ElectronAPI => {
return { return {
ping: async () => 'pong (mock)', ping: async () => 'pong (mock)',

View File

@@ -155,7 +155,6 @@ export const useTestRunnersStore = create<TestRunnersState & TestRunnersActions>
const finishedAt = new Date().toISOString(); const finishedAt = new Date().toISOString();
// Remove from active sessions since it's no longer running // Remove from active sessions since it's no longer running
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { [session.worktreePath]: _, ...remainingActive } = state.activeSessionByWorktree; const { [session.worktreePath]: _, ...remainingActive } = state.activeSessionByWorktree;
return { return {
@@ -202,7 +201,6 @@ export const useTestRunnersStore = create<TestRunnersState & TestRunnersActions>
const session = state.sessions[sessionId]; const session = state.sessions[sessionId];
if (!session) return state; if (!session) return state;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { [sessionId]: _, ...remainingSessions } = state.sessions; const { [sessionId]: _, ...remainingSessions } = state.sessions;
// Remove from active if this was the active session // Remove from active if this was the active session
@@ -231,7 +229,6 @@ export const useTestRunnersStore = create<TestRunnersState & TestRunnersActions>
}); });
// Remove from active // Remove from active
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { [worktreePath]: _, ...remainingActive } = state.activeSessionByWorktree; const { [worktreePath]: _, ...remainingActive } = state.activeSessionByWorktree;
return { return {