From 58897689c887b9b87272d0f0a1487cfcfac2ac00 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 26 May 2026 23:12:19 +0000 Subject: [PATCH 1/4] feat(macos): persisted settings store via electron-store MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit LUM-1846. Replaces the renderer's `window.vellum.settings.{get,set}` stubs (Promise.reject) with a real implementation backed by electron-store. Settings live in the OS-canonical userData path with atomic file writes; the renderer reads/writes via the same generic contextBridge API the scaffold already declared. Schema (validated at write time): - `hotkeys: Record` — key bindings keyed by action name - `theme: "light" | "dark" | "system"` — color-scheme preference - `windowState: { x?, y?, width?, height? }` — placeholder for window geometry restore in a future ticket - `featureFlags: Record` — local overrides The four categories come from the LUM-1846 spec; specific hotkey actions and window-restore logic land in the feature tickets that consume them. electron-store rejects writes that don't match the schema (validated by ajv under the hood), surfacing as a rejected Promise to the renderer. Wiring: - New `apps/macos/src/main/settings.ts` owns the store instance + schema. - Main process registers `vellum:settings:get` / `vellum:settings:set` IPC handlers under `installSettingsIpc()` during `whenReady`. - Preload's `settings.{get,set}` now invoke those handlers via `ipcRenderer.invoke`. The bridge surface in `VellumBridge` is unchanged — generic `get(key)` / `set(key, value)`. - `apps/web/src/runtime/is-electron.ts` extends the ambient `Window.vellum` declaration to include the settings methods so renderer consumers in `apps/web` get types. Bundling: electron-store and its `conf` parent are ESM-only. Excluding them from electron-vite's `externalizeDepsPlugin` bundles their ESM source into the main-process CJS bundle, where the default-export interop is handled at bundle time. Without this, `require("electron-store")` returns the module namespace and `new Store(...)` fails with "Store is not a constructor" at runtime. tsconfig: `module`/`moduleResolution` bumped to `Preserve`/`Bundler` so TypeScript reads conf's `exports` field and resolves the inherited get/set signatures correctly. Verified locally with a headless smoke test that exercises the IPC end-to-end: theme + hotkeys roundtripped through set→get, and a schema-violating write was correctly rejected by ajv. https://claude.ai/code/session_011sZ4p8AkqDQMSavHvWfioe --- apps/macos/bun.lock | 53 ++++++++++++++++++-- apps/macos/electron.vite.config.ts | 13 ++++- apps/macos/package.json | 3 ++ apps/macos/src/main/index.ts | 15 +++++- apps/macos/src/main/settings.ts | 75 +++++++++++++++++++++++++++++ apps/macos/src/preload/index.ts | 8 +-- apps/macos/tsconfig.json | 4 +- apps/web/src/runtime/is-electron.ts | 13 +++-- 8 files changed, 169 insertions(+), 15 deletions(-) create mode 100644 apps/macos/src/main/settings.ts diff --git a/apps/macos/bun.lock b/apps/macos/bun.lock index 24ae481fc66..9d1b67a2ee5 100644 --- a/apps/macos/bun.lock +++ b/apps/macos/bun.lock @@ -4,6 +4,9 @@ "workspaces": { "": { "name": "@vellumai/macos", + "dependencies": { + "electron-store": "11.0.2", + }, "devDependencies": { "@types/node": "22.10.5", "electron": "42.2.0", @@ -223,7 +226,9 @@ "agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], - "ajv": ["ajv@6.15.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw=="], + "ajv": ["ajv@8.20.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="], + + "ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="], "ajv-keywords": ["ajv-keywords@3.5.2", "", { "peerDependencies": { "ajv": "^6.9.1" } }, "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ=="], @@ -249,6 +254,8 @@ "at-least-node": ["at-least-node@1.0.0", "", {}, "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg=="], + "atomically": ["atomically@2.1.1", "", { "dependencies": { "stubborn-fs": "^2.0.0", "when-exit": "^2.1.4" } }, "sha512-P4w9o2dqARji6P7MHprklbfiArZAWvo07yW7qs3pdljb3BWr12FIB7W+p0zJiuiVsUpRO0iZn1kFFcpPegg0tQ=="], + "balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], @@ -307,6 +314,8 @@ "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], + "conf": ["conf@15.1.0", "", { "dependencies": { "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "atomically": "^2.0.3", "debounce-fn": "^6.0.0", "dot-prop": "^10.0.0", "env-paths": "^3.0.0", "json-schema-typed": "^8.0.1", "semver": "^7.7.2", "uint8array-extras": "^1.5.0" } }, "sha512-Uy5YN9KEu0WWDaZAVJ5FAmZoaJt9rdK6kH+utItPyGsCqCgaTKkrmZx3zoE0/3q6S3bcp3Ihkk+ZqPxWxFK5og=="], + "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], "core-util-is": ["core-util-is@1.0.2", "", {}, "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ=="], @@ -317,6 +326,8 @@ "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], + "debounce-fn": ["debounce-fn@6.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-rBMW+F2TXryBwB54Q0d8drNEI+TfoS9JpNTAoVpukbWEhjXQq4rySFYLaqXMFXwdv61Zb2OHtj5bviSoimqxRQ=="], + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], "decompress-response": ["decompress-response@6.0.0", "", { "dependencies": { "mimic-response": "^3.1.0" } }, "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ=="], @@ -337,6 +348,8 @@ "dmg-license": ["dmg-license@1.0.11", "", { "dependencies": { "@types/plist": "^3.0.1", "@types/verror": "^1.10.3", "ajv": "^6.10.0", "crc": "^3.8.0", "iconv-corefoundation": "^1.1.7", "plist": "^3.0.4", "smart-buffer": "^4.0.2", "verror": "^1.10.0" }, "os": "darwin", "bin": { "dmg-license": "bin/dmg-license.js" } }, "sha512-ZdzmqwKmECOWJpqefloC5OJy1+WZBBse5+MR88z9g9Zn4VY+WYUkAyojmhzJckH5YbbZGcYIuGAkY5/Ys5OM2Q=="], + "dot-prop": ["dot-prop@10.1.0", "", { "dependencies": { "type-fest": "^5.0.0" } }, "sha512-MVUtAugQMOff5RnBy2d9N31iG0lNwg1qAoAOn7pOK5wf94WIaE3My2p3uwTQuvS2AcqchkcR3bHByjaM0mmi7Q=="], + "dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="], "dotenv-expand": ["dotenv-expand@11.0.7", "", { "dependencies": { "dotenv": "^16.4.5" } }, "sha512-zIHwmZPRshsCdpMDyVsqGmgyP0yT8GAgXUnkdAoJisxvf33k7yO6OuoKmcTGuXPWSsm8Oh88nZicRLA9Y0rUeA=="], @@ -353,6 +366,8 @@ "electron-publish": ["electron-publish@26.8.1", "", { "dependencies": { "@types/fs-extra": "^9.0.11", "builder-util": "26.8.1", "builder-util-runtime": "9.5.1", "chalk": "^4.1.2", "form-data": "^4.0.5", "fs-extra": "^10.1.0", "lazy-val": "^1.0.5", "mime": "^2.5.2" } }, "sha512-q+jrSTIh/Cv4eGZa7oVR+grEJo/FoLMYBAnSL5GCtqwUpr1T+VgKB/dn1pnzxIxqD8S/jP1yilT9VrwCqINR4w=="], + "electron-store": ["electron-store@11.0.2", "", { "dependencies": { "conf": "^15.0.2", "type-fest": "^5.0.1" } }, "sha512-4VkNRdN+BImL2KcCi41WvAYbh6zLX5AUTi4so68yPqiItjbgTjqpEnGAqasgnG+lB6GuAyUltKwVopp6Uv+gwQ=="], + "electron-to-chromium": ["electron-to-chromium@1.5.361", "", {}, "sha512-Q6Hts7N9FnJc5LeGRINFvLhCI9xZmNtTDe5ZbcVezQz7cU4a8Aua3GH1b8J2XY8Al9PF+OCwYqhgsOOheMdvkA=="], "electron-vite": ["electron-vite@5.0.0", "", { "dependencies": { "@babel/core": "^7.28.4", "@babel/plugin-transform-arrow-functions": "^7.27.1", "cac": "^6.7.14", "esbuild": "^0.25.11", "magic-string": "^0.30.19", "picocolors": "^1.1.1" }, "peerDependencies": { "@swc/core": "^1.0.0", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0" }, "optionalPeers": ["@swc/core"], "bin": { "electron-vite": "bin/electron-vite.js" } }, "sha512-OHp/vjdlubNlhNkPkL/+3JD34ii5ov7M0GpuXEVdQeqdQ3ulvVR7Dg/rNBLfS5XPIFwgoBLDf9sjjrL+CuDyRQ=="], @@ -393,6 +408,8 @@ "fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="], + "fast-uri": ["fast-uri@3.1.2", "", {}, "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ=="], + "fd-slicer": ["fd-slicer@1.1.0", "", { "dependencies": { "pend": "~1.2.0" } }, "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g=="], "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], @@ -479,7 +496,9 @@ "json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="], - "json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], + "json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], + + "json-schema-typed": ["json-schema-typed@8.0.2", "", {}, "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA=="], "json-stringify-safe": ["json-stringify-safe@5.0.1", "", {}, "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA=="], @@ -509,6 +528,8 @@ "mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], + "mimic-function": ["mimic-function@5.0.1", "", {}, "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA=="], + "mimic-response": ["mimic-response@3.1.0", "", {}, "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ=="], "minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="], @@ -583,6 +604,8 @@ "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="], + "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], + "resedit": ["resedit@1.7.2", "", { "dependencies": { "pe-library": "^0.4.1" } }, "sha512-vHjcY2MlAITJhC0eRD/Vv8Vlgmu9Sd3LX9zZvtGzU5ZImdTN3+d6e/4mnTyV8vEbyf1sgNIrWxhWlrys52OkEA=="], "resolve-alpn": ["resolve-alpn@1.2.1", "", {}, "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g=="], @@ -635,10 +658,16 @@ "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + "stubborn-fs": ["stubborn-fs@2.0.0", "", { "dependencies": { "stubborn-utils": "^1.0.1" } }, "sha512-Y0AvSwDw8y+nlSNFXMm2g6L51rBGdAQT20J3YSOqxC53Lo3bjWRtr2BKcfYoAf352WYpsZSTURrA0tqhfgudPA=="], + + "stubborn-utils": ["stubborn-utils@1.0.2", "", {}, "sha512-zOh9jPYI+xrNOyisSelgym4tolKTJCQd5GBhK0+0xJvcYDcwlOoxF/rnFKQ2KRZknXSG9jWAp66fwP6AxN9STg=="], + "sumchecker": ["sumchecker@3.0.1", "", { "dependencies": { "debug": "^4.1.0" } }, "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg=="], "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + "tagged-tag": ["tagged-tag@1.0.0", "", {}, "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng=="], + "tar": ["tar@7.5.15", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.1.0", "yallist": "^5.0.0" } }, "sha512-dzGK0boVlC4W5QFuQN1EFSl3bIDYsk7Tj40U6eIBnK2k/8ml7TZ5agbI5j5+qnoVcAA+rNtBml8SEiLxZpNqRQ=="], "temp": ["temp@0.9.4", "", { "dependencies": { "mkdirp": "^0.5.1", "rimraf": "~2.6.2" } }, "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA=="], @@ -655,10 +684,12 @@ "truncate-utf8-bytes": ["truncate-utf8-bytes@1.0.2", "", { "dependencies": { "utf8-byte-length": "^1.0.1" } }, "sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ=="], - "type-fest": ["type-fest@0.13.1", "", {}, "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg=="], + "type-fest": ["type-fest@5.6.0", "", { "dependencies": { "tagged-tag": "^1.0.0" } }, "sha512-8ZiHFm91orbSAe2PSAiSVBVko18pbhbiB3U9GglSzF/zCGkR+rxpHx6sEMCUm4kxY4LjDIUGgCfUMtwfZfjfUA=="], "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + "uint8array-extras": ["uint8array-extras@1.5.0", "", {}, "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A=="], + "undici": ["undici@7.26.0", "", {}, "sha512-3O9Tf67pGhgOv9jM35AbhkXAKi13f3oy3aE4CSgr+TckGeY+/iu97ZXN+J7DpHPzLbVApFd1IFhcnBjREYXYcg=="], "undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="], @@ -675,6 +706,8 @@ "vite": ["vite@7.3.3", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-/4XH147Ui7OGTjg3HbdWe5arnZQSbfuRzdr9Ec7TQi5I7R+ir0Rlc9GIvD4v0XZurELqA035KVXJXpR61xhiTA=="], + "when-exit": ["when-exit@2.1.5", "", {}, "sha512-VGkKJ564kzt6Ms1dbgPP/yuIoQCrsFAnRbptpC5wOEsDaNsbCB2bnfnaA8i/vRs5tjUSEOtIuvl9/MyVsvQZCg=="], + "which": ["which@5.0.0", "", { "dependencies": { "isexe": "^3.1.1" }, "bin": { "node-which": "bin/which.js" } }, "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ=="], "wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], @@ -701,6 +734,8 @@ "@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + "@develar/schema-utils/ajv": ["ajv@6.15.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw=="], + "@electron/asar/minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="], "@electron/fuses/fs-extra": ["fs-extra@9.1.0", "", { "dependencies": { "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ=="], @@ -729,6 +764,8 @@ "@types/yauzl/@types/node": ["@types/node@24.12.4", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GUUEShf+PBCGW2KaXwcIt3Yk+e3pkKwWKb9GSyM9WQVE+ep2jzmHdGsHzu4wgcZy5fN9FBdVzjpBQsYlpfpgLA=="], + "ajv-keywords/ajv": ["ajv@6.15.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw=="], + "app-builder-lib/@electron/get": ["@electron/get@3.1.0", "", { "dependencies": { "debug": "^4.1.1", "env-paths": "^2.2.0", "fs-extra": "^8.1.0", "got": "^11.8.5", "progress": "^2.0.3", "semver": "^6.2.0", "sumchecker": "^3.0.1" }, "optionalDependencies": { "global-agent": "^3.0.0" } }, "sha512-F+nKc0xW+kVbBRhFzaMgPy3KwmuNTYX1fx6+FxxoSnNgwYX6LD7AKBTWkU0MQ6IBoe7dz069CNkR673sPAgkCQ=="], "app-builder-lib/ci-info": ["ci-info@4.3.1", "", {}, "sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA=="], @@ -739,6 +776,8 @@ "dir-compare/minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="], + "dmg-license/ajv": ["ajv@6.15.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw=="], + "electron/@types/node": ["@types/node@24.12.4", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GUUEShf+PBCGW2KaXwcIt3Yk+e3pkKwWKb9GSyM9WQVE+ep2jzmHdGsHzu4wgcZy5fN9FBdVzjpBQsYlpfpgLA=="], "electron-winstaller/fs-extra": ["fs-extra@7.0.1", "", { "dependencies": { "graceful-fs": "^4.1.2", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw=="], @@ -757,12 +796,16 @@ "postject/commander": ["commander@9.5.0", "", {}, "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ=="], + "serialize-error/type-fest": ["type-fest@0.13.1", "", {}, "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg=="], + "tiny-async-pool/semver": ["semver@5.7.2", "", { "bin": { "semver": "bin/semver" } }, "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g=="], "vite/esbuild": ["esbuild@0.27.7", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.7", "@esbuild/android-arm": "0.27.7", "@esbuild/android-arm64": "0.27.7", "@esbuild/android-x64": "0.27.7", "@esbuild/darwin-arm64": "0.27.7", "@esbuild/darwin-x64": "0.27.7", "@esbuild/freebsd-arm64": "0.27.7", "@esbuild/freebsd-x64": "0.27.7", "@esbuild/linux-arm": "0.27.7", "@esbuild/linux-arm64": "0.27.7", "@esbuild/linux-ia32": "0.27.7", "@esbuild/linux-loong64": "0.27.7", "@esbuild/linux-mips64el": "0.27.7", "@esbuild/linux-ppc64": "0.27.7", "@esbuild/linux-riscv64": "0.27.7", "@esbuild/linux-s390x": "0.27.7", "@esbuild/linux-x64": "0.27.7", "@esbuild/netbsd-arm64": "0.27.7", "@esbuild/netbsd-x64": "0.27.7", "@esbuild/openbsd-arm64": "0.27.7", "@esbuild/openbsd-x64": "0.27.7", "@esbuild/openharmony-arm64": "0.27.7", "@esbuild/sunos-x64": "0.27.7", "@esbuild/win32-arm64": "0.27.7", "@esbuild/win32-ia32": "0.27.7", "@esbuild/win32-x64": "0.27.7" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w=="], "@babel/helper-compilation-targets/lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], + "@develar/schema-utils/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], + "@electron/asar/minimatch/brace-expansion": ["brace-expansion@1.1.15", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg=="], "@electron/universal/minimatch/brace-expansion": ["brace-expansion@2.1.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA=="], @@ -779,6 +822,8 @@ "@types/yauzl/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], + "ajv-keywords/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], + "app-builder-lib/@electron/get/env-paths": ["env-paths@2.2.1", "", {}, "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A=="], "app-builder-lib/@electron/get/fs-extra": ["fs-extra@8.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g=="], @@ -789,6 +834,8 @@ "dir-compare/minimatch/brace-expansion": ["brace-expansion@1.1.15", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg=="], + "dmg-license/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], + "electron-winstaller/fs-extra/jsonfile": ["jsonfile@4.0.0", "", { "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg=="], "electron-winstaller/fs-extra/universalify": ["universalify@0.1.2", "", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="], diff --git a/apps/macos/electron.vite.config.ts b/apps/macos/electron.vite.config.ts index 3605df99af5..af5e146c640 100644 --- a/apps/macos/electron.vite.config.ts +++ b/apps/macos/electron.vite.config.ts @@ -1,11 +1,21 @@ -import { defineConfig } from "electron-vite"; +import { defineConfig, externalizeDepsPlugin } from "electron-vite"; // Reference: https://electron-vite.org/config/ // // No renderer config: the renderer is the apps/web/ Vite project, served in // dev via http://localhost:5173 and in prod via a custom `app://` protocol. +// +// `electron-store` (and its `conf` parent) are ESM-only. electron-vite's +// default externalize plugin would emit `require("electron-store")` in the +// CJS main bundle, which returns the module namespace rather than the +// default export and breaks `new Store(...)`. Excluding them from +// externalization tells Rollup to bundle their ESM source inline, where the +// CJS interop is handled correctly at bundle time. +const ESM_ONLY_DEPS_TO_INLINE = ["electron-store", "conf"]; + export default defineConfig({ main: { + plugins: [externalizeDepsPlugin({ exclude: ESM_ONLY_DEPS_TO_INLINE })], build: { outDir: "out/main", lib: { @@ -17,6 +27,7 @@ export default defineConfig({ }, }, preload: { + plugins: [externalizeDepsPlugin()], build: { outDir: "out/preload", lib: { diff --git a/apps/macos/package.json b/apps/macos/package.json index cc463640268..1971ea20b82 100644 --- a/apps/macos/package.json +++ b/apps/macos/package.json @@ -17,5 +17,8 @@ "electron-vite": "5.0.0", "typescript": "5.9.3", "vite": "7.3.3" + }, + "dependencies": { + "electron-store": "11.0.2" } } diff --git a/apps/macos/src/main/index.ts b/apps/macos/src/main/index.ts index 70e78f26f98..c1cdeaa872c 100644 --- a/apps/macos/src/main/index.ts +++ b/apps/macos/src/main/index.ts @@ -1,9 +1,11 @@ -import { app, BrowserWindow, net, protocol, session, shell } from "electron"; +import { app, BrowserWindow, ipcMain, net, protocol, session, shell } from "electron"; import { spawn, type ChildProcess } from "node:child_process"; import fs from "node:fs/promises"; import { pathToFileURL } from "node:url"; import path from "node:path"; +import { readSetting, writeSetting } from "./settings.js"; + const DEV_SERVER_URL = "http://localhost:5173"; const DEV_SERVER_ORIGIN = new URL(DEV_SERVER_URL).origin; const APP_PROTOCOL = "app"; @@ -152,6 +154,16 @@ const installPermissionHandler = (): void => { ); }; +// IPC bridge for the `window.vellum.settings.*` API exposed by preload. +// Errors from electron-store's schema validator (thrown as SyntaxError from +// `set`) propagate as rejected Promises to the renderer. +const installSettingsIpc = (): void => { + ipcMain.handle("vellum:settings:get", (_event, key: string) => readSetting(key)); + ipcMain.handle("vellum:settings:set", (_event, key: string, value: unknown) => { + writeSetting(key, value); + }); +}; + // --------------------------------------------------------------------------- // Daemon supervisor // --------------------------------------------------------------------------- @@ -258,6 +270,7 @@ app registerAppProtocol(); } installPermissionHandler(); + installSettingsIpc(); spawnDaemon(); createWindow(); diff --git a/apps/macos/src/main/settings.ts b/apps/macos/src/main/settings.ts new file mode 100644 index 00000000000..07496690bb9 --- /dev/null +++ b/apps/macos/src/main/settings.ts @@ -0,0 +1,75 @@ +import Store, { type Schema } from "electron-store"; + +/** + * Persisted user preferences shape. The schema below validates writes; reads + * are returned as `null` when a key has never been written and no default + * applies. Top-level keys are the four named categories from LUM-1846 — + * additional categories get added here as future tickets need them, with a + * matching schema entry to keep validation honest. + */ +export interface AppSettings { + hotkeys: Record; + theme: "light" | "dark" | "system"; + windowState: { + x?: number; + y?: number; + width?: number; + height?: number; + }; + featureFlags: Record; +} + +const schema: Schema = { + hotkeys: { + type: "object", + additionalProperties: { type: "string" }, + default: {}, + }, + theme: { + type: "string", + enum: ["light", "dark", "system"], + default: "system", + }, + windowState: { + type: "object", + properties: { + x: { type: "number" }, + y: { type: "number" }, + width: { type: "number" }, + height: { type: "number" }, + }, + additionalProperties: false, + }, + featureFlags: { + type: "object", + additionalProperties: { type: "boolean" }, + default: {}, + }, +}; + +let instance: Store | null = null; + +const store = (): Store => { + if (!instance) { + instance = new Store({ schema }); + } + return instance; +}; + +/** + * Read a setting. Returns `null` (not `undefined`) when the key is absent so + * the IPC channel marshals cleanly across the contextBridge. + */ +export const readSetting = (key: string): unknown => { + const value = store().get(key as keyof AppSettings); + return value === undefined ? null : value; +}; + +/** + * Write a setting. electron-store validates the value against the schema and + * throws `SyntaxError` (with the ajv error message) when invalid; that + * surfaces to the renderer as a rejected Promise from `window.vellum.settings.set`. + */ +export const writeSetting = (key: string, value: unknown): void => { + store().set(key as keyof AppSettings, value as never); +}; diff --git a/apps/macos/src/preload/index.ts b/apps/macos/src/preload/index.ts index 6b0249e6c19..9d2b3fdf262 100644 --- a/apps/macos/src/preload/index.ts +++ b/apps/macos/src/preload/index.ts @@ -1,4 +1,4 @@ -import { contextBridge } from "electron"; +import { contextBridge, ipcRenderer } from "electron"; // Surface exposed to the renderer as `window.vellum`. Implementations land in // follow-up tickets; for now these are typed stubs so the renderer can @@ -30,8 +30,10 @@ const bridge: VellumBridge = { getToken: notImplemented("auth.getToken"), }, settings: { - get: notImplemented("settings.get"), - set: notImplemented("settings.set"), + get: (key: string): Promise => + ipcRenderer.invoke("vellum:settings:get", key) as Promise, + set: (key: string, value: T): Promise => + ipcRenderer.invoke("vellum:settings:set", key, value) as Promise, }, helper: { ping: notImplemented("helper.ping"), diff --git a/apps/macos/tsconfig.json b/apps/macos/tsconfig.json index 345e37d40f2..a7a1d400a7c 100644 --- a/apps/macos/tsconfig.json +++ b/apps/macos/tsconfig.json @@ -1,8 +1,8 @@ { "compilerOptions": { "target": "ES2022", - "module": "CommonJS", - "moduleResolution": "Node", + "module": "Preserve", + "moduleResolution": "Bundler", "lib": ["ES2023"], "outDir": "out", "strict": true, diff --git a/apps/web/src/runtime/is-electron.ts b/apps/web/src/runtime/is-electron.ts index 4104bacf36a..45dc7a643f2 100644 --- a/apps/web/src/runtime/is-electron.ts +++ b/apps/web/src/runtime/is-electron.ts @@ -1,15 +1,18 @@ /** * Minimal ambient declaration of the `window.vellum` bridge exposed by the - * Electron preload script (see `apps/macos/src/preload/index.ts`). Only the - * `platform` discriminator is declared here — additional bridge surfaces - * (`auth`, `settings`, `helper`, etc.) are added in the follow-up tickets - * that wire each feature so the renderer's view of the bridge stays honest - * about what's actually implemented at any given commit. + * Electron preload script (see `apps/macos/src/preload/index.ts`). Surface is + * expanded here as each follow-up ticket wires a real implementation, keeping + * the renderer's view of the bridge honest about what's actually available + * at any given commit. */ declare global { interface Window { vellum?: { platform: "electron"; + settings: { + get(key: string): Promise; + set(key: string, value: T): Promise; + }; }; } } From 08269c2d786a6c4187c8f98673b0db45e3457ddc Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 26 May 2026 23:46:56 +0000 Subject: [PATCH 2/4] refactor(macos): drop windowState from settings + align tsconfig MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit After a deeper read of the codebase's actual cross-platform precedent (`native-biometric.ts` — named functions per setting, internal isNativePlatform() gate, localStorage fallback on web), two grounded adjustments: - Remove `windowState` from `AppSettings` and the electron-store schema. Window geometry is main-process-managed in Electron, system-managed on iOS, browser-managed on web — the renderer never reads or writes it, so the bridge shouldn't claim it does. If window-state restore is wired in a future ticket, it lives in its own keyspace or via a dedicated library (e.g. `electron-window-state`). Dead-code rule applied prospectively. - tsconfig `module: "Preserve"` → `"ESNext"` (moduleResolution stays at `"Bundler"`). Matches the upstream `@electron-toolkit/tsconfig.node` baseline every electron-vite starter inherits. `Preserve` worked but isn't the canonical value for the stack. Convention note for the next consumer ticket (theme persistence, hotkey configuration, feature-flag UI): renderer-side wrappers should follow the `apps/web/src/runtime/native-biometric.ts` precedent — a per-capability module with named functions (e.g. `getTheme()` / `setTheme(theme)`), internal platform branch (`isElectron()` → bridge, `isNativePlatform()` → Capacitor when wired, else → localStorage), and TypeScript types as the cross-platform contract. The generic `window.vellum.settings.{get,set}` bridge is the underlying API those wrappers call; consumers should not import it directly. https://claude.ai/code/session_011sZ4p8AkqDQMSavHvWfioe --- apps/macos/src/main/settings.ts | 24 +++++++----------------- apps/macos/tsconfig.json | 2 +- 2 files changed, 8 insertions(+), 18 deletions(-) diff --git a/apps/macos/src/main/settings.ts b/apps/macos/src/main/settings.ts index 07496690bb9..13b8befd61c 100644 --- a/apps/macos/src/main/settings.ts +++ b/apps/macos/src/main/settings.ts @@ -3,19 +3,19 @@ import Store, { type Schema } from "electron-store"; /** * Persisted user preferences shape. The schema below validates writes; reads * are returned as `null` when a key has never been written and no default - * applies. Top-level keys are the four named categories from LUM-1846 — + * applies. Top-level keys are the renderer-facing categories from LUM-1846 — * additional categories get added here as future tickets need them, with a * matching schema entry to keep validation honest. + * + * Note: window geometry (position, size) is intentionally NOT here. It's a + * main-process-managed concern in Electron (system-managed on iOS, + * browser-managed on web), and the renderer never reads or writes it. If + * window-state restore is wired in a future ticket, it lives in its own + * keyspace or via a dedicated library (e.g. `electron-window-state`). */ export interface AppSettings { hotkeys: Record; theme: "light" | "dark" | "system"; - windowState: { - x?: number; - y?: number; - width?: number; - height?: number; - }; featureFlags: Record; } @@ -30,16 +30,6 @@ const schema: Schema = { enum: ["light", "dark", "system"], default: "system", }, - windowState: { - type: "object", - properties: { - x: { type: "number" }, - y: { type: "number" }, - width: { type: "number" }, - height: { type: "number" }, - }, - additionalProperties: false, - }, featureFlags: { type: "object", additionalProperties: { type: "boolean" }, diff --git a/apps/macos/tsconfig.json b/apps/macos/tsconfig.json index a7a1d400a7c..748f35f4ebf 100644 --- a/apps/macos/tsconfig.json +++ b/apps/macos/tsconfig.json @@ -1,7 +1,7 @@ { "compilerOptions": { "target": "ES2022", - "module": "Preserve", + "module": "ESNext", "moduleResolution": "Bundler", "lib": ["ES2023"], "outDir": "out", From 2b97abc92ccb6785d7154056ae886ecc30b8b675 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 27 May 2026 00:13:02 +0000 Subject: [PATCH 3/4] docs(macos): document the bridge extension and renderer-consumer conventions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - apps/macos/README.md: replace the stale "Renderer bridge" section that said all methods were stubs. Document what's wired (platform, settings) vs what's stubbed (auth, helper), how to verify the bridge from the renderer, and add a "When to extend the bridge with new methods" section codifying the generic-KV-vs-dedicated-method rule for non-sensitive vs sensitive capabilities. Cites Electron's "one method per IPC message" security guidance. - apps/macos/src/preload/index.ts: refresh the VellumBridge interface comment to reflect the post-LUM-1846 state and point at the README section for anyone adding new methods. - apps/web/src/runtime/is-electron.ts: tell feature code in apps/web/ not to call window.vellum.* directly. The established pattern is a per-capability module under apps/web/src/runtime/ that owns the cross-platform branch internally — matches native-biometric.ts. No code changes; pure docs/comments. https://claude.ai/code/session_011sZ4p8AkqDQMSavHvWfioe --- apps/macos/README.md | 34 ++++++++++++++++++++++++++--- apps/macos/src/preload/index.ts | 10 ++++++--- apps/web/src/runtime/is-electron.ts | 8 +++++++ 3 files changed, 46 insertions(+), 6 deletions(-) diff --git a/apps/macos/README.md b/apps/macos/README.md index a3a7fa0bf77..81e70e7a047 100644 --- a/apps/macos/README.md +++ b/apps/macos/README.md @@ -59,18 +59,46 @@ apps/macos/ ├── electron.vite.config.ts # main + preload Vite entries (no renderer) ├── src/ │ ├── main/index.ts # window creation, app://, assistant supervisor +│ ├── main/settings.ts # electron-store schema + IPC-backed accessors │ └── preload/index.ts # contextBridge: window.vellum.* └── tsconfig.json ``` ## Renderer bridge -The preload script exposes a typed `window.vellum` API to the renderer. Today -it only reports `platform: "electron"`; auth, settings, and helper methods -are typed stubs to be wired up in follow-up tickets. +The preload script exposes a typed `window.vellum` API to the renderer: + +- `platform: "electron"` — host discriminator. +- `settings.get(key)` / `settings.set(key, value)` — persisted preferences, + backed by `electron-store` in the main process. Writes are validated against + a JSON schema (`hotkeys`, `theme`, `featureFlags`); a schema violation + surfaces as a rejected `Promise`. +- `auth.*` and `helper.*` — typed stubs that reject with "not implemented yet" + until the corresponding feature tickets land. Verify the bridge from the renderer: ```js console.log(window.vellum.platform); // "electron" +await window.vellum.settings.set("theme", "dark"); +console.log(await window.vellum.settings.get("theme")); // "dark" ``` + +### When to extend the bridge with new methods + +The generic `settings.{get,set}` surface is appropriate for user preferences +where the renderer is the source of truth and the value is non-sensitive +(theme, layout, feature-flag overrides, etc.). For higher-sensitivity +capabilities — auth tokens, biometric keys, file paths, anything where the +renderer should not be free to read or write arbitrary keys — add a +dedicated bridge method (`window.vellum..()`) with its +own IPC channel. This follows Electron's "one method per IPC message" +guidance from the [security tutorial](https://www.electronjs.org/docs/latest/tutorial/security#17-validate-the-sender-of-all-ipc-messages), +which keeps the renderer-exposed surface narrow and auditable. + +Renderer-side consumers in `apps/web/` should wrap bridge access in a +per-capability module (see `apps/web/src/runtime/native-biometric.ts` for +the established shape) rather than reaching into `window.vellum.*` +directly from feature code. That keeps the platform-branching logic in +one place and makes the cross-platform contract (web / iOS / Electron) +live in TypeScript types. diff --git a/apps/macos/src/preload/index.ts b/apps/macos/src/preload/index.ts index 9d2b3fdf262..2a83f7c5b47 100644 --- a/apps/macos/src/preload/index.ts +++ b/apps/macos/src/preload/index.ts @@ -1,8 +1,12 @@ import { contextBridge, ipcRenderer } from "electron"; -// Surface exposed to the renderer as `window.vellum`. Implementations land in -// follow-up tickets; for now these are typed stubs so the renderer can -// feature-detect the Electron host. +// Surface exposed to the renderer as `window.vellum`. `platform` and the +// `settings` accessors are wired through IPC; `auth` and `helper` are typed +// stubs that reject with "not implemented yet" until their feature tickets +// land. When adding new bridge methods, see the "When to extend the bridge +// with new methods" section in `apps/macos/README.md` for the convention +// (generic KV for non-sensitive prefs; dedicated `.()` +// methods for sensitive capabilities). export interface VellumBridge { platform: "electron"; auth: { diff --git a/apps/web/src/runtime/is-electron.ts b/apps/web/src/runtime/is-electron.ts index 45dc7a643f2..2671c986fd1 100644 --- a/apps/web/src/runtime/is-electron.ts +++ b/apps/web/src/runtime/is-electron.ts @@ -4,6 +4,14 @@ * expanded here as each follow-up ticket wires a real implementation, keeping * the renderer's view of the bridge honest about what's actually available * at any given commit. + * + * Feature code in `apps/web/` should NOT call `window.vellum.*` directly. + * Instead, wrap each persisted capability in a per-feature module under + * `apps/web/src/runtime/` with named functions (see `native-biometric.ts` + * for the established shape: `isBiometricEnabled()` / `setBiometricEnabled()`). + * The module owns the cross-platform branch — `isElectron()` calls into + * `window.vellum`, `isNativePlatform()` calls Capacitor, and the web branch + * uses `localStorage` — so consumers stay platform-agnostic. */ declare global { interface Window { From 51ed3fa6998c80964610080a9e8a81228c29f52c Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 27 May 2026 00:29:43 +0000 Subject: [PATCH 4/4] fix(macos): close settings store root + ack Bundler resolution in apps/AGENTS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two review-feedback follow-ups, kept in the same PR per the fix-as-you-go convention: - apps/macos/src/main/settings.ts: pass `rootSchema: { additionalProperties: false }` to electron-store. Without it, conf's validator only enforces the per-key shapes, leaving the root object open — a renderer typo like `settings.set("them", "dark")` would silently persist as an unknown top-level key. With the root closed, unknown keys are rejected at validation time. Per-key shape validation is unchanged. - apps/AGENTS.md: the convention bullet previously said "NodeNext module resolution" full stop. Update it to acknowledge that Electron apps may use `moduleResolution: "Bundler"` with `module: "ESNext"` when their bundler (electron-vite, in our case) handles ESM/CJS interop. The `.js`-extension convention applies regardless. Matches the `@electron-toolkit/tsconfig.node` baseline that electron-vite starters inherit. https://claude.ai/code/session_011sZ4p8AkqDQMSavHvWfioe --- apps/AGENTS.md | 7 ++++++- apps/macos/src/main/settings.ts | 8 +++++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/apps/AGENTS.md b/apps/AGENTS.md index 1dd0c6af0ae..e6c4f9aeb24 100644 --- a/apps/AGENTS.md +++ b/apps/AGENTS.md @@ -9,7 +9,12 @@ Applies to all code under `apps/`. Subordinate to root [`AGENTS.md`](../AGENTS.m - No workspaces, no Turborepo. Per-package `bun install`. Exact version pinning is enforced repo-wide; see root `AGENTS.md` for the dependency, license, and tool-version rules. -- TypeScript imports use `.js` extensions (NodeNext module resolution). +- TypeScript imports use `.js` extensions. Default module resolution is + NodeNext; apps that ship with a bundler that handles ESM/CJS interop + (currently `apps/macos/` via electron-vite) may use `moduleResolution: + "Bundler"` with `module: "ESNext"` so the bundler's resolution rules + match TypeScript's view of the import graph. The `.js` extension + convention applies regardless. ## Adding a new app diff --git a/apps/macos/src/main/settings.ts b/apps/macos/src/main/settings.ts index 13b8befd61c..66f9ea5f8b7 100644 --- a/apps/macos/src/main/settings.ts +++ b/apps/macos/src/main/settings.ts @@ -41,7 +41,13 @@ let instance: Store | null = null; const store = (): Store => { if (!instance) { - instance = new Store({ schema }); + instance = new Store({ + schema, + // Close the root so a renderer typo (e.g. `set("them", "dark")`) is + // rejected at validation time instead of silently persisted as an + // unknown top-level key. Per-key shapes are still validated by `schema`. + rootSchema: { additionalProperties: false }, + }); } return instance; };