Skip to content

feat(single-app): collapse daemon + overlay into one Electron app (ADR-0003, Phase 1) - #68

Merged
Snehit70 merged 7 commits into
mainfrom
feat/single-app-spike
Jul 15, 2026
Merged

Snehit70 merged 7 commits into
mainfrom
feat/single-app-spike

Conversation

@Snehit70

@Snehit70 Snehit70 commented Jul 15, 2026 •

Copy link
Copy Markdown
Owner

Summary

Collapses the two-process topology (Bun daemon + Electron overlay talking over a unix socket, with four supervision layers) into one resident Electron app: the main process hosts the daemon and the overlay BrowserWindow, state flows main → renderer via webContents.send, and Electron is the single supervisor. Implements ADR-0003, Phase 1.

Builds on the earlier spike commits (fae7737, 2da7023) already on this branch.

What changed

  • Collapse — delete the supervision stack (supervisor.ts, overlay-process.ts, daemon IPC socket server, overlay ipc-client.ts + its Electron main.ts) and the crash-recovery integration test that exercised the supervisor. DaemonService now extends EventEmitter and emits state/audioLevel instead of pushing over a socket.
  • App host — src/app/main.ts runs the daemon in-process, creates the overlay window, and forwards state to the renderer. src/app/command-server.ts binds the unix command socket for payload-carrying CLI verbs (soniox-toggle) and doubles as the single-instance guard.
  • CLI — src/cli/app-launcher.ts locates the bundle + Electron binary and spawns the app (stripping ELECTRON_OZONE_PLATFORM_HINT so the window comes up as an XWayland client). toggle lazily respawns a dead app before delivering the trigger (crash recovery). install prints Hyprland exec-once guidance instead of writing a systemd unit.
  • Portability — replace Bun-only import.meta.dir with fileURLToPath-based resolution so the daemon runs under Electron/Node.
  • Build — scripts/build-app.ts esbuild-bundles the app to dist/app, writing package.json name: hyprvox-overlay so the window's WM_CLASS is hyprvox-overlay (Hyprland rules target that class). Adds src/app/tsconfig.json and typecheck/build:app scripts.
  • Docs — ADR-0003 with Phase-1 implementation notes; updated ARCHITECTURE, CLI_COMMANDS, AGENTS.

Window identity & XWayland (why it matters)

The overlay parks off-screen via client self-positioning, which native Wayland forbids. The app pins --ozone-platform=x11 and the launcher strips ELECTRON_OZONE_PLATFORM_HINT (the session sets it to auto, which Electron consumes before JS runs and which otherwise suppresses the window entirely). WM_CLASS must be hyprvox-overlay or Hyprland rules don't match.

Verification

  • Root + app typecheck clean.
  • 240 vitest + 252 bun tests green.
  • Live end-to-end: real spoken dictation through the single app — engine groq+deepgram, exact_match merge (confidence 1), transcript reached clipboard + history.

Out of scope (follow-ups)

  • Phase 2 (decideMerge → utilityProcess) intentionally deferred until the ongoing long-uptime overlay-slowdown investigation reports, so the diagnosis drives the fix rather than a guess.
  • electron still sits under overlay devDependencies though it's now the production runtime — worth reclassifying separately.

Summary by CodeRabbit

  • New Features

    • Consolidated the daemon and overlay into a single resident Electron app.
    • Added CLI support for foreground operation, app lifecycle control, and live dictation toggling.
    • Added single-instance protection and command handling through a shared command channel.
    • Added app bundling and improved startup logging.
  • Bug Fixes

    • Improved compatibility when running audio recording across Electron and Node environments.
    • Added recovery for stale process and command-channel state.
  • Documentation

    • Updated architecture, CLI, setup, operational data, and autostart guidance for the new app structure.

Snehit70 added 7 commits July 15, 2026 12:50
Phase 0 proof of the single-app topology. A new src/app/main.ts hosts
DaemonService and the overlay window in one Electron process; the daemon
still emits state over its unix socket and the window's IPCClient connects
in-process (loopback) — Phase 1 replaces that hop with webContents.send.

Three env-gated seams make this bootable alongside the running daemon:
- HYPRVOX_EMBEDDED_OVERLAY: overlay-process.start() no-ops (no child Electron)
- HYPRVOX_SOCKET_PATH: alternate IPC socket (ipc.ts + overlay ipc-client)
- HYPRVOX_PID_FILE: alternate daemon pidfile

Verified: bundles via esbuild (ESM, electron + node-global-key-listener
external), boots under Electron as a single PID, daemon + overlay + IPC all
live, no second Electron spawned, running daemon untouched.
…utdown

Audit of the full dependency tree found four packages that cannot be
inlined by a bundler (vendored binaries, dynamic require, package-relative
asset paths): node-record-lpcm16, node-global-key-listener, clipboardy,
plus a latent clipboardy xsel-fallback path break. Resolution: bundle only
our own code (esbuild --packages=external); all deps resolve from
node_modules at runtime, deleting the whole bug class.

- recorder: default-import node-record-lpcm16 (CJS named export invisible
  to Node's ESM lexer; crashed under Electron, tolerated by Bun)
- app main: boot failure exits instead of leaving a half-alive window;
  SIGTERM/SIGINT -> app.quit -> service.stop so the socket file is
  unlinked and the next boot cannot EADDRINUSE
- tsconfig: exclude src/app (Electron types live in the overlay project;
  the app gets its own tsconfig in Phase 1)

Verified end to end in one Electron process: SIGUSR1 trigger -> arecord ->
Groq+Deepgram streaming -> merge/validation (silence hallucination
correctly filtered) -> idle. 254 tests pass under bun; typecheck clean.
Replace Bun-only import.meta.dir in project-paths with a
fileURLToPath-anchored walk up to the hyprvox package.json, so the
resolver works under Electron/Node as well as Bun. Add socket-path util
exporting HYPRVOX_SOCKET_PATH-aware daemon.sock location for the app
command socket and CLI verbs to share.
…rocess

Delete the supervision stack (supervisor, overlay-process manager, daemon
IPC socket server, overlay ipc-client and its Electron main) and the
crash-recovery integration test that exercised the supervisor. DaemonService
now extends EventEmitter and emits state/audioLevel instead of pushing over
a socket. src/app/main.ts hosts the daemon in-process, creates the overlay
BrowserWindow, and forwards state to the renderer via webContents.send.
command-server.ts binds the unix command socket for payload-carrying CLI
verbs (soniox-toggle) and doubles as the single-instance guard. Overlay
renderer/preload type imports repoint to shared/ipc-types.
Add app-launcher.ts (locates dist/app + overlay's Electron binary,
strips ELECTRON_OZONE_PLATFORM_HINT so the window comes up as an XWayland
client, spawns detached/foreground). start/stop/restart now target the app
process; toggle lazily respawns a dead app before delivering the trigger
(the crash-recovery path). install prints Hyprland exec-once guidance
instead of writing a systemd unit. Remove the standalone overlay command;
reword health and setup for the single-app topology.
scripts/build-app.ts esbuild-bundles src/app/main.ts to dist/app/main.js
(packages external, electron external, tsconfigRaw override to defeat path
mapping, ESM-compat banner) and writes dist/app/package.json with
name hyprvox-overlay so the window's WM_CLASS is hyprvox-overlay.
src/app/tsconfig.json extends root and points electron types at overlay's
node_modules. package.json gains typecheck/typecheck:app/build:app scripts
and the esbuild devDependency; start:overlay removed.
Add ADR-0003 (the daemon+overlay collapse) with Phase 1 implementation
notes on the XWayland dependency, WM_CLASS source, command socket, and the
drop of the systemd install path. Update ARCHITECTURE, CLI_COMMANDS, and
AGENTS to describe the single Electron app, exec-once launch, lazy-spawn
crash recovery, and the command socket.
@coderabbitai

coderabbitai Bot commented Jul 15, 2026 •

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The daemon, overlay, and supervision layers are consolidated into one Electron app. New app bundling and command-socket infrastructure support in-process daemon events, renderer updates, single-instance control, revised CLI lifecycle commands, and Hyprland autostart instructions.

Changes

Single Electron application

Layer / File(s) Summary
Topology and build contract
AGENTS.md, docs/ARCHITECTURE.md, docs/adr/*, package.json, scripts/build-app.ts, overlay/*, src/app/tsconfig.json, src/utils/*, src/audio/recorder.ts, tsconfig.json
Architecture, packaging, build scripts, shared type imports, project-root discovery, socket configuration, and Electron-compatible recorder imports are updated for the single-app topology.
Electron runtime and command server
src/app/*
Electron main hosts the daemon and overlay, while CommandServer provides single-instance guarding and newline-delimited action handling over SOCKET_PATH. Renderer state delivery, overlay recovery, visibility control, and coordinated shutdown are implemented.
Daemon event integration
src/daemon/service.ts
DaemonService emits state and audio-level events, exposes triggerSonioxToggle(), and removes IPC-server and overlay-process lifecycle integration.
CLI lifecycle and autostart
src/cli/*, docs/CLI_COMMANDS.md
CLI lifecycle commands launch and monitor the Electron app, toggle starts the app when needed, Soniox actions use the command socket, and install/setup now show Hyprland autostart instructions. Supporting legacy service and socket health checks are updated.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant CLI
  participant ElectronMain
  participant CommandServer
  participant DaemonService
  participant Renderer
  CLI->>ElectronMain: start or launch app
  ElectronMain->>CommandServer: start single-instance command socket
  ElectronMain->>DaemonService: start daemon in-process
  CLI->>CommandServer: send toggle or soniox-toggle action
  CommandServer->>DaemonService: invoke action callback
  DaemonService->>Renderer: send state and audioLevel events
Loading

Possibly related PRs

  • Snehit70/hyprvox#21: Introduces the earlier multi-process IPC and overlay wiring replaced by this single-app architecture.
  • Snehit70/hyprvox#43: Overlaps with the daemon, streaming, and overlay-process restructuring changed here.
  • Snehit70/hyprvox#66: Shares the soniox-toggle action path through the app command socket.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 31.25% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: collapsing the daemon and overlay into a single Electron app for ADR-0003 Phase 1.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/single-app-spike

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 88c632bfe1

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/cli/app-launcher.ts
Comment on lines +36 to +39
if (!existsSync(join(appDir, "main.js"))) {
throw new Error(
`App bundle not found at ${appDir}. Build it with: bun run build:app`,
);

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 Ship the app bundle with packaged installs

For bun add -g hyprvox installs, package.json still publishes only index.ts, src, README.md, and package.json, so dist/app/main.js and overlay/node_modules/.bin/electron are not present. Since every start/lazy toggle path now calls assertAppLaunchable(), packaged users hit this error and cannot start the app unless they have a source checkout. Please include/build these artifacts during packaging or keep a runnable fallback.

Useful? React with 👍 / 👎.

Comment thread src/cli/index.ts
program
.command("start")
.description("Start the hyprvox app (daemon + overlay in one process)")
.option("--foreground", "Stay attached to the terminal (for debugging)")

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 Keep accepting legacy start flags

Previous hyprvox install wrote a systemd unit with ExecStart=... start --no-supervisor, but the new start command only accepts --foreground. Commander treats unknown options as errors unless allowUnknownOption() is used (docs), so any existing enabled unit fails after an upgrade/reboot before the user removes it. Please keep --no-supervisor as a hidden no-op for compatibility.

Useful? React with 👍 / 👎.

Comment thread src/app/main.ts
"get-connection-status",
(): ConnectionStatus => (service ? "connected" : "disconnected"),
);
mainWindow = createOverlayWindow();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Honor disabled overlay settings

For configs with overlay.enabled=false, the old OverlayProcessManager.start() returned without opening Electron and the service used desktop notifications instead. This boot path always creates the renderer and forwards daemon state, so a disabled-overlay user still gets the window moved on-screen during recording despite health reporting it disabled. Please gate window creation/state forwarding on the overlay config or remove/migrate the setting.

Useful? React with 👍 / 👎.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 10

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/app/main.ts`:
- Around line 24-27: Update src/app/main.ts to import and use the shared pino
logger for every listed console.log and console.error call, including the
display-environment failure and the ranges around lines 62-63, 121-148, 188-193,
and 215-238. Preserve each message, severity, and control flow while routing
output through the logger used by command-server.ts; remove direct console
logging from this file.
- Around line 161-174: Update recreateOverlayWindow to prevent indefinite
recreation attempts when createOverlayWindow repeatedly produces a broken
overlay: track consecutive retry attempts, apply increasing backoff delays, and
stop scheduling retries after a defined maximum. Reset the retry state only
after a successfully usable overlay is established, while preserving the
existing destroy-and-recreate behavior.
- Around line 242-254: Update the will-quit shutdown flow around the stopping
guard to add a finite timeout fallback that calls app.exit(0) even if
service?.stop() or commandServer?.stop() never resolves. Preserve the existing
graceful stop sequence, error logging, and stopping guard, while ensuring the
timeout is cleared when shutdown completes normally to avoid duplicate exit
handling.
- Around line 62-63: Update the uncaughtException and unhandledRejection
handlers in the process-level error handling to terminate the main process with
exit status 1 after logging, using app.exit(1) or equivalent; preserve the
existing error messages and ensure both handlers cannot leave a broken instance
running.

In `@src/cli/app-launcher.ts`:
- Around line 58-64: Update spawnAppDetached() and spawnAppForeground() to
return promises that listen for the child process "error" and "spawn" events,
rejecting on launch failure and resolving only after successful spawning. Ensure
the success message is printed only after awaiting these promises, while
preserving the existing detached/foreground process behavior.
- Around line 53-60: Update spawnAppDetached to direct the detached child’s
stdout and stderr to the existing daily-rotated hyprvox-YYYY-MM-DD.log
destination instead of opening and using app.log. Reuse the established
rotated-log path construction and preserve the existing detached spawn behavior.

In `@src/cli/health.ts`:
- Around line 290-297: Update the socket existence check in the health-check
flow to use the shared SOCKET_PATH contract instead of rebuilding the path with
join(configDir, "daemon.sock"). Import and check SOCKET_PATH directly so
HYPRVOX_SOCKET_PATH overrides are respected.
- Around line 269-279: Update the legacy systemd check in the health command to
distinguish an inactive existing unit from an absent unit: check unit existence
separately from is-active, and reserve the “No legacy systemd service” message
for units that are truly missing. Also update the overlay socket health check to
use the configured SOCKET_PATH value rather than rebuilding daemon.sock from
configDir, preserving support for HYPRVOX_SOCKET_PATH.

In `@src/cli/index.ts`:
- Around line 51-103: Update the stop, restart, and toggle flows to verify the
PID belongs to the running hyprvox instance before sending SIGTERM or SIGUSR1;
readAlivePid only confirms that some process owns the PID. Reuse the existing
command socket or app-owned identity token for this validation, and abort safely
when identity cannot be confirmed.

In `@src/cli/setup.ts`:
- Line 850: Update the remaining messages near the Hyprland autostart prompt and
the corresponding later location to describe skipping autostart instructions
under the --skip-service flow, replacing the outdated “Skipping service install”
wording while preserving the surrounding control flow.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: abb81a9e-4397-4865-b7b3-b8a026fff950

📥 Commits

Reviewing files that changed from the base of the PR and between dd0c365 and 88c632b.

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing (29)
  • AGENTS.md
  • docs/ARCHITECTURE.md
  • docs/CLI_COMMANDS.md
  • docs/adr/0003-collapse-daemon-and-overlay-into-one-electron-app.md
  • overlay/package.json
  • overlay/src/global.d.ts
  • overlay/src/ipc-client.ts
  • overlay/src/main.ts
  • overlay/src/preload.ts
  • overlay/src/renderer/useDaemonState.ts
  • package.json
  • scripts/build-app.ts
  • src/app/command-server.ts
  • src/app/main.ts
  • src/app/tsconfig.json
  • src/audio/recorder.ts
  • src/cli/app-launcher.ts
  • src/cli/health.ts
  • src/cli/index.ts
  • src/cli/overlay.ts
  • src/cli/setup.ts
  • src/daemon/ipc.ts
  • src/daemon/overlay-process.ts
  • src/daemon/service.ts
  • src/daemon/supervisor.ts
  • src/utils/project-paths.ts
  • src/utils/socket-path.ts
  • tests/integration/crash_recovery.test.ts
  • tsconfig.json
💤 Files with no reviewable changes (7)
  • src/daemon/ipc.ts
  • src/daemon/overlay-process.ts
  • overlay/src/main.ts
  • overlay/src/ipc-client.ts
  • src/cli/overlay.ts
  • tests/integration/crash_recovery.test.ts
  • src/daemon/supervisor.ts

Comment thread src/app/main.ts
Comment on lines +24 to +27
if (!process.env.DISPLAY && !process.env.WAYLAND_DISPLAY) {
console.error("[App] No display environment (DISPLAY or WAYLAND_DISPLAY)");
process.exit(1);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Use the shared pino logger instead of console.*.

This file never imports/uses the pino logger (as command-server.ts does) and logs exclusively via console.log/console.error throughout. This loses structured JSON output, level filtering, and the daily rotation the rest of the codebase relies on.

As per coding guidelines, "Use pino for logging, with daily log rotation" for src/**/*.ts.

Also applies to: 62-63, 121-148, 188-193, 215-238

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/main.ts` around lines 24 - 27, Update src/app/main.ts to import and
use the shared pino logger for every listed console.log and console.error call,
including the display-environment failure and the ranges around lines 62-63,
121-148, 188-193, and 215-238. Preserve each message, severity, and control flow
while routing output through the logger used by command-server.ts; remove direct
console logging from this file.

Source: Coding guidelines

Comment thread src/app/main.ts
Comment on lines +62 to +63
process.on("uncaughtException", (err) => console.error("[App] uncaught:", err));
process.on("unhandledRejection", (r) => console.error("[App] unhandled:", r));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== files ==\n'
git ls-files | rg '(^|/)(main\.ts|.*toggle.*|.*cli.*|.*daemon.*|.*adr.*|.*doc.*|README\.md)$' || true

printf '\n== search terms ==\n'
rg -n --hidden --glob '!**/node_modules/**' --glob '!**/dist/**' --glob '!**/build/**' \
  'uncaughtException|unhandledRejection|hyprvox toggle|pidfile|pid file|socket|liveness|respawn|crash recovery|half-alive|half broken|boot\(\)|app.exit\(1\)' .

Repository: Snehit70/hyprvox

Length of output: 14879


🏁 Script executed:

...

Repository: Snehit70/hyprvox

Length of output: 195


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== main.ts slice ==\n'
if [ -f src/app/main.ts ]; then
  nl -ba src/app/main.ts | sed -n '1,320p'
fi

printf '\n== find toggle / crash recovery references ==\n'
rg -n --hidden --glob '!**/node_modules/**' --glob '!**/dist/**' --glob '!**/build/**' \
  'toggle|crash recovery|uncaughtException|unhandledRejection|pidfile|socket|respawn|liveness|boot\(' src README.md .github docs 2>/dev/null || true

Repository: Snehit70/hyprvox

Length of output: 215


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Focus on likely CLI / daemon files and the specific app file.
git ls-files | rg '^(src/.*|README\.md|docs/.*|.*ADR.*)$' | sed -n '1,200p'

printf '\n== direct references ==\n'
rg -n --hidden --glob '!**/node_modules/**' --glob '!**/dist/**' --glob '!**/build/**' \
  'toggle|pidfile|socket|respawn|liveness|daemon|boot\(' src README.md docs . 2>/dev/null || true

printf '\n== main.ts excerpt ==\n'
nl -ba src/app/main.ts | sed -n '50,80p;225,245p'

Repository: Snehit70/hyprvox

Length of output: 50374


Exit the main process on uncaught failures. Logging here is not enough: this app relies on the process dying so the next hyprvox toggle can lazily respawn it, and a half-broken instance can keep the pidfile/socket while serving nothing. Re-throw or call app.exit(1) after logging.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/main.ts` around lines 62 - 63, Update the uncaughtException and
unhandledRejection handlers in the process-level error handling to terminate the
main process with exit status 1 after logging, using app.exit(1) or equivalent;
preserve the existing error messages and ensure both handlers cannot leave a
broken instance running.

Comment thread src/app/main.ts
Comment on lines +161 to +174
function recreateOverlayWindow(): void {
if (mainWindow) {
mainWindow.destroy();
mainWindow = null;
}
setTimeout(() => {
if (!mainWindow) {
mainWindow = createOverlayWindow();
mainWindow.on("closed", () => {
mainWindow = null;
});
}
}, 2000);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Unbounded overlay-recreate loop, no retry cap or backoff.

If the overlay assets are broken (bad build, missing preload) every unresponsive/render-process-gone event retriggers recreateOverlayWindow(), which recreates the same broken window every 2s indefinitely.

🛠️ Suggested fix: cap retries with backoff
+let recreateAttempts = 0;
+const MAX_RECREATE_ATTEMPTS = 5;
+
 function recreateOverlayWindow(): void {
+	recreateAttempts += 1;
+	if (recreateAttempts > MAX_RECREATE_ATTEMPTS) {
+		console.error("[App] Overlay recreate limit exceeded, giving up");
+		return;
+	}
 	if (mainWindow) {
 		mainWindow.destroy();
 		mainWindow = null;
 	}
 	setTimeout(() => {
 		if (!mainWindow) {
 			mainWindow = createOverlayWindow();
+			recreateAttempts = 0;
 			mainWindow.on("closed", () => {
 				mainWindow = null;
 			});
 		}
-	}, 2000);
+	}, 2000 * recreateAttempts);
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function recreateOverlayWindow(): void {
if (mainWindow) {
mainWindow.destroy();
mainWindow = null;
}
setTimeout(() => {
if (!mainWindow) {
mainWindow = createOverlayWindow();
mainWindow.on("closed", () => {
mainWindow = null;
});
}
}, 2000);
}
let recreateAttempts = 0;
const MAX_RECREATE_ATTEMPTS = 5;
function recreateOverlayWindow(): void {
recreateAttempts += 1;
if (recreateAttempts > MAX_RECREATE_ATTEMPTS) {
console.error("[App] Overlay recreate limit exceeded, giving up");
return;
}
if (mainWindow) {
mainWindow.destroy();
mainWindow = null;
}
setTimeout(() => {
if (!mainWindow) {
mainWindow = createOverlayWindow();
recreateAttempts = 0;
mainWindow.on("closed", () => {
mainWindow = null;
});
}
}, 2000 * recreateAttempts);
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/main.ts` around lines 161 - 174, Update recreateOverlayWindow to
prevent indefinite recreation attempts when createOverlayWindow repeatedly
produces a broken overlay: track consecutive retry attempts, apply increasing
backoff delays, and stop scheduling retries after a defined maximum. Reset the
retry state only after a successfully usable overlay is established, while
preserving the existing destroy-and-recreate behavior.

Comment thread src/app/main.ts
Comment on lines +242 to +254
let stopping = false;
app.on("will-quit", (event: { preventDefault: () => void }) => {
if (stopping || (!service && !commandServer)) {
return;
}
stopping = true;
event.preventDefault();
Promise.resolve()
.then(() => service?.stop())
.then(() => commandServer?.stop())
.catch((err) => console.error("[App] Shutdown error:", err))
.finally(() => app.exit(0));
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

No timeout on graceful shutdown; a hung service.stop()/commandServer.stop() blocks termination.

event.preventDefault() blocks quit until this chain resolves. If either stop() call hangs, the process never calls app.exit(), and the stopping guard makes a repeated SIGTERM a no-op — the process becomes unkillable short of SIGKILL.

🛠️ Suggested fix: add a shutdown timeout fallback
 	stopping = true;
 	event.preventDefault();
-	Promise.resolve()
+	const shutdownTimeout = setTimeout(() => {
+		console.error("[App] Shutdown timed out, forcing exit");
+		app.exit(1);
+	}, 5000);
+	Promise.resolve()
 		.then(() => service?.stop())
 		.then(() => commandServer?.stop())
 		.catch((err) => console.error("[App] Shutdown error:", err))
-		.finally(() => app.exit(0));
+		.finally(() => {
+			clearTimeout(shutdownTimeout);
+			app.exit(0);
+		});
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let stopping = false;
app.on("will-quit", (event: { preventDefault: () => void }) => {
if (stopping || (!service && !commandServer)) {
return;
}
stopping = true;
event.preventDefault();
Promise.resolve()
.then(() => service?.stop())
.then(() => commandServer?.stop())
.catch((err) => console.error("[App] Shutdown error:", err))
.finally(() => app.exit(0));
});
let stopping = false;
app.on("will-quit", (event: { preventDefault: () => void }) => {
if (stopping || (!service && !commandServer)) {
return;
}
stopping = true;
event.preventDefault();
const shutdownTimeout = setTimeout(() => {
console.error("[App] Shutdown timed out, forcing exit");
app.exit(1);
}, 5000);
Promise.resolve()
.then(() => service?.stop())
.then(() => commandServer?.stop())
.catch((err) => console.error("[App] Shutdown error:", err))
.finally(() => {
clearTimeout(shutdownTimeout);
app.exit(0);
});
});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/main.ts` around lines 242 - 254, Update the will-quit shutdown flow
around the stopping guard to add a finite timeout fallback that calls
app.exit(0) even if service?.stop() or commandServer?.stop() never resolves.
Preserve the existing graceful stop sequence, error logging, and stopping guard,
while ensuring the timeout is cleared when shutdown completes normally to avoid
duplicate exit handling.

Comment thread src/cli/app-launcher.ts
Comment on lines +53 to +60
export function spawnAppDetached(): number | undefined {
assertAppLaunchable();
mkdirSync(logsDir, { recursive: true, mode: 0o700 });
const logFd = openSync(join(logsDir, "app.log"), "a");

const child = spawn(electronBinary, [appDir], {
detached: true,
stdio: ["ignore", logFd, logFd],

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -n -C3 'app\.log|pino|rotat' src package.json overlay/package.json

Repository: Snehit70/hyprvox

Length of output: 5463


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n## outline: src/utils/logger.ts\n'
ast-grep outline src/utils/logger.ts --view expanded || true

printf '\n## outline: src/cli/app-launcher.ts\n'
ast-grep outline src/cli/app-launcher.ts --view expanded || true

printf '\n## outline: src/cli/logs.ts\n'
ast-grep outline src/cli/logs.ts --view expanded || true

printf '\n## relevant slices\n'
sed -n '1,220p' src/utils/logger.ts
printf '\n--- app-launcher ---\n'
sed -n '1,180p' src/cli/app-launcher.ts
printf '\n--- logs ---\n'
sed -n '1,220p' src/cli/logs.ts

Repository: Snehit70/hyprvox

Length of output: 9889


Redirect detached app output away from app.log (src/cli/app-launcher.ts:56).
The app already writes to the daily-rotated hyprvox-YYYY-MM-DD.log; this extra app.log sink duplicates stdout/stderr and grows without bound. Point detached output at the rotated log path, or rotate app.log too.

🧰 Tools
🪛 ast-grep (0.44.1)

[warning] Importing child_process exposes a command-execution surface; ensure any command/argument built from input is validated, and prefer execFile/spawn with an argument array over exec.
Context: import { spawn } from "node:child_process";
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').

(detect-child-process-typescript)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/cli/app-launcher.ts` around lines 53 - 60, Update spawnAppDetached to
direct the detached child’s stdout and stderr to the existing daily-rotated
hyprvox-YYYY-MM-DD.log destination instead of opening and using app.log. Reuse
the established rotated-log path construction and preserve the existing detached
spawn behavior.

Source: Coding guidelines

Comment thread src/cli/app-launcher.ts
Comment on lines +58 to +64
const child = spawn(electronBinary, [appDir], {
detached: true,
stdio: ["ignore", logFd, logFd],
env: buildAppEnv(),
});
child.unref();
closeSync(logFd);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
ast-grep outline src/cli/app-launcher.ts \
  --match 'spawnAppDetached|spawnAppForeground' --view expanded
rg -n -C3 'child\.(on|once)\("error"|spawnApp(Detached|Foreground)\(' src/cli

Repository: Snehit70/hyprvox

Length of output: 2171


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '1,140p' src/cli/app-launcher.ts
printf '\n---\n'
sed -n '90,130p' src/cli/index.ts
printf '\n---\n'
sed -n '270,305p' src/cli/index.ts

Repository: Snehit70/hyprvox

Length of output: 4671


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
text = Path('src/cli/app-launcher.ts').read_text()
for name in ['spawnAppDetached', 'spawnAppForeground']:
    idx = text.find(f'export function {name}')
    print(f'\n{name}:')
    print(text[idx:text.find('\n}', idx)+2])
PY

Repository: Snehit70/hyprvox

Length of output: 801


Wait for child to emit "spawn" before printing success. spawn() reports launch failures asynchronously via "error", so the current try/catch can still print “App launched” even when Electron never starts. Make spawnAppDetached() and spawnAppForeground() return a promise that rejects on "error" and resolves only after "spawn".

🧰 Tools
🪛 ast-grep (0.44.1)

[warning] Importing child_process exposes a command-execution surface; ensure any command/argument built from input is validated, and prefer execFile/spawn with an argument array over exec.
Context: import { spawn } from "node:child_process";
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').

(detect-child-process-typescript)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/cli/app-launcher.ts` around lines 58 - 64, Update spawnAppDetached() and
spawnAppForeground() to return promises that listen for the child process
"error" and "spawn" events, rejecting on launch failure and resolving only after
successful spawning. Ensure the success message is printed only after awaiting
these promises, while preserving the existing detached/foreground process
behavior.

Comment thread src/cli/health.ts
Comment on lines +269 to 279
`${colors.yellow("⚠️")} Legacy systemd service is active — the app now supervises itself; remove it with ${colors.cyan("hyprvox uninstall")}`,
);
} else {
console.log(
`${colors.yellow("⚠️")} systemd service: ${colors.yellow(isServiceActive)}`,
`${colors.blue("ℹ️")} Legacy systemd service present but ${isServiceActive}`,
);
}
} catch (_e) {
console.log(
`${colors.blue("ℹ️")} systemd service not active or not installed`,
`${colors.blue("ℹ️")} No legacy systemd service (expected; app is launched via exec-once)`,
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set +e
output=$(systemctl --user is-active hyprvox 2>&1)
status=$?
printf 'exit=%s output=%s\n' "$status" "$output"

Repository: Snehit70/hyprvox

Length of output: 213


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show the relevant section of the cited file
wc -l src/cli/health.ts
sed -n '230,320p' src/cli/health.ts

# Find socket-path and systemd-related symbols/usages
rg -n "SOCKET_PATH|systemctl|is-active|legacy systemd|hyprvox uninstall|exec-once" src/cli/health.ts src -g '!**/dist/**' -g '!**/build/**'

Repository: Snehit70/hyprvox

Length of output: 6287


🏁 Script executed:

#!/bin/bash
set -euo pipefail

wc -l src/cli/health.ts
sed -n '230,320p' src/cli/health.ts
rg -n "SOCKET_PATH|systemctl|is-active|legacy systemd|hyprvox uninstall|exec-once" src/cli/health.ts src -g '!**/dist/**' -g '!**/build/**'

Repository: Snehit70/hyprvox

Length of output: 6287


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect imports and the overlay/socket check in the health command
sed -n '1,80p' src/cli/health.ts
sed -n '280,315p' src/cli/health.ts

# Inspect the shared socket path helper
sed -n '1,40p' src/utils/socket-path.ts

Repository: Snehit70/hyprvox

Length of output: 4238


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find where configDir comes from in the health command
rg -n "configDir" src/cli/health.ts src/cli src -g '!**/dist/**' -g '!**/build/**'

# Read the surrounding health.ts slice if configDir is local
sed -n '80,220p' src/cli/health.ts

Repository: Snehit70/hyprvox

Length of output: 7579


Handle inactive legacy units and custom socket paths

  • systemctl --user is-active hyprvox reports inactive through a nonzero exit, so the catch path treats an installed-but-stopped unit as missing. Check unit existence separately and only emit “no legacy systemd service” when it’s actually absent.
  • The overlay health check rebuilds join(configDir, "daemon.sock") instead of using SOCKET_PATH, so HYPRVOX_SOCKET_PATH instances can be reported as down even when their socket exists.
🧰 Tools
🪛 ast-grep (0.44.1)

[warning] Importing child_process exposes a command-execution surface; ensure any command/argument built from input is validated, and prefer execFile/spawn with an argument array over exec.
Context: import { execSync } from "node:child_process";
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').

(detect-child-process-typescript)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/cli/health.ts` around lines 269 - 279, Update the legacy systemd check in
the health command to distinguish an inactive existing unit from an absent unit:
check unit existence separately from is-active, and reserve the “No legacy
systemd service” message for units that are truly missing. Also update the
overlay socket health check to use the configured SOCKET_PATH value rather than
rebuilding daemon.sock from configDir, preserving support for
HYPRVOX_SOCKET_PATH.

Comment thread src/cli/health.ts
Comment on lines 290 to +297
const socketPath = join(configDir, "daemon.sock");
if (existsSync(socketPath)) {
console.log(
`${colors.green("✅")} IPC socket available for overlay connection`,
`${colors.green("✅")} App command socket present (overlay window hosted in-process)`,
);
} else {
console.log(
`${colors.yellow("⚠️")} IPC socket not found (daemon not running?)`,
`${colors.yellow("⚠️")} Command socket not found (app not running?)`,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Use the shared SOCKET_PATH contract.

Reconstructing the default path ignores HYPRVOX_SOCKET_PATH, causing health checks for test or remapped instances to report the app as stopped. Import and check SOCKET_PATH directly.

Proposed fix
-			const socketPath = join(configDir, "daemon.sock");
-			if (existsSync(socketPath)) {
+			if (existsSync(SOCKET_PATH)) {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const socketPath = join(configDir, "daemon.sock");
if (existsSync(socketPath)) {
console.log(
`${colors.green("✅")} IPC socket available for overlay connection`,
`${colors.green("✅")} App command socket present (overlay window hosted in-process)`,
);
} else {
console.log(
`${colors.yellow("⚠️")} IPC socket not found (daemon not running?)`,
`${colors.yellow("⚠️")} Command socket not found (app not running?)`,
if (existsSync(SOCKET_PATH)) {
console.log(
`${colors.green("✅")} App command socket present (overlay window hosted in-process)`,
);
} else {
console.log(
`${colors.yellow("⚠️")} Command socket not found (app not running?)`,
🧰 Tools
🪛 ast-grep (0.44.1)

[warning] Importing child_process exposes a command-execution surface; ensure any command/argument built from input is validated, and prefer execFile/spawn with an argument array over exec.
Context: import { execSync } from "node:child_process";
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').

(detect-child-process-typescript)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/cli/health.ts` around lines 290 - 297, Update the socket existence check
in the health-check flow to use the shared SOCKET_PATH contract instead of
rebuilding the path with join(configDir, "daemon.sock"). Import and check
SOCKET_PATH directly so HYPRVOX_SOCKET_PATH overrides are respected.

Comment thread src/cli/index.ts
Comment on lines +51 to +103
/** Pid from the pidfile if that process is alive; null otherwise. */
function readAlivePid(): number | null {
if (!existsSync(pidFile)) {
return null;
}
try {
const pid = parseInt(readFileSync(pidFile, "utf-8").trim(), 10);
if (Number.isNaN(pid)) {
return null;
}
process.kill(pid, 0);
return pid;
} catch {
return null;
}
}

if (options.supervisor && !process.env.HYPRVOX_DAEMON_WORKER) {
console.log(`${colors.cyan("Starting daemon with supervisor...")}`);
const supervisor = new DaemonSupervisor(join(projectRoot, "index.ts"));
supervisor.start();
} else {
console.log(`${colors.cyan("Starting daemon worker...")}`);
let service: DaemonService;
try {
service = new DaemonService();
service.start().catch((err) => {
console.error(colors.red("\nFailed to start daemon:"), err.message);
process.exit(1);
});
} catch (err: any) {
console.error(
colors.red("\nFailed to initialize daemon:"),
err.message,
);
process.exit(1);
}
function cleanStalePidFile(): void {
if (existsSync(pidFile) && readAlivePid() === null) {
console.log(
colors.yellow("Cleaning up stale PID file from previous session..."),
);
try {
unlinkSync(pidFile);
} catch {
// PID file may have already been removed
}
}
}

process.on("SIGINT", () => {
service.stop();
process.exit(0);
});
/** Wait for the app's DaemonService to write its pidfile after a spawn. */
async function waitForAlivePid(timeoutMs = 10000): Promise<number | null> {
const start = Date.now();
while (Date.now() - start < timeoutMs) {
const pid = readAlivePid();
if (pid !== null) {
return pid;
}
await new Promise((resolve) => setTimeout(resolve, 200));
}
return null;
}

process.on("SIGTERM", () => {
service.stop();
process.exit(0);
});
function startApp(foreground: boolean): void {
const runningPid = readAlivePid();
if (runningPid !== null) {
console.error(
colors.red(`Error: hyprvox is already running (PID: ${runningPid})`),
);
console.log(`To stop it, run: ${colors.cyan("hyprvox stop")}`);
process.exit(1);
}
cleanStalePidFile();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -n -C4 '\breadAlivePid\s*\(|\bprocess\.kill\s*\(' src/cli/index.ts
ast-grep outline src/app/main.ts --match boot --view expanded

Repository: Snehit70/hyprvox

Length of output: 3354


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== src/cli/index.ts outline ==\n'
ast-grep outline src/cli/index.ts --view expanded

printf '\n== identity/socket/token references ==\n'
rg -n -C 3 'socket|token|nonce|uuid|pidfile|SIGUSR1|SIGTERM|DaemonService|webContents.send|ipcMain|net\.createServer|createServer|connect\(' src -g '!**/dist/**' -g '!**/build/**'

printf '\n== likely daemon service files ==\n'
fd -a 'DaemonService.*|daemon.*|main.*|ipc.*|socket.*' src

Repository: Snehit70/hyprvox

Length of output: 35993


🏁 Script executed:

#!/bin/bash
set -euo pipefail

git ls-files 'src/app/**' 'src/cli/**' 'src/**' | sed -n '1,200p'

Repository: Snehit70/hyprvox

Length of output: 2162


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== pidfile writes in daemon service and launcher ==\n'
rg -n -C 3 'pidFile|daemon\.pid|writeFileSync\(.*pid|unlinkSync\(.*pid|process\.pid' src/daemon src/app src/cli

printf '\n== relevant sections of src/daemon/service.ts ==\n'
sed -n '1,240p' src/daemon/service.ts

printf '\n== relevant sections of src/cli/index.ts around start/restart/toggle ==\n'
sed -n '90,330p' src/cli/index.ts

Repository: Snehit70/hyprvox

Length of output: 24014


Validate the live instance before sending SIGTERM/SIGUSR1. src/cli/index.ts:52-60, 139-177, 280-308 A pidfile check only proves some process owns that PID; if it’s stale or reused, stop/restart/toggle can target the wrong process. Use the existing command socket, or another app-owned token, to confirm identity before acting.

🧰 Tools
🪛 ast-grep (0.44.1)

[warning] Importing child_process exposes a command-execution surface; ensure any command/argument built from input is validated, and prefer execFile/spawn with an argument array over exec.
Context: import { execSync } from "node:child_process";
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').

(detect-child-process-typescript)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/cli/index.ts` around lines 51 - 103, Update the stop, restart, and toggle
flows to verify the PID belongs to the running hyprvox instance before sending
SIGTERM or SIGUSR1; readAlivePid only confirms that some process owns the PID.
Reuse the existing command socket or app-owned identity token for this
validation, and abort safely when identity cannot be confirmed.

Comment thread src/cli/setup.ts
}

if (!askYesNoQuit("Install and start the systemd user service now?")) {
if (!askYesNoQuit("Show Hyprland autostart instructions now?")) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Update the remaining “service install” messages.

These changes redefine the flow as showing autostart instructions, but Lines 813, 824, and 833 still say “Skipping service install.” Update them to match the new --skip-service semantics.

Also applies to: 1004-1004

🧰 Tools
🪛 ast-grep (0.44.1)

[warning] Importing child_process exposes a command-execution surface; ensure any command/argument built from input is validated, and prefer execFile/spawn with an argument array over exec.
Context: import { execFileSync } from "node:child_process";
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').

(detect-child-process-typescript)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/cli/setup.ts` at line 850, Update the remaining messages near the
Hyprland autostart prompt and the corresponding later location to describe
skipping autostart instructions under the --skip-service flow, replacing the
outdated “Skipping service install” wording while preserving the surrounding
control flow.

@Snehit70
Snehit70 merged commit 6568e9c into main Jul 15, 2026
2 checks passed
@Snehit70
Snehit70 deleted the feat/single-app-spike branch July 15, 2026 09:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant