[Init] Initial commit - NetMesh terminal manager
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
This commit is contained in:
203
application/state/sessionSnapshotStore.ts
Normal file
203
application/state/sessionSnapshotStore.ts
Normal file
@@ -0,0 +1,203 @@
|
||||
import { useSyncExternalStore } from 'react';
|
||||
|
||||
import type { TerminalSession, Workspace } from '../../domain/models';
|
||||
import type { LogView } from './logViewState';
|
||||
import type { useSessionState } from './useSessionState';
|
||||
|
||||
type SessionState = ReturnType<typeof useSessionState>;
|
||||
|
||||
type Listener = () => void;
|
||||
|
||||
/**
|
||||
* Session/workspace catalog for shell surfaces. `activeTabId` is intentionally
|
||||
* absent — it lives in `activeTabStore` so tab switches stay off this path.
|
||||
*/
|
||||
export type SessionSnapshot = {
|
||||
sessions: readonly TerminalSession[];
|
||||
/** Sessions with no workspace and not hidden from tabs; TopTabs renders these. */
|
||||
orphanSessions: readonly TerminalSession[];
|
||||
workspaces: readonly Workspace[];
|
||||
logViews: readonly LogView[];
|
||||
draggingSessionId: string | null;
|
||||
sessionRenameTarget: TerminalSession | null;
|
||||
workspaceRenameTarget: Workspace | null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Session mutators the shell needs. Derived from the hook return so the store
|
||||
* contract cannot drift from `useSessionState`.
|
||||
*/
|
||||
export type SessionSnapshotActions = Pick<
|
||||
SessionState,
|
||||
| 'setActiveTabId'
|
||||
| 'closeSession'
|
||||
| 'closeSessions'
|
||||
| 'closeWorkspace'
|
||||
| 'openLogView'
|
||||
| 'closeLogView'
|
||||
| 'setDraggingSessionId'
|
||||
| 'startSessionRename'
|
||||
| 'renameSessionInline'
|
||||
| 'submitSessionRename'
|
||||
| 'resetSessionRename'
|
||||
| 'startWorkspaceRename'
|
||||
| 'submitWorkspaceRename'
|
||||
| 'resetWorkspaceRename'
|
||||
| 'removeSessionFromWorkspace'
|
||||
| 'setWorkspaceFocusedSession'
|
||||
| 'toggleWorkspaceViewMode'
|
||||
// Terminal / chrome mutators previously read only via App session runtime.
|
||||
| 'createLocalTerminal'
|
||||
| 'createSerialSession'
|
||||
| 'connectToHost'
|
||||
| 'updateSessionStatus'
|
||||
| 'updateSessionFontSize'
|
||||
| 'clearSessionFontSizeOverride'
|
||||
| 'createWorkspaceWithHosts'
|
||||
| 'createWorkspaceFromSessions'
|
||||
| 'addSessionToWorkspace'
|
||||
| 'appendHostToWorkspace'
|
||||
| 'appendLocalTerminalToWorkspace'
|
||||
| 'createWorkspaceFromTargets'
|
||||
| 'updateSplitSizes'
|
||||
| 'splitSession'
|
||||
| 'reorderWorkspaceSessions'
|
||||
| 'moveFocusInWorkspace'
|
||||
| 'runSnippet'
|
||||
| 'getOrderedWorkTabs'
|
||||
| 'reorderTabs'
|
||||
| 'toggleBroadcast'
|
||||
| 'isBroadcastEnabled'
|
||||
| 'toggleGlobalBroadcast'
|
||||
| 'isGlobalBroadcastEnabled'
|
||||
| 'canUseGlobalBroadcast'
|
||||
| 'copySession'
|
||||
| 'copyWorkspace'
|
||||
| 'createSessionFromCloneSource'
|
||||
| 'updateSessionRestoreCwd'
|
||||
| 'getSessionRestoreCwd'
|
||||
| 'updateSessionDynamicTitle'
|
||||
| 'updateSessionCodingCliProvider'
|
||||
>;
|
||||
|
||||
const EMPTY_SESSIONS: readonly TerminalSession[] = Object.freeze([]);
|
||||
const EMPTY_WORKSPACES: readonly Workspace[] = Object.freeze([]);
|
||||
const EMPTY_LOG_VIEWS: readonly LogView[] = Object.freeze([]);
|
||||
|
||||
export const EMPTY_SESSION_SNAPSHOT: SessionSnapshot = Object.freeze({
|
||||
sessions: EMPTY_SESSIONS,
|
||||
orphanSessions: EMPTY_SESSIONS,
|
||||
workspaces: EMPTY_WORKSPACES,
|
||||
logViews: EMPTY_LOG_VIEWS,
|
||||
draggingSessionId: null,
|
||||
sessionRenameTarget: null,
|
||||
workspaceRenameTarget: null,
|
||||
});
|
||||
|
||||
const SNAPSHOT_KEYS = Object.keys(EMPTY_SESSION_SNAPSHOT) as (keyof SessionSnapshot)[];
|
||||
|
||||
export function sessionSnapshotsEqual(
|
||||
prev: SessionSnapshot,
|
||||
next: SessionSnapshot,
|
||||
): boolean {
|
||||
return SNAPSHOT_KEYS.every((key) => prev[key] === next[key]);
|
||||
}
|
||||
|
||||
/**
|
||||
* External store for the session catalog so TopTabs, the terminal layer and
|
||||
* the host tree can subscribe directly instead of receiving sessions through
|
||||
* the App domain bags.
|
||||
*/
|
||||
class SessionSnapshotStore {
|
||||
private snapshot: SessionSnapshot = EMPTY_SESSION_SNAPSHOT;
|
||||
private actions: SessionSnapshotActions | null = null;
|
||||
private listeners = new Set<Listener>();
|
||||
private actionListeners = new Set<Listener>();
|
||||
|
||||
getSnapshot = (): SessionSnapshot => this.snapshot;
|
||||
|
||||
subscribe = (listener: Listener): (() => void) => {
|
||||
this.listeners.add(listener);
|
||||
return () => {
|
||||
this.listeners.delete(listener);
|
||||
};
|
||||
};
|
||||
|
||||
setSnapshot(next: SessionSnapshot): void {
|
||||
if (sessionSnapshotsEqual(this.snapshot, next)) return;
|
||||
this.snapshot = next;
|
||||
for (const listener of this.listeners) {
|
||||
listener();
|
||||
}
|
||||
}
|
||||
|
||||
getActions = (): SessionSnapshotActions | null => this.actions;
|
||||
|
||||
subscribeActions = (listener: Listener): (() => void) => {
|
||||
this.actionListeners.add(listener);
|
||||
return () => {
|
||||
this.actionListeners.delete(listener);
|
||||
};
|
||||
};
|
||||
|
||||
setActions(next: SessionSnapshotActions | null): void {
|
||||
if (this.actions === next) return;
|
||||
this.actions = next;
|
||||
for (const listener of this.actionListeners) {
|
||||
listener();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const sessionSnapshotStore = new SessionSnapshotStore();
|
||||
|
||||
export function publishSessionSnapshot(snapshot: SessionSnapshot): void {
|
||||
sessionSnapshotStore.setSnapshot(snapshot);
|
||||
}
|
||||
|
||||
export function getSessionSnapshot(): SessionSnapshot {
|
||||
return sessionSnapshotStore.getSnapshot();
|
||||
}
|
||||
|
||||
export function subscribeSessionSnapshot(listener: Listener): () => void {
|
||||
return sessionSnapshotStore.subscribe(listener);
|
||||
}
|
||||
|
||||
export function registerSessionSnapshotActions(
|
||||
actions: SessionSnapshotActions | null,
|
||||
): void {
|
||||
sessionSnapshotStore.setActions(actions);
|
||||
}
|
||||
|
||||
export function getSessionSnapshotActions(): SessionSnapshotActions | null {
|
||||
return sessionSnapshotStore.getActions();
|
||||
}
|
||||
|
||||
export function subscribeSessionSnapshotActions(listener: Listener): () => void {
|
||||
return sessionSnapshotStore.subscribeActions(listener);
|
||||
}
|
||||
|
||||
/** Subscribe to the whole session catalog. Prefer a narrower selector hook. */
|
||||
export function useSessionSnapshot(): SessionSnapshot {
|
||||
return useSyncExternalStore(
|
||||
subscribeSessionSnapshot,
|
||||
getSessionSnapshot,
|
||||
getSessionSnapshot,
|
||||
);
|
||||
}
|
||||
|
||||
/** Subscribe to one session field so unrelated session churn stays out. */
|
||||
export function useSessionSnapshotField<K extends keyof SessionSnapshot>(
|
||||
field: K,
|
||||
): SessionSnapshot[K] {
|
||||
const read = () => getSessionSnapshot()[field];
|
||||
return useSyncExternalStore(subscribeSessionSnapshot, read, read);
|
||||
}
|
||||
|
||||
export function useSessionSnapshotActions(): SessionSnapshotActions | null {
|
||||
return useSyncExternalStore(
|
||||
subscribeSessionSnapshotActions,
|
||||
getSessionSnapshotActions,
|
||||
getSessionSnapshotActions,
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user