@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.
Files changed (34) hide show
  1. package/.env.example +10 -8
  2. package/CHANGELOG.md +207 -0
  3. package/bin/hooks/cos-session-hook +3 -2
  4. package/docs/pairing-fixture.json +1 -1
  5. package/package.json +3 -1
  6. package/server/index.ts +4 -2
  7. package/server/lib/api-auth.ts +3 -2
  8. package/server/lib/archive.ts +4 -2
  9. package/server/lib/claude-hooks-installer.ts +101 -15
  10. package/server/lib/codex-hooks-installer.ts +4 -1
  11. package/server/lib/full-hold.ts +61 -5
  12. package/server/lib/glasses-pairing.ts +2 -1
  13. package/server/lib/permission-broker.ts +130 -14
  14. package/server/lib/question-hold-setting.ts +38 -0
  15. package/server/lib/question-queue.ts +111 -0
  16. package/server/lib/session-hooks-runtime.ts +28 -3
  17. package/server/lib/session-title-cache.ts +40 -0
  18. package/server/lib/turn-end-ask.ts +51 -1
  19. package/server/lib/web-session-auth.ts +17 -0
  20. package/server/routes/agent-sessions.ts +3 -0
  21. package/server/routes/permission-broker.ts +31 -2
  22. package/server/routes/session-hooks.ts +23 -0
  23. package/server/routes/sessions.ts +3 -0
  24. package/server/web-control/.manifest.json +10 -10
  25. package/server/web-control/app.css +26 -3
  26. package/server/web-control/app.js +381 -36
  27. package/server/web-control/control.css +5 -1
  28. package/server/web-control/index.html +7 -5
  29. package/server/web-control/panes/cos-chat.css +21 -0
  30. package/server/web-control/panes/memories.js +18 -9
  31. package/server/web-control/panes/messages.js +15 -3
  32. package/server/web-control/panes/questions.js +6 -1
  33. package/server/web-control/panes/settings.js +18 -10
  34. package/server/web-control/shell.js +8 -13
@@ -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. -->
@@ -220,6 +220,27 @@ html.kb-up .cc-kb .cc-model .field { width: 100%; }
220
220
  html.kb-reply; app.js publishes --vv-below, the layout viewport under the visible box) it sits on the keyboard, the
221
221
  same way the chat's box does; the home indicator is under the keyboard, so its inset goes. */
222
222
  html.kb-reply .cx-reply-form { bottom: var(--vv-below, 0px); padding-bottom: 8px; }
223
+ /* 7.0.31 hotfix (Miles 17:25, the open Duo): from 600 px the message reader is its own frame (app.js rdFrameSync sets
224
+ html.rd-frame on #/messages/m/<no>). The page is one screen, never taller, as in Sessions (panes/sessions.css): the
225
+ app shell is the visible box and the rail a column of it that scrolls by itself. The workspace under the navigation
226
+ bar is the one thing that scrolls: the sign-in notice, a question card and the message scroll together as they did
227
+ in 7.0.30, and Reply sticks to the workspace's bottom. Before, the reader was the document, so Reply stuck to the
228
+ document's viewport and the rail was 100dvh: with Safari's view left panned or short after folds, both ended
229
+ mid-screen and the message drew below them. Nothing here reaches under 600 px, where the phone and the folded Duo
230
+ keep the reader as shipped. */
231
+ @media (min-width: 600px) {
232
+ html.rd-frame, html.rd-frame body { height: 100%; overflow: hidden; overscroll-behavior: none; }
233
+ html.rd-frame body { min-height: 0; }
234
+ html.rd-frame .app-shell { position: fixed; left: 0; right: 0; top: var(--vv-top, 0px); height: var(--vv-h, 100dvh); min-height: 0;
235
+ grid-template-rows: minmax(0, 1fr); overflow: hidden; }
236
+ html.rd-frame .rail { position: static; top: auto; height: auto; min-height: 0; overflow-y: auto; overscroll-behavior: contain; }
237
+ html.rd-frame .main-col { min-height: 0; min-width: 0; display: flex; flex-direction: column; overflow: hidden; }
238
+ html.rd-frame .topbar, html.rd-frame .navbar { flex: none; position: relative; }
239
+ /* The scroller. No bottom padding: at the end of the message, Reply is the last thing and sits on the frame's bottom. */
240
+ html.rd-frame .workspace { flex: 1 1 auto; min-height: 0; min-width: 0; overflow-y: auto; overscroll-behavior: contain; padding-bottom: 0; }
241
+ /* With the keyboard up the frame is already the visible box (--vv-top, --vv-h), so the box stays at the frame's bottom. */
242
+ html.rd-frame.kb-reply .cx-reply-form { bottom: 0; }
243
+ }
223
244
 
224
245
  /* ── the Messages badge in the rail and the section menu ─ */
225
246
  .inbox-count { min-width: 18px; height: 18px; padding: 0 5px; border-radius: 9px; background: var(--hot); color: var(--panel); font: 600 11px/18px var(--body); text-align: center; }
@@ -123,7 +123,8 @@
123
123
  return W.api('/api/web-bridge/memories', { method: 'POST', body: { id: ++seq, op: op, args: a }, timeoutMs: limit }).then(function (r) {
124
124
  if (r && r.ok) return r.details || {};
125
125
  var raw = (r && r.message) || 'Your Mac did not answer that.';
126
- var err = new Error(PLAIN[raw] || raw); err.code = raw; throw err;
126
+ // 7.0.33 A8: a code this pane has no words for goes through the page's plain words, never on screen as is.
127
+ var err = new Error(PLAIN[raw] || (W.plainMessage ? W.plainMessage(raw) : raw)); err.code = raw; throw err;
127
128
  });
128
129
  }
129
130
  // A request that never reached COS is said in plain words by W.api itself (app.js, 6.99.30: code 'unreachable'),
@@ -217,7 +218,7 @@
217
218
  seedDecisions(r.rows); prefetchLessons(r.rows);
218
219
  }, function (e) {
219
220
  if (serial !== serials.recent) return;
220
- if (more) W.toast(e.message); else { L.error = e.message; L.loaded = true; }
221
+ if (more) W.toast(e.message); else { L.error = e.message; L.errorCode = e.code || ''; L.loaded = true; }
221
222
  }).then(function () { if (serial === serials.recent) L.loading = false; paint(); });
222
223
  }
223
224
  function loadReview(more) {
@@ -239,7 +240,7 @@
239
240
  seedDecisions(r.rows);
240
241
  }, function (e) {
241
242
  if (serial !== serials.review) return;
242
- if (more) W.toast(e.message + ' Refresh To review to start again.'); else { L.error = e.message; L.loaded = true; }
243
+ if (more) W.toast(e.message + ' Refresh To review to start again.'); else { L.error = e.message; L.errorCode = e.code || ''; L.loaded = true; }
243
244
  }).then(function () { if (serial === serials.review) L.loading = false; paint(); });
244
245
  }
245
246
  var memTimer = null;
@@ -267,7 +268,7 @@
267
268
  L.loaded = true; L.error = '';
268
269
  }, function (e) {
269
270
  if (serial !== serials.mem) return;
270
- if (more) W.toast(e.message); else { L.error = e.message; L.loaded = true; }
271
+ if (more) W.toast(e.message); else { L.error = e.message; L.errorCode = e.code || ''; L.loaded = true; }
271
272
  }).then(function () { if (serial === serials.mem) L.loading = false; paint(); });
272
273
  }
273
274
  function memQuery(q) {
@@ -777,7 +778,8 @@
777
778
  if (n) parts.push('<span><b>' + fmt(n) + '</b> ' + plural(n, 'change', 'changes') + ' to review</span>');
778
779
  if (S.recent.total != null) parts.push('<span><b>' + fmt(S.recent.total) + '</b> ' + plural(S.recent.total, 'event', 'events') + ' in 90 days</span>');
779
780
  else if (S.recent.loading) parts.push('<span>Counting events…</span>');
780
- else if (S.recent.error) parts.push('<span class="mm-warn">Events in 90 days could not be counted</span>');
781
+ // 7.0.33 A8: said once. When the Recent learning list below already says it failed, this line does not say it again.
782
+ else if (S.recent.error && S.recent.rows.length) parts.push('<span class="mm-warn">Events in 90 days could not be counted</span>');
781
783
  if (S.recent.loaded) parts.push('<span><b>' + fmt(checked) + '</b> ' + plural(checked, 'result', 'results') + ' checked on this page</span>');
782
784
  parts.push('<span>Capture status in ' + textBtn('settings', 'Memory settings') + '</span>');
783
785
  parts.push('<span>' + textBtn('log', 'View activity log') + '<span class="mm-phone-only"><span class="mm-dot" aria-hidden="true"> · </span>' + textBtn('refresh', 'Refresh') + '</span></span>');
@@ -816,6 +818,12 @@
816
818
  }
817
819
  function appliedExcerpt(e) { return ((e.source_refs || [])[0] || {}).excerpt || (e.detail && e.detail.excerpts && e.detail.excerpts[0]) || ''; }
818
820
  function groupHead(text, side) { return '<h2 class="group-title mm-group"><span>' + text + '</span>' + (side ? '<span>' + side + '</span>' : '') + '</h2>'; }
821
+ // 7.0.33 A8: a list that failed because its part of COS is not set up on this Mac says that, once, with no Try again
822
+ // (trying again cannot help); any other failure keeps its own title and Try again.
823
+ function failBox(L, title, retry) {
824
+ if (W.notSetUp && W.notSetUp(L.errorCode)) return stateBox('Not set up on this Mac.', L.error, null);
825
+ return stateBox(title, L.error, retry);
826
+ }
819
827
  function stateBox(title, body, retry) {
820
828
  return '<div class="cx-list"><div class="mm-state"><strong>' + esc(title) + '</strong>' + (body ? '<span>' + esc(body) + '</span>' : '') +
821
829
  (retry ? '<span class="mm-state-act">' + btn(retry, 'Try again') + '</span>' : '') + '</div></div>';
@@ -841,7 +849,7 @@
841
849
  function recentListHtml() {
842
850
  var L = S.recent, sel = selectedId();
843
851
  if (!L.loaded && L.loading) return groupHead('Recent learning', '90 days') + loadingBox('Reading what COS learned in the last 90 days');
844
- if (L.error && !L.rows.length) return groupHead('Recent learning', '90 days') + stateBox('Recent learning did not load.', L.error, 'refresh');
852
+ if (L.error && !L.rows.length) return groupHead('Recent learning', '90 days') + failBox(L, 'Recent learning did not load.', 'refresh');
845
853
  if (!L.rows.length) return groupHead('Recent learning', '90 days') + stateBox('Nothing learned in the last 90 days.', 'Lessons, corrections and recalls show here as COS records them.');
846
854
  return groupHead('Recent learning', L.total != null ? fmt(L.total) + ' in 90 days' : '90 days') +
847
855
  '<div class="cx-list">' + L.rows.map(function (e) { return rowHtml(e, e.event_id === sel); }).join('') + '</div>' +
@@ -859,7 +867,7 @@
859
867
  '<p class="mm-group-note">Suggested rules still gathering evidence. They cannot become a rule until their confidence reaches the bar, and move to Needs you now on their own when it does.</p>' +
860
868
  '<div class="cx-list">' + learning.map(function (e) { return rowHtml(e, e.event_id === sel); }).join('') + '</div>' : '';
861
869
  if (!L.loaded && L.loading) return out + groupHead('Proposed changes') + loadingBox('Reading the changes waiting for you') + learnHtml;
862
- if (L.error && !L.rows.length) return out + groupHead('Proposed changes') + stateBox('The review list did not load.', L.error, 'refresh') + learnHtml;
870
+ if (L.error && !L.rows.length) return out + groupHead('Proposed changes') + failBox(L, 'The review list did not load.', 'refresh') + learnHtml;
863
871
  if (!needs.length && !rest.length) return groupHead('To review') + stateBox('You are up to date.', 'Nothing is waiting for your decision.') + learnHtml;
864
872
  if (other.length) out += groupHead('Proposed changes', fmt(other.length) + ' open') + '<div class="cx-list">' + other.map(function (e) { return rowHtml(e, e.event_id === sel); }).join('') + '</div>';
865
873
  if (ready.length) {
@@ -881,7 +889,7 @@
881
889
  var L = S.mem, q = L.q.trim(), rows = L.hits != null ? L.hits : L.rows, sel = selectedId();
882
890
  var head = groupHead(q ? 'Memories matching “' + esc(q) + '”' : 'Saved memories', !q && L.total != null ? fmt(L.total) + ' stored' : '');
883
891
  if (!L.loaded && L.loading) return head + loadingBox(q ? 'Searching your memories' : 'Reading your saved memories');
884
- if (L.error && !rows.length) return head + stateBox(q ? 'That search did not finish.' : 'Your memories did not load.', L.error, 'mem-retry');
892
+ if (L.error && !rows.length) return head + failBox(L, q ? 'That search did not finish.' : 'Your memories did not load.', 'mem-retry');
885
893
  if (!rows.length) return head + stateBox(q ? 'No memories match that search.' : 'No memories yet.', q ? 'Try fewer or different words.' : 'COS saves memories as you work. Plain notes in the memory folder show here too.');
886
894
  var cursor = q ? L.hitCursor : L.cursor;
887
895
  return head + '<div class="cx-list">' + rows.map(function (m) { return memRowHtml(m, m.id === sel); }).join('') + '</div>' +
@@ -1864,7 +1872,8 @@
1864
1872
  function stopWatches() { clearInterval(watch.ingest); watch.ingest = null; clearInterval(watch.fetch); watch.fetch = null; clearInterval(watch.build); watch.build = null; clearInterval(askTimer); }
1865
1873
 
1866
1874
  // ── decisions (writes, each from a tap) ──────────────────
1867
- function arm(key) { S.armed = key; paint(); setTimeout(function () { if (S.armed === key) { S.armed = ''; paint(); } }, 6000); }
1875
+ // 7.0.33 (Q1): the one expiry every armed button uses (app.js ARM_EXPIRE_MS, 5 s; this was 6 s).
1876
+ function arm(key) { S.armed = key; paint(); setTimeout(function () { if (S.armed === key) { S.armed = ''; paint(); } }, W.armExpireMs || 5000); }
1868
1877
  function afterWrite() { loadStatus(); }
1869
1878
  function reviewMemory(id, decision) {
1870
1879
  if (S.reviewBusy) return;
@@ -468,15 +468,27 @@
468
468
  }).join('') + '</div>';
469
469
  }
470
470
  function turnCards(t, msgKey) {
471
- var a = atts(t);
471
+ // 7.0.33 A2 (Anand round 2, slide 10): a small time under each message, when he asked and when COS answered. The clock
472
+ // alone when it is the answer's day (the header has the date); the date too when the question was on an earlier one.
473
+ // A server before 7.0.33 sends no question time: then only the answer has one.
474
+ function saidTimes(t) {
475
+ var asked = t.queryTimestamp != null ? toDate(t.queryTimestamp) : null, answered = t.timestamp != null ? toDate(t.timestamp) : null;
476
+ var tag = function (d, text) { return '<time datetime="' + esc(d.toISOString()) + '">' + esc(text) + '</time>'; };
477
+ return {
478
+ asked: asked ? tag(asked, answered && sameDay(asked, answered) ? shortTime(asked) : timeLabel(asked)) : '',
479
+ answered: answered ? tag(answered, shortTime(answered)) : ''
480
+ };
481
+ }
482
+ var a = atts(t), when = saidTimes(t);
472
483
  // 7.0.30, Miles's pick A: who said what by side, in the COS chat's own shapes. His words on the right in the chat's
473
484
  // gold bubble with "You" under it; COS on the left beside its mark, on the page. A screen reader hears "You said"
474
485
  // and "COS said" first, since the side and the mark are visual only.
475
486
  return '<div class="cx-said you"><span class="sr-only">You said</span><div class="cx-said-bubble cx-card-body" data-hl>' + esc(String(t.query || '').trim() || '(empty query)').replace(/\n/g, '<br>') + '</div>' +
476
- '<span class="cx-said-who" aria-hidden="true">You</span></div>' +
487
+ (when.asked ? '<span class="cx-said-who"><span aria-hidden="true">You · </span>' + when.asked + '</span>' : '<span class="cx-said-who" aria-hidden="true">You</span>') + '</div>' +
477
488
  stripHtml(a.filter(isUserAtt), true, msgKey) +
478
489
  '<div class="cx-said cos"><span class="cc-avatar" aria-hidden="true"><svg class="icon"><use href="#i-mark"/></svg></span><div class="cx-said-text"><span class="sr-only">COS said</span>' +
479
- '<div class="cx-card-body md" data-hl>' + W.md(W.structureLensAnswer(String(t.text || ''))) + '</div></div></div>' +
490
+ '<div class="cx-card-body md" data-hl>' + W.md(W.structureLensAnswer(String(t.text || ''))) + '</div>' +
491
+ (when.answered ? '<span class="cx-said-who cx-said-when">' + when.answered + '</span>' : '') + '</div></div>' +
480
492
  stripHtml(a.filter(function (x) { return !isUserAtt(x); }), false, msgKey);
481
493
  }
482
494
  function copyTurnText(t) {
@@ -109,7 +109,12 @@
109
109
  var left = Math.ceil((deadline - Date.now()) / 1000);
110
110
  return Math.max(0, Math.min(span, isFinite(left) ? left : 0));
111
111
  }
112
- function timeLeft(s) { return s > 99 ? Math.ceil(s / 60) + ' min left' : s + ' s left'; }
112
+ // 7.0.32 (X1 ask 1): a question can wait 8 hours, so from one hour the time left reads hours and minutes ("7 h 59 m
113
+ // left", never "480 min left"). From an hour on it is rounded down; under an hour it keeps 7.0.31's minutes (rounded up).
114
+ function timeLeft(s) {
115
+ if (s >= 3600) { var h = Math.floor(s / 3600), m = Math.floor((s % 3600) / 60); return h + ' h' + (m ? ' ' + m + ' m' : '') + ' left'; }
116
+ return s > 99 ? Math.ceil(s / 60) + ' min left' : s + ' s left';
117
+ }
113
118
  function engine(item) { return item.provider === 'codex' ? 'Codex' : 'Claude'; }
114
119
 
115
120
  // ── the capability, then the poll ─────────────────────
@@ -24,7 +24,7 @@
24
24
  // 6.99.11 (Drop it P2): shown only when the server has the P2 settings and Drop it is on.
25
25
  { id: 'reach', title: 'COS reaching out', summary: 'What COS tells you about on its own, when it may interrupt, and your reminders.', mac: [], p2: true },
26
26
  { id: 'privacy', title: 'Privacy and permissions', summary: 'What COS can use on this Mac. Each permission turns on one feature.', mac: ['Permissions'] },
27
- { id: 'pet', title: 'Pet', summary: 'Which pet this browser shows on Today, and whether it shows. Size, motion and the Mac\u2019s own pet are set in COS Control.', mac: ['Session pet size and motion', 'Characters on the Mac'] },
27
+ { id: 'pet', title: 'Pet', summary: 'Which pet this browser shows, and where: on Today, in the top bar on a phone, or off. Size, motion and the Mac\u2019s own pet are set in COS Control.', mac: ['Session pet size and motion', 'Characters on the Mac'] },
28
28
  { id: 'system', title: 'System and updates', summary: 'The COS server on this Mac, app updates, and checks when something is off.', mac: ['App updates', 'Troubleshooting'] }
29
29
  ];
30
30
  var DAYS = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
@@ -50,8 +50,17 @@
50
50
  }
51
51
  function petBody() {
52
52
  var prefs = W.petPrefs ? W.petPrefs() : { pet: 'mac', show: true };
53
- var out = group('On this browser', '',
54
- row('Show the pet on Today', 'Only here. The pet on your Mac stays as it is.', toggle(prefs.show, 'data-set="pet-show"', 'Show the pet on Today')));
53
+ // 7.0.33 A13 (Anand round 2, slide 9): where the pet shows, one of three, in the choice rows Character uses.
54
+ var where = !prefs.show ? 'off' : prefs.place === 'bar' ? 'bar' : 'today';
55
+ var placeRow = function (v, name, desc) {
56
+ return '<button type="button" class="cx-set-row cx-set-choice" data-set="pet-place" data-place="' + v + '" aria-pressed="' + (where === v) + '">' +
57
+ '<span class="cx-set-text"><span class="cx-set-name">' + esc(name) + '</span><span class="cx-set-desc">' + esc(desc) + '</span></span>' +
58
+ '<span class="value">' + (where === v ? '<span class="check">✓</span>' : '') + '</span></button>';
59
+ };
60
+ var out = group('On this browser', 'Where the pet shows here. The pet on your Mac stays as it is.',
61
+ placeRow('today', 'On Today', 'As it has been: on Today, with what it is doing under it.') +
62
+ placeRow('bar', 'In the top bar', 'On a phone: small, beside Ask, on every page. Tap it to see what it is doing. The Duo shows it on Today.') +
63
+ placeRow('off', 'Off', 'No pet in this browser.'));
55
64
  if (PET.state === 'idle') loadPetCatalog();
56
65
  if (PET.state !== 'ready') {
57
66
  return out + group('Character', '', PET.state === 'error'
@@ -371,12 +380,11 @@
371
380
  }
372
381
  var t = e.target.closest('[data-set]');
373
382
  if (!t || t.disabled) return;
374
- if (t.getAttribute('data-set') === 'pet-show') {
375
- var shown = t.getAttribute('aria-checked') === 'true';
376
- t.setAttribute('aria-checked', String(!shown));
377
- if (W.setPetPrefs) W.setPetPrefs({ show: !shown });
378
- W.toast(!shown ? 'Pet on' : 'Pet off');
379
- return;
383
+ if (t.getAttribute('data-set') === 'pet-place') {
384
+ var v = t.getAttribute('data-place');
385
+ if (W.setPetPrefs) W.setPetPrefs(v === 'off' ? { show: false } : { show: true, place: v });
386
+ W.toast(v === 'off' ? 'Pet off' : v === 'bar' ? 'Pet in the top bar' : 'Pet on Today');
387
+ return render();
380
388
  }
381
389
  if (t.getAttribute('data-set') === 'pet-retry') { PET.state = 'idle'; return render(); }
382
390
  var act = t.getAttribute('data-set');
@@ -439,7 +447,7 @@
439
447
  return;
440
448
  }
441
449
  if (act === 'release') {
442
- if (t.getAttribute('data-armed') !== '1') { t.setAttribute('data-armed', '1'); t.textContent = 'Tap again to release'; return; }
450
+ if (!W.armTap(t, t, 'Tap again to release')) return;
443
451
  // The server fails closed without confirm; the second tap is the confirmation.
444
452
  api('/api/agent-sessions/fences/release', { method: 'POST', body: { target: t.getAttribute('data-target'), confirm: true } })
445
453
  .then(function () { W.toast('Released'); S.data = null; S.state = 'idle'; load(true); }, function (err) { say(err.message, true); });
@@ -252,16 +252,13 @@
252
252
  bar.classList.toggle('hidden', onToday);
253
253
  document.body.classList.toggle('has-navbar', !onToday);
254
254
  var crumbs = $('crumbs');
255
- var section = r.pane === 'usage' ? 'Sessions' : (W.titles[r.pane] || '');
256
255
  var detail = W.crumb();
257
- var parts = ['<li class="crumb root">COS Control</li>'];
258
- if (r.pane === 'settings') parts.push('<li class="crumb current">Settings</li>');
259
- else {
260
- parts.push('<li class="crumb' + (detail || r.pane === 'usage' ? ' dim' : ' current') + '">' + esc(section) + '</li>');
261
- if (r.pane === 'usage') parts.push('<li class="crumb' + (detail ? ' dim' : ' current') + '">Usage</li>');
262
- if (detail) parts.push('<li class="crumb current detail">' + esc(detail) + '</li>');
263
- }
264
- crumbs.innerHTML = parts.join('');
256
+ // 7.0.33 A11: the way you came, the phone's trail (app.js trailNow), not where the page lives. Each step goes back
257
+ // that many steps; this page last. With no path (a menu pick, a deep link), this page's name alone.
258
+ var now = W.trailNow ? W.trailNow(r) : { steps: [], here: detail || W.titles[r.pane] || '' };
259
+ crumbs.innerHTML = (now.steps || []).map(function (s) {
260
+ return '<li class="crumb dim"><a href="' + esc(s.hash) + '" data-trail-back="' + s.back + '">' + esc(s.label) + '</a></li>';
261
+ }).join('') + '<li class="crumb current' + (detail ? ' detail' : '') + '" aria-current="page">' + esc(now.here) + '</li>';
265
262
  $('nav-back').disabled = onToday;
266
263
  // Below 600 px the section name gets the detail too. On a reader that label is hidden (the section button gives way
267
264
  // to Back, which names where it goes), so this shows only on a list that has a detail.
@@ -271,11 +268,9 @@
271
268
 
272
269
  document.addEventListener('click', function (e) {
273
270
  if (e.target.closest('#section-btn')) loadStories(true);
274
- var t = e.target.closest('#nav-home,#nav-back,#nav-settings');
271
+ var t = e.target.closest('#nav-back');
275
272
  if (!t) return;
276
- if (t.id === 'nav-home') W.go('today');
277
- else if (t.id === 'nav-back') W.goBack();
278
- else W.go('settings');
273
+ W.goBack();
279
274
  });
280
275
  document.addEventListener('keydown', function (e) {
281
276
  if (!(e.metaKey || e.ctrlKey) || e.altKey) return;