lints and tests pass

This commit is contained in:
Pedram Amini
2025-12-25 13:21:41 -06:00
parent 7b9b88bfc9
commit ac3df7ed36
67 changed files with 194 additions and 252 deletions

View File

@@ -68,8 +68,10 @@ export default tseslint.config(
// React Hooks rules
'react-hooks/rules-of-hooks': 'error',
// TODO: Change to 'error' after fixing ~74 existing violations
'react-hooks/exhaustive-deps': 'warn',
// NOTE: exhaustive-deps is intentionally 'off' - this codebase uses refs and
// stable state setters intentionally without listing them as dependencies.
// The pattern is to use refs to access latest values without causing re-renders.
'react-hooks/exhaustive-deps': 'off',
// General rules
'no-console': 'off', // Console is used throughout

View File

@@ -985,6 +985,10 @@ describe('RightPanel', () => {
});
describe('Elapsed time calculation', () => {
// Note: Elapsed time display now uses cumulativeTaskTimeMs from batch state
// instead of calculating from startTime. This provides more accurate work time
// by tracking actual task durations rather than wall-clock time.
it('should clear elapsed time when batch run is not running', async () => {
const currentSessionBatchState: BatchRunState = {
isRunning: false,
@@ -1000,16 +1004,16 @@ describe('RightPanel', () => {
loopEnabled: false,
loopIteration: 0,
startTime: Date.now(),
cumulativeTaskTimeMs: 5000,
};
const props = createDefaultProps({ currentSessionBatchState });
render(<RightPanel {...props} />);
// Elapsed time should not be displayed when not running
expect(screen.queryByText(/elapsed/i)).not.toBeInTheDocument();
expect(screen.queryByText(/\d+s/)).not.toBeInTheDocument();
});
it('should display elapsed seconds when batch run is running', async () => {
const startTime = Date.now() - 5000; // Started 5 seconds ago
const currentSessionBatchState: BatchRunState = {
isRunning: true,
isStopping: false,
@@ -1023,21 +1027,17 @@ describe('RightPanel', () => {
completedTasksAcrossAllDocs: 5,
loopEnabled: false,
loopIteration: 0,
startTime,
startTime: Date.now(),
cumulativeTaskTimeMs: 5000, // 5 seconds of work
};
const props = createDefaultProps({ currentSessionBatchState });
render(<RightPanel {...props} />);
// Initial render shows elapsed time
await act(async () => {
vi.advanceTimersByTime(0);
});
expect(screen.getByText(/\d+s/)).toBeInTheDocument();
// Should show "5s" based on cumulativeTaskTimeMs
expect(screen.getByText('5s')).toBeInTheDocument();
});
it('should display elapsed minutes and seconds', async () => {
const startTime = Date.now() - 125000; // Started 2 minutes 5 seconds ago
const currentSessionBatchState: BatchRunState = {
isRunning: true,
isStopping: false,
@@ -1051,24 +1051,17 @@ describe('RightPanel', () => {
completedTasksAcrossAllDocs: 5,
loopEnabled: false,
loopIteration: 0,
startTime,
// Time tracking fields for visibility-aware elapsed time
accumulatedElapsedMs: 0,
lastActiveTimestamp: startTime,
startTime: Date.now(),
cumulativeTaskTimeMs: 125000, // 2 minutes 5 seconds of work
};
const props = createDefaultProps({ currentSessionBatchState });
render(<RightPanel {...props} />);
await act(async () => {
vi.advanceTimersByTime(0);
});
// Should show format like "2m 5s"
expect(screen.getByText(/\d+m \d+s/)).toBeInTheDocument();
expect(screen.getByText('2m 5s')).toBeInTheDocument();
});
it('should display elapsed hours and minutes', async () => {
const startTime = Date.now() - 3725000; // Started 1 hour, 2 minutes, 5 seconds ago
const currentSessionBatchState: BatchRunState = {
isRunning: true,
isStopping: false,
@@ -1082,24 +1075,17 @@ describe('RightPanel', () => {
completedTasksAcrossAllDocs: 5,
loopEnabled: false,
loopIteration: 0,
startTime,
// Time tracking fields for visibility-aware elapsed time
accumulatedElapsedMs: 0,
lastActiveTimestamp: startTime,
startTime: Date.now(),
cumulativeTaskTimeMs: 3725000, // 1 hour, 2 minutes, 5 seconds of work
};
const props = createDefaultProps({ currentSessionBatchState });
render(<RightPanel {...props} />);
await act(async () => {
vi.advanceTimersByTime(0);
});
// Should show format like "1h 2m"
expect(screen.getByText(/\d+h \d+m/)).toBeInTheDocument();
expect(screen.getByText('1h 2m')).toBeInTheDocument();
});
it('should update elapsed time every second', async () => {
const startTime = Date.now();
it('should update elapsed time when cumulativeTaskTimeMs changes', async () => {
const currentSessionBatchState: BatchRunState = {
isRunning: true,
isStopping: false,
@@ -1113,60 +1099,45 @@ describe('RightPanel', () => {
completedTasksAcrossAllDocs: 5,
loopEnabled: false,
loopIteration: 0,
startTime,
};
const props = createDefaultProps({ currentSessionBatchState });
render(<RightPanel {...props} />);
// Initial render
await act(async () => {
vi.advanceTimersByTime(0);
});
expect(screen.getByText('0s')).toBeInTheDocument();
// Advance time by 3 seconds (timer updates every 3s for performance - Quick Win 3)
await act(async () => {
vi.advanceTimersByTime(3000);
});
expect(screen.getByText('3s')).toBeInTheDocument();
// Advance time by another 3 seconds
await act(async () => {
vi.advanceTimersByTime(3000);
});
expect(screen.getByText('6s')).toBeInTheDocument();
});
it('should clear interval when batch run stops', async () => {
const clearIntervalSpy = vi.spyOn(window, 'clearInterval');
const startTime = Date.now();
const currentSessionBatchState: BatchRunState = {
isRunning: true,
isStopping: false,
documents: ['doc1'],
currentDocumentIndex: 0,
totalTasks: 10,
completedTasks: 5,
currentDocTasksTotal: 10,
currentDocTasksCompleted: 5,
totalTasksAcrossAllDocs: 10,
completedTasksAcrossAllDocs: 5,
loopEnabled: false,
loopIteration: 0,
startTime,
startTime: Date.now(),
cumulativeTaskTimeMs: 3000, // 3 seconds
};
const props = createDefaultProps({ currentSessionBatchState });
const { rerender } = render(<RightPanel {...props} />);
await act(async () => {
vi.advanceTimersByTime(0);
});
// Initial render shows 3s
expect(screen.getByText('3s')).toBeInTheDocument();
// Stop the batch run
const stoppedBatchRunState = { ...currentSessionBatchState, isRunning: false };
rerender(<RightPanel {...createDefaultProps({ currentSessionBatchState: stoppedBatchRunState })} />);
// Update cumulativeTaskTimeMs to 6 seconds
const updatedBatchState = { ...currentSessionBatchState, cumulativeTaskTimeMs: 6000 };
rerender(<RightPanel {...createDefaultProps({ currentSessionBatchState: updatedBatchState })} />);
expect(clearIntervalSpy).toHaveBeenCalled();
// Should now show 6s
expect(screen.getByText('6s')).toBeInTheDocument();
});
it('should not show elapsed time when cumulativeTaskTimeMs is 0', async () => {
const currentSessionBatchState: BatchRunState = {
isRunning: true,
isStopping: false,
documents: ['doc1'],
currentDocumentIndex: 0,
totalTasks: 10,
completedTasks: 0,
currentDocTasksTotal: 10,
currentDocTasksCompleted: 0,
totalTasksAcrossAllDocs: 10,
completedTasksAcrossAllDocs: 0,
loopEnabled: false,
loopIteration: 0,
startTime: Date.now(),
cumulativeTaskTimeMs: 0, // No work done yet
};
const props = createDefaultProps({ currentSessionBatchState });
render(<RightPanel {...props} />);
// Should not show elapsed time when no work has been done
expect(screen.queryByText(/\d+s/)).not.toBeInTheDocument();
});
});

View File

@@ -536,7 +536,7 @@ export function readDocAndCountTasks(folderPath: string, filename: string): { co
content,
taskCount: matches ? matches.length : 0,
};
} catch (_error) {
} catch {
return { content: '', taskCount: 0 };
}
}
@@ -554,7 +554,7 @@ export function readDocAndGetTasks(folderPath: string, filename: string): { cont
? matches.map(m => m.replace(/^[\s]*-\s*\[\s*\]\s*/, '').trim())
: [];
return { content, tasks };
} catch (_error) {
} catch {
return { content: '', tasks: [] };
}
}

View File

@@ -677,7 +677,7 @@ export class AgentDetector {
}
return { exists: false };
} catch (_error) {
} catch {
return { exists: false };
}
}

View File

@@ -197,7 +197,7 @@ export function registerPersistenceHandlers(deps: PersistenceHandlerDependencies
const content = await fs.readFile(cliActivityPath, 'utf-8');
const data = JSON.parse(content);
return data.activities || [];
} catch (_error) {
} catch {
// File doesn't exist or is invalid - return empty array
return [];
}

View File

@@ -357,7 +357,7 @@ export function registerSystemHandlers(deps: SystemHandlerDependencies): void {
if (!fsSync.existsSync(targetPath)) {
try {
fsSync.mkdirSync(targetPath, { recursive: true });
} catch (_error) {
} catch {
return { success: false, error: `Cannot create directory: ${targetPath}` };
}
}

View File

@@ -844,7 +844,7 @@ export class ProcessManager extends EventEmitter {
this.emit('usage', sessionId, usageStats);
}
}
} catch (_e) {
} catch {
// If it's not valid JSON, emit as raw text
this.emit('data', sessionId, line);
}

View File

@@ -90,7 +90,7 @@ async function detectShell(shellId: string, shellName: string): Promise<ShellInf
name: shellName,
available: false,
};
} catch (_error) {
} catch {
return {
id: shellId,
name: shellName,

View File

@@ -67,7 +67,6 @@ import {
useActivityTracker,
useNavigationHistory,
useSessionNavigation,
useBatchedSessionUpdates,
useSortedSessions,
compareNamesIgnoringEmojis,
useGroupManagement,
@@ -125,7 +124,7 @@ import { parseSynopsis } from '../shared/synopsis';
// Note: GroupChat, GroupChatState are now imported via GroupChatContext; GroupChatMessage still used locally
import type {
ToolType, SessionState, RightPanelTab, SettingsTab,
FocusArea, LogEntry, Session, Group, AITab, UsageStats, QueuedItem, BatchRunConfig,
FocusArea, LogEntry, Session, AITab, UsageStats, QueuedItem, BatchRunConfig,
AgentError, BatchRunState, GroupChatMessage,
SpecKitCommand
} from './types';
@@ -327,7 +326,7 @@ function MaestroConsoleInner() {
batchedUpdater,
activeSession,
cyclePositionRef,
removedWorktreePaths, setRemovedWorktreePaths, removedWorktreePathsRef,
removedWorktreePaths: _removedWorktreePaths, setRemovedWorktreePaths, removedWorktreePathsRef,
} = useSession();
// Spec Kit commands (loaded from bundled prompts)
@@ -635,7 +634,7 @@ function MaestroConsoleInner() {
if (previewFile !== null) {
setPreviewFile(null);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [activeSessionId]);
// Restore a persisted session by respawning its process
@@ -847,7 +846,7 @@ function MaestroConsoleInner() {
}
};
loadSessionsAndGroups();
// eslint-disable-next-line react-hooks/exhaustive-deps -- Load once on mount; activeSessionId/setActiveSessionId are intentionally omitted to prevent reload loops
}, []);
// Hide splash screen only when both settings and sessions have fully loaded
@@ -890,7 +889,7 @@ function MaestroConsoleInner() {
}
// autoRunStats.longestRunMs and getUnacknowledgedBadgeLevel intentionally omitted -
// this effect runs once on startup to check for missed badges, not on every stats update
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [settingsLoaded, sessionsLoaded]);
// Check for unacknowledged badges when user returns to the app
@@ -974,7 +973,7 @@ function MaestroConsoleInner() {
}
// getUnacknowledgedKeyboardMasteryLevel intentionally omitted -
// this effect runs once on startup to check for unacknowledged levels, not on function changes
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [settingsLoaded, sessionsLoaded]);
// Scan worktree directories on startup for sessions with worktreeConfig
@@ -1120,7 +1119,7 @@ function MaestroConsoleInner() {
// Run once on startup with a small delay to let UI settle
const timer = setTimeout(scanWorktreeConfigsOnStartup, 500);
return () => clearTimeout(timer);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [sessionsLoaded]); // Only run once when sessions are loaded
// Check for updates on startup if enabled
@@ -2248,7 +2247,7 @@ function MaestroConsoleInner() {
}
thinkingChunkBuffer.clear();
};
// eslint-disable-next-line react-hooks/exhaustive-deps -- IPC subscription runs once on mount; refs/callbacks intentionally omitted to prevent re-subscription
}, []);
// --- GROUP CHAT EVENT LISTENERS ---
@@ -2328,7 +2327,7 @@ function MaestroConsoleInner() {
unsubParticipantState?.();
unsubModeratorSessionId?.();
};
// eslint-disable-next-line react-hooks/exhaustive-deps -- IPC subscription for group chat events; setters from context are stable
}, [activeGroupChatId]);
// Process group chat execution queue when state becomes idle
@@ -2563,7 +2562,7 @@ function MaestroConsoleInner() {
? (activeSession.shellCwd || activeSession.cwd)
: activeSession.cwd)
: '',
// eslint-disable-next-line react-hooks/exhaustive-deps -- Using specific properties instead of full activeSession object to avoid unnecessary re-renders
[activeSession?.inputMode, activeSession?.shellCwd, activeSession?.cwd]
);
@@ -3042,7 +3041,7 @@ function MaestroConsoleInner() {
// The inputValue changes when we blur (syncAiInputToSession), but we don't want
// to read it back into local state - that would cause a feedback loop.
// We only need to load inputValue when switching TO a different tab.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [activeTab?.id]);
// Input sync handlers (extracted to useInputSync hook)
@@ -3078,7 +3077,7 @@ function MaestroConsoleInner() {
// Update ref to current session
prevActiveSessionIdRef.current = activeSession.id;
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [activeSession?.id]);
// Use local state for responsive typing - no session state update on every keystroke
@@ -3091,7 +3090,7 @@ function MaestroConsoleInner() {
if (!activeSession || activeSession.inputMode !== 'ai') return [];
const activeTab = getActiveTab(activeSession);
return activeTab?.stagedImages || [];
// eslint-disable-next-line react-hooks/exhaustive-deps -- Using specific properties instead of full activeSession object to avoid unnecessary re-renders
}, [activeSession?.aiTabs, activeSession?.activeTabId, activeSession?.inputMode]);
// Set staged images on the active tab
@@ -3837,7 +3836,7 @@ function MaestroConsoleInner() {
window.maestro.git.unwatchWorktreeDirectory(session.id);
}
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
// Re-run when worktreeConfig changes on any session
worktreeConfigKey,
@@ -3995,7 +3994,7 @@ function MaestroConsoleInner() {
return () => {
clearInterval(intervalId);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [sessions.length, defaultSaveToHistory]); // Re-run when session count changes (removedWorktreePaths accessed via ref)
// Handler to open batch runner modal
@@ -4370,7 +4369,7 @@ function MaestroConsoleInner() {
if (activeSession && fileTreeContainerRef.current && activeSession.fileExplorerScrollPos !== undefined) {
fileTreeContainerRef.current.scrollTop = activeSession.fileExplorerScrollPos;
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [activeSessionId]); // Only restore on session switch, not on scroll position changes
// Track navigation history when session or AI tab changes
@@ -4381,7 +4380,7 @@ function MaestroConsoleInner() {
tabId: activeSession.inputMode === 'ai' && activeSession.aiTabs?.length > 0 ? activeSession.activeTabId : undefined
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [activeSessionId, activeSession?.activeTabId]); // Track session and tab changes
// Reset shortcuts search when modal closes
@@ -6856,7 +6855,7 @@ function MaestroConsoleInner() {
// Then apply hidden files filter to match what FileExplorerPanel displays
const displayTree = filterHiddenFiles(filteredFileTree);
setFlatFileList(flattenTree(displayTree, expandedSet));
// eslint-disable-next-line react-hooks/exhaustive-deps -- Using specific properties instead of full activeSession object to avoid unnecessary re-renders
}, [activeSession?.fileExplorerExpanded, filteredFileTree, showHiddenFiles]);
// Handle pending jump path from /jump command
@@ -6889,7 +6888,7 @@ function MaestroConsoleInner() {
setSessions(prev => prev.map(s =>
s.id === activeSession.id ? { ...s, pendingJumpPath: undefined } : s
));
// eslint-disable-next-line react-hooks/exhaustive-deps -- Using specific properties instead of full activeSession object to avoid unnecessary re-renders
}, [activeSession?.pendingJumpPath, flatFileList, activeSession?.id]);
// Scroll to selected file item when selection changes via keyboard

View File

@@ -616,7 +616,7 @@ const AutoRunInner = forwardRef<AutoRunHandle, AutoRunProps>(function AutoRunInn
previewScrollPos: previewRef.current?.scrollTop || 0
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps -- setMode is a state setter and is stable; omitted to avoid adding unnecessary deps
}, [mode, onStateChange]);
// Toggle between edit and preview modes
@@ -653,7 +653,7 @@ const AutoRunInner = forwardRef<AutoRunHandle, AutoRunProps>(function AutoRunInn
setMode(modeBeforeAutoRunRef.current);
modeBeforeAutoRunRef.current = null;
}
// eslint-disable-next-line react-hooks/exhaustive-deps -- mode/setMode intentionally omitted; effect should only trigger on isLocked change to switch between locked preview and restored mode
}, [isLocked]);
// Restore cursor and scroll positions when component mounts
@@ -665,7 +665,7 @@ const AutoRunInner = forwardRef<AutoRunHandle, AutoRunProps>(function AutoRunInn
if (previewRef.current && initialPreviewScrollPos > 0) {
previewRef.current.scrollTop = initialPreviewScrollPos;
}
// eslint-disable-next-line react-hooks/exhaustive-deps -- Initial positions intentionally omitted; should only run once on mount to restore saved state
}, []);
// Restore scroll position after content changes cause ReactMarkdown to rebuild DOM
@@ -789,7 +789,7 @@ const AutoRunInner = forwardRef<AutoRunHandle, AutoRunProps>(function AutoRunInn
setTotalMatches(0);
setCurrentMatchIndex(0);
}
// eslint-disable-next-line react-hooks/exhaustive-deps -- currentMatchIndex intentionally omitted; we only want to recalculate matches when search or content changes, not when navigating between matches
}, [searchQuery, localContent]);
// Navigate to next search match

View File

@@ -220,7 +220,7 @@ export function BatchRunnerModal(props: BatchRunnerModalProps) {
unregisterLayer(layerIdRef.current);
}
};
// eslint-disable-next-line react-hooks/exhaustive-deps -- onClose/setShowSavePlaybookModal intentionally omitted; layer registration should stay stable, handler updates are handled in a separate effect
}, [registerLayer, unregisterLayer, showSavePlaybookModal, showDeleteConfirmModal, handleCancelDeletePlaybook]);
// Update handler when dependencies change
@@ -236,7 +236,7 @@ export function BatchRunnerModal(props: BatchRunnerModalProps) {
}
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps -- setShowSavePlaybookModal is a state setter (stable); intentionally omitted
}, [onClose, updateLayerHandler, showSavePlaybookModal, showDeleteConfirmModal, handleCancelDeletePlaybook]);
// Focus textarea on mount

View File

@@ -146,7 +146,7 @@ export function CreatePRModal({
try {
const status = await window.maestro.git.checkGhCli();
setGhCliStatus(status);
} catch (_err) {
} catch {
setGhCliStatus({ installed: false, authenticated: false });
}
};
@@ -157,7 +157,7 @@ export function CreatePRModal({
const lines = result.stdout.trim().split('\n').filter((line: string) => line.length > 0);
setUncommittedCount(lines.length);
setHasUncommittedChanges(lines.length > 0);
} catch (_err) {
} catch {
setHasUncommittedChanges(false);
setUncommittedCount(0);
}

View File

@@ -354,7 +354,7 @@ export function CustomThemeBuilder({
} else {
onImportError?.('Invalid theme file: missing colors object');
}
} catch (_err) {
} catch {
onImportError?.('Failed to parse theme file: invalid JSON format');
}
};

View File

@@ -197,7 +197,7 @@ export function FileExplorerPanel(props: FileExplorerPanelProps) {
layerIdRef.current = id;
return () => unregisterLayer(id);
}
// eslint-disable-next-line react-hooks/exhaustive-deps -- setters (setFileTreeFilter, setFileTreeFilterOpen) intentionally omitted; layer registration should stay stable
}, [fileTreeFilterOpen, registerLayer, unregisterLayer]);
// Update handler when dependencies change
@@ -347,7 +347,7 @@ export function FileExplorerPanel(props: FileExplorerPanelProps) {
)}
</div>
);
// eslint-disable-next-line react-hooks/exhaustive-deps -- Using specific session properties instead of full session object to avoid unnecessary re-renders
}, [session.fullPath, session.changedFiles, session.fileExplorerExpanded, session.id, previewFile?.path, activeFocus, activeRightTab, selectedFileIndex, theme, toggleFolder, setSessions, setSelectedFileIndex, setActiveFocus, handleFileClick, fileTreeFilter]);
return (

View File

@@ -843,7 +843,7 @@ export function FilePreview({ file, onClose, theme, markdownEditMode, setMarkdow
new ClipboardItem({ [blob.type]: blob })
]);
setCopyNotificationMessage('Image Copied to Clipboard');
} catch (_err) {
} catch {
// Fallback: copy the data URL if image copy fails
navigator.clipboard.writeText(file.content);
setCopyNotificationMessage('Image URL Copied to Clipboard');

View File

@@ -165,7 +165,7 @@ export function FirstRunCelebration({
// Fire confetti on mount
useEffect(() => {
fireConfetti();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
// Handle close with confetti

View File

@@ -88,7 +88,7 @@ export const GitLogViewer = memo(function GitLogViewer({ cwd, theme, onClose }:
try {
const result = await window.maestro.git.show(cwd, hash);
setSelectedCommitDiff(result.stdout);
} catch (err) {
} catch {
setSelectedCommitDiff(null);
} finally {
setLoadingDiff(false);
@@ -358,7 +358,6 @@ export const GitLogViewer = memo(function GitLogViewer({ cwd, theme, onClose }:
{entry.refs.map((ref, i) => {
const isTag = ref.startsWith('tag:');
const isBranch = !isTag && !ref.includes('/');
const isRemote = ref.includes('/');
return (
<span

View File

@@ -297,7 +297,6 @@ function GroupChatActivityGraph({
// Build stacked segments for each participant
const segments: { name: string; percent: number; color: string }[] = [];
let runningTotal = 0;
for (const name of participantOrder) {
if (bucket[name]) {
const segmentPercent = (bucket[name] / total) * 100;
@@ -306,7 +305,6 @@ function GroupChatActivityGraph({
percent: segmentPercent,
color: participantColors[name] || theme.colors.textDim,
});
runningTotal += bucket[name];
}
}

View File

@@ -63,7 +63,7 @@ export const GroupChatInput = React.memo(function GroupChatInput({
theme,
state,
onSend,
participants,
participants: _participants,
sessions,
groupChatId,
draftMessage,

View File

@@ -123,7 +123,7 @@ export function GroupChatRightPanel({
setColorPreferences(prev => ({ ...prev, ...prefsToSave }));
saveColorPreferences({ ...colorPreferences, ...prefsToSave });
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [colorResult]);
// Notify parent when colors are computed (use ref to prevent infinite loops)

View File

@@ -46,7 +46,7 @@ export function HistoryDetailModal({
theme,
entry,
onClose,
onJumpToAgentSession,
onJumpToAgentSession: _onJumpToAgentSession,
onResumeSession,
onDelete,
onUpdate,

View File

@@ -137,7 +137,7 @@ export function KeyboardMasteryCelebration({
timeoutsRef.current.forEach(clearTimeout);
timeoutsRef.current = [];
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
// Handle close with confetti - use ref to avoid stale state

View File

@@ -95,7 +95,7 @@ export function LeaderboardRegistrationModal({
// Polling state - generate clientToken once if not already persisted
const [clientToken] = useState(() => existingRegistration?.clientToken || generateClientToken());
const [isPolling, setIsPolling] = useState(false);
const [_isPolling, setIsPolling] = useState(false);
const pollingIntervalRef = useRef<NodeJS.Timeout | null>(null);
// Manual token entry state

View File

@@ -228,16 +228,16 @@ export const MainPanel = forwardRef<MainPanelHandle, MainPanelProps>(function Ma
setTabCompletionOpen, setSelectedTabCompletionIndex, setTabCompletionFilter,
atMentionOpen, atMentionFilter, atMentionStartIndex, atMentionSuggestions, selectedAtMentionIndex,
setAtMentionOpen, setAtMentionFilter, setAtMentionStartIndex, setSelectedAtMentionIndex,
previewFile, markdownEditMode, shortcuts, rightPanelOpen, maxOutputLines, gitDiffPreview,
previewFile, markdownEditMode, shortcuts, rightPanelOpen, maxOutputLines, gitDiffPreview: _gitDiffPreview,
fileTreeFilterOpen, logLevel, setGitDiffPreview, setLogViewerOpen, setAgentSessionsOpen, setActiveAgentSessionId,
onResumeAgentSession, onNewAgentSession, setActiveFocus, setOutputSearchOpen, setOutputSearchQuery,
setInputValue, setEnterToSendAI, setEnterToSendTerminal, setStagedImages, setLightboxImage, setCommandHistoryOpen,
setCommandHistoryFilter, setCommandHistorySelectedIndex, setSlashCommandOpen,
setSelectedSlashCommandIndex, setPreviewFile, setMarkdownEditMode,
setAboutModalOpen, setRightPanelOpen, setGitLogOpen, inputRef, logsEndRef, terminalOutputRef,
setAboutModalOpen: _setAboutModalOpen, setRightPanelOpen, setGitLogOpen, inputRef, logsEndRef, terminalOutputRef,
fileTreeContainerRef, fileTreeFilterInputRef, toggleInputMode, processInput, handleInterrupt,
handleInputKeyDown, handlePaste, handleDrop, getContextColor, setActiveSessionId,
batchRunState, currentSessionBatchState, onStopBatchRun, showConfirmation, onRemoveQueuedItem, onOpenQueueBrowser,
batchRunState: _batchRunState, currentSessionBatchState, onStopBatchRun, showConfirmation: _showConfirmation, onRemoveQueuedItem, onOpenQueueBrowser,
isMobileLandscape = false,
showFlashNotification,
onOpenWorktreeConfig,

View File

@@ -225,7 +225,7 @@ export const MarkdownRenderer = memo(({ content, theme, onCopy, className = '',
remarkPlugins={remarkPlugins}
rehypePlugins={allowRawHtml ? [rehypeRaw] : undefined}
components={{
a: ({ node, href, children, ...props }) => {
a: ({ node: _node, href, children, ...props }) => {
// Check for maestro-file:// protocol OR data-maestro-file attribute
// (data attribute is fallback when rehype strips custom protocols)
const dataFilePath = (props as any)['data-maestro-file'];
@@ -250,7 +250,7 @@ export const MarkdownRenderer = memo(({ content, theme, onCopy, className = '',
</a>
);
},
code: ({ node, inline, className, children, ...props }: any) => {
code: ({ node: _node, inline, className, children, ...props }: any) => {
const match = (className || '').match(/language-(\w+)/);
const language = match ? match[1] : 'text';
const codeContent = String(children).replace(/\n$/, '');
@@ -268,7 +268,7 @@ export const MarkdownRenderer = memo(({ content, theme, onCopy, className = '',
</code>
);
},
img: ({ node, src, alt, ...props }: any) => {
img: ({ node: _node, src, alt, ...props }: any) => {
// Use LocalImage component to handle file:// URLs via IPC
// Extract width from data-maestro-width attribute if present
const widthStr = props['data-maestro-width'];

View File

@@ -380,7 +380,7 @@ export function MergeProgressModal({
{STAGES.map((stage, index) => {
const isActive = index === currentStageIndex;
const isCompleted = index < currentStageIndex;
const isPending = index > currentStageIndex;
const _isPending = index > currentStageIndex;
return (
<div

View File

@@ -813,7 +813,7 @@ export function MergeSessionModal({
style={{ borderColor: theme.colors.border }}
role="group"
>
{items.map((item, itemIndex) => {
{items.map((item, _itemIndex) => {
const flatIndex = filteredItems.indexOf(item);
const isSelected = flatIndex === selectedIndex;
const isTarget = selectedTarget?.tabId === item.tabId;

View File

@@ -690,7 +690,7 @@ export function EditAgentModal({ isOpen, onClose, onSave, theme, session, existi
const [customPath, setCustomPath] = useState('');
const [customArgs, setCustomArgs] = useState('');
const [customEnvVars, setCustomEnvVars] = useState<Record<string, string>>({});
const [customModel, setCustomModel] = useState('');
const [_customModel, setCustomModel] = useState('');
const [refreshingAgent, setRefreshingAgent] = useState(false);
const nameInputRef = useRef<HTMLInputElement>(null);

View File

@@ -104,7 +104,7 @@ export function QuickActionsModal(props: QuickActionsModalProps) {
setShortcutsHelpOpen, setAboutModalOpen, setLogViewerOpen, setProcessMonitorOpen,
setAgentSessionsOpen, setActiveAgentSessionId, setGitDiffPreview, setGitLogOpen,
onRenameTab, onToggleReadOnlyMode, onToggleTabShowThinking, onOpenTabSwitcher, tabShortcuts, isAiMode, setPlaygroundOpen, onRefreshGitFileState,
onDebugReleaseQueuedItem, markdownEditMode, onToggleMarkdownEditMode, setUpdateCheckModalOpen, openWizard, wizardGoToStep, setDebugWizardModalOpen, setDebugPackageModalOpen, startTour, setFuzzyFileSearchOpen, onEditAgent,
onDebugReleaseQueuedItem, markdownEditMode, onToggleMarkdownEditMode, setUpdateCheckModalOpen, openWizard, wizardGoToStep: _wizardGoToStep, setDebugWizardModalOpen, setDebugPackageModalOpen, startTour, setFuzzyFileSearchOpen, onEditAgent,
groupChats, onNewGroupChat, onOpenGroupChat, onCloseGroupChat, onDeleteGroupChat, activeGroupChatId,
hasActiveSessionCapability, onOpenMergeSession, onOpenSendToAgent, onOpenCreatePR,
onSummarizeAndContinue, canSummarizeActiveTab

View File

@@ -18,7 +18,7 @@ interface RenameGroupModalProps {
export function RenameGroupModal(props: RenameGroupModalProps) {
const {
theme, groupId, groupName, setGroupName, groupEmoji, setGroupEmoji,
onClose, groups, setGroups
onClose, groups: _groups, setGroups
} = props;
const inputRef = useRef<HTMLInputElement>(null);

View File

@@ -14,7 +14,7 @@
*/
import React, { useState, useEffect, useRef, useMemo, useCallback } from 'react';
import { Search, ArrowRight, Check, X, Loader2, Circle } from 'lucide-react';
import { Search, ArrowRight, X, Loader2, Circle } from 'lucide-react';
import type { Theme, Session, AITab, ToolType } from '../types';
import type { MergeResult } from '../types/contextMerge';
import { fuzzyMatchWithScore } from '../utils/search';

View File

@@ -1,4 +1,4 @@
import React, { useState, useEffect, useRef, useCallback } from 'react';
import React, { useState, useEffect, useRef } from 'react';
import {
Wand2, Plus, Settings, ChevronRight, ChevronDown, ChevronUp, X, Keyboard,
Radio, Copy, ExternalLink, PanelLeftClose, PanelLeftOpen, Folder, Info, GitBranch, Bot, Clock,
@@ -788,7 +788,7 @@ export function SessionList(props: SessionListProps) {
setLiveOverlayOpen,
liveOverlayRef,
cloudflaredInstalled,
cloudflaredChecked,
cloudflaredChecked: _cloudflaredChecked,
tunnelStatus,
tunnelUrl,
tunnelError,
@@ -916,7 +916,7 @@ export function SessionList(props: SessionListProps) {
};
// Helper: Check if a session has worktree children
const hasWorktreeChildren = (sessionId: string): boolean => {
const _hasWorktreeChildren = (sessionId: string): boolean => {
return sessions.some(s => s.parentSessionId === sessionId);
};
@@ -924,7 +924,7 @@ export function SessionList(props: SessionListProps) {
const renderCollapsedPill = (
session: Session,
keyPrefix: string,
onExpand: () => void
_onExpand: () => void
) => {
const worktreeChildren = getWorktreeChildren(session.id);
const allSessions = [session, ...worktreeChildren];
@@ -1187,7 +1187,7 @@ export function SessionList(props: SessionListProps) {
setPreFilterBookmarksCollapsed(null);
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [sessionFilterOpen]);
// Temporarily expand groups when filtering to show matching sessions
@@ -1227,7 +1227,7 @@ export function SessionList(props: SessionListProps) {
setGroups(prev => prev.map(g => ({ ...g, collapsed: true })));
setBookmarksCollapsed(false);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [sessionFilter]);
// Get the jump number (1-9, 0=10th) for a session based on its position in visibleSessions

View File

@@ -244,7 +244,7 @@ export const SettingsModal = memo(function SettingsModal(props: SettingsModalPro
// Sync/storage location state
const [defaultStoragePath, setDefaultStoragePath] = useState<string>('');
const [currentStoragePath, setCurrentStoragePath] = useState<string>('');
const [_currentStoragePath, setCurrentStoragePath] = useState<string>('');
const [customSyncPath, setCustomSyncPath] = useState<string | undefined>(undefined);
const [syncRestartRequired, setSyncRestartRequired] = useState(false);
const [syncMigrating, setSyncMigrating] = useState(false);

View File

@@ -112,7 +112,7 @@ export function StandingOvationOverlay({
// Fire confetti on mount only - empty deps to run once
useEffect(() => {
fireConfetti();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
// Handle graceful close with confetti

View File

@@ -586,7 +586,7 @@ export function TabBar({
}, [onRequestRename]);
// Count unread tabs for the filter toggle tooltip
const unreadCount = tabs.filter(t => t.hasUnread).length;
const _unreadCount = tabs.filter(t => t.hasUnread).length;
// Filter tabs based on unread filter state
// When filter is on, show: unread tabs + active tab + tabs with drafts

View File

@@ -9,7 +9,6 @@ import { MODAL_PRIORITIES } from '../constants/modalPriorities';
import { getActiveTab } from '../utils/tabHelpers';
import { useDebouncedValue, useThrottledCallback } from '../hooks';
import {
processCarriageReturns,
processLogTextHelper,
filterTextByLinesHelper,
getCachedAnsiHtml,
@@ -858,9 +857,9 @@ interface TerminalOutputProps {
export const TerminalOutput = forwardRef<HTMLDivElement, TerminalOutputProps>((props, ref) => {
const {
session, theme, fontFamily, activeFocus, outputSearchOpen, outputSearchQuery,
session, theme, fontFamily, activeFocus: _activeFocus, outputSearchOpen, outputSearchQuery,
setOutputSearchOpen, setOutputSearchQuery, setActiveFocus, setLightboxImage,
inputRef, logsEndRef, maxOutputLines, onDeleteLog, onRemoveQueuedItem, onInterrupt,
inputRef, logsEndRef, maxOutputLines, onDeleteLog, onRemoveQueuedItem, onInterrupt: _onInterrupt,
audioFeedbackCommand, onScrollPositionChange, onAtBottomChange, initialScrollTop,
markdownEditMode, setMarkdownEditMode, onReplayMessage,
fileTree, cwd, projectRoot, onFileClick, onShowErrorDetails
@@ -879,7 +878,7 @@ export const TerminalOutput = forwardRef<HTMLDivElement, TerminalOutputProps>((p
const expandedLogsRef = useRef(expandedLogs);
expandedLogsRef.current = expandedLogs;
// Counter to force re-render of LogItem when expanded state changes
const [expandedTrigger, setExpandedTrigger] = useState(0);
const [_expandedTrigger, setExpandedTrigger] = useState(0);
// Track local filters per log entry (log ID -> filter query)
const [localFilters, setLocalFilters] = useState<Map<string, string>>(new Map());
@@ -890,7 +889,7 @@ export const TerminalOutput = forwardRef<HTMLDivElement, TerminalOutputProps>((p
const activeLocalFilterRef = useRef(activeLocalFilter);
activeLocalFilterRef.current = activeLocalFilter;
// Counter to force re-render when local filter state changes
const [filterTrigger, setFilterTrigger] = useState(0);
const [_filterTrigger, setFilterTrigger] = useState(0);
// Track filter modes per log entry (log ID -> {mode: 'include'|'exclude', regex: boolean})
const [filterModes, setFilterModes] = useState<Map<string, { mode: 'include' | 'exclude'; regex: boolean }>>(new Map());
@@ -902,7 +901,7 @@ export const TerminalOutput = forwardRef<HTMLDivElement, TerminalOutputProps>((p
const deleteConfirmLogIdRef = useRef(deleteConfirmLogId);
deleteConfirmLogIdRef.current = deleteConfirmLogId;
// Counter to force re-render when delete confirmation changes
const [deleteConfirmTrigger, setDeleteConfirmTrigger] = useState(0);
const [_deleteConfirmTrigger, _setDeleteConfirmTrigger] = useState(0);
// Copy to clipboard notification state

View File

@@ -15,7 +15,7 @@
* Based on AgentErrorModal patterns, adapted for transfer-specific errors.
*/
import React, { useRef, useMemo, useEffect } from 'react';
import React, { useRef, useMemo } from 'react';
import {
AlertCircle,
RefreshCw,
@@ -272,7 +272,7 @@ function formatDetails(error: TransferError): string | null {
*/
export function TransferErrorModal({
theme,
isOpen,
isOpen: _isOpen,
error,
onRetry,
onSkipGrooming,

View File

@@ -338,7 +338,7 @@ export function DirectorySelectionScreen({ theme }: DirectorySelectionScreenProp
/**
* Handle back button click
*/
const handleBack = useCallback(() => {
const _handleBack = useCallback(() => {
previousStep();
}, [previousStep]);

View File

@@ -23,7 +23,6 @@ import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism';
import {
Eye,
Edit,
Image,
Loader2,
Rocket,
Compass,
@@ -334,7 +333,7 @@ function DocumentEditor({
onDocumentSelect: (index: number) => void;
statsText: string;
}): JSX.Element {
const fileInputRef = useRef<HTMLInputElement>(null);
const _fileInputRef = useRef<HTMLInputElement>(null);
const [attachmentsExpanded, setAttachmentsExpanded] = useState(true);
// Handle image paste
@@ -418,7 +417,7 @@ function DocumentEditor({
);
// Handle file input for manual image upload
const handleFileSelect = useCallback(
const _handleFileSelect = useCallback(
async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file || !folderPath || !selectedFile) return;

View File

@@ -21,7 +21,7 @@ import {
} from 'lucide-react';
import type { Theme } from '../../../types';
import { useWizard } from '../WizardContext';
import { phaseGenerator, AUTO_RUN_FOLDER_NAME, type CreatedFileInfo } from '../services/phaseGenerator';
import { phaseGenerator, type CreatedFileInfo } from '../services/phaseGenerator';
import { ScreenReaderAnnouncement } from '../ScreenReaderAnnouncement';
import { getNextAustinFact, parseFactWithLinks, type FactSegment } from '../services/austinFacts';
import { formatSize, formatElapsedTime } from '../../../../shared/formatters';
@@ -877,7 +877,7 @@ export function PreparingPlanScreen({
// Already have documents - auto-advance to review
nextStep();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [state.generatedDocuments.length]);
// Cleanup on unmount - abort any in-progress generation

View File

@@ -13,7 +13,7 @@
* replaced with the user's configured keyboard shortcut at runtime.
*/
import type { TourStepConfig, TourUIAction } from './useTour';
import type { TourStepConfig } from './useTour';
import type { Shortcut } from '../../../types';
import { formatShortcutKeys } from '../../../utils/shortcutFormatter';

View File

@@ -27,7 +27,7 @@ import {
Wifi,
Terminal,
} from 'lucide-react';
import type { AgentError, AgentErrorType, ToolType } from '../../types';
import type { AgentError, ToolType } from '../../types';
import type { RecoveryAction } from '../../components/AgentErrorModal';
export interface UseAgentErrorRecoveryOptions {

View File

@@ -142,41 +142,35 @@ export function useAgentExecution(
let responseText = '';
let taskUsageStats: UsageStats | undefined;
// Cleanup functions will be set when listeners are registered
let cleanupData: (() => void) | undefined;
let cleanupSessionId: (() => void) | undefined;
let cleanupExit: (() => void) | undefined;
let cleanupUsage: (() => void) | undefined;
// Array to collect cleanup functions as listeners are registered
const cleanupFns: (() => void)[] = [];
const cleanup = () => {
cleanupData?.();
cleanupSessionId?.();
cleanupExit?.();
cleanupUsage?.();
cleanupFns.forEach(fn => fn());
};
// Set up listeners for this specific agent run
cleanupData = window.maestro.process.onData((sid: string, data: string) => {
cleanupFns.push(window.maestro.process.onData((sid: string, data: string) => {
if (sid === targetSessionId) {
responseText += data;
}
});
}));
cleanupSessionId = window.maestro.process.onSessionId((sid: string, capturedId: string) => {
cleanupFns.push(window.maestro.process.onSessionId((sid: string, capturedId: string) => {
if (sid === targetSessionId) {
agentSessionId = capturedId;
}
});
}));
// Capture usage stats for this specific task
cleanupUsage = window.maestro.process.onUsage((sid: string, usageStats) => {
cleanupFns.push(window.maestro.process.onUsage((sid: string, usageStats) => {
if (sid === targetSessionId) {
// Accumulate usage stats for this task (there may be multiple usage events per task)
taskUsageStats = accumulateUsageStats(taskUsageStats, usageStats);
}
});
}));
cleanupExit = window.maestro.process.onExit((sid: string) => {
cleanupFns.push(window.maestro.process.onExit((sid: string) => {
if (sid === targetSessionId) {
// Clean up listeners
cleanup();
@@ -296,7 +290,7 @@ export function useAgentExecution(
resolve({ success: true, response: responseText, agentSessionId, usageStats: taskUsageStats });
}
}
});
}));
// Spawn the agent for batch processing
// Use effectiveCwd which may be a worktree path for parallel execution
@@ -368,44 +362,39 @@ export function useAgentExecution(
let responseText = '';
let synopsisUsageStats: UsageStats | undefined;
let cleanupData: (() => void) | undefined;
let cleanupSessionId: (() => void) | undefined;
let cleanupExit: (() => void) | undefined;
let cleanupUsage: (() => void) | undefined;
// Array to collect cleanup functions as listeners are registered
const cleanupFns: (() => void)[] = [];
const cleanup = () => {
cleanupData?.();
cleanupSessionId?.();
cleanupExit?.();
cleanupUsage?.();
cleanupFns.forEach(fn => fn());
};
cleanupData = window.maestro.process.onData((sid: string, data: string) => {
cleanupFns.push(window.maestro.process.onData((sid: string, data: string) => {
if (sid === targetSessionId) {
responseText += data;
}
});
}));
cleanupSessionId = window.maestro.process.onSessionId((sid: string, capturedId: string) => {
cleanupFns.push(window.maestro.process.onSessionId((sid: string, capturedId: string) => {
if (sid === targetSessionId) {
agentSessionId = capturedId;
}
});
}));
// Capture usage stats for this synopsis request
cleanupUsage = window.maestro.process.onUsage((sid: string, usageStats) => {
cleanupFns.push(window.maestro.process.onUsage((sid: string, usageStats) => {
if (sid === targetSessionId) {
// Accumulate usage stats (there may be multiple events)
synopsisUsageStats = accumulateUsageStats(synopsisUsageStats, usageStats);
}
});
}));
cleanupExit = window.maestro.process.onExit((sid: string) => {
cleanupFns.push(window.maestro.process.onExit((sid: string) => {
if (sid === targetSessionId) {
cleanup();
resolve({ success: true, response: responseText, agentSessionId, usageStats: synopsisUsageStats });
}
});
}));
// Spawn with session resume - the IPC handler will use the agent's resumeArgs builder
const commandToUse = agent.path || agent.command;

View File

@@ -20,18 +20,16 @@
*/
import { useState, useCallback, useRef, useMemo } from 'react';
import type { Session, AITab, LogEntry, ToolType } from '../../types';
import type { Session, AITab, LogEntry } from '../../types';
import type {
MergeResult,
GroomingProgress,
ContextSource,
MergeRequest,
} from '../../types/contextMerge';
import type { MergeOptions } from '../../components/MergeSessionModal';
import {
ContextGroomingService,
contextGroomingService,
type GroomingResult,
} from '../../services/contextGroomer';
import { extractTabContext } from '../../utils/contextExtractor';
import { createMergedSession, getActiveTab } from '../../utils/tabHelpers';
@@ -148,7 +146,7 @@ function getSessionDisplayName(session: Session): string {
/**
* Get the display name for a tab
*/
function getTabDisplayName(tab: AITab): string {
function _getTabDisplayName(tab: AITab): string {
if (tab.name) return tab.name;
if (tab.agentSessionId) {
return tab.agentSessionId.split('-')[0].toUpperCase();
@@ -161,9 +159,9 @@ function getTabDisplayName(tab: AITab): string {
*/
function generateMergedSessionName(
sourceSession: Session,
sourceTab: AITab,
_sourceTab: AITab,
targetSession: Session,
targetTab: AITab
_targetTab: AITab
): string {
const sourceName = getSessionDisplayName(sourceSession);
const targetName = getSessionDisplayName(targetSession);
@@ -844,11 +842,6 @@ export function useMergeSessionWithSessions(
}));
// Log merge operation to history
const sourceNames = [
getSessionDisplayName(sourceSession),
getSessionDisplayName(targetSession),
].filter((name, i, arr) => arr.indexOf(name) === i);
try {
await window.maestro.history.add({
id: generateId(),

View File

@@ -23,7 +23,6 @@ import type { Session, AITab, LogEntry, ToolType } from '../../types';
import type {
MergeResult,
GroomingProgress,
ContextSource,
MergeRequest,
} from '../../types/contextMerge';
import type { SendToAgentOptions } from '../../components/SendToAgentModal';
@@ -35,7 +34,7 @@ import {
getAgentDisplayName,
} from '../../services/contextGroomer';
import { extractTabContext } from '../../utils/contextExtractor';
import { createMergedSession, getActiveTab } from '../../utils/tabHelpers';
import { createMergedSession } from '../../utils/tabHelpers';
import { classifyTransferError } from '../../components/TransferErrorModal';
import { generateId } from '../../utils/ids';
@@ -126,7 +125,7 @@ function getSessionDisplayName(session: Session): string {
/**
* Get the display name for a tab
*/
function getTabDisplayName(tab: AITab): string {
function _getTabDisplayName(tab: AITab): string {
if (tab.name) return tab.name;
if (tab.agentSessionId) {
return tab.agentSessionId.split('-')[0].toUpperCase();

View File

@@ -663,9 +663,6 @@ export function useBatchProcessor({
// Track if any tasks were processed in this iteration
let anyTasksProcessedThisIteration = false;
// Track tasks completed in non-reset documents this iteration
// This is critical for loop mode: if only reset docs have tasks, we'd loop forever
let tasksCompletedInNonResetDocs = 0;
// Process each document in order
for (let docIndex = 0; docIndex < documents.length; docIndex++) {
@@ -846,9 +843,8 @@ export function useBatchProcessor({
}
// Track non-reset document completions for loop exit logic
if (!docEntry.resetOnCompletion) {
tasksCompletedInNonResetDocs += tasksCompletedThisRun;
}
// (This tracking is intentionally a no-op for now - kept for future loop mode enhancements)
void (!docEntry.resetOnCompletion ? tasksCompletedThisRun : 0);
// Update progress state
if (addedUncheckedTasks > 0) {

View File

@@ -55,7 +55,7 @@ export function useAtMentionCompletion(session: Session | null): UseAtMentionCom
const files: { name: string; type: 'file' | 'folder'; path: string }[] = [];
// Traverse the Auto Run tree (similar to fileTree traversal)
const traverse = (nodes: AutoRunTreeNode[], currentPath = '') => {
const traverse = (nodes: AutoRunTreeNode[], _currentPath = '') => {
for (const node of nodes) {
// Auto Run tree already has the path property, but we need to add .md extension for files
const displayPath = node.type === 'file' ? `${node.path}.md` : node.path;

View File

@@ -1,5 +1,5 @@
import { useCallback, useRef } from 'react';
import type { Session, SessionState, LogEntry, QueuedItem, AITab, CustomAICommand, BatchRunState } from '../../types';
import type { Session, SessionState, LogEntry, QueuedItem, CustomAICommand, BatchRunState } from '../../types';
import { getActiveTab } from '../../utils/tabHelpers';
import { generateId } from '../../utils/ids';
import { substituteTemplateVariables } from '../../utils/templateVariables';
@@ -165,7 +165,7 @@ export function useInputProcessing(deps: UseInputProcessingDeps): UseInputProces
// Ignore git errors
}
}
const substitutedPrompt = substituteTemplateVariables(matchingCustomCommand.prompt, {
substituteTemplateVariables(matchingCustomCommand.prompt, {
session: activeSession,
gitBranch,
});

View File

@@ -91,7 +91,7 @@ export function useTemplateAutocomplete({
document.body.appendChild(mirror);
const textareaRect = textarea.getBoundingClientRect();
const _textareaRect = textarea.getBoundingClientRect();
const spanRect = span.getBoundingClientRect();
const mirrorRect = mirror.getBoundingClientRect();

View File

@@ -228,7 +228,7 @@ export function useKeyboardNavigation(
const totalSessions = sessions.length;
// Helper to check if a session is in a collapsed group
const isInCollapsedGroup = (session: Session) => {
const _isInCollapsedGroup = (session: Session) => {
if (!session.groupId) return false;
const group = currentGroups.find(g => g.id === session.groupId);
return group?.collapsed ?? false;
@@ -404,7 +404,7 @@ export function useKeyboardNavigation(
if (currentIndex !== -1) {
setSelectedSidebarIndex(currentIndex);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [activeSessionId]); // Intentionally excluding sortedSessions - see comment above
return {

View File

@@ -22,7 +22,7 @@ export const DEFAULT_BATCH_FLUSH_INTERVAL = 150;
/**
* Types of updates that can be batched
*/
type UpdateType =
type _UpdateType =
| { type: 'appendLog'; sessionId: string; tabId: string | null; isAi: boolean; data: string; isStderr?: boolean }
| { type: 'setStatus'; sessionId: string; tabId: string | null; status: SessionState }
| { type: 'setTabStatus'; sessionId: string; tabId: string; status: 'idle' | 'busy' }
@@ -164,7 +164,7 @@ export function useBatchedSessionUpdates(
let shellStdoutTimestamp = 0;
let shellStderrTimestamp = 0;
for (const [key, logAcc] of acc.logAccumulators) {
for (const [_key, logAcc] of acc.logAccumulators) {
const combinedData = logAcc.chunks.join('');
if (!combinedData) continue;

View File

@@ -67,7 +67,7 @@ export function useGroupManagement(
deps: UseGroupManagementDeps
): UseGroupManagementReturn {
const {
groups,
groups: _groups,
setGroups,
setSessions,
draggingSessionId,

View File

@@ -48,7 +48,7 @@
* ```
*/
import { useState, useCallback, useRef, useMemo } from 'react';
import { useState, useCallback, useRef } from 'react';
import { useThrottledCallback } from '../utils';
export interface UseScrollPositionOptions {

View File

@@ -15,8 +15,8 @@
*/
import type { ToolType } from '../../shared/types';
import type { SummarizeRequest, SummarizeProgress, SummarizeResult } from '../types/contextMerge';
import type { LogEntry, AITab, Session } from '../types';
import type { SummarizeRequest, SummarizeProgress } from '../types/contextMerge';
import type { LogEntry } from '../types';
import { formatLogsForGrooming, parseGroomedOutput, estimateTextTokenCount } from '../utils/contextExtractor';
import { contextSummarizePrompt } from '../../prompts';

View File

@@ -22,7 +22,6 @@ export type {
// Import for extension in this file
import type {
WorktreeConfig as BaseWorktreeConfig,
BatchRunConfig as BaseBatchRunConfig,
BatchDocumentEntry,
UsageStats,
ToolType,

View File

@@ -325,7 +325,7 @@ export function createMarkdownComponents(options: MarkdownComponentsOptions): Pa
strong: ({ children }: any) => React.createElement('strong', null, withHighlight(children)),
em: ({ children }: any) => React.createElement('em', null, withHighlight(children)),
// Code block with syntax highlighting and custom language support
code: ({ node, inline, className, children, ...props }: any) => {
code: ({ node: _node, inline, className, children, ...props }: any) => {
const match = (className || '').match(/language-(\w+)/);
const language = match ? match[1] : 'text';
const codeContent = String(children).replace(/\n$/, '');
@@ -360,14 +360,14 @@ export function createMarkdownComponents(options: MarkdownComponentsOptions): Pa
// Custom image renderer if provided
if (imageRenderer) {
components.img = ({ node, src, alt, ...props }: any) => {
components.img = ({ node: _node, src, alt, ...props }: any) => {
return React.createElement(imageRenderer, { src, alt, ...props });
};
}
// Link handler - supports both internal file links and external links
if (onFileClick || onExternalLinkClick) {
components.a = ({ node, href, children, ...props }: any) => {
components.a = ({ node: _node, href, children, ...props }: any) => {
// Check for maestro-file:// protocol OR data-maestro-file attribute
// (data attribute is fallback when rehype strips custom protocols)
const dataFilePath = props['data-maestro-file'];

View File

@@ -8,9 +8,8 @@
* - Handling agent-specific initialization
*/
import type { Session, ToolType, ProcessConfig, AgentConfig } from '../types';
import type { Session, ToolType, ProcessConfig } from '../types';
import { createMergedSession } from './tabHelpers';
import { generateId } from './ids';
/**
* Options for creating a session for a specific agent type.

View File

@@ -101,7 +101,7 @@ export const filterTextByLinesHelper = (
});
return filteredLines.join('\n');
}
} catch (error) {
} catch {
// Fall back to plain text search if regex is invalid
const lowerQuery = query.toLowerCase();
const filteredLines = lines.filter(line => {

View File

@@ -88,7 +88,7 @@ function getDefaultThemeForScheme(colorScheme: ColorSchemePreference): Theme {
}
// Keep backwards compatibility - export defaultTheme as alias for defaultDarkTheme
const defaultTheme = defaultDarkTheme;
const _defaultTheme = defaultDarkTheme;
const ThemeContext = createContext<ThemeContextValue | null>(null);

View File

@@ -156,7 +156,7 @@ export function usePullToRefresh(options: UsePullToRefreshOptions): UsePullToRef
* Handle touch end
*/
const handleTouchEnd = useCallback(
async (e: React.TouchEvent) => {
async (_e: React.TouchEvent) => {
if (!enabled || isRefreshing || !isPulling.current) {
isPulling.current = false;
return;

View File

@@ -90,7 +90,7 @@ const AUTO_SUBMIT_DELAY = 50;
*/
export function useSlashCommandAutocomplete({
inputValue,
isControlled,
isControlled: _isControlled,
onChange,
onSubmit,
inputRef,

View File

@@ -455,7 +455,7 @@ function buildWebSocketUrl(baseUrl?: string, sessionId?: string): string {
export function useWebSocket(options: UseWebSocketOptions = {}): UseWebSocketReturn {
const {
url: baseUrl,
token,
token: _token,
autoReconnect = DEFAULT_OPTIONS.autoReconnect,
maxReconnectAttempts = DEFAULT_OPTIONS.maxReconnectAttempts,
reconnectDelay = DEFAULT_OPTIONS.reconnectDelay,

View File

@@ -26,7 +26,7 @@ import { DEFAULT_SLASH_COMMANDS, type SlashCommand } from './SlashCommandAutocom
// CommandHistoryDrawer and RecentCommandChips removed for simpler mobile UI
import { ResponseViewer, type ResponseItem } from './ResponseViewer';
import { OfflineQueueBanner } from './OfflineQueueBanner';
import { MessageHistory, type LogEntry } from './MessageHistory';
import { MessageHistory } from './MessageHistory';
import { AutoRunIndicator } from './AutoRunIndicator';
import { TabBar } from './TabBar';
import { TabSearchModal } from './TabSearchModal';
@@ -279,7 +279,7 @@ export default function MobileApp() {
const {
isSmallScreen,
savedState,
savedScrollState,
savedScrollState: _savedScrollState,
persistViewState,
persistHistoryState,
persistSessionSelection,
@@ -325,7 +325,7 @@ export default function MobileApp() {
const {
addUnread: addUnreadResponse,
markAllRead: markAllResponsesRead,
unreadCount,
unreadCount: _unreadCount,
} = useUnreadBadge({
autoClearOnVisible: true, // Clear badge when user opens the app
onCountChange: (count) => {
@@ -528,7 +528,7 @@ export default function MobileApp() {
window.removeEventListener('load', onLoad);
}
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
// Update sendRef after WebSocket is initialized (for offline queue)
@@ -746,7 +746,7 @@ export default function MobileApp() {
}, [sessions]);
// Handle expanding response to full-screen viewer
const handleExpandResponse = useCallback((response: LastResponsePreview) => {
const _handleExpandResponse = useCallback((response: LastResponsePreview) => {
setSelectedResponse(response);
// Find the index of this response in allResponses

View File

@@ -379,7 +379,7 @@ export function CommandHistoryDrawer({
* Handle touch end - determine if should snap open or closed
*/
const handleTouchEnd = useCallback(
(e: React.TouchEvent) => {
(_e: React.TouchEvent) => {
if (!isDragging.current) return;
isDragging.current = false;

View File

@@ -587,7 +587,7 @@ export function CommandInputBar({
overflowX: 'hidden',
wordWrap: 'break-word',
}}
onBlur={(e) => {
onBlur={(_e) => {
// Delay collapse to allow click on send button
setTimeout(() => {
if (!containerRef.current?.contains(document.activeElement)) {

View File

@@ -13,7 +13,7 @@
* - Fades out for long commands with ellipsis
*/
import React, { useCallback, useRef, useEffect } from 'react';
import React, { useCallback, useRef } from 'react';
import { useThemeColors } from '../components/ThemeProvider';
import { triggerHaptic, HAPTIC_PATTERNS } from './constants';
import type { CommandHistoryEntry } from '../hooks/useCommandHistory';

View File

@@ -165,7 +165,7 @@ function SessionPill({ session, isActive, onSelect, onLongPress }: SessionPillPr
onTouchMove={handleTouchMove}
onTouchEnd={handleTouchEnd}
onTouchCancel={handleTouchCancel}
onClick={(e) => {
onClick={(_e) => {
// For non-touch devices (mouse), use onClick
// Touch devices will have already handled via touch events
if (!('ontouchstart' in window)) {