Skip to content

Commit e42ccbd

Browse files
chore(v2): merge main into the V2 branch (48 commits)
Merges origin/main (a21b42c) into t3code/codex-turn-mapping so V2 carries main's fixes. A merge rather than a rebase keeps the 715 V2 commits and the stacked PRs on this branch intact. Conflict resolutions that change V2 code: - server.ts: keep V2's layer graph; add main's ReplayMarkers layer next to the other background layers. - serverRuntimeStartup.ts: keep V2's imports plus main's flushCompileCache. - bin.ts: keep V2's thin entry (the command list lives in binCli.ts). - ProjectionSnapshotQuery (V1): drop the second shell-snapshot decode (#13693); do not add listThreadsWithPullRequests (V2's PR sync does not read the V1 table it queries). - PullRequestSyncReactor, ThreadPullRequestService: keep V2's versions. - CursorDriver / cursorUsageLimits: keep V2's SDK-based driver and pass main's cursorKeychainUsageEnabled opt-in through to usage limits. Main's lowercase cursorCredentialStore.ts collides by case with V2's CursorCredentialStore.ts, so it becomes cursorKeychainToken.ts. - Chat width (#11594): keep V2's chat-content-lane markup; main's settings row sits next to V2's composer-context row. - #13590 env-mode label: V2's thread details panel now passes envMode through to BranchToolbar like the composer strip does. - Sidebar provider stack badge uses main's text-5xs token instead of V2's one-off 7px class (same size). - Deleted-in-V2 V1 files that main modified stay deleted. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2 parents 70629cd + a21b42c commit e42ccbd

224 files changed

Lines changed: 7658 additions & 738 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.

‎.github/triage/PLAYBOOK.md‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,9 @@ different code depending on it:
5959

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

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

‎.github/workflows/release-desktop.yml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -510,6 +510,7 @@ jobs:
510510
"release/*.dmg"
511511
"release/*.zip"
512512
"release/*.AppImage"
513+
"release/*.deb"
513514
"release/*.exe"
514515
)
515516
# Preview builds have no publish config, so electron-builder writes

‎.github/workflows/release.yml‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -625,7 +625,7 @@ jobs:
625625
clerk_cli_oauth_client_id: ${{ needs.relay_public_config.outputs.clerk_cli_oauth_client_id }}
626626
relay_url: ${{ needs.relay_public_config.outputs.relay_url }}
627627
label: Linux arm64
628-
runner: ubuntu-24.04-arm
628+
runner: blacksmith-16vcpu-ubuntu-2404-arm
629629
platform: linux
630630
target: AppImage
631631
arch: arm64
@@ -875,6 +875,7 @@ jobs:
875875
echo 'release-assets/*.dmg'
876876
echo 'release-assets/*.zip'
877877
echo 'release-assets/*.AppImage'
878+
echo 'release-assets/*.deb'
878879
echo 'release-assets/*.exe'
879880
if [[ "${{ needs.preflight.outputs.release_channel }}" != "preview" ]]; then
880881
echo 'release-assets/*.blockmap'

‎README.md‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,14 @@ winget install T3Tools.T3Code
5454
brew install --cask t3-code
5555
```
5656

57+
#### Debian, Ubuntu (`.deb`)
58+
59+
Download the `.deb` from [GitHub Releases](https://github.com/pingdotgg/t3code/releases), then:
60+
61+
```bash
62+
sudo apt install ./T3-Code-*.deb
63+
```
64+
5765
#### Arch Linux (AUR)
5866

5967
Stable:

‎apps/desktop/package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"version": "0.0.42",
44
"private": true,
55
"type": "module",
6-
"main": "dist-electron/main.cjs",
6+
"main": "dist-electron/boot.cjs",
77
"scripts": {
88
"ensure:electron": "node scripts/ensure-electron-runtime.mjs",
99
"start": "node scripts/start-electron.mjs",

‎apps/desktop/scripts/main-process-bundle.test.mjs‎

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import * as NodeChildProcess from "node:child_process";
12
import * as NodeFSP from "node:fs/promises";
23
import * as NodeOS from "node:os";
34
import * as NodePath from "node:path";
@@ -108,3 +109,65 @@ void import("./linux.ts").then(({ result }) => process.emit("ready", result));`,
108109
await NodeFSP.rm(directory, { recursive: true, force: true });
109110
}
110111
});
112+
113+
it("loads the emitted packaged boot entry and backend cache preload", async () => {
114+
const directory = await NodeFSP.mkdtemp(NodePath.join(NodeOS.tmpdir(), "t3-desktop-boot-"));
115+
try {
116+
const entries = ["src/boot.ts", "src/compileCache.ts"];
117+
await NodeFSP.mkdir(NodePath.join(directory, "src"));
118+
await Promise.all(
119+
entries.map((entry) =>
120+
NodeFSP.copyFile(new URL(`../${entry}`, import.meta.url), NodePath.join(directory, entry)),
121+
),
122+
);
123+
assert.ok(Array.isArray(desktopConfig.pack));
124+
for (const packConfig of desktopConfig.pack) {
125+
if (!Array.isArray(packConfig.entry)) continue;
126+
if (!packConfig.entry.some((entry) => entries.includes(entry))) continue;
127+
await build({
128+
...packConfig,
129+
config: false,
130+
cwd: directory,
131+
tsconfig: false,
132+
sourcemap: false,
133+
onSuccess: undefined,
134+
logLevel: "silent",
135+
});
136+
}
137+
const outputDirectory = NodePath.join(directory, "dist-electron");
138+
const fixture = `console.log(require('node:module').getCompileCacheDir() ? 'cached' : 'uncached');`;
139+
await NodeFSP.writeFile(NodePath.join(outputDirectory, "main.cjs"), fixture);
140+
await NodeFSP.writeFile(
141+
NodePath.join(outputDirectory, "backend.mjs"),
142+
`import { getCompileCacheDir } from 'node:module'; console.log(getCompileCacheDir() ? 'cached' : 'uncached');`,
143+
);
144+
for (const disabled of [false, true]) {
145+
for (const args of [
146+
[NodePath.join(outputDirectory, "boot.cjs")],
147+
[
148+
"--require",
149+
NodePath.join(outputDirectory, "compileCache.cjs"),
150+
NodePath.join(outputDirectory, "backend.mjs"),
151+
],
152+
]) {
153+
const child = NodeChildProcess.spawnSync(process.execPath, args, {
154+
encoding: "utf8",
155+
env: {
156+
...process.env,
157+
APPIMAGE: "",
158+
NODE_COMPILE_CACHE: undefined,
159+
NODE_DISABLE_COMPILE_CACHE: disabled ? "1" : undefined,
160+
XDG_CACHE_HOME: directory,
161+
TMPDIR: directory,
162+
TEMP: directory,
163+
TMP: directory,
164+
},
165+
});
166+
assert.equal(child.status, 0, child.stderr);
167+
assert.equal(child.stdout.trim(), disabled ? "uncached" : "cached");
168+
}
169+
}
170+
} finally {
171+
await NodeFSP.rm(directory, { recursive: true, force: true });
172+
}
173+
});

‎apps/desktop/scripts/smoke-test.mjs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { resolveElectronLaunchCommand } from "./electron-launcher.mjs";
55

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

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

‎apps/desktop/src/app/DesktopAppActivation.test.ts‎

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,25 @@ function request(requestId: string, platform: NodeJS.Platform): DesktopAppActiva
4444
};
4545
}
4646

47+
function startOkServer(target: ReturnType<typeof makeTarget>, userId: number | undefined) {
48+
return startDesktopAppControlServer({
49+
...target,
50+
userId,
51+
handle: async (input) => ({
52+
version: 1,
53+
requestId: input.requestId,
54+
ok: true,
55+
projectId: ProjectId.make("project-1"),
56+
threadId: ThreadId.make("thread-1"),
57+
}),
58+
cancel: () => undefined,
59+
onReclaimError: () => undefined,
60+
}).then((server) => {
61+
openServers.push(server);
62+
return server;
63+
});
64+
}
65+
4766
function exchange(address: string, payload: DesktopAppActivationRequest) {
4867
return new Promise<DesktopAppActivationResponse>((resolve, reject) => {
4968
const socket = NodeNet.createConnection(address);
@@ -84,6 +103,7 @@ describe("desktop app control server", () => {
84103
};
85104
},
86105
cancel: () => undefined,
106+
onReclaimError: () => undefined,
87107
});
88108
openServers.push(server);
89109

@@ -117,6 +137,7 @@ describe("desktop app control server", () => {
117137
userId,
118138
handle: () => new Promise(() => undefined),
119139
cancel: resolveCanceled,
140+
onReclaimError: () => undefined,
120141
});
121142
openServers.push(server);
122143
const socket = NodeNet.createConnection(target.address);
@@ -137,4 +158,50 @@ describe("desktop app control server", () => {
137158
});
138159
}),
139160
);
161+
162+
// Two desktop apps can share one state dir, such as nightly and a preview build.
163+
it.effect("keeps a newer app's socket when an older app on the same state dir quits", () =>
164+
Effect.gen(function* () {
165+
const platform = yield* HostProcessPlatform;
166+
const userId = yield* HostProcessUserId;
167+
if (platform === "win32") return;
168+
yield* Effect.promise(async () => {
169+
const root = await NodeFSP.mkdtemp(NodePath.join(NodeOS.tmpdir(), "t3-app-takeover-test-"));
170+
const target = makeTarget(NodePath.join(root, "userdata"), platform, userId);
171+
const older = await startOkServer(target, userId);
172+
await startOkServer(target, userId);
173+
174+
await older.close();
175+
176+
await expect(
177+
exchange(target.address, request("after-quit", platform)),
178+
).resolves.toMatchObject({ ok: true, requestId: "after-quit" });
179+
await NodeFSP.rm(root, { recursive: true, force: true });
180+
});
181+
}),
182+
);
183+
184+
it.effect("binds its address again after the socket file is removed", () =>
185+
Effect.gen(function* () {
186+
const platform = yield* HostProcessPlatform;
187+
const userId = yield* HostProcessUserId;
188+
if (platform === "win32") return;
189+
yield* Effect.promise(async () => {
190+
const root = await NodeFSP.mkdtemp(NodePath.join(NodeOS.tmpdir(), "t3-app-reclaim-test-"));
191+
const target = makeTarget(NodePath.join(root, "userdata"), platform, userId);
192+
const server = await startOkServer(target, userId);
193+
194+
await NodeFSP.unlink(target.address);
195+
await server.reclaim();
196+
197+
await expect(
198+
exchange(target.address, request("reclaimed", platform)),
199+
).resolves.toMatchObject({
200+
ok: true,
201+
requestId: "reclaimed",
202+
});
203+
await NodeFSP.rm(root, { recursive: true, force: true });
204+
});
205+
}),
206+
);
140207
});

0 commit comments

Comments
 (0)