Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
c672c36
feat(mobile): plan mode (legacy) toggle with cross-client sync
Aug 14, 2026
0b1f719
fix(mobile): address review-bot findings
Aug 14, 2026
c04006b
fix(mobile): residual review findings from stacked PR triage
Aug 15, 2026
5068725
fix(mobile): address follow-up review findings
Aug 15, 2026
67df07d
fix(web): address review finding on synced settings hook
Aug 15, 2026
a28abf5
fix(mobile): bounded preference retry and stream compat
Aug 15, 2026
cef7f85
fix(mobile): retry exhausted preference reconciliation on next trigger
Aug 15, 2026
f842dd1
fix(mobile): guard stale settles in preference sync
Aug 15, 2026
54ca4bf
fix(web): keep adoption guard on inactive settles
Aug 15, 2026
bd13cef
refactor(mobile): simplify synced preference machinery
Aug 15, 2026
c37046e
fix(mobile): deterministic multi-env settle and read-only fallback
Aug 15, 2026
73f17e5
fix(mobile): per-field last-write-wins for synced preferences
Aug 15, 2026
ee1eec9
fix(mobile): monotonic write stamps
Aug 15, 2026
8786028
refactor(mobile): remove review-round slop
Aug 15, 2026
f187a21
fix(mobile): address review findings
Aug 15, 2026
e7ceaca
fix(mobile): preference write coherence and hydration gating
Aug 15, 2026
b9c3507
fix(mobile): stable reconciliation readiness under reconnects
Aug 15, 2026
4e90cc8
fix(mobile): gating coverage and share-transfer race
Aug 15, 2026
65b10cd
fix(mobile): settle reconciliation readiness semantics and secondary …
Aug 15, 2026
74c6466
fix(mobile): deterministic equal-stamp preference tiebreak
Aug 15, 2026
2a06432
fix(mobile): consistent hydration gate and read-only reconcile
Aug 15, 2026
33efe9c
fix(mobile): stamp preservation and retry budget semantics
Aug 15, 2026
4cf411d
fix(mobile): value-aware reconciliation watermark
Aug 15, 2026
f2de8bd
refactor(mobile): apply anti-slop lint pass
Aug 15, 2026
0ca623a
feat(mobile): themes with appearance override and custom import
Aug 14, 2026
85a1185
feat(mobile): sync appearance mode and theme across clients
Aug 14, 2026
e464ea4
fix(mobile): address review-bot findings
Aug 14, 2026
a93a45e
fix(mobile): use effective appearance in new-task composer
Aug 15, 2026
e932c56
fix(mobile): address follow-up review findings
Aug 15, 2026
1a9c9bd
fix(mobile): hsl none components and popover appearance override
Aug 15, 2026
d6a1092
fix(mobile): theme form sheet surfaces with appearance override
Aug 15, 2026
dba88bb
test(web): port inactive-settle adoption guard test to generalized API
Aug 15, 2026
1dfeff0
fix(mobile): opaque custom theme ids and forced-scheme propagation
Aug 15, 2026
f9f7f46
fix(mobile): splash plugin mod shape, unsent-draft live activity, opa…
Aug 15, 2026
f6b69cf
refactor(mobile): simplify theme internals
Aug 15, 2026
1c8ed6d
fix(mobile): rgb clamping parity and themed new-task fade
Aug 15, 2026
a915dff
refactor(mobile): remove review-round slop
Aug 15, 2026
e474c66
fix(mobile): address review findings
Aug 15, 2026
061c307
fix(mobile): theme removal respects newer selection
Aug 15, 2026
a8cc43f
fix(mobile): parse newline-separated color components
Aug 15, 2026
ae02d72
refactor(mobile): apply anti-slop lint pass
Aug 15, 2026
d84cff6
fix(mobile): outbound stamp clamp and custom-theme sync scope
Aug 16, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 4 additions & 13 deletions apps/mobile/app.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { ExpoConfig } from "expo/config";

import { BRAND_ASSET_PATHS } from "../../scripts/lib/brand-assets.ts";
import { loadRepoEnv } from "../../scripts/lib/public-config.ts";
import { createSchemeNeutralSplashConfig } from "./src/lib/splashConfig.ts";

type AppVariant = "development" | "preview" | "production";

Expand Down Expand Up @@ -299,19 +300,9 @@ const config: ExpoConfig = {
},
],
["expo-image-picker", { photosPermission: false, microphonePermission: false }],
[
"expo-splash-screen",
{
image: variant.assets.splashIcon,
resizeMode: "contain",
backgroundColor: "#ffffff",
imageWidth: 220,
dark: {
image: variant.assets.splashIcon,
backgroundColor: "#0a0a0a",
},
},
],
// Expo's storyboard mod runs first, then this inlines its generated color.
"./plugins/withIosSplashScreenInlineBackground.cjs",
["expo-splash-screen", createSchemeNeutralSplashConfig(variant.assets.splashIcon)],
[
"expo-build-properties",
{
Expand Down
4 changes: 4 additions & 0 deletions apps/mobile/global.css
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,10 @@
--color-md-blockquote-bg: rgba(0, 0, 0, 0.02);
--color-md-code-bg: rgba(0, 0, 0, 0.04);
--color-md-code-text: #262626;
--color-md-inline-code-text: #5f6368;
--color-md-user-code-bg: rgba(255, 255, 255, 0.22);
--color-md-user-code-text: #ffffff;
--color-md-user-inline-code-text: rgba(255, 255, 255, 0.82);
--color-md-user-fence-bg: rgba(0, 0, 0, 0.16);
--color-md-user-fence-text: #ffffff;
--color-md-hr: rgba(0, 0, 0, 0.08);
Expand Down Expand Up @@ -174,8 +176,10 @@
--color-md-blockquote-bg: rgba(255, 255, 255, 0.03);
--color-md-code-bg: rgba(255, 255, 255, 0.06);
--color-md-code-text: #e5e5e5;
--color-md-inline-code-text: #b8bcc2;
--color-md-user-code-bg: rgba(255, 255, 255, 0.18);
--color-md-user-code-text: #ffffff;
--color-md-user-inline-code-text: rgba(255, 255, 255, 0.82);
--color-md-user-fence-bg: rgba(0, 0, 0, 0.28);
--color-md-user-fence-text: #ffffff;
--color-md-hr: rgba(255, 255, 255, 0.08);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1404,16 +1404,27 @@ private fun parseStringSet(value: String): Set<String> = try {
emptySet()
}

private fun parseColor(value: String, fallback: Int): Int = try {
Color.parseColor(value)
} catch (_: Exception) {
fallback
private fun parseColor(value: String, fallback: Int): Int = parseColorOrNull(value) ?: fallback

private fun parseColorOrNull(value: String): Int? {
decodeCssHexColor(value)?.let { return it }
return try {
Color.parseColor(value)
} catch (_: Exception) {
null
}
}

private fun parseColorOrNull(value: String): Int? = try {
Color.parseColor(value)
} catch (_: Exception) {
null
private fun decodeCssHexColor(value: String): Int? {
val hex = value.trim().takeIf { it.startsWith("#") }?.drop(1) ?: return null
if (hex.length != 6 && hex.length != 8) return null
if (!hex.all { it in '0'..'9' || it in 'a'..'f' || it in 'A'..'F' }) return null
val raw = hex.toLongOrNull(16) ?: return null
if (hex.length == 6) return (0xff000000L or raw).toInt()

val alpha = raw and 0xff
val rgb = raw shr 8
return ((alpha shl 24) or rgb).toInt()
}

private fun JSONObject.optNullableString(key: String): String? =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -428,10 +428,24 @@ class T3TerminalView(context: Context, appContext: AppContext) : ExpoView(contex
}
}

private fun parseColor(value: String, fallback: Int): Int =
try {
private fun parseColor(value: String, fallback: Int): Int {
decodeCssHexColor(value)?.let { return it }
return try {
Color.parseColor(value)
} catch (_: IllegalArgumentException) {
fallback
}
}

private fun decodeCssHexColor(value: String): Int? {
val hex = value.trim().takeIf { it.startsWith("#") }?.drop(1) ?: return null
if (hex.length != 6 && hex.length != 8) return null
if (!hex.all { it in '0'..'9' || it in 'a'..'f' || it in 'A'..'F' }) return null
val raw = hex.toLongOrNull(16) ?: return null
if (hex.length == 6) return (0xff000000L or raw).toInt()

val alpha = raw and 0xff
val rgb = raw shr 8
return ((alpha shl 24) or rgb).toInt()
}
}
38 changes: 27 additions & 11 deletions apps/mobile/modules/t3-terminal/ios/T3TerminalView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -181,15 +181,27 @@ private enum TerminalAppearanceScheme: String {
}

private extension UIColor {
convenience init(hexString: String) {
let sanitized = hexString.replacingOccurrences(of: "#", with: "")
let value = Int(sanitized, radix: 16) ?? 0
self.init(
red: CGFloat((value >> 16) & 0xFF) / 255,
green: CGFloat((value >> 8) & 0xFF) / 255,
blue: CGFloat(value & 0xFF) / 255,
alpha: 1
)
convenience init?(cssHex: String) {
let value = cssHex.trimmingCharacters(in: .whitespacesAndNewlines)
guard value.hasPrefix("#") else { return nil }
let hex = String(value.dropFirst())
guard hex.count == 6 || hex.count == 8, let raw = UInt64(hex, radix: 16) else { return nil }

if hex.count == 8 {
self.init(
red: CGFloat((raw >> 24) & 0xFF) / 255,
green: CGFloat((raw >> 16) & 0xFF) / 255,
blue: CGFloat((raw >> 8) & 0xFF) / 255,
alpha: CGFloat(raw & 0xFF) / 255
)
} else {
self.init(
red: CGFloat((raw >> 16) & 0xFF) / 255,
green: CGFloat((raw >> 8) & 0xFF) / 255,
blue: CGFloat(raw & 0xFF) / 255,
alpha: 1
)
}
}
}

Expand All @@ -211,7 +223,7 @@ public final class T3TerminalView: ExpoView, UITextFieldDelegate {
private var isCreatingSurface = false
private var surfaceCreationFailed = false
private var appearance = TerminalAppearanceScheme.dark
private var backgroundColorValue = UIColor(hexString: "#24292e")
private var backgroundColorValue = UIColor(cssHex: "#24292e") ?? .black

let onInput = EventDispatcher()
let onResize = EventDispatcher()
Expand Down Expand Up @@ -276,7 +288,11 @@ public final class T3TerminalView: ExpoView, UITextFieldDelegate {

var backgroundColorHex: String = "#24292e" {
didSet {
backgroundColorValue = UIColor(hexString: backgroundColorHex)
guard let color = UIColor(cssHex: backgroundColorHex) else {
backgroundColorHex = oldValue
return
}
backgroundColorValue = color
applyTheme()
}
}
Expand Down
35 changes: 35 additions & 0 deletions apps/mobile/plugins/withIosSplashScreenInlineBackground.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
const { withMod } = require("expo/config-plugins");

function inlineSplashScreenBackground(modResults) {
// Expo's splashScreenStoryboard modResults is the parsed XML root, whose
// top-level `document` property contains the storyboard document.
const document = modResults.document;
const mainView = document.scenes?.[0]?.scene?.[0]?.objects?.[0]?.viewController?.[0]?.view?.[0];
const namedBackground = document.resources?.[0]?.namedColor?.find(
(entry) => entry.$?.name === "SplashScreenBackground",
);
const backgroundReference = mainView?.color?.find((entry) => entry.$?.key === "backgroundColor");
const concreteColor = namedBackground?.color?.[0]?.$;

if (backgroundReference?.$?.name !== "SplashScreenBackground" || !concreteColor) {
throw new Error(
"Could not find Expo's generated splash background in SplashScreen.storyboard.",
);
}

mainView.color = [{ $: { key: "backgroundColor", ...concreteColor } }];
return modResults;
}

module.exports = function withIosSplashScreenInlineBackground(config) {
return withMod(config, {
platform: "ios",
mod: "splashScreenStoryboard",
action: async (nextConfig) => {
nextConfig.modResults = inlineSplashScreenBackground(nextConfig.modResults);
return nextConfig;
},
});
};

module.exports.inlineSplashScreenBackground = inlineSplashScreenBackground;
107 changes: 107 additions & 0 deletions apps/mobile/plugins/withIosSplashScreenInlineBackground.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import * as NodeModule from "node:module";

import { describe, expect, it } from "vite-plus/test";

type StoryboardColor = {
$: Record<string, string>;
};

type StoryboardModResults = {
document: {
scenes: Array<{
scene: Array<{
objects: Array<{
viewController: Array<{
view: Array<{
color: Array<StoryboardColor>;
}>;
}>;
}>;
}>;
}>;
resources: Array<{
namedColor: Array<{
$: { name: string };
color: Array<StoryboardColor>;
}>;
}>;
};
};

const require = NodeModule.createRequire(import.meta.url);
const {
inlineSplashScreenBackground,
}: {
inlineSplashScreenBackground: (modResults: StoryboardModResults) => StoryboardModResults;
} = require("./withIosSplashScreenInlineBackground.cjs");

describe("iOS splash screen inline background", () => {
it("transforms Expo's real storyboard modResults shape", () => {
const modResults: StoryboardModResults = {
document: {
scenes: [
{
scene: [
{
objects: [
{
viewController: [
{
view: [
{
color: [
{
$: {
key: "backgroundColor",
name: "SplashScreenBackground",
},
},
],
},
],
},
],
},
],
},
],
},
],
resources: [
{
namedColor: [
{
$: { name: "SplashScreenBackground" },
color: [
{
$: {
alpha: "1.000",
blue: "0.450980392156863",
green: "0.450980392156863",
red: "0.450980392156863",
},
},
],
},
],
},
],
},
};

expect(inlineSplashScreenBackground(modResults)).toBe(modResults);
expect(
modResults.document.scenes[0]?.scene[0]?.objects[0]?.viewController[0]?.view[0]?.color,
).toEqual([
{
$: {
key: "backgroundColor",
alpha: "1.000",
blue: "0.450980392156863",
green: "0.450980392156863",
red: "0.450980392156863",
},
},
]);
});
});
Loading
Loading