@gotcos/glasses-server 7.0.31 → 7.0.34

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.
@@ -294,6 +294,26 @@
294
294
  return function () { if (!once) { once = true; WRITES.n = Math.max(0, WRITES.n - 1); } };
295
295
  }
296
296
  if (typeof window === 'object' && window) window.CosBusy = function () { return WRITES.n > 0 || !!(typeof state === 'object' && state && state.reading && state.reading.sending); };
297
+ // 7.0.33 A8 (Anand round 2, slide 19): an answer that is only a machine code ("cos_pipeline_not_configured") is never
298
+ // put on screen as is. Known codes get plain words that say what to do; any other bare code gets one plain sentence.
299
+ // err.code keeps the code, so a pane can still tell them apart.
300
+ var NOT_SET_UP = 'This part of COS is not set up on this Mac, so there is nothing to show here. Nothing is broken.';
301
+ // When trying again does not help, the next step: COS Control's menu restarts the server.
302
+ var STILL = ' If it keeps happening, restart COS from the COS Control menu on your Mac.';
303
+ var PLAIN_CODES = {
304
+ cos_pipeline_not_configured: NOT_SET_UP, pipeline_missing: NOT_SET_UP,
305
+ // Q1: a scripts folder is set (COS_SCRIPTS_DIR) but COS cannot run it: a broken setup, not a missing feature.
306
+ bridge_missing: 'COS on your Mac is set to use a scripts folder it cannot run, so this cannot be read. Check the COS_SCRIPTS_DIR setting on your Mac, then restart COS from the COS Control menu.',
307
+ task_runtime_unavailable: 'Work is not set up on this Mac yet. Open COS Control on your Mac to finish setting it up.',
308
+ unknown_command: 'COS on your Mac is older than this page. Update it from COS Control on your Mac.',
309
+ bridge_error: 'COS on your Mac could not read this just now. Try again in a moment.' + STILL
310
+ };
311
+ function plainMessage(msg) {
312
+ var m = String(msg || '').trim();
313
+ if (!/^[a-z0-9]+(?:_[a-z0-9]+)+$/.test(m)) return msg;
314
+ return PLAIN_CODES[m] || 'COS on your Mac could not do that just now. Try again in a moment.' + STILL;
315
+ }
316
+ function notSetUp(code) { return code === 'cos_pipeline_not_configured' || code === 'pipeline_missing'; }
297
317
  function api(path, opts) {
298
318
  var o = opts || {};
299
319
  var headers = { 'X-COS-Web': '1' };
@@ -322,7 +342,7 @@
322
342
  try { data = text ? JSON.parse(text) : {}; } catch (_e) { data = {}; }
323
343
  if (!res.ok) {
324
344
  var msg = (data && (data.message || (data.error && data.error.message) || data.error)) || ('The server answered ' + res.status + '.');
325
- var err = new Error(typeof msg === 'string' ? msg : 'The server answered ' + res.status + '.');
345
+ var err = new Error((typeof plainMessage === 'function' ? plainMessage : String)(typeof msg === 'string' ? msg : 'The server answered ' + res.status + '.'));
326
346
  err.status = res.status;
327
347
  // The machine-readable reason, so a pane can say the right thing for a refusal whose
328
348
  // body carries a code and no sentence (for example work-intake's {error: {code}}).
@@ -597,6 +617,10 @@
597
617
  else pos = NAV.replace ? NAV.replace.pos : history.length - 1;
598
618
  var back = null;
599
619
  if (!st && !NAV.replace && NAV.last && typeof NAV.last.pos === 'number' && pos === NAV.last.pos + 1) back = NAV.last.hash;
620
+ // 7.0.33 A11 (Anand round 2, slides 5 and 28): a place picked from the menu (the rail, the pane menu, Ask's Go to)
621
+ // starts a new trail. That page has no path behind it, so Back goes up from it, as after a deep link.
622
+ if (NAV.fresh && Date.now() - NAV.fresh < 2000) back = null;
623
+ NAV.fresh = 0;
600
624
  NAV.replace = null;
601
625
  // 7.0.30, Miles's pick B: the steps before this one ride in the same mark, the last of them being where Back goes.
602
626
  var trail = back ? (NAV.last.trail || []).concat([{ h: NAV.last.hash, t: NAV.last.label || '' }]).slice(-NAV_TRAIL_MAX) : null;
@@ -621,15 +645,22 @@
621
645
  var t = st.cosNav.trail;
622
646
  return t.map(function (s, i) { return { label: trailName(s.h, s.t), hash: s.h, back: t.length - i }; });
623
647
  }
624
- // A step is named by what kind of page it is, not its title (the title is the heading under the row): "Meeting",
625
- // "Session", "Naming voices". A list is its pane. A message, a day and a chat keep their own short name.
648
+ // 7.0.33 A11: a step is named by what it shows, its title (a meeting's, a session's, "Message #2", a day's date), cut
649
+ // to TRAIL_NAME_MAX characters with an ellipsis; a list is its pane. Only while the title is not loaded does the kind
650
+ // stand in ("Meeting", "Session", "Naming voices"). The same rule as COS Control's trail on the Mac (D4, 0.5.394).
651
+ var TRAIL_NAME_MAX = 28;
652
+ function trailCap(text) { var t = String(text || '').replace(/\s+/g, ' ').trim(); return t.length > TRAIL_NAME_MAX ? t.slice(0, TRAIL_NAME_MAX - 1).replace(/\s+$/, '') + '\u2026' : t; }
626
653
  function trailName(hash, crumb) {
627
654
  var a = String(hash || '').replace(/^#\/?/, '').split('/').filter(function (x) { return x !== ''; }), p = ROOT_PANES.indexOf(a[0]) >= 0 ? a[0] : 'today';
628
655
  if (a.length < 2 || isTabRoute(a.slice(1))) return TITLES[p] || 'Today';
656
+ // Naming voices is a page of its own about a meeting; its crumb is the meeting's title, so by title it read the same
657
+ // as the meeting after it ("Speakers / Pricing review / Pricing review"). It keeps its name.
658
+ if (p === 'speakers' && a[1] === 'review') return 'Naming voices';
659
+ if (crumb) return trailCap(crumb);
629
660
  if (p === 'meetings') return 'Meeting';
630
661
  if (p === 'sessions') return 'Session';
631
662
  if (p === 'speakers' && a[1] === 'review') return 'Naming voices';
632
- return crumb || TITLES[p] || 'Today';
663
+ return TITLES[p] || 'Today';
633
664
  }
634
665
  // One line that never wraps. When it is too wide, steps leave from the middle (the one after the first, then the next),
635
666
  // never the first, the one before this page, or this page; "…" stands where they were. widths: each step's, then this
@@ -807,8 +838,12 @@
807
838
  if (typeof paintTrail === 'function') paintTrail(r);
808
839
  closeMenu();
809
840
  if (changedPane) window.scrollTo(0, 0);
810
- $('topbar').classList.remove('tucked');
841
+ // 7.0.33 A12 (Q1): through barShow, so a bar sliding back after a page change catches its first tap too.
842
+ barShow($('topbar'));
811
843
  onScroll();
844
+ // 7.0.33 A13: the pet in the top bar stays on every page; its card closes when the page changes.
845
+ if (typeof petCardClose === 'function') petCardClose();
846
+ if (typeof petInBar === 'function' && petInBar() && state.sessions) { placePet(); if (changedPane && $('today-pet').classList.contains('hidden')) renderPet(state.sessions); }
812
847
 
813
848
  if (!PANES[r.pane] && LAZY()[r.pane]) {
814
849
  var lazyPane = r.pane, lazyEl = $('pane-' + lazyPane), wasEmpty = lazyEl && !lazyEl.getAttribute('data-lazy-ready');
@@ -861,16 +896,44 @@
861
896
  var lastY = 0;
862
897
  function onScroll() {
863
898
  var y = Math.max(0, window.scrollY || 0);
864
- // On a reader the bar stays (its Back is there). The folded Duo's Back is on the side rail (the bar's is hidden by CSS,
865
- // not the class), so there the bar tucks as before (Q1 7.0.30 M1).
866
- var bar = $('topbar'), back = $('topbar-back'), onReader = !!(back && !back.classList.contains('hidden')) && !(typeof sideRail === 'function' && sideRail());
899
+ // On a reader the bar stays (its Back is there). 7.0.33 A12: on the folded Duo too, where the bar now shows a named
900
+ // Back (app.css); before, the bar tucked there and a tap meant for it landed on the page.
901
+ var bar = $('topbar'), back = $('topbar-back'), onReader = !!(back && !back.classList.contains('hidden'));
867
902
  bar.classList.toggle('scrolled', y > 8);
868
- if (y < 60 || y < lastY - 4 || onReader) bar.classList.remove('tucked');
903
+ if (y < 60 || y < lastY - 4 || onReader) barShow(bar);
869
904
  else if (y > lastY + 4 && isPhone()) bar.classList.add('tucked');
870
905
  lastY = y;
871
906
  }
872
907
  function openMenu() { if (sideRail()) return; $('section-menu').classList.remove('hidden'); var cur = document.querySelector('#section-menu [aria-current="page"]') || document.querySelector('#section-menu [data-goto]'); if (cur) cur.focus(); }
873
908
  function closeMenu() { $('section-menu').classList.add('hidden'); }
909
+ // 7.0.33 A12 (Anand round 2, slide 27): no tap falls through a hidden top bar. Once the bar has slid away its band shows
910
+ // the page, and a tap meant for the bar (Ask, Back) landed on whatever was there; on Settings that armed, then fired,
911
+ // "Sign out every browser". Now, while the bar is away or still sliding back, the first tap in its band only brings
912
+ // the bar back. Every page and size: the class is the test, so wherever the bar tucks, its band catches.
913
+ var barBackAt = 0, barCatch = null;
914
+ function barShow(bar) { if (bar.classList.contains('tucked')) { bar.classList.remove('tucked'); barBackAt = Date.now(); } }
915
+ // Inside the bar's own band only: its height, and its own left and right (Q1: on the folded Duo the rail's top icon
916
+ // sits beside the bar, and a tap on it while the bar was away was swallowed). The bar only moves up and down, so its
917
+ // left and right hold while it is away.
918
+ function barCatches(x, y) {
919
+ var bar = $('topbar');
920
+ if (!bar || !(y >= 0) || y >= bar.offsetHeight) return false;
921
+ var r = bar.getBoundingClientRect ? bar.getBoundingClientRect() : null;
922
+ if (r && !(x >= r.left && x < r.right)) return false;
923
+ return bar.classList.contains('tucked') || Date.now() - barBackAt < BAR_SLIDE_MS;
924
+ }
925
+ var BAR_SLIDE_MS = 320; // app.css: the bar's transform takes 220 ms; a little more for the finger
926
+ document.addEventListener('pointerdown', function (e) {
927
+ barCatch = null;
928
+ if (!barCatches(e.clientX, e.clientY)) return;
929
+ barCatch = { y: e.clientY, until: Date.now() + 1500 };
930
+ barShow($('topbar')); lastY = Math.max(0, window.scrollY || 0);
931
+ }, true);
932
+ document.addEventListener('click', function (e) {
933
+ var c = barCatch; barCatch = null;
934
+ if (!c || Date.now() > c.until || Math.abs(e.clientY - c.y) > 24) return;
935
+ e.preventDefault(); e.stopPropagation();
936
+ }, true);
874
937
 
875
938
  // ── auth ────────────────────────────────────────────────
876
939
  // 7.0.19: the first paint (index.html #first-paint) ends the moment the sign-in card or the app shows, whichever
@@ -1433,17 +1496,19 @@
1433
1496
  function petPrefs() {
1434
1497
  var p = readStore(PET_KEY);
1435
1498
  var pet = typeof p.pet === 'string' && /^(mac|(shipped|gallery):[a-z0-9][a-z0-9-]{0,80})$/.test(p.pet) ? p.pet : 'mac';
1436
- return { pet: pet, show: p.show !== false };
1499
+ // 7.0.33 A13 (Anand round 2, slide 9): where it shows. 'today' as before, or 'bar': small in the top bar on a phone.
1500
+ return { pet: pet, show: p.show !== false, place: p.place === 'bar' ? 'bar' : 'today' };
1437
1501
  }
1438
1502
  function setPetPrefs(next) {
1439
1503
  var cur = petPrefs();
1440
- var merged = { pet: next.pet != null ? next.pet : cur.pet, show: next.show != null ? !!next.show : cur.show };
1504
+ var merged = { pet: next.pet != null ? next.pet : cur.pet, show: next.show != null ? !!next.show : cur.show, place: next.place != null ? (next.place === 'bar' ? 'bar' : 'today') : cur.place };
1441
1505
  writeStore(PET_KEY, merged);
1442
1506
  // Forget the old pet's manifest and loop; Today repaints with the new choice next time it shows.
1443
1507
  PET.manifest = null; PET.fetchedAt = 0; PET.pose = null; PET.token++; petStop();
1444
1508
  var box = $('today-pet');
1445
1509
  if (box) { box.innerHTML = ''; box.classList.add('hidden'); }
1446
- if (state.pane === 'today' && state.sessions) renderPet(state.sessions);
1510
+ placePet();
1511
+ if ((state.pane === 'today' || petInBar()) && state.sessions) renderPet(state.sessions);
1447
1512
  return merged;
1448
1513
  }
1449
1514
  var PET_HEIGHT = 140;
@@ -1560,13 +1625,72 @@
1560
1625
  // Folded (one column) the pet sits above the story. Unfolded (two columns, the same 760px line as
1561
1626
  // .today-cols in app.css) it moves to the top of the right-hand column, above Needs you (Miles 11/06).
1562
1627
  var PET_WIDE = window.matchMedia ? window.matchMedia('(min-width: 760px)') : null;
1628
+ // 7.0.33 A13 (Anand round 2, slide 9; Miles 12:43, the three choices and Anand's top bar mock): "In the top bar" puts the
1629
+ // same pet, small and round, in a phone's top bar left of Ask, on every page; a tap opens a card under it (the pet, its
1630
+ // line, Change pet and Hide). The folded and the open Duo have the rail instead of that bar: there it stays on Today
1631
+ // until its place there is chosen (render sent to the lead). The default stays On Today.
1632
+ var PET_BAR_MQ = window.matchMedia ? window.matchMedia('(min-width: 600px)') : null;
1633
+ function petInBar() { var p = petPrefs(); return p.show && p.place === 'bar' && !(PET_BAR_MQ && PET_BAR_MQ.matches) && !(typeof sideRail === 'function' && sideRail()); }
1563
1634
  function placePet() {
1564
1635
  var box = $('today-pet'), side = document.querySelector('#pane-today .today-side'), rec = $('today-rec');
1565
1636
  if (!box || !side || !rec) return;
1637
+ var bar = $('topbar'), ask = bar && bar.querySelector('[data-open-ask]');
1638
+ if (petInBar() && ask) { if (ask.previousElementSibling !== box) bar.insertBefore(box, ask); box.classList.add('in-bar'); return; }
1639
+ box.classList.remove('in-bar'); petCardClose();
1566
1640
  if (PET_WIDE && PET_WIDE.matches) { if (side.firstElementChild !== box) side.insertBefore(box, side.firstElementChild); }
1567
1641
  else if (rec.previousElementSibling !== box) rec.parentNode.insertBefore(box, rec);
1568
1642
  }
1569
- if (PET_WIDE) { if (PET_WIDE.addEventListener) PET_WIDE.addEventListener('change', placePet); else if (PET_WIDE.addListener) PET_WIDE.addListener(placePet); }
1643
+ function petReplace() { placePet(); if (petInBar() && state.sessions) renderPet(state.sessions); }
1644
+ [PET_WIDE, PET_BAR_MQ].forEach(function (mq) { if (mq) { if (mq.addEventListener) mq.addEventListener('change', petReplace); else if (mq.addListener) mq.addListener(petReplace); } });
1645
+ // The card under the small pet, as in Anand's mock: the pet, its line (petCaption, the words Today shows under it), one
1646
+ // more line when nothing runs, then Change pet and Hide. With something to open, the pet and its line open it, as the
1647
+ // pet on Today does (petTap). Hide turns the pet off and says where it comes back.
1648
+ var PET_IDLE_LINE = 'I\u2019ll wave when an agent starts.';
1649
+ function petCardToggle() {
1650
+ var box = $('today-pet'); if (!box) return;
1651
+ if (box.querySelector('.pet-card')) return petCardClose();
1652
+ var cap = box.querySelector('.pet-caption'), txt = cap ? cap.textContent : '', btn = box.querySelector('.pet-btn');
1653
+ var live = btn && btn.getAttribute('aria-disabled') === 'false';
1654
+ box.insertAdjacentHTML('beforeend', '<div class="pet-card" role="dialog" aria-label="Your pet">' +
1655
+ '<button type="button" class="pet-card-head" data-today="pet-open"' + (live ? '' : ' aria-disabled="true"') + '><span class="pet-card-pet" aria-hidden="true"></span>' +
1656
+ '<span class="pet-card-text"><span class="pet-card-title"></span>' + (txt === 'Nothing running' ? '<span class="pet-card-sub">' + esc(PET_IDLE_LINE) + '</span>' : '') + '</span></button>' +
1657
+ '<div class="pet-card-acts"><button type="button" class="cx-btn quiet" data-goto="settings/pet">Change pet</button><button type="button" class="cx-btn quiet" data-action="pet-hide">Hide</button></div></div>');
1658
+ box.querySelector('.pet-card-title').textContent = txt;
1659
+ var art = box.querySelector('.pet-btn .pet-still, .pet-btn .pet-sprite');
1660
+ if (art) box.querySelector('.pet-card-pet').appendChild(art.cloneNode(true));
1661
+ if (btn) btn.setAttribute('aria-expanded', 'true');
1662
+ petCardPlace(box.querySelector('.pet-card'));
1663
+ }
1664
+ // The card hangs under the pet, but never over a banner's own buttons (the sign-in notice's Hide, the question banner's
1665
+ // Answer, lead 12:5x): if it would cover one, it sits just below the lowest such banner instead.
1666
+ function petCardPlace(card) {
1667
+ if (!card || !card.getBoundingClientRect) return;
1668
+ var c = card.getBoundingClientRect(), drop = 0;
1669
+ document.querySelectorAll('#conn-notice:not([hidden]) button, #sq-host:not([hidden]) button, #sq-host:not([hidden]) a, .rec-banner').forEach(function (b) {
1670
+ var r = b.getBoundingClientRect();
1671
+ if (r.width && r.height && r.left < c.right && r.right > c.left && r.top < c.bottom && r.bottom > c.top) {
1672
+ var host = b.closest('#conn-notice, #sq-host'), h = host ? host.getBoundingClientRect() : r; // the recording banner is its own button
1673
+ drop = Math.max(drop, h.bottom + 8 - c.top);
1674
+ }
1675
+ });
1676
+ if (drop > 0) card.style.top = 'calc(100% + 8px + ' + Math.ceil(drop) + 'px)';
1677
+ }
1678
+ function petHide() {
1679
+ petCardClose(); setPetPrefs({ show: false });
1680
+ toast('Pet hidden. Turn it back on in Settings, Pet.');
1681
+ }
1682
+ if (typeof document !== 'undefined' && document.addEventListener) {
1683
+ document.addEventListener('click', function (e) {
1684
+ var box = $('today-pet');
1685
+ if (box && box.querySelector('.pet-card') && !box.contains(e.target)) petCardClose();
1686
+ }, true);
1687
+ document.addEventListener('keydown', function (e) { if (e.key === 'Escape') petCardClose(); });
1688
+ }
1689
+ function petCardClose() {
1690
+ var box = $('today-pet'), card = box && box.querySelector('.pet-card');
1691
+ if (card) card.remove();
1692
+ var btn = box && box.querySelector('.pet-btn'); if (btn) btn.removeAttribute('aria-expanded');
1693
+ }
1570
1694
  var PET_CHANGE = '<button type="button" class="pet-change" data-goto="settings/pet">Change pet</button>';
1571
1695
  // A still pet (a gallery pick, Cloud Puff, or a single image on the Mac): one picture for every state.
1572
1696
  function renderPetStill(box, m, sessions) {
@@ -1626,9 +1750,11 @@
1626
1750
  if (still || spec.frames < 2) return;
1627
1751
  var step = function () {
1628
1752
  // Runs only while Today is on screen: leaving the pane or hiding the page stops the loop.
1629
- if (token !== PET.token || document.hidden || state.pane !== 'today') { PET.timer = null; return; }
1753
+ if (token !== PET.token || document.hidden || (state.pane !== 'today' && !petInBar())) { PET.timer = null; return; }
1630
1754
  PET.frame = (PET.frame + 1) % spec.frames;
1631
1755
  petPaint(el, spec, PET.frame, scale);
1756
+ // 7.0.33 A13: the card's copy of the pet moves with it.
1757
+ var cardArt = box.querySelector('.pet-card .pet-sprite'); if (cardArt) petPaint(cardArt, spec, PET.frame, scale);
1632
1758
  PET.timer = setTimeout(step, Math.max(20, spec.interval * 1000));
1633
1759
  };
1634
1760
  PET.timer = setTimeout(step, Math.max(20, spec.interval * 1000));
@@ -1862,7 +1988,10 @@
1862
1988
  var write = typeof storyWrite === 'function' ? storyWrite : function (e, kind, text, when) {
1863
1989
  if (kind === 'brief') e.innerHTML = esc(text) + '<span class="story-when">' + esc(when) + '</span>'; else e.textContent = text;
1864
1990
  };
1865
- if (TODAY.offline) { askBox(null); write(el, 'offline', 'COS on ' + state.serverName + ' is not answering right now. Today fills in when it does.'); return; }
1991
+ // 7.0.33 A6 (Anand round 2, slide 20): what the sentence says, Needs you and Running do not say again. TODAY.storySaid
1992
+ // is the list of failures this sentence named ('offline' when it says the Mac is not answering); renderNeeds reads it.
1993
+ var said = function (v, brief) { var was = JSON.stringify([TODAY.storySaid, TODAY.storyBrief]); TODAY.storySaid = v; TODAY.storyBrief = !!brief; if (was !== JSON.stringify([v, !!brief]) && typeof renderNeeds === 'function' && $('needs-list')) renderNeeds(m); };
1994
+ if (TODAY.offline) { askBox(null); write(el, 'offline', 'COS on ' + state.serverName + ' is not answering right now. Today fills in when it does.'); said('offline'); return; }
1866
1995
  // The model's summary when there is one (it never blocks: the counts line below shows until it lands). The test
1867
1996
  // harnesses that run this block alone have no requestBrief, so it is looked up, never assumed.
1868
1997
  askBox(null);
@@ -1870,7 +1999,7 @@
1870
1999
  requestBrief(m);
1871
2000
  var brief = briefShown();
1872
2001
  askBox(brief);
1873
- if (brief) { write(el, 'brief', brief.text, briefWhen(brief, m)); return; }
2002
+ if (brief) { write(el, 'brief', brief.text, briefWhen(brief, m)); said([], true); return; }
1874
2003
  }
1875
2004
  var first = '';
1876
2005
  if (m.sessionsWaiting > 0) first = countWord(m.sessionsWaiting) + ' ' + plural(m.sessionsWaiting, 'session is', 'sessions are') + ' waiting on you';
@@ -1891,7 +2020,11 @@
1891
2020
  else second = 'No agents are running';
1892
2021
  if (TODAY.recording) second += (unread || working > 0 || open > 0 ? ', and ' : '. ') + 'COS is recording a meeting';
1893
2022
  var out = (first ? first + '. ' : '') + second + '.';
1894
- var others = m.failed.filter(function (n) { return n !== 'sessions'; });
2023
+ // 7.0.33 A6: more than two sources unread is the Mac not answering, said once, here (it was Needs you's row).
2024
+ if (m.failed.length > 2) { said(m.failed.slice()); write(el, 'counts', (first ? first + '. ' : '') + 'Your Mac is not answering, so this may not be everything.'); return; }
2025
+ var others = m.failed.filter(function (n) { return n !== 'sessions'; }), named = unread ? ['sessions'] : [];
2026
+ if (!m.settled && !m.now.length && !m.later.length && (unread ? others : m.failed).length) named = named.concat(unread ? others : m.failed);
2027
+ said(named);
1895
2028
  if (!m.settled && !m.now.length && !m.later.length) out += (unread ? others : m.failed).length ? ' Couldn\u2019t read ' + listWords(unread ? others : m.failed) + ', so this may not be everything.' : unread ? ' This may not be everything that needs you.' : ' Still reading what needs you.';
1896
2029
  else if (unread) out += ' This may not be everything that needs you.';
1897
2030
  write(el, 'counts', out);
@@ -1921,6 +2054,10 @@
1921
2054
  $('run-count').textContent = working.length + jobs.n ? String(working.length + jobs.n) : '';
1922
2055
  var list = $('run-list');
1923
2056
  // Never "Nothing running" about a list that was not read.
2057
+ // 7.0.33 A6: when nothing runs, the sentence ("No agents are running", or that sessions could not be read) is all
2058
+ // Today says about it; the panel shows again the moment a session or a COS job runs.
2059
+ var panel = list.closest ? list.closest('.run-panel') : null;
2060
+ if (panel && panel.classList) panel.classList.toggle('hidden', !jobs.n && (TODAY.sessionsState === 'failed' || (TODAY.sessionsState === 'answered' && !working.length && !open.length)));
1924
2061
  if (TODAY.sessionsState === 'failed') { list.innerHTML = jobs.html + '<button type="button" class="run-more run-failed" data-today-needs="retry">Couldn\u2019t read sessions. Try again</button>'; return; }
1925
2062
  var shown = working.slice(0, 3);
1926
2063
  var html = jobs.html + shown.map(function (s) { return runRow(s, true); }).join('');
@@ -2426,12 +2563,18 @@
2426
2563
  if (m.later.length && !TODAY.showLater) html += '<button type="button" class="need-more" data-today-needs="later">' + esc(m.later.length + (m.later.length === 1 ? ' thing can' : ' things can') + ' wait until tomorrow. Show') + '</button>';
2427
2564
  // 6.99.30 (design QA): no empty line above a failed read (it left a 45 px gap), and one retry row, never one per
2428
2565
  // source: with the Mac not answering (sessions and Work both unread, or more than two sources) it says so once.
2429
- if (!m.rows.length) html = m.failed.length ? '' : '<p class="card-empty">' + esc(m.settled ? 'Nothing is waiting on you.' : 'Reading sessions, Work, voices and memories.') + '</p>';
2430
- if (m.failed.length) {
2431
- var silent = TODAY.offline || m.failed.length > 2;
2432
- html += '<button type="button" class="need-more need-failed" data-today-needs="retry">' + esc(silent ? 'Your Mac is not answering. Try again.' : 'Couldn’t read ' + listWords(m.failed) + '. Try again') + '</button>';
2566
+ // 7.0.33 A6: a failure the sentence above already names is not said again here (Refresh at the top tries again), and
2567
+ // with nothing to show (nothing waiting, which the sentence says, or only what it said) the card steps aside.
2568
+ var said = TODAY.storySaid || [], left = said === 'offline' ? [] : m.failed.filter(function (n) { return said.indexOf(n) < 0; });
2569
+ // A model summary does not say "Nothing is waiting on you", so under one the card still does.
2570
+ if (!m.rows.length) html = m.failed.length || (m.settled && !TODAY.storyBrief) ? '' : '<p class="card-empty">' + esc(m.settled ? 'Nothing is waiting on you.' : 'Reading sessions, Work, voices and memories.') + '</p>';
2571
+ if (left.length) {
2572
+ var silent = TODAY.offline || left.length > 2;
2573
+ html += '<button type="button" class="need-more need-failed" data-today-needs="retry">' + esc(silent ? 'Your Mac is not answering. Try again.' : 'Couldn’t read ' + listWords(left) + '. Try again') + '</button>';
2433
2574
  }
2434
2575
  list.innerHTML = html;
2576
+ var card = list.closest ? list.closest('.card') : null;
2577
+ if (card && card.classList) card.classList.toggle('hidden', !html);
2435
2578
  }
2436
2579
  // Weather sits under the date at the top left (Miles, 11/07), where the eye starts, not inside Next up.
2437
2580
  // It shows whether or not a meeting is next.
@@ -2764,7 +2907,7 @@
2764
2907
  view: sxPref('view', 'sessions', ['sessions', 'usage', 'cost']),
2765
2908
  recency: sxPref('recency', 'newest', ['newest', 'oldest', 'match']),
2766
2909
  q: '', instant: [], remote: [], searching: false, searchError: '', ranFor: '', semantic: null, semanticReason: '', searchSeq: 0, searchTimer: null,
2767
- hooks: null, hooksAt: 0,
2910
+ hooks: null, hooksAt: 0, hooksOffer: false, offerPutAway: false,
2768
2911
  usage: null, usageAt: 0, usageLoading: false, usageError: '',
2769
2912
  uPeriod: 'Last 30 days', uTool: 'All tools', uModel: 'All models',
2770
2913
  // Usage on a wide screen (uWideModel): the custom range, the chart sentence, the chosen bar, the table's order.
@@ -2902,7 +3045,8 @@
2902
3045
  var stats = view === 'sessions' ? sxStats(vis) : [];
2903
3046
  var busy = view === 'sessions' ? SX.loading : SX.usageLoading;
2904
3047
  hero.innerHTML = '<span class="cx-glyph"><svg class="icon"><use href="#i-sessions"/></svg></span>' +
2905
- '<div class="cx-hero-text"><h1 class="cx-title">Sessions</h1><p class="cx-detail-line">' + esc(detail) + liveHeroNote(view) + '</p></div>' +
3048
+ // 7.0.33 A5: the heading names the view the top bar names (Usage, COS cost), not the place, so the page has one name.
3049
+ '<div class="cx-hero-text"><h1 class="cx-title">' + esc(SX_VIEWS[view] || 'Sessions') + '</h1><p class="cx-detail-line">' + esc(detail) + liveHeroNote(view) + '</p></div>' +
2906
3050
  '<div class="cx-actions"><button type="button" class="cx-btn quiet" data-sx="refresh"' + (busy ? ' disabled' : '') + '><svg class="icon"><use href="#i-refresh"/></svg>Refresh</button></div>' +
2907
3051
  (stats.length ? '<div class="cx-stats">' + stats.map(function (s) { return '<span class="cx-stat"><b>' + esc(typeof s[0] === 'number' ? s[0].toLocaleString() : s[0]) + '</b><span>' + s[1] + '</span></span>'; }).join('') + '</div>' : '');
2908
3052
  // 7.0.23 step 2b: Updated and Opened are the sort menu now, and Pinned a group; no clock switches here.
@@ -3876,17 +4020,40 @@
3876
4020
  if (Date.now() - SX.hooksAt < 60000) { paintHooks(); return; }
3877
4021
  SX.hooksAt = Date.now();
3878
4022
  api('/api/session-hooks/status').then(function (d) {
3879
- SX.hooks = (d && d.sessionHooks && d.sessionHooks.state) || null; paintHooks();
3880
- }, function (err) { SX.hooks = err && err.status === 404 ? 'route_absent' : null; paintHooks(); });
4023
+ SX.hooks = (d && d.sessionHooks && d.sessionHooks.state) || null;
4024
+ // 7.0.34 (X1): the 8 hour question wait is on by default and offered until this Mac reinstalls its hooks.
4025
+ SX.hooksOffer = !!(d && d.sessionHooks && d.sessionHooks.questionWait && d.sessionHooks.questionWait.offered === true);
4026
+ paintHooks();
4027
+ }, function (err) { SX.hooks = err && err.status === 404 ? 'route_absent' : null; SX.hooksOffer = false; paintHooks(); });
4028
+ }
4029
+ // 7.0.34 (X1): "Not now" puts the 8 hour offer away in THIS browser only. A store that cannot be read or written
4030
+ // just means the line shows again; a hooks-problem line is never hidden by it.
4031
+ var SX_OFFER_HIDE_KEY = 'cos.sx.questionWaitOfferHidden';
4032
+ function offerHidden() {
4033
+ try { return typeof localStorage === 'object' && !!localStorage && localStorage.getItem(SX_OFFER_HIDE_KEY) === '1'; } catch (_e) { return false; }
4034
+ }
4035
+ function hideOffer() {
4036
+ try { if (typeof localStorage === 'object' && localStorage) localStorage.setItem(SX_OFFER_HIDE_KEY, '1'); } catch (_e) { /* not kept: it shows next visit */ }
3881
4037
  }
3882
4038
  function paintHooks() {
3883
4039
  var el = $('sessions-hooks'); if (!el) return;
3884
4040
  var st = SX.hooks;
3885
- if (['missing', 'drift', 'script_outdated', 'route_absent'].indexOf(st) < 0) { el.innerHTML = ''; return; }
3886
- var text = st === 'route_absent' ? 'Live session state needs COS server 6.48.0 or later.'
3887
- : st === 'missing' ? 'Claude Code hooks are not installed: sessions read from their transcripts only.'
3888
- : 'Claude Code hooks need a refresh (' + (st === 'drift' ? 'settings changed' : 'script outdated') + ').';
3889
- var where = st === 'route_absent' ? 'Update Server from COS Control on your Mac.' : (st === 'missing' ? 'Install hooks' : 'Reinstall hooks') + ' from COS Control on your Mac.';
4041
+ if (['missing', 'drift', 'script_outdated', 'route_absent'].indexOf(st) < 0) {
4042
+ // 7.0.34 (X1): read-only, no dot. A hooks problem above already says Reinstall hooks, so this shows only
4043
+ // when there is none (A8: each thing once). Installing hooks stays a Mac action.
4044
+ var offer = st === 'installed' && SX.hooksOffer && !SX.offerPutAway && !offerHidden();
4045
+ el.innerHTML = offer
4046
+ ? '<div class="sx-hooks"><span>' + esc('Questions can now wait up to 8 hours.') + ' <span class="sx-hooks-where">' + esc('In COS Control on your Mac, choose Reinstall hooks.') + '</span> <button type="button" class="sx-link" data-sx-offer="hide">' + esc('Not now') + '</button></span></div>'
4047
+ : '';
4048
+ var notNow = offer && el.querySelector ? el.querySelector('[data-sx-offer="hide"]') : null;
4049
+ if (notNow) notNow.onclick = function () { SX.offerPutAway = true; hideOffer(); paintHooks(); };
4050
+ return;
4051
+ }
4052
+ // 7.0.33 A8: plain words, then what to do.
4053
+ var text = st === 'route_absent' ? 'Whether a session is working or waiting needs a newer COS on your Mac.'
4054
+ : st === 'missing' ? 'COS cannot yet tell when a Claude Code session is working or waiting on you.'
4055
+ : 'COS\u2019s link to Claude Code needs a refresh, so working and waiting may be out of date.';
4056
+ var where = st === 'route_absent' ? 'Update it from COS Control on your Mac.' : 'In COS Control on your Mac, choose ' + (st === 'missing' ? 'Install hooks' : 'Reinstall hooks') + '.';
3890
4057
  el.innerHTML = '<div class="sx-hooks"><span class="sx-hooks-dot" aria-hidden="true"></span><span>' + esc(text) + ' <span class="sx-hooks-where">' + esc(where) + '</span></span></div>';
3891
4058
  }
3892
4059
 
@@ -7635,13 +7802,18 @@
7635
7802
  (recent.length ? '<h2 class="group-title">Recent sessions</h2><div class="list">' + recent.map(function (s) { return sessionRow(s); }).join('') + '</div>' : '');
7636
7803
  return;
7637
7804
  }
7638
- var jumps = JUMPS.filter(function (j) { return j[1].toLowerCase().indexOf(q.toLowerCase()) === 0; });
7639
- out.innerHTML = '<div class="list" style="margin-top:14px">' +
7805
+ // 7.0.33 A7 (Anand round 2, slide 20): typing "usage" put Go to Usage last, under four Ask rows. When the words name a
7806
+ // place (its whole name, or its first three letters or more, "go to" allowed in front), Go to comes first.
7807
+ var ql = q.toLowerCase().replace(/^go to\s+/, '');
7808
+ var jumps = JUMPS.filter(function (j) { return j[1].toLowerCase().indexOf(ql) === 0; });
7809
+ var placeFirst = jumps.length > 0 && (ql.length >= 3 || jumps.some(function (j) { return j[1].toLowerCase() === ql; }));
7810
+ var jumpRows = jumps.map(function (j) { return '<button type="button" class="nav-row" data-goto="' + j[0] + '"><svg class="icon"><use href="#i-' + j[0] + '"/></svg><span class="grow">Go to ' + esc(j[1]) + '</span><span class="chev"></span></button>'; }).join('');
7811
+ out.innerHTML = '<div class="list" style="margin-top:14px">' + (placeFirst ? jumpRows : '') +
7640
7812
  '<button type="button" class="nav-row" data-action="ask-cos"><svg class="icon"><use href="#i-ask"/></svg><span class="grow">Ask COS<span class="note">' + esc(askRowNote(q)) + '</span></span></button>' +
7641
7813
  '<button type="button" class="nav-row" data-action="ask-file"' + (ASK_FILE.busy ? ' disabled' : '') + '><svg class="icon"><use href="#i-work"/></svg><span class="grow">' + (ASK_FILE.busy ? 'Filing\u2026' : 'File to Work inbox') + fileNoteHtml(q) + '</span></button>' +
7642
7814
  '<button type="button" class="nav-row" data-action="ask-meaning"><svg class="icon"><use href="#i-sessions"/></svg><span class="grow">Find a session about this<span class="note">Ranks every session by meaning</span></span></button>' +
7643
7815
  '<button type="button" class="nav-row" data-action="ask-work"><svg class="icon"><use href="#i-work"/></svg><span class="grow">Find a task about this<span class="note">Searches Work by meaning</span></span></button>' +
7644
- jumps.map(function (j) { return '<button type="button" class="nav-row" data-goto="' + j[0] + '"><svg class="icon"><use href="#i-' + j[0] + '"/></svg><span class="grow">Go to ' + esc(j[1]) + '</span><span class="chev"></span></button>'; }).join('') +
7816
+ (placeFirst ? '' : jumpRows) +
7645
7817
  '</div><div id="ask-hits"></div><p class="status-line" id="ask-status"></p>';
7646
7818
  paintAskRow();
7647
7819
  clearTimeout(renderAsk.t);
@@ -7691,7 +7863,12 @@
7691
7863
  return;
7692
7864
  }
7693
7865
  if (t.hasAttribute('data-open-ask')) return openAsk();
7694
- if (t.hasAttribute('data-today')) return t.getAttribute('data-today') === 'pet' ? petTap(t) : openTodayAction(t);
7866
+ if (t.hasAttribute('data-today')) {
7867
+ var dt = t.getAttribute('data-today');
7868
+ if (dt === 'pet-open') { if (t.getAttribute('aria-disabled') !== 'true') { petCardClose(); petTap($('today-pet').querySelector('.pet-btn')); } return; }
7869
+ return dt === 'pet' ? (petInBar() ? petCardToggle() : petTap(t)) : openTodayAction(t);
7870
+ }
7871
+ if (t.getAttribute('data-action') === 'pet-hide') return petHide();
7695
7872
  if (t.hasAttribute('data-today-next')) return prepNext();
7696
7873
  if (t.hasAttribute('data-next-prep')) return toggleNextPrep(t.getAttribute('data-next-prep'));
7697
7874
  if (t.hasAttribute('data-next-prep-cancel')) return closeNextPrep(true);
@@ -7705,6 +7882,8 @@
7705
7882
  }
7706
7883
  if (t.hasAttribute('data-theme-choice')) { window.CosTheme.set(t.getAttribute('data-theme-choice')); paintAppearance(); return; }
7707
7884
  if (t.hasAttribute('data-goto') || t.hasAttribute('data-pane')) {
7885
+ // 7.0.33 A11: a pick from the menu (the rail, the pane menu, Ask's Go to) starts a new trail (navStamp).
7886
+ if (t.closest('.rail, #section-menu, #ask-results')) NAV.fresh = Date.now();
7708
7887
  if (inSheet) closeAsk(); closeMenu();
7709
7888
  // "work/sort" is a pane and its arguments, not one name.
7710
7889
  var dest = String(t.getAttribute('data-goto') || t.getAttribute('data-pane')).split('/');
@@ -7723,7 +7902,7 @@
7723
7902
  if (action === 'drop-after') return dropHeld();
7724
7903
  if (action === 'unqueue') return removeQueued(t.getAttribute('data-turn-key'));
7725
7904
  if (action === 'revoke-all') {
7726
- if (t.getAttribute('data-armed') !== '1') { t.setAttribute('data-armed', '1'); t.querySelector('.grow').firstChild.textContent = 'Tap again to sign out every browser'; return; }
7905
+ if (!window.COSWeb.armTap(t, t.querySelector('.grow').firstChild, 'Tap again to sign out every browser')) return;
7727
7906
  api('/api/web-session/revoke-all', { method: 'POST', body: {} }).then(function () { showAuth('Every browser is signed out, this one too.'); }).catch(function (err) { setStatus($('settings-status'), err.message, true); });
7728
7907
  return;
7729
7908
  }
@@ -8090,6 +8269,9 @@
8090
8269
  // once a detail has loaded; route() clears it on every navigation.
8091
8270
  setCrumb: function (text) { crumbDetail = String(text || ''); navLabel(crumbDetail); paintTrail(); runHooks(routeHooks, parseRoute()); },
8092
8271
  crumb: function () { return crumbDetail; },
8272
+ // 7.0.33 A11: the trail for the navigation bar from 600 px (shell.js paintNavbar): the same steps and names as the
8273
+ // phone's row, and this page's name.
8274
+ trailNow: function (r) { r = r || parseRoute(); var steps = r.args.length && !isTabRoute(r.args) ? navTrail() : null; return { steps: steps || [], here: steps || r.args.length ? trailName(location.hash, crumbDetail) : (crumbDetail || TITLES[r.pane] || 'Today') }; },
8093
8275
  // Panes that read with their own fetch call this on a 401 so the sign-in screen shows.
8094
8276
  showAuth: function (message) { showAuth(message); },
8095
8277
  // Control's session state (the Sessions pane's sxState) and warm-up test, so the rail's
@@ -8124,12 +8306,29 @@
8124
8306
  // Today as a place to act (panes/today-actions.js, Miles 11/06): what the Today cards show, a repaint after a
8125
8307
  // decision, and the Sessions reader's own pieces, so the overlay sends and reads through the reader's code.
8126
8308
  window.COSWeb.pane = function (name) { return PANES[name] || null; };
8309
+ window.COSWeb.plainMessage = plainMessage; window.COSWeb.notSetUp = notSetUp;
8127
8310
  // Tap, then tap again (Today's Dismiss, Stop and Send anyway; a question card's Allow, Deny and Send): the second tap
8128
8311
  // counts only once the first has been on screen ARM_CONFIRM_MS, so a double tap or a finger bounce arms and never
8129
8312
  // confirms (W2-r2). The one rule both panes/today-actions.js and panes/questions.js read.
8130
8313
  var ARM_CONFIRM_MS = 400;
8131
8314
  window.COSWeb.armConfirmMs = ARM_CONFIRM_MS;
8132
8315
  window.COSWeb.confirmReady = function (armedAt) { return typeof armedAt === 'number' && Date.now() - armedAt >= ARM_CONFIRM_MS; };
8316
+ // 7.0.33 A12: the same rule for a button armed in place (Sign out every browser, Settings' Release), which until now
8317
+ // stayed armed with no time limit, so a stray tap minutes later confirmed it. armTap(button, label, armedText) returns
8318
+ // true on a confirming tap; otherwise it arms (label says armedText) for ARM_EXPIRE_MS and puts the words back after.
8319
+ var ARM_EXPIRE_MS = 5000;
8320
+ window.COSWeb.armExpireMs = ARM_EXPIRE_MS;
8321
+ window.COSWeb.armTap = function (el, label, armedText) {
8322
+ var now = Date.now(), at = Number(el.getAttribute('data-armed-at')) || 0;
8323
+ if (el.getAttribute('data-armed') === '1' && now - at < ARM_EXPIRE_MS) return now - at >= ARM_CONFIRM_MS;
8324
+ if (el.getAttribute('data-armed') !== '1') el.setAttribute('data-idle-text', label.textContent);
8325
+ el.setAttribute('data-armed', '1'); el.setAttribute('data-armed-at', String(now)); label.textContent = armedText;
8326
+ setTimeout(function () {
8327
+ if (el.getAttribute('data-armed-at') !== String(now)) return;
8328
+ el.removeAttribute('data-armed'); el.removeAttribute('data-armed-at'); label.textContent = el.getAttribute('data-idle-text') || label.textContent;
8329
+ }, ARM_EXPIRE_MS);
8330
+ return false;
8331
+ };
8133
8332
  window.COSWeb.today = {
8134
8333
  data: function () {
8135
8334
  return { needs: TODAY.needs, working: TODAY.working, open: TODAY.open, welcome: TODAY.welcome, offline: TODAY.offline, at: TODAY.at,
@@ -104,7 +104,9 @@
104
104
  .nav-btn:disabled { opacity: 0.4; }
105
105
  .crumbs { display: flex; align-items: center; min-width: 0; list-style: none; margin: 0 0 0 6px; padding: 0; font: 400 12.5px/1.2 var(--body); color: var(--sub); }
106
106
  .crumb { white-space: nowrap; }
107
- .crumb + .crumb::before { content: '›'; margin: 0 6px; color: var(--tertiary); }
107
+ /* 7.0.33 A11: the phone trail's look: '/' between steps, a step you can go back to in the accent colour. */
108
+ .crumb + .crumb::before { content: '/'; content: '/' / ''; margin: 0 6px; color: var(--tertiary); }
109
+ .crumb a { color: var(--accent); }
108
110
  .crumb.root { font-weight: 600; color: var(--ink); }
109
111
  .crumb.current { font-weight: 600; color: var(--ink); }
110
112
  .crumb.dim { color: var(--sub); }
@@ -246,6 +248,8 @@
246
248
  .cx-said-bubble { max-width: min(80%, 560px); background: color-mix(in srgb, var(--gold) 22%, var(--panel)); border: 1px solid var(--said-edge);
247
249
  border-radius: 18px 18px 6px 18px; padding: 10px 14px; line-height: 1.45; }
248
250
  .cx-said-who { font-size: 12px; color: var(--sub); margin: 4px 4px 0; }
251
+ /* 7.0.33 A2: COS's answer time, under its text on the left, as "You" sits under the question on the right. */
252
+ .cx-said-when { display: block; margin: 6px 0 0; }
249
253
  .cx-said.cos { display: grid; grid-template-columns: 30px minmax(0, 1fr); gap: 10px; align-items: start; margin: 0 0 14px; }
250
254
  .cx-said-text { min-width: 0; }
251
255
  .cx-card-body { font-size: 15px; line-height: 1.5; color: var(--ink); overflow-wrap: anywhere; }
@@ -115,7 +115,7 @@
115
115
  <form id="auth-form">
116
116
  <div id="auth-code-block">
117
117
  <label for="auth-code">Sign-in code</label>
118
- <input id="auth-code" class="field code-field" type="text" inputmode="text" autocomplete="one-time-code" autocapitalize="characters" spellcheck="false" maxlength="9" placeholder="K7P3-QX9M">
118
+ <input id="auth-code" class="field code-field" type="text" inputmode="text" autocomplete="one-time-code" autocapitalize="characters" spellcheck="false" maxlength="17" placeholder="K7P3-QX9M">
119
119
  <p class="auth-foot" style="margin-top:8px">Scan the QR with your phone's camera. A browser that is already signed in shows one under Settings, Use COS on another phone or browser. COS Control on your Mac shows one under Open COS on your phone. Or type the code shown with the QR.</p>
120
120
  <p class="auth-foot">The glasses pairing code does not work here.</p>
121
121
  </div>
@@ -155,12 +155,12 @@
155
155
 
156
156
  <div class="main-col">
157
157
  <!-- Control's navigation bar (ActivityWindow navigationBar): shown off Today, from 600px. -->
158
- <nav class="navbar hidden" id="navbar" aria-label="Where you are">
159
- <button type="button" class="nav-btn" id="nav-home" title="Activity home"><svg class="icon"><use href="#i-today"/></svg><span>Home</span></button>
158
+ <!-- 7.0.33 A10 and A11 (Anand round 2, slides 5 and 28): Back and the way you came, as on the phone. Home and
159
+ Settings left this bar: the rail has Today and Settings. -->
160
+ <nav class="navbar hidden" id="navbar" aria-label="How you got here">
160
161
  <button type="button" class="nav-btn" id="nav-back" title="Go back one step"><svg class="icon"><use href="#i-back"/></svg><span>Back</span></button>
161
162
  <ol class="crumbs" id="crumbs"></ol>
162
163
  <span class="grow"></span>
163
- <button type="button" class="nav-btn" id="nav-settings" aria-label="Open Activity settings"><svg class="icon"><use href="#i-settings"/></svg><span>Settings</span></button>
164
164
  </nav>
165
165
  <header class="topbar" id="topbar">
166
166
  <div class="topbar-left">
@@ -245,11 +245,13 @@
245
245
  <!-- SESSIONS -->
246
246
  <section id="pane-sessions" class="pane wide hidden" data-title="Sessions">
247
247
  <div class="cx sx" data-tint="sessions">
248
+ <!-- 7.0.33 A5 (Anand round 2, slide 17): the view switch comes first, so Sessions, Usage and COS cost sit in one place
249
+ on all three, right under the top bar; Usage's and COS cost's header follows it. -->
250
+ <div class="cx-controls sx-views" id="sessions-views"></div>
248
251
  <header class="cx-hero sx-hero" id="sessions-hero" data-tint="sessions">
249
252
  <span class="cx-glyph"><svg class="icon"><use href="#i-sessions"/></svg></span>
250
253
  <div class="cx-hero-text"><h1 class="cx-title">Sessions</h1><p class="cx-detail-line">Claude · Codex · Cursor · reading the last 7 days…</p></div>
251
254
  </header>
252
- <div class="cx-controls sx-views" id="sessions-views"></div>
253
255
  <div class="split sx-split" id="sessions-split">
254
256
  <div class="split-list" id="sessions-list-col">
255
257
  <!-- 7.0.23 step 2b: the list's own head (DESIGN_PLAN v2 "The list"): the Sessions menu, Search, one quiet line and the sort menu. -->