refactor(ProjectScanner): enhance file detail retrieval and session metadata handling
- Updated the ProjectScanner class to utilize a new asynchronous method for resolving file details, improving accuracy in file metadata retrieval. - Introduced birthtimeMs to session file information, ensuring comprehensive metadata is available for session management. - Adjusted session creation logic to account for the new birthtimeMs, enhancing the integrity of session timestamps. - Improved the handling of auto-scroll behavior in ChatHistory and useAutoScrollBottom hook, allowing for smoother user experience during content updates. This commit enhances the ProjectScanner's efficiency and improves user experience in chat history management.
This commit is contained in:
parent
b1e37470cb
commit
e0e399ec31
5 changed files with 90 additions and 51 deletions
|
|
@ -416,8 +416,10 @@ export class ProjectScanner {
|
||||||
sessionFiles.map(async (file) => {
|
sessionFiles.map(async (file) => {
|
||||||
const sessionId = extractSessionId(file.name);
|
const sessionId = extractSessionId(file.name);
|
||||||
const filePath = path.join(projectPath, file.name);
|
const filePath = path.join(projectPath, file.name);
|
||||||
const prefetchedMtimeMs = file.mtimeMs;
|
const fileDetails = await this.resolveFileDetails(file, filePath);
|
||||||
const prefetchedSize = file.size;
|
const prefetchedMtimeMs = fileDetails.mtimeMs;
|
||||||
|
const prefetchedSize = fileDetails.size;
|
||||||
|
const prefetchedBirthtimeMs = fileDetails.birthtimeMs;
|
||||||
|
|
||||||
if (shouldFilterNoise) {
|
if (shouldFilterNoise) {
|
||||||
// Check if session has non-noise messages (delegated to SessionContentFilter)
|
// Check if session has non-noise messages (delegated to SessionContentFilter)
|
||||||
|
|
@ -438,7 +440,8 @@ export class ProjectScanner {
|
||||||
filePath,
|
filePath,
|
||||||
decodedPath,
|
decodedPath,
|
||||||
prefetchedMtimeMs,
|
prefetchedMtimeMs,
|
||||||
prefetchedSize
|
prefetchedSize,
|
||||||
|
prefetchedBirthtimeMs
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
@ -503,6 +506,7 @@ export class ProjectScanner {
|
||||||
filePath: string;
|
filePath: string;
|
||||||
mtimeMs: number;
|
mtimeMs: number;
|
||||||
size: number;
|
size: number;
|
||||||
|
birthtimeMs: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const fileInfos = await this.collectFulfilledInBatches(
|
const fileInfos = await this.collectFulfilledInBatches(
|
||||||
|
|
@ -518,6 +522,7 @@ export class ProjectScanner {
|
||||||
filePath,
|
filePath,
|
||||||
mtimeMs: fileDetails.mtimeMs,
|
mtimeMs: fileDetails.mtimeMs,
|
||||||
size: fileDetails.size,
|
size: fileDetails.size,
|
||||||
|
birthtimeMs: fileDetails.birthtimeMs,
|
||||||
} satisfies SessionFileInfo;
|
} satisfies SessionFileInfo;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
@ -642,7 +647,8 @@ export class ProjectScanner {
|
||||||
fileInfo.filePath,
|
fileInfo.filePath,
|
||||||
decodedPath,
|
decodedPath,
|
||||||
fileInfo.mtimeMs,
|
fileInfo.mtimeMs,
|
||||||
fileInfo.size
|
fileInfo.size,
|
||||||
|
fileInfo.birthtimeMs
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
sessions.push(...builtSessions);
|
sessions.push(...builtSessions);
|
||||||
|
|
@ -703,14 +709,17 @@ export class ProjectScanner {
|
||||||
filePath: string,
|
filePath: string,
|
||||||
projectPath: string,
|
projectPath: string,
|
||||||
prefetchedMtimeMs?: number,
|
prefetchedMtimeMs?: number,
|
||||||
prefetchedSize?: number
|
prefetchedSize?: number,
|
||||||
|
prefetchedBirthtimeMs?: number
|
||||||
): Promise<Session> {
|
): Promise<Session> {
|
||||||
const usePrefetchedStats =
|
const hasPrefetchedCoreStats =
|
||||||
typeof prefetchedMtimeMs === 'number' && typeof prefetchedSize === 'number';
|
typeof prefetchedMtimeMs === 'number' && typeof prefetchedSize === 'number';
|
||||||
const stats = usePrefetchedStats ? null : await this.fsProvider.stat(filePath);
|
const needsBirthtimeStat = typeof prefetchedBirthtimeMs !== 'number';
|
||||||
|
const stats =
|
||||||
|
hasPrefetchedCoreStats && !needsBirthtimeStat ? null : await this.fsProvider.stat(filePath);
|
||||||
const effectiveMtime = prefetchedMtimeMs ?? stats?.mtimeMs ?? Date.now();
|
const effectiveMtime = prefetchedMtimeMs ?? stats?.mtimeMs ?? Date.now();
|
||||||
const effectiveSize = prefetchedSize ?? stats?.size ?? -1;
|
const effectiveSize = prefetchedSize ?? stats?.size ?? -1;
|
||||||
const birthtimeMs = stats?.birthtimeMs ?? effectiveMtime;
|
const birthtimeMs = prefetchedBirthtimeMs ?? stats?.birthtimeMs ?? effectiveMtime;
|
||||||
const cachedMetadata = this.sessionMetadataCache.get(filePath);
|
const cachedMetadata = this.sessionMetadataCache.get(filePath);
|
||||||
const metadata =
|
const metadata =
|
||||||
cachedMetadata?.mtimeMs === effectiveMtime && cachedMetadata.size === effectiveSize
|
cachedMetadata?.mtimeMs === effectiveMtime && cachedMetadata.size === effectiveSize
|
||||||
|
|
@ -730,13 +739,18 @@ export class ProjectScanner {
|
||||||
this.loadTodoData(sessionId),
|
this.loadTodoData(sessionId),
|
||||||
]);
|
]);
|
||||||
const metadataLevel: SessionMetadataLevel = 'deep';
|
const metadataLevel: SessionMetadataLevel = 'deep';
|
||||||
|
const firstMessageTimestampMs = this.parseTimestampMs(metadata.firstUserMessage?.timestamp);
|
||||||
|
const createdAt =
|
||||||
|
firstMessageTimestampMs !== null && Number.isFinite(firstMessageTimestampMs)
|
||||||
|
? firstMessageTimestampMs
|
||||||
|
: birthtimeMs;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: sessionId,
|
id: sessionId,
|
||||||
projectId,
|
projectId,
|
||||||
projectPath,
|
projectPath,
|
||||||
todoData,
|
todoData,
|
||||||
createdAt: Math.floor(birthtimeMs),
|
createdAt: Math.floor(createdAt),
|
||||||
firstMessage: metadata.firstUserMessage?.text,
|
firstMessage: metadata.firstUserMessage?.text,
|
||||||
messageTimestamp: metadata.firstUserMessage?.timestamp,
|
messageTimestamp: metadata.firstUserMessage?.timestamp,
|
||||||
hasSubagents,
|
hasSubagents,
|
||||||
|
|
@ -757,14 +771,17 @@ export class ProjectScanner {
|
||||||
filePath: string,
|
filePath: string,
|
||||||
projectPath: string,
|
projectPath: string,
|
||||||
prefetchedMtimeMs?: number,
|
prefetchedMtimeMs?: number,
|
||||||
prefetchedSize?: number
|
prefetchedSize?: number,
|
||||||
|
prefetchedBirthtimeMs?: number
|
||||||
): Promise<Session> {
|
): Promise<Session> {
|
||||||
const usePrefetchedStats =
|
const hasPrefetchedCoreStats =
|
||||||
typeof prefetchedMtimeMs === 'number' && typeof prefetchedSize === 'number';
|
typeof prefetchedMtimeMs === 'number' && typeof prefetchedSize === 'number';
|
||||||
const stats = usePrefetchedStats ? null : await this.fsProvider.stat(filePath);
|
const needsBirthtimeStat = typeof prefetchedBirthtimeMs !== 'number';
|
||||||
|
const stats =
|
||||||
|
hasPrefetchedCoreStats && !needsBirthtimeStat ? null : await this.fsProvider.stat(filePath);
|
||||||
const effectiveMtime = prefetchedMtimeMs ?? stats?.mtimeMs ?? Date.now();
|
const effectiveMtime = prefetchedMtimeMs ?? stats?.mtimeMs ?? Date.now();
|
||||||
const effectiveSize = prefetchedSize ?? stats?.size ?? -1;
|
const effectiveSize = prefetchedSize ?? stats?.size ?? -1;
|
||||||
const birthtimeMs = stats?.birthtimeMs ?? effectiveMtime;
|
const birthtimeMs = prefetchedBirthtimeMs ?? stats?.birthtimeMs ?? effectiveMtime;
|
||||||
const cachedPreview = this.sessionPreviewCache.get(filePath);
|
const cachedPreview = this.sessionPreviewCache.get(filePath);
|
||||||
const preview =
|
const preview =
|
||||||
cachedPreview?.mtimeMs === effectiveMtime && cachedPreview.size === effectiveSize
|
cachedPreview?.mtimeMs === effectiveMtime && cachedPreview.size === effectiveSize
|
||||||
|
|
@ -778,12 +795,17 @@ export class ProjectScanner {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const metadataLevel: SessionMetadataLevel = 'light';
|
const metadataLevel: SessionMetadataLevel = 'light';
|
||||||
|
const previewTimestampMs = this.parseTimestampMs(preview?.timestamp);
|
||||||
|
const createdAt =
|
||||||
|
previewTimestampMs !== null && Number.isFinite(previewTimestampMs)
|
||||||
|
? previewTimestampMs
|
||||||
|
: birthtimeMs;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: sessionId,
|
id: sessionId,
|
||||||
projectId,
|
projectId,
|
||||||
projectPath,
|
projectPath,
|
||||||
createdAt: Math.floor(birthtimeMs),
|
createdAt: Math.floor(createdAt),
|
||||||
firstMessage: preview?.text,
|
firstMessage: preview?.text,
|
||||||
messageTimestamp: preview?.timestamp,
|
messageTimestamp: preview?.timestamp,
|
||||||
hasSubagents: false,
|
hasSubagents: false,
|
||||||
|
|
@ -803,7 +825,8 @@ export class ProjectScanner {
|
||||||
filePath: string,
|
filePath: string,
|
||||||
projectPath: string,
|
projectPath: string,
|
||||||
prefetchedMtimeMs?: number,
|
prefetchedMtimeMs?: number,
|
||||||
prefetchedSize?: number
|
prefetchedSize?: number,
|
||||||
|
prefetchedBirthtimeMs?: number
|
||||||
): Promise<Session> {
|
): Promise<Session> {
|
||||||
if (metadataLevel === 'light') {
|
if (metadataLevel === 'light') {
|
||||||
return this.buildLightSessionMetadata(
|
return this.buildLightSessionMetadata(
|
||||||
|
|
@ -812,7 +835,8 @@ export class ProjectScanner {
|
||||||
filePath,
|
filePath,
|
||||||
projectPath,
|
projectPath,
|
||||||
prefetchedMtimeMs,
|
prefetchedMtimeMs,
|
||||||
prefetchedSize
|
prefetchedSize,
|
||||||
|
prefetchedBirthtimeMs
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -823,7 +847,8 @@ export class ProjectScanner {
|
||||||
filePath,
|
filePath,
|
||||||
projectPath,
|
projectPath,
|
||||||
prefetchedMtimeMs,
|
prefetchedMtimeMs,
|
||||||
prefetchedSize
|
prefetchedSize,
|
||||||
|
prefetchedBirthtimeMs
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// In SSH mode, never drop a visible session row due to transient deep-parse failures.
|
// In SSH mode, never drop a visible session row due to transient deep-parse failures.
|
||||||
|
|
@ -838,7 +863,8 @@ export class ProjectScanner {
|
||||||
filePath,
|
filePath,
|
||||||
projectPath,
|
projectPath,
|
||||||
prefetchedMtimeMs,
|
prefetchedMtimeMs,
|
||||||
prefetchedSize
|
prefetchedSize,
|
||||||
|
prefetchedBirthtimeMs
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1069,6 +1095,14 @@ export class ProjectScanner {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private parseTimestampMs(timestamp: string | undefined): number | null {
|
||||||
|
if (!timestamp) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const parsed = Date.parse(timestamp);
|
||||||
|
return Number.isFinite(parsed) ? parsed : null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Runs async mapping in bounded batches and returns only fulfilled results.
|
* Runs async mapping in bounded batches and returns only fulfilled results.
|
||||||
* This prevents overwhelming SFTP servers with unbounded parallel requests.
|
* This prevents overwhelming SFTP servers with unbounded parallel requests.
|
||||||
|
|
|
||||||
|
|
@ -339,13 +339,13 @@ export const ChatHistory = ({ tabId }: ChatHistoryProps): JSX.Element => {
|
||||||
rootRef: scrollContainerRef,
|
rootRef: scrollContainerRef,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Auto-scroll to bottom when new content is added
|
// Auto-follow when conversation updates, but only if the user was already near bottom.
|
||||||
// Disabled during navigation to prevent conflicts with deep link scrolling
|
// This preserves manual reading position when the user scrolls up.
|
||||||
// Uses shared scrollContainerRef created above
|
// Disabled during navigation to prevent conflicts with deep-link/search scrolling.
|
||||||
// resetKey ensures auto-scroll state resets when switching tabs/sessions
|
useAutoScrollBottom([conversation], {
|
||||||
useAutoScrollBottom([conversation?.items.length], {
|
|
||||||
threshold: 150,
|
threshold: 150,
|
||||||
smoothDuration: 300,
|
smoothDuration: 300,
|
||||||
|
autoBehavior: 'auto',
|
||||||
disabled: shouldDisableAutoScroll,
|
disabled: shouldDisableAutoScroll,
|
||||||
externalRef: scrollContainerRef,
|
externalRef: scrollContainerRef,
|
||||||
resetKey: effectiveTabId,
|
resetKey: effectiveTabId,
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,12 @@ interface UseAutoScrollBottomOptions {
|
||||||
*/
|
*/
|
||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scroll behavior used for automatic follow when content updates.
|
||||||
|
* Default: 'smooth'
|
||||||
|
*/
|
||||||
|
autoBehavior?: ScrollBehavior;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether auto-scroll is temporarily disabled (e.g., during navigation).
|
* Whether auto-scroll is temporarily disabled (e.g., during navigation).
|
||||||
* Unlike enabled, this is for transient disabling during specific operations.
|
* Unlike enabled, this is for transient disabling during specific operations.
|
||||||
|
|
@ -115,6 +121,7 @@ export function useAutoScrollBottom(
|
||||||
threshold = 100,
|
threshold = 100,
|
||||||
smoothDuration = 300,
|
smoothDuration = 300,
|
||||||
enabled = true,
|
enabled = true,
|
||||||
|
autoBehavior = 'smooth',
|
||||||
disabled = false,
|
disabled = false,
|
||||||
externalRef,
|
externalRef,
|
||||||
resetKey,
|
resetKey,
|
||||||
|
|
@ -241,11 +248,11 @@ export function useAutoScrollBottom(
|
||||||
|
|
||||||
// Only auto-scroll if user was at bottom before the update
|
// Only auto-scroll if user was at bottom before the update
|
||||||
if (wasAtBottomBeforeUpdateRef.current) {
|
if (wasAtBottomBeforeUpdateRef.current) {
|
||||||
scrollToBottom('smooth');
|
scrollToBottom(autoBehavior);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps -- Dynamic dependencies array is intentional design
|
// eslint-disable-next-line react-hooks/exhaustive-deps -- Dynamic dependencies array is intentional design
|
||||||
}, [...dependencies, enabled, disabled, scrollToBottom]);
|
}, [...dependencies, enabled, disabled, autoBehavior, scrollToBottom]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Getter function for isAtBottom to avoid accessing ref.current during render.
|
* Getter function for isAtBottom to avoid accessing ref.current during render.
|
||||||
|
|
|
||||||
|
|
@ -73,9 +73,10 @@ export function initializeNotificationListeners(): () => void {
|
||||||
|
|
||||||
const scheduleSessionRefresh = (projectId: string, sessionId: string): void => {
|
const scheduleSessionRefresh = (projectId: string, sessionId: string): void => {
|
||||||
const key = `${projectId}/${sessionId}`;
|
const key = `${projectId}/${sessionId}`;
|
||||||
const existingTimer = pendingSessionRefreshTimers.get(key);
|
// Throttle (not trailing debounce): keep at most one pending refresh per session.
|
||||||
if (existingTimer) {
|
// Debounce can delay updates indefinitely while the file is continuously appended.
|
||||||
clearTimeout(existingTimer);
|
if (pendingSessionRefreshTimers.has(key)) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
const timer = setTimeout(() => {
|
const timer = setTimeout(() => {
|
||||||
pendingSessionRefreshTimers.delete(key);
|
pendingSessionRefreshTimers.delete(key);
|
||||||
|
|
@ -218,25 +219,26 @@ export function initializeNotificationListeners(): () => void {
|
||||||
const matchesSelectedProject =
|
const matchesSelectedProject =
|
||||||
!!selectedProjectId &&
|
!!selectedProjectId &&
|
||||||
(eventProjectBaseId == null || selectedProjectBaseId === eventProjectBaseId);
|
(eventProjectBaseId == null || selectedProjectBaseId === eventProjectBaseId);
|
||||||
|
const isTopLevelSessionEvent = !event.isSubagent;
|
||||||
const isUnknownSessionInSidebar =
|
const isUnknownSessionInSidebar =
|
||||||
event.sessionId != null && !state.sessions.some((session) => session.id === event.sessionId);
|
event.sessionId == null || !state.sessions.some((session) => session.id === event.sessionId);
|
||||||
const shouldRefreshForPotentialNewSession =
|
const shouldRefreshForPotentialNewSession =
|
||||||
event.type === 'change' &&
|
isTopLevelSessionEvent &&
|
||||||
!event.isSubagent &&
|
|
||||||
matchesSelectedProject &&
|
matchesSelectedProject &&
|
||||||
state.connectionMode === 'local' &&
|
isUnknownSessionInSidebar &&
|
||||||
isUnknownSessionInSidebar;
|
(event.type === 'add' || (state.connectionMode === 'local' && event.type === 'change'));
|
||||||
|
|
||||||
// Refresh sidebar session list when a new top-level session is detected.
|
// Refresh sidebar session list only when a truly new top-level session appears.
|
||||||
// In local mode, some files can be observed as "change" before/without "add".
|
// Local fs.watch can report "change" before/without "add" for newly created files.
|
||||||
if ((event.type === 'add' && !event.isSubagent) || shouldRefreshForPotentialNewSession) {
|
if (shouldRefreshForPotentialNewSession) {
|
||||||
if (matchesSelectedProject && selectedProjectId) {
|
if (matchesSelectedProject && selectedProjectId) {
|
||||||
scheduleProjectRefresh(selectedProjectId);
|
scheduleProjectRefresh(selectedProjectId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Keep opened session view in sync on content changes.
|
// Keep opened session view in sync on content changes.
|
||||||
if (event.type === 'change' && selectedProjectId) {
|
// Some local writers emit rename/add for in-place updates, so include "add".
|
||||||
|
if ((event.type === 'change' || event.type === 'add') && selectedProjectId) {
|
||||||
const activeSessionId = state.selectedSessionId;
|
const activeSessionId = state.selectedSessionId;
|
||||||
const eventSessionId = event.sessionId;
|
const eventSessionId = event.sessionId;
|
||||||
const isViewingEventSession =
|
const isViewingEventSession =
|
||||||
|
|
|
||||||
|
|
@ -462,14 +462,14 @@ export const createSessionDetailSlice: StateCreator<AppState, [], [], SessionDet
|
||||||
}
|
}
|
||||||
|
|
||||||
const refreshKey = `${projectId}/${sessionId}`;
|
const refreshKey = `${projectId}/${sessionId}`;
|
||||||
const generation = (sessionRefreshGeneration.get(refreshKey) ?? 0) + 1;
|
|
||||||
sessionRefreshGeneration.set(refreshKey, generation);
|
|
||||||
|
|
||||||
// Coalesce duplicate in-flight refreshes for the same session.
|
// Coalesce duplicate in-flight refreshes for the same session.
|
||||||
if (sessionRefreshInFlight.has(refreshKey)) {
|
if (sessionRefreshInFlight.has(refreshKey)) {
|
||||||
sessionRefreshQueued.add(refreshKey);
|
sessionRefreshQueued.add(refreshKey);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const generation = (sessionRefreshGeneration.get(refreshKey) ?? 0) + 1;
|
||||||
|
sessionRefreshGeneration.set(refreshKey, generation);
|
||||||
sessionRefreshInFlight.add(refreshKey);
|
sessionRefreshInFlight.add(refreshKey);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
@ -501,10 +501,10 @@ export const createSessionDetailSlice: StateCreator<AppState, [], [], SessionDet
|
||||||
}
|
}
|
||||||
|
|
||||||
const latestState = get();
|
const latestState = get();
|
||||||
const latestActiveTab = latestState.getActiveTab();
|
const latestAllTabs = getAllTabs(latestState.paneLayout);
|
||||||
const stillViewingSession =
|
const stillViewingSession =
|
||||||
latestState.selectedSessionId === sessionId ||
|
latestState.selectedSessionId === sessionId ||
|
||||||
(latestActiveTab?.type === 'session' && latestActiveTab.sessionId === sessionId);
|
latestAllTabs.some((tab) => tab.type === 'session' && tab.sessionId === sessionId);
|
||||||
if (!stillViewingSession) {
|
if (!stillViewingSession) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -532,17 +532,14 @@ export const createSessionDetailSlice: StateCreator<AppState, [], [], SessionDet
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Also update the session's isOngoing in the sessions array
|
|
||||||
// This keeps the sidebar in sync with the chat view
|
|
||||||
const updatedSessions = currentState.sessions.map((s) =>
|
|
||||||
s.id === sessionId ? { ...s, isOngoing: detail.session?.isOngoing ?? false } : s
|
|
||||||
);
|
|
||||||
|
|
||||||
// Update only the data, preserve UI states
|
// Update only the data, preserve UI states
|
||||||
set({
|
set((state) => ({
|
||||||
sessionDetail: detail,
|
sessionDetail: detail,
|
||||||
conversation: newConversation,
|
conversation: newConversation,
|
||||||
sessions: updatedSessions,
|
// Update on latest sessions state to avoid restoring stale sidebar snapshots.
|
||||||
|
sessions: state.sessions.map((s) =>
|
||||||
|
s.id === sessionId ? { ...s, isOngoing: detail.session?.isOngoing ?? false } : s
|
||||||
|
),
|
||||||
// Preserve visible group if it still exists, otherwise keep current
|
// Preserve visible group if it still exists, otherwise keep current
|
||||||
...(visibleGroupStillExists
|
...(visibleGroupStillExists
|
||||||
? {
|
? {
|
||||||
|
|
@ -551,11 +548,10 @@ export const createSessionDetailSlice: StateCreator<AppState, [], [], SessionDet
|
||||||
: {}),
|
: {}),
|
||||||
// Note: aiGroupExpansionLevels and expandedStepIds are NOT touched
|
// Note: aiGroupExpansionLevels and expandedStepIds are NOT touched
|
||||||
// so expansion states are preserved
|
// so expansion states are preserved
|
||||||
});
|
}));
|
||||||
|
|
||||||
// Also update per-tab session data for all tabs viewing this session
|
// Also update per-tab session data for all tabs viewing this session
|
||||||
const latestTabSessionData = { ...get().tabSessionData };
|
const latestTabSessionData = { ...get().tabSessionData };
|
||||||
const latestAllTabs = getAllTabs(get().paneLayout);
|
|
||||||
for (const tab of latestAllTabs) {
|
for (const tab of latestAllTabs) {
|
||||||
if (tab.type === 'session' && tab.sessionId === sessionId && latestTabSessionData[tab.id]) {
|
if (tab.type === 'session' && tab.sessionId === sessionId && latestTabSessionData[tab.id]) {
|
||||||
const tabData = latestTabSessionData[tab.id];
|
const tabData = latestTabSessionData[tab.id];
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue