mirror of
https://github.com/jlengrand/Maestro.git
synced 2026-03-10 08:31:19 +00:00
fix(file-preview): stabilize fileTree prop to complete memoization chain
The fileTree prop was passed as `activeSession?.fileTree || []` which creates a new array reference on every render, defeating React.memo() on FilePreview during agent activity. Memoize it with useMemo so the reference only changes when the actual fileTree changes.
This commit is contained in:
@@ -2850,7 +2850,9 @@ function MaestroConsoleInner() {
|
||||
if (groupChatParsed.isGroupChat) {
|
||||
const groupChatId = groupChatParsed.groupChatId!;
|
||||
const isModeratorError = groupChatParsed.isModerator ?? false;
|
||||
const participantOrModerator = isModeratorError ? 'moderator' : groupChatParsed.participantName!;
|
||||
const participantOrModerator = isModeratorError
|
||||
? 'moderator'
|
||||
: groupChatParsed.participantName!;
|
||||
|
||||
console.log('[onAgentError] Group chat error received:', {
|
||||
rawSessionId: sessionId,
|
||||
@@ -3656,7 +3658,13 @@ function MaestroConsoleInner() {
|
||||
*/
|
||||
const handleOpenFileTab = useCallback(
|
||||
(
|
||||
file: { path: string; name: string; content: string; sshRemoteId?: string; lastModified?: number },
|
||||
file: {
|
||||
path: string;
|
||||
name: string;
|
||||
content: string;
|
||||
sshRemoteId?: string;
|
||||
lastModified?: number;
|
||||
},
|
||||
options?: {
|
||||
/** If true, create new tab adjacent to current file tab. If false, replace current file tab content. Default: true (create new tab) */
|
||||
openInNewTab?: boolean;
|
||||
@@ -3694,9 +3702,7 @@ function MaestroConsoleInner() {
|
||||
if (!openInNewTab && s.activeFileTabId) {
|
||||
const currentTabId = s.activeFileTabId;
|
||||
const currentTab = s.filePreviewTabs.find((tab) => tab.id === currentTabId);
|
||||
const extension = file.name.includes('.')
|
||||
? '.' + file.name.split('.').pop()
|
||||
: '';
|
||||
const extension = file.name.includes('.') ? '.' + file.name.split('.').pop() : '';
|
||||
const nameWithoutExtension = extension
|
||||
? file.name.slice(0, -extension.length)
|
||||
: file.name;
|
||||
@@ -3770,9 +3776,7 @@ function MaestroConsoleInner() {
|
||||
|
||||
// Create a new file preview tab
|
||||
const newTabId = generateId();
|
||||
const extension = file.name.includes('.')
|
||||
? '.' + file.name.split('.').pop()
|
||||
: '';
|
||||
const extension = file.name.includes('.') ? '.' + file.name.split('.').pop() : '';
|
||||
const nameWithoutExtension = extension
|
||||
? file.name.slice(0, -extension.length)
|
||||
: file.name;
|
||||
@@ -4741,19 +4745,13 @@ You are taking over this conversation. Based on the context above, provide a bri
|
||||
}
|
||||
})
|
||||
.filter((tab): tab is UnifiedTab => tab !== null);
|
||||
}, [
|
||||
activeSession?.aiTabs,
|
||||
activeSession?.filePreviewTabs,
|
||||
activeSession?.unifiedTabOrder,
|
||||
]);
|
||||
}, [activeSession?.aiTabs, activeSession?.filePreviewTabs, activeSession?.unifiedTabOrder]);
|
||||
|
||||
// Get the active file preview tab (if a file tab is active)
|
||||
const activeFileTab = useMemo((): FilePreviewTab | null => {
|
||||
if (!activeSession?.activeFileTabId) return null;
|
||||
return (
|
||||
activeSession.filePreviewTabs.find(
|
||||
(tab) => tab.id === activeSession.activeFileTabId
|
||||
) ?? null
|
||||
activeSession.filePreviewTabs.find((tab) => tab.id === activeSession.activeFileTabId) ?? null
|
||||
);
|
||||
}, [activeSession?.activeFileTabId, activeSession?.filePreviewTabs]);
|
||||
|
||||
@@ -5172,7 +5170,13 @@ You are taking over this conversation. Based on the context above, provide a bri
|
||||
forceCloseFileTab(tabId);
|
||||
}
|
||||
},
|
||||
[sessions, forceCloseFileTab, setConfirmModalMessage, setConfirmModalOnConfirm, setConfirmModalOpen]
|
||||
[
|
||||
sessions,
|
||||
forceCloseFileTab,
|
||||
setConfirmModalMessage,
|
||||
setConfirmModalOnConfirm,
|
||||
setConfirmModalOpen,
|
||||
]
|
||||
);
|
||||
|
||||
/**
|
||||
@@ -5281,9 +5285,7 @@ You are taking over this conversation. Based on the context above, provide a bri
|
||||
* If fileTabAutoRefreshEnabled setting is true, checks if file has changed on disk and refreshes content.
|
||||
*/
|
||||
const handleSelectFileTab = useCallback(async (tabId: string) => {
|
||||
const currentSession = sessionsRef.current.find(
|
||||
(s) => s.id === activeSessionIdRef.current
|
||||
);
|
||||
const currentSession = sessionsRef.current.find((s) => s.id === activeSessionIdRef.current);
|
||||
if (!currentSession) return;
|
||||
|
||||
// Verify the file tab exists
|
||||
@@ -5320,9 +5322,7 @@ You are taking over this conversation. Based on the context above, provide a bri
|
||||
return {
|
||||
...s,
|
||||
filePreviewTabs: s.filePreviewTabs.map((tab) =>
|
||||
tab.id === tabId
|
||||
? { ...tab, content, lastModified: currentMtime }
|
||||
: tab
|
||||
tab.id === tabId ? { ...tab, content, lastModified: currentMtime } : tab
|
||||
),
|
||||
};
|
||||
})
|
||||
@@ -5340,34 +5340,31 @@ You are taking over this conversation. Based on the context above, provide a bri
|
||||
* relative to each other. The fromIndex and toIndex refer to positions in unifiedTabOrder.
|
||||
* This replaces/supplements handleTabReorder for the unified tab system.
|
||||
*/
|
||||
const handleUnifiedTabReorder = useCallback(
|
||||
(fromIndex: number, toIndex: number) => {
|
||||
setSessions((prev) =>
|
||||
prev.map((s) => {
|
||||
if (s.id !== activeSessionIdRef.current) return s;
|
||||
const handleUnifiedTabReorder = useCallback((fromIndex: number, toIndex: number) => {
|
||||
setSessions((prev) =>
|
||||
prev.map((s) => {
|
||||
if (s.id !== activeSessionIdRef.current) return s;
|
||||
|
||||
// Validate indices
|
||||
if (
|
||||
fromIndex < 0 ||
|
||||
fromIndex >= s.unifiedTabOrder.length ||
|
||||
toIndex < 0 ||
|
||||
toIndex >= s.unifiedTabOrder.length ||
|
||||
fromIndex === toIndex
|
||||
) {
|
||||
return s;
|
||||
}
|
||||
// Validate indices
|
||||
if (
|
||||
fromIndex < 0 ||
|
||||
fromIndex >= s.unifiedTabOrder.length ||
|
||||
toIndex < 0 ||
|
||||
toIndex >= s.unifiedTabOrder.length ||
|
||||
fromIndex === toIndex
|
||||
) {
|
||||
return s;
|
||||
}
|
||||
|
||||
// Reorder the unifiedTabOrder array
|
||||
const newOrder = [...s.unifiedTabOrder];
|
||||
const [movedRef] = newOrder.splice(fromIndex, 1);
|
||||
newOrder.splice(toIndex, 0, movedRef);
|
||||
// Reorder the unifiedTabOrder array
|
||||
const newOrder = [...s.unifiedTabOrder];
|
||||
const [movedRef] = newOrder.splice(fromIndex, 1);
|
||||
newOrder.splice(toIndex, 0, movedRef);
|
||||
|
||||
return { ...s, unifiedTabOrder: newOrder };
|
||||
})
|
||||
);
|
||||
},
|
||||
[]
|
||||
);
|
||||
return { ...s, unifiedTabOrder: newOrder };
|
||||
})
|
||||
);
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Internal tab close handler that performs the actual close.
|
||||
@@ -5496,9 +5493,7 @@ You are taking over this conversation. Based on the context above, provide a bri
|
||||
// Close file tab by removing from arrays
|
||||
updatedSession = {
|
||||
...updatedSession,
|
||||
filePreviewTabs: updatedSession.filePreviewTabs.filter(
|
||||
(t) => t.id !== tabRef.id
|
||||
),
|
||||
filePreviewTabs: updatedSession.filePreviewTabs.filter((t) => t.id !== tabRef.id),
|
||||
unifiedTabOrder: updatedSession.unifiedTabOrder.filter(
|
||||
(ref) => !(ref.type === 'file' && ref.id === tabRef.id)
|
||||
),
|
||||
@@ -5551,9 +5546,7 @@ You are taking over this conversation. Based on the context above, provide a bri
|
||||
// Close file tab by removing from arrays
|
||||
updatedSession = {
|
||||
...updatedSession,
|
||||
filePreviewTabs: updatedSession.filePreviewTabs.filter(
|
||||
(t) => t.id !== tabRef.id
|
||||
),
|
||||
filePreviewTabs: updatedSession.filePreviewTabs.filter((t) => t.id !== tabRef.id),
|
||||
unifiedTabOrder: updatedSession.unifiedTabOrder.filter(
|
||||
(ref) => !(ref.type === 'file' && ref.id === tabRef.id)
|
||||
),
|
||||
@@ -5606,9 +5599,7 @@ You are taking over this conversation. Based on the context above, provide a bri
|
||||
// Close file tab by removing from arrays
|
||||
updatedSession = {
|
||||
...updatedSession,
|
||||
filePreviewTabs: updatedSession.filePreviewTabs.filter(
|
||||
(t) => t.id !== tabRef.id
|
||||
),
|
||||
filePreviewTabs: updatedSession.filePreviewTabs.filter((t) => t.id !== tabRef.id),
|
||||
unifiedTabOrder: updatedSession.unifiedTabOrder.filter(
|
||||
(ref) => !(ref.type === 'file' && ref.id === tabRef.id)
|
||||
),
|
||||
@@ -7372,9 +7363,7 @@ You are taking over this conversation. Based on the context above, provide a bri
|
||||
lines.push('|-------|--------|-------------|');
|
||||
for (const skill of projectSkills) {
|
||||
const desc =
|
||||
skill.description && skill.description !== 'No description'
|
||||
? skill.description
|
||||
: '—';
|
||||
skill.description && skill.description !== 'No description' ? skill.description : '—';
|
||||
lines.push(`| **${skill.name}** | ${formatTokenCount(skill.tokenCount)} | ${desc} |`);
|
||||
}
|
||||
lines.push('');
|
||||
@@ -7387,9 +7376,7 @@ You are taking over this conversation. Based on the context above, provide a bri
|
||||
lines.push('|-------|--------|-------------|');
|
||||
for (const skill of userSkills) {
|
||||
const desc =
|
||||
skill.description && skill.description !== 'No description'
|
||||
? skill.description
|
||||
: '—';
|
||||
skill.description && skill.description !== 'No description' ? skill.description : '—';
|
||||
lines.push(`| **${skill.name}** | ${formatTokenCount(skill.tokenCount)} | ${desc} |`);
|
||||
}
|
||||
}
|
||||
@@ -8617,7 +8604,9 @@ You are taking over this conversation. Based on the context above, provide a bri
|
||||
...s,
|
||||
aiTabs: s.aiTabs.map((tab) =>
|
||||
// Clear isGeneratingName to cancel any in-progress automatic naming
|
||||
tab.id === renameTabId ? { ...tab, name: newName || null, isGeneratingName: false } : tab
|
||||
tab.id === renameTabId
|
||||
? { ...tab, name: newName || null, isGeneratingName: false }
|
||||
: tab
|
||||
),
|
||||
};
|
||||
})
|
||||
@@ -12289,7 +12278,13 @@ You are taking over this conversation. Based on the context above, provide a bri
|
||||
} else {
|
||||
setChatRawTextMode(!chatRawTextMode);
|
||||
}
|
||||
}, [activeSession?.activeFileTabId, markdownEditMode, chatRawTextMode, setMarkdownEditMode, setChatRawTextMode]);
|
||||
}, [
|
||||
activeSession?.activeFileTabId,
|
||||
markdownEditMode,
|
||||
chatRawTextMode,
|
||||
setMarkdownEditMode,
|
||||
setChatRawTextMode,
|
||||
]);
|
||||
const handleQuickActionsStartTour = useCallback(() => {
|
||||
setTourFromWizard(false);
|
||||
setTourOpen(true);
|
||||
@@ -12833,6 +12828,11 @@ You are taking over this conversation. Based on the context above, provide a bri
|
||||
// to prevent re-evaluating 50-100+ props on every state change.
|
||||
// ============================================================================
|
||||
|
||||
// Stable fileTree reference - prevents FilePreview re-renders during agent activity.
|
||||
// Without this, activeSession?.fileTree || [] creates a new empty array on every render
|
||||
// when fileTree is undefined, and a new reference whenever activeSession updates.
|
||||
const stableFileTree = useMemo(() => activeSession?.fileTree || [], [activeSession?.fileTree]);
|
||||
|
||||
const mainPanelProps = useMainPanelProps({
|
||||
// Core state
|
||||
logViewerOpen,
|
||||
@@ -12885,7 +12885,7 @@ You are taking over this conversation. Based on the context above, provide a bri
|
||||
currentSessionBatchState: currentSessionBatchState ?? undefined,
|
||||
|
||||
// File tree
|
||||
fileTree: activeSession?.fileTree || [],
|
||||
fileTree: stableFileTree,
|
||||
|
||||
// File preview navigation (per-tab)
|
||||
canGoBack: fileTabCanGoBack,
|
||||
@@ -13753,174 +13753,174 @@ You are taking over this conversation. Based on the context above, provide a bri
|
||||
theme={theme}
|
||||
isOpen={symphonyModalOpen}
|
||||
onClose={() => setSymphonyModalOpen(false)}
|
||||
onStartContribution={async (data: SymphonyContributionData) => {
|
||||
console.log('[Symphony] Creating session for contribution:', data);
|
||||
onStartContribution={async (data: SymphonyContributionData) => {
|
||||
console.log('[Symphony] Creating session for contribution:', data);
|
||||
|
||||
// Get agent definition
|
||||
const agent = await window.maestro.agents.get(data.agentType);
|
||||
if (!agent) {
|
||||
console.error(`Agent not found: ${data.agentType}`);
|
||||
addToast({
|
||||
type: 'error',
|
||||
title: 'Symphony Error',
|
||||
message: `Agent not found: ${data.agentType}`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
// Get agent definition
|
||||
const agent = await window.maestro.agents.get(data.agentType);
|
||||
if (!agent) {
|
||||
console.error(`Agent not found: ${data.agentType}`);
|
||||
addToast({
|
||||
type: 'error',
|
||||
title: 'Symphony Error',
|
||||
message: `Agent not found: ${data.agentType}`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate uniqueness
|
||||
const validation = validateNewSession(
|
||||
data.sessionName,
|
||||
data.localPath,
|
||||
data.agentType as ToolType,
|
||||
sessions
|
||||
);
|
||||
if (!validation.valid) {
|
||||
console.error(`Session validation failed: ${validation.error}`);
|
||||
addToast({
|
||||
type: 'error',
|
||||
title: 'Session Creation Failed',
|
||||
message: validation.error || 'Cannot create duplicate session',
|
||||
});
|
||||
return;
|
||||
}
|
||||
// Validate uniqueness
|
||||
const validation = validateNewSession(
|
||||
data.sessionName,
|
||||
data.localPath,
|
||||
data.agentType as ToolType,
|
||||
sessions
|
||||
);
|
||||
if (!validation.valid) {
|
||||
console.error(`Session validation failed: ${validation.error}`);
|
||||
addToast({
|
||||
type: 'error',
|
||||
title: 'Session Creation Failed',
|
||||
message: validation.error || 'Cannot create duplicate session',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const newId = generateId();
|
||||
const initialTabId = generateId();
|
||||
const newId = generateId();
|
||||
const initialTabId = generateId();
|
||||
|
||||
// Check git repo status
|
||||
const isGitRepo = await gitService.isRepo(data.localPath);
|
||||
let gitBranches: string[] | undefined;
|
||||
let gitTags: string[] | undefined;
|
||||
let gitRefsCacheTime: number | undefined;
|
||||
// Check git repo status
|
||||
const isGitRepo = await gitService.isRepo(data.localPath);
|
||||
let gitBranches: string[] | undefined;
|
||||
let gitTags: string[] | undefined;
|
||||
let gitRefsCacheTime: number | undefined;
|
||||
|
||||
if (isGitRepo) {
|
||||
[gitBranches, gitTags] = await Promise.all([
|
||||
gitService.getBranches(data.localPath),
|
||||
gitService.getTags(data.localPath),
|
||||
]);
|
||||
gitRefsCacheTime = Date.now();
|
||||
}
|
||||
if (isGitRepo) {
|
||||
[gitBranches, gitTags] = await Promise.all([
|
||||
gitService.getBranches(data.localPath),
|
||||
gitService.getTags(data.localPath),
|
||||
]);
|
||||
gitRefsCacheTime = Date.now();
|
||||
}
|
||||
|
||||
// Create initial tab
|
||||
const initialTab: AITab = {
|
||||
id: initialTabId,
|
||||
agentSessionId: null,
|
||||
name: null,
|
||||
starred: false,
|
||||
logs: [],
|
||||
inputValue: '',
|
||||
stagedImages: [],
|
||||
createdAt: Date.now(),
|
||||
state: 'idle',
|
||||
saveToHistory: defaultSaveToHistory,
|
||||
};
|
||||
// Create initial tab
|
||||
const initialTab: AITab = {
|
||||
id: initialTabId,
|
||||
agentSessionId: null,
|
||||
name: null,
|
||||
starred: false,
|
||||
logs: [],
|
||||
inputValue: '',
|
||||
stagedImages: [],
|
||||
createdAt: Date.now(),
|
||||
state: 'idle',
|
||||
saveToHistory: defaultSaveToHistory,
|
||||
};
|
||||
|
||||
// Create session with Symphony metadata
|
||||
const newSession: Session = {
|
||||
id: newId,
|
||||
name: data.sessionName,
|
||||
toolType: data.agentType as ToolType,
|
||||
state: 'idle',
|
||||
cwd: data.localPath,
|
||||
fullPath: data.localPath,
|
||||
projectRoot: data.localPath,
|
||||
isGitRepo,
|
||||
gitBranches,
|
||||
gitTags,
|
||||
gitRefsCacheTime,
|
||||
aiLogs: [],
|
||||
shellLogs: [
|
||||
{
|
||||
id: generateId(),
|
||||
timestamp: Date.now(),
|
||||
source: 'system',
|
||||
text: 'Shell Session Ready.',
|
||||
},
|
||||
],
|
||||
workLog: [],
|
||||
contextUsage: 0,
|
||||
inputMode: 'ai',
|
||||
aiPid: 0,
|
||||
terminalPid: 0,
|
||||
port: 3000 + Math.floor(Math.random() * 100),
|
||||
isLive: false,
|
||||
changedFiles: [],
|
||||
fileTree: [],
|
||||
fileExplorerExpanded: [],
|
||||
fileExplorerScrollPos: 0,
|
||||
fileTreeAutoRefreshInterval: 180,
|
||||
shellCwd: data.localPath,
|
||||
aiCommandHistory: [],
|
||||
shellCommandHistory: [],
|
||||
executionQueue: [],
|
||||
activeTimeMs: 0,
|
||||
aiTabs: [initialTab],
|
||||
activeTabId: initialTabId,
|
||||
closedTabHistory: [],
|
||||
filePreviewTabs: [],
|
||||
activeFileTabId: null,
|
||||
unifiedTabOrder: [{ type: 'ai' as const, id: initialTabId }],
|
||||
unifiedClosedTabHistory: [],
|
||||
// Custom agent config
|
||||
customPath: data.customPath,
|
||||
customArgs: data.customArgs,
|
||||
customEnvVars: data.customEnvVars,
|
||||
// Auto Run setup - use autoRunPath from contribution
|
||||
autoRunFolderPath: data.autoRunPath,
|
||||
// Symphony metadata for tracking
|
||||
symphonyMetadata: {
|
||||
isSymphonySession: true,
|
||||
contributionId: data.contributionId,
|
||||
repoSlug: data.repo.slug,
|
||||
issueNumber: data.issue.number,
|
||||
issueTitle: data.issue.title,
|
||||
documentPaths: data.issue.documentPaths.map((d) => d.path),
|
||||
status: 'running',
|
||||
},
|
||||
};
|
||||
// Create session with Symphony metadata
|
||||
const newSession: Session = {
|
||||
id: newId,
|
||||
name: data.sessionName,
|
||||
toolType: data.agentType as ToolType,
|
||||
state: 'idle',
|
||||
cwd: data.localPath,
|
||||
fullPath: data.localPath,
|
||||
projectRoot: data.localPath,
|
||||
isGitRepo,
|
||||
gitBranches,
|
||||
gitTags,
|
||||
gitRefsCacheTime,
|
||||
aiLogs: [],
|
||||
shellLogs: [
|
||||
{
|
||||
id: generateId(),
|
||||
timestamp: Date.now(),
|
||||
source: 'system',
|
||||
text: 'Shell Session Ready.',
|
||||
},
|
||||
],
|
||||
workLog: [],
|
||||
contextUsage: 0,
|
||||
inputMode: 'ai',
|
||||
aiPid: 0,
|
||||
terminalPid: 0,
|
||||
port: 3000 + Math.floor(Math.random() * 100),
|
||||
isLive: false,
|
||||
changedFiles: [],
|
||||
fileTree: [],
|
||||
fileExplorerExpanded: [],
|
||||
fileExplorerScrollPos: 0,
|
||||
fileTreeAutoRefreshInterval: 180,
|
||||
shellCwd: data.localPath,
|
||||
aiCommandHistory: [],
|
||||
shellCommandHistory: [],
|
||||
executionQueue: [],
|
||||
activeTimeMs: 0,
|
||||
aiTabs: [initialTab],
|
||||
activeTabId: initialTabId,
|
||||
closedTabHistory: [],
|
||||
filePreviewTabs: [],
|
||||
activeFileTabId: null,
|
||||
unifiedTabOrder: [{ type: 'ai' as const, id: initialTabId }],
|
||||
unifiedClosedTabHistory: [],
|
||||
// Custom agent config
|
||||
customPath: data.customPath,
|
||||
customArgs: data.customArgs,
|
||||
customEnvVars: data.customEnvVars,
|
||||
// Auto Run setup - use autoRunPath from contribution
|
||||
autoRunFolderPath: data.autoRunPath,
|
||||
// Symphony metadata for tracking
|
||||
symphonyMetadata: {
|
||||
isSymphonySession: true,
|
||||
contributionId: data.contributionId,
|
||||
repoSlug: data.repo.slug,
|
||||
issueNumber: data.issue.number,
|
||||
issueTitle: data.issue.title,
|
||||
documentPaths: data.issue.documentPaths.map((d) => d.path),
|
||||
status: 'running',
|
||||
},
|
||||
};
|
||||
|
||||
setSessions((prev) => [...prev, newSession]);
|
||||
setActiveSessionId(newId);
|
||||
setSymphonyModalOpen(false);
|
||||
setSessions((prev) => [...prev, newSession]);
|
||||
setActiveSessionId(newId);
|
||||
setSymphonyModalOpen(false);
|
||||
|
||||
// Register active contribution in Symphony persistent state
|
||||
// This makes it show up in the Active tab of the Symphony modal
|
||||
window.maestro.symphony
|
||||
.registerActive({
|
||||
contributionId: data.contributionId,
|
||||
sessionId: newId,
|
||||
repoSlug: data.repo.slug,
|
||||
repoName: data.repo.name,
|
||||
issueNumber: data.issue.number,
|
||||
issueTitle: data.issue.title,
|
||||
localPath: data.localPath,
|
||||
branchName: data.branchName || '',
|
||||
totalDocuments: data.issue.documentPaths.length,
|
||||
agentType: data.agentType,
|
||||
})
|
||||
.catch((err: unknown) => {
|
||||
console.error('[Symphony] Failed to register active contribution:', err);
|
||||
});
|
||||
// Register active contribution in Symphony persistent state
|
||||
// This makes it show up in the Active tab of the Symphony modal
|
||||
window.maestro.symphony
|
||||
.registerActive({
|
||||
contributionId: data.contributionId,
|
||||
sessionId: newId,
|
||||
repoSlug: data.repo.slug,
|
||||
repoName: data.repo.name,
|
||||
issueNumber: data.issue.number,
|
||||
issueTitle: data.issue.title,
|
||||
localPath: data.localPath,
|
||||
branchName: data.branchName || '',
|
||||
totalDocuments: data.issue.documentPaths.length,
|
||||
agentType: data.agentType,
|
||||
})
|
||||
.catch((err: unknown) => {
|
||||
console.error('[Symphony] Failed to register active contribution:', err);
|
||||
});
|
||||
|
||||
// Track stats
|
||||
updateGlobalStats({ totalSessions: 1 });
|
||||
window.maestro.stats.recordSessionCreated({
|
||||
sessionId: newId,
|
||||
agentType: data.agentType,
|
||||
projectPath: data.localPath,
|
||||
createdAt: Date.now(),
|
||||
isRemote: false,
|
||||
});
|
||||
// Track stats
|
||||
updateGlobalStats({ totalSessions: 1 });
|
||||
window.maestro.stats.recordSessionCreated({
|
||||
sessionId: newId,
|
||||
agentType: data.agentType,
|
||||
projectPath: data.localPath,
|
||||
createdAt: Date.now(),
|
||||
isRemote: false,
|
||||
});
|
||||
|
||||
// Focus input
|
||||
setActiveFocus('main');
|
||||
setTimeout(() => inputRef.current?.focus(), 50);
|
||||
// Focus input
|
||||
setActiveFocus('main');
|
||||
setTimeout(() => inputRef.current?.focus(), 50);
|
||||
|
||||
// Switch to Auto Run tab so user sees the documents
|
||||
setActiveRightTab('autorun');
|
||||
}}
|
||||
/>
|
||||
// Switch to Auto Run tab so user sees the documents
|
||||
setActiveRightTab('autorun');
|
||||
}}
|
||||
/>
|
||||
</Suspense>
|
||||
)}
|
||||
|
||||
@@ -13929,7 +13929,10 @@ You are taking over this conversation. Based on the context above, provide a bri
|
||||
{gistPublishModalOpen && (activeFileTab || tabGistContent) && (
|
||||
<GistPublishModal
|
||||
theme={theme}
|
||||
filename={tabGistContent?.filename ?? (activeFileTab ? activeFileTab.name + activeFileTab.extension : 'conversation.md')}
|
||||
filename={
|
||||
tabGistContent?.filename ??
|
||||
(activeFileTab ? activeFileTab.name + activeFileTab.extension : 'conversation.md')
|
||||
}
|
||||
content={tabGistContent?.content ?? activeFileTab?.content ?? ''}
|
||||
onClose={() => {
|
||||
setGistPublishModalOpen(false);
|
||||
@@ -13969,67 +13972,69 @@ You are taking over this conversation. Based on the context above, provide a bri
|
||||
{graphFocusFilePath && (
|
||||
<Suspense fallback={null}>
|
||||
<DocumentGraphView
|
||||
isOpen={isGraphViewOpen}
|
||||
onClose={() => {
|
||||
setIsGraphViewOpen(false);
|
||||
setGraphFocusFilePath(undefined);
|
||||
// Return focus to file preview if it was open
|
||||
requestAnimationFrame(() => {
|
||||
mainPanelRef.current?.focusFilePreview();
|
||||
});
|
||||
}}
|
||||
theme={theme}
|
||||
rootPath={activeSession?.projectRoot || activeSession?.cwd || ''}
|
||||
onDocumentOpen={async (filePath) => {
|
||||
// Open the document in a file tab (migrated from legacy setPreviewFile overlay)
|
||||
const treeRoot = activeSession?.projectRoot || activeSession?.cwd || '';
|
||||
const fullPath = `${treeRoot}/${filePath}`;
|
||||
const filename = filePath.split('/').pop() || filePath;
|
||||
isOpen={isGraphViewOpen}
|
||||
onClose={() => {
|
||||
setIsGraphViewOpen(false);
|
||||
setGraphFocusFilePath(undefined);
|
||||
// Return focus to file preview if it was open
|
||||
requestAnimationFrame(() => {
|
||||
mainPanelRef.current?.focusFilePreview();
|
||||
});
|
||||
}}
|
||||
theme={theme}
|
||||
rootPath={activeSession?.projectRoot || activeSession?.cwd || ''}
|
||||
onDocumentOpen={async (filePath) => {
|
||||
// Open the document in a file tab (migrated from legacy setPreviewFile overlay)
|
||||
const treeRoot = activeSession?.projectRoot || activeSession?.cwd || '';
|
||||
const fullPath = `${treeRoot}/${filePath}`;
|
||||
const filename = filePath.split('/').pop() || filePath;
|
||||
// Note: sshRemoteId is only set after AI agent spawns. For terminal-only SSH sessions,
|
||||
// use sessionSshRemoteConfig.remoteId as fallback (see CLAUDE.md SSH Remote Sessions)
|
||||
const sshRemoteId =
|
||||
activeSession?.sshRemoteId ||
|
||||
activeSession?.sessionSshRemoteConfig?.remoteId ||
|
||||
undefined;
|
||||
try {
|
||||
// Fetch content and stat in parallel for efficiency
|
||||
const [content, stat] = await Promise.all([
|
||||
window.maestro.fs.readFile(fullPath, sshRemoteId),
|
||||
window.maestro.fs.stat(fullPath, sshRemoteId).catch(() => null), // stat is optional
|
||||
]);
|
||||
if (content !== null) {
|
||||
const lastModified = stat?.modifiedAt
|
||||
? new Date(stat.modifiedAt).getTime()
|
||||
: undefined;
|
||||
handleOpenFileTab({
|
||||
path: fullPath,
|
||||
name: filename,
|
||||
content,
|
||||
sshRemoteId,
|
||||
lastModified,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[DocumentGraph] Failed to open file:', error);
|
||||
}
|
||||
setIsGraphViewOpen(false);
|
||||
}}
|
||||
onExternalLinkOpen={(url) => {
|
||||
// Open external URL in default browser
|
||||
window.maestro.shell.openExternal(url);
|
||||
}}
|
||||
focusFilePath={graphFocusFilePath}
|
||||
defaultShowExternalLinks={documentGraphShowExternalLinks}
|
||||
onExternalLinksChange={settings.setDocumentGraphShowExternalLinks}
|
||||
defaultMaxNodes={documentGraphMaxNodes}
|
||||
defaultPreviewCharLimit={documentGraphPreviewCharLimit}
|
||||
onPreviewCharLimitChange={settings.setDocumentGraphPreviewCharLimit}
|
||||
// Note: sshRemoteId is only set after AI agent spawns. For terminal-only SSH sessions,
|
||||
// use sessionSshRemoteConfig.remoteId as fallback (see CLAUDE.md SSH Remote Sessions)
|
||||
const sshRemoteId =
|
||||
sshRemoteId={
|
||||
activeSession?.sshRemoteId ||
|
||||
activeSession?.sessionSshRemoteConfig?.remoteId ||
|
||||
undefined;
|
||||
try {
|
||||
// Fetch content and stat in parallel for efficiency
|
||||
const [content, stat] = await Promise.all([
|
||||
window.maestro.fs.readFile(fullPath, sshRemoteId),
|
||||
window.maestro.fs.stat(fullPath, sshRemoteId).catch(() => null), // stat is optional
|
||||
]);
|
||||
if (content !== null) {
|
||||
const lastModified = stat?.modifiedAt ? new Date(stat.modifiedAt).getTime() : undefined;
|
||||
handleOpenFileTab({
|
||||
path: fullPath,
|
||||
name: filename,
|
||||
content,
|
||||
sshRemoteId,
|
||||
lastModified,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[DocumentGraph] Failed to open file:', error);
|
||||
undefined
|
||||
}
|
||||
setIsGraphViewOpen(false);
|
||||
}}
|
||||
onExternalLinkOpen={(url) => {
|
||||
// Open external URL in default browser
|
||||
window.maestro.shell.openExternal(url);
|
||||
}}
|
||||
focusFilePath={graphFocusFilePath}
|
||||
defaultShowExternalLinks={documentGraphShowExternalLinks}
|
||||
onExternalLinksChange={settings.setDocumentGraphShowExternalLinks}
|
||||
defaultMaxNodes={documentGraphMaxNodes}
|
||||
defaultPreviewCharLimit={documentGraphPreviewCharLimit}
|
||||
onPreviewCharLimitChange={settings.setDocumentGraphPreviewCharLimit}
|
||||
// Note: sshRemoteId is only set after AI agent spawns. For terminal-only SSH sessions,
|
||||
// use sessionSshRemoteConfig.remoteId as fallback (see CLAUDE.md SSH Remote Sessions)
|
||||
sshRemoteId={
|
||||
activeSession?.sshRemoteId ||
|
||||
activeSession?.sessionSshRemoteConfig?.remoteId ||
|
||||
undefined
|
||||
}
|
||||
/>
|
||||
/>
|
||||
</Suspense>
|
||||
)}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user