mirror of
https://github.com/jlengrand/Maestro.git
synced 2026-03-10 08:31:19 +00:00
MAESTRO: Add file tab content management with SSH loading state support
- Fix MainPanel to use activeFileTab.content as source (was using empty editContent) - Add sshRemoteId and isLoading fields to FilePreviewTab interface - Update handleOpenFileTab to accept optional sshRemoteId parameter - Add handleOpenFileTabAsync for SSH files with async loading: - Creates tab immediately with loading state - Fetches content asynchronously - Updates tab when content loaded (or removes on error) - Add loading state UI in MainPanel for file tabs - Add tests for file tab content storage and SSH loading support This enables proper file content management for the unified tab system, with support for SSH remote files that need async loading.
This commit is contained in:
@@ -5157,9 +5157,11 @@ You are taking over this conversation. Based on the context above, provide a bri
|
||||
* Open a file preview tab. If a tab with the same path already exists, select it.
|
||||
* Otherwise, create a new FilePreviewTab, add it to filePreviewTabs and unifiedTabOrder,
|
||||
* and set it as the active file tab (deselecting any active AI tab).
|
||||
*
|
||||
* For SSH remote files, pass sshRemoteId so content can be re-fetched if needed.
|
||||
*/
|
||||
const handleOpenFileTab = useCallback(
|
||||
(file: { path: string; name: string; content: string }) => {
|
||||
(file: { path: string; name: string; content: string; sshRemoteId?: string }) => {
|
||||
setSessions((prev) =>
|
||||
prev.map((s) => {
|
||||
if (s.id !== activeSessionIdRef.current) return s;
|
||||
@@ -5167,9 +5169,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 - select it (deselect AI tab)
|
||||
// Tab exists - update content 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
|
||||
);
|
||||
return {
|
||||
...s,
|
||||
filePreviewTabs: updatedTabs,
|
||||
activeFileTabId: existingTab.id,
|
||||
activeTabId: s.activeTabId, // Keep AI tab reference but it's not visually active
|
||||
};
|
||||
@@ -5195,6 +5203,8 @@ You are taking over this conversation. Based on the context above, provide a bri
|
||||
editMode: false,
|
||||
editContent: undefined,
|
||||
createdAt: Date.now(),
|
||||
sshRemoteId: file.sshRemoteId,
|
||||
isLoading: false, // Content is already loaded when this is called
|
||||
};
|
||||
|
||||
// Create the unified tab reference
|
||||
@@ -5214,6 +5224,138 @@ You are taking over this conversation. Based on the context above, provide a bri
|
||||
[]
|
||||
);
|
||||
|
||||
/**
|
||||
* Open a file tab with async content loading (for SSH remote files).
|
||||
* Creates the tab immediately with loading state, fetches content, then updates the tab.
|
||||
* If a tab for this path already exists, selects it and optionally refreshes content.
|
||||
*/
|
||||
const handleOpenFileTabAsync = useCallback(
|
||||
async (file: { path: string; name: string; sshRemoteId?: string }) => {
|
||||
const currentSession = sessionsRef.current.find(
|
||||
(s) => s.id === activeSessionIdRef.current
|
||||
);
|
||||
if (!currentSession) return;
|
||||
|
||||
// Get SSH remote ID from the file or from session (convert null to undefined)
|
||||
const sshRemoteId =
|
||||
file.sshRemoteId ||
|
||||
currentSession.sshRemoteId ||
|
||||
currentSession.sessionSshRemoteConfig?.remoteId ||
|
||||
undefined;
|
||||
|
||||
// Check if a tab with this path already exists
|
||||
const existingTab = currentSession.filePreviewTabs.find(
|
||||
(tab) => tab.path === file.path
|
||||
);
|
||||
|
||||
if (existingTab) {
|
||||
// Tab exists - just select it
|
||||
setSessions((prev) =>
|
||||
prev.map((s) =>
|
||||
s.id === currentSession.id
|
||||
? { ...s, activeFileTabId: existingTab.id }
|
||||
: s
|
||||
)
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Create a new file tab with loading state
|
||||
const newTabId = generateId();
|
||||
const extension = file.name.includes('.')
|
||||
? '.' + file.name.split('.').pop()
|
||||
: '';
|
||||
const nameWithoutExtension = extension
|
||||
? file.name.slice(0, -extension.length)
|
||||
: file.name;
|
||||
|
||||
const newFileTab: FilePreviewTab = {
|
||||
id: newTabId,
|
||||
path: file.path,
|
||||
name: nameWithoutExtension,
|
||||
extension,
|
||||
content: '', // Will be populated after fetch
|
||||
scrollTop: 0,
|
||||
searchQuery: '',
|
||||
editMode: false,
|
||||
editContent: undefined,
|
||||
createdAt: Date.now(),
|
||||
sshRemoteId,
|
||||
isLoading: true, // Show loading state
|
||||
};
|
||||
|
||||
const newTabRef: UnifiedTabRef = { type: 'file', id: newTabId };
|
||||
|
||||
// Add the tab in loading state
|
||||
setSessions((prev) =>
|
||||
prev.map((s) => {
|
||||
if (s.id !== currentSession.id) return s;
|
||||
return {
|
||||
...s,
|
||||
filePreviewTabs: [...s.filePreviewTabs, newFileTab],
|
||||
unifiedTabOrder: [...s.unifiedTabOrder, newTabRef],
|
||||
activeFileTabId: newTabId,
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
// Fetch content asynchronously
|
||||
try {
|
||||
const content = await window.maestro.fs.readFile(file.path, sshRemoteId);
|
||||
// Update the tab with loaded content
|
||||
setSessions((prev) =>
|
||||
prev.map((s) => {
|
||||
if (s.id !== currentSession.id) return s;
|
||||
return {
|
||||
...s,
|
||||
filePreviewTabs: s.filePreviewTabs.map((tab) =>
|
||||
tab.id === newTabId
|
||||
? { ...tab, content, isLoading: false }
|
||||
: tab
|
||||
),
|
||||
};
|
||||
})
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('[handleOpenFileTabAsync] Failed to load file:', error);
|
||||
// Remove the tab on error (or could show error state)
|
||||
setSessions((prev) =>
|
||||
prev.map((s) => {
|
||||
if (s.id !== currentSession.id) return s;
|
||||
// Remove the failed tab
|
||||
const updatedFileTabs = s.filePreviewTabs.filter(
|
||||
(tab) => tab.id !== newTabId
|
||||
);
|
||||
const updatedTabOrder = s.unifiedTabOrder.filter(
|
||||
(ref) => !(ref.type === 'file' && ref.id === newTabId)
|
||||
);
|
||||
// Select next available tab
|
||||
const remainingTabs = updatedTabOrder.length;
|
||||
let newActiveFileTabId: string | null = null;
|
||||
let newActiveTabId = s.activeTabId;
|
||||
if (remainingTabs > 0) {
|
||||
const lastRef = updatedTabOrder[remainingTabs - 1];
|
||||
if (lastRef.type === 'file') {
|
||||
newActiveFileTabId = lastRef.id;
|
||||
} else {
|
||||
newActiveTabId = lastRef.id;
|
||||
newActiveFileTabId = null;
|
||||
}
|
||||
}
|
||||
return {
|
||||
...s,
|
||||
filePreviewTabs: updatedFileTabs,
|
||||
unifiedTabOrder: updatedTabOrder,
|
||||
activeFileTabId: newActiveFileTabId,
|
||||
activeTabId: newActiveTabId,
|
||||
};
|
||||
})
|
||||
);
|
||||
}
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
/**
|
||||
* Close a file preview tab. Removes it from filePreviewTabs and unifiedTabOrder.
|
||||
* If this was the active file tab, selects the next tab in unifiedTabOrder (could be AI or file).
|
||||
|
||||
Reference in New Issue
Block a user