Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,15 @@ import { computeFixedPopoverPosition } from '@/shared/utils/fixedPopoverViewport
import { sessionAPI } from '@/infrastructure/api/service-api/SessionAPI';
import { confirmWarning } from '@/component-library/components/ConfirmDialog/confirmService';
import ScheduledJobsModal from '@/app/components/scheduled-jobs/ScheduledJobsModal';
import { scheduleAfterStartupPaint, scheduleAfterStartupSignal } from '@/shared/utils/startupTaskScheduling';
import {
SESSION_METADATA_DEFERRED_FALLBACK_MS,
SESSION_METADATA_DEFERRED_FRAME_COUNT,
SESSION_METADATA_DEFERRED_SIGNAL,
getDeferredSessionMetadataDelayMs,
getInitialSessionMetadataLoadMode,
hasStartupOverlayHandedOff,
} from './sessionMetadataStartup';
import './SessionsSection.scss';

/** Top-level parent sessions shown at each expand step (children still nest under visible parents). */
Expand Down Expand Up @@ -127,7 +136,7 @@ const SessionsSection: React.FC<SessionsSectionProps> = ({
workspacePath,
remoteConnectionId = null,
remoteSshHost = null,
isActiveWorkspace: _isActiveWorkspace = true,
isActiveWorkspace = true,
assistantLabel,
showSessionModeIcon = true,
isVisible = true,
Expand Down Expand Up @@ -164,6 +173,7 @@ const SessionsSection: React.FC<SessionsSectionProps> = ({
const sessionMenuPopoverRef = useRef<HTMLDivElement>(null);
const sessionMenuAnchorRef = useRef<HTMLButtonElement>(null);
const metadataLoadRequestIdRef = useRef(0);
const initialMetadataLoadKeyRef = useRef<string | null>(null);

// Subscribe to state machine changes for running status
useEffect(() => {
Expand Down Expand Up @@ -203,6 +213,7 @@ const SessionsSection: React.FC<SessionsSectionProps> = ({

useEffect(() => {
metadataLoadRequestIdRef.current += 1;
initialMetadataLoadKeyRef.current = null;
setExpandLevel(0);
setMetadataPageState({
totalTopLevelCount: null,
Expand Down Expand Up @@ -251,13 +262,98 @@ const SessionsSection: React.FC<SessionsSectionProps> = ({
[workspacePath, remoteConnectionId, remoteSshHost]
);

const initialMetadataKey = useMemo(
() => [
workspacePath ?? '',
remoteConnectionId ?? '',
remoteSshHost ?? '',
].join('\n'),
[workspacePath, remoteConnectionId, remoteSshHost],
);

const loadInitialMetadataPage = useCallback(
async (source: string) => {
if (!workspacePath) {
return;
}
if (initialMetadataLoadKeyRef.current === initialMetadataKey) {
return;
}

initialMetadataLoadKeyRef.current = initialMetadataKey;
const page = await loadMetadataPage(SESSIONS_LEVEL_0, undefined, source);
if (!page && initialMetadataLoadKeyRef.current === initialMetadataKey) {
initialMetadataLoadKeyRef.current = null;
}
},
[initialMetadataKey, loadMetadataPage, workspacePath],
);

useEffect(() => {
if (!isVisible || !workspacePath) {
return;
}

void loadMetadataPage(SESSIONS_LEVEL_0, undefined, 'sessions_nav_initial');
}, [isVisible, workspacePath, remoteConnectionId, remoteSshHost, loadMetadataPage]);
const loadMode = getInitialSessionMetadataLoadMode({
hasWorkspacePath: Boolean(workspacePath),
isActiveWorkspace,
isVisible,
startupOverlayHandedOff: hasStartupOverlayHandedOff(),
});

if (loadMode === 'skip') {
return;
}

if (loadMode === 'immediate') {
void loadInitialMetadataPage('sessions_nav_initial_active');
return;
}

let cancelled = false;
let delayTimer: number | null = null;
const scheduleDeferredMetadataLoad = () => {
if (cancelled) {
return;
}
const delayMs = getDeferredSessionMetadataDelayMs(workspaceId ?? workspacePath);
const runDeferredLoad = () => {
delayTimer = null;
if (!cancelled) {
void loadInitialMetadataPage('sessions_nav_initial_deferred');
}
};

if (delayMs > 0) {
delayTimer = window.setTimeout(runDeferredLoad, delayMs);
return;
}
runDeferredLoad();
};
const cancelStartupSchedule = loadMode === 'after-startup-paint'
? scheduleAfterStartupPaint(scheduleDeferredMetadataLoad, {
frameCount: SESSION_METADATA_DEFERRED_FRAME_COUNT,
})
: scheduleAfterStartupSignal(scheduleDeferredMetadataLoad, {
signalName: SESSION_METADATA_DEFERRED_SIGNAL,
fallbackTimeoutMs: SESSION_METADATA_DEFERRED_FALLBACK_MS,
frameCount: SESSION_METADATA_DEFERRED_FRAME_COUNT,
});

return () => {
cancelled = true;
cancelStartupSchedule();
if (delayTimer !== null) {
window.clearTimeout(delayTimer);
}
};
}, [
isActiveWorkspace,
isVisible,
loadInitialMetadataPage,
workspaceId,
workspacePath,
]);

useEffect(() => {
if (!openMenuSessionId) return;
Expand Down Expand Up @@ -379,6 +475,12 @@ const SessionsSection: React.FC<SessionsSectionProps> = ({
const totalTopLevelSessionCount = metadataPageState.totalTopLevelCount ?? topLevelSessions.length;
const hasMoreUnloadedSessions =
metadataPageState.hasMore || topLevelSessions.length < totalTopLevelSessionCount;
const expandToggleAction =
expandLevel === 0
? 'show-more'
: expandLevel === 1 && totalTopLevelSessionCount > SESSIONS_LEVEL_1
? 'show-all'
: 'show-less';

const visibleItems = useMemo(() => {
const visibleParents = topLevelSessions.slice(0, sessionDisplayLimit);
Expand Down Expand Up @@ -961,6 +1063,7 @@ const SessionsSection: React.FC<SessionsSectionProps> = ({
type="button"
className={`bitfun-nav-panel__inline-toggle${metadataPageState.isLoading ? ' is-loading' : ''}`}
data-testid="session-nav-show-more"
data-session-nav-toggle-action={expandToggleAction}
disabled={metadataPageState.isLoading}
onClick={() => { void handleExpandToggle(); }}
>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { afterEach, describe, expect, it, vi } from 'vitest';

import {
SESSION_METADATA_DEFERRED_FALLBACK_MS,
SESSION_METADATA_DEFERRED_FRAME_COUNT,
SESSION_METADATA_DEFERRED_SIGNAL,
getDeferredSessionMetadataDelayMs,
getInitialSessionMetadataLoadMode,
hasStartupOverlayHandedOff,
} from './sessionMetadataStartup';

describe('session metadata startup scheduling', () => {
afterEach(() => {
vi.unstubAllGlobals();
});

it('chooses the startup gate for each initial metadata load path', () => {
expect(getInitialSessionMetadataLoadMode({
hasWorkspacePath: false,
isActiveWorkspace: true,
isVisible: true,
startupOverlayHandedOff: false,
})).toBe('skip');

expect(getInitialSessionMetadataLoadMode({
hasWorkspacePath: true,
isActiveWorkspace: true,
isVisible: true,
startupOverlayHandedOff: false,
})).toBe('immediate');

expect(getInitialSessionMetadataLoadMode({
hasWorkspacePath: true,
isActiveWorkspace: false,
isVisible: true,
startupOverlayHandedOff: false,
})).toBe('after-startup-signal');

expect(getInitialSessionMetadataLoadMode({
hasWorkspacePath: true,
isActiveWorkspace: false,
isVisible: true,
startupOverlayHandedOff: true,
})).toBe('after-startup-paint');

expect(getInitialSessionMetadataLoadMode({
hasWorkspacePath: true,
isActiveWorkspace: false,
isVisible: false,
startupOverlayHandedOff: true,
})).toBe('skip');
});

it('keeps deferred metadata tied to the startup overlay handoff', () => {
expect(SESSION_METADATA_DEFERRED_SIGNAL).toBe('bitfun:startup-overlay-hidden');
expect(SESSION_METADATA_DEFERRED_FALLBACK_MS).toBe(10000);
expect(SESSION_METADATA_DEFERRED_FRAME_COUNT).toBe(1);
});

it('uses a stable bounded stagger for background workspace metadata', () => {
const first = getDeferredSessionMetadataDelayMs('D:\\workspace\\BitFun');
const second = getDeferredSessionMetadataDelayMs('D:\\workspace\\BitFun');
const other = getDeferredSessionMetadataDelayMs('D:\\workspace\\Other');

expect(first).toBe(second);
expect(first).toBeGreaterThanOrEqual(0);
expect(first).toBeLessThanOrEqual(120);
expect(other).toBeGreaterThanOrEqual(0);
expect(other).toBeLessThanOrEqual(120);
expect(getDeferredSessionMetadataDelayMs()).toBe(0);
});

it('detects when a late-mounted section has already missed the overlay handoff event', () => {
vi.stubGlobal('document', {
getElementById: vi.fn(() => ({})),
});
expect(hasStartupOverlayHandedOff()).toBe(false);

vi.stubGlobal('document', {
getElementById: vi.fn(() => null),
});
expect(hasStartupOverlayHandedOff()).toBe(true);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { STARTUP_OVERLAY_HIDDEN_EVENT } from '@/app/startup/startupSignals';
import { isStartupOverlayPresent } from '@/app/startup/startupOverlay';

export const SESSION_METADATA_DEFERRED_SIGNAL = STARTUP_OVERLAY_HIDDEN_EVENT;
export const SESSION_METADATA_DEFERRED_FALLBACK_MS = 10000;
export const SESSION_METADATA_DEFERRED_FRAME_COUNT = 1;

export type InitialSessionMetadataLoadMode =
| 'skip'
| 'immediate'
| 'after-startup-signal'
| 'after-startup-paint';

export function getInitialSessionMetadataLoadMode({
hasWorkspacePath,
isActiveWorkspace,
isVisible,
startupOverlayHandedOff,
}: {
hasWorkspacePath: boolean;
isActiveWorkspace: boolean;
isVisible: boolean;
startupOverlayHandedOff: boolean;
}): InitialSessionMetadataLoadMode {
if (!hasWorkspacePath || !isVisible) {
return 'skip';
}

if (isActiveWorkspace) {
return 'immediate';
}

return startupOverlayHandedOff ? 'after-startup-paint' : 'after-startup-signal';
}

export function getDeferredSessionMetadataDelayMs(workspaceKey?: string | null): number {
const normalized = workspaceKey?.trim();
if (!normalized) {
return 0;
}

let hash = 0;
for (let index = 0; index < normalized.length; index += 1) {
hash = (hash * 31 + normalized.charCodeAt(index)) >>> 0;
}

return (hash % 4) * 40;
}

export function hasStartupOverlayHandedOff(): boolean {
if (typeof document === 'undefined') {
return true;
}
return !isStartupOverlayPresent();
}
17 changes: 17 additions & 0 deletions src/web-ui/src/app/startup/startupPerformanceContract.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,23 @@ describe('startup performance contract', () => {
expect(getSource).not.toContain('restore_session');
});

it('keeps non-active workspace session metadata out of the first startup window', () => {
const source = readSource('../../app/components/NavPanel/sections/sessions/SessionsSection.tsx');

expect(source).toContain('isActiveWorkspace = true');
expect(source).not.toContain('isActiveWorkspace: _isActiveWorkspace');
expect(source).toContain('getInitialSessionMetadataLoadMode');
expect(source).toContain("loadMode === 'immediate'");
expect(source).toContain("loadMode === 'after-startup-paint'");
expect(source).toContain('scheduleAfterStartupSignal');
expect(source).toContain('scheduleAfterStartupPaint');
expect(source).toContain('hasStartupOverlayHandedOff');
expect(source).toContain('SESSION_METADATA_DEFERRED_SIGNAL');
expect(source).toContain('sessions_nav_initial_active');
expect(source).toContain('sessions_nav_initial_deferred');
expect(source).toContain('data-session-nav-toggle-action');
});

it('keeps Git diff editor from importing the broad editor barrel', () => {
const source = readSource('../../tools/git/components/GitDiffEditor/GitDiffEditor.tsx');

Expand Down
Loading
Loading