Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
4da872d
fix(desktop): Group Linux windows under the installed launcher
mwolson Sep 30, 2026
40d09e9
fix(web): Copy sole fenced messages without delimiters
mwolson Sep 22, 2026
18ba005
fix(mobile): Apply native header options only while focused
mwolson Sep 22, 2026
88929aa
fix(mobile): Retry account loading without losing navigation
mwolson Sep 22, 2026
007f9b4
fix(mobile): Keep custom answers owned by the displayed request
mwolson Sep 22, 2026
8197f20
fix(clients): Prefer last-used models over project defaults
mwolson Sep 22, 2026
c6c22ba
fix(server): Keep each thread's provider event log in its own file
mwolson Sep 29, 2026
9819940
fix(orchestrator): Steer compatible active model selections
mwolson Sep 23, 2026
32c37ae
fix(orchestrator): Safely admit execution after uncertain selection
mwolson Sep 24, 2026
e3c06fa
fix(mobile): Ignore child-only history in Home empty state
mwolson Sep 24, 2026
8878be3
fix(mobile): Label background work as Waiting
mwolson Sep 24, 2026
ff879c7
fix(mobile): Surface response fork failures
mwolson Sep 24, 2026
06e5681
fix(mobile): Import legacy model options without replacing choices
mwolson Sep 24, 2026
b9e090b
fix(clients): Present legacy wakes as notifications
mwolson Sep 24, 2026
42a3fe0
fix(orchestrator): Preserve error occurrences through session cleanup
mwolson Sep 25, 2026
cc6ec4b
fix(pi): Fail turns that end on an unresolved abort
mwolson Sep 25, 2026
5fe2c77
fix(orchestrator): Settle inherited work when provider starts fail
mwolson Sep 25, 2026
38471b9
fix(orchestrator): Recover cancelled deliveries only on explicit rearm
mwolson Sep 25, 2026
b232d1c
fix(orchestrator): Recover delegated completions stranded at settlement
mwolson Sep 25, 2026
373ebe5
fix(orchestrator): Batch compatible queued Codex command completions
mwolson Sep 25, 2026
fd3d586
feat(orchestrator): Page capability catalogs on demand
mwolson Sep 25, 2026
ffd7883
feat(orchestrator): Start threads in known projects with scoped access
mwolson Sep 25, 2026
339aa2b
test(acp): Accept either shell exit status for a missing wrapped command
mwolson Sep 26, 2026
88b8810
fix(mobile): Link bundled Oniguruma and React Native's fbjni in Andro…
mwolson Sep 26, 2026
2dff93e
feat(server): report OpenCode 2 usage, context windows, models and hi…
juliusmarminge Sep 30, 2026
8517e59
feat(server): OpenCode 2 approvals, questions and every runtime mode
juliusmarminge Sep 30, 2026
7f19b4a
feat(server): OpenCode 2 subagents and background wake-ups
juliusmarminge Sep 30, 2026
a0d1bbb
feat(server): OpenCode 2 native steering, fork and rollback
juliusmarminge Sep 30, 2026
4e6fe5c
feat(server): OpenCode 2 compaction, plan mode and workspace inventory
juliusmarminge Sep 30, 2026
c9c1391
feat(server): OpenCode 2 text generation, MCP injection and restart r…
juliusmarminge Oct 1, 2026
2676d08
fix(opencode): Let OpenCode 2 background wake-ups pass continuation a…
mwolson Oct 1, 2026
828fb9f
Join the main contributions into the CTM contributions
mwolson Oct 1, 2026
bac2e3f
fix(mobile): Omit team capabilities for personal iOS builds
mwolson Sep 23, 2026
8ef1920
fix(desktop): Keep the Clerk token file owner-only
mwolson Sep 29, 2026
1778426
test(desktop): Keep WSL busy-runtime fixtures visible when sh is bash
mwolson Sep 29, 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
54 changes: 54 additions & 0 deletions apps/desktop/src/app/ClerkTokenStorage.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import * as NodeServices from "@effect/platform-node/NodeServices";
import { assert, describe, it } from "@effect/vitest";
import { isHostWindows } from "@t3tools/shared/hostProcess";
import * as Effect from "effect/Effect";
import * as FileSystem from "effect/FileSystem";
import { vi } from "vite-plus/test";

vi.mock("electron", () => ({
safeStorage: {
isEncryptionAvailable: () => true,
encryptString: (value: string) => Buffer.from(value),
decryptString: (value: Buffer) => value.toString(),
},
}));

import { storage } from "@clerk/electron/storage";

const fileMode = (path: string) =>
Effect.gen(function* () {
const fileSystem = yield* FileSystem.FileSystem;
const info = yield* fileSystem.stat(path);
return info.mode & 0o777;
});

describe("Clerk token storage", () => {
it.effect("writes the token file readable and writable only by its owner", () =>
Effect.gen(function* () {
if (yield* isHostWindows) return;
const fileSystem = yield* FileSystem.FileSystem;
const stateDir = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-clerk-tokens-" });

yield* Effect.promise(async () => {
await storage({ path: stateDir }).setItem("__clerk_client_jwt", "t");
});

assert.equal(yield* fileMode(`${stateDir}/clerk-tokens.json`), 0o600);
}).pipe(Effect.provide(NodeServices.layer), Effect.scoped),
);

it.effect("tightens a token file left world-writable by an earlier version", () =>
Effect.gen(function* () {
if (yield* isHostWindows) return;
const fileSystem = yield* FileSystem.FileSystem;
const stateDir = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-clerk-tokens-" });
const tokenFile = `${stateDir}/clerk-tokens.json`;
yield* fileSystem.writeFileString(tokenFile, "{}");
yield* fileSystem.chmod(tokenFile, 0o666);

storage({ path: stateDir });

assert.equal(yield* fileMode(tokenFile), 0o600);
}).pipe(Effect.provide(NodeServices.layer), Effect.scoped),
);
});
42 changes: 25 additions & 17 deletions apps/desktop/src/wsl/DesktopWslEnvironment.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,28 @@ const runShell = (script: string) => {

const sh = (value: string) => `'${value.replaceAll("'", "'\\''")}'`;

// Polls for up to five seconds until a background holder has taken effect.
const awaitHolder = (condition: string) =>
`i=0; until ${condition}; do i=$((i + 1)); [ "$i" -lt 500 ] || exit 1; sleep 0.01; done`;

// Holds a cache busy the way a running server does: the holder's argv[0] is the
// runtime entry, which the prune and install scripts look for in /proc cmdline.
// It reads a pipe the script keeps open on fd 7, so it exits with the script
// instead of outliving it on a timer.
const holdRuntimeBusy = (entry: string) =>
[
`exec 7> >(exec -a ${entry} cat >/dev/null 2>&1)`,
awaitHolder(`grep -qF -- ${entry} /proc/[0-9]*/cmdline 2>/dev/null`),
].join("\n");

// Holds a cache's install lock the way a concurrent install does, and likewise
// releases it when the script exits.
const holdInstallLock = (lock: string) =>
[
`exec 6> >(exec 9> ${lock}; flock -x 9; cat >/dev/null 2>&1)`,
awaitHolder(`! flock -n ${lock} true`),
].join("\n");

const readField = (stdout: string, field: string) => {
const line = stdout.split("\n").find((candidate) => candidate.startsWith(`${field}:`));
if (line === undefined) throw new Error(`missing ${field} in fixture output: ${stdout}`);
Expand Down Expand Up @@ -712,9 +734,7 @@ describe.skipIf(posixShellRunner === null)("WSL runtime install script (executed
`runtime_root=${sh(fixture.runtimeRoot)}`,
`runtime_parent=${sh(fixture.runtimeParent)}`,
'rm "$runtime_root/.t3code-wsl-runtime-ready"',
'sh -c "sleep 30" "$runtime_root/t3" >/dev/null 2>&1 &',
"active_pid=$!",
"sleep 0.1",
holdRuntimeBusy('"$runtime_root/t3"'),
fixture.installScript(),
'stale=$(find "$runtime_parent" -maxdepth 1 -type d -name ".sha256-*.stale.*" -print -quit)',
'test -n "$stale"',
Expand All @@ -723,8 +743,6 @@ describe.skipIf(posixShellRunner === null)("WSL runtime install script (executed
"export HOME",
buildWslRuntimePruneScript(fixture.runtimeId),
'test ! -e "$stale"',
"kill $active_pid",
"wait $active_pid 2>/dev/null || true",
].join("\n"),
);

Expand All @@ -750,15 +768,8 @@ describe.skipIf(posixShellRunner === null)("WSL runtime install script (executed
'touch -d "4 minutes ago" "$runtime_parent/sha256-active"',
'touch -d "3 minutes ago" "$runtime_parent/sha256-old"',
'touch -d "2 minutes ago" "$runtime_parent/sha256-locked"',
'sh -c "sleep 30" "$runtime_parent/sha256-active/t3" >/dev/null 2>&1 &',
"active_pid=$!",
"(",
' exec 9> "$runtime_parent/.sha256-locked.install.lock"',
" flock -x 9",
" sleep 30",
") >/dev/null 2>&1 &",
"lock_pid=$!",
"sleep 0.1",
holdRuntimeBusy('"$runtime_parent/sha256-active/t3"'),
holdInstallLock('"$runtime_parent/.sha256-locked.install.lock"'),
`HOME="$home"`,
"export HOME",
buildWslRuntimePruneScript("sha256-current"),
Expand All @@ -769,9 +780,6 @@ describe.skipIf(posixShellRunner === null)("WSL runtime install script (executed
'test -d "$runtime_parent/versions"',
'test ! -e "$runtime_parent/sha256-old"',
'test ! -e "$runtime_parent/sha256-markerless"',
"kill $active_pid $lock_pid",
"wait $active_pid 2>/dev/null || true",
"wait $lock_pid 2>/dev/null || true",
'rm -rf "$work"',
].join("\n"),
);
Expand Down
191 changes: 191 additions & 0 deletions apps/mobile/app.config.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
import type { ExpoConfig } from "expo/config";
import { afterEach, describe, expect, it, vi } from "vite-plus/test";

const { loadRepoEnv } = vi.hoisted(() => ({
loadRepoEnv: vi.fn<() => Record<string, string | undefined>>(),
}));

vi.mock("../../scripts/lib/public-config.ts", () => ({ loadRepoEnv }));

afterEach(() => {
vi.resetModules();
loadRepoEnv.mockReset();
});

const variants = [
{ value: undefined, name: "T3 Code", suffix: "", policy: "fingerprint" },
{ value: "production", name: "T3 Code", suffix: "", policy: "fingerprint" },
{ value: "development", name: "T3 Code Dev", suffix: ".dev", policy: "appVersion" },
{ value: "preview", name: "T3 Code Preview", suffix: ".preview", policy: "fingerprint" },
] as const;

const personalBundleIdentifier = "com.example.t3code.personal";

describe("mobile app configuration", () => {
for (const variant of variants) {
describe(variant.value ?? "default release", () => {
it("retains the standard identity and team capabilities by default", async () => {
const config = await evaluateConfig({ APP_VARIANT: variant.value });
const bundleIdentifier = `com.t3tools.t3code${variant.suffix}`;

expect(config.name).toBe(variant.name);
expect(config.scheme).toBe(`t3code${variant.suffix.replace(".", "-")}`);
expect(config.runtimeVersion).toEqual({ policy: variant.policy });
expect(config.ios).toMatchObject({
bundleIdentifier,
appleTeamId: "ARK85ZXQ4Z",
associatedDomains: ["applinks:clerk.t3.codes", "webcredentials:clerk.t3.codes"],
entitlements: {
"keychain-access-groups": [`$(AppIdentifierPrefix)${bundleIdentifier}`],
},
});
expect(config.android?.package).toBe(bundleIdentifier);
expect(config.extra?.iosPersonalTeamBuild).toBe(false);
expect(plugin(config, "@clerk/expo")).toEqual([
"@clerk/expo",
{ theme: "./clerk-theme.json", appleSignIn: true },
]);
expect(plugin(config, "expo-widgets")).toEqual([
"expo-widgets",
expect.objectContaining({
bundleIdentifier: `${bundleIdentifier}.widgets`,
groupIdentifier: `group.${bundleIdentifier}`,
}),
]);
expect(plugin(config, "expo-sharing")).toEqual([
"expo-sharing",
expect.objectContaining({
ios: expect.objectContaining({
enabled: true,
extensionBundleIdentifier: `${bundleIdentifier}.sharing`,
appGroupId: `group.${bundleIdentifier}`,
}),
}),
]);
expect(plugin(config, "./plugins/withShareExtensionDisplayName.cjs")).toBeDefined();
expect(plugin(config, "./plugins/withoutIosPersonalTeamCapabilities.cjs")).toBeUndefined();
});

it.each(["", "0", "true"])(
"requires explicit opt-in, not flag %j or a bundle override alone",
async (flag) => {
const standard = await evaluateConfig({ APP_VARIANT: variant.value });
const flagOff = await evaluateConfig({
APP_VARIANT: variant.value,
T3CODE_IOS_PERSONAL_TEAM: flag,
T3CODE_IOS_PERSONAL_TEAM_BUNDLE_ID: "invalid bundle ignored without opt-in",
});
expect(flagOff).toEqual(standard);
expect(
await evaluateConfig({
APP_VARIANT: variant.value,
T3CODE_IOS_PERSONAL_TEAM_BUNDLE_ID: personalBundleIdentifier,
}),
).toEqual(standard);
},
);

it("uses the explicit personal identifier and omits unsupported team capabilities", async () => {
const standard = await evaluateConfig({ APP_VARIANT: variant.value });
const config = await evaluateConfig({
APP_VARIANT: variant.value,
T3CODE_IOS_PERSONAL_TEAM: "1",
T3CODE_IOS_PERSONAL_TEAM_BUNDLE_ID: ` ${personalBundleIdentifier} `,
});

expect(config.ios?.bundleIdentifier).toBe(personalBundleIdentifier);
expect(config.ios).not.toHaveProperty("appleTeamId");
expect(config.ios).not.toHaveProperty("associatedDomains");
expect(config.ios).not.toHaveProperty("entitlements");
const expectedIos = { ...standard.ios, bundleIdentifier: personalBundleIdentifier };
delete expectedIos.appleTeamId;
delete expectedIos.associatedDomains;
delete expectedIos.entitlements;
expect(config.ios).toEqual(expectedIos);
expect(config.extra?.iosPersonalTeamBuild).toBe(true);
expect(plugin(config, "expo-widgets")).toBeUndefined();
expect(plugin(config, "./plugins/withWidgetLogoAsset.cjs")).toBeUndefined();
expect(plugin(config, "./plugins/withShareExtensionDisplayName.cjs")).toBeUndefined();
expect(plugin(config, "@clerk/expo")).toEqual([
"@clerk/expo",
{ theme: "./clerk-theme.json", appleSignIn: false },
]);
expect(plugin(config, "expo-sharing")).toEqual([
"expo-sharing",
expect.objectContaining({
ios: expect.objectContaining({
enabled: false,
extensionBundleIdentifier: `${personalBundleIdentifier}.sharing`,
appGroupId: `group.${personalBundleIdentifier}`,
}),
}),
]);
expect(plugin(config, "./plugins/withoutIosPersonalTeamCapabilities.cjs")).toBeDefined();
});

it("leaves Android values and plugins unchanged by the iOS opt-in", async () => {
const env = {
APP_VARIANT: variant.value,
T3CODE_ANDROID_GOOGLE_SERVICES_FILE: "./fixture-google-services.json",
};
const standard = await evaluateConfig(env);
const personal = await evaluateConfig({
...env,
T3CODE_IOS_PERSONAL_TEAM: "1",
T3CODE_IOS_PERSONAL_TEAM_BUNDLE_ID: personalBundleIdentifier,
});

expect(personal.android).toEqual(standard.android);
expect(personal.android?.googleServicesFile).toBe(env.T3CODE_ANDROID_GOOGLE_SERVICES_FILE);
expect(androidPlugins(personal)).toEqual(androidPlugins(standard));
expect(plugin(personal, "expo-notifications")).toEqual(
plugin(standard, "expo-notifications"),
);
expect(plugin(personal, "expo-quick-actions")).toEqual(
plugin(standard, "expo-quick-actions"),
);
});
});
}

it.each([undefined, "", " ", "singleword", "com..example", "com.example_bad", "com.example.*"])(
"rejects missing or invalid personal bundle identifier %j",
async (bundleIdentifier) => {
await expect(
evaluateConfig({
T3CODE_IOS_PERSONAL_TEAM: "1",
T3CODE_IOS_PERSONAL_TEAM_BUNDLE_ID: bundleIdentifier,
}),
).rejects.toThrow(
"T3CODE_IOS_PERSONAL_TEAM_BUNDLE_ID must be a reverse-DNS identifier such as com.example.t3code when T3CODE_IOS_PERSONAL_TEAM=1.",
);
},
);
});

async function evaluateConfig(env: Record<string, string | undefined>) {
const originalEnv = process.env;
// Evaluate the real module afresh without reading repository env files or host values.
process.env = { NODE_ENV: "test" };
loadRepoEnv.mockReturnValue(env);
vi.resetModules();
try {
return (await import("./app.config.ts")).default;
} finally {
process.env = originalEnv;
}
}

function plugin(config: ExpoConfig, name: string) {
return config.plugins?.find((entry) => (Array.isArray(entry) ? entry[0] : entry) === name);
}

function androidPlugins(config: ExpoConfig) {
return config.plugins?.flatMap<{ name: string; android?: unknown }>((entry) => {
if (typeof entry === "string") {
return entry.startsWith("./plugins/withAndroid") ? [{ name: entry }] : [];
}
if (entry[0] === undefined || entry[1]?.android === undefined) return [];
return [{ name: entry[0], android: entry[1].android }];
});
}
20 changes: 12 additions & 8 deletions apps/mobile/app.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -240,14 +240,18 @@ const config: ExpoConfig = {
// 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}`,
],
entitlements: {
"keychain-access-groups": [`$(AppIdentifierPrefix)${variant.iosBundleIdentifier}`],
},
...(isIosPersonalTeamBuild
? {}
: {
appleTeamId: "ARK85ZXQ4Z",
associatedDomains: [
`applinks:${variant.relyingParty}`,
`webcredentials:${variant.relyingParty}`,
],
entitlements: {
"keychain-access-groups": [`$(AppIdentifierPrefix)${variant.iosBundleIdentifier}`],
},
}),
infoPlist: {
NSAppTransportSecurity: {
NSAllowsArbitraryLoads: true,
Expand Down
8 changes: 7 additions & 1 deletion apps/mobile/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
AppearancePreferencesProvider,
useAppearancePreferences,
} from "./features/settings/appearance/AppearancePreferencesProvider";
import { readRememberedNavigationState, rememberNavigationState } from "./navigationPersistence";
import { RootStack } from "./Stack";
import { appAtomRegistry } from "./state/atom-registry";
import { OverlayPortalHost } from "./components/OverlayPortal";
Expand Down Expand Up @@ -87,7 +88,12 @@ function AppContent() {
the system is in dark mode. */}
<View style={{ flex: 1 }}>
<IncomingShareProvider>
<Navigation linking={appLinking} theme={navigationTheme} />
<Navigation
initialState={readRememberedNavigationState()}
linking={appLinking}
onStateChange={rememberNavigationState}
theme={navigationTheme}
/>
</IncomingShareProvider>
<ConfirmDialogHost />
<ThreadArrangementHost />
Expand Down
Loading