@gotcos/glasses-server 6.99.20 → 6.99.29
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/CHANGELOG.md +257 -0
- package/docs/work-orchestrator.md +20 -0
- package/package.json +1 -1
- package/server/index.ts +48 -5
- package/server/lib/agent-session-output-images.ts +280 -0
- package/server/lib/agent-session-store.ts +3 -1
- package/server/lib/agent-session-turns.ts +18 -7
- package/server/lib/client-liveness.ts +38 -3
- package/server/lib/codex-subagent-activity.ts +36 -0
- package/server/lib/g2-turn-provenance.ts +679 -0
- package/server/lib/media-store.ts +41 -2
- package/server/lib/permission-broker.ts +94 -17
- package/server/lib/queue-turn-evidence.ts +19 -2
- package/server/lib/session-halt.ts +15 -1
- package/server/lib/session-stream-events.ts +4 -0
- package/server/lib/session-subagents.ts +155 -0
- package/server/lib/session-turn-attachments.ts +245 -0
- package/server/lib/thread-turn-queue-deliver.ts +17 -2
- package/server/lib/thread-turn-queue.ts +10 -0
- package/server/lib/today-brief.ts +731 -0
- package/server/lib/turn-file-store.ts +202 -0
- package/server/lib/turn-footer.ts +81 -0
- package/server/lib/web-control-static.ts +226 -0
- package/server/lib/web-memories-bridge.ts +9 -8
- package/server/lib/web-pet-choice.ts +225 -0
- package/server/lib/web-pet.ts +226 -0
- package/server/lib/work-lens-rounds.ts +29 -4
- package/server/routes/agent-session-bindings.ts +164 -16
- package/server/routes/agent-sessions.ts +77 -8
- package/server/routes/client-instance.ts +12 -1
- package/server/routes/cursor-stop-followup.ts +30 -1
- package/server/routes/g2-turns.ts +46 -0
- package/server/routes/health.ts +9 -0
- package/server/routes/media.ts +32 -8
- package/server/routes/permission-broker.ts +47 -4
- package/server/routes/thread-turn-queue.ts +148 -8
- package/server/routes/today-brief.ts +148 -0
- package/server/routes/web-control.ts +110 -7
- package/server/routes/work-lens-rounds.ts +19 -5
- package/server/web-control/app.css +26 -5
- package/server/web-control/app.js +1351 -177
- package/server/web-control/control.css +1 -1
- package/server/web-control/index.html +31 -13
- package/server/web-control/memories/memory-workspace.js +1 -1
- package/server/web-control/panes/cos-chat.css +22 -0
- package/server/web-control/panes/cos-chat.js +264 -48
- package/server/web-control/panes/meetings.css +15 -2
- package/server/web-control/panes/meetings.js +20 -9
- package/server/web-control/panes/memories.css +291 -0
- package/server/web-control/panes/memories.js +2208 -0
- package/server/web-control/panes/questions.css +39 -1
- package/server/web-control/panes/questions.js +419 -64
- package/server/web-control/panes/reader-media.js +154 -0
- package/server/web-control/panes/sessions.css +2 -0
- package/server/web-control/panes/settings.js +136 -6
- package/server/web-control/panes/today-actions.css +167 -0
- package/server/web-control/panes/today-actions.js +1303 -0
- package/server/web-control/panes/work.css +54 -0
- package/server/web-control/panes/work.js +1037 -18
- package/server/web-control/shell.js +6 -2
- package/server/web-control/theme.js +0 -2
- package/server/web-control/memories/memories-app.js +0 -1561
- package/server/web-control/memories/memories-theme.css +0 -73
- package/server/web-control/memories/memories.html +0 -72
- package/server/web-control/memories/memory-stewardship.js +0 -63
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/* Native session output images. Only opaque DTO ids reach the content route; markdown and paths are never sources. */
|
|
2
|
+
(function () {
|
|
3
|
+
'use strict';
|
|
4
|
+
var W = window.COSWeb;
|
|
5
|
+
if (!W) return;
|
|
6
|
+
var scopes = new Map(), viewer = null, requests = [], activeRequests = 0;
|
|
7
|
+
var ID = /^ai_[a-f0-9]{64}$/;
|
|
8
|
+
// outside_workspace is permanent. unavailable means the workspace lookup failed this time; the content route re-resolves it.
|
|
9
|
+
var REASONS = { outside_workspace: 'This image is outside the session workspace.',
|
|
10
|
+
unavailable: 'This image is temporarily unavailable. Retry in a moment.' };
|
|
11
|
+
var style = document.createElement('style');
|
|
12
|
+
style.textContent = '.rm-item{display:flex;flex-direction:column;gap:6px;max-width:180px}.rm-label,.rm-status{font:12px/1.4 var(--body);color:var(--sub);overflow-wrap:anywhere}.rm-label{color:var(--text)}.rm-item .cx-thumb{cursor:pointer}.rm-view{position:fixed;inset:0;z-index:10000;background:var(--bg,#171717);color:var(--text,#fff);display:flex;flex-direction:column;padding: max(16px,env(safe-area-inset-top)) max(16px,env(safe-area-inset-right)) max(16px,env(safe-area-inset-bottom)) max(16px,env(safe-area-inset-left));gap:16px}.rm-view-head{display:flex;align-items:center;justify-content:space-between;gap:16px}.rm-view-label{margin:0;overflow-wrap:anywhere;font:16px/1.4 var(--body)}.rm-view-body{flex:1;min-height:0;display:flex;align-items:center;justify-content:center;flex-direction:column;gap:16px}.rm-view img{max-width:100%;max-height:100%;object-fit:contain}.rm-viewing{overflow:hidden!important}';
|
|
13
|
+
document.head.appendChild(style);
|
|
14
|
+
|
|
15
|
+
function element(tag, cls, text) {
|
|
16
|
+
var el = document.createElement(tag); if (cls) el.className = cls;
|
|
17
|
+
if (text != null) el.textContent = text;
|
|
18
|
+
return el;
|
|
19
|
+
}
|
|
20
|
+
function button(text, action, cls) {
|
|
21
|
+
var b = element('button', cls || 'cx-btn quiet', text); b.type = 'button'; b.addEventListener('click', action); return b;
|
|
22
|
+
}
|
|
23
|
+
function path(scope, id, variant) {
|
|
24
|
+
return '/api/agent-sessions/' + encodeURIComponent(scope.provider) + '/' + encodeURIComponent(scope.id) + '/output-images/' + encodeURIComponent(id) + '/content?variant=' + variant;
|
|
25
|
+
}
|
|
26
|
+
function revoke(item) { if (item.url) { URL.revokeObjectURL(item.url); item.url = ''; } }
|
|
27
|
+
function failWords(err) {
|
|
28
|
+
if (err && err.status === 401) return 'Signed out. Connect this browser again to view the image.';
|
|
29
|
+
if (err && err.status === 404) return 'This image is no longer available. Reopen the session to refresh its images.';
|
|
30
|
+
return 'The image could not load. Check that your Mac is awake, then retry.';
|
|
31
|
+
}
|
|
32
|
+
function retryable(err) { return !(err && (err.status === 401 || err.status === 404)); }
|
|
33
|
+
function imageBlob(blob) { return blob && /^image\/(png|jpeg|webp|gif|avif)$/.test(blob.type || '') && blob.size > 0; }
|
|
34
|
+
// The server normalizes at most two images at once. Share that budget across both readers and the viewer.
|
|
35
|
+
function pump() {
|
|
36
|
+
while (activeRequests < 2 && requests.length) {
|
|
37
|
+
var job = requests.shift();
|
|
38
|
+
if (!job.valid()) { job.resolve(null); continue; }
|
|
39
|
+
activeRequests++;
|
|
40
|
+
(function (j) {
|
|
41
|
+
Promise.resolve().then(j.run).then(j.resolve, j.reject).then(function () { activeRequests--; pump(); });
|
|
42
|
+
})(job);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function imageRequest(url, valid, foreground) {
|
|
46
|
+
return new Promise(function (resolve, reject) {
|
|
47
|
+
var job = { valid: valid, resolve: resolve, reject: reject, run: function () { return W.api(url, { responseType: 'blob', timeoutMs: 20000 }); } };
|
|
48
|
+
if (foreground) requests.unshift(job); else requests.push(job);
|
|
49
|
+
pump();
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
function fetchImage(scope, item, variant, target, ready, failed) {
|
|
53
|
+
var generation = ++target.generation;
|
|
54
|
+
imageRequest(path(scope, item.id, variant), function () { return scope.alive && generation === target.generation; }, variant === 'phone').then(function (blob) {
|
|
55
|
+
if (!scope.alive || generation !== target.generation) return;
|
|
56
|
+
if (!imageBlob(blob)) throw new Error('Not a supported image');
|
|
57
|
+
revoke(target); target.url = URL.createObjectURL(blob); ready(target.url);
|
|
58
|
+
}).catch(function (err) {
|
|
59
|
+
if (scope.alive && generation === target.generation) failed(err);
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
function loadThumb(scope, item) {
|
|
63
|
+
if (item.busy) return;
|
|
64
|
+
item.busy = true; item.open.textContent = 'Loading image…'; item.status.textContent = 'Loading image…'; item.retry.hidden = true; item.open.disabled = true;
|
|
65
|
+
fetchImage(scope, item, 'thumb', item, function (url) {
|
|
66
|
+
item.busy = false; item.open.textContent = '';
|
|
67
|
+
var img = element('img'); img.alt = item.label;
|
|
68
|
+
img.addEventListener('error', function () {
|
|
69
|
+
if (!scope.alive || item.url !== url) return;
|
|
70
|
+
revoke(item); item.open.textContent = 'Image unavailable'; item.open.disabled = true;
|
|
71
|
+
item.status.textContent = 'The image could not be displayed.'; item.retry.hidden = false;
|
|
72
|
+
});
|
|
73
|
+
img.src = url; item.open.appendChild(img); item.open.disabled = false; item.status.textContent = '';
|
|
74
|
+
}, function (err) {
|
|
75
|
+
item.busy = false; item.open.textContent = 'Image unavailable'; item.status.textContent = failWords(err); item.retry.hidden = !retryable(err);
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
function closeViewer(restore) {
|
|
79
|
+
var v = viewer; if (!v) return;
|
|
80
|
+
viewer = null; v.generation++; revoke(v); v.el.remove(); document.documentElement.classList.remove('rm-viewing');
|
|
81
|
+
if (restore !== false && v.opener && v.opener.isConnected) v.opener.focus();
|
|
82
|
+
}
|
|
83
|
+
function openViewer(scope, item) {
|
|
84
|
+
closeViewer(false);
|
|
85
|
+
var el = element('div', 'rm-view'), head = element('div', 'rm-view-head'), body = element('div', 'rm-view-body');
|
|
86
|
+
el.setAttribute('role', 'dialog'); el.setAttribute('aria-modal', 'true'); el.setAttribute('aria-label', 'Image preview: ' + item.label);
|
|
87
|
+
var close = button('Close image', function () { closeViewer(); });
|
|
88
|
+
head.appendChild(element('h2', 'rm-view-label', item.label)); head.appendChild(close); el.appendChild(head); el.appendChild(body);
|
|
89
|
+
var v = viewer = { el: el, body: body, scope: scope, opener: item.open, close: close, url: '', generation: 0 };
|
|
90
|
+
document.body.appendChild(el); document.documentElement.classList.add('rm-viewing'); close.focus();
|
|
91
|
+
function load() {
|
|
92
|
+
body.textContent = ''; var status = element('p', 'rm-status', 'Loading image…'); status.setAttribute('role', 'status'); body.appendChild(status);
|
|
93
|
+
fetchImage(scope, item, 'phone', v, function (url) {
|
|
94
|
+
if (viewer !== v) return;
|
|
95
|
+
body.textContent = ''; var img = element('img'); img.alt = item.label;
|
|
96
|
+
img.addEventListener('error', function () { if (viewer === v && v.url === url) { revoke(v); failure(new Error('Image decode failed')); } });
|
|
97
|
+
img.src = url; body.appendChild(img);
|
|
98
|
+
}, failure);
|
|
99
|
+
}
|
|
100
|
+
function failure(err) {
|
|
101
|
+
if (viewer !== v) return;
|
|
102
|
+
body.textContent = ''; var status = element('p', 'rm-status', failWords(err)); status.setAttribute('role', 'status'); body.appendChild(status);
|
|
103
|
+
if (retryable(err)) body.appendChild(button('Retry image', load));
|
|
104
|
+
}
|
|
105
|
+
load();
|
|
106
|
+
}
|
|
107
|
+
// Window capture runs before Today's document capture handler. An image never pushes history or closes its parent.
|
|
108
|
+
window.addEventListener('keydown', function (e) {
|
|
109
|
+
if (!viewer) return;
|
|
110
|
+
if (e.key === 'Escape') { e.preventDefault(); e.stopImmediatePropagation(); closeViewer(); return; }
|
|
111
|
+
if (e.key === 'Tab') {
|
|
112
|
+
var buttons = Array.prototype.slice.call(viewer.el.querySelectorAll('button')).filter(function (b) { return !b.hidden && !b.disabled; });
|
|
113
|
+
var at = buttons.indexOf(document.activeElement), next = e.shiftKey ? (at <= 0 ? buttons.length - 1 : at - 1) : (at + 1) % buttons.length;
|
|
114
|
+
e.preventDefault(); e.stopImmediatePropagation(); if (buttons[next]) buttons[next].focus();
|
|
115
|
+
}
|
|
116
|
+
}, true);
|
|
117
|
+
function clearRecord(scope, record) {
|
|
118
|
+
record.items.forEach(function (item) { item.generation++; revoke(item); });
|
|
119
|
+
if (viewer && viewer.scope === scope && record.items.some(function (item) { return item.open === viewer.opener; })) closeViewer(false);
|
|
120
|
+
}
|
|
121
|
+
function unmount(host) {
|
|
122
|
+
var scope = scopes.get(host); if (!scope) return;
|
|
123
|
+
scope.alive = false; scope.records.forEach(function (record) { clearRecord(scope, record); }); scopes.delete(host);
|
|
124
|
+
}
|
|
125
|
+
function mount(host, provider, sessionId) {
|
|
126
|
+
if (!host || !provider || !sessionId) return;
|
|
127
|
+
var scope = scopes.get(host);
|
|
128
|
+
if (scope && (scope.provider !== provider || scope.id !== sessionId)) { unmount(host); scope = null; }
|
|
129
|
+
if (!scope) { scope = { alive: true, provider: String(provider), id: String(sessionId), records: new Map() }; scopes.set(host, scope); }
|
|
130
|
+
var slots = Array.prototype.slice.call(host.querySelectorAll('[data-reader-images]'));
|
|
131
|
+
scope.records.forEach(function (record, slot) { if (slots.indexOf(slot) < 0) { clearRecord(scope, record); scope.records.delete(slot); } });
|
|
132
|
+
slots.forEach(function (slot) {
|
|
133
|
+
if (scope.records.has(slot)) return;
|
|
134
|
+
var raw; try { raw = JSON.parse(slot.getAttribute('data-reader-images') || '[]'); } catch (_e) { raw = []; }
|
|
135
|
+
slot.textContent = ''; slot.className = 'cx-strip'; slot.setAttribute('aria-label', 'Session output images');
|
|
136
|
+
var record = { items: [] }; scope.records.set(slot, record);
|
|
137
|
+
(Array.isArray(raw) ? raw : []).forEach(function (dto) {
|
|
138
|
+
if (!dto || !ID.test(dto.id || '') || ['available', 'unavailable'].indexOf(dto.state) < 0) return;
|
|
139
|
+
var item = { id: dto.id, label: String(dto.label || 'Output image').slice(0, 180), generation: 0, url: '', busy: false };
|
|
140
|
+
record.items.push(item);
|
|
141
|
+
var wrap = element('div', 'rm-item');
|
|
142
|
+
item.open = button('Image unavailable', function () { openViewer(scope, item); }, 'cx-thumb');
|
|
143
|
+
item.open.setAttribute('aria-label', 'Open image: ' + item.label); item.open.disabled = true;
|
|
144
|
+
item.status = element('span', 'rm-status'); item.status.setAttribute('role', 'status');
|
|
145
|
+
item.retry = button('Retry image', function () { loadThumb(scope, item); }, 'link-btn'); item.retry.hidden = true;
|
|
146
|
+
wrap.appendChild(item.open); wrap.appendChild(element('span', 'rm-label', item.label)); wrap.appendChild(item.status); wrap.appendChild(item.retry); slot.appendChild(wrap);
|
|
147
|
+
if (dto.state === 'available') loadThumb(scope, item);
|
|
148
|
+
else { item.status.textContent = REASONS[dto.reason] || REASONS.unavailable; item.retry.hidden = dto.reason === 'outside_workspace'; }
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
W.readerMedia = { mount: mount, unmount: unmount, isOpen: function () { return !!viewer; }, close: closeViewer };
|
|
153
|
+
W.registerPane('readerMedia', W.readerMedia);
|
|
154
|
+
})();
|
|
@@ -181,6 +181,8 @@
|
|
|
181
181
|
.sx-queue-row { display: grid; grid-template-columns: 96px minmax(0, 1fr) auto; gap: 8px 10px; align-items: center; padding: 8px 10px; border-radius: 9px; background: var(--panel); border: 1px solid var(--panel-border); }
|
|
182
182
|
.sx-queue-state { font: 700 9.5px/1.2 var(--mono); color: var(--accent); }
|
|
183
183
|
.sx-queue-text { font-size: 13px; line-height: 1.4; color: var(--ink); overflow-wrap: anywhere; }
|
|
184
|
+
.sx-queue-refused .sx-queue-state { color: var(--sub); }
|
|
185
|
+
.sx-queue-reason { display: block; margin-top: 2px; font-size: 12px; color: var(--sub); }
|
|
184
186
|
@media (max-width: 599px) {
|
|
185
187
|
.sx-queue-row { grid-template-columns: minmax(0, 1fr) auto; }
|
|
186
188
|
.sx-queue-state { grid-column: 1 / -1; }
|
|
@@ -24,21 +24,76 @@
|
|
|
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: '
|
|
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'] },
|
|
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'];
|
|
31
31
|
|
|
32
32
|
function soft(p) { return p.then(function (d) { return d; }, function () { return null; }); }
|
|
33
|
+
// ── Pet (this browser) ──────────────────────────────────
|
|
34
|
+
// Which pet Today shows here and whether it shows at all. Kept per browser (COSWeb.petPrefs), like the theme.
|
|
35
|
+
// The Mac's own pet is changed in COS Control: the app does not watch its pet folder, so a change from here
|
|
36
|
+
// would leave the Mac showing the old pet. Catalog: GET /api/web-pet/catalog (lib/web-pet-choice.ts).
|
|
37
|
+
var PET = { catalog: null, state: 'idle', q: '' };
|
|
38
|
+
function loadPetCatalog() {
|
|
39
|
+
if (PET.state === 'loading' || PET.state === 'ready') return;
|
|
40
|
+
PET.state = 'loading';
|
|
41
|
+
api('/api/web-pet/catalog').then(function (c) { PET.catalog = c; PET.state = 'ready'; render(); },
|
|
42
|
+
function () { PET.state = 'error'; render(); });
|
|
43
|
+
}
|
|
44
|
+
function petChoiceRow(pet, name, desc, preview, current) {
|
|
45
|
+
var on = current === pet;
|
|
46
|
+
return '<button type="button" class="cx-set-row cx-set-choice pet-choice" data-pet="' + esc(pet) + '" aria-pressed="' + on + '">' +
|
|
47
|
+
(preview ? '<img class="pet-thumb" alt="" loading="lazy" src="' + esc(preview) + '">' : '<span class="pet-thumb"></span>') +
|
|
48
|
+
'<span class="cx-set-text"><span class="cx-set-name">' + esc(name) + '</span>' + (desc ? '<span class="cx-set-desc">' + esc(desc) + '</span>' : '') + '</span>' +
|
|
49
|
+
'<span class="value">' + (on ? '<span class="check">✓</span>' : '') + '</span></button>';
|
|
50
|
+
}
|
|
51
|
+
function petBody() {
|
|
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')));
|
|
55
|
+
if (PET.state === 'idle') loadPetCatalog();
|
|
56
|
+
if (PET.state !== 'ready') {
|
|
57
|
+
return out + group('Character', '', PET.state === 'error'
|
|
58
|
+
? '<p class="cx-set-row">Could not load the pets. <button type="button" class="cx-btn quiet" data-set="pet-retry">Try again</button></p>'
|
|
59
|
+
: W.skeletonRows(3));
|
|
60
|
+
}
|
|
61
|
+
var c = PET.catalog || {}, shipped = c.shipped || [], gallery = c.gallery || [];
|
|
62
|
+
out += group('Character', 'The pet this browser shows on Today. It does not change the pet on your Mac.',
|
|
63
|
+
petChoiceRow('mac', 'Same as my Mac', c.mac && c.mac.available ? 'Follows the pet COS Control shows.' : 'Your Mac has no pet set.', c.mac && c.mac.preview, prefs.pet) +
|
|
64
|
+
shipped.map(function (p) { return petChoiceRow(p.pet, p.name, p.summary || (p.animated ? 'Animated.' : ''), p.preview, prefs.pet); }).join(''));
|
|
65
|
+
if (gallery.length) {
|
|
66
|
+
var picked = gallery.filter(function (p) { return p.pet === prefs.pet; })[0];
|
|
67
|
+
out += '<div class="cx-set-group"><h3 class="cx-set-title">Gallery</h3><p class="cx-set-blurb">' + gallery.length +
|
|
68
|
+
' pets from the OpenPets gallery COS Control uses. Each shows as one still picture, as it does on the Mac.' + (picked ? ' In use: ' + esc(picked.name) + '.' : '') + '</p>' +
|
|
69
|
+
'<input type="search" class="pet-search" id="pet-search" placeholder="Search the gallery" aria-label="Search the gallery" value="' + esc(PET.q) + '">' +
|
|
70
|
+
'<div class="pet-grid" id="pet-grid">' + gallery.map(function (p) {
|
|
71
|
+
var on = p.pet === prefs.pet, words = (p.name + ' ' + p.category + ' ' + p.description).toLowerCase();
|
|
72
|
+
return '<button type="button" class="pet-tile" data-pet="' + esc(p.pet) + '" data-words="' + esc(words) + '" aria-pressed="' + on + '" title="' + esc(p.description || p.name) + '">' +
|
|
73
|
+
'<img alt="" loading="lazy" src="' + esc(p.preview) + '"><span>' + esc(p.name) + '</span></button>';
|
|
74
|
+
}).join('') + '</div><p class="cx-set-blurb hidden" id="pet-none">No pets match that search.</p></div>';
|
|
75
|
+
}
|
|
76
|
+
return out;
|
|
77
|
+
}
|
|
78
|
+
function filterPets() {
|
|
79
|
+
var q = PET.q.trim().toLowerCase(), grid = $('pet-grid'), shown = 0;
|
|
80
|
+
if (!grid) return;
|
|
81
|
+
grid.querySelectorAll('.pet-tile').forEach(function (t) {
|
|
82
|
+
var hit = !q || (t.getAttribute('data-words') || '').indexOf(q) >= 0;
|
|
83
|
+
t.classList.toggle('hidden', !hit); if (hit) shown++;
|
|
84
|
+
});
|
|
85
|
+
var none = $('pet-none'); if (none) none.classList.toggle('hidden', shown > 0);
|
|
86
|
+
}
|
|
87
|
+
|
|
33
88
|
function load(force) {
|
|
34
89
|
if (S.state === 'loading' || (S.data && !force)) return;
|
|
35
90
|
S.state = 'loading';
|
|
36
91
|
Promise.all([
|
|
37
92
|
soft(api('/api/decision-provider')), soft(api('/api/live-cues/v2/settings')), soft(api('/api/morning-brief')),
|
|
38
93
|
soft(api('/api/domains')), soft(api('/api/agent-sessions/fences')), soft(api('/api/meeting-import/fireflies/status')),
|
|
39
|
-
soft(api('/api/health')), soft(api('/api/drops/settings/p2'))
|
|
94
|
+
soft(api('/api/health')), soft(api('/api/drops/settings/p2')), soft(api('/api/today/brief/settings'))
|
|
40
95
|
]).then(function (r) {
|
|
41
|
-
S.data = { dp: r[0], cues: r[1], mb: r[2], domains: r[3], fences: r[4], ff: r[5], health: r[6], p2: r[7] };
|
|
96
|
+
S.data = { dp: r[0], cues: r[1], mb: r[2], domains: r[3], fences: r[4], ff: r[5], health: r[6], p2: r[7], tb: r[8] };
|
|
42
97
|
S.state = 'ready';
|
|
43
98
|
render();
|
|
44
99
|
});
|
|
@@ -67,7 +122,7 @@
|
|
|
67
122
|
return '<section class="cx-set-cat"><h2 class="cx-col-title">This browser</h2>' +
|
|
68
123
|
group('Appearance', '', themeBtn('system', 'Match this device') + themeBtn('light', 'Light') + themeBtn('dark', 'Dark')) +
|
|
69
124
|
group('Sign-in', '',
|
|
70
|
-
'<button type="button" class="cx-set-row" id="device-code-btn"><span class="cx-set-text"><span class="cx-set-name">
|
|
125
|
+
'<button type="button" class="cx-set-row" id="device-code-btn"><span class="cx-set-text"><span class="cx-set-name">Use COS on another phone or browser</span><span class="cx-set-desc">A QR for your iPhone Camera, and a code to type. This is not glasses pairing; pair glasses from COS Control on your Mac.</span></span><span class="chev"></span></button>' +
|
|
71
126
|
'<div id="device-code-panel" class="device-code hidden" aria-live="polite"></div>' +
|
|
72
127
|
'<button type="button" class="cx-set-row" id="copy-link"><span class="cx-set-text"><span class="cx-set-name">Copy link for your phone</span><span class="cx-set-desc" id="tailnet-url">' + esc(W.state.tailnetUrl || 'Checking Tailscale') + '</span></span></button>' +
|
|
73
128
|
'<button type="button" class="cx-set-row" id="sign-out"><span class="cx-set-text"><span class="cx-set-name">Sign out of this browser</span></span></button>' +
|
|
@@ -150,10 +205,30 @@
|
|
|
150
205
|
}, function (err) { if (undo) undo(); say((err && err.message) || 'That did not save.', true); });
|
|
151
206
|
}
|
|
152
207
|
|
|
208
|
+
// One line under the Today summary toggle: paused until, today's cap reached, or the last call failed (GET /api/today/brief/settings status).
|
|
209
|
+
function briefStatus(tb) {
|
|
210
|
+
var st = tb && tb.enabled && tb.status;
|
|
211
|
+
if (!st) return '';
|
|
212
|
+
var at = function (iso) { var d = new Date(iso); return isNaN(d) ? '' : d.toLocaleTimeString(undefined, { hour: 'numeric', minute: '2-digit' }); };
|
|
213
|
+
if (st.kind === 'paused') return 'Paused after three failed tries. It tries again at ' + at(st.until) + '.';
|
|
214
|
+
if (st.kind === 'daily_cap') return 'Today\u2019s ' + st.count + ' are used. It writes again tomorrow.';
|
|
215
|
+
if (st.kind === 'failed') return 'The last try failed at ' + at(st.at) + (st.code === 'timeout' ? ' (it took too long).' : st.code === 'unavailable' ? ' (Claude Code was not found on your Mac).' : '.');
|
|
216
|
+
return '';
|
|
217
|
+
}
|
|
153
218
|
function categoryBody(cat) {
|
|
154
219
|
var d = S.data || {};
|
|
155
220
|
var out = '';
|
|
156
221
|
if (cat.id === 'reach') return reachBody();
|
|
222
|
+
if (cat.id === 'pet') return petBody() + onMac(cat.mac);
|
|
223
|
+
// The Today summary (routes/today-brief.ts): Haiku on the Mac writes Today's top lines. Off unless switched on here or
|
|
224
|
+
// by COS_TODAY_BRIEF_LLM on the Mac, which wins when set.
|
|
225
|
+
if (cat.id === 'general' && d.tb && typeof d.tb.enabled === 'boolean') {
|
|
226
|
+
out += group('Today summary', '',
|
|
227
|
+
row('Write the Today summary with a model', d.tb.source === 'env' ? 'Set on the Mac by the server environment.' :
|
|
228
|
+
'Haiku writes two or three sentences after the count of what needs you. It reads what Today shows (session titles and states, each session\u2019s latest reply, the questions sessions ask you, what needs you) and, on your Mac, each session\u2019s sub-agents and the Work cards in progress. It runs only while Today is open, only when something changed, never during a recording, at most every 10 minutes and 60 times a day.',
|
|
229
|
+
toggle(d.tb.enabled, 'data-set="today-brief"' + (d.tb.source === 'env' ? ' disabled' : ''), 'Write the Today summary with a model')) +
|
|
230
|
+
(briefStatus(d.tb) ? '<p class="cx-set-blurb" id="today-brief-status">' + esc(briefStatus(d.tb)) + '</p>' : ''));
|
|
231
|
+
}
|
|
157
232
|
if (cat.id === 'ai' && d.dp) {
|
|
158
233
|
var p = d.dp.provider || '';
|
|
159
234
|
var name = function (x) { return x === 'subq' ? 'SubQ' : x === 'jev' ? 'Jev' : x || 'Not set'; };
|
|
@@ -226,9 +301,10 @@
|
|
|
226
301
|
browserGroup() +
|
|
227
302
|
(S.state !== 'ready' ? '<div class="cx-list" style="margin-top:18px">' + W.skeletonRows(4) + '</div>' :
|
|
228
303
|
CATEGORIES.filter(function (cat) { return !cat.p2 || p2On(); }).map(function (cat) {
|
|
229
|
-
return '<section class="cx-set-cat"><h2 class="cx-col-title">' + esc(cat.title) + '</h2><p class="cx-set-blurb">' + esc(cat.summary) + '</p>' + categoryBody(cat) + '</section>';
|
|
304
|
+
return '<section class="cx-set-cat" id="set-cat-' + cat.id + '"><h2 class="cx-col-title">' + esc(cat.title) + '</h2><p class="cx-set-blurb">' + esc(cat.summary) + '</p>' + categoryBody(cat) + '</section>';
|
|
230
305
|
}).join('')) +
|
|
231
306
|
'<p class="status-line" id="settings-status"></p></div>';
|
|
307
|
+
if (PET.q) filterPets();
|
|
232
308
|
}
|
|
233
309
|
|
|
234
310
|
function say(text, isError) { var el = $('settings-status'); if (el) { el.textContent = text; el.classList.toggle('error', !!isError); } }
|
|
@@ -236,8 +312,26 @@
|
|
|
236
312
|
pane.addEventListener('click', function (e) {
|
|
237
313
|
var go = e.target.closest('[data-go]');
|
|
238
314
|
if (go) { var parts = go.getAttribute('data-go').split('/'); return W.go(parts[0], parts.slice(1)); }
|
|
315
|
+
var pick = e.target.closest('[data-pet]');
|
|
316
|
+
if (pick && W.setPetPrefs) {
|
|
317
|
+
W.setPetPrefs({ pet: pick.getAttribute('data-pet') });
|
|
318
|
+
pane.querySelectorAll('[data-pet]').forEach(function (b) {
|
|
319
|
+
var on = b === pick; b.setAttribute('aria-pressed', String(on));
|
|
320
|
+
var v = b.querySelector('.value'); if (v) v.innerHTML = on ? '<span class="check">✓</span>' : '';
|
|
321
|
+
});
|
|
322
|
+
W.toast('Pet changed. Today shows it here.');
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
239
325
|
var t = e.target.closest('[data-set]');
|
|
240
326
|
if (!t || t.disabled) return;
|
|
327
|
+
if (t.getAttribute('data-set') === 'pet-show') {
|
|
328
|
+
var shown = t.getAttribute('aria-checked') === 'true';
|
|
329
|
+
t.setAttribute('aria-checked', String(!shown));
|
|
330
|
+
if (W.setPetPrefs) W.setPetPrefs({ show: !shown });
|
|
331
|
+
W.toast(!shown ? 'Pet on' : 'Pet off');
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
if (t.getAttribute('data-set') === 'pet-retry') { PET.state = 'idle'; return render(); }
|
|
241
335
|
var act = t.getAttribute('data-set');
|
|
242
336
|
if (act === 'refresh') { S.data = null; S.state = 'idle'; load(true); return render(); }
|
|
243
337
|
if (act === 'provider') {
|
|
@@ -252,6 +346,19 @@
|
|
|
252
346
|
.then(function () { W.toast(!on ? 'Live cues on' : 'Live cues off'); }, function (err) { t.setAttribute('aria-checked', String(on)); say(err.message, true); });
|
|
253
347
|
return;
|
|
254
348
|
}
|
|
349
|
+
if (act === 'today-brief') {
|
|
350
|
+
var ton = t.getAttribute('aria-checked') === 'true';
|
|
351
|
+
t.setAttribute('aria-checked', String(!ton));
|
|
352
|
+
api('/api/today/brief/settings', { method: 'POST', body: { enabled: !ton } })
|
|
353
|
+
.then(function (v) {
|
|
354
|
+
if (S.data && v) S.data.tb = v;
|
|
355
|
+
// Today forgets the summary it showed, so turning it on asks at once and turning it off shows the counts line.
|
|
356
|
+
if (W.resetTodayBrief) W.resetTodayBrief();
|
|
357
|
+
W.toast(!ton ? 'Today summary on' : 'Today summary off');
|
|
358
|
+
render();
|
|
359
|
+
}, function (err) { t.setAttribute('aria-checked', String(ton)); say(err.message, true); });
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
255
362
|
if (act === 'brief') {
|
|
256
363
|
var bon = t.getAttribute('aria-checked') === 'true';
|
|
257
364
|
t.setAttribute('aria-checked', String(!bon));
|
|
@@ -273,6 +380,15 @@
|
|
|
273
380
|
}
|
|
274
381
|
});
|
|
275
382
|
|
|
383
|
+
// A gallery picture that will not load shows the pet's name alone, never a broken image.
|
|
384
|
+
pane.addEventListener('error', function (e) {
|
|
385
|
+
var img = e.target;
|
|
386
|
+
if (img && img.tagName === 'IMG' && img.closest && img.closest('.pet-tile, .pet-choice')) img.classList.add('hidden');
|
|
387
|
+
}, true);
|
|
388
|
+
pane.addEventListener('input', function (e) {
|
|
389
|
+
if (e.target && e.target.id === 'pet-search') { PET.q = e.target.value; filterPets(); }
|
|
390
|
+
});
|
|
391
|
+
|
|
276
392
|
// Number, time, day and steer fields save when they change (a number is clamped by the server, then shown as saved).
|
|
277
393
|
pane.addEventListener('change', function (e) {
|
|
278
394
|
var f = e.target && e.target.closest ? e.target.closest('[data-p2]') : null;
|
|
@@ -287,6 +403,20 @@
|
|
|
287
403
|
});
|
|
288
404
|
|
|
289
405
|
W.registerPane('settings', {
|
|
290
|
-
load: function () {
|
|
406
|
+
load: function (args) {
|
|
407
|
+
load(false); render();
|
|
408
|
+
// "Change pet" on Today opens Settings at the Pet section.
|
|
409
|
+
if (args && args[0] === 'pet') {
|
|
410
|
+
var tries = 0;
|
|
411
|
+
(function scroll() {
|
|
412
|
+
var el = document.getElementById('set-cat-pet');
|
|
413
|
+
// Land on the pet in use once the catalog has drawn; until then, the Pet section's top.
|
|
414
|
+
var chosen = el && el.querySelector('[data-pet][aria-pressed="true"]');
|
|
415
|
+
if (chosen) { chosen.scrollIntoView({ block: 'center' }); return; }
|
|
416
|
+
if (el && tries === 0) el.scrollIntoView({ block: 'start' });
|
|
417
|
+
if (++tries < 40) setTimeout(scroll, 150);
|
|
418
|
+
})();
|
|
419
|
+
}
|
|
420
|
+
}
|
|
291
421
|
});
|
|
292
422
|
})();
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/* COS Control Web: Today as a place to act (panes/today-actions.js). A bottom sheet on the folded phone, a side sheet
|
|
2
|
+
from 600 px. Built from the page's own tokens and pieces: the composer, .btn, .link-btn, .cx-btn, the reader's
|
|
3
|
+
activity rows (.sx-act-*) and boxes (.sx-box), and the question cards (questions.css). */
|
|
4
|
+
|
|
5
|
+
/* The Today rows that open it: whole-row buttons. */
|
|
6
|
+
.need-open { display: block; width: 100%; text-align: left; color: inherit; }
|
|
7
|
+
.need-open .need-top { display: flex; }
|
|
8
|
+
.need-open .need-foot { display: flex; }
|
|
9
|
+
.intake-row { width: 100%; text-align: left; color: inherit; align-items: center; min-height: 40px; }
|
|
10
|
+
.intake-row::after { content: ''; width: 7px; height: 7px; border-right: 2px solid var(--grey); border-bottom: 2px solid var(--grey); transform: rotate(-45deg); flex: none; opacity: 0.6; margin-left: 2px; }
|
|
11
|
+
.intake-row .intake-meeting { flex: 1; }
|
|
12
|
+
|
|
13
|
+
/* The page behind does not scroll. Only the root: hidden on body too would make body a scroll container and unstick the rail. */
|
|
14
|
+
html.ta-open { overflow: hidden; }
|
|
15
|
+
|
|
16
|
+
.ta-layer {
|
|
17
|
+
/* 6.99.29 (Miles's unfolded Duo, IMG_0503, 11/07 12:48): with the keyboard closed the layer is the whole fixed
|
|
18
|
+
viewport, down to the physical bottom edge, whatever visualViewport reports. Sized from visualViewport.height it
|
|
19
|
+
ended about 106 px short and Today showed below the backdrop and the sheet. The home indicator is cleared once, by
|
|
20
|
+
the padding inside the sheet (.ta-foot, or .ta-body when there is no reply box), never by the layer or the sheet. */
|
|
21
|
+
position: fixed; inset: 0; z-index: 45;
|
|
22
|
+
--ta-bleed: 120px;
|
|
23
|
+
--run-green: rgb(56, 145, 99);
|
|
24
|
+
}
|
|
25
|
+
/* The keyboard is up (today-actions.js applyFit): the layer is the visible viewport, so it never covers the reply box. */
|
|
26
|
+
.ta-layer.ta-kb { top: var(--ta-vt, 0px); height: var(--ta-vh, 100dvh); }
|
|
27
|
+
:root[data-theme='dark'] .ta-layer { --run-green: rgb(108, 207, 143); }
|
|
28
|
+
@media (prefers-color-scheme: dark) { :root:not([data-theme='light']) .ta-layer { --run-green: rgb(108, 207, 143); } }
|
|
29
|
+
.ta-layer[hidden] { display: none; }
|
|
30
|
+
.ta-scrim { position: absolute; inset: 0; background: rgba(23, 18, 14, 0.45); touch-action: none; animation: fade 160ms ease; }
|
|
31
|
+
/* Past the bottom edge, in case Safari's fixed viewport ever stops short of it: the dim and the sheet run on below the
|
|
32
|
+
screen, so no band of Today can show under them. Off with the keyboard up (the keyboard covers that edge).
|
|
33
|
+
QA 6.99.29: the dim's bleed is the LAYER's own pseudo-element, never the scrim, so it is visual only: a tap on that
|
|
34
|
+
band lands on the layer (no data-ta, so nothing happens) instead of closing the overlay or reaching Today below. */
|
|
35
|
+
.ta-layer:not(.ta-kb)::before { content: ''; position: absolute; left: 0; right: 0; top: 100%; height: var(--ta-bleed); background: rgba(23, 18, 14, 0.45); touch-action: none; animation: fade 160ms ease; }
|
|
36
|
+
.ta-layer:not(.ta-kb) .ta-sheet::after { content: ''; position: absolute; left: 0; right: 0; top: 100%; height: var(--ta-bleed); background: var(--page); pointer-events: none; }
|
|
37
|
+
|
|
38
|
+
.ta-sheet {
|
|
39
|
+
position: absolute; left: 0; right: 0; bottom: 0;
|
|
40
|
+
/* A strip of Today stays in view above it: context, and somewhere to tap outside. */
|
|
41
|
+
max-height: calc(100% - var(--safe-top) - 56px);
|
|
42
|
+
display: flex; flex-direction: column;
|
|
43
|
+
background: var(--page); color: var(--ink);
|
|
44
|
+
border-radius: 20px 20px 0 0; box-shadow: 0 -12px 40px rgba(0, 0, 0, 0.25);
|
|
45
|
+
padding-left: var(--safe-left); padding-right: var(--safe-right);
|
|
46
|
+
outline: none;
|
|
47
|
+
animation: ta-rise 220ms cubic-bezier(0.2, 0.8, 0.2, 1);
|
|
48
|
+
}
|
|
49
|
+
.ta-grip { flex: none; width: 36px; height: 5px; border-radius: 3px; background: var(--panel-border); margin: 8px auto 0; }
|
|
50
|
+
.ta-head { flex: none; display: flex; align-items: flex-start; gap: 10px; padding: 6px var(--gutter) 4px; }
|
|
51
|
+
.ta-head-main { flex: 1; min-width: 0; }
|
|
52
|
+
.ta-back { display: inline-flex; align-items: center; min-height: 44px; margin: 0 0 -6px; padding: 0; }
|
|
53
|
+
.ta-back[hidden] { display: none; }
|
|
54
|
+
.ta-title { flex: 1; min-width: 0; margin: 8px 0 0; font: 500 22px/1.2 var(--display); letter-spacing: -0.005em; overflow-wrap: anywhere; }
|
|
55
|
+
.ta-x { flex: none; display: grid; place-items: center; width: 44px; height: 44px; margin-right: -8px; border-radius: 12px; color: var(--ink); }
|
|
56
|
+
.ta-x .icon { width: 20px; height: 20px; stroke-width: 1.8; }
|
|
57
|
+
.ta-body { flex: 1 1 auto; min-height: 0; overflow: auto; overscroll-behavior: contain; -webkit-overflow-scrolling: touch; padding: 2px var(--gutter) 16px; }
|
|
58
|
+
.ta-foot { flex: none; padding: 10px var(--gutter) calc(var(--safe-bottom) + 10px); border-top: 1px solid var(--row-border); background: var(--page); }
|
|
59
|
+
.ta-foot:empty, .ta-foot[hidden] { display: none; }
|
|
60
|
+
.ta-foot .composer { margin-top: 0; position: static; }
|
|
61
|
+
.ta-foot .status-line { margin: 6px 4px 0; min-height: 0; }
|
|
62
|
+
.ta-foot .status-line:empty { display: none; }
|
|
63
|
+
.ta-foot .sx-box, .ta-foot .sx-park { margin: 0 0 10px; }
|
|
64
|
+
/* No reply box (the session list, a question answered in its card): the sheet itself keeps the home indicator's strip,
|
|
65
|
+
so the end of the list and the question card's footer stop above it and nothing scrolls under it. With the keyboard
|
|
66
|
+
up the keyboard covers that strip. */
|
|
67
|
+
.ta-layer:not(.ta-kb) .ta-sheet:has(> .ta-foot:empty), .ta-layer:not(.ta-kb) .ta-sheet:has(> .ta-foot[hidden]) { padding-bottom: var(--safe-bottom); }
|
|
68
|
+
|
|
69
|
+
.ta-sub { display: flex; flex-wrap: wrap; align-items: center; gap: 4px 8px; margin: 0 0 8px; color: var(--sub); font-size: 14.5px; line-height: 1.4; }
|
|
70
|
+
.ta-note { margin: 6px 0 0; color: var(--hot); font-size: 13.5px; line-height: 1.4; }
|
|
71
|
+
.ta-card-line { margin: 0 0 8px; font-size: 14px; color: var(--ink); overflow-wrap: anywhere; }
|
|
72
|
+
.ta-quote { margin: 8px 0 10px; padding-left: 12px; border-left: 2px solid var(--btn-border); font: italic 400 16px/1.42 var(--display); color: var(--ink); overflow-wrap: anywhere; }
|
|
73
|
+
.ta-ask-line { margin: 8px 0 4px; font-size: 14.5px; font-weight: 600; color: var(--hot); }
|
|
74
|
+
.ta-links { display: flex; flex-wrap: wrap; gap: 0 18px; }
|
|
75
|
+
.ta-q { display: grid; gap: 12px; margin: 8px 0 12px; }
|
|
76
|
+
.ta-q[hidden] { display: none; }
|
|
77
|
+
/* 6.99.26: Needs you says where a waiting session can be answered; a pet with nothing in play does nothing */
|
|
78
|
+
.need-where[data-where="here"] { color: var(--hot); font-weight: 600; }
|
|
79
|
+
.pet-btn[aria-disabled="true"] { cursor: default; }
|
|
80
|
+
/* 6.99.26: a question only the Mac's own prompt can answer */
|
|
81
|
+
.ta-mac-line { color: var(--ink); }
|
|
82
|
+
/* 6.99.26: the pet's sessions: waiting on you, running, done today */
|
|
83
|
+
.ta-pet-group { margin: 4px 0 14px; }
|
|
84
|
+
.ta-pet-head { margin: 10px 0 2px; font: 600 15px/1.3 var(--body); color: var(--ink); }
|
|
85
|
+
.ta-pet-row { display: flex; flex-direction: column; gap: 4px; width: 100%; min-height: 56px; padding: 10px 0; text-align: left; border-top: 1px solid var(--row-border); color: inherit; }
|
|
86
|
+
.ta-pet-head + .ta-pet-row { border-top: 0; }
|
|
87
|
+
.ta-pet-top { display: flex; align-items: center; gap: 8px; min-width: 0; }
|
|
88
|
+
.ta-pet-title { font-size: 15.5px; font-weight: 600; line-height: 1.3; overflow-wrap: anywhere; }
|
|
89
|
+
.ta-pet-line { font-size: 13.5px; line-height: 1.4; color: var(--sub); overflow-wrap: anywhere; }
|
|
90
|
+
.ta-activity { margin: 8px 0 4px; }
|
|
91
|
+
.ta-activity .sx-act-state { margin-top: 0; }
|
|
92
|
+
.ta-act-prompt { margin: 2px 0 0; font-size: 13.5px; line-height: 1.4; color: var(--ink); overflow-wrap: anywhere; }
|
|
93
|
+
.ta-act-tail { margin-top: 6px; display: flex; flex-direction: column; }
|
|
94
|
+
.ta-last { margin-top: 14px; padding: 12px 14px; border-radius: 14px; background: var(--panel); border: 1px solid var(--panel-border); font-size: 15px; line-height: 1.5; }
|
|
95
|
+
.ta-last-head { margin: 0 0 6px; font-size: 13px; font-weight: 600; color: var(--sub); }
|
|
96
|
+
.ta-last-text.clamped { max-height: 18em; overflow: hidden; -webkit-mask-image: linear-gradient(#000 72%, transparent); mask-image: linear-gradient(#000 72%, transparent); }
|
|
97
|
+
|
|
98
|
+
/* From today's meetings */
|
|
99
|
+
.ta-list { margin-top: 2px; }
|
|
100
|
+
.ta-group { margin: 18px 0 0; font: 600 15px/1.3 var(--body); color: var(--ink); overflow-wrap: anywhere; }
|
|
101
|
+
.ta-sugg { padding: 12px 0 10px; border-top: 1px solid var(--row-border); }
|
|
102
|
+
.ta-group + .ta-sugg { border-top: 0; padding-top: 8px; }
|
|
103
|
+
.ta-sugg-text { margin: 0; font-size: 15px; font-weight: 600; line-height: 1.35; overflow-wrap: anywhere; }
|
|
104
|
+
.ta-sugg-src { margin: 4px 0 0; font-size: 13.5px; line-height: 1.4; color: var(--sub); overflow-wrap: anywhere; }
|
|
105
|
+
.ta-acts { display: flex; flex-wrap: wrap; align-items: center; gap: 2px 18px; margin-top: 6px; }
|
|
106
|
+
.ta-acts .btn.small { min-height: 44px; padding: 0 16px; }
|
|
107
|
+
.ta-acts .link-btn { font-size: 14.5px; }
|
|
108
|
+
.link-btn.quiet { color: var(--sub); font-weight: 500; }
|
|
109
|
+
.ta-wait { display: inline-flex; align-items: center; min-height: 44px; font-size: 14px; color: var(--sub); }
|
|
110
|
+
.ta-done { font-size: 14.5px; font-weight: 600; color: var(--ink); }
|
|
111
|
+
.ta-sugg.settled { display: flex; flex-wrap: wrap; align-items: center; gap: 0 16px; }
|
|
112
|
+
.ta-sugg.settled > p { flex-basis: 100%; }
|
|
113
|
+
.ta-sugg.settled .ta-sugg-text { color: var(--sub); font-weight: 500; }
|
|
114
|
+
|
|
115
|
+
/* Unfolded Duo, tablets, desktop: a sheet at the right edge, Today still in view beside it. */
|
|
116
|
+
@media (min-width: 600px) {
|
|
117
|
+
.ta-sheet {
|
|
118
|
+
left: auto; top: 0; width: clamp(360px, 60vw, 480px); max-height: none;
|
|
119
|
+
/* At the right edge only the right inset (a landscape notch or rounded corner) applies. */
|
|
120
|
+
padding-left: 0;
|
|
121
|
+
border-radius: 20px 0 0 20px; box-shadow: -16px 0 48px rgba(0, 0, 0, 0.22);
|
|
122
|
+
animation-name: ta-slide;
|
|
123
|
+
}
|
|
124
|
+
.ta-grip { display: none; }
|
|
125
|
+
.ta-head { padding-top: calc(var(--safe-top) + 14px); }
|
|
126
|
+
.ta-foot { padding-bottom: calc(var(--safe-bottom) + 14px); }
|
|
127
|
+
}
|
|
128
|
+
/* 6.99.29 (Miles's unfolded Duo, 11/07): the on-screen keyboard is up, at any width (today-actions.js applyFit marks the
|
|
129
|
+
layer ta-kb when the visible viewport is 120 px or more shorter than the layout one). The sheet fills what is visible,
|
|
130
|
+
the header folds to one compact row, the reply box stays pinned at the bottom just above the keyboard, and the
|
|
131
|
+
conversation takes the rest and scrolls. The keyboard covers the home indicator, so the foot drops that inset. */
|
|
132
|
+
.ta-layer.ta-kb .ta-sheet { top: 0; bottom: 0; max-height: none; overflow: hidden; }
|
|
133
|
+
.ta-layer.ta-kb .ta-grip { display: none; }
|
|
134
|
+
.ta-layer.ta-kb .ta-head { align-items: center; padding-top: calc(var(--safe-top) + 6px); padding-bottom: 4px; }
|
|
135
|
+
.ta-layer.ta-kb .ta-head-main { display: flex; align-items: center; gap: 12px; }
|
|
136
|
+
.ta-layer.ta-kb .ta-back { flex: none; min-height: 36px; margin: 0; }
|
|
137
|
+
.ta-layer.ta-kb .ta-title { margin: 0; font-size: 17px; line-height: 1.3; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; overflow-wrap: normal; }
|
|
138
|
+
.ta-layer.ta-kb .ta-x { width: 36px; height: 36px; margin-right: -6px; }
|
|
139
|
+
.ta-layer.ta-kb .ta-body { padding-bottom: 8px; }
|
|
140
|
+
.ta-layer.ta-kb .ta-foot { padding-top: 8px; padding-bottom: 8px; }
|
|
141
|
+
.ta-layer.ta-kb .ta-foot .composer textarea { max-height: 96px; }
|
|
142
|
+
/* Under about three lines left for the conversation: the "Working right now" note waits until the keyboard closes. */
|
|
143
|
+
.ta-layer.ta-kb-tight .ta-foot .sx-park { display: none; }
|
|
144
|
+
@keyframes ta-rise { from { transform: translateY(32px); opacity: 0.6; } }
|
|
145
|
+
@keyframes ta-slide { from { transform: translateX(32px); opacity: 0.6; } }
|
|
146
|
+
/* Dismiss armed: the second tap removes it (no Undo on this server). */
|
|
147
|
+
.link-btn.quiet.armed { color: var(--hot); font-weight: 600; }
|
|
148
|
+
/* Needs you: "Show N more", "can wait until tomorrow" and a source that could not be read (app.js renderNeeds). */
|
|
149
|
+
.need-more { display: block; width: 100%; text-align: left; padding: 12px 16px; min-height: 44px; border-top: 1px solid var(--row-border); color: var(--accent); font-weight: 600; font-size: 15px; }
|
|
150
|
+
.need-more.need-failed { color: var(--hot); font-weight: 500; }
|
|
151
|
+
/* Next up: Prep me and Open meeting (app.js renderNext). */
|
|
152
|
+
.next-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 4px 18px; padding: 0 16px 14px; }
|
|
153
|
+
.next-actions:empty { display: none; }
|
|
154
|
+
.next-actions .btn.small { min-height: 44px; }
|
|
155
|
+
/* The Ask sheet's File to Work inbox result, on its own row (app.js fileNoteHtml). */
|
|
156
|
+
.nav-row .note.file-error { color: var(--hot); white-space: normal; }
|
|
157
|
+
/* Running card when the session list could not be read (app.js renderRunning). */
|
|
158
|
+
.run-more.run-failed { color: var(--run-open); }
|
|
159
|
+
|
|
160
|
+
/* Files with a reply or a note (11/07): the COS chat's own chips and clip (cos-chat.css .cc-chip, .cc-attach), placed in
|
|
161
|
+
the reply box. The chips take their own row above the words; the clip sits beside Send. */
|
|
162
|
+
.ta-composer { flex-wrap: wrap; }
|
|
163
|
+
.ta-composer .ta-files { flex: 1 1 100%; padding: 2px 0 4px; }
|
|
164
|
+
.ta-composer .ta-files.hidden { display: none; }
|
|
165
|
+
.ta-clip:empty { display: none; }
|
|
166
|
+
.ta-clip .cc-attach { height: 38px; width: 42px; border-radius: var(--radius-sm); }
|
|
167
|
+
.ta-composer.dragging { outline: 2px dashed var(--accent); outline-offset: 4px; }
|