Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
138 commits
Select commit Hold shift + click to select a range
03ac1f0
fix(release): stage pnpm 11 allowBuilds for desktop installs (#3781)
juliusmarminge Jul 7, 2026
e775bc6
Upgrade Clerk toolchain to latest versions (#3785)
juliusmarminge Jul 8, 2026
0c66565
fix(release): bump electron-builder so pnpm 11 deduped deps land in t…
avocardow Jul 8, 2026
18a4138
Fix desktop native optional dependency packaging (#3816)
Prgm-code Jul 9, 2026
e912765
[codex] Upgrade Clerk stack (#3821)
juliusmarminge Jul 9, 2026
3201e00
[codex] Preserve worktree metadata during branch sync (#3822)
juliusmarminge Jul 9, 2026
2250e3e
feat(client): persist offline environment data and mobile preferences…
juliusmarminge Jul 9, 2026
ca1e08b
[codex] Label max and ultra reasoning (#3824)
juliusmarminge Jul 9, 2026
843cf17
fix(mobile): embed fonts and render project favicons reliably (#3823)
juliusmarminge Jul 9, 2026
8619ef2
Show compact PR number badges in mobile thread rows (#3827)
juliusmarminge Jul 9, 2026
f61fa94
Expose mobile PR indicator labels to accessibility (#3828)
juliusmarminge Jul 9, 2026
ef943a2
Fix truncated chat error alert layout (#3899)
jakeleventhal Jul 12, 2026
619b0ec
fix(marketing): show platform-appropriate commit shortcut on the webs…
VedankPurohit Jul 12, 2026
c1ec191
[codex] Add Android mobile support (#3579)
juliusmarminge Jul 12, 2026
bfd9ca3
fix(mobile): Backport iOS runtime and chat stability fixes
mwolson Jul 11, 2026
3e7ed41
fix(mobile): Account for header in thread fit checks
mwolson Jul 12, 2026
c62877c
fix(mobile): Stabilize screenshot attachment layout
mwolson Jul 12, 2026
ca998a4
fix(mobile): Harden iOS crash capture and stack option updates
mwolson Jul 12, 2026
f347d3e
fix(mobile): Satisfy SwiftLint opening_brace in T3CrashLog
mwolson Jul 12, 2026
de8b9f6
fix(client-runtime): Heal shell archive membership on reconnect
mwolson Jul 13, 2026
666bf0a
fix(client-runtime): Apply socket shell heal authoritatively
mwolson Jul 13, 2026
292609f
fix(client-runtime): Clear socket-authoritative flag after HTTP heal
mwolson Jul 13, 2026
07c6c32
test(server): Harden CursorAdapter log wait for CI load
mwolson Jul 13, 2026
7b2a0a3
test(server): Use wall-clock waits in CursorAdapter log polling
mwolson Jul 13, 2026
ed1a5f2
fix(mobile): Keep threadScreenOptions useMemo above early returns
mwolson Jul 13, 2026
4311beb
Confirm resumed thread subscriptions (#2)
twistedgrim Jul 13, 2026
73c2c48
fix(orchestration): Gate thread resume sync marker after catch-up
mwolson Jul 13, 2026
936f67f
fix(client-runtime): Hold shell list until server heal on reconnect
mwolson Jul 13, 2026
6aabd97
fix(client-runtime): Delay shell disk paint and re-arm sync marker
mwolson Jul 13, 2026
924ad10
fix(mobile): Keep chat content above composer when keyboard opens on …
mwolson Jul 14, 2026
c68ddeb
feat(integration): Merge codex-turn-mapping into ios-fixes-main as v2…
mwolson Jul 14, 2026
03025ce
fix(mobile): Stabilize CTM iOS draft attachments, Hermes sort, and Home
mwolson Jul 12, 2026
0a2efa9
fix(client-runtime): Normalize V2 shell archive membership
mwolson Jul 13, 2026
8346525
fix(mobile): Preserve Android scroll position on keyboard close
mwolson Jul 14, 2026
2ec85a8
fix(deps): Update keyboard controller patch lockfile
mwolson Jul 14, 2026
8de254a
Use client-side fallbacks for missing project favicons (#3959)
juliusmarminge Jul 14, 2026
735240f
Skip stale working-task notifications (#3961)
juliusmarminge Jul 14, 2026
3513fa0
Prepare Android beta branding and review diff UI (#3967)
juliusmarminge Jul 14, 2026
2fdc704
perf(web): duty-cycle status animations and remove fixed noise overla…
t3dotgg Jul 15, 2026
7b928d5
fix(docs): correct CI task-runner commands in ci.md (#3990)
kridaydave Jul 15, 2026
c69120f
fix(docs): repair broken source links in architecture overview (#3991)
kridaydave Jul 15, 2026
4624426
fix(docs): replace stale codething-mvp absolute paths with repo-relat…
kridaydave Jul 15, 2026
d359789
docs: Add T3 Code Legal Docs (#3972)
juliusmarminge Jul 15, 2026
ecb35f7
Fix Legal modal header crash (#4000)
juliusmarminge Jul 15, 2026
b982337
[codex] Fix onboarding connection status (#4001)
juliusmarminge Jul 16, 2026
5e8b2c8
Isolate native diff highlight grammar state (#4029)
juliusmarminge Jul 16, 2026
0418301
Fix macOS fullscreen titlebar spacing (#4019)
D3OXY Jul 16, 2026
d114e27
Prevent duplicate project workspace roots (#3829)
juliusmarminge Jul 16, 2026
54f167e
Normalize over-indented markdown list items (#4020)
juliusmarminge Jul 16, 2026
1620f1f
fix(mobile): Persist crash diagnostics before runtime fatals
juliusmarminge Jul 16, 2026
22c58ed
fix(mobile): Recover thread state reliably after reconnects
juliusmarminge Jul 16, 2026
d007a73
fix(mobile): Stabilize thread feed layout and Markdown rendering
juliusmarminge Jul 16, 2026
7996467
fix(mobile): Stabilize native stack option updates
juliusmarminge Jul 16, 2026
b5b2ecd
chore: update patched dependency lockfile
juliusmarminge Jul 16, 2026
8f45962
fix(mobile): Keep chat pinned during Android keyboard open
juliusmarminge Jul 16, 2026
f4457a8
chore: update patched dependency lockfile
juliusmarminge Jul 16, 2026
ab640b5
chore: update patched dependency lockfile
juliusmarminge Jul 16, 2026
bde0a4c
Resolve localhost preview URLs for remote environments (#4011)
juliusmarminge Jul 16, 2026
959a39b
refactor(mobile): Keep outbox attachment conversion with composer upl…
juliusmarminge Jul 16, 2026
f356466
fix(mobile): Compare native stack options structurally
juliusmarminge Jul 16, 2026
3c97855
fix(mobile): Include native header inset in feed fit checks
juliusmarminge Jul 16, 2026
5fcfe27
fix(mobile): Refresh thread feed composer height estimates
juliusmarminge Jul 16, 2026
af4ea04
fix(mobile): Preserve crash log capture ordering
juliusmarminge Jul 16, 2026
2f86672
fix(mobile): Wait for thread synchronization before retrying
juliusmarminge Jul 16, 2026
26bb099
fix(mobile): Keep native header callbacks current
juliusmarminge Jul 16, 2026
6209d13
fix(mobile): Safely serialize fatal errors
juliusmarminge Jul 16, 2026
8b0ce3c
fix(mobile): Preserve measured thread composer height
juliusmarminge Jul 16, 2026
f3d9691
fix(mobile): Handle cyclic native stack options
juliusmarminge Jul 16, 2026
9ff248f
fix(mobile): Preserve stalled thread retry attempts
juliusmarminge Jul 16, 2026
6abdfd3
fix(mobile): Reset thread composer measurements
juliusmarminge Jul 16, 2026
775d0b1
fix(mobile): Preserve composer card measurements
juliusmarminge Jul 16, 2026
2a33a18
fix(mobile): Send composer images in upload wire format (#4035)
juliusmarminge Jul 16, 2026
5d68fca
Fix iOS terminal Enter input encoding (#4043)
juliusmarminge Jul 16, 2026
fdca154
Add native mobile share target support (#4021)
juliusmarminge Jul 16, 2026
62f3298
feat(integration): Merge ios-fixes-main into v2-base
mwolson Jul 17, 2026
30b5502
feat(integration): Merge v2-base into ios-v2-attachment-persistence
mwolson Jul 17, 2026
8a02c71
[codex] Expand real-route app store screenshot harness (#4014)
juliusmarminge Jul 17, 2026
eb733c1
fix(server): use CLAUDE_CONFIG_DIR instead of HOME for Claude instanc…
dmstoykov Jul 17, 2026
7f53a54
Merge branch 'main' into codex/pr3910-thread-feed-layout
juliusmarminge Jul 17, 2026
c14a5ca
Fix dropped events during initial thread snapshot (#4079)
D3OXY Jul 17, 2026
1f27973
feat: show nightly update changelog tooltip (#3832)
HugoVizcainoSantana Jul 17, 2026
58302b2
fix(git): treat selected commit paths literally (#3998)
EricTsai83 Jul 17, 2026
8b54698
fix(server): stabilize non-repository Git diagnostics (#4077)
EricTsai83 Jul 17, 2026
6e9987c
Refresh app icons across release variants (#4080)
juliusmarminge Jul 17, 2026
0748134
Update marketing GitHub star count (#4088)
AmoonPod Jul 17, 2026
4804a98
fix(marketing): correct Cursor icon color (#4090)
AmoonPod Jul 17, 2026
c49d424
Normalize protocol-relative remote host input as https (#3971)
kridaydave Jul 17, 2026
7f1cb61
fix(cursor): default binary path to cursor-agent (avoid path conflict…
BunnyGamezsc Jul 17, 2026
f9a4f59
Fix documented task-runner commands (bun run -> vp) (#3965)
kridaydave Jul 17, 2026
a7f6d97
Allow preview panel to grow on wide displays (#4044)
olivoil Jul 17, 2026
acfb78a
Merge branch 'main' into codex/pr3910-reconnect-recovery
juliusmarminge Jul 17, 2026
d73fe59
Merge branch 'main' into codex/pr3910-stack-options
juliusmarminge Jul 17, 2026
4aa272b
fix(mobile): reset stalled thread watchdog on navigation
juliusmarminge Jul 17, 2026
e057e2e
fix: prevent initial right-click from selecting a context menu item (…
Fazalkadivar21 Jul 17, 2026
7dc7512
Fix duplicate keybinding rule when replacing with an existing rule (#…
kridaydave Jul 17, 2026
ed81c15
fix(server): image upload crashed dispatchCommand with a stack overfl…
t3dotgg Jul 17, 2026
b49a2e6
Remove unused code parameter from describePreviewError (#3970)
kridaydave Jul 17, 2026
bcd640b
[codex] prevent ACP assistant ID collisions after restarts (#3932)
maxwellyoung Jul 17, 2026
7b8d126
fix(web): inset Windows desktop scrollbars from resize edge (#4097)
nateEc Jul 17, 2026
6a17629
[codex] fix mobile composer Enter behavior (#3930)
maxwellyoung Jul 17, 2026
cf58cad
feat(server): include runtime model and effort in Codex developer ins…
t3dotgg Jul 17, 2026
143b9c9
fix(ux): spamming cmd + , no longer stack opening settings (#2757)
jamesx0416 Jul 17, 2026
1047dac
fix(terminal): strip AppImage runtime env from spawned terminals (#3108)
leorivastech Jul 17, 2026
4e3f2f0
fix(server): thread cwd through Claude capability probe (#2048) (#2124)
mvanhorn Jul 17, 2026
b655a9f
[codex] fix: guard invalid web timestamps (#3515)
StiensWout Jul 17, 2026
43e2aea
[codex] fix: tolerate invalid latest user message timestamps (#3521)
StiensWout Jul 17, 2026
73bf127
[codex] Fix provider update checks restore defaults (#3531)
StiensWout Jul 17, 2026
31ca9e5
fix(server): skip undecodable provider runtime rows when listing sess…
t3dotgg Jul 17, 2026
24f9c2a
Share MCP OAuth locks across Codex shadow homes (#4104)
juliusmarminge Jul 17, 2026
8b33799
Preserve T3 Code identity in macOS development launcher (#4102)
juliusmarminge Jul 17, 2026
5ca3266
fix(web): increase contrast of question option descriptions (#3867)
xxashxx-svg Jul 17, 2026
1bd2fa2
feat: draft hero landing on the index route (#4055)
yordis Jul 18, 2026
4cfec8c
feat: file explorer mention actions and zoom-aware context menus (#4054)
yordis Jul 18, 2026
2318e00
fix(web): avoid duplicate mention text on paste
juliusmarminge Jul 18, 2026
03e20c8
fix(mobile): restore iOS home screen branding (#4025)
PixPMusic Jul 18, 2026
765e1b5
perf(client): defer active thread cache writes (#4006)
Chrrxs Jul 18, 2026
38e0bb0
Default diffs to working changes (#3974)
jakeleventhal Jul 18, 2026
e350c0a
Add Grok to marketing site provider list (#3484)
Aditya190803 Jul 18, 2026
31caa56
Fix reopening existing Diff tab (#3973)
jakeleventhal Jul 18, 2026
d8b12ae
Fix sending messages during active turns (#3919)
jakeleventhal Jul 18, 2026
d0b9f8d
[codex] Route OpenCode missing-session errors through Effect (#3608)
StiensWout Jul 18, 2026
c0e161a
[fix/feat:ui] Show default option badge (#3232)
sandersonstabo Jul 18, 2026
e63403f
[fix/feat:ui] Preserve open-in editor brand colors (#3225)
sandersonstabo Jul 18, 2026
b859a57
fix(web): handle macOS Home and End in composer (#2508)
GuilhermeVieiraDev Jul 18, 2026
0506508
Allow failed remote environments to be removed (#4084)
zepi2509 Jul 18, 2026
d120997
Merge branch 'main' into codex/pr3910-reconnect-recovery
juliusmarminge Jul 18, 2026
da3c203
[codex] canonicalize client timestamps (#4112)
maxwellyoung Jul 18, 2026
5c66110
[fix/feat:ui] Make selected menu checks blue (#3234)
sandersonstabo Jul 18, 2026
bf76535
fix(desktop): Validate WSL node version against engine range after pr…
UtkarshUsername Jul 18, 2026
ebe8afb
Refresh splash screen and favicon branding (#4120)
juliusmarminge Jul 18, 2026
b3a26ec
feat(integration): Merge #4033 into ios-integration
mwolson Jul 18, 2026
2c525dd
feat(integration): Merge #4034 into ios-integration
mwolson Jul 18, 2026
87f9a8e
feat(integration): Merge #4036 into ios-integration
mwolson Jul 18, 2026
5c29e8d
feat(integration): Merge #4037 into ios-integration
mwolson Jul 18, 2026
5d521c9
feat(integration): Merge #4038 into ios-integration
mwolson Jul 18, 2026
6b7a171
feat(integration): Merge ios-integration into v2-base
mwolson Jul 18, 2026
09fffc9
fix(web): Subscribe to thread shell unconditionally for draft routes
mwolson Jul 18, 2026
47dc628
feat(integration): Merge v2-base into fix/ios-v2-attachment-persistence
mwolson Jul 18, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
134 changes: 134 additions & 0 deletions .github/workflows/mobile-showcase-screenshots.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
name: Mobile Showcase Screenshots

on:
workflow_dispatch:
inputs:
platform:
description: Device platforms to capture
required: true
default: all
type: choice
options:
- all
- ios
- android
appearance:
description: System appearances to capture
required: true
default: both
type: choice
options:
- both
- dark
- light

permissions:
contents: read

env:
NODE_OPTIONS: --max-old-space-size=8192

jobs:
ios:
name: iPhone 6.9, iPhone 6.5, and iPad 13
if: inputs.platform == 'all' || inputs.platform == 'ios'
runs-on: blacksmith-12vcpu-macos-26
timeout-minutes: 60
steps:
- name: Checkout
uses: actions/checkout@v6

- name: Setup Vite+
uses: voidzero-dev/setup-vp@v1
with:
node-version-file: package.json
cache: true
run-install: true

- name: Expose pnpm
run: |
pnpm_version="$(node --print "require('./package.json').packageManager.split('@').pop()")"
vp_pnpm_bin="$HOME/.vite-plus/package_manager/pnpm/$pnpm_version/pnpm/bin"
echo "$vp_pnpm_bin" >> "$GITHUB_PATH"
"$vp_pnpm_bin/pnpm" --version

- name: Capture iOS showcase
run: pnpm screenshots:mobile --platform ios --appearance "${{ inputs.appearance }}"

- name: Validate App Store Connect assets
run: pnpm screenshots:mobile --platform ios --appearance "${{ inputs.appearance }}" --validate-only

- name: Upload iOS screenshots
if: always()
uses: actions/upload-artifact@v7
with:
name: app-store-connect-screenshots
path: artifacts/app-store/screenshots/apple/
if-no-files-found: warn
retention-days: 14

android:
name: Android phone, 7-inch tablet, and 10-inch tablet
if: inputs.platform == 'all' || inputs.platform == 'android'
runs-on: blacksmith-16vcpu-ubuntu-2404
timeout-minutes: 60
env:
T3_SHOWCASE_ANDROID_ABI: x86_64
steps:
- name: Checkout
uses: actions/checkout@v6

- name: Setup Vite+
uses: voidzero-dev/setup-vp@v1
with:
node-version-file: package.json
cache: true
run-install: true

- name: Expose pnpm
run: |
pnpm_version="$(node --print "require('./package.json').packageManager.split('@').pop()")"
vp_pnpm_bin="$HOME/.vite-plus/package_manager/pnpm/$pnpm_version/pnpm/bin"
echo "$vp_pnpm_bin" >> "$GITHUB_PATH"
"$vp_pnpm_bin/pnpm" --version

- name: Setup Java
uses: actions/setup-java@v5
with:
distribution: temurin
java-version: 17

- name: Setup Gradle cache
uses: gradle/actions/setup-gradle@v5

- name: Enable KVM
run: |
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS="static_node=kvm"' \
| sudo tee /etc/udev/rules.d/99-kvm4all.rules
sudo udevadm control --reload-rules
sudo udevadm trigger --name-match=kvm

- name: Capture Android showcase
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: 36
target: google_apis
arch: x86_64
profile: pixel_7_pro
avd-name: Pixel_10_Pro
cores: 8
ram-size: 4096M
disable-animations: false
script: pnpm screenshots:mobile --platform android --appearance "${{ inputs.appearance }}"

- name: Validate Google Play assets
run: pnpm screenshots:mobile --platform android --appearance "${{ inputs.appearance }}" --validate-only

- name: Upload Android screenshots
if: always()
uses: actions/upload-artifact@v7
with:
name: google-play-screenshots
path: artifacts/app-store/screenshots/google-play/
if-no-files-found: warn
retention-days: 14
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ squashfs-root/
.gstack/
dist-electron/
.electron-runtime/
.showcase/
apps/mobile/.showcase/
artifacts/app-store/screenshots/
node_modules/
.alchemy/
*.log
Expand Down
9 changes: 9 additions & 0 deletions .macroscope/check-run-agents/effect-service-conventions.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,15 @@ Review changed TypeScript and directly affected call sites for the conventions b
- Keep implementation-specific names when an abstract port module contains one of several possible implementations, for example `makeCloudflaredRelayClient` and `layerCloudflared` in `RelayClient.ts`.
- `infra/relay/src/db.ts` is an intentional exception: an inline `Layer.succeed(RelayDb, db)` is acceptable without generic `make`/`layer` exports.

## Dependency acquisition and runtime boundaries

- Production service construction must acquire Effect service dependencies from the environment with `yield* Foo.Foo`, and its `make`/`layer` types must expose those requirements. Flag factories or constructors that accept `Foo["Service"]` (or a plain object whose methods return `Effect`) when that value is an implementation dependency owned by the service. Passing service instances explicitly is acceptable in tests and integration harnesses; passing pure configuration, immutable domain values, or deliberate callback strategies is not service injection.
- Do not hide dependencies in module globals, closures over singleton services, or `Layer.succeed` implementations that call runtime-backed or imperative APIs. Trace helpers used by a supposedly synchronous layer far enough to verify that asynchronous services are represented in the Effect environment.
- `ManagedRuntime.make`, `runPromise`, and `runPromiseExit` belong at explicit application/framework boundaries such as React, native callback, CLI, or HTTP adapters. Flag their use in domain services, repositories, persistence implementations, and service constructors. A clearly named imperative adapter may bridge an Effect service into a Promise API, but it must not become a dependency of another Effect service.
- Do not create per-feature managed runtimes or Atom runtimes to smuggle the same owned resource into multiple consumers. Compose the resource once in an application-owned layer/runtime and provide its context to integration runtimes.
- When acquisition can fail but a caller must retain fallback behavior, keep the failure typed in Effect rather than bypassing the layer through an imperative runtime. Model unavailability in service operations or with an explicit optional-service layer so downstream recovery remains visible and testable.
- During review, search touched code and affected call sites for service-instance parameters, `Layer.succeed`, `ManagedRuntime.make`, and `.runPromise`/`.runPromiseExit`. Verify that each occurrence is a legitimate test seam, pure value injection, or application boundary—not fake dependency injection or a hidden runtime.

## Errors and predicates

- Define service failures with `Schema.TaggedErrorClass` and structured attributes. Derive `message` from those attributes rather than storing an unstructured message as the only data.
Expand Down
3 changes: 3 additions & 0 deletions app.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"expo": {}
}
Comment thread
cursor[bot] marked this conversation as resolved.
2 changes: 1 addition & 1 deletion apps/desktop/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
"@effect/vitest": "catalog:",
"@types/node": "catalog:",
"cross-env": "^10.1.0",
"electron-builder": "26.8.1",
"electron-builder": "26.15.6",
"tailwindcss": "^4.0.0",
"vite-plus": "catalog:"
},
Expand Down
43 changes: 34 additions & 9 deletions apps/desktop/scripts/electron-launcher.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export const APP_BUNDLE_ID = isDevelopment
? `com.t3tools.t3code.dev.${devBundleIdSuffix || "local"}`
: "com.t3tools.t3code";
const APP_PROTOCOL_SCHEMES = isDevelopment ? ["t3code-dev"] : ["t3code"];
const LAUNCHER_VERSION = 12;
const LAUNCHER_VERSION = 14;
const defaultIconPath = NodePath.join(desktopDir, "resources", "icon.icns");
const developmentMacIconPngPath = NodePath.join(
repoRoot,
Expand Down Expand Up @@ -220,11 +220,12 @@ function ensureDevelopmentIconIcns(runtimeDir) {
}
}

function patchMainBundleInfoPlist(appBundlePath, iconPath) {
function patchMainBundleInfoPlist(appBundlePath, iconPath, executableName) {
const infoPlistPath = NodePath.join(appBundlePath, "Contents", "Info.plist");
setPlistString(infoPlistPath, "CFBundleDisplayName", APP_DISPLAY_NAME);
setPlistString(infoPlistPath, "CFBundleName", APP_DISPLAY_NAME);
setPlistString(infoPlistPath, "CFBundleIdentifier", APP_BUNDLE_ID);
setPlistString(infoPlistPath, "CFBundleExecutable", executableName);
setPlistString(infoPlistPath, "CFBundleIconFile", "icon.icns");
setPlistJson(infoPlistPath, "CFBundleURLTypes", [
{
Expand Down Expand Up @@ -277,11 +278,25 @@ function readJson(path) {
}
}

export function resolveMacLauncherPaths(appBundlePath, displayName = APP_DISPLAY_NAME) {
const executableDir = NodePath.join(appBundlePath, "Contents", "MacOS");
const launcherExecutableName = `${displayName} Launcher`;
return {
launcherExecutableName,
launcherBinaryPath: NodePath.join(executableDir, launcherExecutableName),
runtimeElectronBinaryPath: NodePath.join(executableDir, "Electron"),
};
}

function buildMacLauncher(electronBinaryPath) {
const sourceAppBundlePath = NodePath.resolve(NodePath.dirname(electronBinaryPath), "../..");
const runtimeDir = NodePath.join(desktopDir, ".electron-runtime");
const targetAppBundlePath = NodePath.join(runtimeDir, `${APP_DISPLAY_NAME}.app`);
const targetBinaryPath = NodePath.join(targetAppBundlePath, "Contents", "MacOS", "Electron");
const developmentPaths = resolveMacLauncherPaths(targetAppBundlePath);
const runtimeElectronBinaryPath = developmentPaths.runtimeElectronBinaryPath;
const launcherBinaryPath = isDevelopment
? developmentPaths.launcherBinaryPath
: runtimeElectronBinaryPath;
const iconPath = isDevelopment ? ensureDevelopmentIconIcns(runtimeDir) : defaultIconPath;
const metadataPath = NodePath.join(runtimeDir, "metadata.json");

Expand All @@ -298,18 +313,19 @@ function buildMacLauncher(electronBinaryPath) {

const currentMetadata = readJson(metadataPath);
if (
NodeFS.existsSync(targetBinaryPath) &&
NodeFS.existsSync(launcherBinaryPath) &&
(!isDevelopment || NodeFS.existsSync(runtimeElectronBinaryPath)) &&
currentMetadata &&
JSON.stringify(currentMetadata) === JSON.stringify(expectedMetadata)
) {
if (isDevelopment) {
// The launcher also handles protocol activations outside the dev runner,
// so refresh its fallback environment on every launch. Never let a value
// captured by an older parent app override the live dev-runner environment.
writeDevelopmentLauncherScript(targetBinaryPath, electronBinaryPath);
writeDevelopmentLauncherScript(launcherBinaryPath, runtimeElectronBinaryPath);
}
registerMacLauncherBundle(targetAppBundlePath);
return targetBinaryPath;
return launcherBinaryPath;
}

NodeFS.rmSync(targetAppBundlePath, { recursive: true, force: true });
Expand All @@ -321,15 +337,24 @@ function buildMacLauncher(electronBinaryPath) {
recursive: true,
verbatimSymlinks: true,
});
patchMainBundleInfoPlist(targetAppBundlePath, iconPath);
patchMainBundleInfoPlist(
targetAppBundlePath,
iconPath,
isDevelopment ? developmentPaths.launcherExecutableName : "Electron",
);
patchHelperBundleInfoPlists(targetAppBundlePath);
if (isDevelopment) {
writeDevelopmentLauncherScript(targetBinaryPath, electronBinaryPath);
// Keep Electron's native executable inside the branded bundle. Launching the
// node_modules copy makes macOS associate the process (and Dock label) with
// Electron.app even though this bundle's Info.plist has the T3 Code name.
// Its conventional executable name also keeps Electron's default-app runtime
// in development mode instead of making app.isPackaged report true.
writeDevelopmentLauncherScript(launcherBinaryPath, runtimeElectronBinaryPath);
}
NodeFS.writeFileSync(metadataPath, `${JSON.stringify(expectedMetadata, null, 2)}\n`);
registerMacLauncherBundle(targetAppBundlePath);

return targetBinaryPath;
return launcherBinaryPath;
}

function isLinuxSetuidSandboxConfigured(electronBinaryPath) {
Expand Down
35 changes: 34 additions & 1 deletion apps/desktop/scripts/electron-launcher.test.mjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { assert, describe, it } from "vite-plus/test";

import { makeDevelopmentLauncherScript, resolveElectronBinaryPath } from "./electron-launcher.mjs";
import {
makeDevelopmentLauncherScript,
resolveElectronBinaryPath,
resolveMacLauncherPaths,
} from "./electron-launcher.mjs";

describe("electron development launcher", () => {
it("uses captured values only as fallbacks for a live runner environment", () => {
Expand Down Expand Up @@ -45,4 +49,33 @@ describe("electron development launcher", () => {
);
assert.deepEqual(calls, ["ensure", "require:electron"]);
});

it("keeps the native Electron executable name inside the branded macOS bundle", () => {
const paths = resolveMacLauncherPaths(
"/repo/apps/desktop/.electron-runtime/T3 Code (Dev).app",
"T3 Code (Dev)",
);

assert.equal(paths.launcherExecutableName, "T3 Code (Dev) Launcher");
assert.equal(
paths.launcherBinaryPath,
"/repo/apps/desktop/.electron-runtime/T3 Code (Dev).app/Contents/MacOS/T3 Code (Dev) Launcher",
);
assert.equal(
paths.runtimeElectronBinaryPath,
"/repo/apps/desktop/.electron-runtime/T3 Code (Dev).app/Contents/MacOS/Electron",
);

const script = makeDevelopmentLauncherScript({
electronBinaryPath: paths.runtimeElectronBinaryPath,
mainEntryPath: "/repo/apps/desktop/dist-electron/main.cjs",
desktopRoot: "/repo/apps/desktop",
environment: {},
});
assert.include(
script,
"exec '/repo/apps/desktop/.electron-runtime/T3 Code (Dev).app/Contents/MacOS/Electron'",
);
assert.notInclude(script, "node_modules/electron");
});
});
14 changes: 12 additions & 2 deletions apps/desktop/src/electron/ElectronMenu.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,12 @@ const TestLayer = ElectronMenu.layer.pipe(
Layer.provide(Layer.succeed(HostProcessPlatform, "linux")),
);

const makeWindow = (zoomFactor = 1): Electron.BrowserWindow =>
({
id: 7,
webContents: { getZoomFactor: () => zoomFactor },
}) as unknown as Electron.BrowserWindow;

describe("ElectronMenu", () => {
beforeEach(() => {
buildFromTemplateMock.mockReset();
Expand Down Expand Up @@ -70,7 +76,7 @@ describe("ElectronMenu", () => {

const electronMenu = yield* ElectronMenu.ElectronMenu;
const selectedItemId = yield* electronMenu.showContextMenu({
window: {} as Electron.BrowserWindow,
window: makeWindow(),
items: [{ id: "copy", label: "Copy" }],
position: Option.none(),
});
Expand All @@ -81,20 +87,24 @@ describe("ElectronMenu", () => {

it.effect("resolves with none when the menu closes without a click", () =>
Effect.gen(function* () {
let popupOptions: Electron.PopupOptions | undefined;
buildFromTemplateMock.mockImplementation(() => ({
popup: (options: Electron.PopupOptions) => {
popupOptions = options;
options.callback?.();
},
}));

const electronMenu = yield* ElectronMenu.ElectronMenu;
const selectedItemId = yield* electronMenu.showContextMenu({
window: {} as Electron.BrowserWindow,
window: makeWindow(2),
items: [{ id: "copy", label: "Copy" }],
position: Option.some({ x: 10.8, y: 20.2 }),
});

assert.isTrue(Option.isNone(selectedItemId));
assert.equal(popupOptions?.x, 21);
assert.equal(popupOptions?.y, 40);
assert.deepEqual(buildFromTemplateMock.mock.calls[0]?.[0][0], {
label: "Copy",
enabled: true,
Expand Down
Loading
Loading