Replace immersive instant-switch with animated active chrome theme sync so top tabs match terminal sessions immediately on tab click, and clamp autocomplete popups to the active pane so they stay anchored to the cursor in split workspaces. Co-authored-by: Cursor <cursoragent@cursor.com>
120 lines
4.1 KiB
TypeScript
120 lines
4.1 KiB
TypeScript
import { useCallback, useSyncExternalStore } from 'react';
|
|
|
|
import { terminalLayoutSuppressStore } from './terminalLayoutSuppressStore';
|
|
|
|
// Simple store for active tab that allows fine-grained subscriptions
|
|
type Listener = () => void;
|
|
type SyncListener = (activeTabId: string) => void;
|
|
|
|
// ----- Editor tab id helpers -----
|
|
export const EDITOR_PREFIX = 'editor:';
|
|
|
|
/** Returns true when `id` is an editor tab id (starts with "editor:"). */
|
|
export const isEditorTabId = (id: string): boolean => id.startsWith(EDITOR_PREFIX);
|
|
|
|
/** Convert an editorTab's internal id to a top-tab id understood by the tab bar. */
|
|
export const toEditorTabId = (editorId: string): string => `${EDITOR_PREFIX}${editorId}`;
|
|
|
|
/** Strip the "editor:" prefix to recover the internal editorTab id. */
|
|
export const fromEditorTabId = (tabId: string): string => tabId.slice(EDITOR_PREFIX.length);
|
|
|
|
class ActiveTabStore {
|
|
private activeTabId: string = 'vault';
|
|
private listeners = new Set<Listener>();
|
|
private syncListeners = new Set<SyncListener>();
|
|
private notifyRafId: number | null = null;
|
|
|
|
getActiveTabId = () => this.activeTabId;
|
|
|
|
private scheduleNotify = () => {
|
|
if (this.notifyRafId !== null) return;
|
|
const schedule = typeof requestAnimationFrame === 'function'
|
|
? requestAnimationFrame
|
|
: (cb: () => void) => window.setTimeout(cb, 0) as unknown as number;
|
|
this.notifyRafId = schedule(() => {
|
|
this.notifyRafId = null;
|
|
this.listeners.forEach((listener) => listener());
|
|
});
|
|
};
|
|
|
|
setActiveTabId = (id: string) => {
|
|
if (this.activeTabId !== id) {
|
|
terminalLayoutSuppressStore.begin();
|
|
this.activeTabId = id;
|
|
this.syncListeners.forEach((listener) => listener(id));
|
|
// Coalesce rapid tab switches into one notification per frame and avoid
|
|
// "setState during render" if called from a render phase.
|
|
this.scheduleNotify();
|
|
const schedule = typeof requestAnimationFrame === 'function'
|
|
? requestAnimationFrame
|
|
: (cb: () => void) => window.setTimeout(cb, 0) as unknown as number;
|
|
schedule(() => {
|
|
schedule(() => {
|
|
terminalLayoutSuppressStore.end();
|
|
});
|
|
});
|
|
}
|
|
};
|
|
|
|
subscribe = (listener: Listener) => {
|
|
this.listeners.add(listener);
|
|
return () => this.listeners.delete(listener);
|
|
};
|
|
|
|
subscribeSync = (listener: SyncListener) => {
|
|
this.syncListeners.add(listener);
|
|
return () => this.syncListeners.delete(listener);
|
|
};
|
|
}
|
|
|
|
export const activeTabStore = new ActiveTabStore();
|
|
|
|
// Hook to read active tab ID - only re-renders when activeTabId changes
|
|
export const useActiveTabId = () => {
|
|
return useSyncExternalStore(
|
|
activeTabStore.subscribe,
|
|
activeTabStore.getActiveTabId,
|
|
activeTabStore.getActiveTabId,
|
|
);
|
|
};
|
|
|
|
// Hook to get setter - never causes re-render
|
|
export const useSetActiveTabId = () => {
|
|
return activeTabStore.setActiveTabId;
|
|
};
|
|
|
|
// Check if a specific tab is active - only re-renders when this specific tab's active state changes
|
|
export const useIsTabActive = (tabId: string) => {
|
|
const getSnapshot = useCallback(() => activeTabStore.getActiveTabId() === tabId, [tabId]);
|
|
return useSyncExternalStore(activeTabStore.subscribe, getSnapshot, getSnapshot);
|
|
};
|
|
|
|
// Stable snapshot functions - defined once outside components
|
|
const getIsVaultActive = () => activeTabStore.getActiveTabId() === 'vault';
|
|
const getIsSftpActive = () => activeTabStore.getActiveTabId() === 'sftp';
|
|
|
|
// Check if vault is active
|
|
export const useIsVaultActive = () => {
|
|
return useSyncExternalStore(
|
|
activeTabStore.subscribe,
|
|
getIsVaultActive,
|
|
getIsVaultActive,
|
|
);
|
|
};
|
|
|
|
// Check if sftp is active
|
|
export const useIsSftpActive = () => {
|
|
return useSyncExternalStore(
|
|
activeTabStore.subscribe,
|
|
getIsSftpActive,
|
|
getIsSftpActive,
|
|
);
|
|
};
|
|
|
|
// Check if a specific editor tab is currently active
|
|
export const useIsEditorTabActive = (tabId: string): boolean => {
|
|
const editorTopId = toEditorTabId(tabId);
|
|
const getSnapshot = useCallback(() => activeTabStore.getActiveTabId() === editorTopId, [editorTopId]);
|
|
return useSyncExternalStore(activeTabStore.subscribe, getSnapshot, getSnapshot);
|
|
};
|