@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
|
@@ -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
|
-
.
|
|
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="
|
|
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
|
-
|
|
159
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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') +
|
|
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') +
|
|
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 +
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
54
|
-
|
|
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-
|
|
375
|
-
var
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
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 (
|
|
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
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
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-
|
|
271
|
+
var t = e.target.closest('#nav-back');
|
|
275
272
|
if (!t) return;
|
|
276
|
-
|
|
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;
|