@opengeni/desktop 6.5.2-canary.41607687019001 → 6.7.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.
package/dist/index.js CHANGED
@@ -3327,10 +3327,15 @@ import { Notification, powerMonitor } from "electron";
3327
3327
  async function actOnInboxItem(client, item, action) {
3328
3328
  if (action.kind === "approve" || action.kind === "deny") {
3329
3329
  if (item.kind !== "approval") return false;
3330
- await client.sendApprovalDecision(item.workspaceId, item.sessionId, {
3331
- approvalId: item.sourceKey,
3332
- decision: action.kind === "approve" ? "approve" : "reject"
3333
- });
3330
+ try {
3331
+ await client.sendApprovalDecision(item.workspaceId, item.sessionId, {
3332
+ approvalId: item.sourceKey,
3333
+ decision: action.kind === "approve" ? "approve" : "reject"
3334
+ });
3335
+ } catch (error) {
3336
+ if (error.status === 403) return false;
3337
+ throw error;
3338
+ }
3334
3339
  return true;
3335
3340
  }
3336
3341
  if (item.kind !== "question") return false;
@@ -4317,6 +4322,16 @@ var MeetingDetectionStore = class {
4317
4322
  var PROBLEMS = {
4318
4323
  sound_server_unavailable: "Calls can't be noticed because no sound server (PulseAudio or PipeWire) is running."
4319
4324
  };
4325
+ function recordingMeeting(value) {
4326
+ if (!value || typeof value !== "object") return null;
4327
+ const { title, elapsedMilliseconds, paused } = value;
4328
+ if (typeof elapsedMilliseconds !== "number" || !Number.isFinite(elapsedMilliseconds)) return null;
4329
+ return {
4330
+ title: typeof title === "string" && title.trim() ? title.trim().slice(0, 200) : null,
4331
+ elapsedMilliseconds: Math.max(0, elapsedMilliseconds),
4332
+ paused: paused === true
4333
+ };
4334
+ }
4320
4335
  var RESTART_MIN_MS = 1e3;
4321
4336
  var RESTART_MAX_MS = 6e4;
4322
4337
  var REQUEST_WAIT_MS = 6e4;
@@ -4345,8 +4360,10 @@ var MeetingDetection = class {
4345
4360
  running = false;
4346
4361
  /** Pages that take choices, oldest first. */
4347
4362
  pages = [];
4348
- recordingPages = /* @__PURE__ */ new Set();
4363
+ /** The pages that record, oldest first, with what they said about it. */
4364
+ recordingPages = /* @__PURE__ */ new Map();
4349
4365
  listeners = /* @__PURE__ */ new Set();
4366
+ recordingListeners = /* @__PURE__ */ new Set();
4350
4367
  pending = null;
4351
4368
  cannotRecord = null;
4352
4369
  status() {
@@ -4400,11 +4417,67 @@ var MeetingDetection = class {
4400
4417
  if (!listening || !pending) return;
4401
4418
  if (this.send(pending.request)) this.clearPending();
4402
4419
  }
4403
- /** A page started or stopped recording a meeting. */
4404
- setRecording(page, recording) {
4405
- if (recording) this.recordingPages.add(page);
4406
- else this.recordingPages.delete(page);
4407
- this.prompter.setRecording(this.recordingPages.size > 0, this.now());
4420
+ /**
4421
+ * A page started or stopped recording a meeting, and what it said about
4422
+ * the meeting. A page built before it described its meetings says nothing:
4423
+ * the recording counts from when the app heard of it.
4424
+ */
4425
+ setRecording(page, recording, meeting = null) {
4426
+ const now = this.now();
4427
+ const before = this.recordingPages.get(page);
4428
+ if (!recording) {
4429
+ if (!this.recordingPages.delete(page)) return;
4430
+ } else if (meeting) {
4431
+ this.recordingPages.set(page, { page, ...meeting, at: now, canOpen: true });
4432
+ } else if (!before) {
4433
+ this.recordingPages.set(page, {
4434
+ page,
4435
+ title: null,
4436
+ paused: false,
4437
+ elapsedMilliseconds: 0,
4438
+ at: now,
4439
+ canOpen: false
4440
+ });
4441
+ }
4442
+ this.prompter.setRecording(this.recordingPages.size > 0, now);
4443
+ for (const listener of this.recordingListeners) listener();
4444
+ }
4445
+ /** The meeting being recorded here, the newest when pages record several; null for none. */
4446
+ recording() {
4447
+ let newest = null;
4448
+ for (const [page, recording] of this.recordingPages) {
4449
+ if (!page.isDestroyed()) newest = recording;
4450
+ }
4451
+ return newest;
4452
+ }
4453
+ /** Runs `listener` when a recording starts, stops, or its title or pause changes. */
4454
+ onRecording(listener) {
4455
+ this.recordingListeners.add(listener);
4456
+ return () => {
4457
+ this.recordingListeners.delete(listener);
4458
+ };
4459
+ }
4460
+ /** Stop the recordings going on, from the app's menu; false when none is. */
4461
+ stopRecording() {
4462
+ const pages = [...this.recordingPages.keys()].filter((page) => !page.isDestroyed());
4463
+ if (pages.length === 0) return false;
4464
+ this.options.log("Meeting detection: stopping the recording from the menu.");
4465
+ for (const page of pages) {
4466
+ page.send(contract_exports.DESKTOP_IPC.meetingDetectionRequest, { kind: "stop", app: "" });
4467
+ }
4468
+ return true;
4469
+ }
4470
+ /**
4471
+ * Ask the page recording `recording` to show it; false when that page
4472
+ * can't (it didn't describe its recording) or is gone.
4473
+ */
4474
+ openRecording(recording) {
4475
+ const page = recording.page;
4476
+ if (!recording.canOpen || page.isDestroyed() || !this.recordingPages.has(page)) return false;
4477
+ page.send(contract_exports.DESKTOP_IPC.meetingDetectionRequest, {
4478
+ kind: "open"
4479
+ });
4480
+ return true;
4408
4481
  }
4409
4482
  /** A page closed or went to another address: it neither records nor listens. */
4410
4483
  pageGone(page) {
@@ -5039,21 +5112,21 @@ function handleMeetingDetectionIpc(input) {
5039
5112
  });
5040
5113
  };
5041
5114
  const on = (channel, run2) => {
5042
- ipcMain2.on(channel, (event, arg) => {
5115
+ ipcMain2.on(channel, (event, arg, more) => {
5043
5116
  try {
5044
5117
  allow(event);
5045
5118
  } catch {
5046
5119
  return;
5047
5120
  }
5048
5121
  watch(event.sender);
5049
- run2(event.sender, arg);
5122
+ run2(event.sender, arg, more);
5050
5123
  });
5051
5124
  };
5052
5125
  handle(contract_exports.DESKTOP_IPC.meetingDetectionStatus, () => detection.status());
5053
5126
  handle(contract_exports.DESKTOP_IPC.meetingDetectionUpdate, (change) => detection.update(change));
5054
5127
  on(
5055
5128
  contract_exports.DESKTOP_IPC.meetingDetectionRecording,
5056
- (page, recording) => detection.setRecording(page, recording === true)
5129
+ (page, recording, meeting) => detection.setRecording(page, recording === true, recordingMeeting(meeting))
5057
5130
  );
5058
5131
  on(
5059
5132
  contract_exports.DESKTOP_IPC.meetingDetectionListen,
@@ -5510,6 +5583,14 @@ function stroke(ax, ay, bx, by, width) {
5510
5583
  function capsule(cx, top, bottom, width) {
5511
5584
  return stroke(cx, top + width / 2, cx, bottom - width / 2, width);
5512
5585
  }
5586
+ function roundedBox(left, top, right, bottom, r) {
5587
+ return (x, y) => {
5588
+ if (x < left || x > right || y < top || y > bottom) return false;
5589
+ const dx = Math.max(left + r - x, 0, x - (right - r));
5590
+ const dy = Math.max(top + r - y, 0, y - (bottom - r));
5591
+ return dx * dx + dy * dy <= r * r;
5592
+ };
5593
+ }
5513
5594
  function ink(...parts) {
5514
5595
  return (x, y) => parts.some((part) => part(x, y)) ? "ink" : "none";
5515
5596
  }
@@ -5539,6 +5620,22 @@ var SHAPES = {
5539
5620
  });
5540
5621
  return ink(...bars);
5541
5622
  },
5623
+ // A recording badge: a dot and a sound wave in a pill, a meeting being recorded.
5624
+ meeting: () => {
5625
+ const outer = roundedBox(0.03, 0.25, 0.97, 0.75, 0.25);
5626
+ const inner = roundedBox(0.105, 0.325, 0.895, 0.675, 0.175);
5627
+ const dot = circle(0.3, 0.5, 0.105);
5628
+ const wave = ink(
5629
+ capsule(0.53, 0.43, 0.57, 0.07),
5630
+ capsule(0.645, 0.37, 0.63, 0.07),
5631
+ capsule(0.76, 0.43, 0.57, 0.07)
5632
+ );
5633
+ return (x, y) => {
5634
+ if (dot(x, y)) return "accent";
5635
+ if (outer(x, y) && !inner(x, y)) return "ink";
5636
+ return wave(x, y);
5637
+ };
5638
+ },
5542
5639
  // A check: the text is where it was going.
5543
5640
  done: () => ink(stroke(0.2, 0.52, 0.41, 0.73, 0.11), stroke(0.41, 0.73, 0.8, 0.29, 0.11)),
5544
5641
  // A warning sign with the exclamation mark cut out.
@@ -5555,6 +5652,7 @@ var SHAPES = {
5555
5652
 
5556
5653
  // src/main/tray.ts
5557
5654
  var DICTATION_DONE_MS = 800;
5655
+ var MEETING_TICK_MS = 3e4;
5558
5656
  var PERMISSION_LABELS = {
5559
5657
  screenRecording: "Allow Screen Recording\u2026",
5560
5658
  accessibility: "Allow Accessibility\u2026",
@@ -5576,6 +5674,11 @@ var AppTray = class {
5576
5674
  idle;
5577
5675
  summary = null;
5578
5676
  dictationMenu = null;
5677
+ meeting = null;
5678
+ meetingTimer = null;
5679
+ /** The menu is open: rebuilding it then could close it, so wait. */
5680
+ menuOpen = false;
5681
+ stale = false;
5579
5682
  /** The glyph showing; null for the app's icon. */
5580
5683
  glyph = null;
5581
5684
  images = /* @__PURE__ */ new Map();
@@ -5591,6 +5694,17 @@ var AppTray = class {
5591
5694
  if (menu && menu.activity.phase !== "idle") this.clearDone();
5592
5695
  this.render();
5593
5696
  }
5697
+ /** The meeting being recorded; null when none is. */
5698
+ setMeeting(meeting) {
5699
+ this.meeting = meeting;
5700
+ if (meeting && !this.meetingTimer) {
5701
+ this.meetingTimer = setInterval(() => this.render(), MEETING_TICK_MS);
5702
+ this.meetingTimer.unref?.();
5703
+ } else if (!meeting) {
5704
+ this.stopMeetingTimer();
5705
+ }
5706
+ this.render();
5707
+ }
5594
5708
  /** A dictation's text was delivered: a check, briefly. */
5595
5709
  dictationDone() {
5596
5710
  this.clearDone();
@@ -5603,6 +5717,7 @@ var AppTray = class {
5603
5717
  destroy() {
5604
5718
  this.clearDone();
5605
5719
  this.stopAnimation();
5720
+ this.stopMeetingTimer();
5606
5721
  this.tray.destroy();
5607
5722
  }
5608
5723
  render() {
@@ -5612,15 +5727,30 @@ var AppTray = class {
5612
5727
  this.config.productName,
5613
5728
  this.actions,
5614
5729
  this.dictationMenu,
5615
- this.platform
5730
+ this.platform,
5731
+ this.meeting
5616
5732
  );
5617
5733
  this.tray.setToolTip(toolTip);
5618
- this.tray.setContextMenu(Menu2.buildFromTemplate(template));
5734
+ if (this.menuOpen) {
5735
+ this.stale = true;
5736
+ } else {
5737
+ const menu = Menu2.buildFromTemplate(template);
5738
+ menu.on("menu-will-show", () => {
5739
+ this.menuOpen = true;
5740
+ });
5741
+ menu.on("menu-will-close", () => {
5742
+ this.menuOpen = false;
5743
+ if (!this.stale) return;
5744
+ this.stale = false;
5745
+ setTimeout(() => this.render(), 0);
5746
+ });
5747
+ this.tray.setContextMenu(menu);
5748
+ }
5619
5749
  this.showIcon();
5620
5750
  }
5621
5751
  showIcon() {
5622
5752
  const phase = this.dictationMenu?.activity.phase ?? "idle";
5623
- const glyph = phase !== "idle" ? phase : this.doneTimer ? "done" : null;
5753
+ const glyph = phase !== "idle" ? phase : this.doneTimer ? "done" : this.meeting ? "meeting" : null;
5624
5754
  if (glyph === this.glyph) return;
5625
5755
  this.glyph = glyph;
5626
5756
  this.stopAnimation();
@@ -5652,6 +5782,10 @@ var AppTray = class {
5652
5782
  if (this.animation) clearInterval(this.animation);
5653
5783
  this.animation = null;
5654
5784
  }
5785
+ stopMeetingTimer() {
5786
+ if (this.meetingTimer) clearInterval(this.meetingTimer);
5787
+ this.meetingTimer = null;
5788
+ }
5655
5789
  clearDone() {
5656
5790
  if (this.doneTimer) clearTimeout(this.doneTimer);
5657
5791
  this.doneTimer = null;
@@ -5739,6 +5873,43 @@ function dictationSection(menu, actions, platform) {
5739
5873
  };
5740
5874
  }
5741
5875
  }
5876
+ function recordedWords(milliseconds) {
5877
+ const minutes = Math.floor(milliseconds / 6e4);
5878
+ if (minutes < 1) return "just started";
5879
+ if (minutes < 60) return `${minutes} min`;
5880
+ const hours = Math.floor(minutes / 60);
5881
+ const rest = minutes % 60;
5882
+ return rest === 0 ? `${hours} h` : `${hours} h ${rest} min`;
5883
+ }
5884
+ function clockTime(at) {
5885
+ return new Date(at).toLocaleTimeString(void 0, { hour: "2-digit", minute: "2-digit" });
5886
+ }
5887
+ function meetingSection(meeting, actions, now) {
5888
+ const recorded = meeting.elapsedMilliseconds + (meeting.paused ? 0 : Math.max(0, now - meeting.at));
5889
+ const started = meeting.at - meeting.elapsedMilliseconds;
5890
+ const what = meeting.title ? shorten(meeting.title) : `Started ${clockTime(started)}`;
5891
+ return [
5892
+ {
5893
+ label: `Meeting: ${meeting.paused ? "paused" : "recording"} \xB7 ${recordedWords(recorded)}`,
5894
+ enabled: false
5895
+ },
5896
+ {
5897
+ label: meeting.otherAccount ? `${what} \xB7 ${shorten(meeting.otherAccount)}` : what,
5898
+ enabled: false
5899
+ },
5900
+ {
5901
+ label: meeting.canOpen ? "Open Live Transcript" : "Show Recording",
5902
+ click: () => actions.meeting?.("open")
5903
+ },
5904
+ { label: "Stop Recording", click: () => actions.meeting?.("stop") },
5905
+ { type: "separator" }
5906
+ ];
5907
+ }
5908
+ function meetingTip(meeting, now) {
5909
+ if (!meeting) return null;
5910
+ const recorded = meeting.elapsedMilliseconds + (meeting.paused ? 0 : Math.max(0, now - meeting.at));
5911
+ return meeting.paused ? `meeting recording paused (${recordedWords(recorded)})` : `recording a meeting (${recordedWords(recorded)})`;
5912
+ }
5742
5913
  function dictationTip(menu) {
5743
5914
  const activity = menu?.activity;
5744
5915
  switch (activity?.phase) {
@@ -5754,11 +5925,11 @@ function dictationTip(menu) {
5754
5925
  return null;
5755
5926
  }
5756
5927
  }
5757
- function trayMenu(summary, productName, actions, dictation = null, platform = process.platform) {
5928
+ function trayMenu(summary, productName, actions, dictation = null, platform = process.platform, meeting = null, now = Date.now()) {
5758
5929
  const missing = summary.running ? summary.missingPermissions ?? [] : [];
5759
5930
  const status = !summary.running ? "paused" : summary.needsReconnect ? "needs reconnecting" : missing.length > 0 ? "screen control needs permission" : summary.reconnecting ? "connecting\u2026" : "available to agents";
5760
- const dictating = dictationTip(dictation);
5761
- const toolTip = dictating ? `${productName}: ${dictating}` : !summary.connected ? productName : `${productName}: ${!summary.running ? "this computer is paused" : summary.needsReconnect ? "this computer needs reconnecting" : summary.reconnecting ? "connecting this computer\u2026" : "this computer is available to agents"}`;
5931
+ const busy = dictationTip(dictation) ?? meetingTip(meeting, now);
5932
+ const toolTip = busy ? `${productName}: ${busy}` : !summary.connected ? productName : `${productName}: ${!summary.running ? "this computer is paused" : summary.needsReconnect ? "this computer needs reconnecting" : summary.reconnecting ? "connecting this computer\u2026" : "this computer is available to agents"}`;
5762
5933
  const computer = !summary.connected ? [] : [
5763
5934
  { label: `This computer: ${status}`, enabled: false },
5764
5935
  // Reconnecting needs the account and workspace, which the window has.
@@ -5778,6 +5949,7 @@ function trayMenu(summary, productName, actions, dictation = null, platform = pr
5778
5949
  return {
5779
5950
  toolTip,
5780
5951
  template: [
5952
+ ...meeting ? meetingSection(meeting, actions, now) : [],
5781
5953
  ...dictationItems,
5782
5954
  ...computer,
5783
5955
  { label: `Open ${productName}`, click: () => actions.open() },
@@ -6928,6 +7100,16 @@ var DesktopShell = class _DesktopShell {
6928
7100
  if (activity.phase === "done") this.tray?.dictationDone();
6929
7101
  else this.tray?.setDictation(this.dictation.menu());
6930
7102
  });
7103
+ let recordingMeeting2 = false;
7104
+ this.meetings.onRecording(() => {
7105
+ const recording = this.meetings.recording() !== null;
7106
+ if (recording !== recordingMeeting2) {
7107
+ recordingMeeting2 = recording;
7108
+ void this.refreshTray();
7109
+ } else {
7110
+ this.tray?.setMeeting(this.trayMeeting());
7111
+ }
7112
+ });
6931
7113
  const hidden = (openedAtLogin() || this.reopenedWithoutWindow()) && this.store.active() !== null;
6932
7114
  if (!hidden) this.openWindow();
6933
7115
  if (process.platform === "darwin" && app3.isPackaged && this.config.thisComputer) {
@@ -7778,7 +7960,7 @@ var DesktopShell = class _DesktopShell {
7778
7960
  );
7779
7961
  if (change) setOpenAtLogin(this.config, change === "open");
7780
7962
  }
7781
- const wanted = (process.platform === "darwin" ? summary.connected : hasAccounts) || this.dictation.enabled;
7963
+ const wanted = (process.platform === "darwin" ? summary.connected : hasAccounts) || this.dictation.enabled || this.meetings.recording() !== null;
7782
7964
  if (!wanted) {
7783
7965
  this.tray?.destroy();
7784
7966
  this.tray = null;
@@ -7790,11 +7972,50 @@ var DesktopShell = class _DesktopShell {
7790
7972
  resume: () => void this.computer.resume().catch(() => void 0),
7791
7973
  allow: (permission) => void this.computer.requestPermission(permission).catch(() => void 0),
7792
7974
  quit: () => app3.quit(),
7793
- dictation: (action) => this.dictation.menuAction(action)
7975
+ dictation: (action) => this.dictation.menuAction(action),
7976
+ meeting: (action) => this.meetingAction(action)
7794
7977
  });
7795
7978
  this.tray.setDictation(this.dictation.menu());
7979
+ this.tray.setMeeting(this.trayMeeting());
7796
7980
  this.tray.update(summary);
7797
7981
  }
7982
+ /** The meeting being recorded, for the menu: the newest, in whichever account records it. */
7983
+ trayMeeting() {
7984
+ const recording = this.meetings.recording();
7985
+ if (!recording) return null;
7986
+ const accountId = this.recordingAccount(recording);
7987
+ const shown = this.window?.accountId ?? this.store.snapshot().activeId;
7988
+ const other = accountId && accountId !== shown ? this.store.get(accountId) : null;
7989
+ return {
7990
+ title: recording.title,
7991
+ paused: recording.paused,
7992
+ elapsedMilliseconds: recording.elapsedMilliseconds,
7993
+ at: recording.at,
7994
+ canOpen: recording.canOpen,
7995
+ otherAccount: other ? other.email : null
7996
+ };
7997
+ }
7998
+ recordingAccount(recording) {
7999
+ const page = recording.page;
8000
+ return this.window?.accountFor(page) ?? null;
8001
+ }
8002
+ /**
8003
+ * The menu's meeting items. Opening shows the account that records, switching
8004
+ * to it first, then asks its page to show the meeting's live transcript.
8005
+ */
8006
+ meetingAction(action) {
8007
+ const recording = this.meetings.recording();
8008
+ if (!recording) return;
8009
+ if (action === "stop") {
8010
+ this.meetings.stopRecording();
8011
+ return;
8012
+ }
8013
+ const accountId = this.recordingAccount(recording);
8014
+ if (accountId && accountId !== this.window?.accountId) this.showAccount(accountId);
8015
+ else this.reveal();
8016
+ app3.focus({ steal: true });
8017
+ this.meetings.openRecording(recording);
8018
+ }
7798
8019
  };
7799
8020
  function setOpenAtLogin(config, open) {
7800
8021
  if (process.platform === "linux") {