Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
0cab7d5
fix(server): Grok accounts with no usage yet no longer vanish from Li…
jakeleventhal Sep 25, 2026
91e53e5
fix(server): report the Grok account email so usage limits merge acro…
jakeleventhal Sep 25, 2026
3e2370f
feat(web): add usage page keybinding (#9434)
jakeleventhal Sep 25, 2026
568c9bc
chore: clear Effect language service suggestions (#13536)
juliusmarminge Sep 25, 2026
fc46b8c
ci(relay): add a forced manual relay deploy (#13550)
juliusmarminge Sep 25, 2026
ebdcda1
fix(web): selected text stays visible on a revealed file line (#13548)
t3dotgg Sep 25, 2026
29abbf9
fix(web): collapsed composer bar stops flipping its labels while you …
t3dotgg Sep 25, 2026
ab70c89
fix(clients): sync status no longer flickers when opening running thr…
t3dotgg Sep 25, 2026
6391be2
fix(server): newer Codex models get T3 Code's instructions again (#13…
t3dotgg Sep 25, 2026
9957349
feat(web): control Android foldables in the Device panel (#13534)
juliusmarminge Sep 25, 2026
4f27a84
fix(mcp): preview snapshots fit in the agent's tool output again (#13…
t3dotgg Sep 25, 2026
0c84b42
fix(web): paste after clicking away from the composer lands in it aga…
t3dotgg Sep 25, 2026
99641fd
feat(desktop): keep running threads synced in the background (#13554)
t3dotgg Sep 25, 2026
1e192b2
fix(mcp): preview errors tell agents what to do instead (#13559)
t3dotgg Sep 25, 2026
df9826f
feat(web): agents working banner links to the Agents panel (#13572)
t3dotgg Sep 25, 2026
7b84431
fix(web): size the Android fold model from the inner display (#13574)
juliusmarminge Sep 25, 2026
a107f8a
fix(clients): a preview app no longer knocks the desktop's own server…
t3dotgg Sep 25, 2026
20f0ff1
fix(web): keep nested task states out of parent bullets (#11477)
dominic-r Sep 25, 2026
c13f7d9
feat(release): ship a Linux .deb that updates itself (#13575)
t3dotgg Sep 25, 2026
d10bd13
perf(desktop): cache compiled JavaScript between launches (#13501)
t3dotgg Sep 25, 2026
b3de243
fix(dev): one t3.json setup action that works on every OS (#13589)
t3dotgg Sep 25, 2026
13d6b30
fix(web): new worktree threads no longer say "checkout" during setup …
t3dotgg Sep 25, 2026
86054b6
fix(desktop): `t3 app` keeps working after a second desktop app quits…
t3dotgg Sep 25, 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
13 changes: 12 additions & 1 deletion .github/workflows/deploy-relay.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,15 @@ on:
push:
branches:
- main
# Alchemy does not redeploy the Worker when only a Config value read in its
# Init changes (alchemy-run/alchemy#1831), so a changed repository variable
# needs a forced deploy to reach production.
workflow_dispatch:
inputs:
force:
description: Redeploy every resource, including ones with no detected changes
type: boolean
default: true

permissions:
contents: read
Expand All @@ -17,6 +26,8 @@ concurrency:
jobs:
deploy_relay:
name: Deploy production relay
# A manual run from another branch would deploy that branch to production.
if: github.ref == 'refs/heads/main'
runs-on: blacksmith-8vcpu-ubuntu-2404
timeout-minutes: 15
environment:
Expand Down Expand Up @@ -55,7 +66,7 @@ jobs:
- --filter=t3code-relay...

- name: Deploy production relay stage
run: vp run --filter t3code-relay deploy --stage prod --yes --no-input
run: vp run --filter t3code-relay deploy --stage prod --yes --no-input ${{ inputs.force && '--force' || '' }}
env:
# The PublishClientConfig action writes the client env here instead
# of the repo-root .env; nothing on the runner reads it.
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/release-desktop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -510,6 +510,7 @@ jobs:
"release/*.dmg"
"release/*.zip"
"release/*.AppImage"
"release/*.deb"
"release/*.exe"
)
# Preview builds have no publish config, so electron-builder writes
Expand Down
3 changes: 2 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -625,7 +625,7 @@ jobs:
clerk_cli_oauth_client_id: ${{ needs.relay_public_config.outputs.clerk_cli_oauth_client_id }}
relay_url: ${{ needs.relay_public_config.outputs.relay_url }}
label: Linux arm64
runner: ubuntu-24.04-arm
runner: blacksmith-16vcpu-ubuntu-2404-arm
platform: linux
target: AppImage
arch: arm64
Expand Down Expand Up @@ -875,6 +875,7 @@ jobs:
echo 'release-assets/*.dmg'
echo 'release-assets/*.zip'
echo 'release-assets/*.AppImage'
echo 'release-assets/*.deb'
echo 'release-assets/*.exe'
if [[ "${{ needs.preflight.outputs.release_channel }}" != "preview" ]]; then
echo 'release-assets/*.blockmap'
Expand Down
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,14 @@ winget install T3Tools.T3Code
brew install --cask t3-code
```

#### Debian, Ubuntu (`.deb`)

Download the `.deb` from [GitHub Releases](https://github.com/pingdotgg/t3code/releases), then:

```bash
sudo apt install ./T3-Code-*.deb
```

#### Arch Linux (AUR)

Stable:
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"version": "0.0.42",
"private": true,
"type": "module",
"main": "dist-electron/main.cjs",
"main": "dist-electron/boot.cjs",
"scripts": {
"ensure:electron": "node scripts/ensure-electron-runtime.mjs",
"start": "node scripts/start-electron.mjs",
Expand Down
63 changes: 63 additions & 0 deletions apps/desktop/scripts/main-process-bundle.test.mjs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import * as NodeChildProcess from "node:child_process";
import * as NodeFSP from "node:fs/promises";
import * as NodeOS from "node:os";
import * as NodePath from "node:path";
Expand Down Expand Up @@ -108,3 +109,65 @@ void import("./linux.ts").then(({ result }) => process.emit("ready", result));`,
await NodeFSP.rm(directory, { recursive: true, force: true });
}
});

it("loads the emitted packaged boot entry and backend cache preload", async () => {
const directory = await NodeFSP.mkdtemp(NodePath.join(NodeOS.tmpdir(), "t3-desktop-boot-"));
try {
const entries = ["src/boot.ts", "src/compileCache.ts"];
await NodeFSP.mkdir(NodePath.join(directory, "src"));
await Promise.all(
entries.map((entry) =>
NodeFSP.copyFile(new URL(`../${entry}`, import.meta.url), NodePath.join(directory, entry)),
),
);
assert.ok(Array.isArray(desktopConfig.pack));
for (const packConfig of desktopConfig.pack) {
if (!Array.isArray(packConfig.entry)) continue;
if (!packConfig.entry.some((entry) => entries.includes(entry))) continue;
await build({
...packConfig,
config: false,
cwd: directory,
tsconfig: false,
sourcemap: false,
onSuccess: undefined,
logLevel: "silent",
});
}
const outputDirectory = NodePath.join(directory, "dist-electron");
const fixture = `console.log(require('node:module').getCompileCacheDir() ? 'cached' : 'uncached');`;
await NodeFSP.writeFile(NodePath.join(outputDirectory, "main.cjs"), fixture);
await NodeFSP.writeFile(
NodePath.join(outputDirectory, "backend.mjs"),
`import { getCompileCacheDir } from 'node:module'; console.log(getCompileCacheDir() ? 'cached' : 'uncached');`,
);
for (const disabled of [false, true]) {
for (const args of [
[NodePath.join(outputDirectory, "boot.cjs")],
[
"--require",
NodePath.join(outputDirectory, "compileCache.cjs"),
NodePath.join(outputDirectory, "backend.mjs"),
],
]) {
const child = NodeChildProcess.spawnSync(process.execPath, args, {
encoding: "utf8",
env: {
...process.env,
APPIMAGE: "",
NODE_COMPILE_CACHE: undefined,
NODE_DISABLE_COMPILE_CACHE: disabled ? "1" : undefined,
XDG_CACHE_HOME: directory,
TMPDIR: directory,
TEMP: directory,
TMP: directory,
},
});
assert.equal(child.status, 0, child.stderr);
assert.equal(child.stdout.trim(), disabled ? "uncached" : "cached");
}
}
} finally {
await NodeFSP.rm(directory, { recursive: true, force: true });
}
});
2 changes: 1 addition & 1 deletion apps/desktop/scripts/smoke-test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { resolveElectronLaunchCommand } from "./electron-launcher.mjs";

const __dirname = NodePath.dirname(NodeURL.fileURLToPath(import.meta.url));
const desktopDir = NodePath.resolve(__dirname, "..");
const mainJs = NodePath.resolve(desktopDir, "dist-electron/main.cjs");
const mainJs = NodePath.resolve(desktopDir, "dist-electron/boot.cjs");

console.log("\nLaunching Electron smoke test...");

Expand Down
67 changes: 67 additions & 0 deletions apps/desktop/src/app/DesktopAppActivation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,25 @@ function request(requestId: string, platform: NodeJS.Platform): DesktopAppActiva
};
}

function startOkServer(target: ReturnType<typeof makeTarget>, userId: number | undefined) {
return startDesktopAppControlServer({
...target,
userId,
handle: async (input) => ({
version: 1,
requestId: input.requestId,
ok: true,
projectId: ProjectId.make("project-1"),
threadId: ThreadId.make("thread-1"),
}),
cancel: () => undefined,
onReclaimError: () => undefined,
}).then((server) => {
openServers.push(server);
return server;
});
}

function exchange(address: string, payload: DesktopAppActivationRequest) {
return new Promise<DesktopAppActivationResponse>((resolve, reject) => {
const socket = NodeNet.createConnection(address);
Expand Down Expand Up @@ -84,6 +103,7 @@ describe("desktop app control server", () => {
};
},
cancel: () => undefined,
onReclaimError: () => undefined,
});
openServers.push(server);

Expand Down Expand Up @@ -117,6 +137,7 @@ describe("desktop app control server", () => {
userId,
handle: () => new Promise(() => undefined),
cancel: resolveCanceled,
onReclaimError: () => undefined,
});
openServers.push(server);
const socket = NodeNet.createConnection(target.address);
Expand All @@ -137,4 +158,50 @@ describe("desktop app control server", () => {
});
}),
);

// Two desktop apps can share one state dir, such as nightly and a preview build.
it.effect("keeps a newer app's socket when an older app on the same state dir quits", () =>
Effect.gen(function* () {
const platform = yield* HostProcessPlatform;
const userId = yield* HostProcessUserId;
if (platform === "win32") return;
yield* Effect.promise(async () => {
const root = await NodeFSP.mkdtemp(NodePath.join(NodeOS.tmpdir(), "t3-app-takeover-test-"));
const target = makeTarget(NodePath.join(root, "userdata"), platform, userId);
const older = await startOkServer(target, userId);
await startOkServer(target, userId);

await older.close();

await expect(
exchange(target.address, request("after-quit", platform)),
).resolves.toMatchObject({ ok: true, requestId: "after-quit" });
await NodeFSP.rm(root, { recursive: true, force: true });
});
}),
);

it.effect("binds its address again after the socket file is removed", () =>
Effect.gen(function* () {
const platform = yield* HostProcessPlatform;
const userId = yield* HostProcessUserId;
if (platform === "win32") return;
yield* Effect.promise(async () => {
const root = await NodeFSP.mkdtemp(NodePath.join(NodeOS.tmpdir(), "t3-app-reclaim-test-"));
const target = makeTarget(NodePath.join(root, "userdata"), platform, userId);
const server = await startOkServer(target, userId);

await NodeFSP.unlink(target.address);
await server.reclaim();

await expect(
exchange(target.address, request("reclaimed", platform)),
).resolves.toMatchObject({
ok: true,
requestId: "reclaimed",
});
await NodeFSP.rm(root, { recursive: true, force: true });
});
}),
);
});
Loading
Loading