Skip to content

feat: platform abstraction layer (Windows foundation)#15

Closed
elicep01 wants to merge 50 commits intomainfrom
feat/windows-foundation
Closed

feat: platform abstraction layer (Windows foundation)#15
elicep01 wants to merge 50 commits intomainfrom
feat/windows-foundation

Conversation

@elicep01
Copy link
Collaborator

What this does

Introduces src/main/platform/ — a clean separation between macOS-specific native code and the rest of the app. This is the foundation for Windows support.

Files added

File Purpose
platform/interface.ts TypeScript contract every platform must satisfy
platform/darwin.ts macOS implementation (self-contained, mirrors logic from main.ts)
platform/windows.ts Windows stubs — every method returns a safe value so the app runs without crashing
platform/index.ts Exports platform — picks darwin on macOS, windows on win32

What was migrated out of main.ts

Feature Before After
Microphone status inline + platform guard platform.readMicrophoneAccessStatus()
Microphone native helper inline + platform guard platform.requestMicrophoneAccessViaNative()
Audio duration probe inline afinfo call platform.probeAudioDurationMs()
TTS backend resolution inline + platform guard platform.resolveSpeakBackend()
Color picker IPC inline execFile block platform.pickColor()
Snippet expander spawn inline binary + compile logic platform.spawnSnippetExpander()
Hotkey hold monitor spawn inline binary + compile logic platform.spawnHotkeyHoldMonitor()

Duplicate type declarations (MicrophoneAccessStatus, MicrophonePermissionResult, LocalSpeakBackend) removed from main.ts and imported from platform instead.

Safety

  • Zero behaviour change on macOSdarwin.ts contains identical logic
  • TypeScript compiles clean with no errors
  • Windows stubs never crash — each returns null, false, or 'granted' as appropriate with a comment pointing to the follow-up PR that will implement it

What's next (follow-up PRs on this branch)

  • Windows hotkey hold monitor (Win32 SetWindowsHookEx)
  • Windows snippet expander (Win32 keyboard hook)
  • Windows color picker (Win32 ChooseColor or JS fallback)
  • electron-builder win target + .ico icon
  • CI job for Windows build

Introduces src/main/platform/ — a thin interface layer that separates
macOS-specific native code from the rest of the app.

- interface.ts  defines PlatformCapabilities (microphone, audio probe,
                TTS backend, hotkey monitor, snippet expander, color picker)
- darwin.ts     macOS implementation, self-contained (mirrors logic currently
                inline in main.ts so main.ts can migrate incrementally)
- windows.ts    Windows stubs — every method returns a safe value so the app
                runs without crashing; real implementations follow in later PRs
- index.ts      exports `platform` (darwin on macOS, windows on win32)

main.ts is untouched — no behaviour changes on macOS.
Replaces inline macOS-guarded implementations in main.ts with calls to
the platform layer (platform/darwin.ts on macOS, platform/windows.ts stubs
on Windows):

- readMicrophoneAccessStatus()  → platform.readMicrophoneAccessStatus()
- probeAudioDurationMs()        → platform.probeAudioDurationMs()
- resolveLocalSpeakBackend()    → platform.resolveSpeakBackend()
- native-pick-color IPC handler → platform.pickColor()

Types MicrophoneAccessStatus, MicrophonePermissionResult, LocalSpeakBackend
are now imported from platform/interface.ts instead of being redeclared.

No behaviour change on macOS. On Windows the app no longer crashes on
these code paths — each returns a safe stub value.
…m layer

- refreshSnippetExpander: binary resolution + spawn moved to platform.spawnSnippetExpander()
- startWhisperHoldWatcher: removes ensureWhisperHoldWatcherBinary(), uses platform.spawnHotkeyHoldMonitor()
- startFnSpeakToggleWatcher: same — uses platform.spawnHotkeyHoldMonitor()
- HotkeyModifiers: added fn field to match Swift binary arg order (keyCode cmd ctrl alt shift fn)
- darwin.ts: folds in compile-on-demand logic from ensureWhisperHoldWatcherBinary

Event handlers remain in main.ts since they reference main.ts-local state.
No behaviour change on macOS. On Windows both features return null and are skipped gracefully.
- requestMicrophoneAccessViaNative() → platform.requestMicrophoneAccessViaNative()
- ensureMicrophoneAccess(): remove process.platform guard — readMicrophoneAccessStatus()
  already returns 'granted' on Windows via the platform layer, so the guard
  was redundant

All platform-specific native code in main.ts is now behind platform.*
…build script

- supercmd.ico: Windows icon with 6 sizes (16/32/48/64/128/256px)
- scripts/build-native.js: replaces the inline swiftc chain in package.json;
  compiles Swift helpers on macOS and exits cleanly (no-op) on Windows
- package.json: build:native now calls the script; adds electron-builder
  win target (nsis installer, x64 + arm64) and nsis config
- test job: runs unit tests on ubuntu-latest (same as feat/add-test-coverage)
- build-windows job: runs on windows-latest, compiles TypeScript + Vite,
  skips Swift (build:native is a no-op on Windows), then does an unsigned
  electron-builder dir build to confirm the app packages without errors

Triggers on push/PR to main and feat/windows-foundation.
- Add vitest@^4.0.0 to devDependencies and a `test` script so `npm test`
  works in the Tests CI job
- Regenerate package-lock.json so `npm ci` can install vitest in CI
- Create vitest.config.ts (node environment, src/main/__tests__ glob)
- Exclude __tests__ / *.test.ts files from tsconfig.main.json so the
  Windows build job no longer fails with TS2307 (cannot find 'vitest')
- Guard icons.test.ts with describe.runIf(darwin) — iconutil is
  macOS-only and would error on the Linux test runner
Setting WIN_CSC_LINK to an empty string causes electron-builder to resolve
it as a relative path (→ cwd), which fails with "not a file". Leave it
unset — CSC_IDENTITY_AUTO_DISCOVERY=false is sufficient to skip signing
when no certificate is available.
Color picker
- Implements platform.pickColor() using a small Electron BrowserWindow
  with an inlined <input type="color"> UI. Returns the hex string on OK,
  null on Cancel/Escape. No native binary required.

Hotkey hold monitor
- Adds src/native/hotkey-hold-monitor.c — a Win32 WH_KEYBOARD_LL hook
  that follows the exact same JSON-over-stdout protocol as the macOS
  Swift binary ({"ready"}, {"pressed"}, {"released"}).
- Maps macOS CGKeyCodes (used by parseHoldShortcutConfig) to Windows VKs.
  Command and Fn args are accepted but ignored (no Win32 equivalent).
- Updates scripts/build-native.js to compile it with gcc (MinGW) on
  Windows; existing macOS Swift compilation is unchanged.
- Wires platform/windows.ts to spawn hotkey-hold-monitor.exe from
  dist/native/, falling back gracefully if the binary is absent.

CI artifact
- Uploads out/win-unpacked/ as "SuperCmd-win-x64-portable" (14-day
  retention) so testers can download, extract, and run SuperCmd.exe
  directly from any passing CI run without a full NSIS installer.
- Updates the stale comment that said build:native was a no-op on Windows.
Three issues blocked running SuperCmd on Windows when launched from VS Code
(or any Electron-based IDE):

1. ELECTRON_RUN_AS_NODE=1 is inherited from the VS Code parent process,
   causing Electron to run as plain Node.js instead of an Electron app.
   This made `require('electron')` resolve to the npm package path string
   instead of the Electron API, crashing at app.getVersion(). Fix: add
   scripts/launch-electron.js which explicitly deletes ELECTRON_RUN_AS_NODE
   from the env before spawning the Electron binary.

2. Alt+Space is reserved by Windows (opens the window system menu) and
   cannot be registered as a global shortcut. Fix: use Ctrl+Space as the
   default on Windows, with an automatic migration for existing settings
   that saved Alt+Space.

3. build:native crashed with a non-zero exit code when gcc/clang/cl were
   not in PATH, blocking `npm run build`. Fix: probe for available compilers
   and warn+skip instead of crashing; the app still runs, the hold-hotkey
   feature is gracefully disabled.
- Step 0: show Windows-appropriate 'what gets configured' list
- Step 2: hide macOS 'Replace Spotlight' section; show Ctrl instead of Cmd
- Step 3: replace 4 macOS permission rows with simplified Windows screen
  (Windows manages Accessibility/Input Monitoring automatically;
  only Microphone is noted as needed for Whisper)
- Step 4: show Windows-specific hint about Fn key and edge-tts
- Step 5: show Ctrl+Shift+S keycap instead of ⌘Cmd+Shift+S
- Fix default shortcut fallback to Ctrl+Space on win32
- Add 'platform' and 'homeDir' to ElectronAPI type definition
- Remove forced migration of Alt+Space → Ctrl+Space in settings-store.ts;
  fresh installs still default to Ctrl+Space via DEFAULT_GLOBAL_SHORTCUT
- Replace macOS Spotlight section in onboarding Step 2 with a PowerToys-aware
  hint explaining how to use Alt+Space when PowerToys Run is disabled first
Reduce ONBOARDING_WINDOW_WIDTH from 1120→900 and ONBOARDING_WINDOW_HEIGHT
from 740→600. Position the window vertically centered (workArea height / 2)
instead of at the fixed topFactor offset near the top.
900px width broke the lg: Tailwind breakpoints causing all two-column
layouts to collapse vertically. Keep width at 1120 (above 1024px lg
threshold) and reduce height from 740 to 680 for a shorter window that
still fits all content. Vertical centering from previous commit retained.
…tion

- Raise text opacity from white/55 to white/75 so the hint is readable
- Explain that Windows routes Alt+Space to PowerToys/system before
  SuperCmd can capture it, so users must disable PowerToys Run first
- Highlight the action step in white/90 so the fix is immediately clear
@elicep01 elicep01 force-pushed the feat/windows-foundation branch from 7837ef3 to b67e367 Compare February 18, 2026 14:39
elicep01 and others added 14 commits February 18, 2026 08:46
- Step 3 onboarding: add Request Access button that triggers the real
  Windows microphone permission dialog via getUserMedia, with granted/
  error feedback instead of passive static text
- Default hold-to-talk key changed from Fn to Ctrl+Shift+Space on
  Windows — Fn is handled by keyboard firmware and never reaches Win32
- Step 4 hint updated to explain why Fn is unsupported and what to use
The default speechToTextModel was 'native' on all platforms. On Windows
there is no native Swift speech recognizer, so the whisper-transcribe
IPC handler silently returned an empty string — the UI appeared but
nothing was transcribed.

- settings-store: default speechToTextModel to '' on Windows so fresh
  installs fall through to OpenAI (gpt-4o-transcribe) automatically
- main: when sttModel === 'native' on Windows, redirect to OpenAI
  instead of returning '' — gives a proper 'API key not configured'
  error rather than silent failure for users with saved native setting

Users need an OpenAI API key set in Settings -> AI for dictation.
…oard

On Windows osascript is unavailable so getSelectedTextForSpeak always
returned empty string, causing startSpeakFromSelection to return false
and Read mode to never open.

Add a Windows-specific path that simulates Ctrl+C on the foreground
window (which still has focus because speak mode runs showWindow:false),
polls the clipboard until it changes, captures the selection, then
restores the previous clipboard — same pattern as the macOS fallback
but using PowerShell SendKeys instead of osascript.
Text selection:
- Replace Ctrl+C (SendKeys) with UIAutomation as primary method — reads
  selected text directly from the focused element without sending any
  keystrokes to the user's app, like macOS AXSelectedText
- Keep Ctrl+C only as fallback for apps that don't expose TextPattern
- Add windowsHide:true to all PowerShell spawns to prevent focus theft

Audio playback (ENOENT fix):
- /usr/bin/afplay does not exist on Windows; replace with PowerShell
  -STA + System.Windows.Media.MediaPlayer which handles MP3 natively
- Pass audio path via SUPERCMD_AUDIO_PATH env var to avoid escaping
- Applied to both the main playAudioFile loop and speak-preview-voice
…indow is focused

When the onboarding window (or any SuperCmd BrowserWindow) has focus,
UIAutomation and Ctrl+C cannot read text selected inside the Chromium
renderer. Add a first-priority check that calls executeJavaScript to
get window.getSelection().toString() directly from the focused window,
falling through to UIAutomation/clipboard only for external apps.
…s blur

backdrop-filter compositing on Windows causes a visible fade-in delay
and the semi-transparent rgba values (0.74–0.80) look far more
see-through than on macOS where vibrancy provides the backing material.

- Stamp data-platform attribute on <body> at startup
- CSS: [data-platform="win32"] overrides .glass-effect and
  .cursor-prompt-surface to rgba(12,12,18,0.97) with no backdrop-filter
- OnboardingExtension: Windows uses fully opaque gradient wrapper and
  opaque contentBackground base (no "transparent" bleed-through)
- App: actions dropdown and context menu inline styles conditioned on
  isWindows — remove backdropFilter, lift background to 0.99 opacity
…dows

On Windows there is no SFSpeechRecognizer (macOS-only Swift binary).
Rather than falling back to the native binary path (which silently fails),
introduce a 'webspeech' backend that uses Chromium's built-in Web Speech
API — the same engine Chrome uses, requiring no API key.

- resolveSessionConfig picks 'webspeech' on win32 when no cloud key is set
- startListening: new webspeech branch creates a SpeechRecognition instance
  with continuous=true, interimResults=true; accumulates onresult events
  into combinedTranscriptRef (same store used by the native path)
- finalizeAndClose: webspeech treated as isNativeBackend; stop() called on
  release, 250 ms drain for trailing callbacks, then text is pasted
- forceStopCapture: abort() the recognition if still active
- Network error shown with a helpful fallback message (configure OpenAI key)
… onboarding hotkey

- build-native.js: add findMsvcCl() to search VS 2017/2019/2022 install
  locations for cl.exe when it is not on PATH; add makeMsvcCompiler() to
  build a compiler entry with the correct MSVC include/lib paths and
  Windows SDK headers so hotkey-hold-monitor.exe compiles without a
  Developer Command Prompt.  Path calculation fixed to go exactly 3 levels
  up from the x64 bin dir to reach the MSVC version root.

- OnboardingExtension.tsx: show 'Ctrl+Shift+Space' as the default
  hold-to-talk hotkey on Windows instead of 'Fn' (which is a firmware key
  on Windows and cannot be registered as a global shortcut).

- .gitignore: add *.obj to suppress MSVC intermediate object files.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
On Windows, openPermissionTarget was always trying to open
x-apple.systempreferences:... URLs after requesting microphone access,
causing a Windows dialog: 'How do you want to open this?'

- Skip all x-apple.systempreferences URLs on Windows entirely
- On Windows, only open ms-settings:privacy-microphone when access is
  explicitly denied/restricted (so the user can unblock it in Settings)
- Fix error note messages to say 'Settings → Privacy & Security →
  Microphone' instead of 'System Settings' on Windows

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… paths

Root causes of broken Windows dictation:
1. hotkey-hold-monitor.exe path wrong: windows.ts compiles to
   dist/main/platform/ so __dirname/../native = dist/main/native (not found).
   Fixed to go up two levels so the path resolves to dist/native/.

2. Web Speech API ('webspeech' backend) fails with 'network' error in
   Electron because Electron builds do not include Google's API keys that
   Chrome embeds. Switched to the 'native' backend on all platforms.

3. Windows has no native speech binary. Added speech-recognizer.cs — a
   C# console app using System.Speech.Recognition (built into .NET
   Framework 4.x, available on every Windows 10/11 machine, works fully
   offline with no API key). Protocol: emits {"ready":true},
   {"transcript":"...","isFinal":true|false}, {"error":"..."} JSON lines
   to stdout, same as the macOS Swift binary.

4. whisper-start-native in main.ts used 'speech-recognizer' (no .exe) on
   all platforms; Windows needs 'speech-recognizer.exe'. Added platform
   check. On-demand compile fallback now uses csc.exe + the WPF-subfolder
   System.Speech.dll path on Windows.

build-native.js changes:
- Add C# compilation step using C:\Windows\Microsoft.NET\Framework64\
  v4.0.30319\csc.exe with System.Speech.dll from the WPF subfolder.
- Both hotkey-hold-monitor.exe and speech-recognizer.exe are now built
  by `npm run build:native`.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- index.html: add <meta name="color-scheme" content="dark"> so Chromium
  renders all native form elements (select dropdowns, scrollbars, inputs)
  in dark mode regardless of the Windows system theme.

- main.ts: set nativeTheme.themeSource = 'dark' so Electron's own chrome
  and any OS-level dialogs also use dark styling.

- main.ts: call app.setAppUserModelId('com.supercmd.app') on Windows so
  all SuperCmd windows are grouped under one taskbar entry with the
  correct app name.

- main.ts: set icon: path.join(__dirname, '../../supercmd.ico') on all
  three BrowserWindows (main launcher, Settings, Extension Store) on
  Windows so the taskbar and alt-tab switcher show the SuperCmd icon
  instead of the default Electron icon.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Set color-scheme: dark directly on select elements so Chromium renders
the OS-level popup list in dark mode on Windows.  Also make all select
backgrounds fully opaque (replacing semi-transparent rgba values that
composite to white against the transparent window).  Covers both the
Settings tab selects and the SuperCmd Read widget voice/speed selects.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
elicep01 and others added 13 commits February 18, 2026 23:33
- discoverWindowsApplications(): scans Start Menu .lnk shortcuts via a
  single PowerShell invocation (WScript.Shell COM), resolves each to its
  .exe target path, deduplicates by path, filters uninstallers.
  Populates the 'app' category in the launcher.

- extractWindowsIcons(): batch-extracts .exe icons via PowerShell +
  System.Drawing.Icon.ExtractAssociatedIcon; results cached to disk so
  subsequent launches are instant.  Runs in batches of 20.

- discoverWindowsSystemSettings(): returns a static curated list of 37
  Windows Settings panels (ms-settings: URIs) with keywords and emoji
  icons.  Populates the 'settings' category in the launcher.

- openAppByPath(): uses Electron shell.openPath() on Windows (ShellExecute).
- openSettingsPane(): uses shell.openExternal(ms-settings:...) on Windows.
- discoverAndBuildCommands(): guards NSWorkspace block behind !win32 and
  runs Windows icon extraction instead.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Rewrites filterCommands() with a new scoreWord() engine that scores
queries across 8 tiers: exact → prefix → word-boundary substring →
substring → acronym-exact → acronym-prefix → acronym-contains →
fuzzy subsequence (consecutive/word-start bonuses).

Multi-word queries now split on whitespace and require every word to
match somewhere in title or keywords (e.g. "dark mode" finds
"Toggle Dark / Light Mode").

Also adds proper Lucide icons for the 7 new PowerToys-parity system
commands in getSystemCommandFallbackIcon.

Verified working: global hotkey (Ctrl+Space), fuzzy search across
all 169+ commands on Windows.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Fix context menu mousedown handler to only close when clicking
  outside the popup (was firing before onClick, preventing actions)
- Replace all e.metaKey checks with cmdOrCtrl (metaKey || ctrlKey on
  Windows) so Ctrl+K, Ctrl+Shift+P, Ctrl+Shift+D, Ctrl+Alt+Up/Down
  all work correctly on Windows
- Update shortcut labels in context menu to show Ctrl on Windows
  instead of Cmd

Verified: pin/unpin via right-click and Ctrl+Shift+P both working.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…s can be re-enabled

Added 'Applications' and 'System Settings' groups to the left panel in
Settings → Extensions. Previously only 'system', 'extension', and 'script'
category commands were listed, making it impossible to re-enable Windows
apps or Settings panels that had been disabled from the launcher.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…tc.)

Previously only Start Menu .lnk → .exe shortcuts were indexed, missing all
UWP/packaged apps. Now a second PowerShell pass via Get-StartApps finds every
app whose AppID contains '!' (PackageFamilyName!AppId), adds them as 'app'
category commands with a shell:AppsFolder\{AUMID} path, and deduplicates
against the traditional apps already found.

openAppByPath() now routes shell: URIs through shell.openExternal() so UWP
apps (Settings, Calculator, Store, Xbox, Mail, etc.) launch correctly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ows ✅

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The backend (applyOpenAtLogin, set-open-at-login IPC, setOpenAtLogin bridge)
was already fully implemented but the UI toggle was never added to GeneralTab.
Added a checkbox card that calls window.electron.setOpenAtLogin() and updates
local state immediately.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two bugs:
1. The .lnk scan was picking up Calculator and other UWP apps as
   C:\Program Files\WindowsApps\...\*.exe paths — these pass Test-Path but
   fail to open because the WindowsApps folder is access-restricted. Added
   `-notmatch 'WindowsApps'` to exclude them; Get-StartApps then picks
   them up with the correct shell:AppsFolder\{AUMID} path.

2. shell.openExternal() is unreliable for shell: URIs on Windows. Replaced
   with PowerShell Start-Process which handles shell:AppsFolder\ URIs natively.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@bircni
Copy link

bircni commented Feb 20, 2026

will this be continued?

CUrrently a file is missing...

@bircni
Copy link

bircni commented Feb 21, 2026

Installing extesnsion does not work as it searches git with brew

@elicep01 elicep01 closed this Mar 2, 2026
@elicep01 elicep01 deleted the feat/windows-foundation branch March 2, 2026 22:42
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.

2 participants