[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:
163
application/state/sessionPresentationStore.ts
Normal file
163
application/state/sessionPresentationStore.ts
Normal file
@@ -0,0 +1,163 @@
|
||||
import { useSyncExternalStore } from 'react';
|
||||
|
||||
import type { CodingCliProviderId } from '../../domain/codingCliProviders';
|
||||
|
||||
export type SessionPresentation = {
|
||||
dynamicTitle?: string | null;
|
||||
codingCliProviderId?: CodingCliProviderId | null;
|
||||
};
|
||||
|
||||
type Listener = () => void;
|
||||
|
||||
/** Encode undefined / null / present distinctly for useSyncExternalStore snapshots. */
|
||||
function encodePresentationField(value: string | null | undefined): string {
|
||||
if (value === undefined) return 'u';
|
||||
if (value === null) return 'n';
|
||||
return `v:${value}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Presentation-only session chrome (tab title / coding-CLI icon) separate from
|
||||
* structural session identity used by TerminalLayer pane equality.
|
||||
*/
|
||||
class SessionPresentationStore {
|
||||
private bySession = new Map<string, SessionPresentation>();
|
||||
private version = 0;
|
||||
private listeners = new Set<Listener>();
|
||||
|
||||
getVersion = (): number => this.version;
|
||||
|
||||
getPresentation = (sessionId: string): SessionPresentation | undefined =>
|
||||
this.bySession.get(sessionId);
|
||||
|
||||
/**
|
||||
* Stable per-session snapshot for useSyncExternalStore. Global listeners still
|
||||
* fire on any change, but React skips re-render when this string is unchanged
|
||||
* for the subscribed sessionId (Object.is).
|
||||
*
|
||||
* Encodes undefined / null / value distinctly so a null tombstone is not
|
||||
* Object.is-equal to a missing field (both used to collapse to '').
|
||||
*/
|
||||
getSessionSnapshot = (sessionId: string): string => {
|
||||
const presentation = this.bySession.get(sessionId);
|
||||
if (!presentation) return '';
|
||||
return `${encodePresentationField(presentation.dynamicTitle)}\0${encodePresentationField(presentation.codingCliProviderId)}`;
|
||||
};
|
||||
|
||||
subscribe = (listener: Listener): (() => void) => {
|
||||
this.listeners.add(listener);
|
||||
return () => {
|
||||
this.listeners.delete(listener);
|
||||
};
|
||||
};
|
||||
|
||||
setPresentation(sessionId: string, patch: SessionPresentation): void {
|
||||
const prev = this.bySession.get(sessionId) ?? {};
|
||||
const next: SessionPresentation = { ...prev, ...patch };
|
||||
// Distinguish missing (undefined) from explicit clear (null) so the first
|
||||
// tombstone is stored even when the session snapshot still has a stale
|
||||
// title/provider and the store had no prior entry.
|
||||
if (
|
||||
prev.dynamicTitle === next.dynamicTitle
|
||||
&& prev.codingCliProviderId === next.codingCliProviderId
|
||||
) {
|
||||
return;
|
||||
}
|
||||
this.bySession.set(sessionId, next);
|
||||
this.version += 1;
|
||||
for (const listener of this.listeners) listener();
|
||||
}
|
||||
|
||||
clearSession(sessionId: string): void {
|
||||
if (!this.bySession.has(sessionId)) return;
|
||||
this.bySession.delete(sessionId);
|
||||
this.version += 1;
|
||||
for (const listener of this.listeners) listener();
|
||||
}
|
||||
|
||||
prune(validSessionIds: ReadonlySet<string>): void {
|
||||
let changed = false;
|
||||
for (const id of this.bySession.keys()) {
|
||||
if (!validSessionIds.has(id)) {
|
||||
this.bySession.delete(id);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
if (!changed) return;
|
||||
this.version += 1;
|
||||
for (const listener of this.listeners) listener();
|
||||
}
|
||||
}
|
||||
|
||||
export const sessionPresentationStore = new SessionPresentationStore();
|
||||
|
||||
export function publishSessionDynamicTitle(sessionId: string, title: string | null): void {
|
||||
sessionPresentationStore.setPresentation(sessionId, { dynamicTitle: title });
|
||||
}
|
||||
|
||||
export function publishSessionCodingCliProvider(
|
||||
sessionId: string,
|
||||
providerId: CodingCliProviderId | null,
|
||||
): void {
|
||||
sessionPresentationStore.setPresentation(sessionId, { codingCliProviderId: providerId });
|
||||
}
|
||||
|
||||
type SessionWithPresentation = {
|
||||
id: string;
|
||||
dynamicTitle?: string;
|
||||
codingCliProviderId?: CodingCliProviderId;
|
||||
};
|
||||
|
||||
/**
|
||||
* Overlay live presentation chrome onto a session snapshot.
|
||||
* Used by TopTabs, focus sidebar, and pane chrome so title/provider updates
|
||||
* stay live without structural setSessions thrash.
|
||||
*/
|
||||
export function applySessionPresentation<T extends SessionWithPresentation>(session: T): T {
|
||||
const presentation = sessionPresentationStore.getPresentation(session.id);
|
||||
if (!presentation) return session;
|
||||
const nextTitle = presentation.dynamicTitle === undefined
|
||||
? session.dynamicTitle
|
||||
: (presentation.dynamicTitle ?? undefined);
|
||||
const nextProvider = presentation.codingCliProviderId === undefined
|
||||
? session.codingCliProviderId
|
||||
: (presentation.codingCliProviderId ?? undefined);
|
||||
if (
|
||||
nextTitle === session.dynamicTitle
|
||||
&& nextProvider === session.codingCliProviderId
|
||||
) {
|
||||
return session;
|
||||
}
|
||||
return {
|
||||
...session,
|
||||
dynamicTitle: nextTitle,
|
||||
codingCliProviderId: nextProvider,
|
||||
};
|
||||
}
|
||||
|
||||
/** Subscribe to live title/provider chrome version for multi-session consumers. */
|
||||
export function useSessionPresentationVersion(): number {
|
||||
return useSyncExternalStore(
|
||||
sessionPresentationStore.subscribe,
|
||||
sessionPresentationStore.getVersion,
|
||||
sessionPresentationStore.getVersion,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Per-session presentation snapshot. Other sessions' title updates notify the
|
||||
* store but do not re-render this consumer when its own snapshot is unchanged.
|
||||
*/
|
||||
export function useSessionPresentationSnapshot(sessionId: string): string {
|
||||
return useSyncExternalStore(
|
||||
sessionPresentationStore.subscribe,
|
||||
() => sessionPresentationStore.getSessionSnapshot(sessionId),
|
||||
() => sessionPresentationStore.getSessionSnapshot(sessionId),
|
||||
);
|
||||
}
|
||||
|
||||
/** Session snapshot with live presentation overlay applied (single session). */
|
||||
export function usePresentedSession<T extends SessionWithPresentation>(session: T): T {
|
||||
useSessionPresentationSnapshot(session.id);
|
||||
return applySessionPresentation(session);
|
||||
}
|
||||
Reference in New Issue
Block a user