Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
114 commits
Select commit Hold shift + click to select a range
44a7cb1
fix(relay): tunnel recovery no longer drives the Cloudflare API into …
juliusmarminge Oct 20, 2026
143ab16
fix(acp): settle native foreground work independently (#21616)
juliusmarminge Oct 20, 2026
64be361
fix(usage): retain transcript diagnostics across scans (#21862)
juliusmarminge Oct 20, 2026
a09b9ca
fix(claude): retain approvals when bypass is disabled (#21805)
juliusmarminge Oct 20, 2026
1327e7b
fix(server): preserve Claude question option previews (#21932)
juliusmarminge Oct 20, 2026
fb14869
fix(server): expose turns blocked only on T3 thread waits (#21499)
juliusmarminge Oct 20, 2026
7ee6428
fix(web): deleted answer chips no longer return after picking an opti…
ScottN-PV Oct 20, 2026
651fe94
fix(mobile): composer buttons stay visible with animations removed (#…
Pipyakas Oct 20, 2026
da26f1e
fix(web): decode timeline image attachments asynchronously (#19923)
rohan-patnaik Oct 20, 2026
dc52893
fix(mobile): the Android composer no longer lags the keyboard or drop…
ThallesP Oct 20, 2026
6fc3c9e
fix(mobile): bound huge native review diff payloads (#19240)
Pipyakas Oct 20, 2026
5188454
fix(acp-registry): list every compatible agent in Add provider (#19145)
davisonio Oct 20, 2026
1a684f0
fix(server): PR cache cleanup runs once per shared layer (#19791)
aravhawk Oct 20, 2026
e859e31
fix(codex): end the session when an incoming message is too long (#19…
only21mil Oct 20, 2026
b5febb1
fix(ci): forward fork PR events to Cursor (#14481)
juliusmarminge Oct 20, 2026
3fc8e5d
fix(web): reopening the Changes panel refreshes its diff (#19550)
whoisaldo Oct 20, 2026
105ee2e
fix(resource-monitor): avoid retaining unrelated macOS process detail…
voltcrash Oct 20, 2026
7beeb63
fix(mobile): Android composer paste no longer drops trailing text (#1…
only21mil Oct 20, 2026
4513fce
fix(server): keep newer forced workspace scan results (#19465)
Exotic209093 Oct 20, 2026
def1c3b
fix(shared): keep native stack order when some saved layer lists are …
ScottN-PV Oct 20, 2026
85d5dc8
feat(mobile): put Duo thread filtering beside compose in the rail (#1…
PixPMusic Oct 20, 2026
bac96aa
fix(web): Enter on a bare "2." in the composer adds a line instead of…
Batuhan1999 Oct 20, 2026
219afe3
fix(web): keep long PR badge lists within the viewport (#18941)
0bkevin Oct 20, 2026
1e659c9
fix(desktop): register Windows OAuth callbacks with the app entry poi…
AskPlays Oct 20, 2026
be21de4
perf(server): skip unused default-branch lookups (#19825)
IfkumRfnl Oct 20, 2026
2532ed7
fix(mobile): T3 Connect network hint no longer suggests a phone hotsp…
shivamhwp Oct 20, 2026
22ad904
fix(server): t3-code tools work again after a thread sits idle for a …
willsheldon Oct 20, 2026
b21fcb6
fix(web): long file names in the file tree truncate cleanly (#18984)
akash3444 Oct 20, 2026
5ca23fe
fix(mobile): visible chat rows refresh when skills or content width c…
Onkarj012 Oct 20, 2026
e0eb8c9
fix(mobile): lock the composer while a queued edit saves (#18846)
saphid Oct 20, 2026
5f89a05
fix(models): Claude Haiku 4.5 shows thinking on by default (#14445)
incognitojam Oct 20, 2026
4908d8c
fix(server): keep Claude renames when importing sessions (#12292)
Gigioxx Oct 20, 2026
6dc1462
fix(web): model picker no longer jumps at the end of its open animati…
Claudesaul Oct 20, 2026
30b9b64
fix(mobile): keep the composer's draft and typing after answering a q…
Claudesaul Oct 20, 2026
3664114
fix(clients): show question option previews (#21933)
juliusmarminge Oct 20, 2026
6fe97fe
fix(web): new-thread shortcuts work while the command palette lists t…
rgrvlsk Oct 20, 2026
e3538ac
fix(provider): leave PR watching to parent threads (#19801)
aryasaatvik Oct 20, 2026
03b55a2
fix(web): hide native subagent threads in the legacy sidebar (#20808)
lancy69 Oct 20, 2026
6958838
fix(acp): project and resume native child sessions (#21617)
juliusmarminge Oct 20, 2026
48745ff
fix(web): hovering a thread automation shows its full title and sched…
tristanmanchester Oct 20, 2026
3fdcef0
fix(server): recover pulls from unrelated ref locks (#22107)
Yash-Singh1 Oct 20, 2026
c632d7e
fix(provider-grok): give ACP initialize enough time to answer (#18771)
baktun14 Oct 20, 2026
4bc6d0b
fix(web): keep thread context when stashing prompts (#18843)
saphid Oct 20, 2026
2fc244a
fix(web): branch row hover matches nearby controls (#21292)
voltcrash Oct 20, 2026
fd1f0b5
style(web): center Markdown table cells by default (#20325)
GuyvDev Oct 20, 2026
8b094ee
fix(mobile): no-project drafts name No project in the project picker …
tris203 Oct 20, 2026
dd86a02
fix(web,client-runtime): subagent cards show Cursor reasoning effort …
ilyaliao Oct 20, 2026
7a286c9
fix(ssh): retain actionable errors from failed device commands (#19972)
Quicksaver Oct 20, 2026
9497ab5
fix(desktop): allow half-screen tiling on scaled displays (#19232)
tannerpolley Oct 20, 2026
060ac67
fix(web): show the full command when the Running banner truncates (#1…
macodev00 Oct 20, 2026
8046e1d
fix(provider-cursor): say when Cursor rejects a free-plan account (#1…
whoisaldo Oct 20, 2026
6ac70aa
fix(cli): use GitHub tokens when checking for updates (#19507)
Exotic209093 Oct 20, 2026
892cdaf
fix(server): limit restart loops within a five-minute window (#20306)
spencermorris04 Oct 20, 2026
7116325
fix(web): Edit from here warns that "Revert files too" also undoes yo…
shivamhwp Oct 20, 2026
0f06d27
fix(mobile): compact Dynamic Island opens the Live Activity's thread …
fixfon Oct 20, 2026
cfbd463
perf(server): untracked change totals use less CPU (#19676)
jetpham Oct 20, 2026
4846ba8
fix(desktop): skip WSL distro probing while the WSL backend is off (#…
only21mil Oct 20, 2026
15c51cb
fix(web): removing a project returns to the project overview (#20231)
Nelglor Oct 20, 2026
c360851
fix(web): center usage models without a breakdown bar (#21247)
Aforno Oct 20, 2026
181fbb2
fix(shared): unwatch, review guide, and task cancel labels read as pl…
sg448 Oct 20, 2026
15341e8
fix(shared): preserve SQLite reader finalizers during shutdown (#20305)
spencermorris04 Oct 20, 2026
bb76f12
fix(web): keep typed underscores literal in the rich text composer (#…
ScottN-PV Oct 20, 2026
6fc7062
fix(mobile): pulling to refresh a pull request fetches current checks…
g-h-miles Oct 20, 2026
ee7f8dc
fix(web): open remote preview files in VS Code (#19898)
fixfon Oct 20, 2026
48fcf90
fix(web): expanding stacked notices no longer scrolls a working threa…
derektrimm Oct 20, 2026
bbe5008
fix(desktop): a slow PowerShell profile delays Windows startup by 5 s…
luke2x Oct 20, 2026
5932c19
fix(server): a slow Claude CLI is no longer reported as failing to ru…
yaacovcorcos Oct 20, 2026
8b26d5c
fix(web): pasting into the device screen types into the device (#21197)
jaikhuranna Oct 20, 2026
3c4df28
fix(web): middle-click closing a panel tab no longer pastes the prima…
MSliwa01 Oct 20, 2026
e67b555
fix(server): reveal opens the default Windows file manager (#19526)
Cody-Stevens Oct 20, 2026
762813c
fix(providers): respect mise ownership in native install locations (#…
tlconde Oct 20, 2026
fe949fe
fix(server): terminals open for threads with long ids (#18669)
Batuhan1999 Oct 20, 2026
d1b4ce9
fix(server): Vite+ provider installs get update prompts on Windows (#…
tonank Oct 20, 2026
cc93dc1
fix(ssh): ask for a password or verification code, not only a passwor…
only21mil Oct 20, 2026
3496ecb
perf(server): skip periodic checks for disabled providers (#18851)
saphid Oct 20, 2026
ce87dc7
fix(server): opening a folder without Git installed no longer shows a…
tiliakoos Oct 20, 2026
4583cad
fix(web): PR checks scrollbar no longer covers Details (#19404)
psv2522 Oct 20, 2026
1de7b16
fix(pi): say why text generation cannot load a model (#19070)
ScottN-PV Oct 20, 2026
7f1ecf3
fix(web): pull request header no longer flickers when scrolled to the…
RakshithBhat03 Oct 20, 2026
c5520b5
fix(mobile): Android image viewer keeps panning after an interrupted …
Pipyakas Oct 20, 2026
a8dec0e
fix(web): Mod+T opens the tab menu while a pull request tab is open (…
riccardopll Oct 20, 2026
c768c6d
fix(web): opening a deleted thread from the timeline stays on the cur…
voida6 Oct 20, 2026
62144bf
fix(desktop): read Chromium cookies stored as text (#19377)
deanhtid Oct 20, 2026
9ac1355
fix(web): terminal links keep Unicode file paths intact (#20264)
Maseeek Oct 20, 2026
38b8c20
fix(usage): reconcile Codex request and cumulative counters (#21863)
juliusmarminge Oct 20, 2026
c7a2fdc
fix(web): name the missing repository in the PR link dialog (#20533)
ScottN-PV Oct 20, 2026
0bf4787
fix(web): preserve HTML input when preview URLs renew (#17736)
JDeffner Oct 20, 2026
6e1bf41
fix(server): resolve git common dirs through symlinked subdirectories…
macodev00 Oct 20, 2026
b1167b6
fix(acp-registry): enable Claude native background lifecycle (#21618)
juliusmarminge Oct 20, 2026
cd8a986
fix(server): T3 Connect backs off from a failing relay instead of ham…
juliusmarminge Oct 20, 2026
ed2ca82
fix(opencode): a tool killed for memory no longer stops every OpenCod…
t3dotgg Oct 20, 2026
50c5437
fix(web): avoid submitting PR titles during IME composition (#18087)
aminevg Oct 20, 2026
32e3b90
fix(web): limit bars with many accounts no longer clip names to one l…
joshrobertson8 Oct 20, 2026
a261a71
fix(web): server update banner only offers details when its text is c…
tris203 Oct 20, 2026
32c9304
feat(mobile): close browser tabs from the Browser page (#19579)
tris203 Oct 20, 2026
39b1ca9
fix(mobile): iOS builds no longer fail on Xcode 27.2 (#19037)
nekohasekai Oct 20, 2026
236be55
fix(providers): declare CLI-owned setup capabilities (#18425)
nicholasg-dev Oct 20, 2026
c1c6be8
perf(server): avoid remote enumeration during status reads (#19802)
IfkumRfnl Oct 20, 2026
3c63b50
fix(web): open the startup composer without waiting for every environ…
shxntanu Oct 20, 2026
55a8fe9
fix(server): preserve Claude final-result API error text (#19136)
Marve10s Oct 20, 2026
88a779d
fix(server): Antigravity can reach Google on NixOS (#20551)
felixlaga Oct 20, 2026
64a76c8
fix(web): expanding all folders in Files no longer walks node_modules…
tiliakoos Oct 20, 2026
5803c8b
fix(web): identify cleanup results by machine and folder (#20211)
razzeee Oct 20, 2026
a2157ec
fix(provider-grok): recheck Grok login once after a logged-out probe …
only21mil Oct 20, 2026
33c0ebb
fix(server): PR sync ignores tokenized avatar URLs on ghe.com (#20352)
fixfon Oct 20, 2026
51d1f6f
fix(server): custom Claude endpoint no longer reads as logged out (#2…
callmearta Oct 20, 2026
fd8dd51
fix(claude): map monthly account spending limits (#19433)
Exotic209093 Oct 20, 2026
9d68e1e
fix(web): skip production Clerk on unsupported browser origins (#20424)
r-near Oct 20, 2026
86d130a
feat(server): export server and process-group resource metrics (#22122)
juliusmarminge Oct 20, 2026
b430de5
fix(server): a missing t3.json is no longer a failed read (#19949)
ruck-syu Oct 20, 2026
0979221
perf(web): long composer code blocks no longer re-highlight on every …
MohibShaikh Oct 20, 2026
69ceea9
fix(server): T3 Connect links from the desktop app when the backend r…
aididhaiqal Oct 20, 2026
9aaca71
fix(claude): preserve workflow member completion timestamps (#18142)
ashx-j Oct 20, 2026
38c6b3d
Merge remote-tracking branch 'upstream/main' into yordis/chore-sync-u…
yordis Oct 20, 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
6 changes: 4 additions & 2 deletions .github/workflows/cursor-hygiene-webhook.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ name: Forward to Cursor hygiene
on:
push:
branches: [main]
pull_request:
# Use trusted workflow code and repository secrets for fork PRs.
# Keep this job payload-only: never check out or run PR code.
pull_request_target:
types: [opened, reopened, ready_for_review]
issues:
types: [opened, closed, reopened]
Expand Down Expand Up @@ -34,7 +36,7 @@ jobs:
--output /dev/null --write-out '%{http_code}' \
-H "Authorization: $AUTH" \
-H "Content-Type: application/json" \
-H "X-GitHub-Event: ${{ github.event_name }}" \
-H "X-GitHub-Event: ${{ github.event_name == 'pull_request_target' && 'pull_request' || github.event_name }}" \
-H "X-GitHub-Delivery: ${{ github.run_id }}-${{ github.run_attempt }}" \
--data-binary @"${{ github.event_path }}") || curl_status=$?
if [ "$curl_status" -eq 0 ]; then
Expand Down
70 changes: 68 additions & 2 deletions apps/desktop/src/app/DesktopClerk.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// @effect-diagnostics nodeBuiltinImport:off globalFetchInEffect:off - Hosted handoff test uses a real localhost listener without an OpenAI account.
import * as NodeHttp from "node:http";
import * as NodePath from "@effect/platform-node/NodePath";
import * as NodePath from "node:path";
import * as PlatformNodePath from "@effect/platform-node/NodePath";
import { codexAuthHandoffUrl, readCodexAuthDelivery } from "@t3tools/shared/codexAuthHandoff";
import { EnvironmentId, ProviderInstanceId } from "@t3tools/contracts";
import * as HostProcess from "@t3tools/shared/HostProcess";
Expand Down Expand Up @@ -59,11 +60,15 @@ const layerDesktopClerk = (
copyText: () => Effect.void,
},
listeners = new Map<string, (...args: unknown[]) => void>(),
isPackaged = false,
) => {
const environment = DesktopEnvironment.DesktopEnvironment.of({
stateDir: "/tmp/t3-state",
isDevelopment,
appDataDirectory: "/tmp/app-data",
isPackaged,
dirname: platform === "win32" ? String.raw`C:\T3 Code\dist-electron` : "/tmp/dist-electron",
path: platform === "win32" ? NodePath.win32 : NodePath.posix,
platform,
} as unknown as DesktopEnvironment.DesktopEnvironment["Service"]);

Expand Down Expand Up @@ -95,7 +100,7 @@ const layerDesktopClerk = (
return DesktopClerk.layer.pipe(
Layer.provide(
Layer.mergeAll(
NodePath.layerPosix,
PlatformNodePath.layerPosix,
Layer.succeed(DesktopEnvironment.DesktopEnvironment, environment),
Layer.succeed(ElectronApp.ElectronApp, electronApp),
Layer.succeed(ElectronShell.ElectronShell, shell),
Expand All @@ -112,6 +117,67 @@ describe("DesktopClerk", () => {
storageMock.mockReset();
});

it.effect.each([
{ platform: "win32", isPackaged: false, isDevelopment: false, registers: true },
{ platform: "win32", isPackaged: false, isDevelopment: true, registers: false },
{ platform: "win32", isPackaged: true, isDevelopment: false, registers: false },
{ platform: "darwin", isPackaged: false, isDevelopment: false, registers: false },
{ platform: "linux", isPackaged: false, isDevelopment: false, registers: false },
] as const)(
"registers an entry point only for unpackaged non-development Windows ($platform, packaged=$isPackaged, development=$isDevelopment)",
({ platform, isPackaged, isDevelopment, registers }) => {
const events: string[] = [];
storageMock.mockReturnValue(storageAdapter);
createClerkBridgeMock.mockImplementation(() => {
events.push("bridge");
return { cleanup: vi.fn() };
});
const setAsDefaultProtocolClient = vi.fn(() =>
Effect.sync(() => {
events.push("register");
return true;
}),
);
const electronApp = {
on: () => Effect.void,
setAsDefaultProtocolClient,
} as unknown as ElectronApp.ElectronApp["Service"];

return Effect.gen(function* () {
const clerk = yield* DesktopClerk.DesktopClerk;
yield* clerk.configure;

assert.deepEqual(
setAsDefaultProtocolClient.mock.calls,
registers
? [["t3code", process.execPath, [String.raw`C:\T3 Code\dist-electron\main.cjs`]]]
: [],
);
assert.deepEqual(events, registers ? ["bridge", "register"] : ["bridge"]);
}).pipe(
Effect.scoped,
Effect.provide(
layerDesktopClerk(
isDevelopment,
[],
platform,
undefined,
undefined,
undefined,
isPackaged,
),
),
Effect.provideService(ElectronApp.ElectronApp, electronApp),
Effect.provideService(
ElectronWindow.ElectronWindow,
{} as ElectronWindow.ElectronWindow["Service"],
),
Effect.provideService(DesktopWebLinks.DesktopWebLinks, ignoreWebLinks),
Effect.provideService(HostProcess.Arguments, ["electron"]),
);
},
);

it.effect("acquires and releases the SDK bridge with the layer", () => {
const cleanup = vi.fn();
const events: string[] = [];
Expand Down
14 changes: 14 additions & 0 deletions apps/desktop/src/app/DesktopClerk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,20 @@ export const make = Effect.gen(function* () {
const context = yield* Effect.context<ElectronWindow.ElectronWindow>();
const runPromise = Effect.runPromiseWith(context);

// Clerk's bare scheme registration omits the app entry on unpackaged Windows.
// Development callbacks also need dev server environment this command cannot restore.
if (
!environment.isPackaged &&
!environment.isDevelopment &&
environment.platform === "win32"
) {
yield* electronApp.setAsDefaultProtocolClient(
ElectronProtocol.getDesktopScheme(environment.isDevelopment),
process.execPath,
[environment.path.join(environment.dirname, "main.cjs")],
);
}

const startProviderAuthHandoff = (value: string | undefined) => {
if (!value) return false;
const request = readCodexAuthHandoff(value, environment.isDevelopment);
Expand Down
37 changes: 37 additions & 0 deletions apps/desktop/src/ipc/methods/wsl.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,43 @@ describe("WSL IPC", () => {
),
);

it.effect("does not enumerate distros until the WSL backend is enabled", () => {
const distros = [
{ name: "Ubuntu", isDefault: true, version: 2 as const },
{ name: "Debian", isDefault: false, version: 2 as const },
];
const layer = Layer.mergeAll(
DesktopAppSettings.layerTest({
...DesktopAppSettings.DEFAULT_DESKTOP_SETTINGS,
wslDistro: "Debian",
}),
DesktopWslEnvironment.layerTest({ isAvailable: true, distros }),
layerWslBackend(),
layerLifecycle([]),
layerUnusedLifecycleRuntime,
);

return Effect.gen(function* () {
const wsl = yield* DesktopWslEnvironment.DesktopWslEnvironment;
const state = yield* getWslState.handler(undefined).pipe(
Effect.provideService(DesktopWslEnvironment.DesktopWslEnvironment, {
...wsl,
listDistros: Effect.die("must not enumerate distros"),
}),
Effect.flatMap(decodeWslState),
);
assert.equal(state.enabled, false);
assert.equal(state.available, true);
assert.equal(state.distro, "Debian");
assert.deepEqual(state.distros, []);

const enabledState = yield* invokeSetWslBackendEnabled(true);
assert.equal(enabledState.enabled, true);
assert.equal(enabledState.distro, "Debian");
assert.deepEqual(enabledState.distros, distros);
}).pipe(Effect.provide(layer));
});

it.effect("stages dual-backend preferences before enabling without relaunching", () => {
const relaunchReasons: Array<string> = [];
const layer = Layer.mergeAll(
Expand Down
6 changes: 3 additions & 3 deletions apps/desktop/src/ipc/methods/wsl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,9 @@ const readWslState: Effect.Effect<
const wslBackend = yield* DesktopWslBackend.DesktopWslBackend;
const settings = yield* appSettings.get;
const available = settings.localEnvironmentEnabled && (yield* wslEnvironment.isAvailable);
// Only enumerate distros when WSL is actually available — listDistros on a
// non-WSL host would spawn wsl.exe and hit the timeout for nothing.
const distros = available ? yield* wslEnvironment.listDistros : [];
// Windows ships a wsl.exe stub that can prompt to install WSL when invoked.
// Keep disabled state reads from launching it to enumerate distros.
const distros = available && settings.wslBackendEnabled ? yield* wslEnvironment.listDistros : [];
const preflightError = yield* wslBackend.lastPreflightError;
return {
enabled: settings.wslBackendEnabled,
Expand Down
95 changes: 48 additions & 47 deletions apps/desktop/src/preview/BrowserImport/ChromiumCookies.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,56 +151,57 @@ describe("readChromiumCookieDatabase", () => {
}).pipe(Effect.provide(NodeServices.layer), Effect.scoped),
);

it.effect("reads plaintext, encrypted, and genuinely empty cookie values", () =>
Effect.gen(function* () {
const fileSystem = yield* FileSystem.FileSystem;
const directory = yield* fileSystem.makeTempDirectoryScoped({
prefix: "t3code-chromium-cookies-",
});
const filename = `${directory}/Cookies`;
const key = Buffer.from("0123456789abcdef");
it.effect.each([14, 24])(
"imports mixed TEXT and BLOB cookie values, including empty values, in schema %i",
(schemaVersion) =>
Effect.gen(function* () {
const fileSystem = yield* FileSystem.FileSystem;
const directory = yield* fileSystem.makeTempDirectoryScoped({
prefix: "t3code-chromium-cookies-",
});
const filename = `${directory}/Cookies`;
const key = Buffer.from("0123456789abcdef");
const host = "secure.example";
const value = "stored encrypted";
const payload =
schemaVersion >= 24
? Buffer.concat([
NodeCrypto.createHash("sha256").update(host).digest(),
Buffer.from(value),
])
: Buffer.from(value);
const encrypted = encryptV10(payload, key);

yield* Effect.gen(function* () {
const sql = yield* SqlClient.SqlClient;
yield* sql`create table meta (key text primary key, value text not null)`;
yield* sql`insert into meta values ('version', 23)`;
yield* sql`
create table cookies (
host_key text not null,
name text not null,
value text not null,
encrypted_value blob not null,
path text not null,
expires_utc integer not null,
is_secure integer not null,
is_httponly integer not null,
samesite integer not null,
top_frame_site_key text not null default ''
)
`;
yield* sql`
insert into cookies (host_key, name, value, encrypted_value, path, expires_utc, is_secure, is_httponly, samesite) values
('plain.example', 'plain', 'stored plaintext', ${new Uint8Array()}, '/', 0, 0, 0, -1)
`;
yield* sql`
insert into cookies (host_key, name, value, encrypted_value, path, expires_utc, is_secure, is_httponly, samesite) values
('secure.example', 'encrypted', '', ${encryptV10("stored encrypted", key)}, '/', 0, 1, 1, 2)
`;
yield* sql`
insert into cookies (host_key, name, value, encrypted_value, path, expires_utc, is_secure, is_httponly, samesite) values
('empty.example', 'empty', '', ${new Uint8Array()}, '/', 0, 0, 0, 0)
`;
}).pipe(Effect.provide(NodeSqliteClient.layer({ filename })));
yield* Effect.gen(function* () {
const sql = yield* SqlClient.SqlClient;
yield* sql`create table meta (key text primary key, value text not null)`;
yield* sql`insert into meta values ('version', ${schemaVersion})`;
yield* sql`create table cookies (
host_key text not null, name text not null, value text not null,
encrypted_value blob not null, path text not null, expires_utc integer not null,
is_secure integer not null, is_httponly integer not null, samesite integer not null
)`;
if (schemaVersion >= 15) {
yield* sql`alter table cookies add column top_frame_site_key text not null default ''`;
}
yield* sql`insert into cookies
(host_key, name, value, encrypted_value, path, expires_utc, is_secure, is_httponly, samesite) values
('plain.example', 'plain', 'stored plaintext', '', '/', 0, 0, 0, -1),
(${host}, 'text-encrypted', '', cast(${encrypted} as text), '/', 0, 1, 1, 2),
(${host}, 'blob-encrypted', '', ${encrypted}, '/', 0, 1, 1, 2),
('empty.example', 'empty', '', ${new Uint8Array()}, '/', 0, 0, 0, 0)`;
}).pipe(Effect.provide(NodeSqliteClient.layer({ filename })));

const result = yield* readChromiumCookieDatabase(filename, { cbcV10: key }, "darwin");
const result = yield* readChromiumCookieDatabase(filename, { cbcV10: key }, "darwin");

expect(result.undecryptable).toBe(0);
expect(result.cookies.map(({ name, value }) => ({ name, value }))).toEqual([
{ name: "plain", value: "stored plaintext" },
{ name: "encrypted", value: "stored encrypted" },
{ name: "empty", value: "" },
]);
}).pipe(Effect.provide(NodeServices.layer), Effect.scoped),
expect(result.undecryptable).toBe(0);
expect(result.cookies.map(({ name, value }) => ({ name, value }))).toEqual([
{ name: "plain", value: "stored plaintext" },
{ name: "text-encrypted", value },
{ name: "blob-encrypted", value },
{ name: "empty", value: "" },
]);
}).pipe(Effect.provide(NodeServices.layer), Effect.scoped),
);

it.effect("enforces domain binding only for schema 24 and newer", () =>
Expand Down
6 changes: 4 additions & 2 deletions apps/desktop/src/preview/BrowserImport/ChromiumCookies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -200,12 +200,14 @@ export const readChromiumCookieDatabase = Effect.fn("ChromiumCookies.readChromiu
Effect.flatMap(decodeSchemaVersion),
Effect.map(([row]) => row.value),
);
// SQLite's BLOB affinity still permits TEXT. Cast before Node decodes
// those bytes as UTF-8, which can corrupt an encrypted payload.
const raw =
schemaVersion >= 15
? yield* sql`select host_key, name, value, encrypted_value, path,
? yield* sql`select host_key, name, value, cast(encrypted_value as blob) as encrypted_value, path,
expires_utc / 1000000 as expires_seconds, is_secure, is_httponly,
samesite, top_frame_site_key from cookies`
: yield* sql`select host_key, name, value, encrypted_value, path,
: yield* sql`select host_key, name, value, cast(encrypted_value as blob) as encrypted_value, path,
expires_utc / 1000000 as expires_seconds, is_secure, is_httponly,
samesite, '' as top_frame_site_key from cookies`;
return { rows: yield* decodeCookieRows(raw), schemaVersion };
Expand Down
13 changes: 10 additions & 3 deletions apps/desktop/src/settings/DesktopAppSettings.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -283,7 +283,7 @@ describe("DesktopSettings", () => {
Effect.gen(function* () {
const settings = yield* DesktopAppSettings.DesktopAppSettings;
yield* writeSettingsPatch({
mainWindowBounds: { x: 10.5, y: 20, width: 839, height: 620 },
mainWindowBounds: { x: 10, y: 20, width: 599, height: 620 },
mainWindowMaximized: true,
serverExposureMode: "network-accessible",
});
Expand Down Expand Up @@ -342,17 +342,24 @@ describe("DesktopSettings", () => {
const fileSystem = yield* FileSystem.FileSystem;
const settings = yield* DesktopAppSettings.DesktopAppSettings;

yield* settings.setMainWindowBounds({ x: -1200, y: 40, width: 1440, height: 960 }, true);
yield* settings.setMainWindowBounds({ x: -1200, y: 40, width: 600, height: 620 }, true);
yield* settings.setServerExposureMode("network-accessible");

const persisted = yield* decodeDesktopSettingsPatch(
yield* fileSystem.readFileString(environment.desktopSettingsPath),
);
assert.deepEqual(persisted, {
mainWindowBounds: { x: -1200, y: 40, width: 1440, height: 960 },
mainWindowBounds: { x: -1200, y: 40, width: 600, height: 620 },
mainWindowMaximized: true,
serverExposureMode: "network-accessible",
} satisfies typeof DesktopSettingsPatch.Type);

const restored = yield* Effect.gen(function* () {
const freshSettings = yield* DesktopAppSettings.DesktopAppSettings;
return yield* freshSettings.load;
}).pipe(Effect.provide(DesktopAppSettings.layer));
assert.deepEqual(restored.mainWindowBounds, persisted.mainWindowBounds);
assert.isTrue(restored.mainWindowMaximized);
}),
),
);
Expand Down
4 changes: 2 additions & 2 deletions apps/desktop/src/settings/DesktopAppSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,8 @@ export interface DesktopSettingsChange {
}

const DEFAULT_TAILSCALE_SERVE_PORT = 443;
const MIN_MAIN_WINDOW_SIZE = {
width: 840,
export const MIN_MAIN_WINDOW_SIZE = {
width: 600,
height: 620,
} as const;
export const DesktopWindowBoundsSchema = Schema.Struct({
Expand Down
Loading
Loading