Skip to content
Merged
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
c07575f
feat(server): show finished paragraphs and code blocks while the resp…
t3dotgg Sep 14, 2026
2d73746
fix(web): disconnect offline servers from threads (#11671)
t3dotgg Sep 14, 2026
5e961d3
feat(web): flatten the connections page into one environments list (#…
t3dotgg Sep 14, 2026
5647191
fix(mobile): keep usage widget rows consistently sized (#11669)
juliusmarminge Sep 14, 2026
3b75e60
feat(server): add reusable auth token for dev worktrees (#8606)
t3dotgg Sep 14, 2026
1bbca0e
feat(settings): choose how responses stream, with a warning on legacy…
t3dotgg Sep 14, 2026
d81278a
revert(web): remove the compact sidebar (#11685)
maria-rcks Sep 14, 2026
683aa87
build(desktop): bundle the main process and stage only its native ext…
juliusmarminge Sep 14, 2026
06de59b
build(server): make the CLI bundle loadable as a Node single-executab…
juliusmarminge Sep 14, 2026
eb8f6f4
ci(release): build, sign, and publish self-contained CLI archives (#1…
juliusmarminge Sep 14, 2026
8f90b38
feat(server): install preview runtimes from release archives (#11318)
juliusmarminge Sep 14, 2026
13c134c
feat(ssh): run preview builds on remotes from the release archive (#1…
juliusmarminge Sep 14, 2026
c7f23c4
feat(cli): add t3 update for self-contained installs (#11451)
juliusmarminge Sep 14, 2026
af6c138
feat(server): manage runtimes as release archives only, never from np…
juliusmarminge Sep 14, 2026
0754920
feat(desktop): run the WSL backend from the Linux CLI archive (#11511)
juliusmarminge Sep 14, 2026
2c54f2f
ci(release): build CLI archives for five targets, each on its own arc…
juliusmarminge Sep 14, 2026
2f7616e
ci(release): build the JS bundle once and run every platform and arch…
juliusmarminge Sep 14, 2026
91cd91c
feat(release): publish npx t3 as a launcher over per-platform executa…
juliusmarminge Sep 14, 2026
b70015b
feat(cli): add t3 uninstall for self-contained installs (#11659)
juliusmarminge Sep 14, 2026
73b206f
feat(web): show each worktree setup step and let users cancel it (#11…
t3dotgg Sep 14, 2026
1ced38a
fix(server): skip device hosts that resolve to the local machine (#11…
juliusmarminge Sep 14, 2026
8984f81
fix(web): test device hosts across selected environments (#11699)
juliusmarminge Sep 14, 2026
0b54e00
Change input type from 'full_diff' to 'incremental'
juliusmarminge Sep 14, 2026
e3792a5
Update model and input type in ui-consistency.md
juliusmarminge Sep 14, 2026
cba7dd7
feat(desktop): allow disabling the local environment (#9194)
juliusmarminge Sep 14, 2026
d8655ed
feat(cli): add t3 service restart and make t3 update repoint the serv…
juliusmarminge Sep 14, 2026
01e05c1
docs(claude): clarify OpenRouter model selection (#11369)
shivamhwp Sep 14, 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
528 changes: 528 additions & 0 deletions .github/workflows/release-desktop.yml

Large diffs are not rendered by default.

813 changes: 358 additions & 455 deletions .github/workflows/release.yml

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ node_modules
*.log
*.tsbuildinfo
apps/*/dist
apps/*/dist-exe
infra/*/dist
.astro
packages/*/dist
Expand Down
2 changes: 1 addition & 1 deletion .macroscope/check-run-agents/effect-service-conventions.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
title: Effect Service Conventions
model: gpt-5-6-sol
effort: medium
input: full_diff
input: incremental
tools:
- browse_code
- modify_pr
Expand Down
4 changes: 2 additions & 2 deletions .macroscope/check-run-agents/ui-consistency.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
---
title: UI Consistency
model: gpt-5-6-terra
model: gpt-5-6-sol
effort: medium
input: full_diff
input: incremental
tools:
- browse_code
- modify_pr
Expand Down
4 changes: 2 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,8 @@ The most common defect in this repo is a change that works on the path you teste
- `vp i` installs. Worktrees get this from the t3.json setup script; if module resolution looks broken, it probably did not run.
- `vp run dev` starts server and web. In a worktree, state defaults to that worktree's gitignored `.t3`, which deliberately outranks an ambient `T3CODE_HOME` so you cannot land on shared state by accident. An explicit `--home-dir` still wins.
- Ports derive from the worktree path and are stable across restarts, but read the real ones from the `[dev-runner]` line since occupied ports shift.
- Sharing over the tailnet is three steps: run `vp run dev --share` in the background, wait for the `pairingUrl:` line in its output, paste that full URL (token included) in your reply. Do not wire up `tailscale serve` by hand for this, and do not open the URL yourself.
- The web app requires pairing. Hand over the pairing URL, not the bare origin. A URL without its token is useless to whoever you gave it to. If the token got consumed, mint a fresh one with `node apps/server/src/bin.ts pair` — note it carries standard scopes, while the startup URL carries admin scopes (needed for Settings → Connections management).
- Sharing over the tailnet is three steps: run `vp run dev --share` in the background, wait for the `pairingUrl:` line in its output, then give that full URL to an unpaired browser. Do not wire up `tailscale serve` by hand, open the URL yourself, or consume the user's pairing link. A browser with the reusable dev cookie can use the bare origin. If a normal one-time token was consumed, mint a fresh one with `node apps/server/src/bin.ts pair`. It carries standard scopes, while the startup URL carries admin scopes needed for Connections settings.
- To reuse web dev auth across worktrees, configure one fixed `T3CODE_DEV_AUTH_TOKEN` in the main checkout's gitignored `.env`. The `t3.json` setup links that file into worktrees. Never commit or publish the token or a startup URL. See [Reusable dev credential](docs/operations/development.md#reusable-dev-credential).
- Stop what you started, by the PID you tracked. See rule 1.

## Test data
Expand Down
57 changes: 34 additions & 23 deletions apps/desktop/src/app/DesktopApp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,18 +158,43 @@ export const stopAllPoolInstances = Effect.fn("desktop.app.stopAllPoolInstances"
);

const bootstrap = Effect.gen(function* () {
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;
const snapShot = yield* DesktopSnapShot.DesktopSnapShot;
const appActivation = yield* DesktopAppActivation.DesktopAppActivation;
yield* logBootstrapInfo("bootstrap start");

const settings = yield* desktopSettings.get;
// The renderer is served from the bundled client (or Vite in development)
// rather than through the local backend, so the window can open without one.
const electronProtocol = yield* ElectronProtocol.ElectronProtocol;
yield* electronProtocol.registerDesktopProtocol({
scheme: ElectronProtocol.getDesktopScheme(environment.isDevelopment),
...(environment.isDevelopment
? { targetOrigin: Option.getOrThrow(environment.devServerUrl) }
: { assetDirectory: environment.clientAssetsDir }),
clerkFrontendApiHostname: DesktopClerk.desktopClerkFrontendApiHostname,
});
yield* installDesktopIpcHandlers();
yield* logBootstrapInfo("bootstrap ipc handlers registered");

yield* snapShot.initialize;

if (!settings.localEnvironmentEnabled) {
yield* logBootstrapInfo("bootstrap skipping local environment (disabled in settings)");
if (!(yield* Ref.get(state.quitting))) {
yield* desktopWindow.createMainIfBackendReady;
}
return;
}

const pool = yield* DesktopBackendPool.DesktopBackendPool;
const primaryBackend = yield* pool.primary;
const serverExposure = yield* DesktopServerExposure.DesktopServerExposure;
const wslBackend = yield* DesktopWslBackend.DesktopWslBackend;

if (environment.isDevelopment && Option.isNone(environment.configuredBackendPort)) {
return yield* new DesktopDevelopmentBackendPortRequiredError();
}
Expand All @@ -186,24 +211,13 @@ const bootstrap = Effect.gen(function* () {
},
);

const settings = yield* desktopSettings.get;
if (settings.serverExposureMode !== environment.defaultDesktopSettings.serverExposureMode) {
yield* logBootstrapInfo("bootstrap restoring persisted server exposure mode", {
mode: settings.serverExposureMode,
});
}
const serverExposureState = yield* serverExposure.configureFromSettings({ port: backendPort });
const backendConfig = yield* serverExposure.backendConfig;
const electronProtocol = yield* ElectronProtocol.ElectronProtocol;
const rendererTarget = environment.isDevelopment
? Option.getOrThrow(environment.devServerUrl)
: backendConfig.httpBaseUrl;
yield* electronProtocol.registerDesktopProtocol({
scheme: ElectronProtocol.getDesktopScheme(environment.isDevelopment),
targetOrigin: rendererTarget,
backendOrigin: backendConfig.httpBaseUrl,
clerkFrontendApiHostname: DesktopClerk.desktopClerkFrontendApiHostname,
});
yield* logBootstrapInfo("bootstrap resolved backend endpoint", {
baseUrl: backendConfig.httpBaseUrl.href,
});
Expand All @@ -219,16 +233,13 @@ const bootstrap = Effect.gen(function* () {
"bootstrap fell back to local-only because no advertised network host was available",
);
}
yield* snapShot.initialize;

yield* installDesktopIpcHandlers();
yield* logBootstrapInfo("bootstrap ipc handlers registered");

if (!(yield* Ref.get(state.quitting))) {
// 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.)
// The main window waits for the primary backend. In wsl-only mode that is
// 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;
}
Expand Down
4 changes: 4 additions & 0 deletions apps/desktop/src/app/DesktopEnvironment.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,10 @@ describe("DesktopEnvironment", () => {
environment.backendEntryPath,
"/install/resources/server.asar/apps/server/dist/bin.mjs",
);
assert.equal(
environment.clientAssetsDir,
"/install/resources/server.asar/apps/server/dist/client",
);
}),
);

Expand Down
3 changes: 3 additions & 0 deletions apps/desktop/src/app/DesktopEnvironment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ export class DesktopEnvironment extends Context.Service<
// extracts on demand (see DesktopWslServerTree).
readonly serverRoot: string;
readonly backendEntryPath: string;
// Built web client the packaged renderer is served from over t3code://app.
readonly clientAssetsDir: string;
readonly backendCwd: string;
readonly preloadPath: string;
readonly appUpdateYmlPath: string;
Expand Down Expand Up @@ -211,6 +213,7 @@ const make = Effect.fn("desktop.environment.make")(function* (
appRoot,
serverRoot,
backendEntryPath: path.join(serverRoot, "apps/server/dist/bin.mjs"),
clientAssetsDir: path.join(serverRoot, "apps/server/dist/client"),
backendCwd: input.isPackaged ? homeDirectory : appRoot,
preloadPath: path.join(input.dirname, "preload.cjs"),
appUpdateYmlPath: input.isPackaged
Expand Down
101 changes: 44 additions & 57 deletions apps/desktop/src/backend/DesktopBackendConfiguration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -374,9 +374,10 @@ describe("DesktopBackendConfiguration", () => {
runtimeId: string;
sha256: string;
}> = [];
const observedNodePtyRoots: string[] = [];
const observedProbeRoots: string[] = [];
let legacyCleanupCount = 0;
const linuxAppRoot = "/home/test/.t3/wsl-runtime/1.2.3-x64";
const resolvedPath = "/home/test/.local/bin:/usr/bin:/bin";

return withPackagedWslHarness(
{
Expand All @@ -394,9 +395,14 @@ describe("DesktopBackendConfiguration", () => {
});
return { ok: true, linuxAppRoot };
},
ensureNodePty: (_distro, root) => {
observedNodePtyRoots.push(root);
return { ok: true, nodePath: "/usr/bin/node", resolvedPath: "/usr/bin:/bin" };
probeRuntime: (_distro, root) => {
observedProbeRoots.push(root);
return { ok: true, resolvedPath };
},
// The staged runtime carries its own Node, so the preflight must not
// go looking for one in the distro.
ensureNodePty: () => {
throw new Error("the staged runtime must not probe for Node");
},
}),
},
Expand All @@ -413,12 +419,22 @@ describe("DesktopBackendConfiguration", () => {
sha256: archiveHash,
},
]);
assert.deepEqual(observedNodePtyRoots, [linuxAppRoot]);
assert.deepEqual(observedProbeRoots, [linuxAppRoot]);
assert.equal(
config.entryPath,
path.join(baseDir, "server.asar/apps/server/dist/bin.mjs"),
);
assert.include(config.args, `${linuxAppRoot}/apps/server/dist/bin.mjs`);
assert.deepEqual(config.args, [
"-d",
"Ubuntu",
"--exec",
"env",
`PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:${resolvedPath}`,
`${linuxAppRoot}/t3`,
"--bootstrap-fd",
"0",
]);
assert.notInclude(config.args, "/usr/bin/node");
assert.equal(config.wslRuntimeId, `sha256-${archiveHash}`);
assert.equal(legacyCleanupCount, 1);
assert.isTrue(Option.isNone(config.preflightFailure));
Expand Down Expand Up @@ -457,8 +473,11 @@ describe("DesktopBackendConfiguration", () => {

assert.deepEqual(observedRuntimeIds, [`sha256-${firstHash}`, `sha256-${secondHash}`]);
assert.equal(first.wslRuntimeId, observedRuntimeIds[0]);
assert.include(first.args, `/runtime/sha256-${firstHash}/t3`);
assert.equal(second.wslRuntimeId, observedRuntimeIds[1]);
assert.include(second.args, `/runtime/sha256-${secondHash}/t3`);
assert.isUndefined(invalidIdentity.wslRuntimeId);
assert.include(invalidIdentity.args, "/usr/bin/node");
assert.include(invalidIdentity.args, `${mountedAppRoot}/apps/server/dist/bin.mjs`);
}),
);
Expand All @@ -484,16 +503,18 @@ describe("DesktopBackendConfiguration", () => {

assert.deepEqual(observedNodePtyRoots, [mountedAppRoot]);
assert.equal(config.entryPath, mountedEntryPath);
assert.include(config.args, "/usr/bin/node");
assert.include(config.args, `${mountedAppRoot}/apps/server/dist/bin.mjs`);
assert.isUndefined(config.wslRuntimeId);
assert.isTrue(Option.isNone(config.preflightFailure));
}),
);
});

it.effect("resolveWsl retires a staged runtime that cannot load node-pty", () => {
it.effect("resolveWsl retires a staged runtime whose executable does not start", () => {
const archiveHash = "c".repeat(64);
const stagedAppRoot = `/home/test/.t3/wsl-runtime/sha256-${archiveHash}`;
const observedProbeRoots: string[] = [];
const observedNodePtyRoots: string[] = [];
const invalidatedRuntimeIds: string[] = [];
return withPackagedWslHarness(
Expand All @@ -505,11 +526,13 @@ describe("DesktopBackendConfiguration", () => {
Effect.sync(() => {
invalidatedRuntimeIds.push(runtimeId);
}),
probeRuntime: (_distro, root) => {
observedProbeRoots.push(root);
return { ok: false, reason: `${root}/t3 --version failed (exit 127)` };
},
ensureNodePty: (_distro, root) => {
observedNodePtyRoots.push(root);
return root === stagedAppRoot
? { ok: false, reason: "pty.node could not be loaded", fatal: true }
: { ok: true, nodePath: "/usr/bin/node", resolvedPath: "/usr/bin:/bin" };
return { ok: true, nodePath: "/usr/bin/node", resolvedPath: "/usr/bin:/bin" };
},
}),
},
Expand All @@ -518,8 +541,11 @@ describe("DesktopBackendConfiguration", () => {
const configuration = yield* DesktopBackendConfiguration.DesktopBackendConfiguration;
const config = yield* configuration.resolveWsl({ port: 5000, distro: "Ubuntu" });

assert.deepEqual(observedNodePtyRoots, [stagedAppRoot, mountedAppRoot]);
assert.deepEqual(observedProbeRoots, [stagedAppRoot]);
assert.deepEqual(observedNodePtyRoots, [mountedAppRoot]);
assert.include(config.args, "/usr/bin/node");
assert.include(config.args, `${mountedAppRoot}/apps/server/dist/bin.mjs`);
assert.notInclude(config.args, `${stagedAppRoot}/t3`);
assert.equal(config.entryPath, mountedEntryPath);
assert.isUndefined(config.wslRuntimeId);
assert.isTrue(Option.isNone(config.preflightFailure));
Expand All @@ -540,12 +566,13 @@ describe("DesktopBackendConfiguration", () => {
Effect.sync(() => {
invalidatedRuntimeIds.push(runtimeId);
}),
ensureNodePty: (_distro, root) => ({
probeRuntime: () => ({
ok: false,
reason:
root === stagedAppRoot
? "unsupported CPU architecture or incompatible system libraries"
: "mounted tree is broken in some other way",
reason: "unsupported CPU architecture or incompatible system libraries",
}),
ensureNodePty: () => ({
ok: false,
reason: "mounted tree is broken in some other way",
fatal: true,
}),
}),
Expand Down Expand Up @@ -575,51 +602,11 @@ describe("DesktopBackendConfiguration", () => {
Effect.sync(() => {
invalidatedRuntimeIds.push(runtimeId);
}),
ensureNodePty: (_distro, root) =>
root === stagedAppRoot
? { ok: false, reason: "pty.node could not be loaded", fatal: true }
: {
ok: false,
reason: "WSL backend preflight timed out while probing for Node.js.",
fatal: false,
},
}),
},
() =>
Effect.gen(function* () {
const configuration = yield* DesktopBackendConfiguration.DesktopBackendConfiguration;
const config = yield* configuration.resolveWsl({ port: 5000, distro: "Ubuntu" });
const failure = Option.getOrThrow(config.preflightFailure);

assert.isFalse(failure.fatal);
assert.equal(failure.retryLimit, 12);
assert.include(failure.reason, "timed out");
assert.deepEqual(invalidatedRuntimeIds, []);
}),
);
});

it.effect("resolveWsl retries the staged runtime after a transient probe failure", () => {
const invalidatedRuntimeIds: string[] = [];
return withPackagedWslHarness(
{
archiveHash: "e".repeat(64),
forbidFallback: "A transient probe failure must not extract the fallback",
forbidCleanup: "A transient probe failure must not clean the fallback tree",
wsl: () => ({
prepareRuntime: () => ({
ok: true,
linuxAppRoot: "/home/test/.t3/wsl-runtime/cache",
}),
invalidateRuntime: (_distro, runtimeId) =>
Effect.sync(() => {
invalidatedRuntimeIds.push(runtimeId);
}),
probeRuntime: () => ({ ok: false, reason: "t3 --version failed (exit 1)" }),
ensureNodePty: () => ({
ok: false,
reason: "WSL backend preflight timed out while probing for Node.js.",
fatal: false,
retryLimit: 12,
}),
}),
},
Expand Down
Loading
Loading