refactor(panel): add @luke/panel with the bridge and platform doors
Summary
Step 16 of the web plan ("š± Web support", Refactors to land first) moves the desktop panel out of apps/desktop/src/renderer into a package, so apps/web can mount the same panel later. This is the first of about 13 PRs. The move goes bottom-up, one dependency band at a time, because tasks/, desktop/, settings/ and the renderer root import each other in both directions, and a package may not import an app.
This PR adds packages/panel (@luke/panel) and moves its first band as-is. Only the import specifiers change:
- the bridge door
app-bridge.tsand the platform doorapp-platform.ts - the act door
act.ts - the 7 hooks that reach only those doors:
use-sign-in,desktop/use-run-choices,settings/writes,tasks/use-inbox-pages,tasks/use-open-link,tasks/use-taskless-inbox-items,tasks/use-workspace-members
Git records all 10 files as renames. The 116 desktop files that imported them now import @luke/panel/<path>. Biome re-sorted those imports, which is why each import line moves. The package's exports names exactly the subpaths the desktop still imports, and later bands prune that list as their importers move.
Beyond renames and import rewrites (please look at these specifically):
packages/panelskeleton:package.json,tsconfig.json(copied from@luke/ui's),src/sources.css(@source "./", the@luke/ui/theme.csspattern). The desktop'stailwind.cssimportssources.css, so the utilities a panel module spells keep being generated. Novitest.config.tsyet, because no test moves in this band; the first band that moves one adds the config and the rootprojectsrow.packages/AGENTS.md(approved by the developer): the DOM may live inpackages/uiandpackages/panel; Electron in neither.scripts/repository-checks.sh:- The logic-lives-in-packages shell list drops the two door rows (
renderer/app-bridge.ts,renderer/app-platform.ts), as their comment promised for step 16. It addsrenderer/desktop-platform.ts, the desktop's answer to the platform door: with the doors gone it imports only packages, and it is shell by role. - A new
panel_shell_reachesrefusal keepspackages/panel/srcoffwindow.luke, every desktop alias (#shared/ #renderer/ #main/ #preload/ #testing/) andelectron/electron-updater/@sentry/electron. - The renderer's path-keyed checks also scan
packages/panel/src:renderer_escapes,drawn_hosted_quota,node_reaching_effectand themkdtemprule.
- The logic-lives-in-packages shell list drops the two door rows (
apps/desktop/src/renderer/AGENTS.md: one path changes. "renderer/act.tsis the only caller" now reads "@luke/panel'sact.ts".apps/desktop/package.jsongains@luke/panel: workspace:*, andpnpm-lock.yamlis regenerated bypnpm install.
The desktop's behavior is unchanged: no code inside a moved file changed beyond its relative specifiers gaining .js.
Evidence
-
./scripts/repository-checks.sh: passed. -
tscfor@luke/paneland@luke/desktop: passed. -
lint,knip,discarded-effect(turbo,--concurrency=1): passed. -
@luke/desktopvitest: 215 files, 2043 tests passed. The count is unchanged: no test was added or removed. -
@luke/desktopbuild, bundle budget included: passed.panel.jsplus its static chunks carries the same modules. Only their order within the shared chunks moves. -
I booted the built app on Linux under
xvfb-run -awith--disable-gpu --disable-dev-shm-usage --fixture smoke, driven through Playwright's_electron. Not run on a Mac.- The panel drew Home with the fixture's tasks.
- The hidden voice window mounted
VoiceHost(<div id="root"><audio autoplay hidden>ā¦). call.htmldrew the call card.- The only console errors were Sentry's
sentry-ipc://requests, which also appear onmain.
Panel (
--fixture smoke) and the call window:

Invariants
-
./scripts/check.shgreen. (CI) - JSON Schema goldens unchanged.
- Every behavior change has, in this PR, the test that fails without it; no change-detector test added. (No behavior change.)
- Gateway envelope goldens unchanged.
- No new
astype assertion outside the allowlisted files. - No
Effect.runPromise/runSync/runForkoutside the runtime edges. - No new raw async primitives.
- No Effect, Stream, or Layer written as a statement and dropped.
- Test count equals the pre-PR count (2043).
- No hand-rolled file replaced.
- Every
CLAUDE.md/AGENTS.mdsentence naming a changed part is edited. -
PRIVACY.mdunchanged. - Package
package.jsondeps match what the sources import by bare specifier. - Barrel/door rule: no Node-reaching Effect module behind a barrel the renderer opens. The package is now covered by the
node_reaching_effectcheck.
Notes
- Next: the renderer runtime and
use-app-state, plus their cascade. That PR also changes the path in the rootAGENTS.md(approved). - Follow-up, not in this PR: the changelog-screenshots skill's hook patch for future releases names
apps/desktop/src/renderer/act.ts. A release cut after this merges needs that path moved topackages/panel/src/act.ts. The per-release patches for past releases stay valid.
Made with Luke