refactor(terminal): keep mobile content-fact caches in their producers (#27537)

The agent-status projection and mobile terminal theme kept their caches on
the shared graph state object. Each producer now owns its cache, so neither
imports the graph state or graph types; the graph still calls them as before.
This commit is contained in:
Jinwoo Hong
2026-10-11 15:20:24 -04:00
committed by GitHub
parent 5e5865a226
commit dbd97f7d67
6 changed files with 42 additions and 38 deletions
@@ -19,14 +19,19 @@ import { performance } from 'node:perf_hooks'
import { fileURLToPath } from 'node:url'
const GRAPH_SOURCE = readFileSync(
fileURLToPath(new URL('../../src/renderer/src/runtime/sync-runtime-graph.ts', import.meta.url)),
fileURLToPath(
new URL(
'../../src/renderer/src/runtime/sync-runtime-graph/agent-status-projection.ts',
import.meta.url
)
),
'utf8'
)
const bucketMatch = GRAPH_SOURCE.match(/AGENT_STATUS_SYNC_UPDATED_AT_BUCKET_MS = ([0-9_]+)/)
if (!bucketMatch) {
throw new Error(
'sync-runtime-graph.ts no longer defines the updatedAt bucket; re-sync this benchmark.'
'agent-status-projection.ts no longer defines the updatedAt bucket; re-sync this benchmark.'
)
}
const BUCKET_MS = Number(bucketMatch[1].replaceAll('_', ''))
@@ -20,6 +20,7 @@ vi.mock('./sync-runtime-graph/editor-draft-hash', async (importOriginal) => {
const { stableHashString } = await import('./sync-runtime-graph/editor-draft-hash')
const {
AGENT_STATUS_SYNC_UPDATED_AT_BUCKET_MS_FOR_TESTS: AGENT_STATUS_SYNC_UPDATED_AT_BUCKET_MS,
buildRuntimeMobileAgentStatusProjectionForTests,
getRuntimeMobileSessionSyncKey,
resetRuntimeMobileAgentStatusProjectionCacheForTests,
@@ -31,7 +32,6 @@ const {
buildRuntimeMobileEditorDraftsProjection,
buildRuntimeMobileOpenFilesProjection
} = await import('./sync-runtime-graph/sync-projections')
const { AGENT_STATUS_SYNC_UPDATED_AT_BUCKET_MS } = await import('./sync-runtime-graph/graph-state')
// ── Reference implementations: the pre-change bodies, kept verbatim ────────────────────
@@ -1,7 +1,19 @@
import type { AppState } from '@/store/types'
import type { AgentMainAgentStatus } from '../../../../shared/main-agent-status'
import { AGENT_STATUS_SYNC_UPDATED_AT_BUCKET_MS, graphState } from './graph-state'
import type { AgentStatusProjectionCacheEntry } from './types'
const AGENT_STATUS_SYNC_UPDATED_AT_BUCKET_MS = 30_000
type AgentStatusProjectionCacheEntry = {
entry: AppState['agentStatusByPaneKey'][string]
projection: string
}
type AgentStatusProjectionCache = {
source: AppState['agentStatusByPaneKey']
entries: Map<string, AgentStatusProjectionCacheEntry>
projection: string
}
let cachedAgentStatusProjection: AgentStatusProjectionCache | null = null
function mainAgentKey(mainAgent: AgentMainAgentStatus | undefined) {
return mainAgent ? [mainAgent.state, mainAgent.outcome ?? null, mainAgent.stateStartedAt] : null
@@ -44,7 +56,7 @@ function serializeAgentStatusEntry(
export function buildRuntimeMobileAgentStatusProjection(
agentStatusByPaneKey: AppState['agentStatusByPaneKey']
): string {
const cached = graphState.cachedAgentStatusProjection
const cached = cachedAgentStatusProjection
if (cached?.source === agentStatusByPaneKey) {
return cached.projection
}
@@ -59,7 +71,7 @@ export function buildRuntimeMobileAgentStatusProjection(
nextEntries.length === cached.entries.size &&
nextEntries.every(([paneKey, entry]) => cached.entries.get(paneKey)?.entry === entry)
) {
graphState.cachedAgentStatusProjection = {
cachedAgentStatusProjection = {
...cached,
source: agentStatusByPaneKey
}
@@ -85,7 +97,7 @@ export function buildRuntimeMobileAgentStatusProjection(
}
// Same-bucket heartbeats must not rejoin every pane's accumulated preview text.
const projection = projectionUnchanged && cached ? cached.projection : `[${parts.join(',')}]`
graphState.cachedAgentStatusProjection = {
cachedAgentStatusProjection = {
source: agentStatusByPaneKey,
entries,
projection
@@ -103,5 +115,5 @@ export const AGENT_STATUS_SYNC_UPDATED_AT_BUCKET_MS_FOR_TESTS =
AGENT_STATUS_SYNC_UPDATED_AT_BUCKET_MS
export function resetRuntimeMobileAgentStatusProjectionCacheForTests(): void {
graphState.cachedAgentStatusProjection = null
cachedAgentStatusProjection = null
}
@@ -2,11 +2,9 @@ import { createBrowserUuid } from '@/lib/browser-uuid'
import type { AppState } from '@/store/types'
import type {
RuntimeMobileSessionSnapshotTab,
RuntimeMobileSessionTabsSnapshot,
RuntimeMobileTerminalTheme
RuntimeMobileSessionTabsSnapshot
} from '../../../../shared/runtime-types'
import type {
AgentStatusProjectionCache,
BrowserPagesProjectionCache,
BrowserWorkspacesProjectionCache,
EditorDraftHashCache,
@@ -17,7 +15,6 @@ import type {
} from './types'
export const NO_TRANSPORT_GRACE_MS = 10_000
export const AGENT_STATUS_SYNC_UPDATED_AT_BUCKET_MS = 30_000
export const RUNTIME_GRAPH_SYNC_COALESCE_MS = 16
export const EMPTY_ACTIVE_BROWSER_TAB_ID_BY_WORKTREE: AppState['activeBrowserTabIdByWorktree'] = {}
@@ -41,7 +38,6 @@ export const graphState = {
syncEnabled: false,
syncTimer: null as ReturnType<typeof setTimeout> | null,
getStoreState: null as (() => AppState) | null,
mobileSessionSnapshotVersion: 0,
mobileSessionSnapshotCacheByWorktree: new Map<
string,
{
@@ -52,17 +48,13 @@ export const graphState = {
>(),
publishedMobileSessionSnapshotByWorktree: new Map<string, RuntimeMobileSessionTabsSnapshot>(),
cachedTabsProjection: null as TabsProjectionCache | null,
cachedAgentStatusProjection: null as AgentStatusProjectionCache | null,
cachedOpenFilesProjection: null as OpenFilesProjectionCache | null,
cachedBrowserWorkspacesProjection: null as BrowserWorkspacesProjectionCache | null,
cachedBrowserPagesProjection: null as BrowserPagesProjectionCache | null,
cachedOpenFileIndexesSource: null as AppState['openFiles'] | null,
cachedOpenFileIndexes: null as OpenFileIndexes | null,
cachedEditorDraftHashes: null as EditorDraftHashCache | null,
cachedMobileTerminalThemeSettings: null as AppState['settings'] | null,
cachedMobileTerminalThemeSystemPrefersDark: null as boolean | null,
cachedMobileTerminalTheme: undefined as RuntimeMobileTerminalTheme | undefined,
hasCachedMobileTerminalTheme: false
mobileSessionSnapshotVersion: 0
}
/** IDs occurring more than once cannot address the legacy tab-keyed runtime maps safely. */
@@ -1,7 +1,6 @@
import { getSystemPrefersDark, resolveEffectiveTerminalAppearance } from '@/lib/terminal-theme'
import type { AppState } from '@/store/types'
import type { RuntimeMobileTerminalTheme } from '../../../../shared/runtime-types'
import { graphState } from './graph-state'
import { normalizeTerminalMinimumContrastRatio } from '@/lib/terminal-contrast-correction'
function hexToRgba(hex: string, alpha: number): string {
@@ -64,20 +63,25 @@ export function resolveMobileTerminalTheme(
}
}
let cachedMobileTerminalThemeSettings: AppState['settings'] | null = null
let cachedMobileTerminalThemeSystemPrefersDark: boolean | null = null
let cachedMobileTerminalTheme: RuntimeMobileTerminalTheme | undefined
let hasCachedMobileTerminalTheme = false
export function getMobileTerminalTheme(
state: AppState,
systemPrefersDark = getSystemPrefersDark()
): RuntimeMobileTerminalTheme | undefined {
if (
graphState.hasCachedMobileTerminalTheme &&
graphState.cachedMobileTerminalThemeSettings === state.settings &&
graphState.cachedMobileTerminalThemeSystemPrefersDark === systemPrefersDark
hasCachedMobileTerminalTheme &&
cachedMobileTerminalThemeSettings === state.settings &&
cachedMobileTerminalThemeSystemPrefersDark === systemPrefersDark
) {
return graphState.cachedMobileTerminalTheme
return cachedMobileTerminalTheme
}
graphState.cachedMobileTerminalTheme = resolveMobileTerminalTheme(state, systemPrefersDark)
graphState.cachedMobileTerminalThemeSettings = state.settings
graphState.cachedMobileTerminalThemeSystemPrefersDark = systemPrefersDark
graphState.hasCachedMobileTerminalTheme = true
return graphState.cachedMobileTerminalTheme
cachedMobileTerminalTheme = resolveMobileTerminalTheme(state, systemPrefersDark)
cachedMobileTerminalThemeSettings = state.settings
cachedMobileTerminalThemeSystemPrefersDark = systemPrefersDark
hasCachedMobileTerminalTheme = true
return cachedMobileTerminalTheme
}
@@ -92,15 +92,6 @@ export type EditorDraftHashCache = {
hashByFileId: Map<string, string>
projection: string
}
export type AgentStatusProjectionCacheEntry = {
entry: AppState['agentStatusByPaneKey'][string]
projection: string
}
export type AgentStatusProjectionCache = {
source: AppState['agentStatusByPaneKey']
entries: Map<string, AgentStatusProjectionCacheEntry>
projection: string
}
export type MobileSessionAgentStatusByWorktree = ReadonlyMap<
string,
ReadonlyMap<string, AppState['agentStatusByPaneKey'][string]>