Skip to content

Feat/electron softphone - #1469

Open
dlohvinov wants to merge 10 commits into
mainfrom
feat/electron-softphone
Open

Feat/electron softphone#1469
dlohvinov wants to merge 10 commits into
mainfrom
feat/electron-softphone

Conversation

@dlohvinov

Copy link
Copy Markdown
Contributor

No description provided.

dlohvinov and others added 10 commits July 30, 2026 20:39
Shared native dependency for electron-workspace and the upcoming
electron-softphone package. file: links outside a package root do not
get their transitive deps installed by npm, so electron-sip now carries
its own node_modules (CI installs them in a dedicated step).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Tray-only Electron app that is the native SIP endpoint (pjsip via
electron-sip) while the web workspace keeps the call-control UI.
Hosts a loopback WebSocket control channel (default 127.0.0.1:10029):
the web app delivers its access token via hello and sends answer/call
commands; everything else stays on the web app's own socket. Runs its
own webitel-sdk Client in the main process (registerWebDevice: false,
SIP creds from user_default_device) with reconnect and duplicate-answer
guards.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
When CLI.externalSoftphone.enabled, the browser stops registering the
web SIP device (no mic prompt) and attaches a RemotePhone instead: a
SipClient stub (ExternalClient pattern) that proxies answer/call to the
local webitel-softphone utility over ws://127.0.0.1:<port>. Registration
state from the utility feeds the existing phone_registered/isPhoneReg
flow; un-answered calls get stub sessions on (re)attach so they stay
answerable after reconnects. Hold/mute/DTMF/transfer/bridge are already
websocket actions and stay unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
electron-builder config mirrors electron-workspace (asar off for the
native addon, hardened+notarized mac build with microphone usage
description); manual build-electron-softphone workflow reuses the
committed electron-sip prebuilds.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- serialize softphone lifecycle (restart/reconnect/destroy) through a
  promise chain so overlapping starts cannot clobber client/adapter
- treat early 'unregistered' during REGISTERING as register_failed with
  reconnect (the addon resolves register() before the registrar answers)
  and schedule a fallback reconnect on lost registration
- attach the phone before subscribeCall so snapshot calls get sip
  sessions; listen to sdk 'error' (Call.answer swallows SIP failures)
  and check answer()'s boolean result
- graceful quit: tray uses app.quit(), before-quit awaits teardown;
  catch startup failures of whenReady
- web: destroyClient() clears the external-softphone client reference
  so utility state can't re-attach onto a destroyed client; sessions
  created from sip_id never leak that id as callId
- rotate softphone.log at 5MB; fix stale prebuilds path in install.js

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… found in first live smoke

ESM + typed sources (tsconfig, typecheck script, electron-sip .d.ts,
typed protocol/state/events); no require/module.exports left.

Bugs found and fixed while smoke-testing the built app for the first
time:
- SoftphoneTray no longer extends Tray: Electron's Tray constructor
  returns a native wrapper that drops the subclass prototype, so
  subclass methods didn't exist at runtime — the throw inside the ws
  connection event silently killed message delivery. Composition now.
- every SDK setup step (connect/auth/subscribeCall/deviceConfig) is
  raced against a timeout: in the Electron main process the global
  WebSocket fires onerror but never onclose on unreachable endpoints,
  and the SDK only wires onclose, leaving connect() pending forever.
- prevent-app-suspension power-save blocker: macOS App-Naps a tray-only
  app with a hidden dock, freezing SIP and the local server.
- uncaughtException/unhandledRejection are logged (no window exists for
  Electron's default error dialog).
- answer uses the SDK's actual field ({audio:true}; useAudio was never
  read by the SDK).

Verified live: hello → connecting → connect timeout → error state
broadcast + ack → reconnect loop, all over the loopback socket against
the built app.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
electron 35→43, typescript 5.9→7, @types/node 24→26, webitel-sdk
26.1→26.4, ws 8.18→8.21, electron-builder 26.0→26.15. electron-vite
stays on 6.0.0-beta.1 — it is newer than the latest stable (5.0.0) and
matches electron-workspace. Electron 43's Node also fixes the
onclose-never-fires WebSocket quirk (SDK connect now rejects with 1006
instantly; the setup timeouts remain as a safety net).

Verified: typecheck (TS 7), build, live smoke under Electron 43
(hello → error state → reconnect loop), N-API addon loads, packaging
dry run + packaged addon load.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
When the last workspace connection drops, a linger timer
(workspaceLingerSec, default 30s) suspends the session — SIP
unregister + SDK socket close — so calls stop routing to an unattended
device; the tray goes back to 'Waiting for workspace'. A page reload
reconnects well within the window, so the registration doesn't flap.
An active call postpones the suspend until it ends; the next hello
brings the session back up. Standalone dev runs (config.dev.json) are
exempt — they have no workspace connections by design.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The icon was copied from electron-workspace where it is a call-popup
asset (256x256), not a tray icon — it rendered huge in the macOS menu
bar. Now matches the other 32x32 tray state icons.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…port cycle

The endpoint const moves to websocket/endpoint.ts. The cycle was
runtime-safe (all cross-references were lazy), but one future top-level
access to endpoint from the softphone module would have hit the TDZ.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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