Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
7246b0a
Improve live activity routing and diagnostics (#3685)
juliusmarminge Jul 7, 2026
97f833b
Prevent Add Project sheet from collapsing on relayout (#3759)
juliusmarminge Jul 7, 2026
e8be5c6
Use variant-specific splash icons in mobile app (#3762)
juliusmarminge Jul 7, 2026
04b305f
Fix Expo widget asset wiring order (#3763)
juliusmarminge Jul 7, 2026
d74cfc9
Extend Done display to 15 minutes and show up to 5 Live Activity bann…
juliusmarminge Jul 7, 2026
0dc262e
Clear VCS presentation state on finish (#3764)
juliusmarminge Jul 7, 2026
7d61eae
Lead with the outcome when no agents are active in the Live Activity …
juliusmarminge Jul 7, 2026
2816b06
Add T3 Connect onboarding for mobile and web (#3765)
juliusmarminge Jul 7, 2026
caeaf30
Revert "Add T3 Connect onboarding for mobile and web" (#3776)
juliusmarminge Jul 7, 2026
13d3bc2
Expose Clerk Google sign-in env vars to Expo (#3772)
juliusmarminge Jul 7, 2026
bf7817e
Set up Cursor Cloud dev environment (web + Android toolchain) (#3755)
t3dotgg Jul 7, 2026
55e7e79
Revert "Revert "Add T3 Connect onboarding for mobile and web"" (#3777)
juliusmarminge Jul 7, 2026
afe116c
Use rounded depth logo for production splash screen (#3780)
juliusmarminge Jul 7, 2026
224f269
fix(release): stage pnpm 11 allowBuilds for desktop installs (#3781)
juliusmarminge Jul 7, 2026
87ddd54
chore(sync): regenerate pnpm-lock.yaml after upstream cherry-pick
wizzoapp[bot] Jul 8, 2026
e78fad5
chore(sync): take upstream react-native-screens patch verbatim
wizzoapp[bot] Jul 8, 2026
3ab3490
fix(desktop): package ffi-rs sidecars in staged artifacts
wizzoapp[bot] Jul 8, 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
72 changes: 72 additions & 0 deletions .cursor/rules/cursor-cloud.mdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
---
description: Cursor Cloud environment setup and run notes for T3 Code (Cursor-specific; not for other editors/CI)
alwaysApply: true
---

# Cursor Cloud specific instructions

These notes apply only to Cursor Cloud agents (kept out of the shared `AGENTS.md` since T3 Code is
developed in many places). They cover the pre-provisioned VM environment and non-obvious run caveats.

## Toolchain

Pre-installed and wired into login shells (`~/.bashrc`/`~/.profile`): Node 24 (via nvm), the global
`vp` (Vite+) CLI, and its bundled pnpm. `vp` is the package manager + task runner for this repo
(`vp i`, `vp check`, `vp run typecheck`, `vp test`, `vp run --filter <pkg> <script>`).

- Node gotcha: the base image also ships an older Node on `/exec-daemon` that wins on `PATH` in
non-login shells. Run commands in a login shell (`bash -lc '...'`) or prepend
`$HOME/.nvm/versions/node/v24.13.1/bin` so Node 24 (`engines.node: ^24.13.1`) is used.
- `vp check` also runs the formatter (not just lint); markdown edits must be formatter-clean. Run
`vp check --fix` before committing.

## Running the web app

- `npm run dev` (from repo root) starts contracts (watch), web (Vite, port 5733), and the server
(`node --watch`, port 13773) together, auto-wiring `VITE_HTTP_URL`/`VITE_WS_URL`. Use
`npm run dev:server` / `npm run dev:web` for one side. If base ports are taken it auto-offsets to
the next free pair, so read the actual ports from stdout. Avoid production `build`/`start` in dev.
- Auth/pairing: the server is unauthenticated by default and prints a pairing URL to stdout on startup
(e.g. `http://localhost:5733/pair#token=XXXX`). Open that URL in the browser to pair before the web
UI can talk to the server. Server state (SQLite, auth, projects) lives under `~/.t3`
(`T3CODE_HOME`); multiple `npm run dev` instances bind different ports but share that same DB.
- Agent providers are external CLIs (`codex`, `claude`, `cursor-agent`, `opencode`) probed on `PATH`.
Without one installed the UI loads but no agent can run. Claude Code
(`npm i -g @anthropic-ai/claude-code`) works headlessly using `ANTHROPIC_API_KEY` from the
environment (the server forwards `process.env` to the child; no interactive `claude auth login`
needed). Provider probing runs at startup and refreshes roughly every 5 minutes — restart the dev
server to pick up a newly installed provider CLI immediately.

## Android native builds (apps/mobile)

The Android toolchain is pre-installed and wired into login shells: OpenJDK 17
(`JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64`; the base image's default `java` is 21, so builds must
use `JAVA_HOME`), and the Android SDK at `$HOME/Android/Sdk` (`ANDROID_HOME`/`ANDROID_SDK_ROOT`) with
platform-tools, `platforms;android-36`, `build-tools;36.0.0`, `ndk;27.1.12297006`, and `cmake;3.22.1`.
Gradle auto-downloads any additional pinned SDK packages on demand.

- Generate the native project first: from `apps/mobile`,
`EXPO_NO_GIT_STATUS=1 APP_VARIANT=development npx expo prebuild --clean --platform android`
(the `android/` folder is generated, not committed). RN 0.85 defaults: compileSdk/targetSdk 36,
minSdk 24, NDK `27.1.12297006` (see `react-native/gradle/libs.versions.toml`).
- Build with Gradle from `apps/mobile/android`, e.g. `./gradlew :app:assembleDebug`. First build is
slow (Kotlin + C++/NDK codegen). Limit ABIs to speed things up, e.g.
`-PreactNativeArchitectures=arm64-v8a`. Verified working end-to-end on a native module:
`./gradlew :react-native-worklets:assembleDebug` compiles Kotlin + C++ and emits an AAR + `.so`s.
- No emulator: this VM has no `/dev/kvm` (nested virtualization), so a hardware-accelerated Android
emulator will not run. Use a physical device (`adb`) or EAS cloud builds (`vp run eas:android:*`) to
actually run the app. The full IDE GUI is unnecessary here — the SDK/NDK/Gradle toolchain that
Android Studio bundles is what is installed.
- Known blocker for the full-app `:app:assembleDebug` (pre-existing, app-level — not the environment):
`:react-native-screens:compileDebugKotlin` fails because `patches/react-native-screens@4.25.2.patch`
adds new codegen props (`subtitle`, `largeSubtitle`, `navigationItemStyle`,
`headerCenterBarButtonItems`, `headerToolbarItems`) to the JS spec + iOS native code but includes no
Android Kotlin setters, so the generated `ScreenStackHeaderConfigManagerInterface` is unimplemented.
The mobile README documents only iOS for local builds and routes Android through EAS. Fixing local
Android app builds requires an app/patch change (add the Android Kotlin setters), which is out of
scope for environment setup.

## Known flaky tests (unrelated to setup)

- `apps/server/src/git/GitManager.test.ts` (cross-repo PR metadata, can time out at 12s).
- `apps/server/src/provider/Layers/ProviderRegistry.test.ts` (codex binaryPath re-probe ordering).
5 changes: 4 additions & 1 deletion .github/workflows/mobile-eas-production.yml
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,10 @@ jobs:
with:
eas-version: latest
token: ${{ secrets.EXPO_TOKEN }}
packager: pnpm
# npm, not pnpm: this only installs eas-cli into the action's own
# tool dir, and pnpm 11 hard-fails that install on dtrace-provider's
# ignored build script (no allowBuilds config outside the repo).
packager: npm

- name: Pull production environment variables
if: steps.expo-token.outputs.present == 'true'
Expand Down
26 changes: 24 additions & 2 deletions apps/mobile/app.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ const VARIANT_CONFIG: Record<
readonly appName: string;
readonly scheme: string;
readonly iosIcon: string;
readonly splashIcon: string;
readonly iosBundleIdentifier: string;
readonly androidPackage: string;
readonly relyingParty?: string;
Expand All @@ -27,6 +28,7 @@ const VARIANT_CONFIG: Record<
appName: "T3 Code Dev",
scheme: "t3code-dev",
iosIcon: "./assets/icon-composer-dev.icon",
splashIcon: "./assets/splash-icon-dev.png",
iosBundleIdentifier: "com.t3tools.t3code.dev",
androidPackage: "com.t3tools.t3code.dev",
relyingParty: "clerk.t3.codes",
Expand All @@ -35,6 +37,7 @@ const VARIANT_CONFIG: Record<
appName: "T3 Code Preview",
scheme: "t3code-preview",
iosIcon: "./assets/icon-composer-prod.icon",
splashIcon: "./assets/splash-icon-prod.png",
iosBundleIdentifier: "com.t3tools.t3code.preview",
androidPackage: "com.t3tools.t3code.preview",
relyingParty: "clerk.t3.codes",
Expand All @@ -43,6 +46,7 @@ const VARIANT_CONFIG: Record<
appName: "T3 Code",
scheme: "t3code",
iosIcon: "./assets/icon-composer-prod.icon",
splashIcon: "./assets/splash-icon-prod.png",
iosBundleIdentifier: "com.t3tools.t3code",
androidPackage: "com.t3tools.t3code",
relyingParty: "clerk.t3.codes",
Expand Down Expand Up @@ -171,12 +175,12 @@ const config: ExpoConfig = {
[
"expo-splash-screen",
{
image: "./assets/splash-icon.png",
image: variant.splashIcon,
resizeMode: "contain",
backgroundColor: "#ffffff",
imageWidth: 220,
dark: {
image: "./assets/splash-icon.png",
image: variant.splashIcon,
backgroundColor: "#0a0a0a",
},
},
Expand All @@ -195,12 +199,21 @@ const config: ExpoConfig = {
},
],
"./plugins/withIosCocoaPodsUuidCache.cjs",
// Must be listed BEFORE expo-widgets: same-type mods run last-registered-
// first, so registering earlier makes this plugin's mods run AFTER
// 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,
},
],
Expand All @@ -218,6 +231,15 @@ const config: ExpoConfig = {
publishableKey: repoEnv.EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY ?? null,
jwtTemplate: repoEnv.EXPO_PUBLIC_CLERK_JWT_TEMPLATE ?? null,
},
// Native Google sign-in credentials. @clerk/expo reads these from `extra`
// under their exact env-var names (not nested), and its config plugin reads
// the iOS URL scheme at prebuild to register it in Info.plist.
// Unset values must be omitted (not null): the public manifest serializes
// null to {}, which is truthy and would defeat Clerk's fallback checks.
EXPO_PUBLIC_CLERK_GOOGLE_WEB_CLIENT_ID: repoEnv.EXPO_PUBLIC_CLERK_GOOGLE_WEB_CLIENT_ID,
EXPO_PUBLIC_CLERK_GOOGLE_IOS_CLIENT_ID: repoEnv.EXPO_PUBLIC_CLERK_GOOGLE_IOS_CLIENT_ID,
EXPO_PUBLIC_CLERK_GOOGLE_ANDROID_CLIENT_ID: repoEnv.EXPO_PUBLIC_CLERK_GOOGLE_ANDROID_CLIENT_ID,
EXPO_PUBLIC_CLERK_GOOGLE_IOS_URL_SCHEME: repoEnv.EXPO_PUBLIC_CLERK_GOOGLE_IOS_URL_SCHEME,
observability: {
tracesUrl: repoEnv.EXPO_PUBLIC_OTLP_TRACES_URL ?? "https://api.axiom.co/v1/traces",
tracesDataset: repoEnv.EXPO_PUBLIC_OTLP_TRACES_DATASET ?? null,
Expand Down
Binary file added apps/mobile/assets/splash-icon-prod.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions apps/mobile/assets/widget/T3Mark.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
81 changes: 81 additions & 0 deletions apps/mobile/plugins/lib/addWidgetAssetCatalog.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
"use strict";

// Bundle the widget asset catalog into an app-extension (widget) target.
//
// expo-widgets generates the widget target without a Resources build phase, and
// hand-adding a PBXResourcesBuildPhase does NOT get picked up — xcodebuild's
// planner never schedules `actool` for it (verified: even a full re-plan skips
// it). So instead we add a shell-script phase that runs `actool` directly and
// drops the compiled Assets.car into the extension bundle. Marked
// alwaysOutOfDate so the build system always runs it.
//
// Idempotent across re-runs. Returns true when it added the phase, false when
// it was already present. Throws when the target does not exist — that means
// this ran before expo-widgets created the target (plugin ordering bug) and
// silently skipping would ship a widget without its assets.

const PHASE_NAME = "Compile Widget Assets";

// Compiles ExpoWidgetsTarget/Assets.xcassets into the extension's resources dir.
// Uses only Xcode-provided build settings so it works for device + simulator.
const ACTOOL_SCRIPT = [
"set -e",
'CATALOG="${SRCROOT}/ExpoWidgetsTarget/Assets.xcassets"',
'if [ ! -d "$CATALOG" ]; then',
' echo "error: widget asset catalog not found at $CATALOG (expo-widgets wiped it? check plugin ordering in app.config.ts)"',
" exit 1",
"fi",
'DEST="${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}"',
'mkdir -p "$DEST"',
'xcrun actool "$CATALOG" --compile "$DEST" --platform "${PLATFORM_NAME}" --minimum-deployment-target "${IPHONEOS_DEPLOYMENT_TARGET:-16.0}" --output-format human-readable-text',
].join("\n");

function stripComments(map) {
const out = {};
for (const key of Object.keys(map || {})) {
if (key.endsWith("_comment")) continue;
out[key] = map[key];
}
return out;
}

function findByName(map, name) {
for (const [uuid, value] of Object.entries(stripComments(map))) {
if (value && value.name === name) return { uuid, value };
}
return null;
}

/**
* @param {import('xcode').XcodeProject} proj
* @param {{ targetName: string }} opts
*/
function addWidgetAssetCatalog(proj, opts) {
const objects = proj.hash.project.objects;
const target = findByName(objects.PBXNativeTarget, opts.targetName);
if (!target) {
throw new Error(
`addWidgetAssetCatalog: target "${opts.targetName}" not found — ` +
"withWidgetLogoAsset must be registered before expo-widgets so its " +
"xcodeproj mod runs after the widget target is created.",
);
}

const phases = target.value.buildPhases || [];
const existing = objects.PBXShellScriptBuildPhase || {};
const already = Object.entries(stripComments(existing)).some(
([uuid, value]) =>
value && value.name === `"${PHASE_NAME}"` && phases.some((p) => p.value === uuid),
);
if (already) return false;

const { uuid } = proj.addBuildPhase([], "PBXShellScriptBuildPhase", PHASE_NAME, target.uuid, {
shellPath: "/bin/sh",
shellScript: ACTOOL_SCRIPT,
});
// Always run: input-analysis is exactly what skipped the Resources phase.
objects.PBXShellScriptBuildPhase[uuid].alwaysOutOfDate = 1;
return true;
}

module.exports = { addWidgetAssetCatalog };
68 changes: 68 additions & 0 deletions apps/mobile/plugins/withWidgetLogoAsset.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
"use strict";

// Ships the branded T3 mark to the Live Activity / widget extension.
//
// expo-widgets generates ExpoWidgetsTarget without a Resources build phase and
// has no asset support, so this plugin (a) writes an SVG template image set into
// the generated widget asset catalog and (b) wires that catalog into the widget
// target with an actool build phase.
//
// ORDERING: must be listed BEFORE "expo-widgets" in the plugins array. Expo
// chains same-type mods so the last-registered runs FIRST; registering this
// plugin earlier makes its mods run AFTER expo-widgets' mods. That matters
// twice: expo-widgets' dangerous mod rmSync's ios/ExpoWidgetsTarget/ (deleting
// any catalog written before it), and its xcodeproj mod is what creates the
// widget target. Listed after expo-widgets, both steps silently no-op on a
// fresh prebuild — which is how prod build 8 shipped without the logo.

const path = require("path");
const fs = require("fs");
const { withDangerousMod, withXcodeProject } = require("expo/config-plugins");
const { addWidgetAssetCatalog } = require("./lib/addWidgetAssetCatalog.cjs");

const TARGET_NAME = "ExpoWidgetsTarget";
const CATALOG_NAME = "Assets.xcassets";
const IMAGE_SET = "T3Mark.imageset";
const SVG_NAME = "T3Mark.svg";

const CATALOG_CONTENTS = JSON.stringify({ info: { author: "expo", version: 1 } }, null, 2) + "\n";
const IMAGE_SET_CONTENTS =
JSON.stringify(
{
images: [{ idiom: "universal", filename: SVG_NAME }],
info: { author: "expo", version: 1 },
properties: {
"preserves-vector-representation": true,
"template-rendering-intent": "template",
},
},
null,
2,
) + "\n";

function withAssetFiles(config) {
return withDangerousMod(config, [
"ios",
(cfg) => {
const source = path.join(cfg.modRequest.projectRoot, "assets", "widget", SVG_NAME);
const catalogDir = path.join(cfg.modRequest.platformProjectRoot, TARGET_NAME, CATALOG_NAME);
const imageSetDir = path.join(catalogDir, IMAGE_SET);
fs.mkdirSync(imageSetDir, { recursive: true });
fs.writeFileSync(path.join(catalogDir, "Contents.json"), CATALOG_CONTENTS);
fs.writeFileSync(path.join(imageSetDir, "Contents.json"), IMAGE_SET_CONTENTS);
fs.copyFileSync(source, path.join(imageSetDir, SVG_NAME));
return cfg;
},
]);
}

function withAssetWiring(config) {
return withXcodeProject(config, (cfg) => {
addWidgetAssetCatalog(cfg.modResults, { targetName: TARGET_NAME });
return cfg;
});
}

module.exports = function withWidgetLogoAsset(config) {
return withAssetWiring(withAssetFiles(config));
};
30 changes: 30 additions & 0 deletions apps/mobile/scripts/wire-widget-asset-catalog.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
"use strict";

// One-off: apply the widget asset-catalog wiring to the already-generated
// ios/ project so the current build compiles ExpoWidgetsTarget/Assets.xcassets
// without a full `expo prebuild`. The durable equivalent lives in
// plugins/withWidgetLogoAsset.cjs and runs on prebuild.

const path = require("path");
const fs = require("fs");

const xcodePath = require.resolve("xcode", {
paths: [
require.resolve("@expo/config-plugins", { paths: [require.resolve("expo/package.json")] }),
],
});
const xcode = require(xcodePath);
const { addWidgetAssetCatalog } = require("../plugins/lib/addWidgetAssetCatalog.cjs");

const pbxprojPath = path.join(__dirname, "..", "ios", "T3CodeDev.xcodeproj", "project.pbxproj");
const proj = xcode.project(pbxprojPath);
proj.parseSync();

const added = addWidgetAssetCatalog(proj, { targetName: "ExpoWidgetsTarget" });

if (added) {
fs.writeFileSync(pbxprojPath, proj.writeSync());
console.log("Added widget asset-compile phase to ExpoWidgetsTarget.");
} else {
console.log("No change: phase already present.");
}
Loading
Loading