Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
42f695c
Add Android mobile support
juliusmarminge Jun 27, 2026
253da1e
Checkpoint Android Ghostty terminal prototype
juliusmarminge Jun 27, 2026
f875c7d
Fix Android terminal input handling
juliusmarminge Jun 27, 2026
6c37ea1
Use Shiki for markdown code blocks
juliusmarminge Jun 27, 2026
2ae9365
fix(mobile): align Android terminal for 16KB pages (#3595)
HorusGoul Jun 29, 2026
2b34778
fix(mobile): stub Android setters for patched RNS glass-chrome props
juliusmarminge Jul 3, 2026
f9cf148
fix(mobile): use theme-aware surface for markdown tables in dark mode
maria-rcks Jul 4, 2026
9f2843b
fix(mobile): render copy button icons on Android
maria-rcks Jul 4, 2026
dd35910
feat(mobile): add copy button to chat code blocks
maria-rcks Jul 4, 2026
bdae991
fix(mobile): render project folder icons on Android with open state
maria-rcks Jul 4, 2026
9af8867
fix(mobile): render work log icons on Android
maria-rcks Jul 4, 2026
10e662d
fix(mobile): raise Gradle heap to fix D8 OOM during dex merge
shivamhwp Jul 5, 2026
e7a3d08
feat(mobile): polish Android chrome — home FAB, themed popup menus, f…
shivamhwp Jul 5, 2026
86c45a4
fix(mobile): native right-aligned checks, opaque popups, chevron subm…
shivamhwp Jul 5, 2026
759ce44
feat(mobile): make the Android new-task draft read like a thread page
shivamhwp Jul 5, 2026
f21e32b
fix(mobile): flat Android settings sections and visible row icons
shivamhwp Jul 5, 2026
f758fd6
fix(mobile): keep Appearance page cards and render environment row icons
shivamhwp Jul 5, 2026
b6ac02c
style(mobile): fix formatter and ktlint violations
shivamhwp Jul 5, 2026
ce29994
fix(deps): pin expo-modules-jsi to the patched version
shivamhwp Jul 5, 2026
901dd13
style(mobile): suppress detekt complexity findings in native module p…
shivamhwp Jul 5, 2026
4f5617d
fix(mobile): render all Android icons through the AppSymbol wrapper
shivamhwp Jul 5, 2026
e119c7f
feat(mobile): swipe-only thread rows and folder-state project headers…
shivamhwp Jul 5, 2026
98337c0
feat(mobile): push git flows and environments as full pages on Android
shivamhwp Jul 5, 2026
1c36b5b
fix(mobile): stop dev-client launches from hitting NotFound and Wi-Fi
shivamhwp Jul 5, 2026
480f60d
Improve Android collapsed composer alignment and toolbar
shivamhwp Jul 5, 2026
7ea930f
Use 36px height for collapsed composer on Android
shivamhwp Jul 5, 2026
baf7cbe
fix(mobile): archive page chrome and home header polish on Android
shivamhwp Jul 6, 2026
f38d3f1
Use form sheets for Android git flows and polish mobile UX
shivamhwp Jul 6, 2026
04bc32c
Merge upstream/main into android-dev-pr-3514
shivamhwp Jul 6, 2026
a17bf3d
fix(mobile): drop per-row overflow menu in archived threads
shivamhwp Jul 6, 2026
3f3f41f
Modernize Android alert dialogs and popup menus with app tokens
shivamhwp Jul 6, 2026
8ed80f8
Compact Android git sheet header and align card radius
shivamhwp Jul 6, 2026
f35dccd
Replace stock Android dropdowns with token-styled anchored menu
shivamhwp Jul 6, 2026
ed1e47f
Merge branch 'main' into android-dev-pr-3514
shivamhwp Jul 6, 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
3 changes: 3 additions & 0 deletions app.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"expo": {}
}
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
108 changes: 81 additions & 27 deletions apps/mobile/app.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,15 @@ 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();

if (isIosPersonalTeamBuild && !personalTeamBundleIdentifier) {
throw new Error(
"T3CODE_IOS_PERSONAL_TEAM_BUNDLE_ID is required when T3CODE_IOS_PERSONAL_TEAM=1.",
);
}

const VARIANT_CONFIG: Record<
AppVariant,
Expand Down Expand Up @@ -58,6 +67,27 @@ function resolveAppVariant(value: string | undefined): AppVariant {
}

const variant = VARIANT_CONFIG[APP_VARIANT];
const iosBundleIdentifier =
isIosPersonalTeamBuild && personalTeamBundleIdentifier
? personalTeamBundleIdentifier
: variant.iosBundleIdentifier;

const widgetsPlugin = [
"expo-widgets",
{
bundleIdentifier: `${iosBundleIdentifier}.widgets`,
groupIdentifier: `group.${iosBundleIdentifier}`,
enablePushNotifications: true,
widgets: [
{
name: "AgentActivity",
displayName: "Agent Activity",
description: "Shows the current state of active T3 Code agents.",
supportedFamilies: ["systemSmall", "systemMedium", "accessoryRectangular"],
},
],
},
] satisfies NonNullable<ExpoConfig["plugins"]>[number];

const config: ExpoConfig = {
name: variant.appName,
Expand All @@ -80,15 +110,20 @@ const config: ExpoConfig = {
ios: {
icon: variant.iosIcon,
supportsTablet: true,
bundleIdentifier: variant.iosBundleIdentifier,
// Pin code signing to the T3 Tools team so non-interactive `expo run:ios`
// does not fall back to a personal team (which cannot sign app groups,
// Sign in with Apple, or push notification entitlements).
appleTeamId: "ARK85ZXQ4Z",
associatedDomains: [
`applinks:${variant.relyingParty}`,
`webcredentials:${variant.relyingParty}`,
],
bundleIdentifier: iosBundleIdentifier,
...(isIosPersonalTeamBuild
? {}
: {
// Pin code signing to the T3 Tools team so non-interactive `expo run:ios`
// does not fall back to a personal team (which cannot sign app groups,
// Sign in with Apple, or push notification entitlements). Personal-team
// builds opt out explicitly via T3CODE_IOS_PERSONAL_TEAM=1.
appleTeamId: "ARK85ZXQ4Z",
associatedDomains: [
`applinks:${variant.relyingParty}`,
`webcredentials:${variant.relyingParty}`,
],
}),
infoPlist: {
NSAppTransportSecurity: {
NSAllowsArbitraryLoads: true,
Expand All @@ -113,9 +148,39 @@ const config: ExpoConfig = {
favicon: "./assets/favicon.png",
},
plugins: [
"expo-font",
"expo-asset",
[
"expo-font",
{
// Embeds DM Sans as native Android font resources (res/font) so
// native chrome themed by the config plugins (alert dialogs) can use
// the app typeface; JS font loading via @expo-google-fonts is
// unaffected.
android: {
fonts: [
{
fontFamily: "DM Sans",
fontDefinitions: [
{
path: "node_modules/@expo-google-fonts/dm-sans/400Regular/DMSans_400Regular.ttf",
weight: 400,
},
{
path: "node_modules/@expo-google-fonts/dm-sans/500Medium/DMSans_500Medium.ttf",
weight: 500,
},
{
path: "node_modules/@expo-google-fonts/dm-sans/700Bold/DMSans_700Bold.ttf",
weight: 700,
},
],
},
],
},
},
],
"expo-secure-store",
["@clerk/expo", { theme: "./clerk-theme.json" }],
Comment thread
cursor[bot] marked this conversation as resolved.
["@clerk/expo", { theme: "./clerk-theme.json", appleSignIn: !isIosPersonalTeamBuild }],
"expo-web-browser",
[
"expo-camera",
Expand Down Expand Up @@ -151,24 +216,13 @@ const config: ExpoConfig = {
},
],
"./plugins/withIosCocoaPodsUuidCache.cjs",
[
"expo-widgets",
{
bundleIdentifier: `${variant.iosBundleIdentifier}.widgets`,
groupIdentifier: `group.${variant.iosBundleIdentifier}`,
enablePushNotifications: true,
widgets: [
{
name: "AgentActivity",
displayName: "Agent Activity",
description: "Shows the current state of active T3 Code agents.",
supportedFamilies: ["systemSmall", "systemMedium", "accessoryRectangular"],
},
],
},
],
...(!isIosPersonalTeamBuild ? [widgetsPlugin] : []),
"./plugins/withIosSceneLifecycle.cjs",
"./plugins/withAndroidCleartextTraffic.cjs",
"./plugins/withAndroidGradleHeap.cjs",
"./plugins/withAndroidModernPopupMenu.cjs",
"./plugins/withAndroidModernAlertDialog.cjs",
...(isIosPersonalTeamBuild ? ["./plugins/withoutIosPersonalTeamCapabilities.cjs"] : []),
],
extra: {
appVariant: APP_VARIANT,
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
19 changes: 19 additions & 0 deletions apps/mobile/modules/t3-composer-editor/android/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
apply plugin: 'com.android.library'
apply plugin: 'org.jetbrains.kotlin.android'

group = 'com.t3tools.composereditor'
version = '0.0.0'

android {
namespace 'expo.modules.t3composereditor'
compileSdk rootProject.ext.compileSdkVersion

defaultConfig {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
}
}

dependencies {
implementation project(':expo-modules-core')
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" />
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package expo.modules.t3composereditor

import expo.modules.kotlin.modules.Module
import expo.modules.kotlin.modules.ModuleDefinition

class T3ComposerEditorModule : Module() {
override fun definition() = ModuleDefinition {
Name("T3ComposerEditor")

View(T3ComposerEditorView::class) {
Prop("controlledDocumentJson") { view: T3ComposerEditorView, documentJson: String ->
view.setControlledDocumentJson(documentJson)
}
Prop("themeJson") { view: T3ComposerEditorView, themeJson: String ->
view.setThemeJson(themeJson)
}
Prop("placeholder") { view: T3ComposerEditorView, placeholder: String ->
view.setPlaceholder(placeholder)
}
Prop("fontFamily") { view: T3ComposerEditorView, fontFamily: String ->
view.setFontFamily(fontFamily)
}
Prop("fontSize") { view: T3ComposerEditorView, fontSize: Double ->
view.setFontSize(fontSize.toFloat())
}
Prop("lineHeight") { view: T3ComposerEditorView, lineHeight: Double ->
view.setLineHeight(lineHeight.toFloat())
}
Prop("contentInsetVertical") { view: T3ComposerEditorView, contentInsetVertical: Double ->
view.setContentInsetVertical(contentInsetVertical.toInt())
}

Prop("singleLineCentered") { view: T3ComposerEditorView, singleLineCentered: Boolean ->
view.setSingleLineCentered(singleLineCentered)
}
Prop("editable") { view: T3ComposerEditorView, editable: Boolean ->
view.setEditable(editable)
}
Prop("scrollEnabled") { view: T3ComposerEditorView, scrollEnabled: Boolean ->

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.

🟡 Medium t3composereditor/T3ComposerEditorModule.kt:35

The scrollEnabled prop does not actually disable scrolling. setScrollEnabled only toggles editor.isVerticalScrollBarEnabled, which controls whether the scrollbar is drawn — not whether the view scrolls. When scrollEnabled={false} is passed from JS, the editor remains scrollable and only the scrollbar disappears, so the prop silently does not work. Consider disabling touch interception or overriding onTouchEvent to actually prevent scrolling when scrollEnabled is false.

Also found in 1 other location(s)

apps/mobile/modules/t3-composer-editor/android/src/main/java/expo/modules/t3composereditor/T3ComposerEditorView.kt:199

setScrollEnabled() at line 199 only assigns editor.isVerticalScrollBarEnabled. Android's setVerticalScrollBarEnabled() controls whether the scrollbar is drawn, not whether an EditText can actually scroll. When callers pass scrollEnabled=false, long composer contents can still be vertically scrolled; only the scrollbar disappears, so the exposed prop does not work on Android.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/mobile/modules/t3-composer-editor/android/src/main/java/expo/modules/t3composereditor/T3ComposerEditorModule.kt around line 35:

The `scrollEnabled` prop does not actually disable scrolling. `setScrollEnabled` only toggles `editor.isVerticalScrollBarEnabled`, which controls whether the scrollbar is drawn — not whether the view scrolls. When `scrollEnabled={false}` is passed from JS, the editor remains scrollable and only the scrollbar disappears, so the prop silently does not work. Consider disabling touch interception or overriding `onTouchEvent` to actually prevent scrolling when `scrollEnabled` is false.

Also found in 1 other location(s):
- apps/mobile/modules/t3-composer-editor/android/src/main/java/expo/modules/t3composereditor/T3ComposerEditorView.kt:199 -- `setScrollEnabled()` at line `199` only assigns `editor.isVerticalScrollBarEnabled`. Android's `setVerticalScrollBarEnabled()` controls whether the scrollbar is drawn, not whether an `EditText` can actually scroll. When callers pass `scrollEnabled=false`, long composer contents can still be vertically scrolled; only the scrollbar disappears, so the exposed prop does not work on Android.

view.setScrollEnabled(scrollEnabled)
}
Prop("autoFocus") { view: T3ComposerEditorView, autoFocus: Boolean ->
view.setAutoFocus(autoFocus)
}
Prop("autoCorrect") { view: T3ComposerEditorView, autoCorrect: Boolean ->
view.setAutoCorrect(autoCorrect)
}
Prop("spellCheck") { view: T3ComposerEditorView, spellCheck: Boolean ->
view.setSpellCheck(spellCheck)
}

Events(

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.

🟡 Medium t3composereditor/T3ComposerEditorModule.kt:48

The Events(...) declaration omits onComposerSubmit, and T3ComposerEditorView defines no corresponding EventDispatcher or hardware-keyboard handler. As a result, the onSubmit prop exposed by ComposerEditorProps is never invoked on Android — hardware-keyboard submit that works on iOS silently does nothing here. Consider adding "onComposerSubmit" to the Events list, wiring up an onComposerSubmit dispatcher in the view, and detecting the submit key combination (e.g., Enter without Shift) to fire it.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/mobile/modules/t3-composer-editor/android/src/main/java/expo/modules/t3composereditor/T3ComposerEditorModule.kt around line 48:

The `Events(...)` declaration omits `onComposerSubmit`, and `T3ComposerEditorView` defines no corresponding `EventDispatcher` or hardware-keyboard handler. As a result, the `onSubmit` prop exposed by `ComposerEditorProps` is never invoked on Android — hardware-keyboard submit that works on iOS silently does nothing here. Consider adding `"onComposerSubmit"` to the `Events` list, wiring up an `onComposerSubmit` dispatcher in the view, and detecting the submit key combination (e.g., Enter without Shift) to fire it.

"onComposerChange",
"onComposerSelectionChange",
"onComposerFocus",
"onComposerBlur",
"onComposerPasteImages",
"onComposerContentSizeChange",
)

AsyncFunction("focus") { view: T3ComposerEditorView ->
view.focusEditor()
}
AsyncFunction("blur") { view: T3ComposerEditorView ->
view.blurEditor()
}
AsyncFunction("setSelection") { view: T3ComposerEditorView, start: Int, end: Int ->
view.setSelection(start, end)
}
}
}
}
Loading
Loading