@gotcos/glasses-server 6.99.25 → 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 +166 -0
- package/package.json +1 -1
- package/server/index.ts +46 -4
- 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/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/web-control/app.css +2 -0
- package/server/web-control/app.js +738 -102
- package/server/web-control/index.html +5 -1
- package/server/web-control/panes/cos-chat.css +18 -0
- package/server/web-control/panes/cos-chat.js +239 -45
- package/server/web-control/panes/meetings.js +20 -9
- package/server/web-control/panes/questions.css +39 -1
- package/server/web-control/panes/questions.js +368 -51
- 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 +34 -2
- package/server/web-control/panes/today-actions.css +62 -4
- package/server/web-control/panes/today-actions.js +384 -53
|
@@ -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; }
|
|
@@ -91,9 +91,9 @@
|
|
|
91
91
|
Promise.all([
|
|
92
92
|
soft(api('/api/decision-provider')), soft(api('/api/live-cues/v2/settings')), soft(api('/api/morning-brief')),
|
|
93
93
|
soft(api('/api/domains')), soft(api('/api/agent-sessions/fences')), soft(api('/api/meeting-import/fireflies/status')),
|
|
94
|
-
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'))
|
|
95
95
|
]).then(function (r) {
|
|
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] };
|
|
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] };
|
|
97
97
|
S.state = 'ready';
|
|
98
98
|
render();
|
|
99
99
|
});
|
|
@@ -205,11 +205,30 @@
|
|
|
205
205
|
}, function (err) { if (undo) undo(); say((err && err.message) || 'That did not save.', true); });
|
|
206
206
|
}
|
|
207
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
|
+
}
|
|
208
218
|
function categoryBody(cat) {
|
|
209
219
|
var d = S.data || {};
|
|
210
220
|
var out = '';
|
|
211
221
|
if (cat.id === 'reach') return reachBody();
|
|
212
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
|
+
}
|
|
213
232
|
if (cat.id === 'ai' && d.dp) {
|
|
214
233
|
var p = d.dp.provider || '';
|
|
215
234
|
var name = function (x) { return x === 'subq' ? 'SubQ' : x === 'jev' ? 'Jev' : x || 'Not set'; };
|
|
@@ -327,6 +346,19 @@
|
|
|
327
346
|
.then(function () { W.toast(!on ? 'Live cues on' : 'Live cues off'); }, function (err) { t.setAttribute('aria-checked', String(on)); say(err.message, true); });
|
|
328
347
|
return;
|
|
329
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
|
+
}
|
|
330
362
|
if (act === 'brief') {
|
|
331
363
|
var bon = t.getAttribute('aria-checked') === 'true';
|
|
332
364
|
t.setAttribute('aria-checked', String(!bon));
|
|
@@ -14,15 +14,26 @@
|
|
|
14
14
|
html.ta-open { overflow: hidden; }
|
|
15
15
|
|
|
16
16
|
.ta-layer {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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;
|
|
20
23
|
--run-green: rgb(56, 145, 99);
|
|
21
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); }
|
|
22
27
|
:root[data-theme='dark'] .ta-layer { --run-green: rgb(108, 207, 143); }
|
|
23
28
|
@media (prefers-color-scheme: dark) { :root:not([data-theme='light']) .ta-layer { --run-green: rgb(108, 207, 143); } }
|
|
24
29
|
.ta-layer[hidden] { display: none; }
|
|
25
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; }
|
|
26
37
|
|
|
27
38
|
.ta-sheet {
|
|
28
39
|
position: absolute; left: 0; right: 0; bottom: 0;
|
|
@@ -37,16 +48,23 @@ html.ta-open { overflow: hidden; }
|
|
|
37
48
|
}
|
|
38
49
|
.ta-grip { flex: none; width: 36px; height: 5px; border-radius: 3px; background: var(--panel-border); margin: 8px auto 0; }
|
|
39
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; }
|
|
40
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; }
|
|
41
55
|
.ta-x { flex: none; display: grid; place-items: center; width: 44px; height: 44px; margin-right: -8px; border-radius: 12px; color: var(--ink); }
|
|
42
56
|
.ta-x .icon { width: 20px; height: 20px; stroke-width: 1.8; }
|
|
43
57
|
.ta-body { flex: 1 1 auto; min-height: 0; overflow: auto; overscroll-behavior: contain; -webkit-overflow-scrolling: touch; padding: 2px var(--gutter) 16px; }
|
|
44
58
|
.ta-foot { flex: none; padding: 10px var(--gutter) calc(var(--safe-bottom) + 10px); border-top: 1px solid var(--row-border); background: var(--page); }
|
|
45
|
-
.ta-foot:empty { display: none; }
|
|
59
|
+
.ta-foot:empty, .ta-foot[hidden] { display: none; }
|
|
46
60
|
.ta-foot .composer { margin-top: 0; position: static; }
|
|
47
61
|
.ta-foot .status-line { margin: 6px 4px 0; min-height: 0; }
|
|
48
62
|
.ta-foot .status-line:empty { display: none; }
|
|
49
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); }
|
|
50
68
|
|
|
51
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; }
|
|
52
70
|
.ta-note { margin: 6px 0 0; color: var(--hot); font-size: 13.5px; line-height: 1.4; }
|
|
@@ -56,6 +74,19 @@ html.ta-open { overflow: hidden; }
|
|
|
56
74
|
.ta-links { display: flex; flex-wrap: wrap; gap: 0 18px; }
|
|
57
75
|
.ta-q { display: grid; gap: 12px; margin: 8px 0 12px; }
|
|
58
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; }
|
|
59
90
|
.ta-activity { margin: 8px 0 4px; }
|
|
60
91
|
.ta-activity .sx-act-state { margin-top: 0; }
|
|
61
92
|
.ta-act-prompt { margin: 2px 0 0; font-size: 13.5px; line-height: 1.4; color: var(--ink); overflow-wrap: anywhere; }
|
|
@@ -85,6 +116,8 @@ html.ta-open { overflow: hidden; }
|
|
|
85
116
|
@media (min-width: 600px) {
|
|
86
117
|
.ta-sheet {
|
|
87
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;
|
|
88
121
|
border-radius: 20px 0 0 20px; box-shadow: -16px 0 48px rgba(0, 0, 0, 0.22);
|
|
89
122
|
animation-name: ta-slide;
|
|
90
123
|
}
|
|
@@ -92,6 +125,22 @@ html.ta-open { overflow: hidden; }
|
|
|
92
125
|
.ta-head { padding-top: calc(var(--safe-top) + 14px); }
|
|
93
126
|
.ta-foot { padding-bottom: calc(var(--safe-bottom) + 14px); }
|
|
94
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; }
|
|
95
144
|
@keyframes ta-rise { from { transform: translateY(32px); opacity: 0.6; } }
|
|
96
145
|
@keyframes ta-slide { from { transform: translateX(32px); opacity: 0.6; } }
|
|
97
146
|
/* Dismiss armed: the second tap removes it (no Undo on this server). */
|
|
@@ -107,3 +156,12 @@ html.ta-open { overflow: hidden; }
|
|
|
107
156
|
.nav-row .note.file-error { color: var(--hot); white-space: normal; }
|
|
108
157
|
/* Running card when the session list could not be read (app.js renderRunning). */
|
|
109
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; }
|