Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
03ac1f0
fix(release): stage pnpm 11 allowBuilds for desktop installs (#3781)
juliusmarminge Jul 7, 2026
e775bc6
Upgrade Clerk toolchain to latest versions (#3785)
juliusmarminge Jul 8, 2026
0c66565
fix(release): bump electron-builder so pnpm 11 deduped deps land in t…
avocardow Jul 8, 2026
18a4138
Fix desktop native optional dependency packaging (#3816)
Prgm-code Jul 9, 2026
e912765
[codex] Upgrade Clerk stack (#3821)
juliusmarminge Jul 9, 2026
3201e00
[codex] Preserve worktree metadata during branch sync (#3822)
juliusmarminge Jul 9, 2026
2250e3e
feat(client): persist offline environment data and mobile preferences…
juliusmarminge Jul 9, 2026
ca1e08b
[codex] Label max and ultra reasoning (#3824)
juliusmarminge Jul 9, 2026
843cf17
fix(mobile): embed fonts and render project favicons reliably (#3823)
juliusmarminge Jul 9, 2026
8619ef2
Show compact PR number badges in mobile thread rows (#3827)
juliusmarminge Jul 9, 2026
f61fa94
Expose mobile PR indicator labels to accessibility (#3828)
juliusmarminge Jul 9, 2026
ef943a2
Fix truncated chat error alert layout (#3899)
jakeleventhal Jul 12, 2026
619b0ec
fix(marketing): show platform-appropriate commit shortcut on the webs…
VedankPurohit Jul 12, 2026
c1ec191
[codex] Add Android mobile support (#3579)
juliusmarminge Jul 12, 2026
bfd9ca3
fix(mobile): Backport iOS runtime and chat stability fixes
mwolson Jul 11, 2026
3e7ed41
fix(mobile): Account for header in thread fit checks
mwolson Jul 12, 2026
c62877c
fix(mobile): Stabilize screenshot attachment layout
mwolson Jul 12, 2026
ca998a4
fix(mobile): Harden iOS crash capture and stack option updates
mwolson Jul 12, 2026
f347d3e
fix(mobile): Satisfy SwiftLint opening_brace in T3CrashLog
mwolson Jul 12, 2026
de8b9f6
fix(client-runtime): Heal shell archive membership on reconnect
mwolson Jul 13, 2026
666bf0a
fix(client-runtime): Apply socket shell heal authoritatively
mwolson Jul 13, 2026
292609f
fix(client-runtime): Clear socket-authoritative flag after HTTP heal
mwolson Jul 13, 2026
07c6c32
test(server): Harden CursorAdapter log wait for CI load
mwolson Jul 13, 2026
7b2a0a3
test(server): Use wall-clock waits in CursorAdapter log polling
mwolson Jul 13, 2026
ed1a5f2
fix(mobile): Keep threadScreenOptions useMemo above early returns
mwolson Jul 13, 2026
4311beb
Confirm resumed thread subscriptions (#2)
twistedgrim Jul 13, 2026
73c2c48
fix(orchestration): Gate thread resume sync marker after catch-up
mwolson Jul 13, 2026
936f67f
fix(client-runtime): Hold shell list until server heal on reconnect
mwolson Jul 13, 2026
6aabd97
fix(client-runtime): Delay shell disk paint and re-arm sync marker
mwolson Jul 13, 2026
924ad10
fix(mobile): Keep chat content above composer when keyboard opens on …
mwolson Jul 14, 2026
c68ddeb
feat(integration): Merge codex-turn-mapping into ios-fixes-main as v2…
mwolson Jul 14, 2026
03025ce
fix(mobile): Stabilize CTM iOS draft attachments, Hermes sort, and Home
mwolson Jul 12, 2026
0a2efa9
fix(client-runtime): Normalize V2 shell archive membership
mwolson Jul 13, 2026
8346525
fix(mobile): Preserve Android scroll position on keyboard close
mwolson Jul 14, 2026
2ec85a8
fix(deps): Update keyboard controller patch lockfile
mwolson Jul 14, 2026
62f3298
feat(integration): Merge ios-fixes-main into v2-base
mwolson Jul 17, 2026
30b5502
feat(integration): Merge v2-base into ios-v2-attachment-persistence
mwolson Jul 17, 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
9 changes: 9 additions & 0 deletions .macroscope/check-run-agents/effect-service-conventions.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,15 @@ Review changed TypeScript and directly affected call sites for the conventions b
- Keep implementation-specific names when an abstract port module contains one of several possible implementations, for example `makeCloudflaredRelayClient` and `layerCloudflared` in `RelayClient.ts`.
- `infra/relay/src/db.ts` is an intentional exception: an inline `Layer.succeed(RelayDb, db)` is acceptable without generic `make`/`layer` exports.

## Dependency acquisition and runtime boundaries

- Production service construction must acquire Effect service dependencies from the environment with `yield* Foo.Foo`, and its `make`/`layer` types must expose those requirements. Flag factories or constructors that accept `Foo["Service"]` (or a plain object whose methods return `Effect`) when that value is an implementation dependency owned by the service. Passing service instances explicitly is acceptable in tests and integration harnesses; passing pure configuration, immutable domain values, or deliberate callback strategies is not service injection.
- Do not hide dependencies in module globals, closures over singleton services, or `Layer.succeed` implementations that call runtime-backed or imperative APIs. Trace helpers used by a supposedly synchronous layer far enough to verify that asynchronous services are represented in the Effect environment.
- `ManagedRuntime.make`, `runPromise`, and `runPromiseExit` belong at explicit application/framework boundaries such as React, native callback, CLI, or HTTP adapters. Flag their use in domain services, repositories, persistence implementations, and service constructors. A clearly named imperative adapter may bridge an Effect service into a Promise API, but it must not become a dependency of another Effect service.
- Do not create per-feature managed runtimes or Atom runtimes to smuggle the same owned resource into multiple consumers. Compose the resource once in an application-owned layer/runtime and provide its context to integration runtimes.
- When acquisition can fail but a caller must retain fallback behavior, keep the failure typed in Effect rather than bypassing the layer through an imperative runtime. Model unavailability in service operations or with an explicit optional-service layer so downstream recovery remains visible and testable.
- During review, search touched code and affected call sites for service-instance parameters, `Layer.succeed`, `ManagedRuntime.make`, and `.runPromise`/`.runPromiseExit`. Verify that each occurrence is a legitimate test seam, pure value injection, or application boundary—not fake dependency injection or a hidden runtime.

## Errors and predicates

- Define service failures with `Schema.TaggedErrorClass` and structured attributes. Derive `message` from those attributes rather than storing an unstructured message as the only data.
Expand Down
3 changes: 3 additions & 0 deletions app.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"expo": {}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Empty root Expo config committed

Low Severity

A new repo-root app.json with an empty "expo": {} object was added. Mobile already configures Expo via apps/mobile/app.config.ts, and the PR notes call out excluding local scaffolding, so this looks like accidental tooling output that can confuse monorepo Expo resolution.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 0a2efa9. Configure here.

2 changes: 1 addition & 1 deletion apps/desktop/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
"@effect/vitest": "catalog:",
"@types/node": "catalog:",
"cross-env": "^10.1.0",
"electron-builder": "26.8.1",
"electron-builder": "26.15.6",
"tailwindcss": "^4.0.0",
"vite-plus": "catalog:"
},
Expand Down
4 changes: 3 additions & 1 deletion apps/marketing/src/pages/index.astro
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,7 @@ const mobileEndorsementRows = [
<button class="pr-button">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><circle cx="6" cy="6" r="2.2"/><circle cx="6" cy="18" r="2.2"/><circle cx="18" cy="6" r="2.2"/><path d="M6 8v8M18 8v5a3 3 0 01-3 3h-3M13 13l-2 3 2 3"/></svg>
Commit &amp; push
<span class="pr-button-kbd">⌘⏎</span>
<span id="pr-button-kbd" class="pr-button-kbd">⌘⏎</span>
</button>
</div>
</div>
Expand Down Expand Up @@ -403,12 +403,14 @@ const mobileEndorsementRows = [
const label = document.getElementById("download-label");
const ctaBtn = document.getElementById("cta-download-btn") as HTMLAnchorElement | null;
const ctaLabel = document.getElementById("cta-download-label");
const kbd = document.getElementById("pr-button-kbd");

const platform = detectPlatform();
if (!platform) return;
document.documentElement.dataset.platform = platform.os;
if (label) label.textContent = platform.label;
if (ctaLabel) ctaLabel.textContent = platform.label;
if (kbd && platform.os !== "mac") kbd.textContent = "Ctrl ⏎";

try {
const release = await fetchLatestRelease();
Expand Down
24 changes: 24 additions & 0 deletions apps/mobile/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,30 @@ Build and run the local iOS dev client:
vp run ios:dev
```

If your Xcode account only has a Personal Team, use a bundle identifier you control and opt into the
reduced-capability local build. Personal Team builds omit the widget extension, push entitlement, and
native Sign in with Apple entitlement; builds without this opt-in are unchanged.

```bash
T3CODE_IOS_PERSONAL_TEAM=1 \
T3CODE_IOS_PERSONAL_TEAM_BUNDLE_ID=com.example.t3code.dev \
vp run ios:dev
```

Build and install a self-contained Release app that does not need Metro:

```bash
vp run ios:release
```

The Personal Team equivalent also needs a unique bundle identifier:

```bash
T3CODE_IOS_PERSONAL_TEAM=1 \
T3CODE_IOS_PERSONAL_TEAM_BUNDLE_ID=com.example.t3code \
vp run ios:release
```

Build and run the local iOS preview app:

```bash
Expand Down
121 changes: 98 additions & 23 deletions apps/mobile/app.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,20 @@ const repoEnv = loadRepoEnv();
Object.assign(process.env, repoEnv);

const APP_VARIANT = resolveAppVariant(repoEnv.APP_VARIANT);
const isIosPersonalTeamBuild = repoEnv.T3CODE_IOS_PERSONAL_TEAM === "1";

const personalTeamBundleIdentifier = repoEnv.T3CODE_IOS_PERSONAL_TEAM_BUNDLE_ID?.trim();
const IOS_BUNDLE_IDENTIFIER_PATTERN = /^[A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)+$/;

if (
isIosPersonalTeamBuild &&
(!personalTeamBundleIdentifier ||
!IOS_BUNDLE_IDENTIFIER_PATTERN.test(personalTeamBundleIdentifier))
) {
throw new Error(
"T3CODE_IOS_PERSONAL_TEAM_BUNDLE_ID must be a reverse-DNS identifier such as com.example.t3code when T3CODE_IOS_PERSONAL_TEAM=1.",
);
}

const VARIANT_CONFIG: Record<
AppVariant,
Expand Down Expand Up @@ -63,6 +77,36 @@ function resolveAppVariant(value: string | undefined): AppVariant {

const variant = VARIANT_CONFIG[APP_VARIANT];

const dmSansFonts = {
regular: "@expo-google-fonts/dm-sans/400Regular/DMSans_400Regular.ttf",
medium: "@expo-google-fonts/dm-sans/500Medium/DMSans_500Medium.ttf",
bold: "@expo-google-fonts/dm-sans/700Bold/DMSans_700Bold.ttf",
} as const;

const widgetsPlugin: NonNullable<ExpoConfig["plugins"]>[number] = [
"expo-widgets",
{
bundleIdentifier: `${variant.iosBundleIdentifier}.widgets`,
groupIdentifier: `group.${variant.iosBundleIdentifier}`,
enablePushNotifications: true,
// Agent activity can update many times an hour; without the
// frequent-updates entitlement iOS throttles the update budget sooner.
frequentUpdates: true,
widgets: [
{
name: "AgentActivity",
displayName: "Agent Activity",
description: "Shows the current state of active T3 Code agents.",
supportedFamilies: ["systemSmall", "systemMedium", "accessoryRectangular"],
},
],
},
];

// These aliases match the fonts' PostScript names on iOS. Register the same
// names on Android so React Native and the native composer use one set of
// family names without waiting for runtime font loading.

const config: ExpoConfig = {
name: variant.appName,
slug: "t3-code",
Expand Down Expand Up @@ -115,16 +159,59 @@ const config: ExpoConfig = {
backgroundImage: "./assets/android-icon-background.png",
monochromeImage: "./assets/android-icon-monochrome.png",
},
predictiveBackGestureEnabled: false,
// Opts into OnBackInvokedCallback-based back dispatch (Android 13+).
// JS back handling survives it via react-native's Android 16 shim plus
// withAndroidPredictiveBackCompat on Android 13-15.
predictiveBackGestureEnabled: true,
},
web: {
favicon: "./assets/favicon.png",
},
plugins: [
"expo-font",
"expo-asset",
[
"expo-font",
{
ios: {
fonts: [dmSansFonts.regular, dmSansFonts.medium, dmSansFonts.bold],
},
android: {
fonts: [
{
fontFamily: "DMSans-Regular",
fontDefinitions: [{ path: dmSansFonts.regular, weight: 400 }],
},
{
fontFamily: "DMSans-Medium",
fontDefinitions: [{ path: dmSansFonts.medium, weight: 500 }],
},
{
fontFamily: "DMSans-Bold",
fontDefinitions: [{ path: dmSansFonts.bold, weight: 700 }],
},
],
},
},
],
"expo-secure-store",
["@clerk/expo", { theme: "./clerk-theme.json" }],
"expo-sqlite",
// appleSignIn must be gated here: withoutIosPersonalTeamCapabilities.cjs runs before
// plugins earlier in this array, so it cannot strip the entitlement Clerk would add.
["@clerk/expo", { theme: "./clerk-theme.json", appleSignIn: !isIosPersonalTeamBuild }],
"expo-web-browser",
[
"expo-quick-actions",
{
// Adaptive launcher-shortcut icon; referenced by resource name from
// the shortcut items set in src/features/shortcuts.
androidIcons: {
shortcut_icon: {
foregroundImage: "./assets/android-icon-foreground.png",
backgroundColor: "#E6F4FE",
},
},
},
],
[
"expo-camera",
{
Expand Down Expand Up @@ -164,31 +251,19 @@ const config: ExpoConfig = {
// expo-widgets' — its dangerous mod wipes ios/ExpoWidgetsTarget/ (which
// would delete the asset catalog) and its xcodeproj mod creates the widget
// target (which must exist before the compile phase can be attached).
"./plugins/withWidgetLogoAsset.cjs",
[
"expo-widgets",
{
bundleIdentifier: `${variant.iosBundleIdentifier}.widgets`,
groupIdentifier: `group.${variant.iosBundleIdentifier}`,
enablePushNotifications: true,
// Agent activity can update many times an hour; without the
// frequent-updates entitlement iOS throttles the update budget sooner.
frequentUpdates: true,
widgets: [
{
name: "AgentActivity",
displayName: "Agent Activity",
description: "Shows the current state of active T3 Code agents.",
supportedFamilies: ["systemSmall", "systemMedium", "accessoryRectangular"],
},
],
},
],
...(!isIosPersonalTeamBuild ? ["./plugins/withWidgetLogoAsset.cjs", widgetsPlugin] : []),
"./plugins/withIosSceneLifecycle.cjs",
"./plugins/withIosCrashLog.cjs",
"./plugins/withAndroidCleartextTraffic.cjs",
"./plugins/withAndroidGradleHeap.cjs",
"./plugins/withAndroidModernPopupMenu.cjs",
"./plugins/withAndroidModernAlertDialog.cjs",
"./plugins/withAndroidPredictiveBackCompat.cjs",
...(isIosPersonalTeamBuild ? ["./plugins/withoutIosPersonalTeamCapabilities.cjs"] : []),
],
extra: {
appVariant: APP_VARIANT,
iosPersonalTeamBuild: isIosPersonalTeamBuild,
relay: {
url: repoEnv.T3CODE_RELAY_URL ?? null,
},
Expand Down
12 changes: 8 additions & 4 deletions apps/mobile/eas.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
"development": {
"corepack": true,
"env": {
"APP_VARIANT": "development"
"APP_VARIANT": "development",
"NODE_OPTIONS": "--max-old-space-size=4096"
},
"channel": "development",
"developmentClient": true,
Expand All @@ -17,7 +18,8 @@
"preview": {
"corepack": true,
"env": {
"APP_VARIANT": "preview"
"APP_VARIANT": "preview",
"NODE_OPTIONS": "--max-old-space-size=4096"
},
"channel": "preview",
"environment": "preview",
Expand All @@ -27,7 +29,8 @@
"corepack": true,
"env": {
"APP_VARIANT": "preview",
"MOBILE_VERSION_POLICY": "fingerprint"
"MOBILE_VERSION_POLICY": "fingerprint",
"NODE_OPTIONS": "--max-old-space-size=4096"
},
"channel": "preview",
"environment": "preview",
Expand All @@ -40,7 +43,8 @@
"production": {
"corepack": true,
"env": {
"APP_VARIANT": "production"
"APP_VARIANT": "production",
"NODE_OPTIONS": "--max-old-space-size=4096"
},
"channel": "production",
"environment": "production",
Expand Down
19 changes: 11 additions & 8 deletions apps/mobile/global.css
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
/* Card / surface */
--color-card: #ffffff;
--color-card-alt: #f5f5f5;
--color-card-translucent: rgba(255, 255, 255, 0.94);
--color-card-translucent: rgba(255, 255, 255, 0.8);

/* Text */
--color-foreground: #262626;
Expand Down Expand Up @@ -51,6 +51,7 @@
/* Inputs */
--color-input: #ffffff;
--color-input-border: rgba(0, 0, 0, 0.1);
--color-sidebar-search: rgba(118, 118, 128, 0.12);
--color-placeholder: #a3a3a3;

/* Icons */
Expand Down Expand Up @@ -105,7 +106,7 @@
/* Card / surface */
--color-card: #171717;
--color-card-alt: #1c1c1c;
--color-card-translucent: rgba(17, 17, 17, 0.94);
--color-card-translucent: rgba(17, 17, 17, 0.8);

/* Text */
--color-foreground: #f5f5f5;
Expand Down Expand Up @@ -144,6 +145,7 @@
/* Inputs */
--color-input: #141414;
--color-input-border: rgba(255, 255, 255, 0.08);
--color-sidebar-search: rgba(118, 118, 128, 0.24);
--color-placeholder: #8e8e93;

/* Icons */
Expand Down Expand Up @@ -194,9 +196,10 @@

/* ─── Typography ────────────────────────────────────────────────────── */
@theme {
--font-sans: "DMSans_400Regular";
--font-medium: "DMSans_500Medium";
--font-bold: "DMSans_700Bold";
/* Keep these native family names aligned with app.config.ts. */
--font-sans: "DMSans-Regular";
--font-medium: "DMSans-Medium";
--font-bold: "DMSans-Bold";

/* Keep this scale aligned with src/lib/typography.ts for native style props. */
--text-3xs: 11px;
Expand All @@ -221,14 +224,14 @@

/* ─── Custom utilities ──────────────────────────────────────────────── */
@utility font-t3-medium {
font-family: "DMSans_500Medium";
font-family: var(--font-medium);
}

@utility font-t3-bold {
font-family: "DMSans_700Bold";
font-family: var(--font-bold);
}

@utility font-t3-extrabold {
font-family: "DMSans_700Bold";
font-family: var(--font-bold);
font-weight: 800;
}
4 changes: 4 additions & 0 deletions apps/mobile/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
// Installed via a side-effect import listed first so the fatal-error handler
// is in place before the rest of the app module graph evaluates.
import "./src/lib/installCrashLog";

import { registerRootComponent } from "expo";
import "react-native-gesture-handler";
import { featureFlags } from "react-native-screens";
Expand Down
9 changes: 9 additions & 0 deletions apps/mobile/metro.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ const { withUniwindConfig } = require("uniwind/metro");
/** @type {import("expo/metro-config").MetroConfig} */
const config = getDefaultConfig(__dirname);
const workspaceRoot = path.resolve(__dirname, "../..");
const escapedWorkspaceRoot = workspaceRoot.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const mobileShikiRoot = path.dirname(require.resolve("shiki/package.json", { paths: [__dirname] }));
const resolveShikiDependencyRoot = (packageName) => {
const entryPath = require.resolve(packageName, { paths: [mobileShikiRoot] });
Expand All @@ -25,6 +26,14 @@ const resolveShikiDependencyRoot = (packageName) => {
config.watchFolders = [...new Set([...(config.watchFolders ?? []), workspaceRoot])];
config.resolver = {
...config.resolver,
blockList: [
...(Array.isArray(config.resolver?.blockList)
? config.resolver.blockList
: config.resolver?.blockList
? [config.resolver.blockList]
: []),
new RegExp(`${escapedWorkspaceRoot}[/\\\\]\\.t3[/\\\\].*`),
],
extraNodeModules: {
// oxlint-disable-next-line unicorn/no-useless-fallback-in-spread
...(config.resolver?.extraNodeModules ?? {}),
Expand Down
Loading
Loading