@gotcos/glasses-server 6.99.29 → 7.0.7

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 (80) hide show
  1. package/.env.example +18 -0
  2. package/CHANGELOG.md +475 -0
  3. package/README.md +3 -1
  4. package/managed-runtime-contract.json +3 -0
  5. package/package.json +1 -1
  6. package/server/index.ts +29 -7
  7. package/server/lib/agent-session-output-images.ts +627 -49
  8. package/server/lib/agent-session-store.ts +24 -11
  9. package/server/lib/agent-session-tool-images.ts +224 -0
  10. package/server/lib/agent-session-turns.ts +19 -2
  11. package/server/lib/api-auth.ts +6 -0
  12. package/server/lib/ask-work-plan.ts +3 -1
  13. package/server/lib/calendar-source.ts +491 -0
  14. package/server/lib/codex-bridge.ts +4 -1
  15. package/server/lib/codex-run-ledger.ts +29 -0
  16. package/server/lib/context-builder.ts +2 -1
  17. package/server/lib/drop-p2-runtime.ts +1 -1
  18. package/server/lib/drop-store.ts +10 -4
  19. package/server/lib/learning-needs-you.ts +24 -7
  20. package/server/lib/live-events.ts +533 -0
  21. package/server/lib/local-path-redact.ts +272 -0
  22. package/server/lib/meeting-ask-orientation.ts +40 -6
  23. package/server/lib/meeting-ask-runner.ts +30 -10
  24. package/server/lib/morning-brief-runtime.ts +4 -2
  25. package/server/lib/native-mac-caller.ts +29 -0
  26. package/server/lib/ping-engine.ts +1 -1
  27. package/server/lib/ping-sources.ts +11 -6
  28. package/server/lib/preview-content.ts +172 -0
  29. package/server/lib/preview-error-code.ts +10 -0
  30. package/server/lib/preview-folders-setting.ts +133 -0
  31. package/server/lib/preview-log-throttle.ts +18 -0
  32. package/server/lib/preview-thumbs.ts +139 -0
  33. package/server/lib/secret-path-guard.ts +80 -0
  34. package/server/lib/session-files-setting.ts +88 -0
  35. package/server/lib/session-links.ts +280 -0
  36. package/server/lib/thread-turn-queue.ts +10 -0
  37. package/server/lib/today-brief.ts +119 -28
  38. package/server/lib/web-memories-bridge.ts +3 -1
  39. package/server/lib/work-activity.ts +56 -6
  40. package/server/lib/work-session-activity.ts +260 -0
  41. package/server/lib/your-move-count.ts +9 -3
  42. package/server/routes/agent-session-bindings.ts +13 -0
  43. package/server/routes/agent-session-stream.ts +16 -0
  44. package/server/routes/agent-sessions.ts +146 -11
  45. package/server/routes/ask-work-plan.ts +4 -1
  46. package/server/routes/claude-sessions.ts +5 -1
  47. package/server/routes/health.ts +35 -0
  48. package/server/routes/live-events.ts +329 -0
  49. package/server/routes/permission-broker.ts +69 -5
  50. package/server/routes/session-preview.ts +200 -0
  51. package/server/routes/thread-turn-queue.ts +207 -3
  52. package/server/routes/today-brief.ts +44 -9
  53. package/server/routes/welcome-context.ts +23 -3
  54. package/server/routes/work-board.ts +9 -2
  55. package/server/web-control/app.css +79 -9
  56. package/server/web-control/app.js +1543 -65
  57. package/server/web-control/control.css +37 -6
  58. package/server/web-control/index.html +8 -2
  59. package/server/web-control/memory-review.js +100 -0
  60. package/server/web-control/panes/cos-chat.css +51 -6
  61. package/server/web-control/panes/cos-chat.js +268 -16
  62. package/server/web-control/panes/meetings.css +23 -4
  63. package/server/web-control/panes/meetings.js +25 -9
  64. package/server/web-control/panes/memories.css +18 -3
  65. package/server/web-control/panes/memories.js +130 -50
  66. package/server/web-control/panes/messages.js +21 -2
  67. package/server/web-control/panes/preview.js +318 -0
  68. package/server/web-control/panes/questions.css +20 -4
  69. package/server/web-control/panes/questions.js +127 -26
  70. package/server/web-control/panes/reader-media.js +409 -23
  71. package/server/web-control/panes/sessions.css +50 -10
  72. package/server/web-control/panes/settings.js +96 -5
  73. package/server/web-control/panes/speakers.css +3 -5
  74. package/server/web-control/panes/speakers.js +7 -4
  75. package/server/web-control/panes/threads.js +3 -2
  76. package/server/web-control/panes/today-actions.css +49 -18
  77. package/server/web-control/panes/today-actions.js +217 -50
  78. package/server/web-control/panes/work.css +11 -1
  79. package/server/web-control/panes/work.js +10 -2
  80. package/server/web-control/shell.js +20 -14
@@ -47,9 +47,11 @@
47
47
  stopped: false,
48
48
  inflight: false, // a poll is on the wire
49
49
  again: false, // something asked for a poll while one was out: one more as soon as it lands
50
- lastPollAt: 0 // when the last poll went out (Date.now)
50
+ lastPollAt: 0, // when the last poll went out (Date.now)
51
+ load: 'unread' // W13: 'unread' (no read yet), 'ok' (the last read was the server's answer) or 'failed' (it was not)
51
52
  };
52
53
  var DEFAULT_POLL_MS = 10000;
54
+ var POLL_TIMEOUT_MS = 15000;
53
55
  // 6.99.27 (Miles 11/07 09:29, "a big delay to see the questions on the homepage"): while this tab is visible on
54
56
  // Today it reads every 3 s, whatever the server's idle pace (10 s). Before, a question parked just after an idle read
55
57
  // waited up to 10 s plus the round trip for the next one. Hidden, the page reads nothing (visibilitychange below).
@@ -133,6 +135,19 @@
133
135
  // QA 6.99.27 (N4): the read asked for while this one was out goes through nudge, so it waits out the 1 s gap too.
134
136
  if (Q.again) { Q.again = false; nudge(); }
135
137
  }
138
+ // W13 (QA week of 11/01-11/08): whether the last read was the server's answer. A failed read leaves no cards, and Today's
139
+ // sheet read "no card" as "at the Mac's prompt" (a Claude session) with no way to read again. Each change tells the
140
+ // mounted sheets (their onChange), never on a read that changed nothing.
141
+ function noteLoad(state) {
142
+ if (Q.load === state) return;
143
+ Q.load = state;
144
+ tellMounts();
145
+ }
146
+ function tellMounts() {
147
+ mounts.forEach(function (m) {
148
+ if (m.onChange) { try { m.onChange(itemsFor(m.sessionId).length, atMacFor(m.sessionId).length); } catch (_e) {} }
149
+ });
150
+ }
136
151
  function poll() {
137
152
  clearTimeout(Q.timer);
138
153
  if (Q.stopped || document.hidden || !signedIn()) return;
@@ -140,20 +155,44 @@
140
155
  if (Q.inflight) { Q.again = true; return; }
141
156
  Q.inflight = true; Q.again = false; Q.lastPollAt = Date.now();
142
157
  capForPoll().then(function (cap) {
143
- if (!usable(cap)) { Q.again = false; landed(); clearCards(); schedule(60000); return; }
144
- return fetch(pollUrl(), { headers: { 'X-COS-Web': '1' }, credentials: 'same-origin', cache: 'no-store' }).then(function (res) {
145
- if (res.status === 401) { Q.again = false; landed(); Q.stopped = true; clearCards(); return; }
146
- if (!res.ok) { schedule(DEFAULT_POLL_MS); landed(); return; }
158
+ if (!usable(cap)) { Q.again = false; landed(); clearCards(); schedule(60000); noteLoad(cap ? 'ok' : 'failed'); return; }
159
+ // 6.99.32 (QA 6.99.30 canary W1): a Mac that hangs (asleep, a stale Tailscale route) never answered and never
160
+ // refused, so this read stayed in flight forever: no more polls, and the Mac never marked down. It gives up after
161
+ // POLL_TIMEOUT_MS, says the Mac is not answering, and polls again. The limit covers the body too (QA 6.99.32
162
+ // Skeptic W-1): a Mac that sleeps between the headers and the body otherwise held the poll for good.
163
+ var ctrl = typeof AbortController === 'function' ? new AbortController() : null;
164
+ var limit = ctrl ? setTimeout(function () { ctrl.abort(); }, POLL_TIMEOUT_MS) : null;
165
+ return fetch(pollUrl(), { headers: { 'X-COS-Web': '1' }, credentials: 'same-origin', cache: 'no-store', signal: ctrl ? ctrl.signal : undefined }).then(function (res) {
166
+ // QA 6.99.30 (Skeptic): this 3 s read is what Sessions and Meetings pages keep running, so it tells app.js the Mac
167
+ // answered (any status) or did not (below); otherwise a "down" mark stranded Submit, Allow and Deny for minutes.
168
+ if (W.noteServer) W.noteServer(true);
169
+ if (res.status === 401) { clearTimeout(limit); Q.again = false; landed(); Q.stopped = true; clearCards(); return; }
170
+ if (!res.ok) { clearTimeout(limit); schedule(DEFAULT_POLL_MS); landed(); noteLoad('failed'); return; }
147
171
  return res.json().then(function (d) {
148
- if (!d || d.enabled === false) { Q.again = false; landed(); clearCards(); schedule(60000); return; }
172
+ clearTimeout(limit);
173
+ if (!d || d.enabled === false) { Q.again = false; landed(); clearCards(); schedule(60000); noteLoad('ok'); return; }
149
174
  // Painted as soon as the read lands: take() draws the cards and repaints Today's Needs you and the pet.
175
+ // The state first, so a redraw take() makes already reads 'ok'; a read that changed no card redraws nothing, so
176
+ // the sheets still hear that the failure is over.
177
+ var was = Q.load;
178
+ Q.load = 'ok';
150
179
  take(Array.isArray(d.items) ? d.items : [], Array.isArray(d.settled) ? d.settled : []);
180
+ if (was !== 'ok') tellMounts();
151
181
  var every = Number(d.pollIntervalMs) > 0 ? Number(d.pollIntervalMs) : DEFAULT_POLL_MS;
152
182
  schedule(nextWait(every));
153
183
  landed();
184
+ }, function () {
185
+ // The body never finished (cut off, or the limit fired mid-body). The Mac did answer the headers, so it is not
186
+ // marked down; the poll is freed and goes again.
187
+ clearTimeout(limit); schedule(DEFAULT_POLL_MS); landed(); noteLoad('failed');
154
188
  });
155
- }, function () { schedule(DEFAULT_POLL_MS); landed(); });
156
- }).catch(function () { schedule(DEFAULT_POLL_MS); landed(); });
189
+ }, function () {
190
+ // No answer at all: refused, unreachable, or the limit fired before the headers (the only abort this read has).
191
+ clearTimeout(limit);
192
+ if (W.noteServer) W.noteServer(false);
193
+ schedule(DEFAULT_POLL_MS); landed(); noteLoad('failed');
194
+ });
195
+ }).catch(function () { schedule(DEFAULT_POLL_MS); landed(); noteLoad('failed'); });
157
196
  }
158
197
  // Read now, unless a read went out under NUDGE_GAP_MS ago (then at the end of that gap) or one is on the wire (then
159
198
  // once more when it lands). Today calls this when a session goes to waiting, so its question shows at once.
@@ -190,8 +229,9 @@
190
229
  var ids = {}; pending.forEach(function (i) { ids[i.id] = 1; });
191
230
  Object.keys(Q.picks).forEach(function (id) { if (!ids[id]) { delete Q.picks[id]; delete Q.armed[id]; delete Q.notes[id]; delete Q.answerIds[id]; delete Q.missing[id]; } });
192
231
  pending.forEach(function (i) {
193
- if (!Q.picks[i.id]) Q.picks[i.id] = (i.questions || []).map(function () { return { labels: [], other: '' }; });
232
+ if (!Q.picks[i.id]) Q.picks[i.id] = keptPicks(i) || (i.questions || []).map(function () { return { labels: [], other: '' }; });
194
233
  });
234
+ forgetPicks(ids);
195
235
  Q.items = pending;
196
236
  // A card that is already drawn is never redrawn by a poll: a half-typed Other answer stays as it is.
197
237
  if (before !== after) { render(); changed(); }
@@ -295,8 +335,9 @@
295
335
  // always answers: it never greys out for a missing answer, it goes to it.
296
336
  '<div class="sq-foot" data-sq-foot="' + esc(item.id) + '">' +
297
337
  '<p class="sq-progress" data-sq-progress="' + esc(item.id) + '" role="status"' + (Q.missing[item.id] != null && !complete(item) ? ' data-alert="1"' : '') + '>' + esc(progressText(item)) + '</p>' +
298
- '<div class="sq-acts"><button type="button" class="cx-btn primary sq-send" data-sq="send" data-sq-id="' + esc(item.id) + '"' + (sending ? ' disabled' : '') + '>' + sendText(item.id) + '</button>' +
299
- '<button type="button" class="link-btn sq-leave" data-sq="leave" data-sq-id="' + esc(item.id) + '"' + (sending ? ' disabled' : '') + '>Leave for the Mac</button></div>' +
338
+ '<div class="sq-acts"><button type="button" class="cx-btn primary sq-send" data-sq="send" data-sq-id="' + esc(item.id) + '"' + (sending || offline() ? ' disabled' : '') + '>' + sendText(item.id) + '</button>' +
339
+ '<button type="button" class="link-btn sq-leave" data-sq="leave" data-sq-id="' + esc(item.id) + '"' + (sending || offline() ? ' disabled' : '') + '>Leave for the Mac</button></div>' +
340
+ (offline() ? '<p class="sq-offline" role="status">' + esc(OFFLINE_LINE) + '</p>' : '') +
300
341
  noteHtml(item) + '</div></article>';
301
342
  }
302
343
  function approvalCard(item) {
@@ -307,9 +348,10 @@
307
348
  '<header class="sq-head"><h2 class="sq-title">' + esc(engine(item)) + ' wants to use ' + esc(a.tool || item.tool || 'a tool') + '</h2><span class="sq-secs" data-sq-secs="' + esc(item.id) + '">' + esc(timeLeft(secondsLeft(item))) + '</span></header>' +
308
349
  '<pre class="sq-command">' + esc(a.summary || 'No details were sent with this request.') + '</pre>' +
309
350
  (a.detail && a.detail !== a.summary ? '<details class="sq-details"><summary>Full command</summary><pre class="sq-command">' + esc(a.detail) + '</pre></details>' : '') +
310
- '<div class="sq-acts"><button type="button" class="cx-btn primary" data-sq="allow" data-sq-id="' + esc(item.id) + '"' + (sending ? ' disabled' : '') + '>' + (armed === 'allow' ? 'Tap again to allow' : 'Allow once') + '</button>' +
311
- '<button type="button" class="cx-btn sq-deny" data-sq="deny" data-sq-id="' + esc(item.id) + '"' + (sending ? ' disabled' : '') + '>' + (armed === 'deny' ? 'Tap again to deny' : 'Deny') + '</button>' +
312
- '<button type="button" class="link-btn sq-leave" data-sq="leave" data-sq-id="' + esc(item.id) + '"' + (sending ? ' disabled' : '') + '>Leave for the Mac</button></div>' +
351
+ '<div class="sq-acts"><button type="button" class="cx-btn primary" data-sq="allow" data-sq-id="' + esc(item.id) + '"' + (sending || offline() ? ' disabled' : '') + '>' + (armed === 'allow' ? 'Tap again to allow' : 'Allow once') + '</button>' +
352
+ '<button type="button" class="cx-btn sq-deny" data-sq="deny" data-sq-id="' + esc(item.id) + '"' + (sending || offline() ? ' disabled' : '') + '>' + (armed === 'deny' ? 'Tap again to deny' : 'Deny') + '</button>' +
353
+ '<button type="button" class="link-btn sq-leave" data-sq="leave" data-sq-id="' + esc(item.id) + '"' + (sending || offline() ? ' disabled' : '') + '>Leave for the Mac</button></div>' +
354
+ (offline() ? '<p class="sq-offline" role="status">' + esc(OFFLINE_LINE) + '</p>' : '') +
313
355
  noteHtml(item) + '</article>';
314
356
  }
315
357
  // A question or an approval the Mac's own prompt waits on: what it asks when this page saw it held, and where to answer.
@@ -356,7 +398,11 @@
356
398
  var active = document.activeElement;
357
399
  var focus = active && active.hasAttribute && active.hasAttribute('data-sq-other') && m.el.contains && m.el.contains(active) ? { id: active.getAttribute('data-sq-id'), q: active.getAttribute('data-q'), start: active.selectionStart, end: active.selectionEnd } : null;
358
400
  var list = itemsFor(m.sessionId), mac = atMacFor(m.sessionId);
359
- m.el.innerHTML = list.map(function (i) { return i.kind === 'approval' ? approvalCard(i) : questionCard(i, m.scope); }).join('') + mac.map(macCard).join('');
401
+ var cards = list.map(function (i) { return i.kind === 'approval' ? approvalCard(i) : questionCard(i, m.scope); }).join('') + mac.map(macCard).join('');
402
+ // The page's own host (over every pane): folded it is one line until tapped (questions.css .sq-banner, 6.99.30).
403
+ if (m.el === host && list.length) cards = bannerHtml(list) + '<div class="sq-cards' + (Q.bannerOpen ? ' open' : '') + '" id="sq-cards">' + cards + '</div>';
404
+ else if (m.el === host) Q.bannerOpen = false;
405
+ m.el.innerHTML = cards;
360
406
  m.el.hidden = !list.length && !mac.length;
361
407
  if (focus) {
362
408
  var box = m.el.querySelector('[data-sq-other][data-sq-id="' + focus.id + '"][data-q="' + focus.q + '"]');
@@ -364,6 +410,17 @@
364
410
  }
365
411
  if (m.onChange) { try { m.onChange(list.length, mac.length); } catch (_e) {} }
366
412
  }
413
+ // 6.99.30 (canary): "1 question for you. Answer", one line over the pane on a folded phone; a tap opens the cards.
414
+ function bannerHtml(list) {
415
+ var qs = list.filter(function (i) { return i.kind !== 'approval'; }).length, n = list.length;
416
+ var noun = qs === n ? (n === 1 ? 'question' : 'questions') : qs === 0 ? (n === 1 ? 'approval' : 'approvals') : (n === 1 ? 'request' : 'requests');
417
+ return '<button type="button" class="sq-banner" data-sq="banner" aria-expanded="' + (Q.bannerOpen ? 'true' : 'false') + '" aria-controls="sq-cards">' +
418
+ '<span class="sq-banner-text">' + esc(n + ' ' + noun + ' for you.') + '</span><span class="sq-banner-act">' + (Q.bannerOpen ? 'Hide' : 'Answer') + '</span></button>';
419
+ }
420
+ // 6.99.30 (canary): with the Mac not answering (app.js noteServer), Submit, Allow and Deny wait and say why, instead of
421
+ // offering a tap that cannot land.
422
+ function offline() { return !!(W.serverUp && W.serverUp() === false); }
423
+ var OFFLINE_LINE = 'Your Mac is not answering. You can answer here once it is back.';
367
424
  function render() {
368
425
  hostsOf().forEach(drawInto);
369
426
  clearInterval(Q.tick);
@@ -384,7 +441,7 @@
384
441
  }
385
442
  function patchOne(card, id, item) {
386
443
  var send = card.querySelector('[data-sq="send"]');
387
- if (send) { send.disabled = !!Q.sending[id]; send.textContent = sendText(id); }
444
+ if (send) { send.disabled = !!Q.sending[id] || offline(); send.textContent = sendText(id); }
388
445
  var progress = card.querySelector('[data-sq-progress]');
389
446
  if (progress && item.kind === 'question') {
390
447
  progress.textContent = progressText(item);
@@ -487,6 +544,7 @@
487
544
  function onClick(e) {
488
545
  var b = e.target.closest('[data-sq]');
489
546
  if (!b || b.disabled) return;
547
+ if (b.getAttribute('data-sq') === 'banner') { Q.bannerOpen = !Q.bannerOpen; drawInto(hostsOf()[0]); return; }
490
548
  var id = b.getAttribute('data-sq-id'), what = b.getAttribute('data-sq'), item = itemOf(id);
491
549
  if (!item) return;
492
550
  var card = b.closest ? b.closest('.sq-card') : null;
@@ -551,9 +609,36 @@
551
609
  // A change after Submit was armed takes the arm back: review, then submit.
552
610
  Q.armed[id] = '';
553
611
  if (Q.missing[id] != null) delete Q.missing[id];
612
+ keepPicks(id);
554
613
  patchCard(id);
555
614
  return true;
556
615
  }
616
+ // 6.99.30 (QA): a reload mid-answer keeps the picks and the typed Other words, per question id, for this tab only
617
+ // (sessionStorage). Restored only onto the same question (the same number of parts, and labels it still offers);
618
+ // dropped once the question is answered or gone.
619
+ var PICKS_KEY = 'cos.web.questions.picks';
620
+ function picksStore() { try { var v = JSON.parse(window.sessionStorage.getItem(PICKS_KEY) || '{}'); return v && typeof v === 'object' ? v : {}; } catch (_e) { return {}; } }
621
+ function writePicks(all) { try { window.sessionStorage.setItem(PICKS_KEY, JSON.stringify(all)); } catch (_e) {} }
622
+ function keepPicks(id) {
623
+ var all = picksStore(), p = Q.picks[id];
624
+ if (!p) return;
625
+ all[id] = p.map(function (x) { return { labels: x.labels.slice(0, 20), other: String(x.other || '').slice(0, 2000) }; });
626
+ writePicks(all);
627
+ }
628
+ function keptPicks(item) {
629
+ var kept = picksStore()[item.id], qs = item.questions || [];
630
+ if (!Array.isArray(kept) || kept.length !== qs.length) return null;
631
+ return qs.map(function (q, qi) {
632
+ var k = kept[qi] || {}, labels = (q.options || []).map(function (o) { return o.label; });
633
+ return { labels: (Array.isArray(k.labels) ? k.labels : []).filter(function (l) { return labels.indexOf(l) >= 0; }), other: typeof k.other === 'string' ? k.other : '' };
634
+ });
635
+ }
636
+ function forgetPicks(live) {
637
+ var all = picksStore(), gone = Object.keys(all).filter(function (id) { return !live[id]; });
638
+ if (!gone.length) return;
639
+ gone.forEach(function (id) { delete all[id]; });
640
+ writePicks(all);
641
+ }
557
642
  // Keys 1 to 9 while a question has focus, as in the terminal: the option with that number, or the last number to type
558
643
  // your own. Never while typing in Other, and never with a modifier held.
559
644
  function onKey(e) {
@@ -584,6 +669,10 @@
584
669
  var id = t.getAttribute('data-sq-id'), item = itemOf(id); if (!item) return;
585
670
  var qi = Number(t.getAttribute('data-q')), q = item.questions[qi], p = Q.picks[id][qi];
586
671
  p.other = t.value;
672
+ // 6.99.30 (QA): the same card drawn on Today and in the sheet keeps one answer: the words go into every copy.
673
+ hostsOf().forEach(function (m) {
674
+ m.el.querySelectorAll('[data-sq-other][data-sq-id="' + id + '"][data-q="' + qi + '"]').forEach(function (box) { if (box !== t) box.value = t.value; });
675
+ });
587
676
  // Single choice: typing your own answer clears the picked option, as on the phone.
588
677
  if (!q.multiSelect && t.value.trim() && p.labels.length) {
589
678
  p.labels = [];
@@ -591,6 +680,7 @@
591
680
  }
592
681
  Q.armed[id] = '';
593
682
  if (Q.missing[id] != null) delete Q.missing[id];
683
+ keepPicks(id);
594
684
  patchCard(id);
595
685
  }
596
686
  function listen(el) { el.addEventListener('click', onClick); el.addEventListener('change', onChange); el.addEventListener('input', onInput); el.addEventListener('keydown', onKey); }
@@ -604,20 +694,30 @@
604
694
  return { count: function () { return itemsFor(m.sessionId).length; }, unmount: function () { mounts = mounts.filter(function (x) { return x !== m; }); } };
605
695
  }
606
696
 
607
- // QA 6.99.27 (User 2): Today's own card (#sq-host) has a sticky footer, and iOS puts the keyboard over the layout
608
- // viewport, so with Other focused the footer sat under it. The keyboard's height (what of the layout viewport the
609
- // visible one leaves at the bottom) goes on the host as --sq-kb, and questions.css lifts the footer by it. Today's
610
- // overlay needs none of this: its layer already follows the visible viewport (today-actions.js fit).
697
+ // QA 6.99.27 (User 2): Today's own card (#sq-host) has a sticky footer, and the keyboard covers the bottom of the
698
+ // layout viewport, so with Other focused the footer sat under it. What covers the bottom goes on the host as --sq-kb
699
+ // and questions.css lifts the footer by it. 6.99.30: read the one way Control Web reads the visible viewport (app.js
700
+ // COSWeb.viewportFit, which Today's sheet uses too): the layout viewport under the visible box, in layout px, so a
701
+ // pinch zoom alone lifts nothing and a page the browser panned to the field lifts by what is really under it.
702
+ // The card in Today's sheet (.ta-q) needs none of this: the sheet itself is the visible box (today-actions.js fit).
611
703
  function keyboardInset() {
612
- var vv = window.visualViewport;
613
- if (!vv) return 0;
614
- var inset = Math.round((window.innerHeight || 0) - (vv.height + vv.offsetTop));
615
- return inset > 0 ? inset : 0;
704
+ var v = W.viewportFit ? W.viewportFit() : null;
705
+ return v && v.vv ? v.below : 0;
616
706
  }
617
707
  function fitKeyboard() {
618
- if (host.style && host.style.setProperty) host.style.setProperty('--sq-kb', keyboardInset() + 'px');
708
+ var inset = keyboardInset();
709
+ if (host.style && host.style.setProperty) host.style.setProperty('--sq-kb', inset + 'px');
710
+ // A keyboard up (120 px or more, or any strip under a field of the card): the footer stops sticking (questions.css).
711
+ var v = W.viewportFit ? W.viewportFit() : null, a = document.activeElement;
712
+ var typing = !!(a && host.contains && host.contains(a) && a.hasAttribute && a.hasAttribute('data-sq-other'));
713
+ if (host.classList) host.classList.toggle('sq-kb-up', !!(v && v.vv && (v.inset >= 120 || (typing && v.inset > 0))));
619
714
  }
620
715
  if (window.visualViewport) { window.visualViewport.addEventListener('resize', fitKeyboard); window.visualViewport.addEventListener('scroll', fitKeyboard); }
716
+ window.addEventListener('resize', fitKeyboard); window.addEventListener('orientationchange', fitKeyboard);
717
+ window.addEventListener('pageshow', fitKeyboard);
718
+ host.addEventListener('focusin', fitKeyboard); host.addEventListener('focusout', function () { setTimeout(fitKeyboard, 0); });
719
+ // The Mac going away or coming back redraws the cards at once (their buttons wait while it is away).
720
+ if (W.onServer) W.onServer(function () { render(); });
621
721
 
622
722
  // Hidden: no reads at all (a phone in a pocket spends nothing). Visible again: one read at once (within the 1 s gap).
623
723
  document.addEventListener('visibilitychange', function () { if (document.hidden) stop(); else if (signedIn()) start(); });
@@ -626,5 +726,6 @@
626
726
  if (W.onRoute) W.onRoute(function (r) { if (r && r.pane === 'today') nudge(); });
627
727
 
628
728
  W.sessionQuestions = { _state: Q, _take: take, _render: render, _presenceAge: presenceAge, _pollUrl: pollUrl, _key: onKey, _advanceMs: ADVANCE_MS, _todayPollMs: TODAY_POLL_MS, _fitKeyboard: fitKeyboard, poll: poll, nudge: nudge, mount: mount,
629
- itemsFor: itemsFor, atMacFor: atMacFor, answeredFor: answeredFor, wordsOf: wordsOf, macLine: MAC_LINE, answeredLine: ANSWERED_LINE };
729
+ itemsFor: itemsFor, atMacFor: atMacFor, answeredFor: answeredFor, wordsOf: wordsOf, macLine: MAC_LINE, answeredLine: ANSWERED_LINE,
730
+ loadState: function () { return Q.load; }, retry: function () { nudge(); } };
630
731
  })();