;
@@ -63,6 +76,47 @@ const helperLayer = (input: {
),
);
+describe("macOS Chromium secrets", () => {
+ const request = {
+ platform: "darwin",
+ keychainService: "Chrome Safe Storage",
+ keychainAccount: "Chrome",
+ linuxSecretApplication: undefined,
+ } as const;
+ const noProcesses = Layer.succeed(
+ ChildProcessSpawner.ChildProcessSpawner,
+ ChildProcessSpawner.make(() => Effect.die("must not spawn")),
+ );
+
+ it.effect("derives the cookie key from the keychain secret", () =>
+ Effect.gen(function* () {
+ getPassword.mockReturnValue("macos-secret");
+ const keys = yield* resolveChromiumKeys(request);
+ expect(keys.cbcV10?.toString("hex")).toBe("3df7306fb1eac353289565a2f6b64f74");
+ }).pipe(Effect.provide(noProcesses)),
+ );
+
+ it.effect("reports a missing keychain entry", () =>
+ Effect.gen(function* () {
+ getPassword.mockReturnValue(null);
+ const error = yield* resolveChromiumKeys(request).pipe(Effect.flip);
+ expect(error.reason).toBe("keychainItemMissing");
+ }).pipe(Effect.provide(noProcesses)),
+ );
+
+ it.effect("preserves a denied keychain approval", () =>
+ Effect.gen(function* () {
+ const denied = new Error("User denied access");
+ getPassword.mockImplementation(() => {
+ throw denied;
+ });
+ const error = yield* resolveChromiumKeys(request).pipe(Effect.flip);
+ expect(error.reason).toBe("needsKeychainApproval");
+ expect(error.cause).toBe(denied);
+ }).pipe(Effect.provide(noProcesses)),
+ );
+});
+
describe("Linux Chromium secrets", () => {
it.effect("retains a missing helper failure alongside the keyring-free fallback", () =>
Effect.gen(function* () {
diff --git a/apps/desktop/src/preview/BrowserImport/ChromiumKeys.ts b/apps/desktop/src/preview/BrowserImport/ChromiumKeys.ts
index 88df1e88af90..9310fd1c92f7 100644
--- a/apps/desktop/src/preview/BrowserImport/ChromiumKeys.ts
+++ b/apps/desktop/src/preview/BrowserImport/ChromiumKeys.ts
@@ -17,7 +17,6 @@
*
* @module ChromiumKeys
*/
-import * as Keyring from "@napi-rs/keyring";
import * as NodeCrypto from "node:crypto";
import { HostProcessEnvironment } from "@t3tools/shared/hostProcess";
@@ -102,6 +101,12 @@ const readKeychainSecret = Effect.fn("ChromiumKeys.readKeychainSecret")(function
service: string,
account: string,
) {
+ // Only macOS cookie imports need this binding; loading it at startup can
+ // prevent the desktop from opening on platforms that never use it.
+ const Keyring = yield* Effect.tryPromise({
+ try: () => import("@napi-rs/keyring"),
+ catch: (cause) => new ChromiumKeyError({ reason: "keychainUnavailable", cause }),
+ });
const secret = yield* Effect.try({
try: () => new Keyring.Entry(service, account).getPassword(),
catch: (cause) => {
diff --git a/apps/desktop/src/settings/DesktopClientSettings.test.ts b/apps/desktop/src/settings/DesktopClientSettings.test.ts
index 83ad62ccffdc..8d6699d014cd 100644
--- a/apps/desktop/src/settings/DesktopClientSettings.test.ts
+++ b/apps/desktop/src/settings/DesktopClientSettings.test.ts
@@ -53,6 +53,7 @@ const clientSettings: ClientSettings = {
planModeEnabled: false,
proactivePanelsEnabled: true,
showSkillsInSlashMenu: false,
+ persistComposerContextStrip: true,
providerModelPreferences: {},
sidebarProjectGroupingMode: "repository_path",
sidebarProjectGroupingOverrides: {
diff --git a/apps/marketing/public/app-desktop.webp b/apps/marketing/public/app-desktop.webp
new file mode 100644
index 000000000000..11b51331eef3
Binary files /dev/null and b/apps/marketing/public/app-desktop.webp differ
diff --git a/apps/marketing/src/pages/index.astro b/apps/marketing/src/pages/index.astro
index cb90f3687184..eb473eb2f875 100644
--- a/apps/marketing/src/pages/index.astro
+++ b/apps/marketing/src/pages/index.astro
@@ -236,7 +236,7 @@ const mobileEndorsementRows = [