@gotcos/glasses-server 7.0.30 → 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.
- package/.env.example +10 -8
- package/CHANGELOG.md +207 -0
- package/bin/hooks/cos-session-hook +3 -2
- package/docs/pairing-fixture.json +1 -1
- package/package.json +3 -1
- package/server/index.ts +4 -2
- package/server/lib/api-auth.ts +3 -2
- package/server/lib/archive.ts +4 -2
- package/server/lib/claude-hooks-installer.ts +101 -15
- package/server/lib/codex-hooks-installer.ts +4 -1
- package/server/lib/full-hold.ts +61 -5
- package/server/lib/glasses-pairing.ts +2 -1
- package/server/lib/permission-broker.ts +130 -14
- package/server/lib/question-hold-setting.ts +38 -0
- package/server/lib/question-queue.ts +111 -0
- package/server/lib/session-hooks-runtime.ts +28 -3
- package/server/lib/session-title-cache.ts +40 -0
- package/server/lib/turn-end-ask.ts +51 -1
- package/server/lib/web-session-auth.ts +17 -0
- package/server/routes/agent-sessions.ts +3 -0
- package/server/routes/permission-broker.ts +31 -2
- package/server/routes/session-hooks.ts +23 -0
- package/server/routes/sessions.ts +3 -0
- package/server/web-control/.manifest.json +10 -10
- package/server/web-control/app.css +26 -3
- package/server/web-control/app.js +381 -36
- package/server/web-control/control.css +5 -1
- package/server/web-control/index.html +7 -5
- package/server/web-control/panes/cos-chat.css +21 -0
- package/server/web-control/panes/memories.js +18 -9
- package/server/web-control/panes/messages.js +15 -3
- package/server/web-control/panes/questions.js +6 -1
- package/server/web-control/panes/settings.js +18 -10
- package/server/web-control/shell.js +8 -13
|
@@ -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
|
-
//
|
|
625
|
-
//
|
|
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
|
|
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
|
|
@@ -742,7 +773,7 @@
|
|
|
742
773
|
// The rail changes the reply box's width: an empty box re-fits its placeholder (autoGrow, F2).
|
|
743
774
|
var si = typeof $ === 'function' ? $('session-input') : null; if (si && typeof sxPlaceholderFit === 'function' && si.phLong) sxPlaceholderFit(si); else if (si && !si.value && typeof autoGrow === 'function') autoGrow(si);
|
|
744
775
|
// A change after boot (a fold, a rotation, a new screen) is reported once, with the screen's own numbers.
|
|
745
|
-
if (RAIL.on !== null && RAIL.on !== on && typeof layoutCrumb === 'function') { var sc = window.screen || {}; layoutCrumb('rail', { sw: sc.width, sh: sc.height, rail: on, railHit: RAIL.hit }); }
|
|
776
|
+
if (RAIL.on !== null && RAIL.on !== on && typeof layoutCrumb === 'function') { var sc = window.screen || {}; layoutCrumb('rail', { sw: sc.width, sh: sc.height, rail: on, railHit: RAIL.hit, dvh: typeof lrProbe === 'function' ? lrProbe('height:100dvh', 'h') : null }); }
|
|
746
777
|
RAIL.on = on;
|
|
747
778
|
if (!RAIL.hooked && typeof window.matchMedia === 'function' && typeof window.addEventListener === 'function') {
|
|
748
779
|
RAIL.hooked = true;
|
|
@@ -782,6 +813,7 @@
|
|
|
782
813
|
|
|
783
814
|
var reading = r.args.length > 0 && !isTabRoute(r.args);
|
|
784
815
|
if (typeof sxFrameSync === 'function') sxFrameSync();
|
|
816
|
+
if (typeof rdFrameSync === 'function') rdFrameSync(r);
|
|
785
817
|
var paneEl = $('pane-' + r.pane);
|
|
786
818
|
if (paneEl) paneEl.classList.toggle('reading', reading);
|
|
787
819
|
var split = paneEl && paneEl.querySelector('.split');
|
|
@@ -806,8 +838,12 @@
|
|
|
806
838
|
if (typeof paintTrail === 'function') paintTrail(r);
|
|
807
839
|
closeMenu();
|
|
808
840
|
if (changedPane) window.scrollTo(0, 0);
|
|
809
|
-
|
|
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'));
|
|
810
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); }
|
|
811
847
|
|
|
812
848
|
if (!PANES[r.pane] && LAZY()[r.pane]) {
|
|
813
849
|
var lazyPane = r.pane, lazyEl = $('pane-' + lazyPane), wasEmpty = lazyEl && !lazyEl.getAttribute('data-lazy-ready');
|
|
@@ -860,16 +896,44 @@
|
|
|
860
896
|
var lastY = 0;
|
|
861
897
|
function onScroll() {
|
|
862
898
|
var y = Math.max(0, window.scrollY || 0);
|
|
863
|
-
// On a reader the bar stays (its Back is there).
|
|
864
|
-
//
|
|
865
|
-
var bar = $('topbar'), back = $('topbar-back'), onReader = !!(back && !back.classList.contains('hidden'))
|
|
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'));
|
|
866
902
|
bar.classList.toggle('scrolled', y > 8);
|
|
867
|
-
if (y < 60 || y < lastY - 4 || onReader) bar
|
|
903
|
+
if (y < 60 || y < lastY - 4 || onReader) barShow(bar);
|
|
868
904
|
else if (y > lastY + 4 && isPhone()) bar.classList.add('tucked');
|
|
869
905
|
lastY = y;
|
|
870
906
|
}
|
|
871
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(); }
|
|
872
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);
|
|
873
937
|
|
|
874
938
|
// ── auth ────────────────────────────────────────────────
|
|
875
939
|
// 7.0.19: the first paint (index.html #first-paint) ends the moment the sign-in card or the app shows, whichever
|
|
@@ -905,6 +969,7 @@
|
|
|
905
969
|
// hidden, so the gate could not scroll and Connect sat below a keyboard-height screen. The gate is a normal page;
|
|
906
970
|
// showShell's route() puts the frame back (sxFrameSync) when the shell returns.
|
|
907
971
|
document.documentElement.classList.remove('sx-frame');
|
|
972
|
+
document.documentElement.classList.remove('rd-frame');
|
|
908
973
|
// A later 401 from a pane still polling (W.api passes no message) must not blank what the gate already says:
|
|
909
974
|
// the reason it opened, or "That code did not match" while the person retypes.
|
|
910
975
|
if (!message && alreadyOpen) return;
|
|
@@ -1431,17 +1496,19 @@
|
|
|
1431
1496
|
function petPrefs() {
|
|
1432
1497
|
var p = readStore(PET_KEY);
|
|
1433
1498
|
var pet = typeof p.pet === 'string' && /^(mac|(shipped|gallery):[a-z0-9][a-z0-9-]{0,80})$/.test(p.pet) ? p.pet : 'mac';
|
|
1434
|
-
|
|
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' };
|
|
1435
1501
|
}
|
|
1436
1502
|
function setPetPrefs(next) {
|
|
1437
1503
|
var cur = petPrefs();
|
|
1438
|
-
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 };
|
|
1439
1505
|
writeStore(PET_KEY, merged);
|
|
1440
1506
|
// Forget the old pet's manifest and loop; Today repaints with the new choice next time it shows.
|
|
1441
1507
|
PET.manifest = null; PET.fetchedAt = 0; PET.pose = null; PET.token++; petStop();
|
|
1442
1508
|
var box = $('today-pet');
|
|
1443
1509
|
if (box) { box.innerHTML = ''; box.classList.add('hidden'); }
|
|
1444
|
-
|
|
1510
|
+
placePet();
|
|
1511
|
+
if ((state.pane === 'today' || petInBar()) && state.sessions) renderPet(state.sessions);
|
|
1445
1512
|
return merged;
|
|
1446
1513
|
}
|
|
1447
1514
|
var PET_HEIGHT = 140;
|
|
@@ -1558,13 +1625,72 @@
|
|
|
1558
1625
|
// Folded (one column) the pet sits above the story. Unfolded (two columns, the same 760px line as
|
|
1559
1626
|
// .today-cols in app.css) it moves to the top of the right-hand column, above Needs you (Miles 11/06).
|
|
1560
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()); }
|
|
1561
1634
|
function placePet() {
|
|
1562
1635
|
var box = $('today-pet'), side = document.querySelector('#pane-today .today-side'), rec = $('today-rec');
|
|
1563
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();
|
|
1564
1640
|
if (PET_WIDE && PET_WIDE.matches) { if (side.firstElementChild !== box) side.insertBefore(box, side.firstElementChild); }
|
|
1565
1641
|
else if (rec.previousElementSibling !== box) rec.parentNode.insertBefore(box, rec);
|
|
1566
1642
|
}
|
|
1567
|
-
|
|
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
|
+
}
|
|
1568
1694
|
var PET_CHANGE = '<button type="button" class="pet-change" data-goto="settings/pet">Change pet</button>';
|
|
1569
1695
|
// A still pet (a gallery pick, Cloud Puff, or a single image on the Mac): one picture for every state.
|
|
1570
1696
|
function renderPetStill(box, m, sessions) {
|
|
@@ -1624,9 +1750,11 @@
|
|
|
1624
1750
|
if (still || spec.frames < 2) return;
|
|
1625
1751
|
var step = function () {
|
|
1626
1752
|
// Runs only while Today is on screen: leaving the pane or hiding the page stops the loop.
|
|
1627
|
-
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; }
|
|
1628
1754
|
PET.frame = (PET.frame + 1) % spec.frames;
|
|
1629
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);
|
|
1630
1758
|
PET.timer = setTimeout(step, Math.max(20, spec.interval * 1000));
|
|
1631
1759
|
};
|
|
1632
1760
|
PET.timer = setTimeout(step, Math.max(20, spec.interval * 1000));
|
|
@@ -1860,7 +1988,10 @@
|
|
|
1860
1988
|
var write = typeof storyWrite === 'function' ? storyWrite : function (e, kind, text, when) {
|
|
1861
1989
|
if (kind === 'brief') e.innerHTML = esc(text) + '<span class="story-when">' + esc(when) + '</span>'; else e.textContent = text;
|
|
1862
1990
|
};
|
|
1863
|
-
|
|
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; }
|
|
1864
1995
|
// The model's summary when there is one (it never blocks: the counts line below shows until it lands). The test
|
|
1865
1996
|
// harnesses that run this block alone have no requestBrief, so it is looked up, never assumed.
|
|
1866
1997
|
askBox(null);
|
|
@@ -1868,7 +1999,7 @@
|
|
|
1868
1999
|
requestBrief(m);
|
|
1869
2000
|
var brief = briefShown();
|
|
1870
2001
|
askBox(brief);
|
|
1871
|
-
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; }
|
|
1872
2003
|
}
|
|
1873
2004
|
var first = '';
|
|
1874
2005
|
if (m.sessionsWaiting > 0) first = countWord(m.sessionsWaiting) + ' ' + plural(m.sessionsWaiting, 'session is', 'sessions are') + ' waiting on you';
|
|
@@ -1889,7 +2020,11 @@
|
|
|
1889
2020
|
else second = 'No agents are running';
|
|
1890
2021
|
if (TODAY.recording) second += (unread || working > 0 || open > 0 ? ', and ' : '. ') + 'COS is recording a meeting';
|
|
1891
2022
|
var out = (first ? first + '. ' : '') + second + '.';
|
|
1892
|
-
|
|
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);
|
|
1893
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.';
|
|
1894
2029
|
else if (unread) out += ' This may not be everything that needs you.';
|
|
1895
2030
|
write(el, 'counts', out);
|
|
@@ -1919,6 +2054,10 @@
|
|
|
1919
2054
|
$('run-count').textContent = working.length + jobs.n ? String(working.length + jobs.n) : '';
|
|
1920
2055
|
var list = $('run-list');
|
|
1921
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)));
|
|
1922
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; }
|
|
1923
2062
|
var shown = working.slice(0, 3);
|
|
1924
2063
|
var html = jobs.html + shown.map(function (s) { return runRow(s, true); }).join('');
|
|
@@ -2424,12 +2563,18 @@
|
|
|
2424
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>';
|
|
2425
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
|
|
2426
2565
|
// source: with the Mac not answering (sessions and Work both unread, or more than two sources) it says so once.
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
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>';
|
|
2431
2574
|
}
|
|
2432
2575
|
list.innerHTML = html;
|
|
2576
|
+
var card = list.closest ? list.closest('.card') : null;
|
|
2577
|
+
if (card && card.classList) card.classList.toggle('hidden', !html);
|
|
2433
2578
|
}
|
|
2434
2579
|
// Weather sits under the date at the top left (Miles, 11/07), where the eye starts, not inside Next up.
|
|
2435
2580
|
// It shows whether or not a meeting is next.
|
|
@@ -2762,7 +2907,7 @@
|
|
|
2762
2907
|
view: sxPref('view', 'sessions', ['sessions', 'usage', 'cost']),
|
|
2763
2908
|
recency: sxPref('recency', 'newest', ['newest', 'oldest', 'match']),
|
|
2764
2909
|
q: '', instant: [], remote: [], searching: false, searchError: '', ranFor: '', semantic: null, semanticReason: '', searchSeq: 0, searchTimer: null,
|
|
2765
|
-
hooks: null, hooksAt: 0,
|
|
2910
|
+
hooks: null, hooksAt: 0, hooksOffer: false, offerPutAway: false,
|
|
2766
2911
|
usage: null, usageAt: 0, usageLoading: false, usageError: '',
|
|
2767
2912
|
uPeriod: 'Last 30 days', uTool: 'All tools', uModel: 'All models',
|
|
2768
2913
|
// Usage on a wide screen (uWideModel): the custom range, the chart sentence, the chosen bar, the table's order.
|
|
@@ -2900,7 +3045,8 @@
|
|
|
2900
3045
|
var stats = view === 'sessions' ? sxStats(vis) : [];
|
|
2901
3046
|
var busy = view === 'sessions' ? SX.loading : SX.usageLoading;
|
|
2902
3047
|
hero.innerHTML = '<span class="cx-glyph"><svg class="icon"><use href="#i-sessions"/></svg></span>' +
|
|
2903
|
-
|
|
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>' +
|
|
2904
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>' +
|
|
2905
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>' : '');
|
|
2906
3052
|
// 7.0.23 step 2b: Updated and Opened are the sort menu now, and Pinned a group; no clock switches here.
|
|
@@ -2935,6 +3081,150 @@
|
|
|
2935
3081
|
if (on) sxFrameHook();
|
|
2936
3082
|
return on;
|
|
2937
3083
|
}
|
|
3084
|
+
// 7.0.31 hotfix (Miles 17:25, the open Duo): the message reader in its own frame from 600 px. html.rd-frame makes the
|
|
3085
|
+
// page one screen, as Sessions' frame does (panes/cos-chat.css): the app shell is the visible box (--vv-top, --vv-h
|
|
3086
|
+
// from viewportFit, which falls back to innerHeight when the visual viewport reads 0 or less), and the reader scrolls
|
|
3087
|
+
// in the workspace under the navigation bar, with Reply pinned to its bottom. Before, the reader was the document: its Reply stuck to the
|
|
3088
|
+
// document's scrollport and the rail was 100dvh, so when Safari's viewport went stale after folds (his Duo reported
|
|
3089
|
+
// visual viewport heights of 0 and -33554432 from 17:17 to 17:24) both ended mid-screen and the message drew below
|
|
3090
|
+
// them. Below 600 px (phones, the folded Duo's side rail) the reader is unchanged; the CSS is gated the same way, so a
|
|
3091
|
+
// class left on across a fold does nothing there. A message's image (#/messages/m/<no>/a/<id>) has no Reply: no frame.
|
|
3092
|
+
function rdFrameOn(r) {
|
|
3093
|
+
var shell = $('app-shell'), wide = !!(window.matchMedia && window.matchMedia('(min-width: 600px)').matches);
|
|
3094
|
+
return !!(r && r.pane === 'messages' && r.args && r.args[0] === 'm' && r.args.length === 2 && wide && !(shell && shell.classList.contains('hidden')));
|
|
3095
|
+
}
|
|
3096
|
+
var RD_FRAME = { hooked: false, n: 0 };
|
|
3097
|
+
function rdFrameSync(r) {
|
|
3098
|
+
var de = document.documentElement, on = rdFrameOn(r || parseRoute());
|
|
3099
|
+
if (de && de.classList) de.classList.toggle('rd-frame', on);
|
|
3100
|
+
if (!RD_FRAME.hooked && window.matchMedia) {
|
|
3101
|
+
RD_FRAME.hooked = true;
|
|
3102
|
+
var mq = window.matchMedia('(min-width: 600px)');
|
|
3103
|
+
if (mq && mq.addEventListener) mq.addEventListener('change', function () { rdFrameSync(); }); else if (mq && mq.addListener) mq.addListener(function () { rdFrameSync(); });
|
|
3104
|
+
}
|
|
3105
|
+
var r2 = r || parseRoute();
|
|
3106
|
+
rdPosHook();
|
|
3107
|
+
// A different message just opened: its place is its top until he scrolls. (This also runs when the width crosses
|
|
3108
|
+
// 600 px; the same message then keeps its anchor and its pending restore.)
|
|
3109
|
+
if (window.requestAnimationFrame) window.requestAnimationFrame(function () {
|
|
3110
|
+
var cx = rdPosReader(), key = cx ? cx.getAttribute('data-reader-key') : '';
|
|
3111
|
+
if (!RD_POS.a || RD_POS.a.key !== key) { RD_POS.pending = false; rdPosCapture(); }
|
|
3112
|
+
});
|
|
3113
|
+
if (r2 && r2.pane === 'messages' && r2.args && r2.args[0] === 'm' && r2.args.length === 2 && window.requestAnimationFrame) window.requestAnimationFrame(function () { rdFrameCrumb('open'); });
|
|
3114
|
+
return on;
|
|
3115
|
+
}
|
|
3116
|
+
// One report per message reader opened, at any width, at most 8 a page load (numbers only; lead 17:48): whether the frame
|
|
3117
|
+
// is on, the visible box viewportFit uses, the reader's box (top, bottom, height), the workspace's height and content height (the frame's scroller), Reply's top
|
|
3118
|
+
// and bottom, the document's height and scroll, and 100dvh. layoutCrumb adds innerWidth and innerHeight and the visual
|
|
3119
|
+
// viewport's height, top and scale, so a pan like 17:25's (top about 409) shows in the same row. The 17:25 frame could
|
|
3120
|
+
// not be measured; the next one will be.
|
|
3121
|
+
function rdFrameCrumb(why) {
|
|
3122
|
+
if (RD_FRAME.n >= 8 || typeof layoutCrumb !== 'function') return false;
|
|
3123
|
+
try {
|
|
3124
|
+
var v = viewportFit(), f = document.querySelector('#pane-messages .cx-reply-form'), cx = document.querySelector('#pane-messages > .cx'), ws = $('workspace');
|
|
3125
|
+
var fr = f && f.getBoundingClientRect ? f.getBoundingClientRect() : null, cr = cx && cx.getBoundingClientRect ? cx.getBoundingClientRect() : null;
|
|
3126
|
+
var se = document.scrollingElement || document.documentElement;
|
|
3127
|
+
var out = { why: String(why || '').slice(0, 24), frame: !!(document.documentElement.classList && document.documentElement.classList.contains('rd-frame')), fb: !!v.bad,
|
|
3128
|
+
boxT: v.top, boxB: v.top + v.height, rdT: cr ? cr.top : -1, rdB: cr ? cr.bottom : -1, rdH: cx ? cx.clientHeight : -1, wsH: ws ? ws.clientHeight : -1, wsSH: ws ? ws.scrollHeight : -1,
|
|
3129
|
+
replyT: fr ? fr.top : -1, replyB: fr ? fr.bottom : -1, docH: se ? se.scrollHeight : -1, sy: window.scrollY || 0, dvh: typeof lrProbe === 'function' ? lrProbe('height:100dvh', 'h') : null };
|
|
3130
|
+
if (layoutCrumb('reader', out)) { RD_FRAME.n++; return true; }
|
|
3131
|
+
} catch (_e) { /* a report never stops the page */ }
|
|
3132
|
+
return false;
|
|
3133
|
+
}
|
|
3134
|
+
// 7.0.31 (Q1 FIX FIRST, lead 20:41): ONE job. A fold or an unfold that crosses 600 px switches the message between the
|
|
3135
|
+
// page's scroll (under 600) and the frame's workspace (600 and up), and the new scroller would open at the top. So the
|
|
3136
|
+
// page keeps an anchor, the character at the top of the reading area, read as he scrolls, and applies it ONCE after a
|
|
3137
|
+
// switch: when no finger is down and the scroll has been still for RD_POS_STILL_MS. If he scrolls on purpose first (a
|
|
3138
|
+
// touch that moves, the wheel, a key), his scroll wins and the anchor is dropped. Nothing else moves the text: no
|
|
3139
|
+
// other resize, no keyboard, no card or notice. Under 600 px with no switch, the reader is 7.0.30's.
|
|
3140
|
+
var RD_POS_STILL_MS = 300, RD_POS_MOVE_PX = 10;
|
|
3141
|
+
var RD_POS = { a: null, mode: '', pending: false, timer: 0, touching: false, ty: 0, moved: false, scrollAt: 0, queued: false, hooked: false };
|
|
3142
|
+
function rdPosReader() {
|
|
3143
|
+
var r = parseRoute();
|
|
3144
|
+
return r && r.pane === 'messages' && r.args && r.args[0] === 'm' && r.args.length === 2 ? document.querySelector('#pane-messages > .cx[data-reader-key]') : null;
|
|
3145
|
+
}
|
|
3146
|
+
function rdPosMode() { var ws = $('workspace'); return ws && getComputedStyle(ws).overflowY === 'auto' ? 'frame' : 'page'; }
|
|
3147
|
+
// The top of the reading area: the workspace's top in the frame; on the page, below the pinned top bar.
|
|
3148
|
+
function rdPosTop(mode) {
|
|
3149
|
+
if (mode === 'frame') return $('workspace').getBoundingClientRect().top;
|
|
3150
|
+
var t = 0;
|
|
3151
|
+
['.topbar', '.navbar'].forEach(function (sel) {
|
|
3152
|
+
var e = document.querySelector(sel), st = e && getComputedStyle(e), r = e && e.getBoundingClientRect();
|
|
3153
|
+
if (e && st.display !== 'none' && r.height > 0 && r.bottom > 0 && (st.position === 'sticky' || st.position === 'fixed')) t = Math.max(t, r.bottom);
|
|
3154
|
+
});
|
|
3155
|
+
return t;
|
|
3156
|
+
}
|
|
3157
|
+
// One character's line box (a collapsed range has none in every engine).
|
|
3158
|
+
function rdPosRect(node, offset) {
|
|
3159
|
+
var len = node && node.nodeValue ? node.nodeValue.length : 0;
|
|
3160
|
+
if (!len) return null;
|
|
3161
|
+
var rg = document.createRange(), at = Math.min(offset, len - 1);
|
|
3162
|
+
rg.setStart(node, at); rg.setEnd(node, at + 1);
|
|
3163
|
+
var r = rg.getBoundingClientRect();
|
|
3164
|
+
return r.height || r.width ? r : null;
|
|
3165
|
+
}
|
|
3166
|
+
function rdPosCapture() {
|
|
3167
|
+
var cx = rdPosReader();
|
|
3168
|
+
if (!cx) { RD_POS.a = null; RD_POS.mode = ''; return null; }
|
|
3169
|
+
var mode = rdPosMode(), top = rdPosTop(mode), key = cx.getAttribute('data-reader-key');
|
|
3170
|
+
RD_POS.mode = mode;
|
|
3171
|
+
if ((mode === 'frame' ? $('workspace').scrollTop : window.scrollY || 0) <= 2) return (RD_POS.a = { key: key, atTop: true });
|
|
3172
|
+
var c = cx.getBoundingClientRect(), body = cx.querySelector('.cx-said.cos .cx-card-body'), xs = [c.left + 8, c.left + c.width / 2];
|
|
3173
|
+
if (body) xs.unshift(body.getBoundingClientRect().left + 6);
|
|
3174
|
+
for (var dy = 2; dy <= 62; dy += 12) {
|
|
3175
|
+
for (var i = 0; i < xs.length; i++) {
|
|
3176
|
+
var hit = document.caretRangeFromPoint ? document.caretRangeFromPoint(xs[i], top + dy) : null, n = hit && hit.startContainer;
|
|
3177
|
+
if (!n || n.nodeType !== 3 || !cx.contains(n) || n.parentElement.closest('.cx-reply-form')) continue;
|
|
3178
|
+
var r = rdPosRect(n, hit.startOffset);
|
|
3179
|
+
if (r) return (RD_POS.a = { key: key, node: n, offset: hit.startOffset, dy: r.top - top });
|
|
3180
|
+
}
|
|
3181
|
+
}
|
|
3182
|
+
return null;
|
|
3183
|
+
}
|
|
3184
|
+
function rdPosApply() {
|
|
3185
|
+
var cx = rdPosReader(), a = RD_POS.a, mode = rdPosMode(), ws = $('workspace');
|
|
3186
|
+
RD_POS.mode = mode;
|
|
3187
|
+
if (!cx || !a || a.key !== cx.getAttribute('data-reader-key')) return false;
|
|
3188
|
+
if (a.atTop) { if (mode === 'frame') ws.scrollTop = 0; else window.scrollTo(0, 0); return true; }
|
|
3189
|
+
var r = a.node.isConnected ? rdPosRect(a.node, a.offset) : null;
|
|
3190
|
+
if (!r) return false;
|
|
3191
|
+
var want = Math.round(r.top - rdPosTop(mode) - a.dy);
|
|
3192
|
+
if (mode === 'frame') ws.scrollTop += want; else window.scrollBy(0, want);
|
|
3193
|
+
return true;
|
|
3194
|
+
}
|
|
3195
|
+
// The scroller changed under him: wait for no finger and a still scroll, then apply once; his own scroll drops it.
|
|
3196
|
+
function rdPosTick() {
|
|
3197
|
+
RD_POS.timer = 0;
|
|
3198
|
+
if (!RD_POS.pending) return;
|
|
3199
|
+
if (RD_POS.moved) { RD_POS.pending = false; rdPosCapture(); return; }
|
|
3200
|
+
if (RD_POS.touching || Date.now() - RD_POS.scrollAt < RD_POS_STILL_MS) { RD_POS.timer = setTimeout(rdPosTick, 80); return; }
|
|
3201
|
+
RD_POS.pending = false;
|
|
3202
|
+
rdPosApply();
|
|
3203
|
+
}
|
|
3204
|
+
function rdPosCheck() {
|
|
3205
|
+
var mode = rdPosReader() ? rdPosMode() : '';
|
|
3206
|
+
if (!mode || !RD_POS.mode || mode === RD_POS.mode || RD_POS.pending) return;
|
|
3207
|
+
RD_POS.pending = true; RD_POS.moved = false;
|
|
3208
|
+
if (!RD_POS.timer) RD_POS.timer = setTimeout(rdPosTick, 80);
|
|
3209
|
+
}
|
|
3210
|
+
function rdPosHook() {
|
|
3211
|
+
if (RD_POS.hooked || !document.addEventListener) return;
|
|
3212
|
+
RD_POS.hooked = true;
|
|
3213
|
+
var o = { capture: true, passive: true };
|
|
3214
|
+
document.addEventListener('scroll', function () {
|
|
3215
|
+
RD_POS.scrollAt = Date.now();
|
|
3216
|
+
if (!rdPosReader()) return;
|
|
3217
|
+
rdPosCheck();
|
|
3218
|
+
if (RD_POS.pending || RD_POS.queued) return;
|
|
3219
|
+
RD_POS.queued = true;
|
|
3220
|
+
requestAnimationFrame(function () { RD_POS.queued = false; rdPosCheck(); if (!RD_POS.pending) rdPosCapture(); });
|
|
3221
|
+
}, o);
|
|
3222
|
+
window.addEventListener('resize', rdPosCheck);
|
|
3223
|
+
document.addEventListener('touchstart', function (e) { RD_POS.touching = true; RD_POS.ty = e.touches && e.touches[0] ? e.touches[0].clientY : 0; }, o);
|
|
3224
|
+
document.addEventListener('touchmove', function (e) { var y = e.touches && e.touches[0] ? e.touches[0].clientY : RD_POS.ty; if (RD_POS.pending && Math.abs(y - RD_POS.ty) > RD_POS_MOVE_PX) RD_POS.moved = true; }, o);
|
|
3225
|
+
['touchend', 'touchcancel'].forEach(function (t) { document.addEventListener(t, function () { RD_POS.touching = false; }, o); });
|
|
3226
|
+
['wheel', 'keydown'].forEach(function (t) { document.addEventListener(t, function () { if (RD_POS.pending) RD_POS.moved = true; }, o); });
|
|
3227
|
+
}
|
|
2938
3228
|
// Where the frame's parts sit against the VISIBLE area (step 1's position report, lead 23:00): on a reader's open, and
|
|
2939
3229
|
// KB_AFTER_MS after an on-screen keyboard closes while the frame is on. The visible area is viewportFit's box (a
|
|
2940
3230
|
// visible height under 80 px is no reading, so it is innerHeight from 0, and fb says so) and, across, visualViewport's
|
|
@@ -3730,17 +4020,40 @@
|
|
|
3730
4020
|
if (Date.now() - SX.hooksAt < 60000) { paintHooks(); return; }
|
|
3731
4021
|
SX.hooksAt = Date.now();
|
|
3732
4022
|
api('/api/session-hooks/status').then(function (d) {
|
|
3733
|
-
SX.hooks = (d && d.sessionHooks && d.sessionHooks.state) || null;
|
|
3734
|
-
|
|
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 */ }
|
|
3735
4037
|
}
|
|
3736
4038
|
function paintHooks() {
|
|
3737
4039
|
var el = $('sessions-hooks'); if (!el) return;
|
|
3738
4040
|
var st = SX.hooks;
|
|
3739
|
-
if (['missing', 'drift', 'script_outdated', 'route_absent'].indexOf(st) < 0) {
|
|
3740
|
-
|
|
3741
|
-
|
|
3742
|
-
|
|
3743
|
-
|
|
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') + '.';
|
|
3744
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>';
|
|
3745
4058
|
}
|
|
3746
4059
|
|
|
@@ -7489,13 +7802,18 @@
|
|
|
7489
7802
|
(recent.length ? '<h2 class="group-title">Recent sessions</h2><div class="list">' + recent.map(function (s) { return sessionRow(s); }).join('') + '</div>' : '');
|
|
7490
7803
|
return;
|
|
7491
7804
|
}
|
|
7492
|
-
|
|
7493
|
-
|
|
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 : '') +
|
|
7494
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>' +
|
|
7495
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>' +
|
|
7496
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>' +
|
|
7497
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>' +
|
|
7498
|
-
|
|
7816
|
+
(placeFirst ? '' : jumpRows) +
|
|
7499
7817
|
'</div><div id="ask-hits"></div><p class="status-line" id="ask-status"></p>';
|
|
7500
7818
|
paintAskRow();
|
|
7501
7819
|
clearTimeout(renderAsk.t);
|
|
@@ -7545,7 +7863,12 @@
|
|
|
7545
7863
|
return;
|
|
7546
7864
|
}
|
|
7547
7865
|
if (t.hasAttribute('data-open-ask')) return openAsk();
|
|
7548
|
-
if (t.hasAttribute('data-today'))
|
|
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();
|
|
7549
7872
|
if (t.hasAttribute('data-today-next')) return prepNext();
|
|
7550
7873
|
if (t.hasAttribute('data-next-prep')) return toggleNextPrep(t.getAttribute('data-next-prep'));
|
|
7551
7874
|
if (t.hasAttribute('data-next-prep-cancel')) return closeNextPrep(true);
|
|
@@ -7559,6 +7882,8 @@
|
|
|
7559
7882
|
}
|
|
7560
7883
|
if (t.hasAttribute('data-theme-choice')) { window.CosTheme.set(t.getAttribute('data-theme-choice')); paintAppearance(); return; }
|
|
7561
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();
|
|
7562
7887
|
if (inSheet) closeAsk(); closeMenu();
|
|
7563
7888
|
// "work/sort" is a pane and its arguments, not one name.
|
|
7564
7889
|
var dest = String(t.getAttribute('data-goto') || t.getAttribute('data-pane')).split('/');
|
|
@@ -7577,7 +7902,7 @@
|
|
|
7577
7902
|
if (action === 'drop-after') return dropHeld();
|
|
7578
7903
|
if (action === 'unqueue') return removeQueued(t.getAttribute('data-turn-key'));
|
|
7579
7904
|
if (action === 'revoke-all') {
|
|
7580
|
-
if (
|
|
7905
|
+
if (!window.COSWeb.armTap(t, t.querySelector('.grow').firstChild, 'Tap again to sign out every browser')) return;
|
|
7581
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); });
|
|
7582
7907
|
return;
|
|
7583
7908
|
}
|
|
@@ -7944,6 +8269,9 @@
|
|
|
7944
8269
|
// once a detail has loaded; route() clears it on every navigation.
|
|
7945
8270
|
setCrumb: function (text) { crumbDetail = String(text || ''); navLabel(crumbDetail); paintTrail(); runHooks(routeHooks, parseRoute()); },
|
|
7946
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') }; },
|
|
7947
8275
|
// Panes that read with their own fetch call this on a 401 so the sign-in screen shows.
|
|
7948
8276
|
showAuth: function (message) { showAuth(message); },
|
|
7949
8277
|
// Control's session state (the Sessions pane's sxState) and warm-up test, so the rail's
|
|
@@ -7978,12 +8306,29 @@
|
|
|
7978
8306
|
// Today as a place to act (panes/today-actions.js, Miles 11/06): what the Today cards show, a repaint after a
|
|
7979
8307
|
// decision, and the Sessions reader's own pieces, so the overlay sends and reads through the reader's code.
|
|
7980
8308
|
window.COSWeb.pane = function (name) { return PANES[name] || null; };
|
|
8309
|
+
window.COSWeb.plainMessage = plainMessage; window.COSWeb.notSetUp = notSetUp;
|
|
7981
8310
|
// Tap, then tap again (Today's Dismiss, Stop and Send anyway; a question card's Allow, Deny and Send): the second tap
|
|
7982
8311
|
// counts only once the first has been on screen ARM_CONFIRM_MS, so a double tap or a finger bounce arms and never
|
|
7983
8312
|
// confirms (W2-r2). The one rule both panes/today-actions.js and panes/questions.js read.
|
|
7984
8313
|
var ARM_CONFIRM_MS = 400;
|
|
7985
8314
|
window.COSWeb.armConfirmMs = ARM_CONFIRM_MS;
|
|
7986
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
|
+
};
|
|
7987
8332
|
window.COSWeb.today = {
|
|
7988
8333
|
data: function () {
|
|
7989
8334
|
return { needs: TODAY.needs, working: TODAY.working, open: TODAY.open, welcome: TODAY.welcome, offline: TODAY.offline, at: TODAY.at,
|