@lightworkai.official/debug-capture 0.17.2 → 0.17.3

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.
@@ -55,3 +55,4 @@ export declare function callFrameFor(): string | null;
55
55
  /** Open the call's frame — once per call; the same call again changes nothing. Shown when it says it is ready. */
56
56
  export declare function openCallFrame(src: string, id: string, h: FrameHandlers, readyWithinMs?: number, token?: Promise<string | null> | null): void;
57
57
  export declare function closeCallFrame(): void;
58
+ export declare function nameTabAtRest(handle: string, readableBy: string): void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lightworkai.official/debug-capture",
3
- "version": "0.17.2",
3
+ "version": "0.17.3",
4
4
  "description": "Report-a-problem widget: rolling network/console/action capture, optional rrweb session replay, annotated screenshot, and submission to a Lightwork Support host",
5
5
  "type": "module",
6
6
  "module": "./dist/index.mjs",
package/src/live.ts CHANGED
@@ -25,7 +25,9 @@
25
25
  import { getConfig } from "./config";
26
26
  import { preparePortalUrl } from "./portal";
27
27
  import { hideRing, quietRing, ringAnswering, ringFallback, ringOpened, ringShownFor, showRing, type RingHandlers } from "./ui/ring";
28
- import { answerInFrame, callFrameFor, closeCallFrame, frameUrl, openCallFrame, voiceInFrame } from "./ui/callFrame";
28
+ import { answerInFrame, callFrameFor, closeCallFrame, frameUrl, nameTabAtRest, openCallFrame, voiceInFrame } from "./ui/callFrame";
29
+ import { readInkOp } from "./ink";
30
+ import { inkOnPage, removeInkLayer } from "./ui/inkOverlay";
29
31
 
30
32
  export interface RingingCall {
31
33
  id: string;
@@ -99,6 +101,11 @@ export function startLiveRing(): void {
99
101
  started = true;
100
102
  stopped = false;
101
103
  warmUp(config.host);
104
+ try {
105
+ nameTabAtRest(`lw-tab:${tabId()}`, new URL(config.host).origin);
106
+ } catch {
107
+ /* not a host we can name */
108
+ }
102
109
  void connect();
103
110
  }
104
111
 
@@ -166,6 +173,8 @@ async function connect(): Promise<void> {
166
173
  socket = ws;
167
174
  ws.onopen = () => {
168
175
  retry = 0;
176
+ // Which tab this is (§21): the call's marks for a screen shared from here reach it on this socket.
177
+ ws.send(JSON.stringify({ type: "tab", id: tabId() }));
169
178
  clearInterval(pinger);
170
179
  pinger = setInterval(() => {
171
180
  if (ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify({ type: "ping" }));
@@ -173,8 +182,16 @@ async function connect(): Promise<void> {
173
182
  };
174
183
  ws.onmessage = (e) => {
175
184
  try {
176
- const frame = JSON.parse(String(e.data)) as { type?: string; call?: RingingCall };
177
- if (frame.type === "call-state" && frame.call) hear(frame.call);
185
+ const frame = JSON.parse(String(e.data)) as { type?: string; call?: RingingCall | string; op?: unknown };
186
+ if (frame.type === "call-state" && frame.call && typeof frame.call === "object") hear(frame.call);
187
+ // The team's marks on this page, shared from the portal's call (§21) — read again, never drawn as received.
188
+ else if (frame.type === "call-ink" && typeof frame.call === "string") {
189
+ const op = readInkOp(frame.op);
190
+ if (op) {
191
+ inkCall = frame.call;
192
+ inkOnPage(op);
193
+ }
194
+ }
178
195
  } catch {
179
196
  /* not ours to read */
180
197
  }
@@ -254,8 +271,37 @@ function frameFor(call: RingingCall): void {
254
271
  }
255
272
  const portalWay = (call: RingingCall): RingHandlers => ({ open: () => answer(call), decline: () => void decline(call) });
256
273
 
274
+ /** This tab's own id (§21), kept across its reloads — what its name says, and what its socket says. */
275
+ let ownTabId: string | null = null;
276
+ function tabId(): string {
277
+ if (ownTabId) return ownTabId;
278
+ try {
279
+ ownTabId = sessionStorage.getItem(TAB_KEY);
280
+ } catch {
281
+ /* no storage: a new id each load */
282
+ }
283
+ if (!ownTabId || !/^[A-Za-z0-9_-]{8,40}$/.test(ownTabId)) {
284
+ const bytes = new Uint8Array(12);
285
+ crypto.getRandomValues(bytes);
286
+ ownTabId = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
287
+ try {
288
+ sessionStorage.setItem(TAB_KEY, ownTabId);
289
+ } catch {
290
+ /* this load only */
291
+ }
292
+ }
293
+ return ownTabId;
294
+ }
295
+ const TAB_KEY = "lw-tab-id";
296
+ /** The call whose marks are on this page (§21): they go with it. */
297
+ let inkCall: string | null = null;
298
+
257
299
  /** The support host's word on a call: ringing here shows the card (and its frame); anything else takes the card away — the frame stays, it IS the call. */
258
300
  function hear(call: RingingCall): void {
301
+ if (!LIVE_STATES.has(call.status) && inkCall === call.id && callFrameFor() !== call.id) {
302
+ inkCall = null;
303
+ removeInkLayer();
304
+ }
259
305
  if (!ringsHere(call)) {
260
306
  hideRing(call.id);
261
307
  // Answered in this tab's frame, then the page reloaded: the frame comes back and takes the call back.
@@ -169,7 +169,7 @@ iframe.on { visibility: visible; }`;
169
169
  }
170
170
 
171
171
  export function closeCallFrame(): void {
172
- if (host) nameTab(null);
172
+ if (host) nameTab(resting?.handle ?? null, resting?.readableBy);
173
173
  removeInkLayer();
174
174
  clearTimeout(readyTimer);
175
175
  window.removeEventListener("message", onMessage);
@@ -250,15 +250,26 @@ function onMessage(e: MessageEvent): void {
250
250
  * browser without Capture Handle simply has no name to give, and the call says it cannot tell.
251
251
  */
252
252
  type CaptureHandleConfig = { handle?: string; exposeOrigin?: boolean; permittedOrigins?: string[] };
253
- function nameTab(handle: string | null): void {
253
+ function nameTab(handle: string | null, readableBy = origin): void {
254
254
  const md = (typeof navigator === "undefined" ? undefined : navigator.mediaDevices) as (MediaDevices & { setCaptureHandleConfig?: (c: CaptureHandleConfig) => void }) | undefined;
255
255
  try {
256
- md?.setCaptureHandleConfig?.(handle ? { handle, exposeOrigin: false, permittedOrigins: [origin] } : {});
256
+ md?.setCaptureHandleConfig?.(handle && readableBy ? { handle, exposeOrigin: false, permittedOrigins: [readableBy] } : {});
257
257
  } catch {
258
258
  /* not a top-level page, or not this browser */
259
259
  }
260
260
  }
261
261
 
262
+ /**
263
+ * The tab's name between calls (0.17.3, the support host's §21): `lw-tab:<id>` — so a call answered on the support
264
+ * host's portal, with THIS tab shared, can tell it is a tab of ours and have the team's marks drawn here (through the
265
+ * live socket). A call's frame names the tab for its call while it lasts, and gives this name back when it goes.
266
+ */
267
+ let resting: { handle: string; readableBy: string } | null = null;
268
+ export function nameTabAtRest(handle: string, readableBy: string): void {
269
+ resting = { handle, readableBy };
270
+ if (!host?.isConnected) nameTab(handle, readableBy);
271
+ }
272
+
262
273
  /** The corner moved by its top line: from where the press began, by how far the pointer went on the screen. */
263
274
  function onDrag(m: FrameMessage): void {
264
275
  if (!frame || layout !== "card" || typeof m.x !== "number" || typeof m.y !== "number") return;