Skip to content

Commit a6c40d6

Browse files
committed
Merge remote-tracking branch 'origin/main' into fix/desktop-update-disabled-reason
2 parents 25978d1 + 9c9ae43 commit a6c40d6

670 files changed

Lines changed: 20868 additions & 11604 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.coderabbit.config.ts‎

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,14 @@
11
import { defineConfig, type CodeRabbitContext } from "@coderabbitai/config";
22

3-
// Org members and collaborators merge their own pull requests. On anyone else's, CodeRabbit
4-
// requests changes until its comments are resolved, then approves.
3+
// Org members and collaborators can approve their own pull requests.
54
const UNGATED_AUTHORS = new Set(["OWNER", "MEMBER", "COLLABORATOR"]);
65
const isGated = ({ pr }: CodeRabbitContext) => !UNGATED_AUTHORS.has(pr?.authorAssociation ?? "");
76

87
export default defineConfig((ctx) => ({
98
reviews: {
109
high_level_summary: false,
1110
review_status: false,
12-
request_changes_workflow: isGated(ctx),
11+
request_changes_workflow: false,
1312
allow_author_approval: !isGated(ctx),
1413
auto_review: {
1514
enabled: true,

‎.github/workflows/cursor-hygiene-webhook.yml‎

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,23 @@ jobs:
2828
echo "Missing CURSOR_T3CODE_WEBHOOK_URL or CURSOR_T3CODE_WEBHOOK_AUTH — skipping."
2929
exit 0
3030
fi
31-
curl -fsS --max-time 60 -X POST "$URL" \
32-
-H "Authorization: $AUTH" \
33-
-H "Content-Type: application/json" \
34-
-H "X-GitHub-Event: ${{ github.event_name }}" \
35-
-H "X-GitHub-Delivery: ${{ github.run_id }}-${{ github.run_attempt }}" \
36-
--data-binary @"${{ github.event_path }}"
31+
for attempt in 1 2 3; do
32+
curl_status=0
33+
http_status=$(curl -fsS --max-time 60 -X POST "$URL" \
34+
--output /dev/null --write-out '%{http_code}' \
35+
-H "Authorization: $AUTH" \
36+
-H "Content-Type: application/json" \
37+
-H "X-GitHub-Event: ${{ github.event_name }}" \
38+
-H "X-GitHub-Delivery: ${{ github.run_id }}-${{ github.run_attempt }}" \
39+
--data-binary @"${{ github.event_path }}") || curl_status=$?
40+
if [ "$curl_status" -eq 0 ]; then
41+
exit 0
42+
fi
43+
if [ "$GITHUB_REF" != "refs/heads/main" ] || \
44+
[ "$curl_status" -ne 22 ] || [ "$http_status" != "400" ] || \
45+
[ "$attempt" -eq 3 ]; then
46+
exit "$curl_status"
47+
fi
48+
echo "Cursor webhook returned HTTP 400; retrying in 5 seconds ($attempt/2)."
49+
sleep 5
50+
done

‎.github/workflows/mobile-eas-production.yml‎

Lines changed: 62 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,11 @@ name: Mobile EAS Production
77
# runner, with corepack pinning pnpm 10.24 in eas.json, local == build.
88
#
99
# Every merge to main that touches the mobile app reconciles, per platform:
10-
# 1. Store builds: if the latest production build's version differs from
11-
# app.config.ts, cut a new build and submit it (TestFlight + Play internal
12-
# track). Bumping `version` is therefore all it takes to
13-
# start the next release train — the first build of a version enters
10+
# 1. Store builds: when no production build matches the current native
11+
# fingerprint, or the latest build's version differs from app.config.ts,
12+
# cut a new build and submit it (TestFlight + Play internal track). Native
13+
# changes therefore ship a binary on merge, and bumping `version` is all
14+
# it takes to start the next release train — the first build of a version enters
1415
# external-TestFlight beta review immediately, and later builds of the
1516
# same version auto-approve until that version is released. After App
1617
# Store approval, Apple closes the release train and `version` must be
@@ -21,10 +22,11 @@ name: Mobile EAS Production
2122
# fingerprint. Older binaries of the same major version with a matching
2223
# fingerprint receive it too; the major version is part of the
2324
# fingerprint (apps/mobile/fingerprint.config.js), so a new major never
24-
# reaches the previous major's store binaries over the air. When native
25-
# drift means no binary could install the update,
26-
# it is skipped and flagged in the job summary instead of published
27-
# into the void.
25+
# reaches the previous major's store binaries over the air. Until a
26+
# build for the new fingerprint finishes, the OTA is skipped rather than
27+
# published into the void.
28+
# Hold native-change PRs (the "📱 Native Change" label) and merge them as a
29+
# batch to avoid a store build per PR.
2830
# workflow_dispatch remains as a manual override for both modes (e.g. to
2931
# retry an errored build or force an OTA).
3032
# Manual v2-preview builds keep the production app identity and disable OTA.
@@ -263,61 +265,74 @@ jobs:
263265
--message "${{ inputs.message || format('Production OTA ({0})', github.sha) }}" \
264266
--non-interactive
265267
266-
# No --status filter on build:list: an in-queue/in-progress build must
267-
# count as existing, or every merge during the build window would cut a
268-
# duplicate. After an errored build, retry via workflow_dispatch
269-
# mode=build — pushes won't re-trigger it until the app version changes.
270-
- id: store_builds
271-
name: Ensure store builds exist for the current app version
268+
# One pass per platform. The fingerprint decides OTA vs store build:
269+
# - a finished build matches → publish the OTA;
270+
# - a queued/in-progress build matches → wait, the next merge after it
271+
# finishes publishes the OTA (no duplicate build per merge);
272+
# - no build matches → native drift, so cut and submit a new build.
273+
# A version bump also cuts a build even when native code is unchanged,
274+
# because only the major version is part of the fingerprint.
275+
# An errored build for this fingerprint is not retried on push (it would
276+
# rebuild on every merge); retry via workflow_dispatch mode=build.
277+
- id: reconcile
278+
name: Publish OTA or cut store build
272279
if: steps.expo-token.outputs.present == 'true' && github.event_name == 'push'
273-
continue-on-error: true
274280
working-directory: apps/mobile
275281
env:
276282
EXPO_TOKEN: ${{ secrets.EXPO_TOKEN }}
277283
run: |
278284
failed=0
279285
version="$(npx expo config --json --type public | jq -r '.version')"
280-
for platform in ios android; do
281-
latest="$(eas build:list --platform "$platform" --build-profile production --limit 1 --json --non-interactive | jq -r '.[0].appVersion // "none"')"
282-
if [ "$latest" = "$version" ]; then
283-
echo "$platform: production build for $version already exists (or is in progress)"
284-
continue
285-
fi
286-
echo "$platform: latest production build is $latest, app.config.ts says $version — building"
287-
if eas build --platform "$platform" --profile production --auto-submit --non-interactive --no-wait; then
288-
echo ":building_construction: $platform: scheduled production build and submission for $version" >> "$GITHUB_STEP_SUMMARY"
289-
else
290-
failed=1
291-
echo ":x: $platform: production build or submission failed for $version" >> "$GITHUB_STEP_SUMMARY"
292-
fi
293-
done
294-
exit "$failed"
295-
296-
- name: Publish fingerprint-gated OTA
297-
if: steps.expo-token.outputs.present == 'true' && github.event_name == 'push'
298-
working-directory: apps/mobile
299-
env:
300-
EXPO_TOKEN: ${{ secrets.EXPO_TOKEN }}
301-
run: |
302286
message="$(git log -1 --pretty=%s | head -c 120) ($(git rev-parse --short=9 HEAD))"
303287
for platform in ios android; do
304288
# eas-cli prints an environment-loaded notice to stdout before the
305289
# JSON even with --json, so discard everything before the document.
306290
hash="$(eas fingerprint:generate --platform "$platform" --environment production --json --non-interactive | sed -n '/^{/,$p' | jq -er '.hash | select(type == "string" and length > 0)')"
307-
matching="$(eas build:list --platform "$platform" --build-profile production --status finished --fingerprint-hash "$hash" --limit 1 --json --non-interactive | jq 'length')"
308-
if [ "$matching" -gt 0 ]; then
309-
eas update \
291+
matches="$(eas build:list --platform "$platform" --build-profile production --fingerprint-hash "$hash" --limit 20 --json --non-interactive)"
292+
finished="$(jq '[.[] | select(.status == "FINISHED")] | length' <<<"$matches")"
293+
active="$(jq '[.[] | select(.status == "NEW" or .status == "IN_QUEUE" or .status == "IN_PROGRESS")] | length' <<<"$matches")"
294+
errored="$(jq '[.[] | select(.status == "ERRORED")] | length' <<<"$matches")"
295+
latest_version="$(eas build:list --platform "$platform" --build-profile production --limit 1 --json --non-interactive | jq -r '.[0].appVersion // "none"')"
296+
297+
if [ "$finished" -gt 0 ]; then
298+
if eas update \
310299
--channel production \
311300
--environment production \
312301
--platform "$platform" \
313302
--message "$message" \
314-
--non-interactive
315-
echo ":white_check_mark: $platform: OTA published to production (fingerprint \`$hash\`)" >> "$GITHUB_STEP_SUMMARY"
303+
--non-interactive; then
304+
echo ":white_check_mark: $platform: OTA published to production (fingerprint \`$hash\`)" >> "$GITHUB_STEP_SUMMARY"
305+
else
306+
failed=1
307+
echo ":x: $platform: OTA publish failed (fingerprint \`$hash\`)" >> "$GITHUB_STEP_SUMMARY"
308+
fi
309+
fi
310+
311+
reason=""
312+
if [ "$finished" -eq 0 ] && [ "$active" -eq 0 ]; then
313+
if [ "$errored" -gt 0 ]; then
314+
echo ":x: $platform: the production build for fingerprint \`$hash\` errored — retry with workflow_dispatch mode=build" >> "$GITHUB_STEP_SUMMARY"
315+
failed=1
316+
continue
317+
fi
318+
reason="native fingerprint \`$hash\` has no build"
319+
elif [ "$latest_version" != "$version" ] && [ "$active" -eq 0 ]; then
320+
reason="latest production build is $latest_version, app.config.ts says $version"
321+
fi
322+
323+
if [ -z "$reason" ]; then
324+
if [ "$finished" -eq 0 ]; then
325+
echo ":hourglass: $platform: production build for fingerprint \`$hash\` is in progress — OTA resumes on the next merge after it finishes" >> "$GITHUB_STEP_SUMMARY"
326+
fi
327+
continue
328+
fi
329+
330+
echo "$platform: $reason — building"
331+
if eas build --platform "$platform" --profile production --auto-submit --non-interactive --no-wait; then
332+
echo ":building_construction: $platform: scheduled production build and submission for $version ($reason)" >> "$GITHUB_STEP_SUMMARY"
316333
else
317-
echo ":warning: $platform: no finished production build matches fingerprint \`$hash\` — OTA skipped; JS changes reach $platform only once a matching build ships" >> "$GITHUB_STEP_SUMMARY"
334+
failed=1
335+
echo ":x: $platform: production build or submission failed for $version" >> "$GITHUB_STEP_SUMMARY"
318336
fi
319337
done
320-
321-
- name: Propagate store build failure
322-
if: steps.store_builds.outcome == 'failure'
323-
run: exit 1
338+
exit "$failed"

‎.github/workflows/release-desktop.yml‎

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,39 @@ jobs:
129129
git sparse-checkout set --no-cone '/*' '!/.repos/'
130130
git -c checkout.workers=0 checkout --detach FETCH_HEAD
131131
132+
# The runner's PowerShell 7.6/.NET 10 intermittently crashes during signing:
133+
# https://github.com/dotnet/runtime/issues/131636
134+
# Put the .NET 8 release first for both workflow shells and electron-builder.
135+
- name: Use PowerShell 7.4 for Windows ARM builds
136+
if: inputs.platform == 'win' && inputs.arch == 'arm64'
137+
shell: powershell
138+
run: |
139+
$ErrorActionPreference = "Stop"
140+
$ProgressPreference = "SilentlyContinue"
141+
$version = "7.4.20"
142+
$archive = Join-Path $env:RUNNER_TEMP "PowerShell-$version-win-arm64.zip"
143+
$installDir = Join-Path $env:RUNNER_TEMP "powershell-$version-arm64"
144+
Invoke-WebRequest -UseBasicParsing `
145+
-Uri "https://github.com/PowerShell/PowerShell/releases/download/v$version/PowerShell-$version-win-arm64.zip" `
146+
-OutFile $archive
147+
$expectedHash = "67eff749d5dae357501c95a3eec1dcd93f669824e3ca2b1d2adcdecdebf04f35"
148+
if ((Get-FileHash $archive -Algorithm SHA256).Hash -ne $expectedHash) {
149+
throw "PowerShell archive checksum mismatch"
150+
}
151+
Expand-Archive -Path $archive -DestinationPath $installDir
152+
$installDir | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
153+
& "$installDir\pwsh.exe" -NoProfile -NonInteractive -Command '$PSVersionTable; [System.Runtime.InteropServices.RuntimeInformation]::ProcessArchitecture'
154+
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
155+
156+
- name: Verify Windows ARM PowerShell selection
157+
if: inputs.platform == 'win' && inputs.arch == 'arm64'
158+
shell: pwsh
159+
run: |
160+
$expectedPath = Join-Path $env:RUNNER_TEMP "powershell-7.4.20-arm64\pwsh.exe"
161+
if ((Get-Command pwsh.exe).Source -ne $expectedPath -or $PSVersionTable.PSVersion -ne [version]"7.4.20") {
162+
throw "Windows ARM builds must use the pinned PowerShell runtime"
163+
}
164+
132165
# Windows installs straight from the registry. There, linking the packages
133166
# takes the time, not downloading them, so a ~700 MB package cache did not
134167
# pay for its own restore. Across recent nightlies, restore plus install

‎apps/desktop/package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
"scripts": {
88
"ensure:electron": "node scripts/ensure-electron-runtime.mjs",
99
"start": "node scripts/start-electron.mjs",
10-
"typecheck": "tsc --noEmit",
10+
"typecheck": "tsc-rs --noEmit",
1111
"test": "vp test run --passWithNoTests",
1212
"smoke-test": "node scripts/smoke-test.mjs"
1313
},

‎apps/desktop/scripts/electron-launcher.mjs‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ const APP_BUNDLE_ID = isDevelopment
2020
? `com.t3tools.t3code.dev.${devBundleIdSuffix || "local"}`
2121
: "com.t3tools.t3code";
2222
const APP_PROTOCOL_SCHEMES = isDevelopment ? ["t3code-dev"] : ["t3code"];
23-
const LAUNCHER_VERSION = 19;
23+
const LAUNCHER_VERSION = 20;
2424
const developmentMacIconPngPath = NodePath.join(
2525
repoRoot,
2626
"assets",
@@ -280,6 +280,8 @@ function patchMainBundleInfoPlist(appBundlePath, iconPath, executableName) {
280280
for (const [key, value] of Object.entries(resolveMacBundleInfoPlistStrings(executableName))) {
281281
setPlistString(infoPlistPath, key, value);
282282
}
283+
// macOS 26 offers Messages/Mail codes in every text field unless opted out.
284+
setPlistJson(infoPlistPath, "NSAutoFillRequiresTextContentTypeForOneTimeCodeOnMac", true);
283285
setPlistJson(infoPlistPath, "CFBundleURLTypes", [
284286
{
285287
CFBundleURLName: APP_BUNDLE_ID,

‎apps/desktop/src/app/DesktopClerk.test.ts‎

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -396,7 +396,7 @@ it.effect.each(["startup", "open-url"] as const)(
396396
}).pipe(Effect.scoped),
397397
);
398398

399-
it.effect("hands a web link to the renderer and leaves other links alone", () =>
399+
it.effect("hands web and app links to the renderer and leaves other links alone", () =>
400400
Effect.gen(function* () {
401401
storageMock.mockReturnValue(storageAdapter);
402402
createClerkBridgeMock.mockReturnValue({ cleanup: vi.fn(), isPrimaryInstance: true });
@@ -420,10 +420,19 @@ it.effect("hands a web link to the renderer and leaves other links alone", () =>
420420
// macOS hands the default browser every web link.
421421
assert.strictEqual(open("https://example.com/page"), 1);
422422
assert.strictEqual(open("http://localhost:3000/"), 1);
423+
// Prompt and thread links ride the same queue; the renderer routes them in the app.
424+
assert.strictEqual(open("t3code://new?prompt=hi"), 1);
425+
assert.strictEqual(open("t3code://threads/env-1/thread-1"), 1);
423426
// Anything else is not a web page; Electron keeps its own handling.
424427
assert.strictEqual(open("mailto:[email protected]"), 0);
428+
assert.strictEqual(open("t3code://somewhere-else"), 0);
425429
yield* Effect.yieldNow;
426-
assert.deepStrictEqual(received, ["https://example.com/page", "http://localhost:3000/"]);
430+
assert.deepStrictEqual(received, [
431+
"https://example.com/page",
432+
"http://localhost:3000/",
433+
"t3code://new?prompt=hi",
434+
"t3code://threads/env-1/thread-1",
435+
]);
427436
}).pipe(
428437
Effect.provide(layerDesktopClerk(true, [], "darwin")),
429438
Effect.provideService(HostProcess.Arguments, ["t3"]),

‎apps/desktop/src/app/DesktopClerk.ts‎

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { codexAuthDeliveryUrl, readCodexAuthHandoff } from "@t3tools/shared/code
1212
import { receiveCodexAuthCallback, CodexAuthCallbackError } from "./CodexAuthCallback.ts";
1313
import * as ElectronShell from "../electron/ElectronShell.ts";
1414
import { providerAuthReturnUrl } from "@t3tools/shared/providerAuthReturnUrl";
15+
import { promptLinkAppPath } from "@t3tools/shared/promptLinks";
1516
import * as HostProcess from "@t3tools/shared/HostProcess";
1617
import { clerkFrontendApiHostnameFromPublishableKey } from "@t3tools/shared/relayAuth";
1718
import * as ElectronApp from "../electron/ElectronApp.ts";
@@ -188,16 +189,28 @@ export const make = Effect.gen(function* () {
188189
);
189190
return true;
190191
};
192+
// Prompt links and thread links (`t3code://new?…`, `t3code://threads/…`) reach the
193+
// renderer through the same queue as web links; it routes them in the app.
194+
const openAppLink = (value: string | undefined) => {
195+
if (!value || promptLinkAppPath(value) === null) return false;
196+
void runPromise(webLinks.receive(value));
197+
return true;
198+
};
191199
const args = yield* HostProcess.Arguments;
192-
args.some((value) => startProviderAuthHandoff(value));
200+
args.some((value) => startProviderAuthHandoff(value) || openAppLink(value));
193201
// As the default browser, macOS hands T3 Code every web link through the same event.
194202
const openWebLink = (url: string) => {
195203
if (!DesktopWebLinks.isWebLink(url)) return false;
196204
void runPromise(webLinks.receive(url));
197205
return true;
198206
};
199207
yield* electronApp.on("open-url", (event: { preventDefault: () => void }, url: string) => {
200-
if (startProviderAuthHandoff(url) || resumeProviderAuth(url) || openWebLink(url))
208+
if (
209+
startProviderAuthHandoff(url) ||
210+
resumeProviderAuth(url) ||
211+
openWebLink(url) ||
212+
openAppLink(url)
213+
)
201214
event.preventDefault();
202215
});
203216
// A browser opens HTML files too, which macOS hands over by path.
@@ -207,7 +220,12 @@ export const make = Effect.gen(function* () {
207220
void runPromise(webLinks.receive(NodeURL.pathToFileURL(path).href));
208221
});
209222
yield* electronApp.on("second-instance", (_event: unknown, argv: readonly string[]) => {
210-
if (argv?.some((value) => startProviderAuthHandoff(value) || resumeProviderAuth(value)))
223+
if (
224+
argv?.some(
225+
(value) =>
226+
startProviderAuthHandoff(value) || resumeProviderAuth(value) || openAppLink(value),
227+
)
228+
)
211229
return;
212230
void runPromise(
213231
Effect.gen(function* () {

0 commit comments

Comments
 (0)