From 71c89e9de443905e446c7b9e58c64010a987ef84 Mon Sep 17 00:00:00 2001
From: Bonerush <2630234655@qq.com>
Date: Thu, 30 Apr 2026 16:15:27 +0800
Subject: [PATCH] fix: theme switching always defaults to dark mode
Root causes:
1. ThemeProvider was imported but never used in App.tsx and showSetupDialog
2. setThemeConfigCallbacks was never called to inject persistence callbacks
3. Preview/save/cancel theme lifecycle had no provider to coordinate
Changes:
- Export setThemeConfigCallbacks from @anthropic/ink
- Wrap App.tsx children with ThemeProvider (initialState from config, onThemeSave persists)
- Wrap showSetupDialog with ThemeProvider for onboarding/trust dialogs
- Call setThemeConfigCallbacks in init.ts to register load/save callbacks
- Update SnapshotUpdateDialog test to account for new ThemeProvider wrapper
Fixes #theme-switching
---
packages/@ant/ink/src/index.ts | 1 +
src/components/App.tsx | 46 ++-
.../__tests__/SnapshotUpdateDialog.test.tsx | 5 +-
src/entrypoints/init.ts | 13 +-
src/interactiveHelpers.tsx | 305 ++++++++----------
5 files changed, 168 insertions(+), 202 deletions(-)
diff --git a/packages/@ant/ink/src/index.ts b/packages/@ant/ink/src/index.ts
index a6300fcb6..0911185bc 100644
--- a/packages/@ant/ink/src/index.ts
+++ b/packages/@ant/ink/src/index.ts
@@ -154,6 +154,7 @@ export { TerminalWriteProvider, useTerminalNotification, type TerminalNotificati
// ============================================================
export {
ThemeProvider,
+ setThemeConfigCallbacks,
usePreviewTheme,
useTheme,
useThemeSetting,
diff --git a/src/components/App.tsx b/src/components/App.tsx
index e88ea7f10..4e6a5f7d8 100644
--- a/src/components/App.tsx
+++ b/src/components/App.tsx
@@ -1,38 +1,36 @@
-import React from 'react'
-import { FpsMetricsProvider } from '../context/fpsMetrics.js'
-import { StatsProvider, type StatsStore } from '../context/stats.js'
-import { type AppState, AppStateProvider } from '../state/AppState.js'
-import { onChangeAppState } from '../state/onChangeAppState.js'
-import type { FpsMetrics } from '../utils/fpsTracker.js'
-import { ThemeProvider } from '@anthropic/ink'
+import React from 'react';
+import { FpsMetricsProvider } from '../context/fpsMetrics.js';
+import { StatsProvider, type StatsStore } from '../context/stats.js';
+import { type AppState, AppStateProvider } from '../state/AppState.js';
+import { onChangeAppState } from '../state/onChangeAppState.js';
+import type { FpsMetrics } from '../utils/fpsTracker.js';
+import { ThemeProvider } from '@anthropic/ink';
+import { getGlobalConfig, saveGlobalConfig } from '../utils/config.js';
type Props = {
- getFpsMetrics: () => FpsMetrics | undefined
- stats?: StatsStore
- initialState: AppState
- children: React.ReactNode
-}
+ getFpsMetrics: () => FpsMetrics | undefined;
+ stats?: StatsStore;
+ initialState: AppState;
+ children: React.ReactNode;
+};
/**
* Top-level wrapper for interactive sessions.
* Provides FPS metrics, stats context, and app state to the component tree.
*/
-export function App({
- getFpsMetrics,
- stats,
- initialState,
- children,
-}: Props): React.ReactNode {
+export function App({ getFpsMetrics, stats, initialState, children }: Props): React.ReactNode {
return (
-
- {children}
+
+ saveGlobalConfig(current => ({ ...current, theme: setting }))}
+ >
+ {children}
+
- )
+ );
}
diff --git a/src/components/agents/__tests__/SnapshotUpdateDialog.test.tsx b/src/components/agents/__tests__/SnapshotUpdateDialog.test.tsx
index b38f947fe..3dfc7bd47 100644
--- a/src/components/agents/__tests__/SnapshotUpdateDialog.test.tsx
+++ b/src/components/agents/__tests__/SnapshotUpdateDialog.test.tsx
@@ -5,7 +5,10 @@ import { buildMergePrompt, SnapshotUpdateDialog } from '../SnapshotUpdateDialog.
import { Select } from '../../CustomSelect/index.js';
function getSnapshotDialogFromRenderedTree(rendered: React.ReactElement) {
- const appStateProvider = rendered as React.ReactElement<{
+ const themeProvider = rendered as React.ReactElement<{
+ children: React.ReactElement;
+ }>;
+ const appStateProvider = themeProvider.props.children as React.ReactElement<{
children: React.ReactElement;
}>;
const keybindingSetup = appStateProvider.props.children as React.ReactElement<{
diff --git a/src/entrypoints/init.ts b/src/entrypoints/init.ts
index c3125b8b7..a610c1c25 100644
--- a/src/entrypoints/init.ts
+++ b/src/entrypoints/init.ts
@@ -20,7 +20,12 @@ import {
import { preconnectAnthropicApi } from '../utils/apiPreconnect.js'
import { applyExtraCACertsFromConfig } from '../utils/caCertsConfig.js'
import { registerCleanup } from '../utils/cleanupRegistry.js'
-import { enableConfigs, recordFirstStartTime } from '../utils/config.js'
+import {
+ enableConfigs,
+ getGlobalConfig,
+ recordFirstStartTime,
+ saveGlobalConfig,
+} from '../utils/config.js'
import { logForDebugging } from '../utils/debug.js'
import { detectCurrentRepository } from '../utils/detectRepository.js'
import { logForDiagnosticsNoPII } from '../utils/diagLogs.js'
@@ -51,6 +56,7 @@ import { setShellIfWindows } from '../utils/windowsPaths.js'
import { initSentry } from '../utils/sentry.js'
import { initUser } from '../utils/user.js'
import { initLangfuse, shutdownLangfuse } from '../services/langfuse/index.js'
+import { setThemeConfigCallbacks } from '@anthropic/ink'
// initialize1PEventLogging is dynamically imported to defer OpenTelemetry sdk-logs/resources
@@ -66,6 +72,11 @@ export const init = memoize(async (): Promise => {
try {
const configsStart = Date.now()
enableConfigs()
+ setThemeConfigCallbacks({
+ loadTheme: () => getGlobalConfig().theme,
+ saveTheme: setting =>
+ saveGlobalConfig(current => ({ ...current, theme: setting })),
+ })
logForDiagnosticsNoPII('info', 'init_configs_enabled', {
duration_ms: Date.now() - configsStart,
})
diff --git a/src/interactiveHelpers.tsx b/src/interactiveHelpers.tsx
index 1e2d6ad44..d7ad64a6a 100644
--- a/src/interactiveHelpers.tsx
+++ b/src/interactiveHelpers.tsx
@@ -1,11 +1,8 @@
-import { feature } from 'bun:bundle'
-import { appendFileSync } from 'fs'
-import React from 'react'
-import { logEvent } from 'src/services/analytics/index.js'
-import {
- gracefulShutdown,
- gracefulShutdownSync,
-} from 'src/utils/gracefulShutdown.js'
+import { feature } from 'bun:bundle';
+import { appendFileSync } from 'fs';
+import React from 'react';
+import { logEvent } from 'src/services/analytics/index.js';
+import { gracefulShutdown, gracefulShutdownSync } from 'src/utils/gracefulShutdown.js';
import {
type ChannelEntry,
getAllowedChannels,
@@ -13,63 +10,59 @@ import {
setHasDevChannels,
setSessionTrustAccepted,
setStatsStore,
-} from './bootstrap/state.js'
-import type { Command } from './commands.js'
-import { createStatsStore, type StatsStore } from './context/stats.js'
-import { getSystemContext } from './context.js'
-import { initializeTelemetryAfterTrust } from './entrypoints/init.js'
-import { isSynchronizedOutputSupported } from '@anthropic/ink'
-import type { RenderOptions, Root, TextProps } from '@anthropic/ink'
-import { KeybindingSetup } from './keybindings/KeybindingProviderSetup.js'
-import { startDeferredPrefetches } from './main.js'
+} from './bootstrap/state.js';
+import type { Command } from './commands.js';
+import { createStatsStore, type StatsStore } from './context/stats.js';
+import { getSystemContext } from './context.js';
+import { initializeTelemetryAfterTrust } from './entrypoints/init.js';
+import { isSynchronizedOutputSupported } from '@anthropic/ink';
+import type { RenderOptions, Root, TextProps } from '@anthropic/ink';
+import { KeybindingSetup } from './keybindings/KeybindingProviderSetup.js';
+import { startDeferredPrefetches } from './main.js';
import {
checkGate_CACHED_OR_BLOCKING,
initializeGrowthBook,
resetGrowthBook,
-} from './services/analytics/growthbook.js'
-import { isQualifiedForGrove } from './services/api/grove.js'
-import { handleMcpjsonServerApprovals } from './services/mcpServerApproval.js'
-import { AppStateProvider } from './state/AppState.js'
-import { onChangeAppState } from './state/onChangeAppState.js'
-import { normalizeApiKeyForConfig } from './utils/authPortable.js'
+} from './services/analytics/growthbook.js';
+import { isQualifiedForGrove } from './services/api/grove.js';
+import { handleMcpjsonServerApprovals } from './services/mcpServerApproval.js';
+import { AppStateProvider } from './state/AppState.js';
+import { onChangeAppState } from './state/onChangeAppState.js';
+import { ThemeProvider } from '@anthropic/ink';
+import { normalizeApiKeyForConfig } from './utils/authPortable.js';
import {
getExternalClaudeMdIncludes,
getMemoryFiles,
shouldShowClaudeMdExternalIncludesWarning,
-} from './utils/claudemd.js'
+} from './utils/claudemd.js';
import {
checkHasTrustDialogAccepted,
getCustomApiKeyStatus,
getGlobalConfig,
saveGlobalConfig,
-} from './utils/config.js'
-import { updateDeepLinkTerminalPreference } from './utils/deepLink/terminalPreference.js'
-import { isEnvTruthy, isRunningOnHomespace } from './utils/envUtils.js'
-import { type FpsMetrics, FpsTracker } from './utils/fpsTracker.js'
-import { updateGithubRepoPathMapping } from './utils/githubRepoPathMapping.js'
-import { applyConfigEnvironmentVariables } from './utils/managedEnv.js'
-import type { PermissionMode } from './utils/permissions/PermissionMode.js'
-import { getBaseRenderOptions } from './utils/renderOptions.js'
-import { getSettingsWithAllErrors } from './utils/settings/allErrors.js'
-import {
- hasSkipDangerousModePermissionPrompt,
-} from './utils/settings/settings.js'
+} from './utils/config.js';
+import { updateDeepLinkTerminalPreference } from './utils/deepLink/terminalPreference.js';
+import { isEnvTruthy, isRunningOnHomespace } from './utils/envUtils.js';
+import { type FpsMetrics, FpsTracker } from './utils/fpsTracker.js';
+import { updateGithubRepoPathMapping } from './utils/githubRepoPathMapping.js';
+import { applyConfigEnvironmentVariables } from './utils/managedEnv.js';
+import type { PermissionMode } from './utils/permissions/PermissionMode.js';
+import { getBaseRenderOptions } from './utils/renderOptions.js';
+import { getSettingsWithAllErrors } from './utils/settings/allErrors.js';
+import { hasSkipDangerousModePermissionPrompt } from './utils/settings/settings.js';
export function completeOnboarding(): void {
saveGlobalConfig(current => ({
...current,
hasCompletedOnboarding: true,
lastOnboardingVersion: MACRO.VERSION,
- }))
+ }));
}
-export function showDialog(
- root: Root,
- renderer: (done: (result: T) => void) => React.ReactNode,
-): Promise {
+export function showDialog(root: Root, renderer: (done: (result: T) => void) => React.ReactNode): Promise {
return new Promise(resolve => {
- const done = (result: T): void => void resolve(result)
- root.render(renderer(done))
- })
+ const done = (result: T): void => void resolve(result);
+ root.render(renderer(done));
+ });
}
/**
@@ -78,12 +71,8 @@ export function showDialog(
* console.error is swallowed by Ink's patchConsole, so we render
* through the React tree instead.
*/
-export async function exitWithError(
- root: Root,
- message: string,
- beforeExit?: () => Promise,
-): Promise {
- return exitWithMessage(root, message, { color: 'error', beforeExit })
+export async function exitWithError(root: Root, message: string, beforeExit?: () => Promise): Promise {
+ return exitWithMessage(root, message, { color: 'error', beforeExit });
}
/**
@@ -96,21 +85,19 @@ export async function exitWithMessage(
root: Root,
message: string,
options?: {
- color?: TextProps['color']
- exitCode?: number
- beforeExit?: () => Promise
+ color?: TextProps['color'];
+ exitCode?: number;
+ beforeExit?: () => Promise;
},
): Promise {
- const { Text } = await import('@anthropic/ink')
- const color = options?.color
- const exitCode = options?.exitCode ?? 1
- root.render(
- color ? {message} : {message},
- )
- root.unmount()
- await options?.beforeExit?.()
+ const { Text } = await import('@anthropic/ink');
+ const color = options?.color;
+ const exitCode = options?.exitCode ?? 1;
+ root.render(color ? {message} : {message});
+ root.unmount();
+ await options?.beforeExit?.();
// eslint-disable-next-line custom-rules/no-process-exit -- exit after Ink unmount
- process.exit(exitCode)
+ process.exit(exitCode);
}
/**
@@ -123,24 +110,26 @@ export function showSetupDialog(
options?: { onChangeAppState?: typeof onChangeAppState },
): Promise {
return showDialog(root, done => (
-
- {renderer(done)}
-
- ))
+ saveGlobalConfig(current => ({ ...current, theme: setting }))}
+ >
+
+ {renderer(done)}
+
+
+ ));
}
/**
* Render the main UI into the root and wait for it to exit.
* Handles the common epilogue: start deferred prefetches, wait for exit, graceful shutdown.
*/
-export async function renderAndRun(
- root: Root,
- element: React.ReactNode,
-): Promise {
- root.render(element)
- startDeferredPrefetches()
- await root.waitUntilExit()
- await gracefulShutdown(0)
+export async function renderAndRun(root: Root, element: React.ReactNode): Promise {
+ root.render(element);
+ startDeferredPrefetches();
+ await root.waitUntilExit();
+ await gracefulShutdown(0);
}
export async function showSetupScreens(
@@ -156,29 +145,29 @@ export async function showSetupScreens(
isEnvTruthy(false) ||
process.env.IS_DEMO // Skip onboarding in demo mode
) {
- return false
+ return false;
}
- const config = getGlobalConfig()
- let onboardingShown = false
+ const config = getGlobalConfig();
+ let onboardingShown = false;
if (
!config.theme ||
!config.hasCompletedOnboarding // always show onboarding at least once
) {
- onboardingShown = true
- const { Onboarding } = await import('./components/Onboarding.js')
+ onboardingShown = true;
+ const { Onboarding } = await import('./components/Onboarding.js');
await showSetupDialog(
root,
done => (
{
- completeOnboarding()
- void done()
+ completeOnboarding();
+ void done();
}}
/>
),
{ onChangeAppState },
- )
+ );
}
// Always show the trust dialog in interactive sessions, regardless of permission mode.
@@ -192,83 +181,71 @@ export async function showSetupScreens(
// If it returns true, the TrustDialog would auto-resolve regardless of
// security features, so we can skip the dynamic import and render cycle.
if (!checkHasTrustDialogAccepted()) {
- const { TrustDialog } = await import(
- './components/TrustDialog/TrustDialog.js'
- )
- await showSetupDialog(root, done => (
-
- ))
+ const { TrustDialog } = await import('./components/TrustDialog/TrustDialog.js');
+ await showSetupDialog(root, done => );
}
// Signal that trust has been verified for this session.
// GrowthBook checks this to decide whether to include auth headers.
- setSessionTrustAccepted(true)
+ setSessionTrustAccepted(true);
// Reset and reinitialize GrowthBook after trust is established.
// Defense for login/logout: clears any prior client so the next init
// picks up fresh auth headers.
- resetGrowthBook()
- void initializeGrowthBook()
+ resetGrowthBook();
+ void initializeGrowthBook();
// Now that trust is established, prefetch system context if it wasn't already
- void getSystemContext()
+ void getSystemContext();
// If settings are valid, check for any mcp.json servers that need approval
- const { errors: allErrors } = getSettingsWithAllErrors()
+ const { errors: allErrors } = getSettingsWithAllErrors();
if (allErrors.length === 0) {
- await handleMcpjsonServerApprovals(root)
+ await handleMcpjsonServerApprovals(root);
}
// Check for claude.md includes that need approval
if (await shouldShowClaudeMdExternalIncludesWarning()) {
- const externalIncludes = getExternalClaudeMdIncludes(
- await getMemoryFiles(true),
- )
- const { ClaudeMdExternalIncludesDialog } = await import(
- './components/ClaudeMdExternalIncludesDialog.js'
- )
+ const externalIncludes = getExternalClaudeMdIncludes(await getMemoryFiles(true));
+ const { ClaudeMdExternalIncludesDialog } = await import('./components/ClaudeMdExternalIncludesDialog.js');
await showSetupDialog(root, done => (
-
- ))
+
+ ));
}
}
// Track current repo path for teleport directory switching (fire-and-forget)
// This must happen AFTER trust to prevent untrusted directories from poisoning the mapping
- void updateGithubRepoPathMapping()
+ void updateGithubRepoPathMapping();
if (feature('LODESTONE')) {
- updateDeepLinkTerminalPreference()
+ updateDeepLinkTerminalPreference();
}
// Apply full environment variables after trust dialog is accepted OR in bypass mode
// In bypass mode (CI/CD, automation), we trust the environment so apply all variables
// In normal mode, this happens after the trust dialog is accepted
// This includes potentially dangerous environment variables from untrusted sources
- applyConfigEnvironmentVariables()
+ applyConfigEnvironmentVariables();
// Initialize telemetry after env vars are applied so OTEL endpoint env vars and
// otelHeadersHelper (which requires trust to execute) are available.
// Defer to next tick so the OTel dynamic import resolves after first render
// instead of during the pre-render microtask queue.
- setImmediate(() => initializeTelemetryAfterTrust())
+ setImmediate(() => initializeTelemetryAfterTrust());
if (await isQualifiedForGrove()) {
- const { GroveDialog } = await import('src/components/grove/Grove.js')
+ const { GroveDialog } = await import('src/components/grove/Grove.js');
const decision = await showSetupDialog(root, done => (
- ))
+ ));
if (decision === 'escape') {
- logEvent('tengu_grove_policy_exited', {})
- gracefulShutdownSync(0)
- return false
+ logEvent('tengu_grove_policy_exited', {});
+ gracefulShutdownSync(0);
+ return false;
}
}
@@ -276,36 +253,24 @@ export async function showSetupScreens(
// On homespace, ANTHROPIC_API_KEY is preserved in process.env for child
// processes but ignored by Claude Code itself (see auth.ts).
if (process.env.ANTHROPIC_API_KEY && !isRunningOnHomespace()) {
- const customApiKeyTruncated = normalizeApiKeyForConfig(
- process.env.ANTHROPIC_API_KEY,
- )
- const keyStatus = getCustomApiKeyStatus(customApiKeyTruncated)
+ const customApiKeyTruncated = normalizeApiKeyForConfig(process.env.ANTHROPIC_API_KEY);
+ const keyStatus = getCustomApiKeyStatus(customApiKeyTruncated);
if (keyStatus === 'new') {
- const { ApproveApiKey } = await import('./components/ApproveApiKey.js')
+ const { ApproveApiKey } = await import('./components/ApproveApiKey.js');
await showSetupDialog(
root,
- done => (
-
- ),
+ done => ,
{ onChangeAppState },
- )
+ );
}
}
if (
- (permissionMode === 'bypassPermissions' ||
- allowDangerouslySkipPermissions) &&
+ (permissionMode === 'bypassPermissions' || allowDangerouslySkipPermissions) &&
!hasSkipDangerousModePermissionPrompt()
) {
- const { BypassPermissionsModeDialog } = await import(
- './components/BypassPermissionsModeDialog.js'
- )
- await showSetupDialog(root, done => (
-
- ))
+ const { BypassPermissionsModeDialog } = await import('./components/BypassPermissionsModeDialog.js');
+ await showSetupDialog(root, done => );
}
// --dangerously-load-development-channels confirmation. On accept, append
@@ -313,72 +278,60 @@ export async function showSetupScreens(
// is NOT bypassed — gateChannelServer() still runs; this flag only exists
// to sidestep the --channels approved-server allowlist.
if (devChannels && devChannels.length > 0) {
- const { DevChannelsDialog } = await import(
- './components/DevChannelsDialog.js'
- )
+ const { DevChannelsDialog } = await import('./components/DevChannelsDialog.js');
await showSetupDialog(root, done => (
{
// Mark dev entries per-entry so the allowlist bypass doesn't leak
// to --channels entries when both flags are passed.
- setAllowedChannels([
- ...getAllowedChannels(),
- ...devChannels.map(c => ({ ...c, dev: true })),
- ])
- setHasDevChannels(true)
- void done()
+ setAllowedChannels([...getAllowedChannels(), ...devChannels.map(c => ({ ...c, dev: true }))]);
+ setHasDevChannels(true);
+ void done();
}}
/>
- ))
+ ));
}
// Show Chrome onboarding for first-time Claude in Chrome users
- if (
- claudeInChrome &&
- !getGlobalConfig().hasCompletedClaudeInChromeOnboarding
- ) {
- const { ClaudeInChromeOnboarding } = await import(
- './components/ClaudeInChromeOnboarding.js'
- )
- await showSetupDialog(root, done => (
-
- ))
+ if (claudeInChrome && !getGlobalConfig().hasCompletedClaudeInChromeOnboarding) {
+ const { ClaudeInChromeOnboarding } = await import('./components/ClaudeInChromeOnboarding.js');
+ await showSetupDialog(root, done => );
}
- return onboardingShown
+ return onboardingShown;
}
export function getRenderContext(exitOnCtrlC: boolean): {
- renderOptions: RenderOptions
- getFpsMetrics: () => FpsMetrics | undefined
- stats: StatsStore
+ renderOptions: RenderOptions;
+ getFpsMetrics: () => FpsMetrics | undefined;
+ stats: StatsStore;
} {
- let lastFlickerTime = 0
- const baseOptions = getBaseRenderOptions(exitOnCtrlC)
+ let lastFlickerTime = 0;
+ const baseOptions = getBaseRenderOptions(exitOnCtrlC);
// Log analytics event when stdin override is active
if (baseOptions.stdin) {
- logEvent('tengu_stdin_interactive', {})
+ logEvent('tengu_stdin_interactive', {});
}
- const fpsTracker = new FpsTracker()
- const stats = createStatsStore()
- setStatsStore(stats)
+ const fpsTracker = new FpsTracker();
+ const stats = createStatsStore();
+ setStatsStore(stats);
// Bench mode: when set, append per-frame phase timings as JSONL for
// offline analysis by bench/repl-scroll.ts. Captures the full TUI
// render pipeline (yoga → screen buffer → diff → optimize → stdout)
// so perf work on any phase can be validated against real user flows.
- const frameTimingLogPath = process.env.CLAUDE_CODE_FRAME_TIMING_LOG
+ const frameTimingLogPath = process.env.CLAUDE_CODE_FRAME_TIMING_LOG;
return {
getFpsMetrics: () => fpsTracker.getMetrics(),
stats,
renderOptions: {
...baseOptions,
onFrame: event => {
- fpsTracker.record(event.durationMs)
- stats.observe('frame_duration_ms', event.durationMs)
+ fpsTracker.record(event.durationMs);
+ stats.observe('frame_duration_ms', event.durationMs);
if (frameTimingLogPath && event.phases) {
// Bench-only env-var-gated path: sync write so no frames dropped
// on abrupt exit. ~100 bytes at ≤60fps is negligible. rss/cpu are
@@ -390,30 +343,30 @@ export function getRenderContext(exitOnCtrlC: boolean): {
...event.phases,
rss: process.memoryUsage.rss(),
cpu: process.cpuUsage(),
- }) + '\n'
+ }) + '\n';
// eslint-disable-next-line custom-rules/no-sync-fs -- bench-only, sync so no frames dropped on exit
- appendFileSync(frameTimingLogPath, line)
+ appendFileSync(frameTimingLogPath, line);
}
// Skip flicker reporting for terminals with synchronized output —
// DEC 2026 buffers between BSU/ESU so clear+redraw is atomic.
if (isSynchronizedOutputSupported()) {
- return
+ return;
}
for (const flicker of event.flickers) {
if (flicker.reason === 'resize') {
- continue
+ continue;
}
- const now = Date.now()
+ const now = Date.now();
if (now - lastFlickerTime < 1000) {
logEvent('tengu_flicker', {
desiredHeight: flicker.desiredHeight,
actualHeight: flicker.availableHeight,
reason: flicker.reason,
- } as unknown as Record)
+ } as unknown as Record);
}
- lastFlickerTime = now
+ lastFlickerTime = now;
}
},
},
- }
+ };
}