fix(app): preserve persisted theme styles after startup

Module-level style composites could materialize the temporary adaptive theme before persisted settings loaded. Keep Unistyles reads in render and guard against eager module-scope access.
This commit is contained in:
Mohamed Boudra
2026-07-18 17:45:02 +02:00
parent c0622a7046
commit e0e50c9a8e
31 changed files with 188 additions and 133 deletions
+5 -7
View File
@@ -210,8 +210,6 @@ function selectedSidebarItemStyle({ hovered }: PressableStateCallbackType & { ho
];
}
const ROW_WITH_BORDER_STYLE = [settingsStyles.row, settingsStyles.rowBorder];
function getSendBehaviorOptions(t: TFunction) {
return [
{ value: "interrupt" as const, label: t("settings.general.defaultSend.options.interrupt") },
@@ -360,7 +358,7 @@ function GeneralSection({
options={sendBehaviorOptions}
/>
</View>
<View style={ROW_WITH_BORDER_STYLE}>
<View style={[settingsStyles.row, settingsStyles.rowBorder]}>
<View style={settingsStyles.rowContent}>
<Text style={settingsStyles.rowTitle}>{t("settings.general.language.label")}</Text>
<Text style={settingsStyles.rowHint}>{t("settings.general.language.description")}</Text>
@@ -387,7 +385,7 @@ function GeneralSection({
</DropdownMenu>
</View>
{isDesktopApp ? (
<View style={ROW_WITH_BORDER_STYLE}>
<View style={[settingsStyles.row, settingsStyles.rowBorder]}>
<View style={settingsStyles.rowContent}>
<Text style={settingsStyles.rowTitle}>{t("settings.general.serviceUrls.label")}</Text>
<Text style={settingsStyles.rowHint}>
@@ -414,7 +412,7 @@ function GeneralSection({
</DropdownMenu>
</View>
) : null}
<View style={ROW_WITH_BORDER_STYLE}>
<View style={[settingsStyles.row, settingsStyles.rowBorder]}>
<View style={settingsStyles.rowContent}>
<Text style={settingsStyles.rowTitle}>
{t("settings.general.terminalScrollback.label")}
@@ -700,7 +698,7 @@ function DesktopAppUpdateRow() {
return (
<>
<View style={ROW_WITH_BORDER_STYLE}>
<View style={[settingsStyles.row, settingsStyles.rowBorder]}>
<View style={settingsStyles.rowContent}>
<Text style={settingsStyles.rowTitle}>{t("settings.about.releaseChannel.label")}</Text>
<Text style={settingsStyles.rowHint}>
@@ -714,7 +712,7 @@ function DesktopAppUpdateRow() {
options={releaseChannelOptions}
/>
</View>
<View style={ROW_WITH_BORDER_STYLE}>
<View style={[settingsStyles.row, settingsStyles.rowBorder]}>
<View style={settingsStyles.rowContent}>
<Text style={settingsStyles.rowTitle}>{t("settings.about.updates.label")}</Text>
<Text style={settingsStyles.rowHint}>{statusText}</Text>
@@ -212,7 +212,6 @@ function AutoExpandReasoningRow({ value, onChange }: AutoExpandReasoningRowProps
);
}
const TOOL_CALL_DETAIL_ROW_STYLE = [settingsStyles.row, settingsStyles.rowBorder];
const TOOL_CALL_DETAIL_LEVELS: readonly AppSettings["toolCallDetailLevel"][] = [
"detailed",
"overview",
@@ -250,7 +249,7 @@ function ToolCallDetailRow({ value, onChange }: ToolCallDetailRowProps) {
const { t } = useTranslation();
const selectedLabel = getToolCallDetailLevelLabel(t, value);
return (
<View style={TOOL_CALL_DETAIL_ROW_STYLE}>
<View style={[settingsStyles.row, settingsStyles.rowBorder]}>
<View style={settingsStyles.rowContent}>
<Text style={settingsStyles.rowTitle}>{t("settings.general.toolCallDetail.label")}</Text>
<Text style={settingsStyles.rowHint}>
@@ -150,7 +150,7 @@ function HostNotFound() {
const { t } = useTranslation();
return (
<View>
<View style={EMPTY_CARD_STYLE}>
<View style={[settingsStyles.card, styles.emptyCard]}>
<Text style={styles.emptyText}>{t("settings.host.notFound")}</Text>
</View>
</View>
@@ -276,7 +276,7 @@ export function HostAgentsPage({ serverId }: { serverId: string }) {
<AppendSystemPromptCard serverId={serverId} />
</SettingsSection>
) : (
<View style={EMPTY_CARD_STYLE}>
<View style={[settingsStyles.card, styles.emptyCard]}>
<Text style={styles.emptyText}>{t("settings.host.agents.unavailable")}</Text>
</View>
)}
@@ -300,7 +300,7 @@ export function HostWorkspacesPage({ serverId }: { serverId: string }) {
<AutoArchiveMergedWorkspacesCard serverId={serverId} />
</SettingsSection>
) : (
<View style={EMPTY_CARD_STYLE}>
<View style={[settingsStyles.card, styles.emptyCard]}>
<Text style={styles.emptyText}>{t("settings.host.workspaces.unavailable")}</Text>
</View>
)}
@@ -1940,4 +1940,3 @@ const styles = StyleSheet.create((theme) => ({
}));
const FLEX_1_STYLE = { flex: 1 };
const EMPTY_CARD_STYLE = [settingsStyles.card, styles.emptyCard];
@@ -264,7 +264,7 @@ export function KeyboardShortcutsSection() {
if (isNative) {
return (
<SettingsSection title={t("settings.sections.shortcuts")}>
<View style={mobileCardStyle}>
<View style={[settingsStyles.card, styles.mobileCard]}>
<Text style={styles.mobileText}>{t("settings.shortcuts.unavailableOnMobile")}</Text>
</View>
</SettingsSection>
@@ -362,5 +362,3 @@ const styles = StyleSheet.create((theme) => ({
color: theme.colors.foregroundMuted,
},
}));
const mobileCardStyle = [settingsStyles.card, styles.mobileCard];
@@ -410,12 +410,12 @@ export function ProvidersSection({ serverId }: ProvidersSectionProps) {
style={styles.sectionSpacing}
>
{!hasServer || !isConnected ? (
<View style={EMPTY_CARD_STYLE}>
<View style={[settingsStyles.card, styles.emptyCard]}>
<Text style={styles.emptyText}>{t("settings.providers.unavailable")}</Text>
</View>
) : null}
{hasServer && isConnected && isLoading ? (
<View style={EMPTY_CARD_STYLE}>
<View style={[settingsStyles.card, styles.emptyCard]}>
<Text style={styles.emptyText}>{t("settings.providers.loading")}</Text>
</View>
) : null}
@@ -543,5 +543,3 @@ const styles = StyleSheet.create((theme) => ({
backgroundColor: theme.colors.surface3,
},
}));
const EMPTY_CARD_STYLE = [settingsStyles.card, styles.emptyCard];
@@ -1165,7 +1165,9 @@ function ResolvedDesktopTabChip({
return (
<View style={styles.tabSlot}>
{showDropIndicatorBefore ? <View style={TAB_DROP_INDICATOR_BEFORE_STYLE} /> : null}
{showDropIndicatorBefore ? (
<View style={[styles.tabDropIndicator, styles.tabDropIndicatorBefore]} />
) : null}
<TabChip
tab={item.tab}
isActive={item.isActive}
@@ -1184,7 +1186,9 @@ function ResolvedDesktopTabChip({
onCloseTab={onCloseTab}
dragHandleProps={dragHandleProps}
/>
{showDropIndicatorAfter ? <View style={TAB_DROP_INDICATOR_AFTER_STYLE} /> : null}
{showDropIndicatorAfter ? (
<View style={[styles.tabDropIndicator, styles.tabDropIndicatorAfter]} />
) : null}
</View>
);
}}
@@ -1375,6 +1379,3 @@ const styles = StyleSheet.create((theme) => ({
height: 14,
},
}));
const TAB_DROP_INDICATOR_BEFORE_STYLE = [styles.tabDropIndicator, styles.tabDropIndicatorBefore];
const TAB_DROP_INDICATOR_AFTER_STYLE = [styles.tabDropIndicator, styles.tabDropIndicatorAfter];
@@ -3341,7 +3341,7 @@ function WorkspaceScreenContent({
[t],
);
const containerStyle = containerWithWorkspaceBackgroundStyle;
const containerStyle = [styles.container, styles.containerWorkspaceBackground];
const menuNewAgentIcon = MENU_NEW_AGENT_ICON;
const menuNewTerminalIcon = MENU_NEW_TERMINAL_ICON;
@@ -4090,9 +4090,4 @@ const styles = StyleSheet.create((theme) => ({
},
}));
const containerWithWorkspaceBackgroundStyle = [
styles.container,
styles.containerWorkspaceBackground,
];
const EXPLORER_TOGGLE_KEYS: ShortcutKey[] = ["mod", "E"];
@@ -88,7 +88,9 @@ function ScriptActionButtonChildren({
) : (
<ThemedPlay size={10} uniProps={colorMapping} {...playFillTransparent} />
);
const labelStyle = hovered ? actionButtonLabelHoveredStyle : styles.actionButtonLabel;
const labelStyle = hovered
? [styles.actionButtonLabel, styles.actionButtonLabelHovered]
: styles.actionButtonLabel;
return (
<>
{iconElement}
@@ -156,7 +158,7 @@ function HostLinkChildren({ hovered, disabled, label }: HostLinkChildrenProps):
const showIcon = !disabled && (hovered || isNative);
const isActive = Boolean(hovered) && !disabled;
const colorMapping = isActive ? foregroundColorMapping : mutedColorMapping;
const hostLabelStyle = isActive ? hostLabelActiveStyle : styles.hostLabel;
const hostLabelStyle = isActive ? [styles.hostLabel, styles.hostLabelActive] : styles.hostLabel;
return (
<>
<Text style={hostLabelStyle} numberOfLines={1}>
@@ -207,7 +209,8 @@ function HostLinkRow({ label, url, scriptName, onOpenInBrowserTab }: HostLinkPro
function ExitCodeBadge({ code }: { code: number }): ReactElement {
const { t } = useTranslation();
const exitTextStyle = code === 0 ? styles.exitBadgeText : exitBadgeTextErrorStyle;
const exitTextStyle =
code === 0 ? styles.exitBadgeText : [styles.exitBadgeText, styles.exitBadgeTextError];
return (
<View style={styles.exitBadge}>
<Text style={exitTextStyle}>{t("workspace.scripts.states.exitCode", { code })}</Text>
@@ -305,7 +308,7 @@ function ScriptRow({
}, [onStartScript, script.scriptName]);
const scriptNameStyle = useMemo(
() => (isRunning ? scriptNameActiveStyle : styles.scriptName),
() => (isRunning ? [styles.scriptName, styles.scriptNameActive] : styles.scriptName),
[isRunning],
);
@@ -631,8 +634,3 @@ const styles = StyleSheet.create((theme) => ({
color: theme.colors.foreground,
},
}));
const actionButtonLabelHoveredStyle = [styles.actionButtonLabel, styles.actionButtonLabelHovered];
const hostLabelActiveStyle = [styles.hostLabel, styles.hostLabelActive];
const scriptNameActiveStyle = [styles.scriptName, styles.scriptNameActive];
const exitBadgeTextErrorStyle = [styles.exitBadgeText, styles.exitBadgeTextError];