mirror of
https://github.com/jlengrand/Maestro.git
synced 2026-03-10 08:31:19 +00:00
lints and tests pass
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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: [] };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -677,7 +677,7 @@ export class AgentDetector {
|
||||
}
|
||||
|
||||
return { exists: false };
|
||||
} catch (_error) {
|
||||
} catch {
|
||||
return { exists: false };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 [];
|
||||
}
|
||||
|
||||
@@ -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}` };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -63,7 +63,7 @@ export const GroupChatInput = React.memo(function GroupChatInput({
|
||||
theme,
|
||||
state,
|
||||
onSend,
|
||||
participants,
|
||||
participants: _participants,
|
||||
sessions,
|
||||
groupChatId,
|
||||
draftMessage,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -46,7 +46,7 @@ export function HistoryDetailModal({
|
||||
theme,
|
||||
entry,
|
||||
onClose,
|
||||
onJumpToAgentSession,
|
||||
onJumpToAgentSession: _onJumpToAgentSession,
|
||||
onResumeSession,
|
||||
onDelete,
|
||||
onUpdate,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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'];
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -338,7 +338,7 @@ export function DirectorySelectionScreen({ theme }: DirectorySelectionScreenProp
|
||||
/**
|
||||
* Handle back button click
|
||||
*/
|
||||
const handleBack = useCallback(() => {
|
||||
const _handleBack = useCallback(() => {
|
||||
previousStep();
|
||||
}, [previousStep]);
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -67,7 +67,7 @@ export function useGroupManagement(
|
||||
deps: UseGroupManagementDeps
|
||||
): UseGroupManagementReturn {
|
||||
const {
|
||||
groups,
|
||||
groups: _groups,
|
||||
setGroups,
|
||||
setSessions,
|
||||
draggingSessionId,
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
* ```
|
||||
*/
|
||||
|
||||
import { useState, useCallback, useRef, useMemo } from 'react';
|
||||
import { useState, useCallback, useRef } from 'react';
|
||||
import { useThrottledCallback } from '../utils';
|
||||
|
||||
export interface UseScrollPositionOptions {
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -22,7 +22,6 @@ export type {
|
||||
// Import for extension in this file
|
||||
import type {
|
||||
WorktreeConfig as BaseWorktreeConfig,
|
||||
BatchRunConfig as BaseBatchRunConfig,
|
||||
BatchDocumentEntry,
|
||||
UsageStats,
|
||||
ToolType,
|
||||
|
||||
@@ -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'];
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -90,7 +90,7 @@ const AUTO_SUBMIT_DELAY = 50;
|
||||
*/
|
||||
export function useSlashCommandAutocomplete({
|
||||
inputValue,
|
||||
isControlled,
|
||||
isControlled: _isControlled,
|
||||
onChange,
|
||||
onSubmit,
|
||||
inputRef,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
Reference in New Issue
Block a user