diff --git a/.oxlintrc.json b/.oxlintrc.json
index 9e389e515..bdcab1dc5 100644
--- a/.oxlintrc.json
+++ b/.oxlintrc.json
@@ -87,6 +87,9 @@
{
"files": ["packages/app/src/**/*.{ts,tsx}"],
"rules": {
+ // React Native style arrays must read Unistyles proxies during render. Hoisting them to
+ // satisfy this allocation rule captures the temporary startup theme instead.
+ "react-perf/jsx-no-new-array-as-prop": "off",
"no-restricted-imports": [
"error",
{
diff --git a/docs/unistyles.md b/docs/unistyles.md
index 255d74d44..619ceed3b 100644
--- a/docs/unistyles.md
+++ b/docs/unistyles.md
@@ -58,6 +58,30 @@ For standard React Native components, the [Unistyles Babel plugin](https://www.u
The important detail: the automatic native path tracks `props.style`. It does not generally track every prop that happens to carry style-like values.
+### Do Not Materialize Styles At Module Scope
+
+Never read a Unistyles style property into a module-level constant. This includes cached arrays:
+
+```tsx
+// Wrong: evaluated while the app may still be using the temporary system theme.
+const ROW_STYLE = [settingsStyles.row, settingsStyles.rowBorder];
+
+// Right: each style proxy is read when this view renders.
+;
+```
+
+Paseo starts with adaptive themes, then applies the persisted theme after async settings load. A
+module-level read can therefore materialize the light style before a persisted dark theme is
+active. If the view mounts after that theme change, React Native receives the stale light object;
+Unistyles registers the node for future changes but does not retroactively replace its initial
+props. Settings dividers once rendered light `#e4e4e7` inside a dark `#252B2A` card for exactly
+this reason.
+
+Render-time array syntax is intentional and exempt from the app's JSX array-allocation lint rule.
+Keep the entries separate so each retains its Unistyles metadata. If composition is needed outside
+JSX, create the array inside the component or in a `useMemo` that first runs when the component
+mounts—never at module evaluation time.
+
[`useUnistyles()`](https://www.unistyl.es/v3/references/use-unistyles) is different. It gives React access to the current theme/runtime and can make a component re-render when those values change. Use it for values that must be rendered through React props, such as icon colors or small escape hatches. Do not expect direct reads from `UnistylesRuntime` to re-render a component; [issue #817](https://github.com/jpudysz/react-native-unistyles/issues/817) is a useful reminder of that invariant.
## Dynamic Pixel Styles On Web
diff --git a/packages/app/src/agent-stream/view.tsx b/packages/app/src/agent-stream/view.tsx
index a1354787f..c67092029 100644
--- a/packages/app/src/agent-stream/view.tsx
+++ b/packages/app/src/agent-stream/view.tsx
@@ -1234,7 +1234,9 @@ function PermissionActionButton({
onPress,
}: PermissionActionButtonProps) {
const handlePress = useCallback(() => onPress(action), [onPress, action]);
- const optionTextStyle = isPrimary ? optionTextPrimaryStyle : permissionStyles.optionText;
+ const optionTextStyle = isPrimary
+ ? [permissionStyles.optionText, permissionStyles.optionTextPrimary]
+ : permissionStyles.optionText;
const colorMapping = isPrimary ? primaryColorMapping : mutedColorMapping;
return (
@@ -1622,8 +1624,6 @@ const permissionStyles = StyleSheet.create((theme) => ({
},
}));
-const optionTextPrimaryStyle = [permissionStyles.optionText, permissionStyles.optionTextPrimary];
-
interface StreamItemWrapperProps {
gapBelow: number;
children: ReactNode;
diff --git a/packages/app/src/app/pair-scan.tsx b/packages/app/src/app/pair-scan.tsx
index c73dc6d93..4408ef33c 100644
--- a/packages/app/src/app/pair-scan.tsx
+++ b/packages/app/src/app/pair-scan.tsx
@@ -257,10 +257,10 @@ export default function PairScanScreen() {
/>
-
-
-
-
+
+
+
+
{isPairing ? {t("pairing.scan.pairing")} : null}
@@ -272,7 +272,3 @@ export default function PairScanScreen() {
}
const BARCODE_SCANNER_SETTINGS: BarcodeSettings = { barcodeTypes: ["qr"] };
-const CORNER_TL_STYLE = [styles.corner, styles.cornerTL];
-const CORNER_TR_STYLE = [styles.corner, styles.cornerTR];
-const CORNER_BL_STYLE = [styles.corner, styles.cornerBL];
-const CORNER_BR_STYLE = [styles.corner, styles.cornerBR];
diff --git a/packages/app/src/components/adaptive-modal-sheet.tsx b/packages/app/src/components/adaptive-modal-sheet.tsx
index 68814e579..9b91a375e 100644
--- a/packages/app/src/components/adaptive-modal-sheet.tsx
+++ b/packages/app/src/components/adaptive-modal-sheet.tsx
@@ -230,7 +230,6 @@ const styles = StyleSheet.create((theme) => ({
},
}));
-const SEARCH_INPUT_STYLE = [styles.searchInput, isWeb && { outlineStyle: "none" }];
const WEB_EXIT_DURATION_MS = 160;
function SheetBackground({ style }: BottomSheetBackgroundProps) {
@@ -373,7 +372,7 @@ export function SheetHeaderView({
@@ -552,7 +552,7 @@ export function AgentList({
@@ -789,6 +789,3 @@ const styles = StyleSheet.create((theme) => ({
fontSize: theme.fontSize.base,
},
}));
-
-const SHEET_CANCEL_BUTTON_STYLE = [styles.sheetButton, styles.sheetCancelButton];
-const SHEET_ARCHIVE_BUTTON_STYLE = [styles.sheetButton, styles.sheetArchiveButton];
diff --git a/packages/app/src/components/dictation-controls.tsx b/packages/app/src/components/dictation-controls.tsx
index eb2b14a25..477717d4e 100644
--- a/packages/app/src/components/dictation-controls.tsx
+++ b/packages/app/src/components/dictation-controls.tsx
@@ -105,7 +105,7 @@ export function DictationControls({
@@ -115,14 +115,14 @@ export function DictationControls({
@@ -240,7 +240,7 @@ export function DictationOverlay({
onPress={onAccept}
accessibilityRole="button"
accessibilityLabel={t("message.dictation.insert")}
- style={OVERLAY_ACCEPT_BUTTON_STYLE}
+ style={[overlayStyles.actionButton, OVERLAY_ACCEPT_BUTTON_BG]}
>
({
},
}));
-const ACTION_CONFIRM_STYLE = [styles.actionButton, styles.actionButtonConfirm];
-const ACTION_SECONDARY_STYLE = [styles.actionButton, styles.actionButtonSecondary];
const OVERLAY_ACCEPT_BUTTON_BG = { backgroundColor: "rgba(255, 255, 255, 0.25)" };
-const OVERLAY_ACCEPT_BUTTON_STYLE = [overlayStyles.actionButton, OVERLAY_ACCEPT_BUTTON_BG];
diff --git a/packages/app/src/components/explorer-sidebar.tsx b/packages/app/src/components/explorer-sidebar.tsx
index 8b6fa0e49..2d7209ae9 100644
--- a/packages/app/src/components/explorer-sidebar.tsx
+++ b/packages/app/src/components/explorer-sidebar.tsx
@@ -211,7 +211,7 @@ export function ExplorerSidebar({
return (
-
+
({
minHeight: 0,
},
}));
-
-const DESKTOP_SIDEBAR_BORDER_STYLE = [styles.desktopSidebarBorder, { flex: 1 }];
diff --git a/packages/app/src/components/file-explorer-pane.tsx b/packages/app/src/components/file-explorer-pane.tsx
index 6707b417c..2a6f42464 100644
--- a/packages/app/src/components/file-explorer-pane.tsx
+++ b/packages/app/src/components/file-explorer-pane.tsx
@@ -575,7 +575,7 @@ function FileExplorerPaneContent(props: FileExplorerPaneContentProps) {
}
return (
-
+
{currentSortLabel}
@@ -1253,5 +1253,3 @@ const styles = StyleSheet.create((theme) => ({
padding: theme.spacing[4],
},
}));
-
-const TREE_PANE_CONTAINER_STYLE = [styles.treePane, styles.treePaneFill];
diff --git a/packages/app/src/components/provider-catalog-list.tsx b/packages/app/src/components/provider-catalog-list.tsx
index f3e0835d0..a31e45be0 100644
--- a/packages/app/src/components/provider-catalog-list.tsx
+++ b/packages/app/src/components/provider-catalog-list.tsx
@@ -155,7 +155,7 @@ export function ProviderCatalogList({
accessibilityLabel={t("providerCatalog.search")}
placeholder={t("providerCatalog.search")}
// @ts-expect-error - outlineStyle is web-only
- style={SEARCH_INPUT_STYLE}
+ style={[styles.searchInput, isWeb && { outlineStyle: "none" }]}
autoCapitalize="none"
autoCorrect={false}
/>
@@ -284,5 +284,3 @@ const styles = StyleSheet.create((theme) => ({
fontSize: theme.fontSize.sm,
},
}));
-
-const SEARCH_INPUT_STYLE = [styles.searchInput, isWeb && { outlineStyle: "none" }];
diff --git a/packages/app/src/components/provider-diagnostic-sheet.tsx b/packages/app/src/components/provider-diagnostic-sheet.tsx
index 9085801e0..b4831acac 100644
--- a/packages/app/src/components/provider-diagnostic-sheet.tsx
+++ b/packages/app/src/components/provider-diagnostic-sheet.tsx
@@ -225,7 +225,7 @@ function AddCustomModelSubSheet({
autoCorrect={false}
returnKeyType="done"
// @ts-expect-error - outlineStyle is web-only
- style={FORM_INPUT_STYLE}
+ style={[sheetStyles.formInput, isWeb && { outlineStyle: "none" }]}
/>
{error ? {error} : null}
@@ -437,7 +437,9 @@ function renderProviderSheetFooter({
const contentStyle = isCompact ? sheetStyles.compactFooterContent : sheetStyles.footerContent;
const actionsStyle = isCompact ? sheetStyles.compactFooterActions : sheetStyles.footerActions;
const buttonStyle = isCompact ? sheetStyles.compactFooterButton : null;
- const metaStyle = isCompact ? COMPACT_FOOTER_META_STYLE : sheetStyles.footerMeta;
+ const metaStyle = isCompact
+ ? [sheetStyles.footerMeta, sheetStyles.compactFooterMeta]
+ : sheetStyles.footerMeta;
return (
@@ -871,9 +873,6 @@ const sheetStyles = StyleSheet.create((theme) => ({
},
}));
-const FORM_INPUT_STYLE = [sheetStyles.formInput, isWeb && { outlineStyle: "none" }];
-const COMPACT_FOOTER_META_STYLE = [sheetStyles.footerMeta, sheetStyles.compactFooterMeta];
-
const MAIN_SNAP_POINTS = ["65%", "92%"];
const ADD_SNAP_POINTS = ["40%"];
const DIAGNOSTIC_SNAP_POINTS = ["50%", "85%"];
diff --git a/packages/app/src/components/sidebar-resize-handle.tsx b/packages/app/src/components/sidebar-resize-handle.tsx
index 98f4e918b..617d9076b 100644
--- a/packages/app/src/components/sidebar-resize-handle.tsx
+++ b/packages/app/src/components/sidebar-resize-handle.tsx
@@ -21,7 +21,10 @@ const webResizeCursorStyle = isWeb
export function SidebarResizeHandle({ edge, gesture, testID }: SidebarResizeHandleProps) {
const [highlighted, setHighlighted] = useState(false);
const highlightTimerRef = useRef | null>(null);
- const hitAreaStyle = edge === "left" ? LEFT_HIT_AREA_STYLE : RIGHT_HIT_AREA_STYLE;
+ const hitAreaStyle =
+ edge === "left"
+ ? [styles.hitArea, styles.leftEdge, webResizeCursorStyle]
+ : [styles.hitArea, styles.rightEdge, webResizeCursorStyle];
const cancelHighlightTimer = useCallback(() => {
if (highlightTimerRef.current === null) return;
@@ -84,6 +87,3 @@ const styles = StyleSheet.create((theme) => ({
opacity: 0.25,
},
}));
-
-const LEFT_HIT_AREA_STYLE = [styles.hitArea, styles.leftEdge, webResizeCursorStyle];
-const RIGHT_HIT_AREA_STYLE = [styles.hitArea, styles.rightEdge, webResizeCursorStyle];
diff --git a/packages/app/src/components/sidebar-workspace-list.tsx b/packages/app/src/components/sidebar-workspace-list.tsx
index ffef8dfd0..44e545ba1 100644
--- a/packages/app/src/components/sidebar-workspace-list.tsx
+++ b/packages/app/src/components/sidebar-workspace-list.tsx
@@ -315,7 +315,9 @@ export function PrBadge({ hint }: { hint: PrHint }) {
const handleHoverIn = useCallback(() => setIsHovered(true), []);
const handleHoverOut = useCallback(() => setIsHovered(false), []);
- const textStyle = isHovered ? prBadgeTextHoveredCombined : prBadgeStyles.text;
+ const textStyle = isHovered
+ ? [prBadgeStyles.text, prBadgeStyles.textHovered]
+ : prBadgeStyles.text;
const iconUniProps = isHovered ? foregroundColorMapping : getPrIconUniMapping(hint.state);
const presentation = getForgePresentation(normalizeForge(hint.forge));
@@ -395,8 +397,6 @@ const prBadgeStyles = StyleSheet.create((theme) => ({
},
}));
-const prBadgeTextHoveredCombined = [prBadgeStyles.text, prBadgeStyles.textHovered];
-
function StatusDotOverlay({
dotColorStyle,
size,
diff --git a/packages/app/src/components/tool-call-details.tsx b/packages/app/src/components/tool-call-details.tsx
index 9025f6a23..72ea48cef 100644
--- a/packages/app/src/components/tool-call-details.tsx
+++ b/packages/app/src/components/tool-call-details.tsx
@@ -711,7 +711,7 @@ function ErrorSection({ errorText, ds }: { errorText: string; ds: DetailStyles }
const { t } = useTranslation();
return (
- {t("toolCallDetails.error")}
+ {t("toolCallDetails.error")}
-
+
{errorText}
@@ -948,6 +952,3 @@ const styles = StyleSheet.create((theme) => {
},
};
});
-
-const SECTION_TITLE_ERROR_STYLE = [styles.sectionTitle, styles.errorText];
-const SCROLL_TEXT_ERROR_STYLE = [styles.scrollText, styles.errorText];
diff --git a/packages/app/src/components/tree-primitives.tsx b/packages/app/src/components/tree-primitives.tsx
index 26c71f2c2..11949c140 100644
--- a/packages/app/src/components/tree-primitives.tsx
+++ b/packages/app/src/components/tree-primitives.tsx
@@ -51,7 +51,7 @@ export function TreeIndentGuides({ depth }: { depth: number }) {
/** Rotating disclosure chevron for a directory row (points right; rotates down when expanded). */
export function TreeChevron({ expanded }: { expanded: boolean }) {
return (
-
+
);
@@ -76,7 +76,3 @@ const styles = StyleSheet.create((theme: Theme) => ({
transform: [{ rotate: "90deg" }],
},
}));
-
-// Stable module-level style ref so TreeChevron passes a constant array, not one created
-// per render — satisfies react-perf (no inline-array prop) without a per-render useMemo.
-const CHEVRON_EXPANDED_STYLE = [styles.chevron, styles.chevronExpanded];
diff --git a/packages/app/src/components/ui/combobox.tsx b/packages/app/src/components/ui/combobox.tsx
index 279954404..81c8f0ef0 100644
--- a/packages/app/src/components/ui/combobox.tsx
+++ b/packages/app/src/components/ui/combobox.tsx
@@ -199,7 +199,7 @@ export function SearchInput({
({
justifyContent: "flex-end",
},
}));
-
-const SEARCH_INPUT_STYLE = [styles.searchInput, IS_WEB && { outlineStyle: "none" }];
diff --git a/packages/app/src/components/workspace-hover-card.tsx b/packages/app/src/components/workspace-hover-card.tsx
index ca2930c25..37cd114fc 100644
--- a/packages/app/src/components/workspace-hover-card.tsx
+++ b/packages/app/src/components/workspace-hover-card.tsx
@@ -418,7 +418,8 @@ function CopyableInfoRow({
if (copied || isHovered) {
iconUniProps = foregroundColorMapping;
}
- const textStyle = copied || isHovered ? cardInfoTextHoveredCombined : styles.cardInfoText;
+ const textStyle =
+ copied || isHovered ? [styles.cardInfoText, styles.cardInfoTextHovered] : styles.cardInfoText;
return (
({
color: theme.colors.statusSuccess,
},
}));
-
-const checksSummaryLabelHoveredCombined = [
- styles.checksSummaryLabel,
- styles.checksSummaryLabelHovered,
-];
-
-const cardInfoTextHoveredCombined = [styles.cardInfoText, styles.cardInfoTextHovered];
diff --git a/packages/app/src/composer/index.tsx b/packages/app/src/composer/index.tsx
index 6faa7a5e5..c2e3e5dbf 100644
--- a/packages/app/src/composer/index.tsx
+++ b/packages/app/src/composer/index.tsx
@@ -570,7 +570,7 @@ function QueuedMessageRow({
@@ -2237,8 +2237,6 @@ const styles = StyleSheet.create((theme: Theme) => ({
},
})) as unknown as Record;
-const QUEUE_SEND_BUTTON_STYLE = [styles.queueActionButton, styles.queueSendButton];
-
const ThemedPencil = withUnistyles(Pencil);
const ThemedArrowUp = withUnistyles(ArrowUp);
const ThemedGitPullRequest = withUnistyles(GitPullRequest);
diff --git a/packages/app/src/desktop/components/desktop-updates-section.tsx b/packages/app/src/desktop/components/desktop-updates-section.tsx
index 9725a1848..5fa4685c9 100644
--- a/packages/app/src/desktop/components/desktop-updates-section.tsx
+++ b/packages/app/src/desktop/components/desktop-updates-section.tsx
@@ -246,7 +246,7 @@ function DaemonInfoCard(props: DaemonInfoCardProps) {
{daemonStatusDetailText}
-
+
{t("desktop.daemon.management.title")}
{t("desktop.daemon.management.hint")}
@@ -258,7 +258,7 @@ function DaemonInfoCard(props: DaemonInfoCardProps) {
accessibilityLabel={t("desktop.daemon.management.title")}
/>
-
+
{t("desktop.daemon.keepRunning.title")}
{t("desktop.daemon.keepRunning.hint")}
@@ -270,7 +270,7 @@ function DaemonInfoCard(props: DaemonInfoCardProps) {
accessibilityLabel={t("desktop.daemon.keepRunning.title")}
/>
-
+
{t("desktop.daemon.logs.title")}
@@ -294,7 +294,7 @@ function DaemonInfoCard(props: DaemonInfoCardProps) {
-
+
{t("desktop.daemon.fullStatus.title")}
{t("desktop.daemon.fullStatus.hint")}
@@ -420,7 +420,7 @@ export function LocalDaemonSection() {
testID="host-page-daemon-lifecycle-card"
>
{isLoading || isLoadingSettings ? (
-
+
) : (
@@ -524,7 +524,5 @@ const styles = StyleSheet.create((theme) => ({
},
}));
-const LOADING_CARD_STYLE = [settingsStyles.card, styles.loadingCard];
-const ROW_WITH_BORDER_STYLE = [settingsStyles.row, settingsStyles.rowBorder];
const LOGS_MODAL_SNAP_POINTS = ["70%", "92%"];
const CLI_STATUS_MODAL_SNAP_POINTS = ["60%", "85%"];
diff --git a/packages/app/src/desktop/components/integrations-section.tsx b/packages/app/src/desktop/components/integrations-section.tsx
index 531172eb3..01d0f5a8b 100644
--- a/packages/app/src/desktop/components/integrations-section.tsx
+++ b/packages/app/src/desktop/components/integrations-section.tsx
@@ -19,8 +19,6 @@ import { useCliInstall, useSkillsStatus } from "@/desktop/hooks/use-install-stat
const CLI_DOCS_URL = "https://paseo.sh/docs/cli";
const SKILLS_DOCS_URL = "https://paseo.sh/docs/skills";
-const ROW_WITH_BORDER_STYLE = [settingsStyles.row, settingsStyles.rowBorder];
-
const OP_KIND_ORDER: Record = { add: 0, update: 1, delete: 2 };
const OP_KIND_LABEL_KEY: Record = {
add: "settings.integrations.operations.add",
@@ -183,7 +181,7 @@ export function IntegrationsSection() {
)}
-
+
diff --git a/packages/app/src/git/diff-pane.tsx b/packages/app/src/git/diff-pane.tsx
index 27db92cd4..5a1d2e007 100644
--- a/packages/app/src/git/diff-pane.tsx
+++ b/packages/app/src/git/diff-pane.tsx
@@ -1091,7 +1091,11 @@ export function DiffFileBody({
);
return (
-
+
{(() => {
if (file.status === "too_large" || file.status === "binary") {
return (
@@ -1118,7 +1122,7 @@ export function DiffFileBody({
if (layout === "split") {
const rows = buildSplitDiffRows(file);
return (
-
+
0 ? scrollViewWidth : Math.max(0, bodyWidth - gutterWidth);
return (
-
+
{computedLines.map(({ line, lineNumber, key, reviewTarget }, index) => (
@@ -3062,7 +3066,4 @@ const styles = StyleSheet.create((theme) => ({
},
}));
-const FILE_SECTION_BODY_STYLE = [styles.fileSectionBodyContainer, styles.fileSectionBorder];
-const DIFF_CONTENT_SPLIT_ROW_STYLE = [styles.diffContent, styles.splitRow];
-const DIFF_CONTENT_ROW_STYLE = [styles.diffContent, styles.diffContentRow];
const DIFF_HEIGHT_CHANGE_EPSILON = 0.5;
diff --git a/packages/app/src/provider-usage/settings-section.tsx b/packages/app/src/provider-usage/settings-section.tsx
index 58da33fff..51da2d710 100644
--- a/packages/app/src/provider-usage/settings-section.tsx
+++ b/packages/app/src/provider-usage/settings-section.tsx
@@ -55,7 +55,7 @@ function ProviderUsageBody({
}) {
if (view.kind === "loading") {
return (
-
+
{providerUsageCopy.loading}
);
@@ -73,7 +73,7 @@ function ProviderUsageBody({
if (view.payload.providers.length === 0) {
return (
-
+
{providerUsageCopy.empty}
);
@@ -92,5 +92,3 @@ const styles = StyleSheet.create((theme) => ({
fontSize: theme.fontSize.sm,
},
}));
-
-const EMPTY_CARD_STYLE = [settingsStyles.card, styles.emptyCard];
diff --git a/packages/app/src/screens/settings-screen.tsx b/packages/app/src/screens/settings-screen.tsx
index 5eefdf6fa..dcbaf1fa3 100644
--- a/packages/app/src/screens/settings-screen.tsx
+++ b/packages/app/src/screens/settings-screen.tsx
@@ -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}
/>
-
+
{t("settings.general.language.label")}
{t("settings.general.language.description")}
@@ -387,7 +385,7 @@ function GeneralSection({
{isDesktopApp ? (
-
+
{t("settings.general.serviceUrls.label")}
@@ -414,7 +412,7 @@ function GeneralSection({
) : null}
-
+
{t("settings.general.terminalScrollback.label")}
@@ -700,7 +698,7 @@ function DesktopAppUpdateRow() {
return (
<>
-
+
{t("settings.about.releaseChannel.label")}
@@ -714,7 +712,7 @@ function DesktopAppUpdateRow() {
options={releaseChannelOptions}
/>
-
+
{t("settings.about.updates.label")}
{statusText}
diff --git a/packages/app/src/screens/settings/appearance/appearance-section.tsx b/packages/app/src/screens/settings/appearance/appearance-section.tsx
index 4ae7b2886..3f8a41fe0 100644
--- a/packages/app/src/screens/settings/appearance/appearance-section.tsx
+++ b/packages/app/src/screens/settings/appearance/appearance-section.tsx
@@ -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 (
-
+
{t("settings.general.toolCallDetail.label")}
diff --git a/packages/app/src/screens/settings/host-page.tsx b/packages/app/src/screens/settings/host-page.tsx
index e2a62970f..4fde68367 100644
--- a/packages/app/src/screens/settings/host-page.tsx
+++ b/packages/app/src/screens/settings/host-page.tsx
@@ -150,7 +150,7 @@ function HostNotFound() {
const { t } = useTranslation();
return (
-
+
{t("settings.host.notFound")}
@@ -276,7 +276,7 @@ export function HostAgentsPage({ serverId }: { serverId: string }) {
) : (
-
+
{t("settings.host.agents.unavailable")}
)}
@@ -300,7 +300,7 @@ export function HostWorkspacesPage({ serverId }: { serverId: string }) {
) : (
-
+
{t("settings.host.workspaces.unavailable")}
)}
@@ -1940,4 +1940,3 @@ const styles = StyleSheet.create((theme) => ({
}));
const FLEX_1_STYLE = { flex: 1 };
-const EMPTY_CARD_STYLE = [settingsStyles.card, styles.emptyCard];
diff --git a/packages/app/src/screens/settings/keyboard-shortcuts-section.tsx b/packages/app/src/screens/settings/keyboard-shortcuts-section.tsx
index de1323df5..b81dcb4ed 100644
--- a/packages/app/src/screens/settings/keyboard-shortcuts-section.tsx
+++ b/packages/app/src/screens/settings/keyboard-shortcuts-section.tsx
@@ -264,7 +264,7 @@ export function KeyboardShortcutsSection() {
if (isNative) {
return (
-
+
{t("settings.shortcuts.unavailableOnMobile")}
@@ -362,5 +362,3 @@ const styles = StyleSheet.create((theme) => ({
color: theme.colors.foregroundMuted,
},
}));
-
-const mobileCardStyle = [settingsStyles.card, styles.mobileCard];
diff --git a/packages/app/src/screens/settings/providers-section.tsx b/packages/app/src/screens/settings/providers-section.tsx
index a21ac56a8..2d951c954 100644
--- a/packages/app/src/screens/settings/providers-section.tsx
+++ b/packages/app/src/screens/settings/providers-section.tsx
@@ -410,12 +410,12 @@ export function ProvidersSection({ serverId }: ProvidersSectionProps) {
style={styles.sectionSpacing}
>
{!hasServer || !isConnected ? (
-
+
{t("settings.providers.unavailable")}
) : null}
{hasServer && isConnected && isLoading ? (
-
+
{t("settings.providers.loading")}
) : null}
@@ -543,5 +543,3 @@ const styles = StyleSheet.create((theme) => ({
backgroundColor: theme.colors.surface3,
},
}));
-
-const EMPTY_CARD_STYLE = [settingsStyles.card, styles.emptyCard];
diff --git a/packages/app/src/screens/workspace/workspace-desktop-tabs-row.tsx b/packages/app/src/screens/workspace/workspace-desktop-tabs-row.tsx
index 3180bbc44..bee528e26 100644
--- a/packages/app/src/screens/workspace/workspace-desktop-tabs-row.tsx
+++ b/packages/app/src/screens/workspace/workspace-desktop-tabs-row.tsx
@@ -1165,7 +1165,9 @@ function ResolvedDesktopTabChip({
return (
- {showDropIndicatorBefore ? : null}
+ {showDropIndicatorBefore ? (
+
+ ) : null}
- {showDropIndicatorAfter ? : null}
+ {showDropIndicatorAfter ? (
+
+ ) : null}
);
}}
@@ -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];
diff --git a/packages/app/src/screens/workspace/workspace-screen.tsx b/packages/app/src/screens/workspace/workspace-screen.tsx
index 16cf2b0cc..828eef184 100644
--- a/packages/app/src/screens/workspace/workspace-screen.tsx
+++ b/packages/app/src/screens/workspace/workspace-screen.tsx
@@ -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"];
diff --git a/packages/app/src/screens/workspace/workspace-scripts-button.tsx b/packages/app/src/screens/workspace/workspace-scripts-button.tsx
index 4d3b25ec6..d310d759c 100644
--- a/packages/app/src/screens/workspace/workspace-scripts-button.tsx
+++ b/packages/app/src/screens/workspace/workspace-scripts-button.tsx
@@ -88,7 +88,9 @@ function ScriptActionButtonChildren({
) : (
);
- 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 (
<>
@@ -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 (
{t("workspace.scripts.states.exitCode", { code })}
@@ -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];
diff --git a/packages/app/src/styles/unistyles-module-scope.test.ts b/packages/app/src/styles/unistyles-module-scope.test.ts
new file mode 100644
index 000000000..714875d97
--- /dev/null
+++ b/packages/app/src/styles/unistyles-module-scope.test.ts
@@ -0,0 +1,76 @@
+import { readdirSync, readFileSync } from "node:fs";
+import path from "node:path";
+import * as ts from "typescript";
+import { describe, expect, it } from "vitest";
+
+const SOURCE_ROOT = path.resolve(__dirname, "..");
+
+function listSourceFiles(directory: string): string[] {
+ return readdirSync(directory, { withFileTypes: true }).flatMap((entry) => {
+ const entryPath = path.join(directory, entry.name);
+ if (entry.isDirectory()) {
+ return listSourceFiles(entryPath);
+ }
+ return /\.tsx?$/.test(entry.name) ? [entryPath] : [];
+ });
+}
+
+function containsEagerStyleRead(initializer: ts.Expression): boolean {
+ if (ts.isFunctionLike(initializer)) {
+ return false;
+ }
+
+ let found = false;
+
+ function visit(node: ts.Node): void {
+ if (node !== initializer && ts.isFunctionLike(node)) {
+ return;
+ }
+ if (
+ ts.isPropertyAccessExpression(node) &&
+ ts.isIdentifier(node.expression) &&
+ node.expression.text !== "StyleSheet" &&
+ /styles$/i.test(node.expression.text)
+ ) {
+ found = true;
+ return;
+ }
+ ts.forEachChild(node, visit);
+ }
+
+ visit(initializer);
+ return found;
+}
+
+function findEagerModuleStyleReads(filePath: string): string[] {
+ const source = readFileSync(filePath, "utf8");
+ const sourceFile = ts.createSourceFile(
+ filePath,
+ source,
+ ts.ScriptTarget.Latest,
+ true,
+ filePath.endsWith(".tsx") ? ts.ScriptKind.TSX : ts.ScriptKind.TS,
+ );
+
+ return sourceFile.statements.flatMap((statement) => {
+ if (!ts.isVariableStatement(statement)) {
+ return [];
+ }
+ return statement.declarationList.declarations.flatMap((declaration) => {
+ if (!declaration.initializer || !containsEagerStyleRead(declaration.initializer)) {
+ return [];
+ }
+ const line =
+ sourceFile.getLineAndCharacterOfPosition(declaration.getStart(sourceFile)).line + 1;
+ return [`${path.relative(SOURCE_ROOT, filePath)}:${line}`];
+ });
+ });
+}
+
+describe("Unistyles module scope", () => {
+ it("does not materialize style proxies before the persisted theme loads", () => {
+ const violations = listSourceFiles(SOURCE_ROOT).flatMap(findEagerModuleStyleReads);
+
+ expect(violations).toEqual([]);
+ });
+});