Rename PowerMonitorService -> PowerService and relocate it to
src/main/core/power/, turning the shutdown-only service into a complete,
self-contained power hub:
- Typed power events (suspend/resume/lock/unlock/power-source) with
suspend/resume + power-source de-duplication; lock/unlock pass-through.
- Bounded, cross-platform shutdown barrier: preventDefault on macOS/Linux,
blockShutdown via a minimal hidden window on Windows; handlers run under a
5s hard timeout, then the app quits through the normal quit flow.
- Best-effort, ref-counted sleep prevention: preventSleep(reason) returns a
Disposable and never throws (the provider degrades internally). The OS
blocker engages only while a hold is held AND the user enabled
app.power.prevent_sleep_when_busy. JobManager is the first registrant,
acquiring a hold per execution attempt.
Add the app.power.prevent_sleep_when_busy preference (v2-only, no v1 source)
and a Settings -> General toggle. Service phase moved Background -> WhenReady.
Collect the WindowManager caller-window controls (close / minimize / maximize /
unmaximize / set_full_screen / is_maximized / is_full_screen / get_init_data) and
the three directed window events (maximized_changed / fullscreen_changed / reused)
onto the IpcApi framework, replacing the legacy this.ipcHandle registrations, the
hand-written preload windowManager namespace, and the IpcChannel enums.
- Requests: ctx.senderId addresses the caller window (was getWindowIdByWebContents);
fire-and-forget controls return void, queries keep their read type.
- Events: directed IpcApiService.send to the affected window, never broadcast.
- openSettings is a navigation concern misfiled under windowManager — moved to
window.api.settings.openSettings (still legacy IPC, not yet on IpcApi).
- WindowManager_Open was dead (no renderer/preload consumer) — deleted.
- MainWindow_* (main-window singleton ops) intentionally left on legacy IPC.
Behavior verified equivalent before/after by independent review.
Store BrowserWindow directly in a private field; mark public getMainWindow()
as @deprecated with a runtime warn. Migrate ~27 webContents.send call sites
across 20 services to WindowManager.broadcastToType(). Move main-window-
specific IPC handlers from the monolithic ipc.ts into MainWindowService and
deparameterize registerIpc(). Rename window/app channels to MainWindow_*
(values main-window:*), keeping the 5 sender-resolution channels as-is for
detached-tab reuse. Swap @DependsOn to WindowManager in migrated broadcasters.
- Rename WindowService -> MainWindowService (git mv, preserves blame/log)
- Register WindowType.Main in windowRegistry (singleton, showMode: manual);
MainWindowService drives construction via wm.open and retains business
logic only: IPC handlers, tray-aware close, crash recovery, show/toggle
- 50 call sites renamed (@DependsOn, application.get keys, imports)
WM Dock visibility refactored from visibility-based to existence-based:
- updateDockVisibility predicate drops isVisible/isMinimized check — a
hidden main window must not remove the Dock icon (Cmd+W semantics)
- Add wm.setMacShowInDockByType(type, value) for tray-mode transitions;
keyed by type so services can suppress Dock before the first instance
exists (tray-on-launch path)
- Reduce triggers to window creation, destruction, and type-override
changes; show/hide/minimize/restore no longer affect Dock state
- dockShouldBeVisible initializes true to match Electron's default
MainWindowService uses setMacShowInDockByType for tray-on-launch, close-
to-tray, and reopen-from-tray paths, replacing manual app.dock?.hide()
calls that raced against WM.
Docs updated across window-manager/{README, overview, platform,
api-reference, migration-guide}. New MainWindowService unit test covers
the close matrix (isQuitting, win/linux tray on/off, mac default, mac
tray on_close, fullscreen edge, Dock override assertions) and crash
recovery (first reload vs second-within-60s forceExit).
Pooled lifecycle windows register a `close` handler that always calls
`event.preventDefault()` to recycle them to the pool. When `app.quit()`
fires `close` on every BrowserWindow, a single preventDefault is enough
for Electron to abort the quit chain, so `will-quit` never fires and
`Application.shutdown()` never runs. Once SelectionToolbar was migrated
into WindowManager's pooled lifecycle (57310618c), Ctrl+C started
leaving Electron as an orphan with `_isQuitting=true`, and a second
quit attempt from the menu was swallowed by the existing
`Already quitting` guard.
Bypass the pooled-close intercept and pool replenishment whenever
`application.isQuitting` is true — close events must reach their
native destination so `will-quit` can drive the normal
`Application.shutdown()` path. Also make `Application.quit()`
re-trigger `app.quit()` when already quitting, so a stalled first
attempt no longer traps the user into `kill -9`.
Covered by a new WindowManager test case asserting that pooled close
is not preventDefault'd while `application.isQuitting === true`.
Align with the existing @logger alias convention by introducing
@application as a short alias for src/main/core/application. This
reduces import verbosity across ~130 main-process files while keeping
4 intentional sub-path imports (@main/core/application/Application)
unchanged to preserve the vi.mock bypass mechanism in tests.
Configured in tsconfig.node.json and electron.vite.config.ts; Vitest
inherits the alias automatically.
Signed-off-by: fullex <0xfullex@gmail.com>
Introduce `Application.initPathRegistry()` and invoke it from preboot in
`main/index.ts` (after the single-instance lock check, before
`crashReporter.start()`). `application.bootstrap()` no longer initializes
the path registry; it asserts the registry is already initialized and
fails fast with a clear error if not.
Why: the registry's only hard dependency is `app.setPath('userData')`
having completed, which `resolveUserDataLocation()` finishes in preboot.
Initializing from preboot decouples path registry lifetime from service
orchestration and exposes the timing contract as an explicit assertion
rather than a silent lazy init inside `bootstrap()`. This opens the door
for future preboot code (migration, rtk extraction, crashReporter) to
consume `application.getPath()` directly.
Also:
- `buildPathRegistry()` JSDoc now documents the constraint that new path
keys must be resolvable in the preboot phase (no `whenReady`-only APIs,
no dependencies on services being started).
- `getPath()` error message updated to point at `initPathRegistry()`.
- Global test mock gains a matching `initPathRegistry` stub.
- `preboot/README.md` and `paths/README.md` sync the new timing.
Signed-off-by: fullex <0xfullex@gmail.com>
Replace consumers of the seven legacy file.ts helpers (getTempDir,
getFilesDir, getNotesDir, getConfigDir, getCacheDir, getMcpDir,
getAppConfigDir) with application.getPath(), then delete the helpers.
All seven map to existing path registry keys — zero new keys needed.
Stable sub-path joins now use dedicated keys directly
(feature.dxt.uploads.temp, feature.preprocess.temp, feature.mcp.oauth,
feature.anthropic.oauth_file, feature.copilot.token_file,
feature.mcp.memory_file).
Auto-ensure on application.getPath() makes the existing fs.existsSync
+ fs.mkdirSync boilerplate redundant; removed initStorageDir from
FileStorage and ensureDirectories from DxtService and
BasePreprocessProvider.
Three top-level singletons (fileStorage, dxtService, copilotService)
are instantiated during the static import graph of src/main/index.ts
— before application.bootstrap() builds the path registry. Field
initializers and constructor assignments calling getPath() at
instantiation time would throw "PATHS not initialized". Each is
converted to a lazy getter (cached in CopilotService where the
resolution has an fs.existsSync side effect) with a TODO(v2) comment
marking it as a workaround for an underlying architectural issue:
these singletons should be migrated into the lifecycle system in a
follow-up.
Module-top-level const path expressions in AnthropicService
(CREDS_PATH) and mcpServers/memory.ts (defaultMemoryPath) are
wrapped in lazy getter functions for the same reason.
Test infrastructure: the unified application mock factory in
tests/__mocks__/main/application.ts now stubs getPath, so future
tests that instantiate services with path-dependent fields work
without per-test setup. The pre-existing ad-hoc mock in
MCPService.test.ts gets the same stub. Four describe blocks for the
deleted helpers are removed from file.test.ts (getAppConfigDir was
confirmed dead code, with no production callers).
src/main/utils/init.ts is intentionally untouched: its getConfigDir
is a private local function (not an import from file.ts), and the
file is @deprecated awaiting v2 BootConfig migration.
Signed-off-by: fullex <0xfullex@gmail.com>
Tests calling application.get('WindowService') now get a minimal mock
instead of throwing "Unknown service: WindowService".
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: suyao <sy20010504@gmail.com>
Replace ad-hoc vi.mock('@main/core/application') calls across test files
with a centralized mockApplicationFactory(overrides?) utility that provides
all registered services by default and supports per-service overrides.
- Add tests/__mocks__/main/DbService.ts (standalone DbService mock)
- Add tests/__mocks__/main/application.ts (unified factory)
- Simplify tests/main.setup.ts global mock
- Migrate 4 test files to use mockApplicationFactory
- Document unified mock system in CLAUDE.md and README.md
Signed-off-by: fullex <0xfullex@gmail.com>