@dualityinstitute/blink 0.0.3 → 0.0.5

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.
@@ -20,13 +20,19 @@ var CHANNEL_ANSWER_START = "blink:answer-start";
20
20
  var CHANNEL_ANSWER_DELTA = "blink:answer-delta";
21
21
  var CHANNEL_ANSWER_DONE = "blink:answer-done";
22
22
  var CHANNEL_ANSWER_ERROR = "blink:answer-error";
23
- var CHANNEL_QUEUE_CHANGE = "blink:queue-change";
24
23
  var CHANNEL_PRESENTER_LOAD = "blink:presenter-load";
25
24
  var CHANNEL_PRESENTER_SAVE = "blink:presenter-save";
26
25
  var CHANNEL_PRESENTER_DISMISS = "blink:presenter-dismiss";
27
26
  var CHANNEL_PRESENTER_SET_PRIVACY = "blink:presenter-set-privacy";
28
27
  var CHANNEL_PRESENTER_PRIVACY_CHANGED = "blink:presenter-privacy-changed";
29
28
  var CHANNEL_PRESENTER_SCROLL = "blink:presenter-scroll";
29
+ var CHANNEL_SETTINGS_LOAD_PROMPTS = "blink:settings-load-prompts";
30
+ var CHANNEL_SETTINGS_SAVE_PROMPTS = "blink:settings-save-prompts";
31
+ var CHANNEL_SETTINGS_LOAD_ANSWERS = "blink:settings-load-answers";
32
+ var CHANNEL_SETTINGS_DISMISS = "blink:settings-dismiss";
33
+ var CHANNEL_SETTINGS_LOAD_TRANSCRIPTS = "blink:settings-load-transcripts";
34
+ var CHANNEL_SETTINGS_READ_TRANSCRIPT = "blink:settings-read-transcript";
35
+ var CHANNEL_SETTINGS_SHOW_TAB = "blink:settings-show-tab";
30
36
 
31
37
  // src/electron/preload.ts
32
38
  function subscribe(channel, callback) {
@@ -51,9 +57,6 @@ var blinkAPI = {
51
57
  onAnswerError(callback) {
52
58
  subscribe(CHANNEL_ANSWER_ERROR, callback);
53
59
  },
54
- onQueueChange(callback) {
55
- subscribe(CHANNEL_QUEUE_CHANGE, callback);
56
- },
57
60
  abort() {
58
61
  import_electron.ipcRenderer.send(CHANNEL_ABORT);
59
62
  },
@@ -111,5 +114,29 @@ var presenterAPI = {
111
114
  subscribe(CHANNEL_PRESENTER_SCROLL, callback);
112
115
  }
113
116
  };
117
+ var settingsAPI = {
118
+ loadPrompts() {
119
+ return import_electron.ipcRenderer.invoke(CHANNEL_SETTINGS_LOAD_PROMPTS);
120
+ },
121
+ savePrompts(prompts) {
122
+ return import_electron.ipcRenderer.invoke(CHANNEL_SETTINGS_SAVE_PROMPTS, prompts);
123
+ },
124
+ loadAnswers() {
125
+ return import_electron.ipcRenderer.invoke(CHANNEL_SETTINGS_LOAD_ANSWERS);
126
+ },
127
+ loadTranscripts(query) {
128
+ return import_electron.ipcRenderer.invoke(CHANNEL_SETTINGS_LOAD_TRANSCRIPTS, query);
129
+ },
130
+ readTranscript(file) {
131
+ return import_electron.ipcRenderer.invoke(CHANNEL_SETTINGS_READ_TRANSCRIPT, file);
132
+ },
133
+ dismiss() {
134
+ import_electron.ipcRenderer.send(CHANNEL_SETTINGS_DISMISS);
135
+ },
136
+ onShowTab(callback) {
137
+ subscribe(CHANNEL_SETTINGS_SHOW_TAB, callback);
138
+ }
139
+ };
114
140
  import_electron.contextBridge.exposeInMainWorld("blink", blinkAPI);
115
141
  import_electron.contextBridge.exposeInMainWorld("presenter", presenterAPI);
142
+ import_electron.contextBridge.exposeInMainWorld("settings", settingsAPI);
@@ -215,6 +215,23 @@ body {
215
215
  border-color: color-mix(in srgb, var(--fg) 24%, transparent);
216
216
  }
217
217
 
218
+ .meeting-control.is-live {
219
+ color: #fff;
220
+ background: var(--leave-bg, #c0392b);
221
+ border-color: color-mix(in srgb, #c0392b 70%, #000);
222
+ cursor: pointer;
223
+ font-variant-numeric: tabular-nums;
224
+ }
225
+
226
+ .meeting-control.is-live:hover {
227
+ background: color-mix(in srgb, #c0392b 85%, #fff);
228
+ }
229
+
230
+ .meeting-control.is-live .meeting-indicator {
231
+ background: #fff;
232
+ border-color: #fff;
233
+ }
234
+
218
235
  .meeting-control.is-meeting .meeting-indicator {
219
236
  background: currentColor;
220
237
  }
@@ -242,6 +259,46 @@ body {
242
259
  outline-offset: 2px;
243
260
  }
244
261
 
262
+ .runtime-error {
263
+ display: flex;
264
+ align-items: flex-start;
265
+ gap: 8px;
266
+ margin: 6px 12px 0 12px;
267
+ padding: 8px 10px;
268
+ border: 1px solid rgba(200, 60, 60, 0.35);
269
+ border-radius: 8px;
270
+ background: rgba(200, 60, 60, 0.12);
271
+ font-size: 12px;
272
+ line-height: 1.4;
273
+ }
274
+
275
+ .runtime-error[hidden] {
276
+ display: none;
277
+ }
278
+
279
+ .runtime-error-text {
280
+ flex: 1;
281
+ min-width: 0;
282
+ overflow-wrap: anywhere;
283
+ color: #ffd9d9;
284
+ }
285
+
286
+ .runtime-error-close {
287
+ flex: none;
288
+ padding: 0 4px;
289
+ border: 0;
290
+ background: transparent;
291
+ color: #ffd9d9;
292
+ font-size: 14px;
293
+ line-height: 1;
294
+ cursor: pointer;
295
+ opacity: 0.75;
296
+ }
297
+
298
+ .runtime-error-close:hover {
299
+ opacity: 1;
300
+ }
301
+
245
302
  .response-context {
246
303
  display: flex;
247
304
  align-items: center;
@@ -395,35 +452,90 @@ body {
395
452
  opacity: 0.45;
396
453
  }
397
454
 
455
+ .workspace {
456
+ display: flex;
457
+ flex: 1 1 auto;
458
+ min-height: 0;
459
+ }
460
+
398
461
  .transcript-panel {
399
- flex: 0 0 auto;
400
- max-height: 74px;
401
- overflow-y: auto;
402
- padding: 6px 14px 7px 16px;
403
- border-bottom: 1px solid var(--divider);
462
+ display: flex;
463
+ flex-direction: column;
464
+ /* Shrinks below its 400px basis only when the window is too narrow to hold
465
+ both panes, so dragging the popup small never hides the answer entirely. */
466
+ flex: 0 1 400px;
467
+ width: 400px;
468
+ min-width: 200px;
469
+ border-right: 1px solid var(--divider);
404
470
  background: color-mix(in srgb, var(--bg-strong) 12%, transparent);
405
471
  font-size: 11.5px;
406
472
  line-height: 1.35;
473
+ }
474
+
475
+ .transcript-panel[hidden] {
476
+ display: none;
477
+ }
478
+
479
+ .transcript-heading {
480
+ display: flex;
481
+ align-items: baseline;
482
+ justify-content: space-between;
483
+ gap: 8px;
484
+ padding: 7px 12px 6px 16px;
485
+ border-bottom: 1px solid var(--divider);
486
+ }
487
+
488
+ .transcript-heading-label {
489
+ font-size: 9.5px;
490
+ font-weight: 700;
491
+ letter-spacing: 0.06em;
492
+ text-transform: uppercase;
493
+ color: var(--fg-subtle);
494
+ }
495
+
496
+ .transcript-count {
497
+ font-size: 9.5px;
498
+ font-variant-numeric: tabular-nums;
499
+ color: var(--fg-subtle);
500
+ }
501
+
502
+ .transcript-scroll {
503
+ flex: 1 1 auto;
504
+ min-height: 0;
505
+ overflow-y: auto;
506
+ padding: 8px 12px 10px 16px;
407
507
  user-select: text;
408
508
  -webkit-user-select: text;
409
509
  scrollbar-width: thin;
410
510
  scrollbar-color: var(--fg-subtle) transparent;
411
511
  }
412
512
 
413
- .transcript-panel::-webkit-scrollbar {
513
+ .transcript-scroll::-webkit-scrollbar {
414
514
  width: 5px;
415
515
  }
416
516
 
417
- .transcript-panel::-webkit-scrollbar-thumb {
517
+ .transcript-scroll::-webkit-scrollbar-thumb {
418
518
  background: var(--fg-subtle);
419
519
  border-radius: 3px;
420
520
  opacity: 0.4;
421
521
  }
422
522
 
523
+ /* Placeholder lives in CSS so an empty transcript needs no throwaway DOM node. */
524
+ .transcript-lines.is-empty::before {
525
+ content: "Waiting for speech…";
526
+ color: var(--fg-subtle);
527
+ font-style: italic;
528
+ }
529
+
423
530
  .transcript-line {
424
531
  display: flex;
425
- gap: 6px;
426
- margin: 1px 0;
532
+ flex-direction: column;
533
+ gap: 1px;
534
+ margin: 0 0 7px;
535
+ }
536
+
537
+ .transcript-line.is-partial .transcript-text {
538
+ color: var(--fg-subtle);
427
539
  }
428
540
 
429
541
  .transcript-speaker {
@@ -451,11 +563,6 @@ body {
451
563
  overflow-wrap: break-word;
452
564
  }
453
565
 
454
- .transcript-empty {
455
- color: var(--fg-subtle);
456
- font-style: italic;
457
- }
458
-
459
566
  .pill.reconciled-pill {
460
567
  background: var(--pill-reconciled-bg);
461
568
  color: var(--pill-reconciled-fg);
@@ -543,8 +650,11 @@ body {
543
650
 
544
651
  .card-body {
545
652
  position: relative;
546
- flex: 1 1 auto;
653
+ /* The floor is what forces a narrowed window to shrink the transcript column
654
+ first; without it the answer silently collapses to an unusable sliver. */
655
+ flex: 1 1 0;
547
656
  min-height: 0;
657
+ min-width: 280px;
548
658
  overflow-y: auto;
549
659
  padding: 12px 14px 14px 14px;
550
660
  scrollbar-width: thin;
@@ -68,13 +68,17 @@
68
68
  </div>
69
69
  </header>
70
70
 
71
+ <div class="runtime-error" id="runtime-error" hidden role="alert">
72
+ <span class="runtime-error-text" id="runtime-error-text"></span>
73
+ <button type="button" class="runtime-error-close" id="runtime-error-close" aria-label="Dismiss">×</button>
74
+ </div>
75
+
71
76
  <div class="response-context">
72
77
  <div class="source" id="source">
73
78
  <span class="source-label">Ready for a screenshot</span>
74
79
  </div>
75
80
  <div class="status-group">
76
81
  <span class="pill usage-pill" id="usage-pill" hidden role="status"></span>
77
- <span class="pill queue-pill" id="queue-pill" hidden aria-hidden="true"></span>
78
82
  <span class="pill reconciled-pill" id="reconciled-pill" hidden aria-hidden="true">recovered</span>
79
83
  <span class="pill stopped-pill" id="stopped-pill" hidden aria-hidden="true">stopped</span>
80
84
  </div>
@@ -108,14 +112,22 @@
108
112
  >Ask</button>
109
113
  </div>
110
114
 
111
- <div
115
+ <div class="workspace" id="workspace">
116
+ <aside
112
117
  class="transcript-panel"
113
118
  id="transcript-panel"
114
119
  aria-label="Live meeting transcript"
115
120
  hidden
116
121
  >
117
- <div class="transcript-lines" id="transcript-lines"></div>
118
- </div>
122
+ <div class="transcript-heading">
123
+ <span class="transcript-heading-label">Live transcript</span>
124
+ <span class="transcript-count" id="transcript-count"></span>
125
+ </div>
126
+ <div class="transcript-scroll" id="transcript-scroll">
127
+ <div class="transcript-lines" id="transcript-lines"></div>
128
+ <div class="transcript-volatile" id="transcript-volatile"></div>
129
+ </div>
130
+ </aside>
119
131
 
120
132
  <section class="card-body" id="body">
121
133
  <div class="answering" id="answering" hidden>
@@ -135,6 +147,7 @@
135
147
  <div class="error-message" id="error-message"></div>
136
148
  </div>
137
149
  </section>
150
+ </div>
138
151
  </main>
139
152
  <script src="./popup.js"></script>
140
153
  </body>
@@ -3736,10 +3736,10 @@ var state = {
3736
3736
  meeting: false,
3737
3737
  meetingStarting: false,
3738
3738
  meetingAsking: false,
3739
- transcriptAvailable: false,
3739
+ monitoring: false,
3740
3740
  screenshotCount: 0,
3741
3741
  showTranscript: true,
3742
- transcriptLines: []
3742
+ meetingStartedAt: null
3743
3743
  };
3744
3744
  function el(id) {
3745
3745
  const node = document.getElementById(id);
@@ -3762,11 +3762,13 @@ var dom = {
3762
3762
  askMeeting: el("ask-meeting"),
3763
3763
  transcriptToggle: el("transcript-toggle"),
3764
3764
  transcriptPanel: el("transcript-panel"),
3765
+ transcriptScroll: el("transcript-scroll"),
3765
3766
  transcriptLines: el("transcript-lines"),
3767
+ transcriptVolatile: el("transcript-volatile"),
3768
+ transcriptCount: el("transcript-count"),
3766
3769
  privacy: el("privacy"),
3767
3770
  privacyLabel: el("privacy-label"),
3768
3771
  usagePill: el("usage-pill"),
3769
- queuePill: el("queue-pill"),
3770
3772
  reconciledPill: el("reconciled-pill"),
3771
3773
  stoppedPill: el("stopped-pill"),
3772
3774
  abort: el("abort"),
@@ -3775,6 +3777,9 @@ var dom = {
3775
3777
  answer: el("answer"),
3776
3778
  error: el("error"),
3777
3779
  errorMessage: el("error-message"),
3780
+ runtimeError: el("runtime-error"),
3781
+ runtimeErrorText: el("runtime-error-text"),
3782
+ runtimeErrorClose: el("runtime-error-close"),
3778
3783
  body: el("body")
3779
3784
  };
3780
3785
  function setState(next) {
@@ -3816,80 +3821,144 @@ function updatePrivacy(hidden) {
3816
3821
  }
3817
3822
  function updateMonitoring(payload) {
3818
3823
  const active = payload.monitoring || payload.monitoringStarting;
3824
+ state.monitoring = payload.monitoring;
3819
3825
  dom.monitoring.checked = active;
3820
3826
  dom.monitoring.disabled = payload.monitoringStarting;
3821
3827
  dom.monitoringLabel.textContent = payload.monitoringStarting ? "Starting…" : "Monitor";
3822
3828
  dom.monitoringControl.classList.toggle("is-monitoring", active);
3823
3829
  dom.monitoringControl.classList.toggle("is-starting", payload.monitoringStarting);
3824
3830
  dom.monitoringControl.setAttribute("aria-busy", payload.monitoringStarting ? "true" : "false");
3831
+ renderMeetingContext();
3825
3832
  }
3833
+ var transcriptRows = new Map;
3826
3834
  function renderTranscript() {
3827
3835
  const expanded = state.showTranscript;
3828
3836
  dom.transcriptToggle.setAttribute("aria-expanded", expanded ? "true" : "false");
3829
- dom.transcriptPanel.hidden = !state.meeting || !expanded;
3830
- if (dom.transcriptPanel.hidden)
3831
- return;
3832
- if (state.transcriptLines.length === 0) {
3833
- const empty = document.createElement("div");
3834
- empty.className = "transcript-empty";
3835
- empty.textContent = "Waiting for speech…";
3836
- dom.transcriptLines.replaceChildren(empty);
3837
- return;
3838
- }
3839
- const rows = state.transcriptLines.map((line) => {
3840
- const row = document.createElement("div");
3841
- row.className = "transcript-line";
3842
- const speaker = document.createElement("span");
3843
- speaker.className = `transcript-speaker is-${line.speaker}`;
3844
- speaker.textContent = line.speaker === "them" ? "Them" : "You";
3845
- const text2 = document.createElement("span");
3846
- text2.className = "transcript-text";
3847
- text2.textContent = line.text;
3848
- row.append(speaker, text2);
3849
- return row;
3850
- });
3851
- dom.transcriptLines.replaceChildren(...rows);
3852
- pinTranscriptToBottom();
3837
+ const visible = state.meeting && expanded;
3838
+ dom.transcriptPanel.hidden = !visible;
3839
+ dom.root.classList.toggle("has-transcript", visible);
3840
+ updateTranscriptPlaceholder();
3841
+ }
3842
+ function buildTranscriptRow(line) {
3843
+ const row = document.createElement("div");
3844
+ row.className = "transcript-line";
3845
+ const speaker = document.createElement("span");
3846
+ speaker.className = `transcript-speaker is-${line.speaker}`;
3847
+ speaker.textContent = line.speaker === "them" ? "Them" : "You";
3848
+ const text2 = document.createElement("span");
3849
+ text2.className = "transcript-text";
3850
+ text2.textContent = line.text;
3851
+ row.append(speaker, text2);
3852
+ return row;
3853
+ }
3854
+ function setRowText(row, text2) {
3855
+ const node = row.querySelector(".transcript-text");
3856
+ if (node !== null)
3857
+ node.textContent = text2;
3858
+ }
3859
+ function updateTranscriptPlaceholder() {
3860
+ const empty = transcriptRows.size === 0 && dom.transcriptVolatile.childElementCount === 0;
3861
+ dom.transcriptLines.classList.toggle("is-empty", empty);
3862
+ dom.transcriptCount.textContent = transcriptRows.size === 0 ? "" : String(transcriptRows.size);
3863
+ }
3864
+ function isTranscriptAtBottom() {
3865
+ const node = dom.transcriptScroll;
3866
+ return node.scrollHeight - node.scrollTop - node.clientHeight < 40;
3853
3867
  }
3854
3868
  function pinTranscriptToBottom() {
3855
- dom.transcriptPanel.scrollTop = dom.transcriptPanel.scrollHeight;
3869
+ dom.transcriptScroll.scrollTop = dom.transcriptScroll.scrollHeight;
3856
3870
  }
3857
3871
  function onTranscriptUpdate(payload) {
3858
- state.transcriptLines = Array.isArray(payload.lines) ? payload.lines : [];
3859
- renderTranscript();
3872
+ if (payload.reset === true) {
3873
+ transcriptRows.clear();
3874
+ dom.transcriptLines.replaceChildren();
3875
+ dom.transcriptVolatile.replaceChildren();
3876
+ }
3877
+ const follow = isTranscriptAtBottom();
3878
+ for (const line of payload.changed ?? []) {
3879
+ const existing = transcriptRows.get(line.id);
3880
+ if (existing !== undefined) {
3881
+ setRowText(existing, line.text);
3882
+ continue;
3883
+ }
3884
+ const row = buildTranscriptRow(line);
3885
+ transcriptRows.set(line.id, row);
3886
+ dom.transcriptLines.append(row);
3887
+ }
3888
+ dom.transcriptVolatile.replaceChildren(...(payload.volatile ?? []).map((line) => {
3889
+ const row = buildTranscriptRow(line);
3890
+ row.classList.add("is-partial");
3891
+ return row;
3892
+ }));
3893
+ updateTranscriptPlaceholder();
3894
+ if (follow)
3895
+ pinTranscriptToBottom();
3896
+ }
3897
+ function clearMeetingAnswer() {
3898
+ const label = document.createElement("span");
3899
+ label.className = "source-label";
3900
+ label.textContent = "Ready for a screenshot";
3901
+ dom.source.replaceChildren(label);
3902
+ delete dom.source.dataset["source"];
3903
+ dom.answer.replaceChildren();
3904
+ dom.errorMessage.textContent = "";
3905
+ state.streamBuffer = "";
3906
+ setReconciled(false);
3907
+ setStopped(false);
3908
+ setState("idle");
3860
3909
  }
3861
3910
  function updateMeeting(payload) {
3911
+ const wasMeeting = state.meeting;
3862
3912
  state.meeting = payload.meeting;
3863
3913
  state.meetingStarting = payload.meetingStarting;
3864
3914
  state.meetingAsking = payload.asking;
3865
- state.transcriptAvailable = payload.transcriptAvailable;
3866
3915
  state.screenshotCount = Math.max(0, Math.floor(payload.screenshotCount));
3867
3916
  state.showTranscript = payload.showTranscript;
3868
3917
  renderTranscript();
3918
+ state.meetingStartedAt = payload.startedAt ?? null;
3869
3919
  const active = payload.meeting || payload.meetingStarting;
3870
3920
  dom.meeting.checked = active;
3871
3921
  dom.meeting.disabled = payload.meetingStarting;
3872
- dom.meetingLabel.textContent = payload.meetingStarting ? "Starting…" : "Meeting";
3873
3922
  dom.meetingControl.classList.toggle("is-meeting", active);
3923
+ dom.meetingControl.classList.toggle("is-live", payload.meeting);
3924
+ dom.meetingControl.title = payload.meeting ? "Leave the meeting and stop recording" : "Capture private Meeting context";
3925
+ renderMeetingClock();
3874
3926
  dom.meetingControl.classList.toggle("is-starting", payload.meetingStarting);
3875
3927
  dom.meetingControl.setAttribute("aria-busy", payload.meetingStarting ? "true" : "false");
3876
3928
  renderMeetingContext();
3929
+ if (wasMeeting && !payload.meeting)
3930
+ clearMeetingAnswer();
3931
+ }
3932
+ function elapsedLabel(startedAt) {
3933
+ const total = Math.max(0, Math.floor((Date.now() - startedAt) / 1000));
3934
+ const hours = Math.floor(total / 3600);
3935
+ const minutes = Math.floor(total % 3600 / 60);
3936
+ const seconds = total % 60;
3937
+ const pad = (value) => String(value).padStart(2, "0");
3938
+ return hours > 0 ? `${hours}:${pad(minutes)}:${pad(seconds)}` : `${minutes}:${pad(seconds)}`;
3939
+ }
3940
+ function renderMeetingClock() {
3941
+ if (state.meetingStarting) {
3942
+ dom.meetingLabel.textContent = "Starting…";
3943
+ return;
3944
+ }
3945
+ if (!state.meeting || state.meetingStartedAt === null) {
3946
+ dom.meetingLabel.textContent = "Meeting";
3947
+ return;
3948
+ }
3949
+ dom.meetingLabel.textContent = `Leave · ${elapsedLabel(state.meetingStartedAt)}`;
3877
3950
  }
3951
+ window.setInterval(renderMeetingClock, 1000);
3878
3952
  function renderMeetingContext() {
3879
- const visible = state.meeting || state.meetingStarting;
3880
- const hasContext = state.transcriptAvailable || state.screenshotCount > 0;
3953
+ const visible = state.meeting || state.meetingStarting || state.monitoring;
3881
3954
  const answerRunning = state.currentState === "answering" || state.currentState === "streaming";
3882
3955
  dom.meetingContext.hidden = !visible;
3883
3956
  dom.root.classList.toggle("has-meeting", visible);
3884
- const details = [];
3885
- if (state.transcriptAvailable)
3886
- details.push("Transcript ready");
3887
- if (state.screenshotCount > 0) {
3888
- details.push(`${state.screenshotCount} screenshot${state.screenshotCount === 1 ? "" : "s"}`);
3889
- }
3890
- dom.contextCopy.textContent = details.length > 0 ? details.join(" · ") : state.meetingStarting ? "Preparing private context…" : "No context yet";
3891
- dom.contextCopy.classList.toggle("has-context", hasContext);
3892
- dom.askMeeting.disabled = !state.meeting || state.meetingStarting || state.meetingAsking || answerRunning || !hasContext;
3957
+ const staged = state.screenshotCount;
3958
+ dom.contextCopy.textContent = state.meetingStarting ? "Preparing private context…" : staged > 0 ? `${staged} screenshot${staged === 1 ? "" : "s"}` : state.meeting ? "Transcript only" : "No screenshots yet";
3959
+ dom.contextCopy.classList.toggle("has-context", staged > 0);
3960
+ const askable = state.meeting || state.monitoring && staged > 0;
3961
+ dom.askMeeting.disabled = !askable || state.meetingStarting || state.meetingAsking || answerRunning;
3893
3962
  dom.askMeeting.textContent = state.meetingAsking ? "Asking…" : "Ask";
3894
3963
  dom.askMeeting.setAttribute("aria-busy", state.meetingAsking ? "true" : "false");
3895
3964
  }
@@ -3910,11 +3979,22 @@ function updateUsage(payload) {
3910
3979
  dom.usagePill.classList.toggle("is-near-limit", usage.status === "active" && !exhausted && usage.answersUsed >= usage.answersLimit * 0.9);
3911
3980
  dom.usagePill.classList.toggle("is-exhausted", usage.status !== "active" || exhausted);
3912
3981
  }
3982
+ function updateRuntimeError(header) {
3983
+ const message = header.lastError;
3984
+ if (message === undefined || message === "") {
3985
+ dom.runtimeError.hidden = true;
3986
+ dom.runtimeErrorText.textContent = "";
3987
+ return;
3988
+ }
3989
+ dom.runtimeErrorText.textContent = message;
3990
+ dom.runtimeError.hidden = false;
3991
+ }
3913
3992
  function applyHeader(header) {
3914
3993
  updatePrivacy(header.hideFromCapture);
3915
3994
  updateMonitoring(header);
3916
3995
  updateMeeting(header);
3917
3996
  updateUsage(header);
3997
+ updateRuntimeError(header);
3918
3998
  }
3919
3999
  async function loadHeader() {
3920
4000
  if (window.blink === undefined)
@@ -3944,16 +4024,6 @@ function maybeScrollToBottom() {
3944
4024
  return;
3945
4025
  dom.body.scrollTop = dom.body.scrollHeight;
3946
4026
  }
3947
- function renderQueue(count) {
3948
- if (count > 0) {
3949
- dom.queuePill.textContent = `${count} queued`;
3950
- dom.queuePill.hidden = false;
3951
- dom.queuePill.setAttribute("aria-hidden", "false");
3952
- } else {
3953
- dom.queuePill.hidden = true;
3954
- dom.queuePill.setAttribute("aria-hidden", "true");
3955
- }
3956
- }
3957
4027
  function setReconciled(reconciled) {
3958
4028
  dom.reconciledPill.hidden = !reconciled;
3959
4029
  dom.reconciledPill.setAttribute("aria-hidden", reconciled ? "false" : "true");
@@ -4003,10 +4073,6 @@ function onError(payload) {
4003
4073
  setStopped(false);
4004
4074
  setState("error");
4005
4075
  }
4006
- function onQueueChange(payload) {
4007
- const count = Number.isFinite(payload.pendingCount) ? Math.max(0, Math.floor(payload.pendingCount)) : 0;
4008
- renderQueue(count);
4009
- }
4010
4076
  function dismiss() {
4011
4077
  if (window.blink !== undefined) {
4012
4078
  window.blink.dismiss();
@@ -4031,7 +4097,6 @@ dom.meeting.addEventListener("change", () => {
4031
4097
  meetingStarting: false,
4032
4098
  asking: state.meetingAsking,
4033
4099
  showTranscript: state.showTranscript,
4034
- transcriptAvailable: state.transcriptAvailable,
4035
4100
  screenshotCount: state.screenshotCount
4036
4101
  });
4037
4102
  window.blink?.setMeeting(meeting);
@@ -4056,6 +4121,9 @@ dom.transcriptToggle.addEventListener("click", () => {
4056
4121
  renderTranscript();
4057
4122
  window.blink?.setShowTranscript(state.showTranscript);
4058
4123
  });
4124
+ dom.runtimeErrorClose.addEventListener("click", () => {
4125
+ dom.runtimeError.hidden = true;
4126
+ });
4059
4127
  dom.abort.addEventListener("click", () => {
4060
4128
  if (window.blink === undefined || dom.abort.disabled)
4061
4129
  return;
@@ -4076,7 +4144,6 @@ function bootstrap() {
4076
4144
  window.blink.onAnswerDelta(onDelta);
4077
4145
  window.blink.onAnswerDone(onDone);
4078
4146
  window.blink.onAnswerError(onError);
4079
- window.blink.onQueueChange(onQueueChange);
4080
4147
  window.blink.onPrivacyChanged(updatePrivacy);
4081
4148
  window.blink.onMonitoringChanged(updateMonitoring);
4082
4149
  window.blink.onMeetingChanged(updateMeeting);