@gotcos/glasses-server 6.99.29 → 6.99.33

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.
@@ -75,7 +75,7 @@
75
75
  var O = {
76
76
  open: null, // { kind: 'need' | 'run' | 'wait' | 'pet' | 'meeting' | 'intake', key, all }
77
77
  opener: null, openerSel: '',
78
- pushed: false, ignorePops: 0,
78
+ pushed: false, ignorePops: 0, pops: [], // pops: our own Back steps still to arrive (quietBack)
79
79
  depth: 1, // history entries the overlay holds: 2 while a session opened from the pet's list shows
80
80
  back: null, // { scroll } the pet's list that session came from, for Back to sessions
81
81
  dirty: false, // something was decided or sent: Today reloads on close
@@ -144,6 +144,8 @@
144
144
  layer.addEventListener('click', onClick);
145
145
  layer.addEventListener('submit', onSubmit);
146
146
  layer.addEventListener('focusin', onFocusIn);
147
+ // Focus leaving a field (the keyboard's Hide key, a tap outside) refits on the next frame, once it has moved.
148
+ layer.addEventListener('focusout', function () { fit(); });
147
149
  layer.addEventListener('input', function (e) {
148
150
  if (e.target && e.target.id === 'ta-input' && O.r) keepDraft(O.r, e.target.value, O.r.draft && O.r.draft.text === e.target.value.trim() ? O.r.draft : undefined);
149
151
  });
@@ -169,46 +171,96 @@
169
171
  layer.addEventListener('touchmove', function (e) { if (e.target && (e.target === layer || (e.target.classList && e.target.classList.contains('ta-scrim')))) e.preventDefault(); }, { passive: false });
170
172
  return layer;
171
173
  }
172
- // With the keyboard up the layer follows the visible viewport, so the keyboard never covers the reply box or Send.
173
- // With it closed the layer is the whole fixed viewport (today-actions.css): --ta-vh and --ta-vt are set but unused, so
174
- // a visible viewport that reports less than the screen (IMG_0503, 11/07 12:48: about 106 px) never leaves a band.
175
- // 6.99.29 (Miles's unfolded Duo, 11/07 11:50): on a wide screen the side sheet kept its tall header and the reply box
176
- // went under the keyboard while the conversation shrank to one line. With the keyboard up, at any width, the layer is
177
- // marked ta-kb (today-actions.css): the sheet fills the visible viewport, the header folds to one compact row, the
178
- // reply box stays pinned at the sheet's bottom just above the keyboard, and the conversation takes what is left. If
179
- // that leaves under about three lines, ta-kb-tight also hides the "Working right now" note until the keyboard closes.
180
- // Keyboard: the visible viewport, at its zoom, is KB_MIN_PX or more shorter than the layout one (a form bar alone is
181
- // about 45 px). Read on visualViewport resize and scroll, at most once a frame, only while the overlay is open.
174
+ // 6.99.30 (Miles's Duo, in Comet as well as Safari): the layer is ALWAYS the visible part of the page, read the one
175
+ // way Control Web reads it (app.js COSWeb.viewportFit): top and height from visualViewport, 100dvh where there is
176
+ // none. Nothing assumes a browser: whatever covers the bottom (an on-screen keyboard, a hardware keyboard's strip,
177
+ // Comet's or any browser's bottom bar) is what the browser reports, and the reply box, Send and a question card's
178
+ // Submit stay above it. Only the backdrop and the sheet's own colour reach on down to the physical bottom
179
+ // (--ta-below plus the bleed, today-actions.css), so no band of Today shows under a bar that lets the page through.
180
+ // (6.99.29 made the closed-keyboard layer the whole layout viewport to cover IMG_0503's 106 px band; that band may be
181
+ // Comet's bottom bar, real covered space, and the composer must not sit under it.)
182
+ // The layout on top of that box:
183
+ // ta-kb an on-screen keyboard: what covers the bottom is KB_MIN_PX or more. The header folds to one
184
+ // compact row, the sheet fills the box, the reply box stays pinned at its bottom, and the
185
+ // conversation takes what is left. Under about three lines left, ta-kb-tight also hides the
186
+ // "Working right now" note until the keyboard closes (or the screen turns).
187
+ // ta-kb-strip a field in the sheet has focus and something under KB_MIN_PX covers the bottom (a hardware
188
+ // keyboard's 55 to 70 px strip): the header stays as it is; the strip covers the home indicator,
189
+ // so the reply box drops that inset.
190
+ // KB_MIN_PX applies whether or not a field has focus: a bottom bar under it (Comet's folded bar measured about 110 px)
191
+ // never folds the header, one of 120 px or more does. A strip counts from KB_STRIP_MIN_PX, so desktop zoom or a
192
+ // scrollbar (1 to 17 px of rounding, QA 6.99.30) never reads as a keyboard strip. Read on visualViewport resize and scroll while the overlay is open, and on window resize and
193
+ // orientationchange, pageshow, the page coming back into view, and focus moving in the sheet: at most once a frame.
182
194
  var KB_MIN_PX = 120;
195
+ var KB_STRIP_MIN_PX = 24;
183
196
  var BODY_MIN_PX = 72; // about three lines of the conversation (15 px text at 1.5)
184
- var KB = { on: false, tight: false, frame: 0, listening: false };
185
- function keyboardInset(vv) {
186
- var layoutH = window.innerHeight || (document.documentElement && document.documentElement.clientHeight) || 0;
187
- var inset = Math.round(layoutH - vv.height * (vv.scale || 1));
188
- return inset > 0 ? inset : 0;
189
- }
197
+ var KB = { on: false, strip: false, tight: false, frame: 0, listening: false, width: 0, inset: 0 };
190
198
  function tall(el) { return (el && el.offsetHeight) || 0; }
191
199
  // What the conversation gets once the compact header and the reply box have theirs.
192
- function bodyRoom(vv) {
200
+ function bodyRoom(height) {
193
201
  var sheet = $('ta-sheet'), grip = sheet && sheet.querySelector ? sheet.querySelector('.ta-grip') : null;
194
- return vv.height - tall($('ta-head')) - tall($('ta-foot')) - tall(grip);
202
+ return height - tall($('ta-head')) - tall($('ta-foot')) - tall(grip);
203
+ }
204
+ // A field that types (the reply box, a question card's Other) has focus inside the sheet.
205
+ function typing() {
206
+ var a = document.activeElement;
207
+ if (!a || !layer || a === layer || !layer.contains || !layer.contains(a)) return false;
208
+ var tag = String(a.tagName || '').toUpperCase();
209
+ if (tag === 'TEXTAREA' || tag === 'SELECT') return true;
210
+ if (tag === 'INPUT') return !/^(checkbox|radio|button|submit|reset|range|color|file|image|hidden)$/i.test(String(a.type || 'text'));
211
+ return !!a.isContentEditable;
212
+ }
213
+ function viewport() { return W.viewportFit ? W.viewportFit() : null; }
214
+ function crumb(what, v, focus) {
215
+ if (!W.layoutCrumb || !O.open || !v) return;
216
+ W.layoutCrumb(what, { kind: O.open.kind, kb: KB.on, strip: KB.strip, tight: KB.tight, focus: !!focus, inset: v.inset, below: v.below,
217
+ lh: v.layoutH, top: v.top, h: v.height, zoomed: v.zoomed, wide: !phone() });
195
218
  }
196
219
  function applyFit() {
197
220
  KB.frame = 0;
198
- var vv = window.visualViewport;
199
- if (!layer || !vv) return;
200
- layer.style.setProperty('--ta-vh', Math.round(vv.height) + 'px');
201
- layer.style.setProperty('--ta-vt', Math.round(vv.offsetTop) + 'px');
202
- var inset = keyboardInset(vv), on = inset >= KB_MIN_PX;
203
- layer.style.setProperty('--ta-kb', (on ? inset : 0) + 'px');
221
+ if (!layer || !O.open) return null;
222
+ var v = viewport();
223
+ if (!v) return null;
224
+ layer.style.setProperty('--ta-vh', v.height + 'px');
225
+ layer.style.setProperty('--ta-vt', v.top + 'px');
226
+ layer.style.setProperty('--ta-below', v.below + 'px');
227
+ var focus = typing(), on = v.inset >= KB_MIN_PX, strip = !on && focus && v.inset >= KB_STRIP_MIN_PX;
228
+ // A turned screen measures the room again: what was tight in landscape is not in portrait (QA 6.99.29).
229
+ var w = window.innerWidth || 0, turned = !!(KB.width && w && w !== KB.width);
230
+ if (w) KB.width = w;
231
+ var was = KB.on + '|' + KB.strip + '|' + KB.tight;
232
+ if (turned) KB.tight = false;
233
+ // A rise is what covers the bottom growing, by any amount, while the keyboard layout is on: not a class flip (a
234
+ // 120 px bar can already hold ta-kb before the keyboard comes, QA 6.99.32 Skeptic N-3), and not a jump past a step
235
+ // (a keyboard animating up in 20 px frames never crossed one, Codex's root regression on 98f3cde). The reveal is
236
+ // scrollIntoView 'nearest', so a frame where the field is already in view moves nothing. Shrinking never reveals.
237
+ var rose = (on || strip) && v.inset > (KB.inset || 0);
238
+ KB.inset = v.inset;
204
239
  if (on !== KB.on) { KB.on = on; KB.tight = false; layer.classList.toggle('ta-kb', on); }
240
+ if (strip !== KB.strip) { KB.strip = strip; layer.classList.toggle('ta-kb-strip', strip); }
241
+ // 6.99.32 (QA 6.99.30 canary W2): the keyboard rises after the focus, so onFocusIn's scroll (250 ms) can land before
242
+ // the layer shrinks and leave a question card's Other field under the keyboard. Once the layout has the keyboard,
243
+ // bring the focused field into view again.
244
+ if (rose && focus) revealFocused();
205
245
  // Measured with the compact header in place. Once tight it stays so while the keyboard is up: showing the note
206
246
  // again would take the room back and flicker.
207
- if (on && !KB.tight) KB.tight = bodyRoom(vv) < BODY_MIN_PX;
247
+ if (on && !KB.tight) KB.tight = bodyRoom(v.height) < BODY_MIN_PX;
208
248
  layer.classList.toggle('ta-kb-tight', on && KB.tight);
249
+ if (was !== KB.on + '|' + KB.strip + '|' + KB.tight) crumb('kb', v, focus);
250
+ return v;
251
+ }
252
+ function revealFocused() {
253
+ // What has focus when the frame runs, not when it was asked for (QA 6.99.32 Skeptic N-2).
254
+ var go = function () {
255
+ if (!O.open || !typing()) return;
256
+ var a = document.activeElement, f = a && a.id === 'ta-input' ? $('ta-form') : a;
257
+ if (f && f.scrollIntoView) f.scrollIntoView({ block: 'nearest' });
258
+ };
259
+ // The next frame, so the scroll measures the layout with ta-kb (or ta-kb-strip) applied.
260
+ if (window.requestAnimationFrame) window.requestAnimationFrame(go); else go();
209
261
  }
210
262
  function fit() {
211
- if (KB.frame) return;
263
+ if (KB.frame || !O.open) return;
212
264
  if (!window.requestAnimationFrame) return applyFit(); // every browser with visualViewport has frames
213
265
  KB.frame = window.requestAnimationFrame(applyFit);
214
266
  }
@@ -222,11 +274,34 @@
222
274
  function unfit() {
223
275
  listenViewport(false);
224
276
  if (KB.frame && window.cancelAnimationFrame) window.cancelAnimationFrame(KB.frame);
225
- KB.frame = 0; KB.on = false; KB.tight = false;
277
+ KB.frame = 0; KB.on = false; KB.strip = false; KB.tight = false; KB.width = 0; KB.inset = 0;
226
278
  if (!layer) return;
227
- layer.classList.remove('ta-kb'); layer.classList.remove('ta-kb-tight');
228
- layer.style.setProperty('--ta-kb', '0px');
229
- }
279
+ layer.classList.remove('ta-kb'); layer.classList.remove('ta-kb-strip'); layer.classList.remove('ta-kb-tight');
280
+ }
281
+ // The rest of the moments the visible viewport can change without a visualViewport event of its own (QA 6.99.29: the
282
+ // layer stayed sized for a keyboard that had gone). Each is a no-op while the overlay is closed.
283
+ window.addEventListener('resize', function () { fit(); });
284
+ window.addEventListener('orientationchange', function () { if (O.open) { KB.tight = false; fit(); } });
285
+ document.addEventListener('visibilitychange', function () {
286
+ if (document.hidden || !O.open) return;
287
+ fit();
288
+ if (O.r && O.open.kind === 'run') paintLive(); // the clock and the state, at once rather than on the next tick
289
+ });
290
+ // Back from the back-forward cache (a swipe back, the app switcher): the live stream is closed and everything shown
291
+ // is as old as the visit away. Open the stream again and read the session again.
292
+ window.addEventListener('pageshow', function (e) {
293
+ if (!O.open) return;
294
+ fit();
295
+ if (e && e.persisted) revive();
296
+ });
297
+ // 6.99.30 (QA): the Mac going away or coming back shows in an open sheet at once, not after the stream gives up: the
298
+ // live line says the Mac is not answering, and back up the stream opens again and the session is read again.
299
+ if (W.onServer) W.onServer(function (up) {
300
+ var r = O.r;
301
+ if (!O.open || !r) return;
302
+ if (!up) { r.feed.connected = false; r.feed.fallback = 'unreachable'; if (r.live) r.live.working = false; paintLive(); paintVerdict(); }
303
+ else revive();
304
+ });
230
305
 
231
306
  function openerSelector(t) {
232
307
  if (!t || !t.getAttribute) return '';
@@ -275,6 +350,8 @@
275
350
  var sheet = $('ta-sheet');
276
351
  O.settleAt = Date.now() + settleMs(sheet);
277
352
  if (sheet && sheet.focus) sheet.focus({ preventScroll: true });
353
+ // Measured once the sheet is drawn, and sent home as a breadcrumb (app.js layoutCrumb, 6.99.30).
354
+ crumb('open', applyFit(), typing());
278
355
  }
279
356
  // How long taps inside are ignored after opening: the longest open motion of the sheet and its backdrop (their computed
280
357
  // animation and transition, delay included: today-actions.css ta-rise or ta-slide, 220 ms, and fade, 160 ms), plus
@@ -314,8 +391,8 @@
314
391
  // X from a session opened off the pet's list unwinds both entries in one step, so the next Back leaves Today as before.
315
392
  if (O.pushed && how !== 'pop' && how !== 'leave') {
316
393
  var n = O.depth > 1 ? O.depth : 1;
317
- O.pushed = false; O.ignorePops++;
318
- try { if (n > 1 && typeof history.go === 'function') history.go(-n); else history.back(); } catch (_e) { O.ignorePops--; }
394
+ O.pushed = false;
395
+ quietBack(n);
319
396
  }
320
397
  if (how === 'pop' || how === 'leave') O.pushed = false;
321
398
  clearTimeout(O.expireTimer);
@@ -333,11 +410,29 @@
333
410
  if (el && el.focus) { try { el.focus({ preventScroll: true }); } catch (_e) { el.focus(); } }
334
411
  }
335
412
  // Leaving for a tab from inside the overlay replaces the overlay's history entry, so Back from there returns to Today.
413
+ // A session opened off the pet's list holds two entries: the list's comes off first (6.99.30, QA: it stayed under
414
+ // the new tab, and Back from there landed on it and showed nothing), then the tab replaces the overlay's own.
336
415
  function leaveTo(pane, args) {
337
416
  var hash = '#/' + [pane].concat(args || []).map(encodeURIComponent).join('/');
338
- var replace = O.pushed;
417
+ var replace = O.pushed, extra = O.pushed && O.depth > 1 ? O.depth - 1 : 0;
339
418
  close('leave');
340
- if (replace) location.replace(hash); else W.go(pane, args || []);
419
+ O.depth = 1;
420
+ if (!replace) { W.go(pane, args || []); return; }
421
+ if (extra) { quietBack(extra, function () { location.replace(hash); }); return; }
422
+ location.replace(hash);
423
+ }
424
+ // A Back step the overlay takes itself (X, Back to sessions, leaving for a tab): its popstate is ours, not the phone's
425
+ // Back, and `then` runs once it lands. If it never arrives (history.go with nothing to go back to, or a browser that
426
+ // drops it) the wait lapses after POP_WAIT_MS and `then` runs anyway, so a real Back later is never swallowed
427
+ // (6.99.30, QA: ignorePops stayed counted and ate the next Back).
428
+ var POP_WAIT_MS = 700;
429
+ function quietBack(n, then) {
430
+ var w = { then: then || null, timer: 0 };
431
+ var drop = function () { var i = O.pops.indexOf(w); if (i < 0) return false; O.pops.splice(i, 1); O.ignorePops = O.pops.length; return true; };
432
+ w.timer = setTimeout(function () { if (drop() && w.then) w.then(); }, POP_WAIT_MS);
433
+ O.pops.push(w); O.ignorePops = O.pops.length;
434
+ try { if (n > 1 && typeof history.go === 'function') history.go(-n); else history.back(); }
435
+ catch (_e) { clearTimeout(w.timer); if (drop() && w.then) w.then(); }
341
436
  }
342
437
  // A reload with the overlay open keeps its history entry, and the entry before it is Today at the same address, so
343
438
  // Back from there showed nothing (measured in Chrome 11/07: a same-document popstate). On load, app.js hands that
@@ -363,7 +458,7 @@
363
458
  });
364
459
  }
365
460
  window.addEventListener('popstate', function () {
366
- if (O.ignorePops > 0) { O.ignorePops--; return; }
461
+ if (O.pops.length) { var w = O.pops.shift(); clearTimeout(w.timer); O.ignorePops = O.pops.length; if (w.then) w.then(); return; }
367
462
  // The phone's Back from a session opened off the pet's list goes back to that list, as Back to sessions does.
368
463
  if (O.open && O.back) { backToList(); return; }
369
464
  if (O.open) close('pop');
@@ -372,7 +467,7 @@
372
467
  function backToList(fromButton) {
373
468
  var b = O.back;
374
469
  if (!b) return;
375
- if (fromButton && O.depth > 1) { O.ignorePops++; try { history.back(); } catch (_e) { O.ignorePops--; } }
470
+ if (fromButton && O.depth > 1) quietBack(1);
376
471
  O.depth = 1;
377
472
  open('pet', '', O.opener, { again: true });
378
473
  var body = $('ta-body'); if (body) body.scrollTop = b.scroll || 0;
@@ -454,11 +549,18 @@
454
549
  function onSubmit(e) {
455
550
  if (e.target && e.target.id === 'ta-form') { e.preventDefault(); sendReply(); }
456
551
  }
457
- // The keyboard rises over the bottom of the sheet: keep the box in view as it does.
552
+ // The keyboard rises over the bottom of the sheet: keep the box in view as it does. Any field that types counts
553
+ // (the reply box, a question card's Other): a hardware keyboard's strip is known only by focus (6.99.30).
458
554
  function onFocusIn(e) {
459
- if (!e.target || e.target.id !== 'ta-input') return;
555
+ if (!e.target || !typing()) return;
460
556
  fit();
461
- setTimeout(function () { if (!O.open) return; applyFit(); var f = $('ta-form'); if (f && f.scrollIntoView) f.scrollIntoView({ block: 'nearest' }); }, 250);
557
+ var reply = e.target.id === 'ta-input';
558
+ setTimeout(function () {
559
+ if (!O.open) return;
560
+ applyFit();
561
+ var f = reply ? $('ta-form') : e.target;
562
+ if (f && f.scrollIntoView) f.scrollIntoView({ block: 'nearest' });
563
+ }, 250);
462
564
  }
463
565
 
464
566
  // ═════════════ From today's meetings ═════════════
@@ -706,7 +808,7 @@
706
808
  setHead(kind === 'need' ? (O.item ? str(O.item.sessionTitle) || str(O.item.title) || 'Handoff' : 'Needs you')
707
809
  : kind === 'wait' ? (O.wait ? O.wait.title : 'A session is waiting for you') : (O.row ? K.title(O.row) : 'Session'));
708
810
  var body = $('ta-body');
709
- if (body) body.innerHTML = '<div id="ta-meta"></div><div id="ta-q" class="ta-q" hidden></div><div id="ta-live"></div><div id="ta-last"></div>';
811
+ if (body) body.innerHTML = '<div id="ta-meta"></div><div id="ta-notsent" class="ta-notsent" role="status"></div><div id="ta-q" class="ta-q" hidden></div><div id="ta-live"></div><div id="ta-last"></div>';
710
812
  var foot = $('ta-foot');
711
813
  if (foot) { foot.innerHTML = O.r ? composerHtml(kind) : ''; foot.hidden = false; }
712
814
  var input = $('ta-input');
@@ -733,8 +835,37 @@
733
835
  if (kind === 'run') { paintLive(); watchStream(O.r); }
734
836
  loadLast(O.r);
735
837
  K.probe(O.r);
838
+ loadNotSent(O.r);
736
839
  if (kind === 'need') recheckNeed(key);
737
840
  }
841
+ // 6.99.30: a queued message for this session that the server refused or let expire, said here as in the reader
842
+ // ("Not sent: <reason>"), from the same queue read (app.js sessionKit.notSent). The Sessions list chip follows it.
843
+ function loadNotSent(r) {
844
+ if (!r || !K.notSent || !K.answer) return;
845
+ K.answer(K.path(r.provider, r.id, '/queued-turns')).then(function (q) {
846
+ if (O.r !== r) return;
847
+ var rows = (q && q.status === 200 && q.body && q.body.turns) || [];
848
+ if (q && q.status === 200) K.notSent.note(r.provider, r.id, rows);
849
+ var box = $('ta-notsent'); if (!box) return;
850
+ box.innerHTML = K.notSent.rows(rows).map(function (t) {
851
+ return '<p class="ta-note ta-notsent-line"><strong>' + esc(clip(t.preview || t.prompt || 'A queued message', 80)) + '</strong> ' + esc(K.notSent.line(t)) + '</p>';
852
+ }).join('');
853
+ }, function () {});
854
+ }
855
+ // Back from the back-forward cache with a session open (pageshow persisted): the stream again, the last reply, the
856
+ // write gate and the questions, as on opening.
857
+ function revive() {
858
+ var r = O.r;
859
+ if (!r || !O.open) return;
860
+ if (O.open.kind === 'run') {
861
+ if (O.stream) { try { O.stream.close(); } catch (_e) {} O.stream = null; }
862
+ watchStream(r); paintLive();
863
+ }
864
+ loadLast(r);
865
+ K.probe(r);
866
+ loadNotSent(r);
867
+ if (W.sessionQuestions && W.sessionQuestions.nudge) W.sessionQuestions.nudge();
868
+ }
738
869
  function stopSession() {
739
870
  if (K.imagesStop) K.imagesStop($('ta-last'));
740
871
  if (O.stream) { try { O.stream.close(); } catch (_e) {} O.stream = null; }
@@ -1074,6 +1205,7 @@
1074
1205
  if (w === 'Working' || w === 'Waiting') {
1075
1206
  O.clock = setInterval(function () {
1076
1207
  if (O.r !== r) { clearInterval(O.clock); return; }
1208
+ if (document.hidden) return; // a hidden page spends nothing; coming back repaints at once (visibilitychange)
1077
1209
  var c = $('ta-clock'); if (c) c.textContent = K.feed.elapsed(r.feed, r);
1078
1210
  if (K.feed.gateStale(r)) K.probe(r);
1079
1211
  var rs = $('ta-run-state'); if (rs) rs.textContent = runWords();
@@ -1219,6 +1351,8 @@
1219
1351
  clearInterval(O.watch);
1220
1352
  var started = Date.now();
1221
1353
  O.watch = setInterval(function () {
1354
+ // Hidden: no reads, and the three minutes count only while the page is in view (6.99.30).
1355
+ if (document.hidden) { started += 3000; return; }
1222
1356
  if (O.r !== r || Date.now() - started > WATCH_MS) { clearInterval(O.watch); return; }
1223
1357
  K.answer('/api/agent-sessions/bindings/' + encodeURIComponent(bindingId) + '/turns/' + encodeURIComponent(key)).then(function (s) {
1224
1358
  if (O.r !== r) return;
@@ -1298,6 +1432,6 @@
1298
1432
  open: open, close: close, restore: restore, summary: summary, summaryMax: SUMMARY_MAX,
1299
1433
  isOpen: function () { return !!O.open; },
1300
1434
  _state: O, _decide: decide, _undo: undo, _intakeHtml: intakeHtml, _send: sendReply, _view: VIEW, _paintVerdict: paintVerdict,
1301
- _paintLive: paintLive, _paintMeta: paintMeta, _onClick: onClick, _expire: expireLater, _kb: KB, _fitNow: applyFit
1435
+ _paintLive: paintLive, _paintMeta: paintMeta, _onClick: onClick, _expire: expireLater, _kb: KB, _fitNow: applyFit, _revive: revive
1302
1436
  });
1303
1437
  })();
@@ -59,7 +59,7 @@
59
59
  .wk-filters { width: 100%; justify-content: flex-start; }
60
60
  .wk-filters .cx-search { max-width: none; flex-basis: 100%; }
61
61
  .wk-pick { flex: 1 1 140px; flex-direction: column; align-items: stretch; gap: 4px; font-size: 11.5px; }
62
- .wk-pick select.field { min-height: 38px; font-size: 15px; width: 100%; background-position: calc(100% - 16px) 17px, calc(100% - 11px) 17px; }
62
+ .wk-pick select.field { font-size: 15px; width: 100%; background-position: calc(100% - 16px) 17px, calc(100% - 11px) 17px; }
63
63
  .wk-text-btn { min-height: 36px; font-size: 14px; }
64
64
  }
65
65
 
@@ -286,6 +286,9 @@
286
286
  });
287
287
 
288
288
  W.onRoute(paintNavbar);
289
+ // 6.99.30: the server line moves the moment a request fails to reach the Mac or reaches it again (app.js noteServer),
290
+ // not on the five-minute check. Back up, the health read confirms it and refreshes what rides on it.
291
+ if (W.onServer) W.onServer(function (up) { setServer(up); if (up) checkServer(); });
289
292
  W.onShell(function () {
290
293
  Object.keys(FALLBACK).forEach(function (k) { setStory(k, FALLBACK[k], false); });
291
294
  setTimeout(function () { loadStories(); checkServer(); }, 2500);