Skip to content
Merged
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
0615fd7
Add middle-click close for right panel tabs (#3161)
huxcrux Jun 30, 2026
51ea084
fix: warm WSL before preflight in WSL-only backend mode (#3588)
UtkarshUsername Jun 30, 2026
9d66b10
Add Claude Sonnet 5 as the default Claude model (#3620)
juliusmarminge Jun 30, 2026
7b9eef7
Restore the ultrathink frame border effect (#3625)
juliusmarminge Jul 1, 2026
6d35a87
fix(dev): Fix electron dev launch and add test (#3662)
t3dotgg Jul 3, 2026
32d17d3
Add adaptive split-view layout for iPad/mobile workspace (#3514)
juliusmarminge Jul 3, 2026
5cda815
fix(mobile): compile patched native pods from source on EAS (#3667)
juliusmarminge Jul 3, 2026
cabc93b
Make the thread composer read as elevated liquid glass (#3668)
juliusmarminge Jul 3, 2026
a7e43b2
Upgrade Vite Plus and enable bundled dev opt-in (#3679)
juliusmarminge Jul 3, 2026
4ac094f
Surface pending tasks in mobile home and draft flow (#3670)
juliusmarminge Jul 3, 2026
e26e55f
fix(mobile): combined test branch — scroll, back-swipe, thread lists,…
t3dotgg Jul 4, 2026
6009720
Add repo-root favicon.svg so t3 code shows its own icon (#3683)
t3dotgg Jul 4, 2026
482d562
Load thread snapshots over HTTP before live sync (#3719)
juliusmarminge Jul 6, 2026
32e7844
Fix mobile legend anchor under automatic iOS insets (#3684)
juliusmarminge Jul 6, 2026
d8e0ba3
Improve live activity routing and diagnostics (#3685)
juliusmarminge Jul 7, 2026
f9727d0
Prevent Add Project sheet from collapsing on relayout (#3759)
juliusmarminge Jul 7, 2026
0b62e2e
Use variant-specific splash icons in mobile app (#3762)
juliusmarminge Jul 7, 2026
bdf5aed
Fix Expo widget asset wiring order (#3763)
juliusmarminge Jul 7, 2026
6e42231
Extend Done display to 15 minutes and show up to 5 Live Activity bann…
juliusmarminge Jul 7, 2026
4414f92
Clear VCS presentation state on finish (#3764)
juliusmarminge Jul 7, 2026
4eb1b57
Lead with the outcome when no agents are active in the Live Activity …
juliusmarminge Jul 7, 2026
fa6073e
Add T3 Connect onboarding for mobile and web (#3765)
juliusmarminge Jul 7, 2026
cd8b9fc
Revert "Add T3 Connect onboarding for mobile and web" (#3776)
juliusmarminge Jul 7, 2026
0e00408
Expose Clerk Google sign-in env vars to Expo (#3772)
juliusmarminge Jul 7, 2026
b9cc8d6
Set up Cursor Cloud dev environment (web + Android toolchain) (#3755)
t3dotgg Jul 7, 2026
dad0889
Revert "Revert "Add T3 Connect onboarding for mobile and web"" (#3777)
juliusmarminge Jul 7, 2026
7778a1c
Use rounded depth logo for production splash screen (#3780)
juliusmarminge Jul 7, 2026
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
056810e
merge upstream main
tarik02 Jul 12, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
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-preview.yml
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,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 preview environment variables
if: steps.expo-token.outputs.present == 'true'
Expand Down
113 changes: 113 additions & 0 deletions .github/workflows/mobile-eas-production.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
name: Mobile EAS Production

# Production builds and OTA updates run from CI (Linux) — never from a laptop.
# Under the fingerprint runtime-version policy the fingerprint must be computed
# in the same OS/pnpm as the EAS build; a macOS `eas build` computes a different
# fingerprint (platform-specific deps + pnpm version) and errors. On this Linux
# runner, with corepack pinning pnpm 10.24 in eas.json, local == build.
on:
workflow_dispatch:
inputs:
mode:
description: "build (+ auto-submit to TestFlight) or update (OTA)"
required: true
type: choice
default: build
options:
- build
- update
platform:
description: "Target platform"
required: true
type: choice
default: ios
options:
- ios
- android
- all
message:
description: "OTA update message (mode=update only)"
required: false
type: string

jobs:
production:
name: EAS Production ${{ inputs.mode }}
runs-on: blacksmith-8vcpu-ubuntu-2404
permissions:
contents: read
env:
APP_VARIANT: production
NODE_OPTIONS: --max-old-space-size=8192
steps:
- id: expo-token
name: Check for EXPO_TOKEN
env:
EXPO_TOKEN: ${{ secrets.EXPO_TOKEN }}
run: |
if [ -n "$EXPO_TOKEN" ]; then
echo "present=true" >> "$GITHUB_OUTPUT"
else
echo "present=false" >> "$GITHUB_OUTPUT"
echo "EXPO_TOKEN is not available; skipping EAS production job."
fi
- name: Checkout
if: steps.expo-token.outputs.present == 'true'
uses: actions/checkout@v6
with:
fetch-depth: 0

- name: Setup Vite+
if: steps.expo-token.outputs.present == 'true'
uses: voidzero-dev/setup-vp@v1
with:
node-version-file: package.json
cache: true
run-install: true

- name: Expose pnpm
if: steps.expo-token.outputs.present == 'true'
run: |
pnpm_version="$(node --print "require('./package.json').packageManager.split('@').pop()")"
vp_pnpm_bin="$HOME/.vite-plus/package_manager/pnpm/$pnpm_version/pnpm/bin"
echo "$vp_pnpm_bin" >> "$GITHUB_PATH"
"$vp_pnpm_bin/pnpm" --version
- name: Setup EAS
if: steps.expo-token.outputs.present == 'true'
uses: expo/expo-github-action@v8
with:
eas-version: latest
token: ${{ secrets.EXPO_TOKEN }}
# 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'
working-directory: apps/mobile
env:
EXPO_TOKEN: ${{ secrets.EXPO_TOKEN }}
run: eas env:pull production --non-interactive

- name: Build and submit
if: steps.expo-token.outputs.present == 'true' && inputs.mode == 'build'
working-directory: apps/mobile
env:
EXPO_TOKEN: ${{ secrets.EXPO_TOKEN }}
run: eas build --platform ${{ inputs.platform }} --profile production --auto-submit --non-interactive --no-wait

- name: Publish OTA update
if: steps.expo-token.outputs.present == 'true' && inputs.mode == 'update'
working-directory: apps/mobile
env:
EXPO_TOKEN: ${{ secrets.EXPO_TOKEN }}
run: |
eas update \
--channel production \
--environment production \
--platform ${{ inputs.platform }} \
--message "${{ inputs.message || format('Production OTA ({0})', github.sha) }}" \
--non-interactive
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
1 change: 1 addition & 0 deletions .vite-hooks/pre-commit
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
vp staged
3 changes: 3 additions & 0 deletions app.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"expo": {}
}
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
19 changes: 13 additions & 6 deletions apps/desktop/scripts/electron-launcher.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -358,10 +358,7 @@ function resolveLinuxSandboxArgs(electronBinaryPath) {
}

export function resolveElectronPath() {
ensureElectronRuntime();

const require = NodeModule.createRequire(import.meta.url);
const electronBinaryPath = require("electron");
const electronBinaryPath = resolveElectronBinaryPath();

if (hostPlatform !== "darwin") {
return electronBinaryPath;
Expand All @@ -378,13 +375,23 @@ export function resolveElectronLaunchCommand(args = []) {
};
}

export function resolveElectronBinaryPath({
ensureRuntime = ensureElectronRuntime,
createRequire = NodeModule.createRequire,
moduleUrl = import.meta.url,
} = {}) {
ensureRuntime();

const require = createRequire(moduleUrl);
return require("electron");
}

export function resolveDevProtocolClient() {
if (hostPlatform !== "darwin" || !isDevelopment) {
return null;
}

const require = NodeModule.createRequire(import.meta.url);
const electronBinaryPath = require("electron");
const electronBinaryPath = resolveElectronBinaryPath();
const launcherBinaryPath = buildMacLauncher(electronBinaryPath);
return {
appBundlePath: NodePath.resolve(launcherBinaryPath, "..", "..", ".."),
Expand Down
22 changes: 21 additions & 1 deletion apps/desktop/scripts/electron-launcher.test.mjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { assert, describe, it } from "vite-plus/test";

import { makeDevelopmentLauncherScript } from "./electron-launcher.mjs";
import { makeDevelopmentLauncherScript, resolveElectronBinaryPath } from "./electron-launcher.mjs";

describe("electron development launcher", () => {
it("uses captured values only as fallbacks for a live runner environment", () => {
Expand All @@ -25,4 +25,24 @@ describe("electron development launcher", () => {
"exec '/repo/node_modules/electron/Electron' --t3code-dev-root='/repo/apps/desktop' '/repo/apps/desktop/dist-electron/main.cjs' \"$@\"",
);
});

it("repairs Electron before loading the package entrypoint", () => {
const calls = [];
const electronPath = resolveElectronBinaryPath({
ensureRuntime: () => {
calls.push("ensure");
},
createRequire: () => (specifier) => {
calls.push(`require:${specifier}`);
return "/repo/node_modules/electron/dist/Electron.app/Contents/MacOS/Electron";
},
moduleUrl: import.meta.url,
});

assert.equal(
electronPath,
"/repo/node_modules/electron/dist/Electron.app/Contents/MacOS/Electron",
);
assert.deepEqual(calls, ["ensure", "require:electron"]);
});
});
1 change: 1 addition & 0 deletions apps/desktop/src/backend/DesktopBackendConfiguration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -607,6 +607,7 @@ export const make = Effect.gen(function* () {
const backendExposure = yield* serverExposure.backendConfig;
const persistedSettings = yield* settings.get;
const shared = yield* sharedInputs;
yield* wslEnvironment.preWarm(persistedSettings.wslDistro);
return yield* resolveWslStartConfig({
...shared,
port: backendExposure.port,
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
1 change: 1 addition & 0 deletions apps/mobile/.swiftlint.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
included:
- ios/T3Code
- modules/t3-composer-editor/ios
- modules/t3-native-controls/ios
- modules/t3-terminal/ios
- modules/t3-review-diff/ios

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
Loading
Loading