mirror of
https://github.com/jlengrand/Maestro.git
synced 2026-03-10 08:31:19 +00:00
MAESTRO: Add file tab content refresh with mtime tracking
- Add `lastModified: number` field to FilePreviewTab interface to track when content was loaded from disk - Add `fileTabAutoRefreshEnabled` setting (default: disabled) to control whether file tabs auto-refresh when switched to - Update handleOpenFileTab to accept and store lastModified - Update handleOpenFileTabAsync to fetch file stat and set lastModified from the file's actual modification time - Modify handleSelectFileTab to check if file changed on disk when auto-refresh is enabled: - Compares current file mtime with stored lastModified - Refreshes content if file was modified since last load - Skips refresh if tab has unsaved edits to prevent data loss - Update all FilePreviewTab test fixtures with lastModified field
This commit is contained in:
@@ -2665,6 +2665,7 @@ describe('FileTab overlay menu', () => {
|
||||
editMode: false,
|
||||
editContent: undefined,
|
||||
createdAt: Date.now(),
|
||||
lastModified: Date.now(),
|
||||
};
|
||||
|
||||
const unifiedTabs = [
|
||||
@@ -3109,6 +3110,7 @@ describe('Unified tabs drag and drop', () => {
|
||||
editMode: false,
|
||||
editContent: undefined,
|
||||
createdAt: Date.now(),
|
||||
lastModified: Date.now(),
|
||||
};
|
||||
|
||||
const fileTab2: FilePreviewTab = {
|
||||
@@ -3122,6 +3124,7 @@ describe('Unified tabs drag and drop', () => {
|
||||
editMode: false,
|
||||
editContent: undefined,
|
||||
createdAt: Date.now() + 1,
|
||||
lastModified: Date.now() + 1,
|
||||
};
|
||||
|
||||
// Unified tabs: AI, File, AI, File
|
||||
@@ -3655,6 +3658,7 @@ describe('Unified active tab styling consistency', () => {
|
||||
editMode: false,
|
||||
editContent: undefined,
|
||||
createdAt: Date.now(),
|
||||
lastModified: Date.now(),
|
||||
};
|
||||
|
||||
const unifiedTabs = [
|
||||
@@ -3723,6 +3727,7 @@ describe('Unified active tab styling consistency', () => {
|
||||
editMode: false,
|
||||
editContent: undefined,
|
||||
createdAt: Date.now(),
|
||||
lastModified: Date.now(),
|
||||
};
|
||||
|
||||
const unifiedTabs = [
|
||||
@@ -3768,6 +3773,7 @@ describe('Unified active tab styling consistency', () => {
|
||||
editMode: false,
|
||||
editContent: undefined,
|
||||
createdAt: Date.now(),
|
||||
lastModified: Date.now(),
|
||||
};
|
||||
|
||||
const unifiedTabs = [
|
||||
@@ -3823,6 +3829,7 @@ describe('File tab content and SSH support', () => {
|
||||
editMode: false,
|
||||
editContent: undefined,
|
||||
createdAt: Date.now(),
|
||||
lastModified: Date.now(),
|
||||
};
|
||||
|
||||
const unifiedTabs = [
|
||||
@@ -3864,6 +3871,7 @@ describe('File tab content and SSH support', () => {
|
||||
editMode: false,
|
||||
editContent: undefined,
|
||||
createdAt: Date.now(),
|
||||
lastModified: Date.now(),
|
||||
sshRemoteId: 'ssh-remote-123', // SSH remote ID for re-fetching
|
||||
isLoading: false,
|
||||
};
|
||||
@@ -3908,6 +3916,7 @@ describe('File tab content and SSH support', () => {
|
||||
editMode: false,
|
||||
editContent: undefined,
|
||||
createdAt: Date.now(),
|
||||
lastModified: 0, // Not yet loaded
|
||||
sshRemoteId: 'ssh-remote-456',
|
||||
isLoading: true, // Currently loading content
|
||||
};
|
||||
@@ -3954,6 +3963,7 @@ describe('File tab content and SSH support', () => {
|
||||
editMode: true,
|
||||
editContent: editedContent, // Has unsaved edits
|
||||
createdAt: Date.now(),
|
||||
lastModified: Date.now(),
|
||||
};
|
||||
|
||||
const unifiedTabs = [
|
||||
|
||||
@@ -555,6 +555,9 @@ function MaestroConsoleInner() {
|
||||
|
||||
// Tab naming settings
|
||||
automaticTabNamingEnabled,
|
||||
|
||||
// File tab refresh settings
|
||||
fileTabAutoRefreshEnabled,
|
||||
} = settings;
|
||||
|
||||
// --- KEYBOARD SHORTCUT HELPERS ---
|
||||
@@ -3647,12 +3650,14 @@ function MaestroConsoleInner() {
|
||||
const speckitCommandsRef = useRef(speckitCommands);
|
||||
const openspecCommandsRef = useRef(openspecCommands);
|
||||
const automaticTabNamingEnabledRef = useRef(automaticTabNamingEnabled);
|
||||
const fileTabAutoRefreshEnabledRef = useRef(fileTabAutoRefreshEnabled);
|
||||
addToastRef.current = addToast;
|
||||
updateGlobalStatsRef.current = updateGlobalStats;
|
||||
customAICommandsRef.current = customAICommands;
|
||||
speckitCommandsRef.current = speckitCommands;
|
||||
openspecCommandsRef.current = openspecCommands;
|
||||
automaticTabNamingEnabledRef.current = automaticTabNamingEnabled;
|
||||
fileTabAutoRefreshEnabledRef.current = fileTabAutoRefreshEnabled;
|
||||
|
||||
// Note: spawnBackgroundSynopsisRef and spawnAgentWithPromptRef are now provided by useAgentExecution hook
|
||||
// Note: addHistoryEntryRef is now provided by useAgentSessionManagement hook
|
||||
@@ -5161,7 +5166,7 @@ You are taking over this conversation. Based on the context above, provide a bri
|
||||
* For SSH remote files, pass sshRemoteId so content can be re-fetched if needed.
|
||||
*/
|
||||
const handleOpenFileTab = useCallback(
|
||||
(file: { path: string; name: string; content: string; sshRemoteId?: string }) => {
|
||||
(file: { path: string; name: string; content: string; sshRemoteId?: string; lastModified?: number }) => {
|
||||
setSessions((prev) =>
|
||||
prev.map((s) => {
|
||||
if (s.id !== activeSessionIdRef.current) return s;
|
||||
@@ -5169,10 +5174,15 @@ You are taking over this conversation. Based on the context above, provide a bri
|
||||
// Check if a tab with this path already exists
|
||||
const existingTab = s.filePreviewTabs.find((tab) => tab.path === file.path);
|
||||
if (existingTab) {
|
||||
// Tab exists - update content if provided (e.g., after re-fetch) and select it
|
||||
// Tab exists - update content and lastModified if provided (e.g., after re-fetch) and select it
|
||||
const updatedTabs = s.filePreviewTabs.map((tab) =>
|
||||
tab.id === existingTab.id
|
||||
? { ...tab, content: file.content, isLoading: false }
|
||||
? {
|
||||
...tab,
|
||||
content: file.content,
|
||||
lastModified: file.lastModified ?? tab.lastModified,
|
||||
isLoading: false,
|
||||
}
|
||||
: tab
|
||||
);
|
||||
return {
|
||||
@@ -5203,6 +5213,7 @@ You are taking over this conversation. Based on the context above, provide a bri
|
||||
editMode: false,
|
||||
editContent: undefined,
|
||||
createdAt: Date.now(),
|
||||
lastModified: file.lastModified ?? Date.now(), // Use file mtime or current time as fallback
|
||||
sshRemoteId: file.sshRemoteId,
|
||||
isLoading: false, // Content is already loaded when this is called
|
||||
};
|
||||
@@ -5280,6 +5291,7 @@ You are taking over this conversation. Based on the context above, provide a bri
|
||||
editMode: false,
|
||||
editContent: undefined,
|
||||
createdAt: Date.now(),
|
||||
lastModified: 0, // Will be populated after fetch
|
||||
sshRemoteId,
|
||||
isLoading: true, // Show loading state
|
||||
};
|
||||
@@ -5299,10 +5311,15 @@ You are taking over this conversation. Based on the context above, provide a bri
|
||||
})
|
||||
);
|
||||
|
||||
// Fetch content asynchronously
|
||||
// Fetch content and stat asynchronously
|
||||
try {
|
||||
const content = await window.maestro.fs.readFile(file.path, sshRemoteId);
|
||||
// Update the tab with loaded content
|
||||
const [content, stat] = await Promise.all([
|
||||
window.maestro.fs.readFile(file.path, sshRemoteId),
|
||||
window.maestro.fs.stat(file.path, sshRemoteId),
|
||||
]);
|
||||
// Parse lastModified from stat (modifiedAt is an ISO string)
|
||||
const lastModified = stat?.modifiedAt ? new Date(stat.modifiedAt).getTime() : Date.now();
|
||||
// Update the tab with loaded content and lastModified
|
||||
setSessions((prev) =>
|
||||
prev.map((s) => {
|
||||
if (s.id !== currentSession.id) return s;
|
||||
@@ -5310,7 +5327,7 @@ You are taking over this conversation. Based on the context above, provide a bri
|
||||
...s,
|
||||
filePreviewTabs: s.filePreviewTabs.map((tab) =>
|
||||
tab.id === newTabId
|
||||
? { ...tab, content, isLoading: false }
|
||||
? { ...tab, content, lastModified, isLoading: false }
|
||||
: tab
|
||||
),
|
||||
};
|
||||
@@ -5434,16 +5451,22 @@ You are taking over this conversation. Based on the context above, provide a bri
|
||||
/**
|
||||
* Select a file preview tab. This sets the file tab as active.
|
||||
* activeTabId is preserved to track the last active AI tab for when the user switches back.
|
||||
* If fileTabAutoRefreshEnabled setting is true, checks if file has changed on disk and refreshes content.
|
||||
*/
|
||||
const handleSelectFileTab = useCallback((tabId: string) => {
|
||||
const handleSelectFileTab = useCallback(async (tabId: string) => {
|
||||
const currentSession = sessionsRef.current.find(
|
||||
(s) => s.id === activeSessionIdRef.current
|
||||
);
|
||||
if (!currentSession) return;
|
||||
|
||||
// Verify the file tab exists
|
||||
const fileTab = currentSession.filePreviewTabs.find((tab) => tab.id === tabId);
|
||||
if (!fileTab) return;
|
||||
|
||||
// Set the tab as active immediately
|
||||
setSessions((prev) =>
|
||||
prev.map((s) => {
|
||||
if (s.id !== activeSessionIdRef.current) return s;
|
||||
|
||||
// Verify the file tab exists
|
||||
const fileTab = s.filePreviewTabs.find((tab) => tab.id === tabId);
|
||||
if (!fileTab) return s;
|
||||
|
||||
return {
|
||||
...s,
|
||||
activeFileTabId: tabId,
|
||||
@@ -5451,6 +5474,38 @@ You are taking over this conversation. Based on the context above, provide a bri
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
// If auto-refresh is enabled and tab has pending edits, skip refresh to avoid losing changes
|
||||
if (fileTabAutoRefreshEnabledRef.current && !fileTab.editContent) {
|
||||
try {
|
||||
// Get the current file stat to check if it has changed
|
||||
const stat = await window.maestro.fs.stat(fileTab.path, fileTab.sshRemoteId);
|
||||
if (!stat || !stat.modifiedAt) return;
|
||||
|
||||
const currentMtime = new Date(stat.modifiedAt).getTime();
|
||||
|
||||
// If file has been modified since we last loaded it, refresh content
|
||||
if (currentMtime > fileTab.lastModified) {
|
||||
const content = await window.maestro.fs.readFile(fileTab.path, fileTab.sshRemoteId);
|
||||
setSessions((prev) =>
|
||||
prev.map((s) => {
|
||||
if (s.id !== activeSessionIdRef.current) return s;
|
||||
return {
|
||||
...s,
|
||||
filePreviewTabs: s.filePreviewTabs.map((tab) =>
|
||||
tab.id === tabId
|
||||
? { ...tab, content, lastModified: currentMtime }
|
||||
: tab
|
||||
),
|
||||
};
|
||||
})
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
// Silently ignore refresh errors - the tab still shows previous content
|
||||
console.debug('[handleSelectFileTab] Auto-refresh failed:', error);
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
/**
|
||||
|
||||
@@ -346,6 +346,10 @@ export interface UseSettingsReturn {
|
||||
// Automatic tab naming settings
|
||||
automaticTabNamingEnabled: boolean;
|
||||
setAutomaticTabNamingEnabled: (value: boolean) => void;
|
||||
|
||||
// File tab auto-refresh settings
|
||||
fileTabAutoRefreshEnabled: boolean;
|
||||
setFileTabAutoRefreshEnabled: (value: boolean) => void;
|
||||
}
|
||||
|
||||
export function useSettings(): UseSettingsReturn {
|
||||
@@ -501,6 +505,9 @@ export function useSettings(): UseSettingsReturn {
|
||||
// Automatic tab naming settings
|
||||
const [automaticTabNamingEnabled, setAutomaticTabNamingEnabledState] = useState(true); // Default: enabled
|
||||
|
||||
// File tab auto-refresh settings
|
||||
const [fileTabAutoRefreshEnabled, setFileTabAutoRefreshEnabledState] = useState(false); // Default: disabled
|
||||
|
||||
// Wrapper functions that persist to electron-store
|
||||
// PERF: All wrapped in useCallback to prevent re-renders
|
||||
const setLlmProvider = useCallback((value: LLMProvider) => {
|
||||
@@ -1309,6 +1316,12 @@ export function useSettings(): UseSettingsReturn {
|
||||
window.maestro.settings.set('automaticTabNamingEnabled', value);
|
||||
}, []);
|
||||
|
||||
// File tab auto-refresh toggle
|
||||
const setFileTabAutoRefreshEnabled = useCallback((value: boolean) => {
|
||||
setFileTabAutoRefreshEnabledState(value);
|
||||
window.maestro.settings.set('fileTabAutoRefreshEnabled', value);
|
||||
}, []);
|
||||
|
||||
// Load settings from electron-store
|
||||
// This function is called on mount and on system resume (after sleep/suspend)
|
||||
// PERF: Use batch loading to reduce IPC calls from ~60 to 3
|
||||
@@ -1382,6 +1395,7 @@ export function useSettings(): UseSettingsReturn {
|
||||
const savedSshRemoteIgnorePatterns = allSettings['sshRemoteIgnorePatterns'];
|
||||
const savedSshRemoteHonorGitignore = allSettings['sshRemoteHonorGitignore'];
|
||||
const savedAutomaticTabNamingEnabled = allSettings['automaticTabNamingEnabled'];
|
||||
const savedFileTabAutoRefreshEnabled = allSettings['fileTabAutoRefreshEnabled'];
|
||||
|
||||
if (savedEnterToSendAI !== undefined) setEnterToSendAIState(savedEnterToSendAI as boolean);
|
||||
if (savedEnterToSendTerminal !== undefined)
|
||||
@@ -1741,6 +1755,11 @@ export function useSettings(): UseSettingsReturn {
|
||||
if (savedAutomaticTabNamingEnabled !== undefined) {
|
||||
setAutomaticTabNamingEnabledState(savedAutomaticTabNamingEnabled as boolean);
|
||||
}
|
||||
|
||||
// File tab auto-refresh settings
|
||||
if (savedFileTabAutoRefreshEnabled !== undefined) {
|
||||
setFileTabAutoRefreshEnabledState(savedFileTabAutoRefreshEnabled as boolean);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[Settings] Failed to load settings:', error);
|
||||
} finally {
|
||||
@@ -1917,6 +1936,8 @@ export function useSettings(): UseSettingsReturn {
|
||||
setSshRemoteHonorGitignore,
|
||||
automaticTabNamingEnabled,
|
||||
setAutomaticTabNamingEnabled,
|
||||
fileTabAutoRefreshEnabled,
|
||||
setFileTabAutoRefreshEnabled,
|
||||
}),
|
||||
[
|
||||
// State values
|
||||
@@ -2060,6 +2081,8 @@ export function useSettings(): UseSettingsReturn {
|
||||
setSshRemoteHonorGitignore,
|
||||
automaticTabNamingEnabled,
|
||||
setAutomaticTabNamingEnabled,
|
||||
fileTabAutoRefreshEnabled,
|
||||
setFileTabAutoRefreshEnabled,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
@@ -458,6 +458,7 @@ export interface FilePreviewTab {
|
||||
editMode: boolean; // Whether tab was in edit mode
|
||||
editContent: string | undefined; // Unsaved edit content (undefined if no pending changes)
|
||||
createdAt: number; // Timestamp for ordering
|
||||
lastModified: number; // Timestamp (ms) when file was last modified on disk (for refresh detection)
|
||||
// SSH remote support
|
||||
sshRemoteId?: string; // SSH remote ID for re-fetching content if needed
|
||||
isLoading?: boolean; // True while content is being loaded (for SSH remote files)
|
||||
|
||||
Reference in New Issue
Block a user