Skip to content

Commit 65ab8a3

Browse files
authored
Merge branch 'main' into feat/marketing-grok
2 parents 2b7e981 + 5ca3266 commit 65ab8a3

235 files changed

Lines changed: 10481 additions & 588 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.
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
name: Mobile Showcase Screenshots
2+
3+
on:
4+
workflow_dispatch:
5+
inputs:
6+
platform:
7+
description: Device platforms to capture
8+
required: true
9+
default: all
10+
type: choice
11+
options:
12+
- all
13+
- ios
14+
- android
15+
appearance:
16+
description: System appearances to capture
17+
required: true
18+
default: both
19+
type: choice
20+
options:
21+
- both
22+
- dark
23+
- light
24+
25+
permissions:
26+
contents: read
27+
28+
env:
29+
NODE_OPTIONS: --max-old-space-size=8192
30+
31+
jobs:
32+
ios:
33+
name: iPhone 6.9, iPhone 6.5, and iPad 13
34+
if: inputs.platform == 'all' || inputs.platform == 'ios'
35+
runs-on: blacksmith-12vcpu-macos-26
36+
timeout-minutes: 60
37+
steps:
38+
- name: Checkout
39+
uses: actions/checkout@v6
40+
41+
- name: Setup Vite+
42+
uses: voidzero-dev/setup-vp@v1
43+
with:
44+
node-version-file: package.json
45+
cache: true
46+
run-install: true
47+
48+
- name: Expose pnpm
49+
run: |
50+
pnpm_version="$(node --print "require('./package.json').packageManager.split('@').pop()")"
51+
vp_pnpm_bin="$HOME/.vite-plus/package_manager/pnpm/$pnpm_version/pnpm/bin"
52+
echo "$vp_pnpm_bin" >> "$GITHUB_PATH"
53+
"$vp_pnpm_bin/pnpm" --version
54+
55+
- name: Capture iOS showcase
56+
run: pnpm screenshots:mobile --platform ios --appearance "${{ inputs.appearance }}"
57+
58+
- name: Validate App Store Connect assets
59+
run: pnpm screenshots:mobile --platform ios --appearance "${{ inputs.appearance }}" --validate-only
60+
61+
- name: Upload iOS screenshots
62+
if: always()
63+
uses: actions/upload-artifact@v7
64+
with:
65+
name: app-store-connect-screenshots
66+
path: artifacts/app-store/screenshots/apple/
67+
if-no-files-found: warn
68+
retention-days: 14
69+
70+
android:
71+
name: Android phone, 7-inch tablet, and 10-inch tablet
72+
if: inputs.platform == 'all' || inputs.platform == 'android'
73+
runs-on: blacksmith-16vcpu-ubuntu-2404
74+
timeout-minutes: 60
75+
env:
76+
T3_SHOWCASE_ANDROID_ABI: x86_64
77+
steps:
78+
- name: Checkout
79+
uses: actions/checkout@v6
80+
81+
- name: Setup Vite+
82+
uses: voidzero-dev/setup-vp@v1
83+
with:
84+
node-version-file: package.json
85+
cache: true
86+
run-install: true
87+
88+
- name: Expose pnpm
89+
run: |
90+
pnpm_version="$(node --print "require('./package.json').packageManager.split('@').pop()")"
91+
vp_pnpm_bin="$HOME/.vite-plus/package_manager/pnpm/$pnpm_version/pnpm/bin"
92+
echo "$vp_pnpm_bin" >> "$GITHUB_PATH"
93+
"$vp_pnpm_bin/pnpm" --version
94+
95+
- name: Setup Java
96+
uses: actions/setup-java@v5
97+
with:
98+
distribution: temurin
99+
java-version: 17
100+
101+
- name: Setup Gradle cache
102+
uses: gradle/actions/setup-gradle@v5
103+
104+
- name: Enable KVM
105+
run: |
106+
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS="static_node=kvm"' \
107+
| sudo tee /etc/udev/rules.d/99-kvm4all.rules
108+
sudo udevadm control --reload-rules
109+
sudo udevadm trigger --name-match=kvm
110+
111+
- name: Capture Android showcase
112+
uses: reactivecircus/android-emulator-runner@v2
113+
with:
114+
api-level: 36
115+
target: google_apis
116+
arch: x86_64
117+
profile: pixel_7_pro
118+
avd-name: Pixel_10_Pro
119+
cores: 8
120+
ram-size: 4096M
121+
disable-animations: false
122+
script: pnpm screenshots:mobile --platform android --appearance "${{ inputs.appearance }}"
123+
124+
- name: Validate Google Play assets
125+
run: pnpm screenshots:mobile --platform android --appearance "${{ inputs.appearance }}" --validate-only
126+
127+
- name: Upload Android screenshots
128+
if: always()
129+
uses: actions/upload-artifact@v7
130+
with:
131+
name: google-play-screenshots
132+
path: artifacts/app-store/screenshots/google-play/
133+
if-no-files-found: warn
134+
retention-days: 14

‎.gitignore‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ squashfs-root/
2727
.gstack/
2828
dist-electron/
2929
.electron-runtime/
30+
.showcase/
31+
apps/mobile/.showcase/
32+
artifacts/app-store/screenshots/
3033
node_modules/
3134
.alchemy/
3235
*.log

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

Lines changed: 34 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ export 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 = 12;
23+
const LAUNCHER_VERSION = 14;
2424
const defaultIconPath = NodePath.join(desktopDir, "resources", "icon.icns");
2525
const developmentMacIconPngPath = NodePath.join(
2626
repoRoot,
@@ -220,11 +220,12 @@ function ensureDevelopmentIconIcns(runtimeDir) {
220220
}
221221
}
222222

223-
function patchMainBundleInfoPlist(appBundlePath, iconPath) {
223+
function patchMainBundleInfoPlist(appBundlePath, iconPath, executableName) {
224224
const infoPlistPath = NodePath.join(appBundlePath, "Contents", "Info.plist");
225225
setPlistString(infoPlistPath, "CFBundleDisplayName", APP_DISPLAY_NAME);
226226
setPlistString(infoPlistPath, "CFBundleName", APP_DISPLAY_NAME);
227227
setPlistString(infoPlistPath, "CFBundleIdentifier", APP_BUNDLE_ID);
228+
setPlistString(infoPlistPath, "CFBundleExecutable", executableName);
228229
setPlistString(infoPlistPath, "CFBundleIconFile", "icon.icns");
229230
setPlistJson(infoPlistPath, "CFBundleURLTypes", [
230231
{
@@ -277,11 +278,25 @@ function readJson(path) {
277278
}
278279
}
279280

281+
export function resolveMacLauncherPaths(appBundlePath, displayName = APP_DISPLAY_NAME) {
282+
const executableDir = NodePath.join(appBundlePath, "Contents", "MacOS");
283+
const launcherExecutableName = `${displayName} Launcher`;
284+
return {
285+
launcherExecutableName,
286+
launcherBinaryPath: NodePath.join(executableDir, launcherExecutableName),
287+
runtimeElectronBinaryPath: NodePath.join(executableDir, "Electron"),
288+
};
289+
}
290+
280291
function buildMacLauncher(electronBinaryPath) {
281292
const sourceAppBundlePath = NodePath.resolve(NodePath.dirname(electronBinaryPath), "../..");
282293
const runtimeDir = NodePath.join(desktopDir, ".electron-runtime");
283294
const targetAppBundlePath = NodePath.join(runtimeDir, `${APP_DISPLAY_NAME}.app`);
284-
const targetBinaryPath = NodePath.join(targetAppBundlePath, "Contents", "MacOS", "Electron");
295+
const developmentPaths = resolveMacLauncherPaths(targetAppBundlePath);
296+
const runtimeElectronBinaryPath = developmentPaths.runtimeElectronBinaryPath;
297+
const launcherBinaryPath = isDevelopment
298+
? developmentPaths.launcherBinaryPath
299+
: runtimeElectronBinaryPath;
285300
const iconPath = isDevelopment ? ensureDevelopmentIconIcns(runtimeDir) : defaultIconPath;
286301
const metadataPath = NodePath.join(runtimeDir, "metadata.json");
287302

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

299314
const currentMetadata = readJson(metadataPath);
300315
if (
301-
NodeFS.existsSync(targetBinaryPath) &&
316+
NodeFS.existsSync(launcherBinaryPath) &&
317+
(!isDevelopment || NodeFS.existsSync(runtimeElectronBinaryPath)) &&
302318
currentMetadata &&
303319
JSON.stringify(currentMetadata) === JSON.stringify(expectedMetadata)
304320
) {
305321
if (isDevelopment) {
306322
// The launcher also handles protocol activations outside the dev runner,
307323
// so refresh its fallback environment on every launch. Never let a value
308324
// captured by an older parent app override the live dev-runner environment.
309-
writeDevelopmentLauncherScript(targetBinaryPath, electronBinaryPath);
325+
writeDevelopmentLauncherScript(launcherBinaryPath, runtimeElectronBinaryPath);
310326
}
311327
registerMacLauncherBundle(targetAppBundlePath);
312-
return targetBinaryPath;
328+
return launcherBinaryPath;
313329
}
314330

315331
NodeFS.rmSync(targetAppBundlePath, { recursive: true, force: true });
@@ -321,15 +337,24 @@ function buildMacLauncher(electronBinaryPath) {
321337
recursive: true,
322338
verbatimSymlinks: true,
323339
});
324-
patchMainBundleInfoPlist(targetAppBundlePath, iconPath);
340+
patchMainBundleInfoPlist(
341+
targetAppBundlePath,
342+
iconPath,
343+
isDevelopment ? developmentPaths.launcherExecutableName : "Electron",
344+
);
325345
patchHelperBundleInfoPlists(targetAppBundlePath);
326346
if (isDevelopment) {
327-
writeDevelopmentLauncherScript(targetBinaryPath, electronBinaryPath);
347+
// Keep Electron's native executable inside the branded bundle. Launching the
348+
// node_modules copy makes macOS associate the process (and Dock label) with
349+
// Electron.app even though this bundle's Info.plist has the T3 Code name.
350+
// Its conventional executable name also keeps Electron's default-app runtime
351+
// in development mode instead of making app.isPackaged report true.
352+
writeDevelopmentLauncherScript(launcherBinaryPath, runtimeElectronBinaryPath);
328353
}
329354
NodeFS.writeFileSync(metadataPath, `${JSON.stringify(expectedMetadata, null, 2)}\n`);
330355
registerMacLauncherBundle(targetAppBundlePath);
331356

332-
return targetBinaryPath;
357+
return launcherBinaryPath;
333358
}
334359

335360
function isLinuxSetuidSandboxConfigured(electronBinaryPath) {

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

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
import { assert, describe, it } from "vite-plus/test";
22

3-
import { makeDevelopmentLauncherScript, resolveElectronBinaryPath } from "./electron-launcher.mjs";
3+
import {
4+
makeDevelopmentLauncherScript,
5+
resolveElectronBinaryPath,
6+
resolveMacLauncherPaths,
7+
} from "./electron-launcher.mjs";
48

59
describe("electron development launcher", () => {
610
it("uses captured values only as fallbacks for a live runner environment", () => {
@@ -45,4 +49,33 @@ describe("electron development launcher", () => {
4549
);
4650
assert.deepEqual(calls, ["ensure", "require:electron"]);
4751
});
52+
53+
it("keeps the native Electron executable name inside the branded macOS bundle", () => {
54+
const paths = resolveMacLauncherPaths(
55+
"/repo/apps/desktop/.electron-runtime/T3 Code (Dev).app",
56+
"T3 Code (Dev)",
57+
);
58+
59+
assert.equal(paths.launcherExecutableName, "T3 Code (Dev) Launcher");
60+
assert.equal(
61+
paths.launcherBinaryPath,
62+
"/repo/apps/desktop/.electron-runtime/T3 Code (Dev).app/Contents/MacOS/T3 Code (Dev) Launcher",
63+
);
64+
assert.equal(
65+
paths.runtimeElectronBinaryPath,
66+
"/repo/apps/desktop/.electron-runtime/T3 Code (Dev).app/Contents/MacOS/Electron",
67+
);
68+
69+
const script = makeDevelopmentLauncherScript({
70+
electronBinaryPath: paths.runtimeElectronBinaryPath,
71+
mainEntryPath: "/repo/apps/desktop/dist-electron/main.cjs",
72+
desktopRoot: "/repo/apps/desktop",
73+
environment: {},
74+
});
75+
assert.include(
76+
script,
77+
"exec '/repo/apps/desktop/.electron-runtime/T3 Code (Dev).app/Contents/MacOS/Electron'",
78+
);
79+
assert.notInclude(script, "node_modules/electron");
80+
});
4881
});

‎apps/desktop/src/electron/ElectronUpdater.test.ts‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ const { autoUpdaterMock } = vi.hoisted(() => ({
1010
autoInstallOnAppQuit: true,
1111
channel: "latest",
1212
disableDifferentialDownload: false,
13+
fullChangelog: false,
1314
checkForUpdates: vi.fn(() => Promise.resolve(null)),
1415
downloadUpdate: vi.fn(() => Promise.resolve([])),
1516
on: vi.fn(),
@@ -33,6 +34,7 @@ describe("ElectronUpdater", () => {
3334
autoUpdaterMock.autoInstallOnAppQuit = true;
3435
autoUpdaterMock.channel = "latest";
3536
autoUpdaterMock.disableDifferentialDownload = false;
37+
autoUpdaterMock.fullChangelog = false;
3638
autoUpdaterMock.checkForUpdates.mockClear();
3739
autoUpdaterMock.checkForUpdates.mockImplementation(() => Promise.resolve(null));
3840
autoUpdaterMock.downloadUpdate.mockClear();
@@ -98,6 +100,18 @@ describe("ElectronUpdater", () => {
98100
}).pipe(Effect.provide(ElectronUpdater.layer)),
99101
);
100102

103+
it.effect("sets full changelog mode", () =>
104+
Effect.gen(function* () {
105+
const updater = yield* ElectronUpdater.ElectronUpdater;
106+
107+
yield* updater.setFullChangelog(true);
108+
assert.equal(autoUpdaterMock.fullChangelog, true);
109+
110+
yield* updater.setFullChangelog(false);
111+
assert.equal(autoUpdaterMock.fullChangelog, false);
112+
}).pipe(Effect.provide(ElectronUpdater.layer)),
113+
);
114+
101115
it.effect("preserves quit-and-install flags and the execution-time channel", () =>
102116
Effect.gen(function* () {
103117
const cause = new Error("quit and install failed");

‎apps/desktop/src/electron/ElectronUpdater.ts‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ export class ElectronUpdater extends Context.Service<
6666
readonly setAllowPrerelease: (value: boolean) => Effect.Effect<void>;
6767
readonly allowDowngrade: Effect.Effect<boolean>;
6868
readonly setAllowDowngrade: (value: boolean) => Effect.Effect<void>;
69+
readonly setFullChangelog: (value: boolean) => Effect.Effect<void>;
6970
readonly setDisableDifferentialDownload: (value: boolean) => Effect.Effect<void>;
7071
readonly checkForUpdates: Effect.Effect<void, ElectronUpdaterCheckForUpdatesError>;
7172
readonly downloadUpdate: Effect.Effect<void, ElectronUpdaterDownloadUpdateError>;
@@ -112,6 +113,11 @@ export const make = ElectronUpdater.of({
112113
autoUpdater.allowDowngrade = value;
113114
return Effect.void;
114115
}),
116+
setFullChangelog: (value) =>
117+
Effect.suspend(() => {
118+
autoUpdater.fullChangelog = value;
119+
return Effect.void;
120+
}),
115121
setDisableDifferentialDownload: (value) =>
116122
Effect.suspend(() => {
117123
autoUpdater.disableDifferentialDownload = value;

‎apps/desktop/src/ipc/DesktopIpcHandlers.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import {
3535
getAppBranding,
3636
getLocalEnvironmentBootstraps,
3737
getLocalEnvironmentBearerToken,
38+
getWindowFullscreenState,
3839
openExternal,
3940
pickFolder,
4041
setTheme,
@@ -48,6 +49,7 @@ export const installDesktopIpcHandlers = Effect.fn("desktop.ipc.installHandlers"
4849
yield* PreviewIpc.installPreviewEventForwarding();
4950

5051
yield* ipc.handleSync(getAppBranding);
52+
yield* ipc.handleSync(getWindowFullscreenState);
5153
yield* ipc.handleSync(getLocalEnvironmentBootstraps);
5254
yield* ipc.handle(getLocalEnvironmentBearerToken);
5355

‎apps/desktop/src/ipc/channels.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ export const SET_THEME_CHANNEL = "desktop:set-theme";
44
export const CONTEXT_MENU_CHANNEL = "desktop:context-menu";
55
export const OPEN_EXTERNAL_CHANNEL = "desktop:open-external";
66
export const MENU_ACTION_CHANNEL = "desktop:menu-action";
7+
export const GET_WINDOW_FULLSCREEN_STATE_CHANNEL = "desktop:get-window-fullscreen-state";
8+
export const WINDOW_FULLSCREEN_STATE_CHANNEL = "desktop:window-fullscreen-state";
79
export const UPDATE_STATE_CHANNEL = "desktop:update-state";
810
export const UPDATE_GET_STATE_CHANNEL = "desktop:update-get-state";
911
export const UPDATE_SET_CHANNEL_CHANNEL = "desktop:update-set-channel";

0 commit comments

Comments
 (0)