@coventia/softphone 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/dist/{call-CaPDWVk3.d.cts → call-FqcvDg1t.d.cts} +168 -12
  2. package/dist/{call-CaPDWVk3.d.ts → call-FqcvDg1t.d.ts} +168 -12
  3. package/dist/chunk-5E5OEXS2.js +14 -0
  4. package/dist/chunk-5E5OEXS2.js.map +1 -0
  5. package/dist/{chunk-EYEDPD7Y.js → chunk-6RR6G35E.js} +92 -17
  6. package/dist/chunk-6RR6G35E.js.map +1 -0
  7. package/dist/{chunk-2ZSOGGPO.js → chunk-FRXHEHQA.js} +15 -2
  8. package/dist/chunk-FRXHEHQA.js.map +1 -0
  9. package/dist/{chunk-UHNLEBBO.js → chunk-I3RSC52R.js} +28 -3
  10. package/dist/chunk-I3RSC52R.js.map +1 -0
  11. package/dist/chunk-M76SAXXM.js +13 -0
  12. package/dist/chunk-M76SAXXM.js.map +1 -0
  13. package/dist/{chunk-7R6LOOAI.js → chunk-N4PLEYFJ.js} +4 -2
  14. package/dist/chunk-N4PLEYFJ.js.map +1 -0
  15. package/dist/element/index.d.ts +198 -6
  16. package/dist/element/index.js +1160 -489
  17. package/dist/element/index.js.map +1 -1
  18. package/dist/{engine-BgBsIcEa.d.ts → engine-CsNRD7B7.d.ts} +1 -1
  19. package/dist/{engine-CU2rEo3g.d.cts → engine-CzOCrfY9.d.cts} +1 -1
  20. package/dist/index.cjs +675 -53
  21. package/dist/index.cjs.map +1 -1
  22. package/dist/index.d.cts +16 -6
  23. package/dist/index.d.ts +16 -6
  24. package/dist/index.js +453 -37
  25. package/dist/index.js.map +1 -1
  26. package/dist/react/index.cjs +26 -0
  27. package/dist/react/index.cjs.map +1 -1
  28. package/dist/react/index.d.cts +24 -5
  29. package/dist/react/index.d.ts +24 -5
  30. package/dist/react/index.js +2 -2
  31. package/dist/{softphone-DEt0TtQw.d.ts → softphone-By9sizlV.d.ts} +44 -3
  32. package/dist/{softphone-dZMkW8V1.d.cts → softphone-JZvJdzU3.d.cts} +44 -3
  33. package/dist/softphone.css +254 -23
  34. package/dist/{sources-M3GYMS6V.js → sources-QL2LRZVH.js} +93 -8
  35. package/dist/sources-QL2LRZVH.js.map +1 -0
  36. package/dist/testing/index.cjs +13 -0
  37. package/dist/testing/index.cjs.map +1 -1
  38. package/dist/testing/index.d.cts +2 -2
  39. package/dist/testing/index.d.ts +2 -2
  40. package/dist/testing/index.js +1 -1
  41. package/dist/ui/index.cjs +670 -20
  42. package/dist/ui/index.cjs.map +1 -1
  43. package/dist/ui/index.d.cts +2 -2
  44. package/dist/ui/index.d.ts +2 -2
  45. package/dist/ui/index.js +642 -22
  46. package/dist/ui/index.js.map +1 -1
  47. package/dist/zadarma/index.cjs +111 -14
  48. package/dist/zadarma/index.cjs.map +1 -1
  49. package/dist/zadarma/index.d.cts +5 -6
  50. package/dist/zadarma/index.d.ts +5 -6
  51. package/dist/zadarma/index.js +3 -2
  52. package/dist/zadarma-engine-TIUE7PZ7.js +6 -0
  53. package/dist/{zadarma-engine-L5DEFXUS.js.map → zadarma-engine-TIUE7PZ7.js.map} +1 -1
  54. package/package.json +1 -1
  55. package/dist/chunk-2ZSOGGPO.js.map +0 -1
  56. package/dist/chunk-7R6LOOAI.js.map +0 -1
  57. package/dist/chunk-EYEDPD7Y.js.map +0 -1
  58. package/dist/chunk-UHNLEBBO.js.map +0 -1
  59. package/dist/sources-M3GYMS6V.js.map +0 -1
  60. package/dist/zadarma-engine-L5DEFXUS.js +0 -5
@@ -44,6 +44,31 @@ function useIsLeader() {
44
44
  useCallback(() => phone.isLeader, [phone])
45
45
  );
46
46
  }
47
+ function useLine() {
48
+ const phone = useSoftphone();
49
+ const cache = useRef(null);
50
+ return useSyncExternalStore(
51
+ useCallback(
52
+ (cb) => {
53
+ const offLine = phone.on("lineChanged" /* LineChanged */, () => cb());
54
+ const offClaim = phone.on("lineClaimChanged" /* LineClaimChanged */, () => cb());
55
+ return () => {
56
+ offLine();
57
+ offClaim();
58
+ };
59
+ },
60
+ [phone]
61
+ ),
62
+ useCallback(() => {
63
+ const { line, lineWhere: where, lineClaim: claim } = phone;
64
+ const cached = cache.current;
65
+ if (cached?.line === line && cached.where === where && cached.claim === claim) return cached;
66
+ const view = { line, where, claim };
67
+ cache.current = view;
68
+ return view;
69
+ }, [phone])
70
+ );
71
+ }
47
72
  function useActiveCall() {
48
73
  const phone = useSoftphone();
49
74
  return useSyncExternalStore(
@@ -140,6 +165,6 @@ function useDevices() {
140
165
  return useSyncExternalStore(subscribe, getSnapshot);
141
166
  }
142
167
 
143
- export { SoftphoneProvider, useActiveCall, useCall, useDevices, useIsLeader, useLastError, usePresence, useSoftphone, useSoftphoneState };
144
- //# sourceMappingURL=chunk-UHNLEBBO.js.map
145
- //# sourceMappingURL=chunk-UHNLEBBO.js.map
168
+ export { SoftphoneProvider, useActiveCall, useCall, useDevices, useIsLeader, useLastError, useLine, usePresence, useSoftphone, useSoftphoneState };
169
+ //# sourceMappingURL=chunk-I3RSC52R.js.map
170
+ //# sourceMappingURL=chunk-I3RSC52R.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/react/context.tsx","../src/react/hooks.ts"],"names":[],"mappings":"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","file":"chunk-I3RSC52R.js","sourcesContent":["import { createContext, useContext, type ReactNode } from 'react';\r\nimport type { Softphone } from '../core/softphone.js';\r\n\r\n/**\r\n * The provider takes an INSTANCE the host constructed — not config props.\r\n *\r\n * This is LiveKit's hard-won lesson (SPEC §5.1): they are deprecating their config-props\r\n * `<LiveKitRoom>` in favour of a provider that accepts an instance. Holding the `Softphone` above the\r\n * React tree lets the app own the connection, survive route changes and unmounts without dropping a\r\n * call, and unit-test the engine with no React at all. Config-props would rebuild the engine on\r\n * every prop change — catastrophic for something holding a live `RTCPeerConnection`.\r\n */\r\nconst SoftphoneContext = createContext<Softphone | null>(null);\r\n\r\nexport interface SoftphoneProviderProps {\r\n readonly softphone: Softphone;\r\n readonly children: ReactNode;\r\n}\r\n\r\nexport function SoftphoneProvider({ softphone, children }: SoftphoneProviderProps) {\r\n return <SoftphoneContext.Provider value={softphone}>{children}</SoftphoneContext.Provider>;\r\n}\r\n\r\n/** The `Softphone` instance from context. Stable — does not re-render on state changes. */\r\nexport function useSoftphone(): Softphone {\r\n const softphone = useContext(SoftphoneContext);\r\n if (!softphone) {\r\n throw new Error('useSoftphone() must be used within a <SoftphoneProvider>.');\r\n }\r\n return softphone;\r\n}\r\n","// Built-in, no shim: the peer floor is React 18 (SPEC §2.7), which ships useSyncExternalStore.\r\nimport { useCallback, useRef, useSyncExternalStore } from 'react';\r\nimport { useSoftphone } from './context.js';\r\nimport { SoftphoneEvent, CallEvent } from '../core/events.js';\r\nimport type { Call, CallState } from '../core/call.js';\r\nimport type { LineClaim, LineLocation, LineState, PresenceEntry, RemoteParty, SoftphoneState } from '../core/types.js';\r\nimport type { AudioDevices } from '../core/softphone.js';\r\nimport type { FatalSoftphoneError } from '../core/errors.js';\r\n\r\n/** Reactive softphone lifecycle state (`stopped` → `online` → …). */\r\n/**\r\n * The reason the phone cannot currently be used, or `null`.\r\n *\r\n * Reads `softphone.lastError`, which is STATE rather than only an event — the widget is usually\r\n * opened *after* something broke, and a listener mounted at that point would have missed the event\r\n * entirely and rendered a dialpad with no explanation.\r\n *\r\n * Re-reads on both channels because both can change it: `Error` sets it, and `StateChanged` clears it\r\n * on reaching `online`. Subscribing to only the first would leave a stale gate on screen after the\r\n * phone recovered.\r\n */\r\nexport function useLastError(): FatalSoftphoneError | null {\r\n const phone = useSoftphone();\r\n return useSyncExternalStore(\r\n useCallback(\r\n (cb: () => void) => {\r\n const offError = phone.on(SoftphoneEvent.Error, () => cb());\r\n const offState = phone.on(SoftphoneEvent.StateChanged, () => cb());\r\n return () => {\r\n offError();\r\n offState();\r\n };\r\n },\r\n [phone],\r\n ),\r\n useCallback(() => phone.lastError, [phone]),\r\n );\r\n}\r\n\r\nexport function useSoftphoneState(): SoftphoneState {\r\n const phone = useSoftphone();\r\n return useSyncExternalStore(\r\n useCallback((cb: () => void) => phone.on(SoftphoneEvent.StateChanged, () => cb()), [phone]),\r\n useCallback(() => phone.state, [phone]),\r\n );\r\n}\r\n\r\n/**\r\n * Whether THIS tab is the one allowed to be reachable (`multi-tab-leader-election.md`). `false` by\r\n * default and while waiting for another open tab to release it — show your own \"active in another\r\n * tab\" state for that duration; the `/ui` widget already does.\r\n */\r\nexport function useIsLeader(): boolean {\r\n const phone = useSoftphone();\r\n return useSyncExternalStore(\r\n useCallback((cb: () => void) => phone.on(SoftphoneEvent.LeaderChanged, () => cb()), [phone]),\r\n useCallback(() => phone.isLeader, [phone]),\r\n );\r\n}\r\n\r\n/**\r\n * Where this extension's calls ring, who has them when it is not here, and how the last attempt to bring them\r\n * here went — see `useLine`.\r\n */\r\nexport interface LineView {\r\n readonly line: LineState;\r\n readonly where: LineLocation | null;\r\n readonly claim: LineClaim;\r\n}\r\n\r\n/**\r\n * Where this extension's incoming calls ring (`phone.line` / `phone.lineWhere` / `phone.lineClaim`,\r\n * `docs/specs/line-owner.md` §5) — reactive on `SoftphoneEvent.LineChanged` and `LineClaimChanged`.\r\n * `elsewhere` is what puts the softphone in `standby`; show your own \"your phone is active elsewhere\" state\r\n * with `takeLine()` behind it, or let the `/ui` widget's do it.\r\n *\r\n * The snapshot is the SAME object until a field changes — compared by value, not rebuilt per read, since\r\n * `useSyncExternalStore` re-renders forever on a fresh object every time.\r\n */\r\nexport function useLine(): LineView {\r\n const phone = useSoftphone();\r\n const cache = useRef<LineView | null>(null);\r\n return useSyncExternalStore(\r\n useCallback(\r\n (cb: () => void) => {\r\n const offLine = phone.on(SoftphoneEvent.LineChanged, () => cb());\r\n const offClaim = phone.on(SoftphoneEvent.LineClaimChanged, () => cb());\r\n return () => {\r\n offLine();\r\n offClaim();\r\n };\r\n },\r\n [phone],\r\n ),\r\n useCallback((): LineView => {\r\n const { line, lineWhere: where, lineClaim: claim } = phone;\r\n const cached = cache.current;\r\n if (cached?.line === line && cached.where === where && cached.claim === claim) return cached;\r\n const view: LineView = { line, where, claim };\r\n cache.current = view;\r\n return view;\r\n }, [phone]),\r\n );\r\n}\r\n\r\n/**\r\n * The current call, or null. Re-renders when it changes — inbound, outbound, or ended.\r\n *\r\n * `phone.activeCall` returns a stable reference for a given call, so it is a safe snapshot.\r\n */\r\nexport function useActiveCall(): Call | null {\r\n const phone = useSoftphone();\r\n return useSyncExternalStore(\r\n useCallback((cb: () => void) => phone.on(SoftphoneEvent.ActiveCallChanged, () => cb()), [phone]),\r\n useCallback(() => phone.activeCall, [phone]),\r\n );\r\n}\r\n\r\n/**\r\n * The live agent-presence list (`phone.presence`, Phase B) — reactive on `SoftphoneEvent.\r\n * PresenceChanged`. `active` gates the SUBSCRIPTION only, not the underlying connection: the socket\r\n * itself stays governed entirely by the softphone — open while it is started and this tab leads (spec's\r\n * own always-connected decision) — so passing `false` here does not close it — it only stops this\r\n * particular component from re-rendering on every update while it has no reason to care, e.g. the\r\n * transfer step being closed (spec §4.5: \"subscribe when the step opens... unsubscribe when it\r\n * closes\"). `phone.presence` is still read fresh on every call, so flipping `active` back to `true`\r\n * never shows a stale snapshot.\r\n */\r\nexport function usePresence(active: boolean): readonly PresenceEntry[] {\r\n const phone = useSoftphone();\r\n return useSyncExternalStore(\r\n useCallback(\r\n (cb: () => void) => {\r\n if (!active) return () => {};\r\n return phone.on(SoftphoneEvent.PresenceChanged, () => cb());\r\n },\r\n [phone, active],\r\n ),\r\n useCallback(() => phone.presence, [phone]),\r\n );\r\n}\r\n\r\n/** An immutable snapshot of a call's reactive fields. */\r\nexport interface CallView {\r\n readonly state: CallState;\r\n readonly isMuted: boolean;\r\n readonly isOnHold: boolean;\r\n readonly remote: RemoteParty;\r\n readonly remoteStream: MediaStream | null;\r\n readonly answeredAt: Date | null;\r\n}\r\n\r\nconst CALL_EVENTS: readonly CallEvent[] = [\r\n CallEvent.StateChanged,\r\n CallEvent.Muted,\r\n CallEvent.Held,\r\n CallEvent.Unheld,\r\n CallEvent.RemotePartyUpdated,\r\n CallEvent.RemoteStream,\r\n CallEvent.Ended,\r\n];\r\n\r\n/**\r\n * Reactive view of a specific call. Re-renders only when one of the call's own events fires — no\r\n * polling, no re-render storms.\r\n *\r\n * The snapshot is CACHED and invalidated only on an event, so `useSyncExternalStore` gets a stable\r\n * reference between changes (returning a fresh object every render would infinite-loop).\r\n */\r\nexport function useCall(call: Call | null): CallView | null {\r\n const cache = useRef<{ call: Call; view: CallView } | null>(null);\r\n\r\n const subscribe = useCallback(\r\n (cb: () => void) => {\r\n if (!call) return () => {};\r\n const unsubs = CALL_EVENTS.map((event) =>\r\n call.on(event, () => {\r\n cache.current = null; // invalidate; getSnapshot rebuilds on the next read\r\n cb();\r\n }),\r\n );\r\n return () => {\r\n for (const off of unsubs) off();\r\n };\r\n },\r\n [call],\r\n );\r\n\r\n const getSnapshot = useCallback((): CallView | null => {\r\n if (!call) return null;\r\n if (cache.current?.call === call) return cache.current.view;\r\n const view: CallView = {\r\n state: call.state,\r\n isMuted: call.isMuted,\r\n isOnHold: call.isOnHold,\r\n remote: call.remote,\r\n remoteStream: call.remoteStream,\r\n answeredAt: call.answeredAt,\r\n };\r\n cache.current = { call, view };\r\n return view;\r\n }, [call]);\r\n\r\n return useSyncExternalStore(subscribe, getSnapshot);\r\n}\r\n\r\n/**\r\n * The audio device surface (`phone.audio`) — reactive. Re-renders on `SoftphoneEvent.DeviceChange`: a\r\n * device plugged/unplugged, or a selection changed (SPEC §7 Phase 2).\r\n *\r\n * The snapshot is CACHED and invalidated only on that event, same reasoning as `useCall`'s own\r\n * `CallView`: `phone.audio` is ONE stable object whose fields mutate in place, so returning it\r\n * directly would never change by reference and `useSyncExternalStore` would never re-render. A fresh\r\n * object, rebuilt only when something actually changed, is what makes this reactive without polling.\r\n */\r\nexport function useDevices(): AudioDevices {\r\n const phone = useSoftphone();\r\n const cache = useRef<{ audio: AudioDevices; view: AudioDevices } | null>(null);\r\n\r\n const subscribe = useCallback(\r\n (cb: () => void) =>\r\n phone.on(SoftphoneEvent.DeviceChange, () => {\r\n cache.current = null; // invalidate; getSnapshot rebuilds on the next read\r\n cb();\r\n }),\r\n [phone],\r\n );\r\n\r\n const getSnapshot = useCallback((): AudioDevices => {\r\n const audio = phone.audio;\r\n if (cache.current?.audio === audio) return cache.current.view;\r\n const view: AudioDevices = {\r\n inputs: audio.inputs,\r\n outputs: audio.outputs,\r\n currentInputId: audio.currentInputId,\r\n currentOutputId: audio.currentOutputId,\r\n currentRingtoneOutputId: audio.currentRingtoneOutputId,\r\n canSelectOutput: audio.canSelectOutput,\r\n canSelectRingtoneOutput: audio.canSelectRingtoneOutput,\r\n setInput: (deviceId) => audio.setInput(deviceId),\r\n setOutput: (deviceId) => audio.setOutput(deviceId),\r\n setRingtoneOutput: (deviceId) => audio.setRingtoneOutput(deviceId),\r\n refresh: () => audio.refresh(),\r\n };\r\n cache.current = { audio, view };\r\n return view;\r\n }, [phone]);\r\n\r\n return useSyncExternalStore(subscribe, getSnapshot);\r\n}\r\n"]}
@@ -0,0 +1,13 @@
1
+ // src/core/api-base.ts
2
+ function normalizeApiBase(url) {
3
+ const trimmed = url.replace(/\/+$/, "");
4
+ if (!trimmed.endsWith("/api/v1")) return trimmed;
5
+ console.warn(
6
+ "[softphone] apiBase should be Coventia's domain only, without /api/v1. Ignoring the /api/v1 suffix."
7
+ );
8
+ return trimmed.slice(0, -"/api/v1".length).replace(/\/+$/, "");
9
+ }
10
+
11
+ export { normalizeApiBase };
12
+ //# sourceMappingURL=chunk-M76SAXXM.js.map
13
+ //# sourceMappingURL=chunk-M76SAXXM.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/core/api-base.ts"],"names":[],"mappings":";AAaO,SAAS,iBAAiB,GAAA,EAAqB;AACpD,EAAA,MAAM,OAAA,GAAU,GAAA,CAAI,OAAA,CAAQ,MAAA,EAAQ,EAAE,CAAA;AACtC,EAAA,IAAI,CAAC,OAAA,CAAQ,QAAA,CAAS,SAAS,GAAG,OAAO,OAAA;AAEzC,EAAA,OAAA,CAAQ,IAAA;AAAA,IACN;AAAA,GACF;AACA,EAAA,OAAO,OAAA,CAAQ,MAAM,CAAA,EAAG,CAAC,UAAU,MAAM,CAAA,CAAE,OAAA,CAAQ,MAAA,EAAQ,EAAE,CAAA;AAC/D","file":"chunk-M76SAXXM.js","sourcesContent":["/**\r\n * A Coventia base URL the way every request is built from it: no trailing slash, and no `/api/v1`.\r\n *\r\n * `apiBase` is Coventia's domain only — the SDK builds every route itself\r\n * (docs/specs/softphone-voice-token-allowlist.md §3.4). A trailing `/api/v1` is the one mistake worth\r\n * absorbing rather than refusing: the Partner API a host's backend calls does live under it, and the first\r\n * partner configured exactly that. Every softphone request then went to `/api/v1/phone/…`, where nothing\r\n * answers a voice token, and the 401 they got read as \"the SDK wants a different token\". Stripping it keeps\r\n * that configuration working; the warning says how to tidy it up.\r\n *\r\n * Only a FINAL `/api/v1` goes: a host proxying Coventia under a path of its own\r\n * (`https://example.com/coventia`) is left alone.\r\n */\r\nexport function normalizeApiBase(url: string): string {\r\n const trimmed = url.replace(/\\/+$/, '');\r\n if (!trimmed.endsWith('/api/v1')) return trimmed;\r\n\r\n console.warn(\r\n \"[softphone] apiBase should be Coventia's domain only, without /api/v1. Ignoring the /api/v1 suffix.\",\r\n );\r\n return trimmed.slice(0, -'/api/v1'.length).replace(/\\/+$/, '');\r\n}\r\n"]}
@@ -9,6 +9,8 @@ var SoftphoneEvent = /* @__PURE__ */ ((SoftphoneEvent2) => {
9
9
  SoftphoneEvent2["LeaderChanged"] = "leaderChanged";
10
10
  SoftphoneEvent2["TransferOutcome"] = "transferOutcome";
11
11
  SoftphoneEvent2["PresenceChanged"] = "presenceChanged";
12
+ SoftphoneEvent2["LineChanged"] = "lineChanged";
13
+ SoftphoneEvent2["LineClaimChanged"] = "lineClaimChanged";
12
14
  return SoftphoneEvent2;
13
15
  })(SoftphoneEvent || {});
14
16
  var CallEvent = /* @__PURE__ */ ((CallEvent2) => {
@@ -27,5 +29,5 @@ var CallEvent = /* @__PURE__ */ ((CallEvent2) => {
27
29
  })(CallEvent || {});
28
30
 
29
31
  export { CallEvent, SoftphoneEvent };
30
- //# sourceMappingURL=chunk-7R6LOOAI.js.map
31
- //# sourceMappingURL=chunk-7R6LOOAI.js.map
32
+ //# sourceMappingURL=chunk-N4PLEYFJ.js.map
33
+ //# sourceMappingURL=chunk-N4PLEYFJ.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/core/events.ts"],"names":["SoftphoneEvent","CallEvent"],"mappings":";AAqBO,IAAK,cAAA,qBAAAA,eAAAA,KAAL;AAEL,EAAAA,gBAAA,cAAA,CAAA,GAAe,cAAA;AAEf,EAAAA,gBAAA,cAAA,CAAA,GAAe,cAAA;AAMf,EAAAA,gBAAA,mBAAA,CAAA,GAAoB,mBAAA;AAQpB,EAAAA,gBAAA,iBAAA,CAAA,GAAkB,iBAAA;AAOlB,EAAAA,gBAAA,cAAA,CAAA,GAAe,cAAA;AAEf,EAAAA,gBAAA,OAAA,CAAA,GAAQ,OAAA;AAMR,EAAAA,gBAAA,eAAA,CAAA,GAAgB,eAAA;AAShB,EAAAA,gBAAA,iBAAA,CAAA,GAAkB,iBAAA;AAQlB,EAAAA,gBAAA,iBAAA,CAAA,GAAkB,iBAAA;AAQlB,EAAAA,gBAAA,aAAA,CAAA,GAAc,aAAA;AAMd,EAAAA,gBAAA,kBAAA,CAAA,GAAmB,kBAAA;AAhET,EAAA,OAAAA,eAAAA;AAAA,CAAA,EAAA,cAAA,IAAA,EAAA;AAiFL,IAAK,SAAA,qBAAAC,UAAAA,KAAL;AAEL,EAAAA,WAAA,cAAA,CAAA,GAAe,cAAA;AAEf,EAAAA,WAAA,UAAA,CAAA,GAAW,UAAA;AAEX,EAAAA,WAAA,OAAA,CAAA,GAAQ,OAAA;AACR,EAAAA,WAAA,OAAA,CAAA,GAAQ,OAAA;AAER,EAAAA,WAAA,MAAA,CAAA,GAAO,MAAA;AACP,EAAAA,WAAA,QAAA,CAAA,GAAS,QAAA;AAET,EAAAA,WAAA,cAAA,CAAA,GAAe,cAAA;AACf,EAAAA,WAAA,aAAA,CAAA,GAAc,aAAA;AACd,EAAAA,WAAA,YAAA,CAAA,GAAa,YAAA;AAEb,EAAAA,WAAA,oBAAA,CAAA,GAAqB,oBAAA;AAMrB,EAAAA,WAAA,cAAA,CAAA,GAAe,cAAA;AAtBL,EAAA,OAAAA,UAAAA;AAAA,CAAA,EAAA,SAAA,IAAA,EAAA","file":"chunk-N4PLEYFJ.js","sourcesContent":["import type { Call, CallState, HangupReason, HoldOriginator } from './call.js';\r\nimport type { SoftphoneError } from './errors.js';\r\nimport type {\r\n LineClaim,\r\n LineLocation,\r\n LineState,\r\n PresenceEntry,\r\n SoftphoneState,\r\n TransferOutcomeEvent,\r\n} from './types.js';\r\n\r\n/**\r\n * Typed events.\r\n *\r\n * Enums rather than magic strings: autocomplete, and rename-safety. Daily's magic strings are the\r\n * counter-example — a typo in `'participant-joined'` is a silent no-op forever.\r\n *\r\n * Each map narrows the listener payload PER EVENT, which is the thing that would have been painful\r\n * to express in JSDoc and is a large part of why this package is TypeScript (SPEC §2.2).\r\n */\r\n\r\nexport enum SoftphoneEvent {\r\n /** Lifecycle. `online` = reachable, NOT SIP-registered — see SoftphoneState. */\r\n StateChanged = 'stateChanged',\r\n /** An INVITE landed. The Call is `ringing`; answer or reject it. */\r\n IncomingCall = 'incomingCall',\r\n /**\r\n * The active call changed — a call began (inbound OR outbound) or ended. Fires for both\r\n * directions, unlike IncomingCall. This is what a UI binds to in order to track \"the current call\"\r\n * without threading the Call through every component.\r\n */\r\n ActiveCallChanged = 'activeCallChanged',\r\n /**\r\n * The browser refused to play audio (autoplay policy).\r\n *\r\n * This is the single most under-documented failure in the industry and it is how calls get MISSED:\r\n * a blocked ringtone rings silently. Surface a button and call `startAudio()` from its click\r\n * handler. Only Twilio and LiveKit document it properly.\r\n */\r\n PlaybackBlocked = 'playbackBlocked',\r\n /**\r\n * `softphone.audio` changed: a device was plugged/unplugged (`inputs`/`outputs`), or the selected\r\n * input/output changed (`currentInputId`/`currentOutputId`). One event for all of it — re-read\r\n * `softphone.audio` rather than inspecting the payload, which carries nothing (`useDevices` does\r\n * exactly this).\r\n */\r\n DeviceChange = 'deviceChange',\r\n /** Fatal or non-fatal — narrow on `error.fatal`. */\r\n Error = 'error',\r\n /**\r\n * This tab's leadership changed (`multi-tab-leader-election.md`) — a DIFFERENT axis from\r\n * `StateChanged`: `state` is reachability, this is whether this tab is even allowed to be\r\n * reachable at all while another tab holding the same extension stays open.\r\n */\r\n LeaderChanged = 'leaderChanged',\r\n /**\r\n * `Completar` completed a transfer, and the target's SECOND ring either connected or didn't —\r\n * `docs/specs/transfer-outcome-relay.md` (Phase A). Fires for BOTH outcomes so a host can react to\r\n * either; only `'not-connected'` is worth surfacing to the agent (spec §5.5) — roughly a minute\r\n * after the transfer for a target who never answers, seconds for a decline or the caller hanging up\r\n * first. Requires a `TransferOutcomeSource` (from `coventia`/`apiBase`, or a host override); never\r\n * fires at all when none is configured, since there is then nothing to poll.\r\n */\r\n TransferOutcome = 'transferOutcome',\r\n /**\r\n * `softphone.presence` changed — the live agent-presence list (Phase B,\r\n * `docs/specs/transfer-presence-picker.md`) gained a snapshot or an updated entry. Requires a\r\n * `PresenceSource` (from `coventia`/`apiBase`, or a host override) AND this tab currently being a\r\n * started leader — `online`, or waiting in `standby` for the line; never fires at all otherwise,\r\n * same reasoning as `TransferOutcome`.\r\n */\r\n PresenceChanged = 'presenceChanged',\r\n /**\r\n * `softphone.line` / `lineWhere` changed (`docs/specs/line-owner.md` §5): this softphone got the line,\r\n * lost it to another softphone of the same extension, or stopped knowing (`unknown` — it then listens,\r\n * as before the line owner existed). A third axis beside `StateChanged` and `LeaderChanged`: losing the\r\n * line is what takes the softphone to `standby`, but not at once while a call is in progress — the line\r\n * can read `elsewhere` while the state is still `online`, until that call ends.\r\n */\r\n LineChanged = 'lineChanged',\r\n /**\r\n * `softphone.lineClaim` changed: an attempt to bring the line here started, was refused, failed, or ended\r\n * (the line came here, or the softphone stopped). Kept apart from `LineChanged`, which only ever means the\r\n * line itself moved.\r\n */\r\n LineClaimChanged = 'lineClaimChanged',\r\n}\r\n\r\nexport interface SoftphoneEventMap {\r\n [SoftphoneEvent.StateChanged]: { readonly state: SoftphoneState; readonly previous: SoftphoneState };\r\n [SoftphoneEvent.IncomingCall]: Call;\r\n [SoftphoneEvent.ActiveCallChanged]: { readonly call: Call | null };\r\n [SoftphoneEvent.PlaybackBlocked]: void;\r\n [SoftphoneEvent.DeviceChange]: void;\r\n [SoftphoneEvent.Error]: SoftphoneError;\r\n [SoftphoneEvent.LeaderChanged]: { readonly isLeader: boolean };\r\n [SoftphoneEvent.TransferOutcome]: TransferOutcomeEvent;\r\n [SoftphoneEvent.PresenceChanged]: { readonly agents: readonly PresenceEntry[] };\r\n [SoftphoneEvent.LineChanged]: { readonly line: LineState; readonly where: LineLocation | null };\r\n [SoftphoneEvent.LineClaimChanged]: { readonly claim: LineClaim };\r\n}\r\n\r\nexport enum CallEvent {\r\n /** Any transition. The catch-all; the specific events below are conveniences. */\r\n StateChanged = 'stateChanged',\r\n /** Media is flowing. */\r\n Accepted = 'accepted',\r\n /** Terminal. Always fires exactly once, whatever the reason. */\r\n Ended = 'ended',\r\n Muted = 'muted',\r\n /** `originator: 'remote'` means the OTHER party held US — new capability; today's softphone is blind to it. */\r\n Held = 'held',\r\n Unheld = 'unheld',\r\n /** Media lost; an ICE restart is in flight. The call is NOT over — show a banner. */\r\n Reconnecting = 'reconnecting',\r\n Reconnected = 'reconnected',\r\n DigitsSent = 'digitsSent',\r\n /** The contact name resolved after the call was already ringing. */\r\n RemotePartyUpdated = 'remotePartyUpdated',\r\n /**\r\n * The remote audio stream is available. Attach it to an `<audio>` element and play it — this is\r\n * how the caller is actually HEARD. Core cannot do this itself (it must not touch `document` —\r\n * SPEC §10.2); the UI layer or the host owns the audio element.\r\n */\r\n RemoteStream = 'remoteStream',\r\n}\r\n\r\nexport interface CallEventMap {\r\n [CallEvent.StateChanged]: { readonly state: CallState; readonly previous: CallState };\r\n [CallEvent.Accepted]: void;\r\n [CallEvent.Ended]: { readonly reason: HangupReason; readonly error?: SoftphoneError };\r\n [CallEvent.Muted]: { readonly muted: boolean };\r\n [CallEvent.Held]: { readonly originator: HoldOriginator };\r\n [CallEvent.Unheld]: { readonly originator: HoldOriginator };\r\n [CallEvent.Reconnecting]: void;\r\n [CallEvent.Reconnected]: void;\r\n [CallEvent.DigitsSent]: { readonly digits: string };\r\n [CallEvent.RemotePartyUpdated]: void;\r\n [CallEvent.RemoteStream]: { readonly stream: MediaStream };\r\n}\r\n\r\n/**\r\n * `on()` returns an unsubscribe function.\r\n *\r\n * Deliberate: it makes the cleanup impossible to get wrong in a React effect\r\n * (`useEffect(() => call.on(...), [call])`) and it avoids Vonage's design, where `on()` returns an\r\n * opaque `symbol` you must retain in order to ever call `off()`.\r\n */\r\nexport interface TypedEmitter<M> {\r\n on<E extends keyof M>(event: E, listener: (payload: M[E]) => void): () => void;\r\n once<E extends keyof M>(event: E, listener: (payload: M[E]) => void): () => void;\r\n off<E extends keyof M>(event: E, listener: (payload: M[E]) => void): void;\r\n}\r\n"]}
@@ -57,8 +57,44 @@ interface CallOptions {
57
57
  * Zadarma registers on-demand, per call, in response to a push (SPEC §3.2); a persistent UA is
58
58
  * ignored by its PBX. Leaking "registered" into the public API would force every future provider —
59
59
  * including our own SIP infrastructure, which registers persistently — to fake it.
60
+ *
61
+ * `standby` means started, and this browser's leader, but ANOTHER softphone of the same extension has
62
+ * the line — a tab in another browser, the desktop app, a partner page (`docs/specs/line-owner.md` §5).
63
+ * Its push socket is closed on purpose, so nothing here registers or rings: `line` says where the calls
64
+ * go, and `takeLine()` — or a `call()` — brings them here. A host that reads `online` as "ready"
65
+ * correctly reads this as not ready.
66
+ */
67
+ type SoftphoneState = 'stopped' | 'starting' | 'online' | 'standby' | 'offline' | 'reconnecting' | 'destroyed';
68
+ /**
69
+ * Where this extension's incoming calls ring, as far as this softphone knows. `here`: it has the line.
70
+ * `elsewhere`: another softphone of the same extension has it, and this one waits in `standby`. `unknown`:
71
+ * nobody has said — no presence source, a backend that predates the line owner, a presence socket that is
72
+ * down — and this softphone listens, exactly as before the line owner existed (§5.1 R7, fail-open).
60
73
  */
61
- type SoftphoneState = 'stopped' | 'starting' | 'online' | 'offline' | 'reconnecting' | 'destroyed';
74
+ type LineState = 'here' | 'elsewhere' | 'unknown';
75
+ /** Who has the line when it is `elsewhere`: the desktop app, or any other softphone (a browser tab). */
76
+ type LineLocation = 'desktop' | 'browser';
77
+ /**
78
+ * The last attempt to bring the line here — `takeLine()`, or a `call()` from `standby`, whoever made it:
79
+ * `claiming` while it is in flight, `refused` when the user is on a call where the line is, `failed` when
80
+ * nothing could decide. `idle` before any attempt, once the line comes here, and after a `stop()`.
81
+ */
82
+ type LineClaim = 'idle' | 'claiming' | 'refused' | 'failed';
83
+ /** The server's word on this softphone's line, as a `PresenceSource` hands it over (`PresenceCallbacks.onLine`). */
84
+ interface LineUpdate {
85
+ readonly state: 'here' | 'elsewhere';
86
+ /** Who has the line when it is `elsewhere`; `null` when it is here, or the server did not say. */
87
+ readonly where: LineLocation | null;
88
+ }
89
+ /** The answer to `PresenceConnection.claimLine()`. */
90
+ interface LineClaimResult {
91
+ readonly ok: boolean;
92
+ /**
93
+ * Why it was refused: `'on_call'` while the user is on a call (the line never moves during one). Any
94
+ * other value means nothing was decided. Absent when `ok`.
95
+ */
96
+ readonly reason?: string;
97
+ }
62
98
  interface ContactsPage {
63
99
  readonly items: readonly Contact[];
64
100
  /**
@@ -109,6 +145,11 @@ interface CallHistoryEntry {
109
145
  readonly outcome?: CallOutcome;
110
146
  /** The org's own number that was dialled — present only (and only meaningful) for an inbound call. */
111
147
  readonly did?: string;
148
+ /**
149
+ * The org's own number an outbound call was made from (what the callee saw) — present only for outbound calls the
150
+ * backend recorded it for (outbound-number-per-agent.md §5.7). Digits, no `+`.
151
+ */
152
+ readonly from?: string;
112
153
  /** True when Coventia has at least one recording for this call. */
113
154
  readonly recorded: boolean;
114
155
  /**
@@ -205,6 +246,18 @@ interface PresenceConnection {
205
246
  * own source that has no such message simply omits it and gets the grace. Sent just before `close()`.
206
247
  */
207
248
  readonly bye?: () => void;
249
+ /**
250
+ * The line owner (`docs/specs/line-owner.md` §5.2): says which softphone this connection speaks for, so
251
+ * the server can tell it whether it has the line — through `PresenceCallbacks.onLine`, right after this
252
+ * and on every change. Sent as the FIRST message of every connection. Optional: a source without it never
253
+ * takes part, and its softphone listens exactly as before (fail-open), reading its line as `unknown`.
254
+ */
255
+ readonly hello?: (clientId: string) => void;
256
+ /**
257
+ * "Recibir aquí": asks for the line. Answered through `PresenceCallbacks.onClaimResult` — and, when it
258
+ * succeeds, by `onLine` saying `here` first. Optional, like `hello`.
259
+ */
260
+ readonly claimLine?: () => void;
208
261
  /**
209
262
  * Ends this connection deliberately. Never causes the SAME callbacks' `onClose` to fire — that is
210
263
  * reserved for the connection ending on its OWN (a network drop, a server-side close, a rejected
@@ -224,6 +277,13 @@ interface PresenceCallbacks {
224
277
  * identical from here, and both get the same treatment (spec §4.4 — quiet retry, nothing surfaced).
225
278
  */
226
279
  readonly onClose: () => void;
280
+ /**
281
+ * The line, as the server says it — right after `PresenceConnection.hello`, and on every change after
282
+ * that. Optional, so callbacks written before the line owner existed still type-check.
283
+ */
284
+ readonly onLine?: (update: LineUpdate) => void;
285
+ /** The answer to `PresenceConnection.claimLine()`. Optional, like `onLine`. */
286
+ readonly onClaimResult?: (result: LineClaimResult) => void;
227
287
  }
228
288
  /**
229
289
  * Opens one live presence connection. Coventia-backed by default (mints a one-time ticket, opens
@@ -276,6 +336,55 @@ interface RingtoneSource {
276
336
  */
277
337
  cached?(): Promise<RingtoneSettings | null>;
278
338
  }
339
+ /**
340
+ * What kind of number this is, for the tags next to it in "Llamar desde" (outbound-number-per-agent.md §5.5).
341
+ * Display only — it never blocks a choice. `mobile` must not be used for customer care or unsolicited commercial
342
+ * calls; `commercial` (the 400 range) is for outbound commercial calls only and receives no calls.
343
+ */
344
+ type OutboundNumberKind = 'landline' | 'mobile' | 'commercial' | 'other';
345
+ /** One of the organization's numbers an agent can call from. */
346
+ interface OutboundNumber {
347
+ /** Stable id — what is stored as the agent's choice. */
348
+ readonly id: string;
349
+ /** International format, digits only, no `+` — e.g. `34960000001`. */
350
+ readonly number: string;
351
+ /** The name the organization gave the number, or `null`. */
352
+ readonly label: string | null;
353
+ readonly kind: OutboundNumberKind;
354
+ }
355
+ /**
356
+ * Where the agent's extension stands. `pending`/`failed`: an assignment change could not update it yet, so the
357
+ * extension may still present an earlier number. `unassigned`: the agent has no extension in this organization.
358
+ */
359
+ type OutboundNumberState = 'applied' | 'pending' | 'failed' | 'unassigned';
360
+ /** The organization's numbers and the caller's choice, as the backend resolves them. */
361
+ interface OutboundNumberSettings {
362
+ /** The organization's active numbers, ordered by number. */
363
+ readonly numbers: readonly OutboundNumber[];
364
+ /** The organization default (an `OutboundNumber.id`), or `null`. */
365
+ readonly orgDefault: string | null;
366
+ /** This agent's own choice, or `null` = "use the organization default". */
367
+ readonly mine: string | null;
368
+ /** The number this agent's calls show: `mine`, else `orgDefault`. */
369
+ readonly effective: string | null;
370
+ /** The agent's extension ("100"), or `null` when they have none. */
371
+ readonly extension: string | null;
372
+ readonly state: OutboundNumberState;
373
+ /** 0 when a change could go out now; otherwise the seconds until it can. */
374
+ readonly changeAvailableInSeconds: number;
375
+ }
376
+ /** The outbound number of the agent the softphone is signed in as, in its organization. */
377
+ interface OutboundNumbersSource {
378
+ get(signal?: AbortSignal): Promise<OutboundNumberSettings>;
379
+ /**
380
+ * Changes this agent's number; `null` = back to the organization default. Resolves to the new settings once the
381
+ * change is in effect. When the request limits refuse it, rejects with an `Error` carrying
382
+ * `refusal: OutboundNumberRefusal` (check with `isOutboundNumberRefused`) — nothing was stored or sent.
383
+ */
384
+ setMine(id: string | null): Promise<OutboundNumberSettings>;
385
+ /** The settings this agent last had on this device, if remembered — instant, no network. Optional. */
386
+ cached?(): Promise<OutboundNumberSettings | null>;
387
+ }
279
388
 
280
389
  /** Contacts, history, caller ID, the agents directory, transfer outcomes, presence and ringtone
281
390
  * settings, all from one config. */
@@ -293,6 +402,8 @@ interface BackendSources {
293
402
  readonly presence: PresenceSource;
294
403
  /** The caller's ringtone settings (custom-ringtone.md §4.4). Always Coventia's — no override. */
295
404
  readonly ringtones: RingtoneSource;
405
+ /** The caller's outbound number (outbound-number-per-agent.md §5.5). Always Coventia's — no override. */
406
+ readonly outboundNumbers: OutboundNumbersSource;
296
407
  }
297
408
 
298
409
  /**
@@ -414,6 +525,16 @@ declare const ERROR_CATALOGUE: {
414
525
  readonly message: "The provider does not support this operation.";
415
526
  readonly solution: "Guard the UI on the operation before offering it.";
416
527
  };
528
+ readonly 'line/claim-refused': {
529
+ readonly fatal: false;
530
+ readonly message: "The line was not moved here: the user is on a call on the softphone that has it.";
531
+ readonly solution: "Nothing to fix. The line never moves during a call; take it again once that call has ended. Incoming calls keep ringing where they ring now.";
532
+ };
533
+ readonly 'line/unavailable': {
534
+ readonly fatal: false;
535
+ readonly message: "The line could not be taken: nothing decided where it is.";
536
+ readonly solution: "The presence connection is down, the backend predates the line owner, or it did not answer in time. Retry in a moment; incoming calls keep ringing where they ring now.";
537
+ };
417
538
  readonly 'provider/provision-failed': {
418
539
  readonly fatal: true;
419
540
  readonly message: "The provider could not issue SIP credentials.";
@@ -500,10 +621,25 @@ declare enum SoftphoneEvent {
500
621
  /**
501
622
  * `softphone.presence` changed — the live agent-presence list (Phase B,
502
623
  * `docs/specs/transfer-presence-picker.md`) gained a snapshot or an updated entry. Requires a
503
- * `PresenceSource` (from `coventia`/`apiBase`, or a host override) AND this tab currently being
504
- * `online && isLeader`; never fires at all otherwise, same reasoning as `TransferOutcome`.
624
+ * `PresenceSource` (from `coventia`/`apiBase`, or a host override) AND this tab currently being a
625
+ * started leader — `online`, or waiting in `standby` for the line; never fires at all otherwise,
626
+ * same reasoning as `TransferOutcome`.
627
+ */
628
+ PresenceChanged = "presenceChanged",
629
+ /**
630
+ * `softphone.line` / `lineWhere` changed (`docs/specs/line-owner.md` §5): this softphone got the line,
631
+ * lost it to another softphone of the same extension, or stopped knowing (`unknown` — it then listens,
632
+ * as before the line owner existed). A third axis beside `StateChanged` and `LeaderChanged`: losing the
633
+ * line is what takes the softphone to `standby`, but not at once while a call is in progress — the line
634
+ * can read `elsewhere` while the state is still `online`, until that call ends.
635
+ */
636
+ LineChanged = "lineChanged",
637
+ /**
638
+ * `softphone.lineClaim` changed: an attempt to bring the line here started, was refused, failed, or ended
639
+ * (the line came here, or the softphone stopped). Kept apart from `LineChanged`, which only ever means the
640
+ * line itself moved.
505
641
  */
506
- PresenceChanged = "presenceChanged"
642
+ LineClaimChanged = "lineClaimChanged"
507
643
  }
508
644
  interface SoftphoneEventMap {
509
645
  [SoftphoneEvent.StateChanged]: {
@@ -524,6 +660,13 @@ interface SoftphoneEventMap {
524
660
  [SoftphoneEvent.PresenceChanged]: {
525
661
  readonly agents: readonly PresenceEntry[];
526
662
  };
663
+ [SoftphoneEvent.LineChanged]: {
664
+ readonly line: LineState;
665
+ readonly where: LineLocation | null;
666
+ };
667
+ [SoftphoneEvent.LineClaimChanged]: {
668
+ readonly claim: LineClaim;
669
+ };
527
670
  }
528
671
  declare enum CallEvent {
529
672
  /** Any transition. The catch-all; the specific events below are conveniences. */
@@ -822,6 +965,39 @@ interface Softphone extends TypedEmitter<SoftphoneEventMap> {
822
965
  * "active in another tab" state for that duration (the `/ui` widget already does).
823
966
  */
824
967
  readonly isLeader: boolean;
968
+ /**
969
+ * Where this extension's incoming calls ring (`docs/specs/line-owner.md` §5) — the axis ACROSS browsers,
970
+ * devices and the desktop app that `isLeader` cannot see. Only one softphone of an extension listens:
971
+ * the first one started keeps the line, and the others wait in `standby` until it is free or the user
972
+ * takes it (`takeLine()`).
973
+ *
974
+ * `unknown` until the backend has said, and whenever it cannot (no presence source, a backend that
975
+ * predates the line owner, a presence socket that is down): the softphone then listens, exactly as
976
+ * before this existed — it never leaves nobody listening. Changes fire `SoftphoneEvent.LineChanged`.
977
+ */
978
+ readonly line: LineState;
979
+ /**
980
+ * Who has the line while it is `elsewhere` — the desktop app, or another softphone (a browser tab) —
981
+ * or `null`: the line is here, unknown, or the backend did not say.
982
+ */
983
+ readonly lineWhere: LineLocation | null;
984
+ /**
985
+ * How the last attempt to bring the line here went, whoever made it (`takeLine()`, or a `call()` from
986
+ * `standby`) — what the waiting screen shows under its title, so a refused click-to-call explains itself
987
+ * just like a refused "Recibir aquí". Changes fire `SoftphoneEvent.LineClaimChanged`.
988
+ */
989
+ readonly lineClaim: LineClaim;
990
+ /**
991
+ * "Recibir aquí": takes the line from whichever softphone has it, so incoming calls ring here. Resolves
992
+ * once the line IS here (at once when it already is); the softphone then leaves `standby` and starts
993
+ * listening on its own.
994
+ *
995
+ * Rejects with a non-fatal `SoftphoneError`: `line/claim-refused` while the user is on a call on the
996
+ * softphone that has the line (it never moves during a call — retry once it ends), or `line/unavailable`
997
+ * when nothing could decide (no presence connection, a backend without the line owner, no answer in
998
+ * time). Neither is emitted on `SoftphoneEvent.Error` — this promise is the answer.
999
+ */
1000
+ takeLine(): Promise<void>;
825
1001
  /**
826
1002
  * Start.
827
1003
  *
@@ -834,6 +1010,11 @@ interface Softphone extends TypedEmitter<SoftphoneEventMap> {
834
1010
  stop(): Promise<void>;
835
1011
  /** Idempotent. After this the instance is dead — construct a new one. */
836
1012
  destroy(): Promise<void>;
1013
+ /**
1014
+ * Dial. From `standby` this takes the line first (`takeLine()`), then dials — a call the host starts is
1015
+ * the user asking for the phone here — and rejects with that method's `line/*` error if the line cannot
1016
+ * be taken.
1017
+ */
837
1018
  call(destination: string, options?: CallOptions): Promise<Call>;
838
1019
  /** The live call, or null. The consult leg of an attended transfer is reachable from it. */
839
1020
  readonly activeCall: Call | null;
@@ -848,8 +1029,9 @@ interface Softphone extends TypedEmitter<SoftphoneEventMap> {
848
1029
  /**
849
1030
  * The live agent-presence list (Phase B, `docs/specs/transfer-presence-picker.md`) — every org
850
1031
  * member with a phone extension, whatever their state. Empty when there is no `PresenceSource`
851
- * (no Coventia backend, or a host that omitted one), or while this tab is not `online && isLeader`
852
- * — never an error, just nothing to show (spec §4.4's own success criterion). Updates fire
1032
+ * (no Coventia backend, or a host that omitted one), or while this tab is not a started leader —
1033
+ * `online`, or waiting in `standby` for the line — or its connection is down: never an error, just
1034
+ * nothing to show (spec §4.4's own success criterion). Updates fire
853
1035
  * `SoftphoneEvent.PresenceChanged`; `usePresence()` is the reactive read.
854
1036
  */
855
1037
  readonly presence: readonly PresenceEntry[];
@@ -997,6 +1179,16 @@ interface SoftphoneElementEventMap {
997
1179
  'cv-leadership-changed': CustomEvent<{
998
1180
  readonly isLeader: boolean;
999
1181
  }>;
1182
+ /**
1183
+ * Where this extension's calls ring changed (`docs/specs/line-owner.md` §5) — the axis across browsers,
1184
+ * devices and the desktop app that `cv-leadership-changed` cannot see. `line: 'elsewhere'` is what puts
1185
+ * the phone in `standby` (`cv-state-changed`), and **the widget already shows its own waiting screen with
1186
+ * "Recibir aquí" for it** — listen here for your own indicator; `el.softphone.takeLine()` takes the line.
1187
+ */
1188
+ 'cv-line-changed': CustomEvent<{
1189
+ readonly line: LineState;
1190
+ readonly where: LineLocation | null;
1191
+ }>;
1000
1192
  /**
1001
1193
  * A transfer's final handoff step resolved — `status` says whether the caller actually reached
1002
1194
  * `target`. Polled, not instant (Phase A, `docs/specs/transfer-outcome-relay.md`); only for that