Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
80 commits
Select commit Hold shift + click to select a range
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
e3e7cc3
fix(usage): price Claude fast-mode requests at the fast rate (#13599)
t3dotgg Sep 25, 2026
e55e731
fix: update OpenAI logo to current brand asset (#13611)
aaditagrawal Sep 25, 2026
1c12706
fix(mobile): render assigned project icons in chat list (#12810)
SunkenInTime Sep 25, 2026
e5a46d6
feat(usage): read cursor, opencode, and antigravity history (#10409)
maria-rcks Sep 25, 2026
d06f0ff
fix(sqlite): retry failed statement preparations (#10584)
yashranaway Sep 25, 2026
7a12aff
fix(mobile): scale Android controls with appearance text size (#13356)
none23 Sep 25, 2026
a4f6078
fix(web): return focus to the composer after saving a citation note (…
mackinleysmith Sep 25, 2026
fd996d1
feat(observability): honor the standard OTLP endpoint, headers, and p…
yordis Sep 25, 2026
1a0c915
fix(terminal): settling a thread closes its idle shells (#13673)
t3dotgg Sep 25, 2026
f5bd2fd
fix(server): load Cursor keyring with createRequire (#13678)
Yash-Singh1 Sep 25, 2026
4293433
perf(server): avoid rereading unchanged files in review previews (#13…
shivamhwp Sep 25, 2026
2598ce7
feat(desktop): honor the standard OTLP variables in the main process …
yordis Sep 25, 2026
ed809f7
fix(web): restore compact provider instance badges (#13700)
t3-code[bot] Sep 25, 2026
525af2d
perf(mobile): lighter Home rows away from the viewport (#13702)
AKolenda Sep 26, 2026
aade3a6
fix(usage): price Cursor cache savings by base model (#13731)
Yash-Singh1 Sep 26, 2026
595a1e1
perf(mobile): render Home rows further ahead while scrolling (#13705)
AKolenda Sep 26, 2026
2a9832b
refactor(observability): name each service after its application (#13…
yordis Sep 26, 2026
c216ba4
perf(server): stop remapping every thread on each thread event (#13720)
t3dotgg Sep 26, 2026
5660ab5
Remove unused items tracking from Claude adapter state (#13718)
t3dotgg Sep 26, 2026
20885ce
feat(observability): name the command on subprocess spans (#13701)
t3dotgg Sep 26, 2026
7c901a3
fix(cli): t3 triage points agents at log files that exist (#13685)
t3dotgg Sep 26, 2026
8aa5be2
fix(server): the SQLite WAL file shrinks back after large writes (#13…
t3dotgg Sep 26, 2026
75bf92e
feat(cli): summarize the server trace file from the command line (#13…
t3dotgg Sep 26, 2026
6530de0
perf(server): pull request sync reads only threads with linked pull r…
t3dotgg Sep 26, 2026
1dc8cbe
feat(observability): write a server heap snapshot on SIGUSR2 (#13694)
t3dotgg Sep 26, 2026
574b180
perf(server): shutdown no longer rewrites every stopped session row (…
t3dotgg Sep 26, 2026
b6eefc9
perf(server): build the thread list snapshot without decoding it twic…
t3dotgg Sep 26, 2026
b2577d6
fix(client): slow servers finish loading the thread list instead of l…
t3dotgg Sep 26, 2026
6989856
perf(web): hidden terminal drawers no longer keep full thread history…
t3dotgg Sep 26, 2026
3b0a495
perf(server): per-thread settlement and PR checks no longer rebuild t…
t3dotgg Sep 26, 2026
ecd3237
fix(mobile): running threads open at the latest message (#13530)
AKolenda Sep 26, 2026
ee18e56
feat(observability): record event loop stalls in the server trace (#1…
t3dotgg Sep 26, 2026
999161e
perf(server): stop re-running git for every project each minute (#13689)
t3dotgg Sep 26, 2026
94c4216
fix(usage): hide the Cursor keychain prompt when Cursor isn't set up …
Gigioxx Sep 26, 2026
04c15f3
feat(web): add chat width setting for wide screens (#11594)
otavio Sep 26, 2026
4408bf5
fix(opencode): accept v2 serve ready line when spawning server (#13651)
shirishpothi Sep 26, 2026
1e844a3
fix(editors): stop treating the agy CLI as the Antigravity IDE (#7079)
ishaanko Sep 26, 2026
eeea71a
fix(web): make the empty workspace draggable on desktop (#13713)
otavio Sep 26, 2026
dd8332d
fix(server): installed editors no longer vanish when discovery is slo…
bfowler Sep 26, 2026
29a3266
fix(git): exclude SSH ports from provider URLs (#12537)
GaMeRaM Sep 26, 2026
5d707bb
fix(web): Mod+B bolds on non-Latin layouts (#13409)
ValeraZSD Sep 26, 2026
a21b42c
fix(server): prune expired replay-protection files from the secrets d…
t3dotgg Sep 26, 2026
e42ccbd
chore(v2): merge main into the V2 branch (48 commits)
juliusmarminge Sep 26, 2026
433ee8b
perf(server): port #13698 to V2 — register the trace CLI command
juliusmarminge Sep 26, 2026
8bc9b78
fix(web): terminal links drop a trailing colon (#13408)
ValeraZSD Sep 26, 2026
4923ff4
fix(server): bump node-pty to 1.2.0-beta.15 for linux-arm64 prebuild …
Ephraim-9 Sep 26, 2026
ace70ca
perf(server): port #13673 to V2 — settling a thread closes its idle s…
juliusmarminge Sep 26, 2026
92f0af2
Show a focus ring on sidebar thread and draft rows (#13344)
ryanilano Sep 26, 2026
dab9561
fix(mobile): keep composer within folded screen after resume (#13310)
PixPMusic Sep 26, 2026
af6754b
perf(server): port #13704 to V2 — pull request sync reads only thread…
juliusmarminge Sep 26, 2026
2679d27
fix(server): let OpenCode generate session titles (#13368)
macodev00 Sep 26, 2026
437b334
perf(server): port #13691 to V2 — per-thread settlement and PR checks…
juliusmarminge Sep 26, 2026
2b9af8f
perf(server): port #13689 to V2 — post-turn PR discovery refreshes th…
juliusmarminge Sep 26, 2026
fe76961
fix(web): port #11594 to V2 — the chat width setting widens V2's chat…
juliusmarminge Sep 26, 2026
def34c2
fix(server): let Antigravity inspect unsupported files by path (#13339)
Bil0000 Sep 26, 2026
fe6388f
fix(mobile): link URLs with ports and single-label hosts (#13795)
Yash-Singh1 Sep 26, 2026
daafcc4
feat(web): add keyboard navigation for usage (#10158)
tris203 Sep 26, 2026
99efaea
perf(observability): stop writing empty spans on spawns, projected ev…
t3dotgg Sep 26, 2026
8b873ea
perf(server): opening Diagnostics no longer loads the whole trace rin…
t3dotgg Sep 26, 2026
6f97b0f
perf(clients): sort projects and settled threads without re-parsing d…
t3dotgg Sep 26, 2026
294dd12
fix(observability): the renderer trace proxy stops tracing itself (#1…
t3dotgg Sep 26, 2026
1d6f23b
perf(server): background sweeps only read threads that can still sett…
t3dotgg Sep 26, 2026
9151ea4
perf(clients): saving the thread list cache no longer freezes the UI …
t3dotgg Sep 26, 2026
8872666
perf(server): cut idle wakeups from the Connect relay and session rea…
t3dotgg Sep 26, 2026
81e0491
fix(mobile): keep trailing underscores and tildes in autolinked URLs …
Yash-Singh1 Sep 26, 2026
295d7cb
fix(web): queued messages send while their thread is not open (#13764)
t3dotgg Sep 26, 2026
95030dc
fix(server): background git status fetches no longer fill the disk wi…
t3dotgg Sep 26, 2026
d07c035
chore(v2): merge the V2 branch head (0481b76bec) into the main merge
juliusmarminge Sep 26, 2026
8119814
chore(v2): merge main into the V2 branch (second batch, 18 commits)
juliusmarminge Sep 26, 2026
1b10d08
fix(web): port #10158 to V2 — Usage shortcuts sort the same whatever …
juliusmarminge Sep 26, 2026
11acae1
perf(server): port #13765 to V2 — background PR discovery reads only …
juliusmarminge Sep 26, 2026
df06b8b
fix(server): port #13368 to V2 — OpenCode names its own sessions
juliusmarminge Sep 26, 2026
d45989a
docs: port #13339 to V2 — Antigravity attachments reach the agent by …
juliusmarminge Sep 26, 2026
b049c24
chore(v2): merge the V2 branch head (2c388dae9a) into the main merge
juliusmarminge Sep 26, 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
5 changes: 3 additions & 2 deletions .github/triage/PLAYBOOK.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,9 @@ different code depending on it:

Then work from evidence, not assumption. In rough order of value:

- The server log and the trace file (`server.trace.ndjson`) around the time of the
problem. Recent failures usually leave a trail here.
- The trace file (`server.trace.ndjson`) around the time of the problem, plus the
service log or desktop backend logs from the context file if they exist. Recent
failures usually leave a trail here.
- The provider event log, for problems with claude/codex/cursor sessions.
- The SQLite database. Read it freely, but only write when a write is necessary
to fix the problem the user described, and get their explicit permission
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