Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
5bba7c0
fix(server): resolve Claude SDK executable path on Windows npm instal…
nsxdavid Jul 20, 2026
6b8c16c
Fix project action preview settings persistence (#3842)
keeperxy Jul 20, 2026
d2b1493
fix(desktop): allow clipboard writes in the preview browser (#3889)
carlosricojr Jul 20, 2026
f462306
fix(web): handle sidebar shortcut before editors (#3921)
Bortlesboat Jul 20, 2026
bfec7d1
fix(server): recognize Bedrock-backed Claude as authenticated (#3931)
PieterVanZyl-Dev Jul 20, 2026
18d5cd0
Fix incorrect pluralization of “entry” (#3933)
McMelonTV Jul 20, 2026
aea0d0c
feat(server): title background-task work-log rows with the task name …
t3dotgg Jul 20, 2026
7902d15
fix: delegate OpenCode session titles to provider (#3720)
tris203 Jul 20, 2026
c740307
Archive selected threads from the context menu (#3895)
theduke Jul 20, 2026
93f662a
fix(cli): support force removing projects (#3922)
Bortlesboat Jul 20, 2026
35238bd
fix: allow sidebar to be shrunk when wider than viewport (#2456)
shoaib050326 Jul 20, 2026
d3d4b8e
fix(codex): show web search query and url in tool call details (#2093)
GuilhermeVieiraDev Jul 20, 2026
a87de2f
Add Codex launch arguments setting (#2892)
jamesx0416 Jul 20, 2026
3faee86
[orchestration] Clear stale active turn when session becomes inactive…
Andrew-Forster Jul 20, 2026
ef88509
Regenerate Codex reset credit protocol bindings (#4173)
juliusmarminge Jul 20, 2026
3e68088
fix(preview): preserve direct localhost navigation (#3939)
Chrrxs Jul 20, 2026
0a0d0c1
Synchronize mobile threads with authoritative shell snapshots (#4163)
juliusmarminge Jul 20, 2026
772d0a9
Gate iOS glass layout on native support (#4032)
juliusmarminge Jul 20, 2026
5a3b307
fix(opencode): resume the OpenCode session on follow-ups instead of s…
vdmkotai Jul 20, 2026
cabdc93
fix(server): use CLI for OpenCode health check instead of spawning se…
UtkarshUsername Jul 20, 2026
45a2ed3
fix(web): scope timeline minimap hover target to the side gutter (#3869)
xxashxx-svg Jul 20, 2026
7573353
[codex] show complete approval details (#4111)
maxwellyoung Jul 20, 2026
0d29c98
fix(web): paint text selection over composer chips (#4139)
yordis Jul 20, 2026
eb29fda
[codex] preserve custom model slugs (#4168)
maxwellyoung Jul 20, 2026
91d69ae
fix(web): preview workspace images in the file panel (#3996)
Rhiz3K Jul 20, 2026
d840366
feat(web): drag files from the explorer into the chat composer (#4140)
yordis Jul 20, 2026
6040ec9
fix(desktop): preserve main window bounds (#3851)
anirudhsama Jul 20, 2026
ad53310
perf(orchestration): speed up new-chat propagation and offline catch-…
RusiruSadathana Jul 20, 2026
64c7fdd
Finale: upgrade changed files card to fix various UI issues (#4113)
sandersonstabo Jul 20, 2026
ee76fa9
fix(web): always show environment chip for remote projects (#4217)
t3dotgg Jul 21, 2026
aeb9362
chore: reconcile upstream sync (20260721) — typecheck/test fixups
wizzoapp[bot] Jul 21, 2026
7366840
Merge remote-tracking branch 'origin/main' into chore/upstream-sync-2…
wizzoapp[bot] Jul 21, 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
1 change: 1 addition & 0 deletions apps/desktop/src/app/DesktopLifecycle.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ function makeDesktopWindowLayer(input: { readonly activateCount: Ref.Ref<number>
handleBackendNotReady: Effect.die("unexpected handleBackendNotReady"),
dispatchMenuAction: () => Effect.die("unexpected dispatchMenuAction"),
syncAppearance: Effect.void,
flushMainWindowBounds: Effect.void,
} satisfies DesktopWindow.DesktopWindow["Service"]);
}

Expand Down
8 changes: 7 additions & 1 deletion apps/desktop/src/app/DesktopLifecycle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,14 @@ function addScopedListener<Args extends ReadonlyArray<unknown>>(
}

const requestDesktopShutdownAndWait = Effect.fn("desktop.lifecycle.requestShutdownAndWait")(
function* (): Effect.fn.Return<void, never, DesktopShutdown.DesktopShutdown> {
function* (): Effect.fn.Return<
void,
never,
DesktopShutdown.DesktopShutdown | DesktopWindow.DesktopWindow
> {
const shutdown = yield* DesktopShutdown.DesktopShutdown;
const desktopWindow = yield* DesktopWindow.DesktopWindow;
yield* desktopWindow.flushMainWindowBounds;
yield* shutdown.request;
yield* shutdown.awaitComplete;
},
Expand Down
1 change: 1 addition & 0 deletions apps/desktop/src/backend/DesktopBackendPool.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ function makePoolLayer(
showConnectingSplash: Effect.void,
handleBackendReady: () => Effect.void,
handleBackendNotReady: Effect.void,
flushMainWindowBounds: Effect.void,
dispatchMenuAction: () => Effect.die("unexpected menu action"),
syncAppearance: Effect.void,
} satisfies DesktopWindow.DesktopWindow["Service"]),
Expand Down
1 change: 1 addition & 0 deletions apps/desktop/src/backend/DesktopServerExposure.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,7 @@ describe("DesktopServerExposure", () => {
const settingsLayer = Layer.succeed(DesktopAppSettings.DesktopAppSettings, {
get: Effect.succeed(DesktopAppSettings.DEFAULT_DESKTOP_SETTINGS),
load: Effect.succeed(DesktopAppSettings.DEFAULT_DESKTOP_SETTINGS),
setMainWindowBounds: () => Effect.die("unexpected main window bounds update"),
setServerExposureMode: () => Effect.fail(settingsFailure),
setTailscaleServe: () => Effect.fail(settingsFailure),
setUpdateChannel: () => Effect.die("unexpected update channel change"),
Expand Down
1 change: 1 addition & 0 deletions apps/desktop/src/electron/ElectronNotification.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,7 @@ function makeLayer(
handleBackendNotReady: Effect.die("unused"),
dispatchMenuAction: () => Effect.die("unused"),
syncAppearance: Effect.die("unused"),
flushMainWindowBounds: Effect.die("unused"),
}),
),
Layer.succeed(
Expand Down
51 changes: 51 additions & 0 deletions apps/desktop/src/preview/BrowserSession.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ const { fromPartition, sessions } = vi.hoisted(() => ({
readonly clearStorageData: ReturnType<typeof vi.fn>;
readonly getUserAgent: ReturnType<typeof vi.fn>;
readonly setPermissionRequestHandler: ReturnType<typeof vi.fn>;
readonly setPermissionCheckHandler: ReturnType<typeof vi.fn>;
readonly setUserAgent: ReturnType<typeof vi.fn>;
}
>(),
Expand All @@ -40,6 +41,7 @@ describe("BrowserSession", () => {
clearStorageData: vi.fn(() => Promise.resolve()),
getUserAgent: vi.fn(() => "Mozilla/5.0 Electron/41.5.0 t3code/0.0.27"),
setPermissionRequestHandler: vi.fn(),
setPermissionCheckHandler: vi.fn(),
setUserAgent: vi.fn(),
};
sessions.set(partition, browserSession);
Expand All @@ -61,6 +63,55 @@ describe("BrowserSession", () => {
}).pipe(Effect.provide(layer)),
);

it.effect("grants clipboard-sanitized-write through both the request and check handlers", () =>
Effect.gen(function* () {
const browserSessions = yield* BrowserSession.BrowserSession;
const partition = yield* browserSessions.getPartition("scope-a");
yield* browserSessions.getSession("scope-a");

const browserSession = sessions.get(partition);
assert.isDefined(browserSession);

const requestHandler = browserSession.setPermissionRequestHandler.mock.calls[0]?.[0];
const checkHandler = browserSession.setPermissionCheckHandler.mock.calls[0]?.[0];
assert.isFunction(requestHandler);
assert.isFunction(checkHandler);

const requestAllows = (permission: string): boolean => {
let granted: boolean | undefined;
requestHandler(null, permission, (value: boolean) => {
granted = value;
});
assert.isDefined(granted);
return granted;
};

for (const permission of [
"clipboard-read",
"clipboard-sanitized-write",
"notifications",
"geolocation",
]) {
assert.isTrue(requestAllows(permission), `request handler should allow ${permission}`);
assert.isTrue(
checkHandler(null, permission) as boolean,
`check handler should allow ${permission}`,
);
}

// `clipboard-write` is not a real Electron permission — the async write API
// uses `clipboard-sanitized-write` — so the stale name must not be granted,
// and unrelated permissions stay denied.
for (const permission of ["clipboard-write", "midi"]) {
assert.isFalse(requestAllows(permission), `request handler should deny ${permission}`);
assert.isFalse(
checkHandler(null, permission) as boolean,
`check handler should deny ${permission}`,
);
}
}).pipe(Effect.provide(layer)),
);

it.effect("preserves partition scope and the platform failure chain", () => {
const nativeCause = new Error("native digest failed");
const platformCause = PlatformError.systemError({
Expand Down
20 changes: 18 additions & 2 deletions apps/desktop/src/preview/BrowserSession.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,20 @@ import * as SynchronizedRef from "effect/SynchronizedRef";

const PREVIEW_PARTITION_PREFIX = "persist:t3code-preview-";

// Permissions granted to preview web content. `clipboard-sanitized-write` is the
// Electron permission behind `navigator.clipboard.writeText()` — note it is NOT
// `clipboard-write`, which is not a valid Electron permission name. Async
// clipboard writes are gated by the permission *check* handler (not only the
// request handler), so both handlers must allow it; otherwise built-in "Copy"
// buttons — e.g. the Next.js / Vercel error overlay — fail with
// `Failed to execute 'writeText' on 'Clipboard': Write permission denied`.
const ALLOWED_PREVIEW_PERMISSIONS: ReadonlySet<string> = new Set([
"clipboard-read",
"clipboard-sanitized-write",
"notifications",
"geolocation",
]);

export class BrowserSessionPartitionDerivationError extends Schema.TaggedErrorClass<BrowserSessionPartitionDerivationError>()(
"BrowserSessionPartitionDerivationError",
{
Expand Down Expand Up @@ -120,9 +134,11 @@ export const make = Effect.gen(function* BrowserSessionMake() {
.replace(/\s*t3code\/[\d.]+/, "");
browserSession.setUserAgent(userAgent);
browserSession.setPermissionRequestHandler((_webContents, permission, callback) => {
const allowed = ["clipboard-read", "clipboard-write", "notifications", "geolocation"];
callback(allowed.includes(permission));
callback(ALLOWED_PREVIEW_PERMISSIONS.has(permission));
});
browserSession.setPermissionCheckHandler((_webContents, permission) =>
ALLOWED_PREVIEW_PERMISSIONS.has(permission),
);
const next = new Map(sessions);
next.set(partition, browserSession);
return [browserSession, next] as const;
Expand Down
45 changes: 45 additions & 0 deletions apps/desktop/src/settings/DesktopAppSettings.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,17 @@ import * as DesktopEnvironment from "../app/DesktopEnvironment.ts";
import * as DesktopAppSettings from "./DesktopAppSettings.ts";

const DesktopSettingsPatch = Schema.Struct({
mainWindowBounds: Schema.optionalKey(
Schema.NullOr(
Schema.Struct({
x: Schema.Number,
y: Schema.Number,
width: Schema.Number,
height: Schema.Number,
}),
),
),
mainWindowMaximized: Schema.optionalKey(Schema.Boolean),
serverExposureMode: Schema.optionalKey(Schema.Literals(["local-only", "network-accessible"])),
tailscaleServeEnabled: Schema.optionalKey(Schema.Boolean),
tailscaleServePort: Schema.optionalKey(Schema.Number),
Expand Down Expand Up @@ -91,6 +102,8 @@ describe("DesktopSettings", () => {
assert.deepEqual(
DesktopAppSettings.resolveDefaultDesktopSettings("0.0.17-nightly.20260415.1"),
{
mainWindowBounds: null,
mainWindowMaximized: false,
serverExposureMode: "local-only",
tailscaleServeEnabled: false,
tailscaleServePort: 443,
Expand All @@ -116,6 +129,8 @@ describe("DesktopSettings", () => {
});

assert.deepEqual(yield* settings.load, {
mainWindowBounds: null,
mainWindowMaximized: false,
serverExposureMode: "network-accessible",
tailscaleServeEnabled: true,
tailscaleServePort: 8443,
Expand Down Expand Up @@ -215,10 +230,13 @@ describe("DesktopSettings", () => {
"serverExposureMode": "network-accessible",
"tailscaleServeEnabled": true,
"tailscaleServePort": 8443,
"mainWindowBounds": { "x": 120, "y": 80, "width": 1280, "height": 900 },
}\n`,
);

assert.deepEqual(yield* settings.load, {
mainWindowBounds: { x: 120, y: 80, width: 1280, height: 900 },
mainWindowMaximized: false,
serverExposureMode: "network-accessible",
tailscaleServeEnabled: true,
tailscaleServePort: 8443,
Expand All @@ -232,19 +250,40 @@ describe("DesktopSettings", () => {
),
);

it.effect("rejects window bounds that do not satisfy the domain schema", () =>
withSettings(
Effect.gen(function* () {
const settings = yield* DesktopAppSettings.DesktopAppSettings;
yield* writeSettingsPatch({
mainWindowBounds: { x: 10.5, y: 20, width: 839, height: 620 },
mainWindowMaximized: true,
serverExposureMode: "network-accessible",
});

const loaded = yield* settings.load;
assert.isNull(loaded.mainWindowBounds);
assert.isFalse(loaded.mainWindowMaximized);
assert.equal(loaded.serverExposureMode, "network-accessible");
}),
),
);

it.effect("persists sparse desktop settings documents", () =>
withSettings(
Effect.gen(function* () {
const environment = yield* DesktopEnvironment.DesktopEnvironment;
const fileSystem = yield* FileSystem.FileSystem;
const settings = yield* DesktopAppSettings.DesktopAppSettings;

yield* settings.setMainWindowBounds({ x: -1200, y: 40, width: 1440, height: 960 }, 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 },
mainWindowMaximized: true,
serverExposureMode: "network-accessible",
} satisfies typeof DesktopSettingsPatch.Type);
}),
Expand All @@ -261,6 +300,8 @@ describe("DesktopSettings", () => {
});

assert.deepEqual(yield* settings.load, {
mainWindowBounds: null,
mainWindowMaximized: false,
serverExposureMode: "local-only",
tailscaleServeEnabled: false,
tailscaleServePort: 443,
Expand All @@ -286,6 +327,8 @@ describe("DesktopSettings", () => {
});

assert.deepEqual(yield* settings.load, {
mainWindowBounds: null,
mainWindowMaximized: false,
serverExposureMode: "local-only",
tailscaleServeEnabled: false,
tailscaleServePort: 443,
Expand All @@ -310,6 +353,8 @@ describe("DesktopSettings", () => {
});

assert.deepEqual(yield* settings.load, {
mainWindowBounds: null,
mainWindowMaximized: false,
serverExposureMode: "local-only",
tailscaleServeEnabled: true,
tailscaleServePort: 443,
Expand Down
Loading
Loading