Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
28107e8
chore: remove `AnnotatableFileDiff` leftovers, rename file (#3488)
ipanasenko Jun 22, 2026
ea52bb1
[codex] fix: guard trace ID clipboard copy (#3505)
StiensWout Jun 22, 2026
8b993bc
[codex] fix: restore pending input keyboard activation (#3501)
StiensWout Jun 22, 2026
c1e2408
[codex] fix: preserve localhost preview hosts (#3499)
StiensWout Jun 22, 2026
8919ae7
[codex] Reject unsupported remote pairing protocols (#3498)
StiensWout Jun 22, 2026
37ac970
Persist mobile composer selectors across drafts (#3496)
juliusmarminge Jun 22, 2026
f5f98cf
Stabilize composer provider state while typing (#3507)
cursor[bot] Jun 22, 2026
fb10345
feat: add persistent word-wrap setting for chat code blocks and table…
imabdulazeez Jun 22, 2026
b2d17b7
Add main sidebar toggle (#3497)
juliusmarminge Jun 22, 2026
8cc9a6f
[codex] Restore T3 Connect account controls (#3492)
juliusmarminge Jun 22, 2026
92e54fb
[codex] fix: guard DPoP fallback URL construction (#3503)
StiensWout Jun 22, 2026
6672a1d
Bump Clerk packages and refresh lockfile (#3511)
juliusmarminge Jun 22, 2026
3083d71
[codex] fix: clarify Cursor CLI setup error (#3519)
StiensWout Jun 23, 2026
4abf8b4
[codex] fix: ignore stale shell reducer events (#3517)
StiensWout Jun 23, 2026
c6c6491
Reduce ChatMarkdown settings rerenders (#3536)
cursor[bot] Jun 23, 2026
a4964b3
[codex] fix: show standalone element-pick context (#3527)
StiensWout Jun 23, 2026
22f021e
[codex] Upgrade Legend List chat scrolling (#3545)
juliusmarminge Jun 24, 2026
c2776c2
Restore right panel inset when maximized (#3555)
juliusmarminge Jun 25, 2026
31dfe35
Fix Electron dev and packaged renderer startup (#3557)
juliusmarminge Jun 25, 2026
ffae541
Route preview automation through live owner streams (#3548)
juliusmarminge Jun 26, 2026
e9ed70c
[codex] Fix preview automation edge cases (#3561)
juliusmarminge Jun 26, 2026
52b04b9
fix(grok): Harden ACP resume with replay-idle load readiness (#3156)
mwolson Jun 26, 2026
24abab7
Stabilize chat scroll anchoring after send (#3564)
juliusmarminge Jun 26, 2026
6245c54
Fix native composer lag with revision-gated updates (#3574)
juliusmarminge Jun 26, 2026
44fb34a
Stabilize preview browser surfaces, automation, and recording (#3565)
juliusmarminge Jun 27, 2026
a9b1190
Desktop: parallel WSL + Windows backends with mode picker (#2751)
Jgratton24 Jun 27, 2026
fda6486
Restore chat scroll affordances and add timeline minimap (#3587)
juliusmarminge Jun 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
69 changes: 67 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -252,10 +252,64 @@ jobs:
echo "clerk_cli_oauth_client_id=$CLERK_CLI_OAUTH_CLIENT_ID" >> "$GITHUB_OUTPUT"
echo "relay_url=https://$relay_domain" >> "$GITHUB_OUTPUT"
# node-pty publishes no Linux prebuilt and the WSL backend runs under the
# distro's own (Linux) Node, which can't load the Windows/Electron binary. We
# build the Linux pty.node here, on Linux, and hand it to the Windows packaging
# job — the Windows artifact then ships a ready WSL backend binary with no
# cross-compiling and no first-launch compiler/node-gyp/network on the user's
# machine. node-pty is N-API, so one binary works across all WSL Node versions.
build_wsl_node_pty:
name: Build WSL node-pty (linux-x64)
needs: [preflight]
if: ${{ !failure() && !cancelled() && needs.preflight.result == 'success' }}
runs-on: blacksmith-8vcpu-ubuntu-2404
timeout-minutes: 15
steps:
- name: Checkout
uses: actions/checkout@v6
with:
ref: ${{ needs.preflight.outputs.ref }}
fetch-depth: 0

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

- name: Build node-pty linux-x64 prebuild
shell: bash
run: |
set -euo pipefail
# Resolve node-pty from apps/server (where it's a dependency) and build
# its native binary from source for Linux. node-addon-api resolves from
# node-pty's own dependency tree, so node-gyp has everything it needs.
pty_pkg="$(node -e "console.log(require.resolve('node-pty/package.json', { paths: ['$GITHUB_WORKSPACE/apps/server'] }))")"
pty_dir="$(dirname "$pty_pkg")"
( cd "$pty_dir" && npx --yes node-gyp rebuild )
mkdir -p wsl-prebuild
cp "$pty_dir/build/Release/pty.node" wsl-prebuild/pty.node
file wsl-prebuild/pty.node
- name: Upload node-pty linux-x64 prebuild
uses: actions/upload-artifact@v7
with:
name: wsl-node-pty-x64
path: wsl-prebuild/pty.node
if-no-files-found: error

build:
name: Build ${{ matrix.label }}
needs: [preflight, relay_public_config]
if: ${{ !failure() && !cancelled() && needs.preflight.result == 'success' && needs.relay_public_config.result == 'success' }}
# build_wsl_node_pty stays in `needs` so it runs first and its artifact is
# available to download, but only the Windows matrix entry consumes it. We
# therefore gate the job on preflight + relay (must succeed) WITHOUT requiring
# build_wsl_node_pty, so a failed Linux prebuild doesn't skip the macOS/Linux
# builds. `!cancelled()` (not `!failure()`) lets the job run even when
# build_wsl_node_pty failed; the Windows-only download step below then fails
# that single platform if the prebuild is missing.
needs: [preflight, relay_public_config, build_wsl_node_pty]
if: ${{ !cancelled() && needs.preflight.result == 'success' && needs.relay_public_config.result == 'success' }}
runs-on: ${{ matrix.runner }}
timeout-minutes: 30
env:
Expand Down Expand Up @@ -323,6 +377,13 @@ jobs:
- name: Align package versions to release version
run: node scripts/update-release-package-versions.ts "${{ needs.preflight.outputs.version }}"

- name: Download WSL node-pty prebuild
if: matrix.platform == 'win'
uses: actions/download-artifact@v7
with:
name: wsl-node-pty-x64
path: wsl-prebuild

- name: Install Spectre-mitigated MSVC libs
if: matrix.platform == 'win'
shell: pwsh
Expand Down Expand Up @@ -476,6 +537,10 @@ jobs:
echo "macOS signing disabled (missing one or more Apple signing secrets)."
fi
elif [[ "${{ matrix.platform }}" == "win" ]]; then
# Bundle the Linux node-pty binary built by the build_wsl_node_pty job
# so the packaged WSL backend ships a ready binary (no first-launch
# compile). Required for a working WSL backend on Windows.
args+=(--wsl-prebuild "$GITHUB_WORKSPACE/wsl-prebuild/pty.node")
if has_all \
"$AZURE_TENANT_ID" \
"$AZURE_CLIENT_ID" \
Expand Down
50 changes: 36 additions & 14 deletions apps/desktop/scripts/electron-launcher.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -100,25 +100,41 @@ function shellSingleQuote(value) {
return `'${value.replaceAll("'", "'\\''")}'`;
}

function writeDevelopmentLauncherScript(targetBinaryPath, electronBinaryPath) {
const mainEntryPath = NodePath.join(desktopDir, "dist-electron", "main.cjs");
export function makeDevelopmentLauncherScript({
electronBinaryPath,
mainEntryPath,
desktopRoot,
environment,
}) {
const envEntries = [
["VITE_DEV_SERVER_URL", process.env.VITE_DEV_SERVER_URL],
["T3CODE_PORT", process.env.T3CODE_PORT],
["T3CODE_HOME", process.env.T3CODE_HOME],
["T3CODE_COMMIT_HASH", process.env.T3CODE_COMMIT_HASH],
["T3CODE_OTLP_TRACES_URL", process.env.T3CODE_OTLP_TRACES_URL],
["T3CODE_OTLP_EXPORT_INTERVAL_MS", process.env.T3CODE_OTLP_EXPORT_INTERVAL_MS],
["VITE_DEV_SERVER_URL", environment.VITE_DEV_SERVER_URL],
["T3CODE_PORT", environment.T3CODE_PORT],
["T3CODE_HOME", environment.T3CODE_HOME],
["T3CODE_COMMIT_HASH", environment.T3CODE_COMMIT_HASH],
["T3CODE_OTLP_TRACES_URL", environment.T3CODE_OTLP_TRACES_URL],
["T3CODE_OTLP_EXPORT_INTERVAL_MS", environment.T3CODE_OTLP_EXPORT_INTERVAL_MS],
["T3CODE_DESKTOP_APP_USER_MODEL_ID", APP_BUNDLE_ID],
].filter((entry) => typeof entry[1] === "string" && entry[1].trim().length > 0);
return [
"#!/bin/sh",
...envEntries.map(
([name, value]) =>
`if [ -z "\${${name}:-}" ]; then export ${name}=${shellSingleQuote(value)}; fi`,
),
`exec ${shellSingleQuote(electronBinaryPath)} --t3code-dev-root=${shellSingleQuote(desktopRoot)} ${shellSingleQuote(mainEntryPath)} "$@"`,
"",
].join("\n");
}

function writeDevelopmentLauncherScript(targetBinaryPath, electronBinaryPath) {
NodeFS.writeFileSync(
targetBinaryPath,
[
"#!/bin/sh",
...envEntries.map(([name, value]) => `export ${name}=${shellSingleQuote(value)}`),
`exec ${shellSingleQuote(electronBinaryPath)} --t3code-dev-root=${shellSingleQuote(desktopDir)} ${shellSingleQuote(mainEntryPath)} "$@"`,
"",
].join("\n"),
makeDevelopmentLauncherScript({
electronBinaryPath,
mainEntryPath: NodePath.join(desktopDir, "dist-electron", "main.cjs"),
desktopRoot: desktopDir,
environment: process.env,
}),
);
NodeFS.chmodSync(targetBinaryPath, 0o755);
}
Expand Down Expand Up @@ -286,6 +302,12 @@ function buildMacLauncher(electronBinaryPath) {
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);
}
registerMacLauncherBundle(targetAppBundlePath);
return targetBinaryPath;
}
Expand Down
28 changes: 28 additions & 0 deletions apps/desktop/scripts/electron-launcher.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { assert, describe, it } from "vite-plus/test";

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

describe("electron development launcher", () => {
it("uses captured values only as fallbacks for a live runner environment", () => {
const script = makeDevelopmentLauncherScript({
electronBinaryPath: "/repo/node_modules/electron/Electron",
mainEntryPath: "/repo/apps/desktop/dist-electron/main.cjs",
desktopRoot: "/repo/apps/desktop",
environment: {
VITE_DEV_SERVER_URL: "http://127.0.0.1:8526",
T3CODE_PORT: "16566",
T3CODE_HOME: "/tmp/t3",
},
});

assert.include(
script,
"if [ -z \"${VITE_DEV_SERVER_URL:-}\" ]; then export VITE_DEV_SERVER_URL='http://127.0.0.1:8526'; fi",
);
assert.notInclude(script, "\nexport VITE_DEV_SERVER_URL=");
assert.include(
script,
"exec '/repo/node_modules/electron/Electron' --t3code-dev-root='/repo/apps/desktop' '/repo/apps/desktop/dist-electron/main.cjs' \"$@\"",
);
});
});
37 changes: 32 additions & 5 deletions apps/desktop/src/app/DesktopApp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ import { installDesktopIpcHandlers } from "../ipc/DesktopIpcHandlers.ts";
import * as DesktopAppIdentity from "./DesktopAppIdentity.ts";
import * as DesktopClerk from "./DesktopClerk.ts";
import * as DesktopApplicationMenu from "../window/DesktopApplicationMenu.ts";
import * as DesktopBackendManager from "../backend/DesktopBackendManager.ts";
import * as DesktopWindow from "../window/DesktopWindow.ts";
import * as DesktopBackendPool from "../backend/DesktopBackendPool.ts";
import * as DesktopEnvironment from "./DesktopEnvironment.ts";
import * as DesktopLifecycle from "./DesktopLifecycle.ts";
import * as DesktopObservability from "./DesktopObservability.ts";
Expand All @@ -23,6 +24,7 @@ import * as DesktopAppSettings from "../settings/DesktopAppSettings.ts";
import * as DesktopShellEnvironment from "../shell/DesktopShellEnvironment.ts";
import * as DesktopState from "./DesktopState.ts";
import * as DesktopUpdates from "../updates/DesktopUpdates.ts";
import * as DesktopWslBackend from "../wsl/DesktopWslBackend.ts";

const DEFAULT_DESKTOP_BACKEND_PORT = 3773;
const MAX_TCP_PORT = 65_535;
Expand Down Expand Up @@ -135,11 +137,14 @@ const fatalStartupCause = <E>(stage: string, cause: Cause.Cause<E>) =>
handleFatalStartupError(stage, Cause.pretty(cause)).pipe(Effect.andThen(Effect.failCause(cause)));

const bootstrap = Effect.gen(function* () {
const backendManager = yield* DesktopBackendManager.DesktopBackendManager;
const pool = yield* DesktopBackendPool.DesktopBackendPool;
const primaryBackend = yield* pool.primary;
const state = yield* DesktopState.DesktopState;
const environment = yield* DesktopEnvironment.DesktopEnvironment;
const desktopSettings = yield* DesktopAppSettings.DesktopAppSettings;
const serverExposure = yield* DesktopServerExposure.DesktopServerExposure;
const wslBackend = yield* DesktopWslBackend.DesktopWslBackend;
const desktopWindow = yield* DesktopWindow.DesktopWindow;
yield* logBootstrapInfo("bootstrap start");

if (environment.isDevelopment && Option.isNone(environment.configuredBackendPort)) {
Expand Down Expand Up @@ -193,8 +198,20 @@ const bootstrap = Effect.gen(function* () {
yield* logBootstrapInfo("bootstrap ipc handlers registered");

if (!(yield* Ref.get(state.quitting))) {
yield* backendManager.start;
// In wsl-only mode the renderer is served by the WSL backend, which can be
// slow to cold-boot — show a "Connecting to WSL" splash immediately so the
// app feels responsive instead of presenting no window until WSL is ready.
// (Dual mode opens fast off the Windows primary, so no splash there.)
if (settings.wslOnly === true && settings.wslBackendEnabled === true) {
yield* desktopWindow.showConnectingSplash;
}
yield* primaryBackend.start;
yield* logBootstrapInfo("bootstrap backend start requested");
// Bring up the WSL backend if the user previously enabled it. The
// primary is already starting; reconcile fires off the WSL register
// in parallel rather than blocking primary readiness on a possibly
// slow first wsl.exe spawn.
yield* Effect.forkScoped(wslBackend.reconcile);
}
}).pipe(Effect.withSpan("desktop.bootstrap"));

Expand Down Expand Up @@ -241,10 +258,20 @@ const scopedProgram = Effect.scoped(
yield* Effect.annotateCurrentSpan({ scope: "desktop", runId });

const shutdown = yield* DesktopShutdown.DesktopShutdown;
const backendManager = yield* DesktopBackendManager.DesktopBackendManager;

yield* Effect.addFinalizer(() =>
backendManager.stop().pipe(Effect.ensuring(shutdown.markComplete)),
Effect.gen(function* () {
const pool = yield* DesktopBackendPool.DesktopBackendPool;
// Stop every backend in the pool, not just the primary. The
// electronApp.quit() path can race ahead of the layer-scope
// cascade, so leaving the WSL instance for its parent scope
// finalizer means it gets hard-killed by the OS instead of
// receiving SIGTERM + grace. Stops run concurrently.
const instances = yield* pool.list;
yield* Effect.forEach(instances, (instance) => instance.stop(), {
concurrency: "unbounded",
});
}).pipe(Effect.ensuring(shutdown.markComplete)),
);

yield* startup;
Expand Down
122 changes: 0 additions & 122 deletions apps/desktop/src/app/DesktopBackendOutputLog.test.ts

This file was deleted.

Loading