Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 1 addition & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -110,11 +110,8 @@ jobs:
- name: Install dependencies
run: pnpm install --frozen-lockfile

- name: Build web application
run: pnpm build:web

- name: Verify unsigned Android debug application
run: pnpm --filter @t4-code/mobile check:android:debug
run: pnpm --filter @t4-code/companion check:android:native

verify:
name: verify
Expand Down
13 changes: 5 additions & 8 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -208,11 +208,8 @@ jobs:
- name: Install dependencies
run: pnpm install --frozen-lockfile

- name: Build web application for Android
run: pnpm build:web

- name: Verify unsigned Android application
run: pnpm --filter @t4-code/mobile check:android:debug
run: pnpm --filter @t4-code/companion check:android:native

- name: Restore and validate Android release signing key
shell: bash
Expand Down Expand Up @@ -249,14 +246,14 @@ jobs:
T4_ANDROID_KEY_ALIAS: ${{ secrets.T4_ANDROID_KEY_ALIAS }}
T4_ANDROID_KEY_PASSWORD: ${{ secrets.T4_ANDROID_KEY_PASSWORD }}
run: |
pnpm --filter @t4-code/mobile build:android:release
pnpm --filter @t4-code/companion build:android:release

- name: Inspect signed Android APK
shell: bash
run: |
set -euo pipefail
apk="apps/mobile/android/app/build/outputs/apk/release/app-release.apk"
metadata="apps/mobile/android/app/build/outputs/apk/release/output-metadata.json"
apk="apps/companion/android/app/build/outputs/apk/release/app-release.apk"
metadata="apps/companion/android/app/build/outputs/apk/release/output-metadata.json"
build_tools="${ANDROID_SDK_ROOT:-$ANDROID_HOME}/build-tools/36.0.0"
node scripts/inspect-android-release.mjs \
--apk "$apk" \
Expand All @@ -271,7 +268,7 @@ jobs:
run: |
mkdir -p artifacts
cp \
apps/mobile/android/app/build/outputs/apk/release/app-release.apk \
apps/companion/android/app/build/outputs/apk/release/app-release.apk \
"artifacts/T4-Code-${VERSION}-android.apk"
(cd artifacts && sha256sum T4-Code-* > SHA256SUMS-android.txt)

Expand Down
43 changes: 43 additions & 0 deletions apps/companion/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files

# dependencies
node_modules/

# Expo
.expo/
dist/
web-build/
expo-env.d.ts

# Native
.kotlin/
*.orig.*
*.jks
*.p8
*.p12
*.key
*.mobileprovision

# Metro
.metro-health-check*

# debug
npm-debug.*
yarn-debug.*
yarn-error.*

# macOS
.DS_Store
*.pem

# local env files
.env*.local

# typescript
*.tsbuildinfo

example

# generated native folders
/ios
/android
1 change: 1 addition & 0 deletions apps/companion/.vscode/extensions.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{ "recommendations": ["expo.vscode-expo-tools"] }
7 changes: 7 additions & 0 deletions apps/companion/.vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"editor.codeActionsOnSave": {
"source.fixAll": "explicit",
"source.organizeImports": "explicit",
"source.sortMembers": "explicit"
}
}
5 changes: 5 additions & 0 deletions apps/companion/AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# T4 Companion build notes

- Keep the checkout path free of spaces for native iOS work. Expo SDK 57's generated `EXConstants` build phase truncates a spaced workspace path and fails with `No such file or directory`.
- Run authoritative checks with the repository's Node 24 toolchain.
- The `ios/` and `android/` directories are generated by Expo prebuild and stay ignored. Change `app.json` or config plugins instead of hand-editing generated native files.
21 changes: 21 additions & 0 deletions apps/companion/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
The MIT License (MIT)

Copyright (c) 2015-present 650 Industries, Inc. (aka Expo)

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
102 changes: 102 additions & 0 deletions apps/companion/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
# T4 Companion

T4 Companion is the iPhone and Android front end for a T4 Code host running on
your Mac. It remembers one stable Tailscale address, reconnects when the app
returns to the foreground, and shows sessions, transcripts, questions, and
approval requests without exposing the host to the public internet.

## One-time phone setup

1. Install Tailscale on the Mac and phone, then sign into the same Tailnet on
both devices. A Tailnet is the private network Tailscale creates for you.
2. On the Mac, open T4 Code's **Phone Setup** card and select
**Companion app**.
3. Scan the QR code with the phone and open the `t4companion://` link.
4. Enter the pairing code shown by T4 Code if the host asks for one.

The app saves the Mac's `.ts.net` address in the phone's protected storage.
That address stays the same unless the Mac is renamed in Tailscale, so normal
use does not require scanning again.

## Development

Use Node 24 and install the monorepo dependencies from the repository root:

```bash
mise exec node@24.17.0 -- pnpm install
```

Run the readiness check before generating native projects. It explains any
missing prerequisite and exits successfully only when the selected platform is
ready:

```bash
mise exec node@24.17.0 -- pnpm --filter @t4-code/companion doctor:ios
mise exec node@24.17.0 -- pnpm --filter @t4-code/companion doctor:android
```

This checkout path must not contain spaces when building the native iOS app.
Expo SDK 57 otherwise generates a broken native file path.

For a physical iPhone, one command now discovers the connected phone, Apple
development team, and stable Tailnet address. It builds a signed Release app,
installs it, opens it, and passes the address into the app:

```bash
mise exec node@24.17.0 -- pnpm --filter @t4-code/companion ios:device
```

The iPhone must be unlocked for the final automatic launch. The command waits
up to one minute if it locks during the build. To retry only the deep link
without rebuilding or reinstalling, run:

```bash
mise exec node@24.17.0 -- pnpm --filter @t4-code/companion ios:device --launch-only
```

Use `ios:device --help` for explicit device, Apple team, Tailnet URL, and
install-without-launch options.

For a simulator, run the iOS development build:

```bash
cd apps/companion
mise exec node@24.17.0 -- pnpm exec expo run:ios
```

The first physical-iPhone build requires an Apple Development certificate and
a provisioning profile for `com.lycaonsolutions.t4code`. Xcode can create both
after the project owner signs in under **Xcode Settings > Accounts**. This is a
one-time Apple Developer account change; later local installs reuse it.

For Android, the matching one-command workflow discovers a connected phone or
starts the `T4_Pixel_API_36` emulator, builds a Release APK for that device's
processor, installs it, opens it, and passes the stable Tailnet address:

```bash
mise exec node@24.17.0 -- pnpm --filter @t4-code/companion android:device
```

To reopen the installed app without rebuilding, run:

```bash
mise exec node@24.17.0 -- pnpm --filter @t4-code/companion android:device --launch-only
```

Use `android:device --help` for an explicit device, virtual-device name,
Tailnet URL, build reuse, and install-without-launch options. The Android phone
or emulator must be able to reach the Mac's Tailnet address. Run
`doctor:android` first if Java 17, the Android SDK, platform tools, or a target
device are missing.

Run focused checks:

```bash
mise exec node@24.17.0 -- pnpm --filter @t4-code/companion typecheck
mise exec node@24.17.0 -- pnpm --filter @t4-code/companion test
mise exec node@24.17.0 -- pnpm --filter @t4-code/companion lint
```

The `ios/` and `android/` directories are generated by Expo and are ignored.
Make permanent native configuration changes in `app.json` or an Expo config
plugin instead of editing generated files.
62 changes: 62 additions & 0 deletions apps/companion/app.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
{
"expo": {
"name": "T4 Companion",
"slug": "t4-companion",
"version": "0.1.29",
"orientation": "portrait",
"icon": "./assets/images/icon.png",
"scheme": "t4companion",
"userInterfaceStyle": "dark",
"backgroundColor": "#090b0f",
"ios": {
"icon": "./assets/images/icon.png",
"bundleIdentifier": "com.lycaonsolutions.t4code",
"supportsTablet": false,
"buildNumber": "10029"
},
"android": {
"package": "com.lycaonsolutions.t4code",
"adaptiveIcon": {
"backgroundColor": "#090b0f",
"foregroundImage": "./assets/images/android-icon-foreground.png",
"monochromeImage": "./assets/images/android-icon-monochrome.png"
},
"predictiveBackGestureEnabled": true,
"versionCode": 10029
},
"web": {
"output": "static",
"favicon": "./assets/images/favicon.png"
},
"plugins": [
"expo-router",
[
"expo-splash-screen",
{
"backgroundColor": "#090b0f",
"image": "./assets/images/splash-icon.png",
"imageWidth": 76
}
],
[
"expo-notifications",
{
"defaultChannel": "attention"
}
],
"expo-secure-store",
[
"expo-speech-recognition",
{
"microphonePermission": "Allow T4 Companion to transcribe voice input.",
"speechRecognitionPermission": "Allow T4 Companion to turn speech into prompt text."
}
],
"./plugins/with-android-release-signing.cjs"
],
"experiments": {
"typedRoutes": true,
"reactCompiler": true
}
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/companion/assets/images/favicon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/companion/assets/images/icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/companion/assets/images/splash-icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
24 changes: 24 additions & 0 deletions apps/companion/eas.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"cli": {
"version": ">= 19.1.0",
"appVersionSource": "remote"
},
"build": {
"development-simulator": {
"developmentClient": true,
"distribution": "internal",
"ios": {
"simulator": true
}
},
"preview": {
"distribution": "internal"
},
"production": {
"autoIncrement": true
}
},
"submit": {
"production": {}
}
}
9 changes: 9 additions & 0 deletions apps/companion/eslint.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
const expoConfig = require("eslint-config-expo/flat");
const { defineConfig } = require("eslint/config");

module.exports = defineConfig([
expoConfig,
{
ignores: ["android/**", "ios/**"],
},
]);
20 changes: 20 additions & 0 deletions apps/companion/metro.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
const { getDefaultConfig } = require("expo/metro-config");

const config = getDefaultConfig(__dirname);

// T4's shared protocol packages publish TypeScript source while using the
// standards-friendly `.js` import spelling expected after compilation. Metro
// does not map those relative imports back to `.ts` by default, so retry the
// same request without the output extension when the importer is TypeScript.
config.resolver.resolveRequest = (context, moduleName, platform) => {
const isTypeScriptImporter = /\.[cm]?tsx?$/.test(context.originModulePath);
const isRelativeJavaScriptImport = moduleName.startsWith(".") && moduleName.endsWith(".js");

if (isTypeScriptImporter && isRelativeJavaScriptImport) {
return context.resolveRequest(context, moduleName.slice(0, -3), platform);
}

return context.resolveRequest(context, moduleName, platform);
};

module.exports = config;
Loading