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)
const invalidChars = /[\s~^:?*\[\\]/;
const invalidChars = /[\s~^:?*[\\]/;
if (invalidChars.test(branchName)) {
res.status(400).json({
success: false,

View File

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

View File

@@ -662,7 +662,7 @@ export class ClaudeUsageService {
resetTime = this.parseResetTime(resetText, type);
// 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 };

View File

@@ -124,7 +124,7 @@ class DevServerService {
/(?:Local|Network):\s+(https?:\/\/[^\s]+)/i, // Vite format
/(?:ready|started server).*?(?:url:\s*)?(https?:\/\/[^\s,]+)/i, // Next.js format
/(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) {

View File

@@ -888,7 +888,7 @@ ${contextSection}${existingWorkSection}`;
for (const line of lines) {
// 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) {
// Save previous suggestion

View File

@@ -119,7 +119,15 @@ const eslintConfig = defineConfig([
},
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/ban-ts-comment': [
'error',

View File

@@ -1545,7 +1545,8 @@ export function BoardView() {
setSpawnParentFeature(feature);
setShowAddDialog(true);
}}
onDuplicate={handleDuplicateFeature}
onDuplicate={(feature) => handleDuplicateFeature(feature, false)}
onDuplicateAsChild={(feature) => handleDuplicateFeature(feature, true)}
featuresWithContext={featuresWithContext}
runningAutoTasks={runningAutoTasksAllWorktrees}
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 { 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 {
feature: Feature;
isDraggable: boolean;
@@ -122,39 +165,10 @@ export const CardHeaderSection = memo(function CardHeaderSection({
<GitFork className="w-3 h-3 mr-2" />
Spawn Sub-Task
</DropdownMenuItem>
{onDuplicate && (
<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>
)}
<DuplicateMenuItems
onDuplicate={onDuplicate}
onDuplicateAsChild={onDuplicateAsChild}
/>
{/* Model info in dropdown */}
{(() => {
const ProviderIcon = getProviderIconForModel(feature.model);
@@ -217,39 +231,10 @@ export const CardHeaderSection = memo(function CardHeaderSection({
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-40">
{onDuplicate && (
<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>
)}
<DuplicateMenuItems
onDuplicate={onDuplicate}
onDuplicateAsChild={onDuplicateAsChild}
/>
</DropdownMenuContent>
</DropdownMenu>
</div>
@@ -337,39 +322,10 @@ export const CardHeaderSection = memo(function CardHeaderSection({
<GitFork className="w-3 h-3 mr-2" />
Spawn Sub-Task
</DropdownMenuItem>
{onDuplicate && (
<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>
)}
<DuplicateMenuItems
onDuplicate={onDuplicate}
onDuplicateAsChild={onDuplicateAsChild}
/>
</DropdownMenuContent>
</DropdownMenu>
</div>
@@ -440,39 +396,10 @@ export const CardHeaderSection = memo(function CardHeaderSection({
<GitFork className="w-3 h-3 mr-2" />
Spawn Sub-Task
</DropdownMenuItem>
{onDuplicate && (
<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>
)}
<DuplicateMenuItems
onDuplicate={onDuplicate}
onDuplicateAsChild={onDuplicateAsChild}
/>
{/* Model info in dropdown */}
{(() => {
const ProviderIcon = getProviderIconForModel(feature.model);

View File

@@ -85,6 +85,11 @@ export function useBoardPersistence({ currentProject }: UseBoardPersistenceProps
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
queryClient.setQueryData<Feature[]>(
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);
if (result.success && result.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) {
throw new Error(result.error || 'Failed to create feature on server');
}
@@ -104,7 +119,10 @@ export function useBoardPersistence({ currentProject }: UseBoardPersistenceProps
});
} catch (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({
queryKey: queryKeys.features.all(currentProject.path),
});
@@ -131,7 +149,6 @@ export function useBoardPersistence({ currentProject }: UseBoardPersistenceProps
try {
const api = getElectronAPI();
if (!api.features) {
logger.error('Features API not available');
// Rollback optimistic deletion since we can't persist
if (previousFeatures) {
queryClient.setQueryData(queryKeys.features.all(currentProject.path), previousFeatures);
@@ -139,7 +156,7 @@ export function useBoardPersistence({ currentProject }: UseBoardPersistenceProps
queryClient.invalidateQueries({
queryKey: queryKeys.features.all(currentProject.path),
});
return;
throw new Error('Features API not available');
}
await api.features.delete(currentProject.path, featureId);

View File

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

View File

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

View File

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

View File

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

View File

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