Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
abd89bc
feat(web): prompt stash — cmd+S saves the composer to a per-provider …
t3dotgg Jul 27, 2026
e6d3e45
feat: allow new thread creation through project breadcrumbs (#4638)
0x4bs3nt Jul 27, 2026
81d8fac
fix(web): scope PR state to the thread branch (#4460)
juliusmarminge Jul 27, 2026
53cf8d8
Fix live sidebar resize limits and defer Alchemy runtime context (#4655)
juliusmarminge Jul 27, 2026
0632881
fix(web): constrain branch toolbar context (#4657)
maxktz Jul 27, 2026
c2d3885
fix: close actions dropdown when editing (#4660)
0x4bs3nt Jul 27, 2026
a45c58f
Prevent draft thread detail polling before shell registration (#4670)
juliusmarminge Jul 27, 2026
209fc7c
feat(diff-panel): show total line additions and deletions (#4674)
0x4bs3nt Jul 27, 2026
b3a7104
Clear provider update actions while updating (#4676)
juliusmarminge Jul 27, 2026
3ce7344
Fix sidebar highlighting for draft threads (#4679)
juliusmarminge Jul 27, 2026
aff6758
Use glass surfaces for web toasts (#4681)
juliusmarminge Jul 27, 2026
e092685
Show origin ref in branch trigger label (#4680)
juliusmarminge Jul 27, 2026
5aa2672
feat: default sidebar v2 on for nightly and dev builds (#4491)
t3dotgg Jul 28, 2026
6144d09
fix(web): 33 web UI fixes (#4700)
maxktz Jul 28, 2026
6c3fcd8
feat(web): add maria's sidebar header artwork toggle (#4652)
maxktz Jul 28, 2026
587459f
feat(web): add appearance settings category (#4715)
maxktz Jul 28, 2026
a7ce492
fix(desktop): allow updater-controlled relaunch (#4721)
0x4bs3nt Jul 28, 2026
b84fd28
fix(web): prevent diff panel scroll jumping (#4724)
0x4bs3nt Jul 28, 2026
6c03ce8
Link inline code file paths in chat markdown (#4726)
juliusmarminge Jul 28, 2026
3ae87bc
Prevent sidebar row labels from truncating (#4789)
juliusmarminge Jul 28, 2026
f9a5fda
fix(web): stashed prompts now survive switching providers (#4787)
t3dotgg Jul 28, 2026
03a0753
fix(web): defer command palette filesystem navigation (#2109)
juliusmarminge Jul 28, 2026
530dc1e
Remove Connect waitlist and add GA announcement tooling (#4691)
juliusmarminge Jul 29, 2026
8fd8430
fix(desktop): restore T3 Connect sign-in (#4809)
PixPMusic Jul 29, 2026
43f98d5
Simplify files panel header (#4828)
juliusmarminge Jul 29, 2026
e72c81e
Preserve the thread shell while detail loads (#4830)
juliusmarminge Jul 29, 2026
f0af22c
fix(web): remember the rendered-markdown choice across threads (#4853)
Sy-D Jul 29, 2026
3e18a1b
fix(web): restore sidebar v2 thread actions and terminal icon (#4712)
Noojuno Jul 30, 2026
12ec52c
fix(web): settle button now works on hover, not just right-click (#4905)
t3dotgg Jul 30, 2026
945d98a
fix(composer): hide default Codex service tier (#4784)
maxktz Jul 30, 2026
58f4f10
fix(web): align remote server update action (#4731)
StiensWout Jul 30, 2026
26f954f
fix(web): show Codex fast mode as a bolt (#4947)
t3dotgg Jul 30, 2026
e269ec3
fix(web): keep worktree default when switching a draft's machine (#4964)
t3dotgg Jul 30, 2026
625bc2a
feat(web): pasting a huge screenshot now compresses it instead of err…
t3dotgg Jul 30, 2026
fe91fe1
fix(web): fill fast mode icon (#5004)
maria-rcks Jul 30, 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/DesktopAppIdentity.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ const makeElectronAppLayer = (calls: ElectronAppCalls) =>
calls.setDockIcon.push(iconPath);
}),
appendCommandLineSwitch: () => Effect.void,
onBeforeQuitForUpdate: () => Effect.void,
on: () => Effect.void,
} satisfies ElectronApp.ElectronApp["Service"]);

Expand Down
123 changes: 123 additions & 0 deletions apps/desktop/src/app/DesktopLifecycle.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import { assert, describe, it } from "@effect/vitest";
import * as Effect from "effect/Effect";
import * as Layer from "effect/Layer";
import * as Ref from "effect/Ref";

import type * as Electron from "electron";

import * as ElectronApp from "../electron/ElectronApp.ts";
import * as ElectronTheme from "../electron/ElectronTheme.ts";
import * as DesktopEnvironment from "./DesktopEnvironment.ts";
import * as DesktopLifecycle from "./DesktopLifecycle.ts";
import * as DesktopShutdown from "./DesktopShutdown.ts";
import * as DesktopState from "./DesktopState.ts";
import * as DesktopWindow from "../window/DesktopWindow.ts";

describe("DesktopLifecycle", () => {
for (const platform of ["darwin", "win32", "linux"] satisfies ReadonlyArray<NodeJS.Platform>) {
it.effect(`lets the updater's quit event proceed on ${platform}`, () => {
const appListeners = new Map<string, (...args: readonly unknown[]) => void>();

const electronAppLayer = Layer.succeed(ElectronApp.ElectronApp, {
metadata: Effect.die("unexpected metadata read"),
name: Effect.succeed("T3 Code"),
whenReady: Effect.void,
quit: Effect.void,
exit: () => Effect.void,
relaunch: () => Effect.void,
setPath: () => Effect.void,
setName: () => Effect.void,
setAboutPanelOptions: () => Effect.void,
setAppUserModelId: () => Effect.void,
requestSingleInstanceLock: Effect.succeed(true),
isDefaultProtocolClient: () => Effect.succeed(false),
setAsDefaultProtocolClient: () => Effect.succeed(true),
setDesktopName: () => Effect.void,
setDockIcon: () => Effect.void,
appendCommandLineSwitch: () => Effect.void,
onBeforeQuitForUpdate: (listener) =>
Effect.acquireRelease(
Effect.sync(() => {
appListeners.set("before-quit-for-update", listener);
}),
() =>
Effect.sync(() => {
appListeners.delete("before-quit-for-update");
}),
).pipe(Effect.asVoid),
on: (eventName, listener) =>
Effect.acquireRelease(
Effect.sync(() => {
appListeners.set(
eventName,
listener as unknown as (...args: readonly unknown[]) => void,
);
}),
() =>
Effect.sync(() => {
appListeners.delete(eventName);
}),
).pipe(Effect.asVoid),
} satisfies ElectronApp.ElectronApp["Service"]);

const electronThemeLayer = Layer.succeed(ElectronTheme.ElectronTheme, {
shouldUseDarkColors: Effect.succeed(false),
setSource: () => Effect.void,
onUpdated: () => Effect.void,
});

const desktopWindowLayer = Layer.succeed(DesktopWindow.DesktopWindow, {
createMain: Effect.die("unexpected window creation"),
ensureMain: Effect.die("unexpected window creation"),
revealOrCreateMain: Effect.die("unexpected window creation"),
activate: Effect.void,
createMainIfBackendReady: Effect.void,
showConnectingSplash: Effect.void,
handleBackendReady: () => Effect.void,
handleBackendNotReady: Effect.void,
flushMainWindowBounds: Effect.void,
dispatchMenuAction: () => Effect.void,
syncAppearance: Effect.void,
});

const environmentLayer = Layer.succeed(DesktopEnvironment.DesktopEnvironment, {
platform,
isDevelopment: false,
} as DesktopEnvironment.DesktopEnvironment["Service"]);

const layer = DesktopLifecycle.layer.pipe(
Layer.provideMerge(electronAppLayer),
Layer.provideMerge(electronThemeLayer),
Layer.provideMerge(desktopWindowLayer),
Layer.provideMerge(environmentLayer),
Layer.provideMerge(DesktopShutdown.layer),
Layer.provideMerge(DesktopState.layer),
);

return Effect.scoped(
Effect.gen(function* () {
const lifecycle = yield* DesktopLifecycle.DesktopLifecycle;
yield* lifecycle.register;

appListeners.get("before-quit-for-update")?.();

let prevented = false;
const event = {
preventDefault: () => {
prevented = true;
},
} as Electron.Event;
appListeners.get("before-quit")?.(event);

assert.isFalse(
prevented,
"cancelling this event prevents the updater from completing its relaunch",
);

const state = yield* DesktopState.DesktopState;
assert.isTrue(yield* Ref.get(state.quitting));
}),
).pipe(Effect.provide(layer));
});
}
});
14 changes: 13 additions & 1 deletion apps/desktop/src/app/DesktopLifecycle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,16 +176,28 @@ export const make = DesktopLifecycle.of({
const context = yield* Effect.context<DesktopLifecycleRuntimeServices>();
const runEffect = Effect.runPromiseWith(context);
let quitAllowed = false;
let updaterQuitAllowed = false;
yield* electronTheme.onUpdated(() => {
void runEffect(
desktopWindow.syncAppearance.pipe(Effect.withSpan("desktop.lifecycle.themeUpdated")),
);
});
yield* electronApp.onBeforeQuitForUpdate(() => {
// Electron's updater owns the remaining quit/install/relaunch sequence.
// Cancelling the following app "before-quit" event breaks that sequence,
// most visibly on macOS where the native updater performs the relaunch.
updaterQuitAllowed = true;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Shut down backends before updater-controlled quit

When before-quit-for-update sets this flag, the following before-quit event takes handleBeforeQuit's early-return path and never calls requestDesktopShutdownAndWait. DesktopApp is still blocked on shutdown.awaitRequest, so its backend-pool finalizer—the path that sends each backend its graceful stop—cannot run before Electron exits; installing an update can therefore hard-kill local/WSL backends and skip their persistence cleanup. Coordinate shutdown before invoking quitAndInstall rather than bypassing it.

AGENTS.md reference: AGENTS.md:L25-L25

Useful? React with 👍 / 👎.

void runEffect(
logLifecycleInfo("allowing updater-controlled quit").pipe(
Effect.withSpan("desktop.lifecycle.beforeQuitForUpdate"),
),
);
});
yield* electronApp.on("before-quit", (event: Electron.Event) => {
handleBeforeQuit(
event,
runEffect,
() => quitAllowed,
() => quitAllowed || updaterQuitAllowed,
() => {
quitAllowed = true;
},
Expand Down
28 changes: 28 additions & 0 deletions apps/desktop/src/electron/ElectronApp.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { beforeEach, vi } from "vite-plus/test";

const {
appendSwitchMock,
autoUpdaterOnMock,
autoUpdaterRemoveListenerMock,
exitMock,
getAppPathMock,
getVersionMock,
Expand All @@ -23,6 +25,8 @@ const {
whenReadyMock,
} = vi.hoisted(() => ({
appendSwitchMock: vi.fn(),
autoUpdaterOnMock: vi.fn(),
autoUpdaterRemoveListenerMock: vi.fn(),
exitMock: vi.fn(),
getAppPathMock: vi.fn(() => "/app"),
getVersionMock: vi.fn(() => "1.2.3"),
Expand All @@ -43,6 +47,10 @@ const {
}));

vi.mock("electron", () => ({
autoUpdater: {
on: autoUpdaterOnMock,
removeListener: autoUpdaterRemoveListenerMock,
},
app: {
commandLine: {
appendSwitch: appendSwitchMock,
Expand Down Expand Up @@ -77,6 +85,8 @@ import * as ElectronApp from "./ElectronApp.ts";
describe("ElectronApp", () => {
beforeEach(() => {
appendSwitchMock.mockClear();
autoUpdaterOnMock.mockClear();
autoUpdaterRemoveListenerMock.mockClear();
exitMock.mockClear();
onMock.mockClear();
quitMock.mockClear();
Expand Down Expand Up @@ -153,4 +163,22 @@ describe("ElectronApp", () => {
assert.deepEqual(removeListenerMock.mock.calls, [["activate", listener]]);
}).pipe(Effect.provide(ElectronApp.layer)),
);

it.effect("scopes native updater quit listeners", () =>
Effect.gen(function* () {
const listener = vi.fn();

yield* Effect.scoped(
Effect.gen(function* () {
const electronApp = yield* ElectronApp.ElectronApp;
yield* electronApp.onBeforeQuitForUpdate(listener);
}),
);

assert.deepEqual(autoUpdaterOnMock.mock.calls, [["before-quit-for-update", listener]]);
assert.deepEqual(autoUpdaterRemoveListenerMock.mock.calls, [
["before-quit-for-update", listener],
]);
}).pipe(Effect.provide(ElectronApp.layer)),
);
});
13 changes: 13 additions & 0 deletions apps/desktop/src/electron/ElectronApp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,9 @@ export class ElectronApp extends Context.Service<
readonly setDesktopName: (desktopName: string) => Effect.Effect<void>;
readonly setDockIcon: (iconPath: string) => Effect.Effect<void>;
readonly appendCommandLineSwitch: (switchName: string, value?: string) => Effect.Effect<void>;
readonly onBeforeQuitForUpdate: (
listener: () => void,
) => Effect.Effect<void, never, Scope.Scope>;
readonly on: <Args extends ReadonlyArray<unknown>>(
eventName: string,
listener: (...args: Args) => void,
Expand Down Expand Up @@ -178,6 +181,16 @@ export const make = ElectronApp.of({
}
Electron.app.commandLine.appendSwitch(switchName, value);
}),
onBeforeQuitForUpdate: (listener) =>
Effect.acquireRelease(
Effect.sync(() => {
Electron.autoUpdater.on("before-quit-for-update", listener);
}),
() =>
Effect.sync(() => {
Electron.autoUpdater.removeListener("before-quit-for-update", listener);
}),
).pipe(Effect.asVoid),
on: addScopedAppListener,
});

Expand Down
2 changes: 2 additions & 0 deletions apps/desktop/src/settings/DesktopClientSettings.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ const clientSettings: ClientSettings = {
confirmThreadDelete: false,
dismissedProviderUpdateNotificationKeys: [],
diffIgnoreWhitespace: true,
environmentIdentificationMode: "artwork",
favorites: [],
glassOpacity: 80,
providerModelPreferences: {},
Expand All @@ -30,6 +31,7 @@ const clientSettings: ClientSettings = {
sidebarThreadSortOrder: "created_at",
sidebarThreadPreviewCount: 6,
sidebarV2Enabled: false,
sidebarV2ConfiguredByUser: false,
timestampFormat: "24-hour",
wordWrap: true,
};
Expand Down
1 change: 1 addition & 0 deletions apps/desktop/src/window/DesktopApplicationMenu.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ const electronAppLayer = Layer.succeed(ElectronApp.ElectronApp, {
setDesktopName: () => Effect.void,
setDockIcon: () => Effect.void,
appendCommandLineSwitch: () => Effect.void,
onBeforeQuitForUpdate: () => Effect.void,
on: () => Effect.void,
} satisfies ElectronApp.ElectronApp["Service"]);

Expand Down
6 changes: 3 additions & 3 deletions apps/mobile/src/Stack.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,6 @@ import { SettingsAuthRouteScreen } from "./features/settings/SettingsAuthRouteSc
import { SettingsEnvironmentsRouteScreen } from "./features/settings/SettingsEnvironmentsRouteScreen";
import { SettingsLegalRouteScreen } from "./features/settings/SettingsLegalRouteScreen";
import { SettingsRouteScreen } from "./features/settings/SettingsRouteScreen";
import { SettingsWaitlistRouteScreen } from "./features/settings/SettingsWaitlistRouteScreen";
import { ShowcaseCaptureCoordinator } from "./features/showcase/ShowcaseCaptureCoordinator";
import {
SettingsLegalDocumentCloseHeaderButton,
Expand Down Expand Up @@ -192,10 +191,11 @@ const SettingsSheetStack = createNativeStackNavigator({
},
}),
SettingsWaitlist: createNativeStackScreen({
screen: SettingsWaitlistRouteScreen,
// Keep the old deep link working after the Connect GA launch.
screen: SettingsAuthRouteScreen,
linking: "waitlist",
options: {
title: "Join the waitlist",
title: "Sign in",
},
}),
},
Expand Down
Loading
Loading