Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
0232932
fix(clients): prompt links find projects cloned from git.cafe (#26042)
juliusmarminge Oct 27, 2026
740de0e
fix(server): retry Codex capacity failures with empty input (#25611)
Yash-Singh1 Oct 27, 2026
07242ab
fix(server): recover handoffs when target context is exhausted (#25610)
Yash-Singh1 Oct 27, 2026
4914cdd
fix(web): chips drop their pill and stay readable on every theme (#25…
flamboh Oct 27, 2026
73acddc
fix(mobile): switching pull request tabs no longer jumps the page (#2…
flamboh Oct 27, 2026
570d4b0
feat(mobile): report crashes to Axiom with symbolicated stacks (#19093)
gabrielelpidio Oct 27, 2026
5808469
fix(web): chip palette test follows the single design (#26131)
juliusmarminge Oct 27, 2026
1b1ad04
feat(server): /mcp narrows a client's modes from T3-Mode-Limit (#16653)
juliusmarminge Oct 27, 2026
888e444
feat(server): t3_thread_launch takes clientRequestId (#16654)
juliusmarminge Oct 27, 2026
4005759
feat(server): link to another environment (#16655)
juliusmarminge Oct 27, 2026
cb98850
feat(server): agents use linked environments through any T3 tool (#16…
juliusmarminge Oct 27, 2026
6918c35
feat(server): work a linked environment starts stays within its link …
juliusmarminge Oct 27, 2026
a6676f9
feat(web,mobile): linked environments in Connections (#16698)
juliusmarminge Oct 27, 2026
594f1ce
feat(server): delegate_task to a linked environment (#16719)
juliusmarminge Oct 27, 2026
8b14660
feat(server,web,mobile): an agent sets up a link to another environme…
juliusmarminge Oct 27, 2026
5e9d564
feat(server,web,mobile): @-mention a machine, and linked environments…
juliusmarminge Oct 27, 2026
c3424e4
merge: integrate upstream agent-link stack
ScriptedAlchemy Oct 27, 2026
0621be0
fix(clients): open remote subagent approvals on their machine
ScriptedAlchemy Oct 27, 2026
e802114
fix: remove duplicate linked-environment instructions
ScriptedAlchemy Oct 27, 2026
f8a0028
fix: address full-suite validation failures after upstream merge
ScriptedAlchemy Oct 27, 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
19 changes: 19 additions & 0 deletions .github/workflows/mobile-eas-production.yml
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,7 @@ jobs:
--environment production \
--platform ${{ inputs.platform }} \
--message "$OTA_MESSAGE" \
--emit-metadata \
--non-interactive
# Reconcile each version separately because the major is in the fingerprint.
Expand Down Expand Up @@ -313,8 +314,11 @@ jobs:
--environment production \
--platform "$platform" \
--message "$message" \
--emit-metadata \
--non-interactive; then
echo ":white_check_mark: $platform/$profile: OTA published to production (fingerprint \`$hash\`)" >> "$GITHUB_STEP_SUMMARY"
# The next target's export replaces dist; keep this one for the map upload.
rm -rf "dist-$platform-$profile" && mv dist "dist-$platform-$profile"
else
failed=1
echo ":x: $platform/$profile: OTA publish failed (fingerprint \`$hash\`)" >> "$GITHUB_STEP_SUMMARY"
Expand Down Expand Up @@ -354,3 +358,18 @@ jobs:
fi
done
exit "$failed"
# Lets the relay symbolicate crash stacks from the updates published
# above. A step of its own so the R2 credentials reach only this script,
# not `eas update` and the dependencies it runs. Never fails the job.
- name: Upload OTA source maps
if: ${{ !cancelled() && steps.expo-token.outputs.present == 'true' }}
working-directory: apps/mobile
env:
MOBILE_SOURCEMAPS_R2: ${{ secrets.MOBILE_SOURCEMAPS_R2 }}
run: |
for dir in dist dist-*; do
if [ -f "$dir/eas-update-metadata.json" ]; then
node scripts/upload-sourcemaps.mts ota "$dir"
fi
done
1 change: 1 addition & 0 deletions apps/desktop/scripts/verify-preload-bundle.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ const executeBundle = (source, sandboxModules) => {
{
process: sandboxProcess,
require: requireSandboxModule,
window: new EventTarget(),
},
{
filename: "desktop-preload.cjs",
Expand Down
10 changes: 10 additions & 0 deletions apps/desktop/scripts/verify-preload-bundle.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,16 @@ const validPreload = `
`;

describe("desktop preload bundle verifier", () => {
it("accepts preloads that register window lifecycle listeners", () => {
assert.doesNotThrow(() =>
verifyPreloadBundle(`
${validPreload}
window.addEventListener("DOMContentLoaded", () => {}, { once: true });
window.addEventListener("resize", () => {});
`),
);
});

it("rejects required API names that only appear in strings", () => {
assert.throws(
() =>
Expand Down
7 changes: 7 additions & 0 deletions apps/mobile/app.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@ const runtimeVersionPolicy =
const personalTeamBundleIdentifier = repoEnv.T3CODE_IOS_PERSONAL_TEAM_BUNDLE_ID?.trim();
const IOS_BUNDLE_IDENTIFIER_PATTERN = /^[A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)+$/;

// Compiled into the native project. Without a relay, Observe's native side could dispatch to
// Expo's own ingest before observe.ts turns it off; `.invalid` never resolves (RFC 6761).
const observeEndpointUrl = repoEnv.T3CODE_RELAY_URL?.trim()
? new URL("/v1/mobile/observe", repoEnv.T3CODE_RELAY_URL.trim()).toString()
: "https://observe.invalid";

const fromRepoRoot = (relativePath: string) => `../../${relativePath}`;
const androidAdaptiveForeground = "./assets/android-icon-foreground.png";

Expand Down Expand Up @@ -450,6 +456,7 @@ const config: ExpoConfig = {
},
eas: {
projectId: "d763fcb8-d37c-41ea-a773-b54a0ab4a454",
observe: { endpointUrl: observeEndpointUrl },
},
},
owner: "pingdotgg",
Expand Down
3 changes: 2 additions & 1 deletion apps/mobile/eas.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,8 @@
"corepack": true,
"env": {
"APP_VARIANT": "production",
"NODE_OPTIONS": "--max-old-space-size=4096"
"NODE_OPTIONS": "--max-old-space-size=4096",
"SOURCEMAP_FILE": "/tmp/t3code-ios-main.jsbundle.map"
},
"channel": "production",
"environment": "production",
Expand Down
2 changes: 2 additions & 0 deletions apps/mobile/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
// Must stay first: the render counter wraps the DevTools hook before any React commit.
import "./src/features/development/render-counter-entry";
// Next, so errors thrown while the rest of the app loads are recorded too.
import "./src/features/observability/observe";
import { registerRootComponent } from "expo";
import "react-native-gesture-handler";
import { LogBox } from "react-native";
Expand Down
3 changes: 3 additions & 0 deletions apps/mobile/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
"measure-renders": "node scripts/measure-renders.mts",
"generate": "node --disable-warning=MODULE_TYPELESS_PACKAGE_JSON scripts/generate-uniwind-themes.mts",
"generate:highlights": "node scripts/generate-highlights.mts",
"eas-build-on-success": "node scripts/upload-sourcemaps.mts build",
"test": "vp test run --project unit --project react",
"typecheck": "tsc --noEmit"
},
Expand Down Expand Up @@ -103,6 +104,7 @@
"expo-linking": "~58.0.13",
"expo-network": "~58.0.3",
"expo-notifications": "~58.1.5",
"expo-observe": "~58.0.15",
"expo-paste-input": "^0.1.15",
"expo-quick-actions": "^6.0.2",
"expo-secure-store": "~58.0.2",
Expand Down Expand Up @@ -145,6 +147,7 @@
"@types/mdast": "^4.0.4",
"@types/react": "catalog:",
"@types/react-dom": "catalog:",
"aws4fetch": "1.0.20",
"babel-preset-expo": "~58.0.17",
"jsdom": "^30.0.1",
"tailwindcss": "^4.0.0",
Expand Down
165 changes: 165 additions & 0 deletions apps/mobile/scripts/sourcemap-upload.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
import * as NodeFS from "node:fs";
import * as NodePath from "node:path";
import { mobileSourceMapKey, type MobilePlatform } from "@t3tools/shared/mobileSourceMaps";
import { AwsClient } from "aws4fetch";

export interface SourceMapUpload {
readonly key: string;
readonly path: string;
}

/** The `MOBILE_SOURCEMAPS_R2` secret: the relay stack's `mobileSourceMapsUploadCredentials`. */
interface UploadCredentials {
readonly endpoint: string;
readonly bucketName: string;
readonly accessKeyId: string;
readonly secretAccessKey: string;
}

const isPlatform = (value: unknown): value is MobilePlatform =>
value === "ios" || value === "android";

const readJson = (path: string): unknown => JSON.parse(NodeFS.readFileSync(path, "utf8"));

const isRecord = (value: unknown): value is Record<string, unknown> =>
typeof value === "object" && value !== null && !Array.isArray(value);

export function parseCredentials(value: string | undefined): UploadCredentials | undefined {
if (!value?.trim()) return undefined;
let parsed: unknown;
try {
parsed = JSON.parse(value);
} catch {
// JSON.parse's message quotes the input, which here is the secret.
throw new Error("MOBILE_SOURCEMAPS_R2 is not valid JSON.");
}
if (!isRecord(parsed)) throw new Error("MOBILE_SOURCEMAPS_R2 is not a JSON object.");
const { endpoint, bucketName, accessKeyId, secretAccessKey } = parsed;
if (
typeof endpoint !== "string" ||
typeof bucketName !== "string" ||
typeof accessKeyId !== "string" ||
typeof secretAccessKey !== "string"
) {
throw new Error(
"MOBILE_SOURCEMAPS_R2 needs endpoint, bucketName, accessKeyId and secretAccessKey.",
);
}
return { endpoint, bucketName, accessKeyId, secretAccessKey };
}

/** One map per update `eas update --emit-metadata` published, next to the bundle `metadata.json` names. */
export function otaSourceMapUploads(distDir: string): ReadonlyArray<SourceMapUpload> {
const metadata = readJson(NodePath.join(distDir, "metadata.json"));
const easMetadata = readJson(NodePath.join(distDir, "eas-update-metadata.json"));
const fileMetadata = isRecord(metadata) ? metadata.fileMetadata : undefined;
const updates = isRecord(easMetadata) ? easMetadata.updates : undefined;
if (!isRecord(fileMetadata) || !Array.isArray(updates)) {
throw new Error(`${distDir} is missing Expo export or EAS update metadata.`);
}
return updates.flatMap((update): Array<SourceMapUpload> => {
if (!isRecord(update) || !isPlatform(update.platform) || typeof update.id !== "string") {
return [];
}
const platformFiles = fileMetadata[update.platform];
const bundle = isRecord(platformFiles) ? platformFiles.bundle : undefined;
if (typeof bundle !== "string") return [];
return [
{
key: mobileSourceMapKey(update.platform, "ota", update.id),
path: NodePath.join(distDir, `${bundle}.map`),
},
];
});
}

/** Android's Gradle plugin writes the map under the release variant; iOS to `SOURCEMAP_FILE` (eas.json). */
export function buildSourceMapUpload(
env: Readonly<Record<string, string | undefined>>,
projectDir: string,
): SourceMapUpload {
const platform = env.EAS_BUILD_PLATFORM;
const buildId = env.EAS_BUILD_ID;
if (!isPlatform(platform) || !buildId) {
throw new Error("EAS_BUILD_PLATFORM and EAS_BUILD_ID are only set on EAS Build.");
}
if (platform === "ios") {
if (!env.SOURCEMAP_FILE) throw new Error("SOURCEMAP_FILE is not set for this iOS build.");
return { key: mobileSourceMapKey("ios", "build", buildId), path: env.SOURCEMAP_FILE };
}
const variantsDir = NodePath.join(projectDir, "android/app/build/generated/sourcemaps/react");
const variant = NodeFS.existsSync(variantsDir)
? NodeFS.readdirSync(variantsDir).find((name) => /release$/iu.test(name))
: undefined;
if (!variant) throw new Error(`No release source map under ${variantsDir}.`);
return {
key: mobileSourceMapKey("android", "build", buildId),
path: NodePath.join(variantsDir, variant, "index.android.bundle.map"),
};
}

/**
* EAS writes the app's debug symbols next to the `.ipa`, so native frames in iOS crash
* reports can be read with `atos`. Undefined when the build left none.
*/
export function buildDsymUpload(
env: Readonly<Record<string, string | undefined>>,
projectDir: string,
): SourceMapUpload | undefined {
if (env.EAS_BUILD_PLATFORM !== "ios" || !env.EAS_BUILD_ID) return undefined;
const buildDir = NodePath.join(projectDir, "ios/build");
const dsym = NodeFS.existsSync(buildDir)
? NodeFS.readdirSync(buildDir).find((name) => name.endsWith(".app.dSYM.zip"))
: undefined;
if (!dsym) return undefined;
return { key: `ios/dsym/${env.EAS_BUILD_ID}.zip`, path: NodePath.join(buildDir, dsym) };
}

/** Only what the relay reads: source text stays out of the bucket, and the rest is dead weight. */
function stripToMappings(text: string): string {
const map: unknown = JSON.parse(text);
if (!isRecord(map)) throw new Error("Source map is not a JSON object.");
const { version, sources, sourceRoot, mappings } = map;
return JSON.stringify({ version, sources, sourceRoot, mappings });
}

export function uploadSourceMap(
credentials: UploadCredentials,
upload: SourceMapUpload,
): Promise<number> {
return putObject(
credentials,
upload.key,
stripToMappings(NodeFS.readFileSync(upload.path, "utf8")),
"application/json",
);
}

export function uploadDsym(credentials: UploadCredentials, upload: SourceMapUpload) {
const body = new Uint8Array(NodeFS.readFileSync(upload.path));
return putObject(credentials, upload.key, body, "application/zip");
}

async function putObject(
credentials: UploadCredentials,
key: string,
body: string | Uint8Array<ArrayBuffer>,
contentType: string,
): Promise<number> {
const client = new AwsClient({
accessKeyId: credentials.accessKeyId,
secretAccessKey: credentials.secretAccessKey,
service: "s3",
region: "auto",
});
const url = `${credentials.endpoint.replace(/\/$/u, "")}/${credentials.bucketName}/${key}`;
const response = await client.fetch(url, {
method: "PUT",
body,
headers: { "content-type": contentType },
});
if (!response.ok) {
throw new Error(`R2 answered ${response.status}: ${(await response.text()).slice(0, 500)}`);
}
return body.length;
}
Loading
Loading