Files
openchamber/packages/ui/src/apps/runtimeEndpointReset.ts
T
Bohdan Triapitsyn 094f728777 perf(ui): swap the session activity spinner for a dot and a turn timer
The spinner ran a CSS animation on every active row for the whole turn,
repainting a composited layer at frame rate. Rows now carry a static dot —
primary while running, info while unread — and the metadata slot on the right
shows how long the turn has been going, updating once per second in the dot's
colour. The counter is the motion the spinner used to provide, at 1 fps.
Collapsed groups, folders and projects take the dot only, since one counter
cannot speak for several running turns.

Elapsed time is measured client-side because SessionStatus carries no
timestamps, and starts are persisted so a reload resumes the same count. Two
rules keep that honest. Only a liveness stamp — refreshed while a session is
observed active, stamped as the page hides, and compared against the page's
navigation start so a slow bootstrap is not charged to the absence — and a 90s
adoption window may expire a record; a snapshot that cannot yet see a session
is not evidence its turn ended. And a busy event is never read as a turn
boundary, because OpenCode republishes busy at every step of the agent loop, so
after a reload one of those repeats normally beats the first status snapshot.
Idle and error events do end a turn, and retire the record with it.

Snapshot reconciliation walks the running turns and asks whether the snapshot
covers each one, rather than being handed everything it covers: only a live
start can settle, so the pass scales with timing work instead of with the
directory's session list, and allocates nothing per poll.

Also applied to the mobile sessions sheet and session switcher. The shared
duration ticker moves to hooks/ now that it has a second consumer.
2026-08-05 03:06:35 +03:00

74 lines
3.9 KiB
TypeScript

import { opencodeClient } from '@/lib/opencode/client';
import type { RuntimeEndpointChangedDetail } from '@/lib/runtime-switch';
import { disposeTerminalInputTransport } from '@/lib/terminalApi';
import { useConfigStore } from '@/stores/useConfigStore';
import { useProjectsStore } from '@/stores/useProjectsStore';
import { useGlobalSessionsStore } from '@/stores/useGlobalSessionsStore';
import { useAutoReviewStore } from '@/stores/useAutoReviewStore';
import { useUIStore } from '@/stores/useUIStore';
import { usePermissionStore } from '@/stores/permissionStore';
import { useFileSearchStore } from '@/stores/useFileSearchStore';
import { useGitStore } from '@/stores/useGitStore';
import { useGitHubPrStatusStore } from '@/stores/useGitHubPrStatusStore';
import { useSessionFoldersStore } from '@/stores/useSessionFoldersStore';
import { useFilesViewTabsStore } from '@/stores/useFilesViewTabsStore';
import { useTerminalStore } from '@/stores/useTerminalStore';
import { useSessionUIStore } from '@/sync/session-ui-store';
import { resetStreamingState } from '@/sync/streaming';
import { useGlobalSessionStatusStore } from '@/sync/global-session-status';
import { resetSessionOrdering } from '@/sync/session-ordering';
import { resetSessionActivityTiming } from '@/sync/session-activity-timing';
import { syncDesktopSettings } from '@/lib/persistence';
// Same-device transport switch (LAN⇄relay for one paired device): rebind the SDK
// to the new transport WITHOUT tearing down connection/session state or remounting
// the sync layer. `reconnectToRuntimeBaseUrl` swaps in a fresh SDK client; the
// caller then forces a re-render so SyncProvider receives it as a new `sdk` prop,
// which re-runs its event-pipeline + bootstrap effects (keyed on `sdk`) to
// reconnect over the new transport IN PLACE. Message-pagination refs, the open
// session, and the whole view are preserved — no reconnecting screen, no flash,
// no bounce back to the draft.
export const reconnectAppForTransportSwitch = (): void => {
disposeTerminalInputTransport();
opencodeClient.reconnectToRuntimeBaseUrl();
resetStreamingState();
};
export const resetAppForRuntimeEndpointChange = (detail: RuntimeEndpointChangedDetail): void => {
useSessionUIStore.getState().prepareForRuntimeSwitch(detail.previousRuntimeKey);
useUIStore.getState().prepareForRuntimeSwitch(detail.previousRuntimeKey);
if (detail.previousRuntimeKey) {
useAutoReviewStore.getState().stopRunningRunsForRuntime(detail.previousRuntimeKey);
}
disposeTerminalInputTransport();
useTerminalStore.getState().clearAll();
opencodeClient.reconnectToRuntimeBaseUrl();
useConfigStore.setState({
providers: [],
agents: [],
isConnected: false,
isInitialized: false,
connectionPhase: 'connecting',
lastDisconnectReason: null,
});
useProjectsStore.getState().resetForRuntimeSwitch();
// Cross-project session list (mobile sessions sheet & co) belongs to the
// previous instance — drop it so stale sessions can't linger after a switch.
useGlobalSessionsStore.getState().resetForRuntimeSwitch();
useGlobalSessionStatusStore.setState({ statusById: new Map() });
resetSessionOrdering();
// Turn timings belong to the previous instance's sessions, and the reset also
// restarts the resume window so the switch is treated as a fresh load.
resetSessionActivityTiming();
usePermissionStore.getState().reset();
useFileSearchStore.getState().resetForRuntimeSwitch();
useGitStore.getState().resetForRuntimeSwitch(detail.runtimeKey);
useGitHubPrStatusStore.getState().resetForRuntimeSwitch();
useSessionFoldersStore.getState().resetForRuntimeSwitch(detail.runtimeKey);
useFilesViewTabsStore.getState().resetForRuntimeSwitch(detail.runtimeKey);
useSessionUIStore.getState().restoreForRuntimeSwitch(detail.runtimeKey);
useUIStore.getState().restoreForRuntimeSwitch(detail.runtimeKey);
resetStreamingState();
queueMicrotask(() => void syncDesktopSettings());
};