@gotcos/glasses-server 6.99.25 → 6.99.33
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 +224 -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 +42 -9
- package/server/web-control/app.js +975 -114
- package/server/web-control/control.css +12 -6
- package/server/web-control/index.html +6 -1
- package/server/web-control/panes/cos-chat.css +27 -3
- package/server/web-control/panes/cos-chat.js +240 -46
- package/server/web-control/panes/meetings.css +5 -2
- package/server/web-control/panes/meetings.js +21 -10
- package/server/web-control/panes/memories.css +1 -1
- package/server/web-control/panes/memories.js +3 -8
- package/server/web-control/panes/questions.css +56 -2
- package/server/web-control/panes/questions.js +454 -56
- package/server/web-control/panes/reader-media.js +154 -0
- package/server/web-control/panes/sessions.css +20 -7
- package/server/web-control/panes/settings.js +34 -2
- package/server/web-control/panes/speakers.css +3 -5
- package/server/web-control/panes/speakers.js +7 -4
- package/server/web-control/panes/threads.js +3 -2
- package/server/web-control/panes/today-actions.css +81 -3
- package/server/web-control/panes/today-actions.js +527 -62
- package/server/web-control/panes/work.css +1 -1
- package/server/web-control/shell.js +3 -0
|
@@ -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
|
+
})();
|
|
@@ -7,11 +7,18 @@
|
|
|
7
7
|
--pv-claude: rgb(199, 115, 56);
|
|
8
8
|
--pv-codex: rgb(26, 140, 122);
|
|
9
9
|
--pv-cursor: rgb(107, 97, 219);
|
|
10
|
+
/* 6.99.30 (design QA, light mode): the provider badges' 9 px words, about 25% darker than the glyph colour, so they
|
|
11
|
+
read at 4.5:1 or better on their tint. Dark mode keeps the glyph colour (below). */
|
|
12
|
+
--pv-claude-ink: rgb(143, 83, 40);
|
|
13
|
+
--pv-codex-ink: rgb(20, 105, 92);
|
|
14
|
+
--pv-cursor-ink: rgb(80, 73, 164);
|
|
10
15
|
--run-green: rgb(56, 145, 99);
|
|
11
16
|
}
|
|
12
|
-
:root[data-theme='dark'] .sx { --pv-claude: rgb(226, 146, 92); --pv-codex: rgb(72, 190, 168); --pv-cursor: rgb(158, 150, 245); --run-green: rgb(108, 207, 143);
|
|
17
|
+
:root[data-theme='dark'] .sx { --pv-claude: rgb(226, 146, 92); --pv-codex: rgb(72, 190, 168); --pv-cursor: rgb(158, 150, 245); --run-green: rgb(108, 207, 143);
|
|
18
|
+
--pv-claude-ink: var(--pv-claude); --pv-codex-ink: var(--pv-codex); --pv-cursor-ink: var(--pv-cursor); }
|
|
13
19
|
@media (prefers-color-scheme: dark) {
|
|
14
|
-
:root:not([data-theme='light']) .sx { --pv-claude: rgb(226, 146, 92); --pv-codex: rgb(72, 190, 168); --pv-cursor: rgb(158, 150, 245); --run-green: rgb(108, 207, 143);
|
|
20
|
+
:root:not([data-theme='light']) .sx { --pv-claude: rgb(226, 146, 92); --pv-codex: rgb(72, 190, 168); --pv-cursor: rgb(158, 150, 245); --run-green: rgb(108, 207, 143);
|
|
21
|
+
--pv-claude-ink: var(--pv-claude); --pv-codex-ink: var(--pv-codex); --pv-cursor-ink: var(--pv-cursor); }
|
|
15
22
|
}
|
|
16
23
|
|
|
17
24
|
/* ── header, views, clock ─────────────────────────────── */
|
|
@@ -41,7 +48,7 @@
|
|
|
41
48
|
@media (max-width: 599px) {
|
|
42
49
|
.sx-search { order: 0; flex: 1 1 auto; }
|
|
43
50
|
.sx-search .cx-clear { min-height: 40px; padding: 0 2px; }
|
|
44
|
-
.sx-select {
|
|
51
|
+
.sx-select { background-position: calc(100% - 16px) 19px, calc(100% - 11px) 19px; }
|
|
45
52
|
#sessions-recency { max-width: 128px; }
|
|
46
53
|
.sx-ufilters .sx-select { flex: 1 1 150px; max-width: none; }
|
|
47
54
|
.sx-utable .hp { display: none; }
|
|
@@ -62,12 +69,14 @@
|
|
|
62
69
|
.sx-title { display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; overflow-wrap: anywhere; font: 600 15px/1.3 var(--body); color: var(--ink); }
|
|
63
70
|
.sx-snippet { display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; overflow-wrap: anywhere; margin-top: 3px; font-size: 12.5px; line-height: 1.4; color: var(--sub); }
|
|
64
71
|
.sx-meta { display: flex; flex-wrap: wrap; align-items: center; gap: 4px 8px; margin-top: 5px; min-width: 0; }
|
|
65
|
-
.sx-badge { display: inline-flex; align-items: center; height: 17px; padding: 0 6px; border-radius: 9px; font: 700 9px/1 var(--mono); letter-spacing: 0.06em; color: var(--pv-claude); background: color-mix(in srgb, var(--pv-claude) 14%, transparent); white-space: nowrap; }
|
|
66
|
-
.sx-badge.codex { color: var(--pv-codex); background: color-mix(in srgb, var(--pv-codex) 14%, transparent); }
|
|
67
|
-
.sx-badge.cursor { color: var(--pv-cursor); background: color-mix(in srgb, var(--pv-cursor) 14%, transparent); }
|
|
72
|
+
.sx-badge { display: inline-flex; align-items: center; height: 17px; padding: 0 6px; border-radius: 9px; font: 700 9px/1 var(--mono); letter-spacing: 0.06em; color: var(--pv-claude-ink, var(--pv-claude)); background: color-mix(in srgb, var(--pv-claude) 14%, transparent); white-space: nowrap; }
|
|
73
|
+
.sx-badge.codex { color: var(--pv-codex-ink, var(--pv-codex)); background: color-mix(in srgb, var(--pv-codex) 14%, transparent); }
|
|
74
|
+
.sx-badge.cursor { color: var(--pv-cursor-ink, var(--pv-cursor)); background: color-mix(in srgb, var(--pv-cursor) 14%, transparent); }
|
|
68
75
|
.sx-pinned { display: inline-flex; align-items: center; height: 17px; padding: 0 6px; border-radius: 9px; font: 700 8.5px/1 var(--mono); letter-spacing: 0.06em; color: var(--sub); background: color-mix(in srgb, var(--ink) 8%, transparent); }
|
|
69
76
|
.sx-chip { font: 600 11.5px/1.3 var(--body); color: var(--sub); }
|
|
70
77
|
.sx-chip.waiting { color: var(--amber); }
|
|
78
|
+
/* 6.99.30: a queued message the server refused or let expire (app.js notSentFor). */
|
|
79
|
+
.sx-chip.refused { color: var(--hot); }
|
|
71
80
|
.sx-chip.running { color: var(--run-green); }
|
|
72
81
|
.sx-when, .sx-ws { font-size: 11.5px; line-height: 1.3; color: var(--sub); }
|
|
73
82
|
.sx-ws { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 100%; }
|
|
@@ -116,7 +125,9 @@
|
|
|
116
125
|
.sx-act-tool { flex: none; font: 400 11px/16px var(--mono); color: var(--sub); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
117
126
|
.sx-act-prose { flex: none; margin-top: 2px; font-size: 13px; line-height: 16px; color: var(--ink); white-space: pre-wrap; overflow-wrap: anywhere; }
|
|
118
127
|
.sx-act-fallback { height: calc(var(--act-state) + var(--act-prompt) + var(--act-tail) + 8px); margin: 0; padding-top: 6px; font-size: 12.5px; line-height: 1.4; color: var(--sub); overflow: hidden; box-sizing: border-box; }
|
|
119
|
-
|
|
128
|
+
/* 6.99.32 (QA Completionist W4): below 600 px every .cx-btn is at least 44 px (app.css), so the row that holds Stop is 46 px;
|
|
129
|
+
at 38 it clipped Stop's border top and bottom. The fallback's height follows the variable. */
|
|
130
|
+
@media (max-width: 599px) { .sx-activity { --act-state: 46px; } }
|
|
120
131
|
.sx-note { font-size: 12px; color: var(--sub); margin: 0 2px 12px; }
|
|
121
132
|
|
|
122
133
|
/* YOU and ASSISTANT cards (Control: card fill, a tinted hairline; sessions tint for you, green for the reply). */
|
|
@@ -181,6 +192,8 @@
|
|
|
181
192
|
.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
193
|
.sx-queue-state { font: 700 9.5px/1.2 var(--mono); color: var(--accent); }
|
|
183
194
|
.sx-queue-text { font-size: 13px; line-height: 1.4; color: var(--ink); overflow-wrap: anywhere; }
|
|
195
|
+
.sx-queue-refused .sx-queue-state { color: var(--sub); }
|
|
196
|
+
.sx-queue-reason { display: block; margin-top: 2px; font-size: 12px; color: var(--sub); }
|
|
184
197
|
@media (max-width: 599px) {
|
|
185
198
|
.sx-queue-row { grid-template-columns: minmax(0, 1fr) auto; }
|
|
186
199
|
.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));
|
|
@@ -221,8 +221,7 @@
|
|
|
221
221
|
#pane-speakers .sp-mrow { grid-template-columns: 32px minmax(0, 1fr) 12px; }
|
|
222
222
|
#pane-speakers .sp-tools { width: 100%; }
|
|
223
223
|
#pane-speakers .sp-search { max-width: none; flex-basis: 100%; }
|
|
224
|
-
|
|
225
|
-
.sp-select select.field { min-height: 38px; font-size: 14px; background-position: calc(100% - 16px) 16px, calc(100% - 11px) 16px; }
|
|
224
|
+
.sp-select select.field { background-position: calc(100% - 16px) 19px, calc(100% - 11px) 19px; }
|
|
226
225
|
#pane-speakers .sp-text { min-height: 36px; font-size: 14.5px; }
|
|
227
226
|
#pane-speakers .sp-field { min-height: 40px; font-size: 16px; width: 100%; }
|
|
228
227
|
#pane-speakers .cx-switch { gap: 12px; flex-wrap: wrap; }
|
|
@@ -242,11 +241,10 @@
|
|
|
242
241
|
.sp-pplay { width: 36px; height: 36px; }
|
|
243
242
|
.sp-pplay-space { width: 36px; }
|
|
244
243
|
.sp-opt { min-height: 40px; font-size: 15px; }
|
|
245
|
-
.sp-vacts .cx-btn {
|
|
244
|
+
.sp-vacts .cx-btn { font-size: 14px; }
|
|
246
245
|
.sp-leg { min-height: 30px; }
|
|
247
246
|
.sp-racts { justify-content: flex-start; }
|
|
248
|
-
.sp-
|
|
249
|
-
.sp-policy .field { min-height: 38px; font-size: 14px; background-position: calc(100% - 16px) 16px, calc(100% - 11px) 16px; }
|
|
247
|
+
.sp-policy .field { background-position: calc(100% - 16px) 19px, calc(100% - 11px) 19px; }
|
|
250
248
|
}
|
|
251
249
|
@media (min-width: 600px) {
|
|
252
250
|
.sp-pills-in { display: none !important; }
|
|
@@ -107,7 +107,8 @@
|
|
|
107
107
|
var off = new Error(OFFLINE); off.code = 'offline'; throw off;
|
|
108
108
|
});
|
|
109
109
|
}
|
|
110
|
-
|
|
110
|
+
// The one sentence Control Web says for a request that never reached the Mac (app.js COSWeb.unreachableCopy, 6.99.30).
|
|
111
|
+
var OFFLINE = W.unreachableCopy || 'Could not reach COS on your Mac. Check that it is awake and on Tailscale, then try again.';
|
|
111
112
|
// The server answered, but not with JSON (a proxy page, a crash): say that, never that it stopped.
|
|
112
113
|
function unreadable(r) { return new Error('The server answered ' + r.status + ' without a readable reply.'); }
|
|
113
114
|
// A 401 hands the page to the shell's sign-in and stops every timer here, so nothing polls a signed-out session.
|
|
@@ -264,7 +265,9 @@
|
|
|
264
265
|
namedVoices: vr ? int(vr.namedVoices) || 0 : null, humanTouched: vr ? vr.humanTouched === true : null
|
|
265
266
|
};
|
|
266
267
|
}
|
|
267
|
-
|
|
268
|
+
// A row's day in plain words ("Today", "Nov 7"), never the ISO date (6.99.30).
|
|
269
|
+
function day(v) { return W.plainDay ? W.plainDay(v) : v; }
|
|
270
|
+
function dateLine(m) { return [day(m.date), m.time ? clock12(m.time) : '', m.duration].filter(Boolean).join(' · '); }
|
|
268
271
|
function countsSummary(m) {
|
|
269
272
|
var parts = [];
|
|
270
273
|
if (m.topicCount > 0) parts.push(m.topicCount + ' topic' + s(m.topicCount));
|
|
@@ -579,7 +582,7 @@
|
|
|
579
582
|
var message = str(r.body.message) || str(r.body.error) || (r.status === 200 ? 'Naming receipt ready' : 'Naming refused (' + r.status + ')');
|
|
580
583
|
return { message: message, receipt: parseReceipt(r.body, r.status) };
|
|
581
584
|
}, function (err) {
|
|
582
|
-
if (err && err.code === 'offline') throw new Error(
|
|
585
|
+
if (err && err.code === 'offline') throw new Error('Could not reach COS on your Mac. Check that it is awake and on Tailscale, then refresh to review any interrupted naming before retrying.');
|
|
583
586
|
throw err;
|
|
584
587
|
});
|
|
585
588
|
}
|
|
@@ -2049,7 +2052,7 @@
|
|
|
2049
2052
|
out += '<div class="cx-list sp-list">' + p.appearances.map(function (a) {
|
|
2050
2053
|
return '<button type="button" class="cx-row sp-arow" data-sp="open-appearance" data-sid="' + esc(a.sessionId) + '">' +
|
|
2051
2054
|
'<span class="cx-row-glyph' + (a.needsReview ? ' sp-amber' : '') + '">' + icon('speakers') + '</span><span class="cx-row-body">' +
|
|
2052
|
-
'<span class="cx-row-title strong sp-one">' + esc(a.title) + '</span><span class="sp-mono">' + esc(a.date
|
|
2055
|
+
'<span class="cx-row-title strong sp-one">' + esc(a.title) + '</span><span class="sp-mono">' + esc([day(a.date), a.segments + ' segments', formatDuration(a.speakingMs)].filter(Boolean).join(' · ')) + '</span></span>' +
|
|
2053
2056
|
'<span class="sp-match"><b>' + esc(a.observedMatch == null ? '–' : pct(a.observedMatch)) + '</b><small>' + (a.confirmedByHuman ? 'human confirmed' : a.needsReview ? 'review' : 'observed match') + '</small></span>' +
|
|
2054
2057
|
'<span class="chev"></span></button>';
|
|
2055
2058
|
}).join('') + '</div>';
|
|
@@ -118,14 +118,15 @@
|
|
|
118
118
|
'<span class="cx-row-glyph"><svg class="icon"><use href="#i-threads"/></svg></span><span class="cx-row-body">' +
|
|
119
119
|
'<span class="cx-row-title strong">' + esc(t.name || '(untitled)') + '</span>' +
|
|
120
120
|
(subtitle(t) ? '<span class="cx-row-sub">' + esc(subtitle(t)) + '</span>' : '') +
|
|
121
|
-
|
|
121
|
+
// 6.99.30 (design QA): no raw id ("t1") on the row; the thread's own page still shows it, to copy.
|
|
122
|
+
'</span><span class="chev"></span></button>';
|
|
122
123
|
}
|
|
123
124
|
function hitRow(h) {
|
|
124
125
|
return '<button type="button" class="cx-row" data-th="open" data-id="' + esc(h.id) + '">' +
|
|
125
126
|
'<span class="cx-row-glyph"><svg class="icon"><use href="#i-threads"/></svg></span><span class="cx-row-body">' +
|
|
126
127
|
'<span class="cx-row-title strong">' + esc(h.title || h.name || '(untitled)') + '</span>' +
|
|
127
128
|
(h.snippet ? '<span class="cx-row-sub">' + esc(h.snippet) + '</span>' : '') +
|
|
128
|
-
'<span class="cx-meta" style="margin-top:4px"><span class="cx-
|
|
129
|
+
'<span class="cx-meta" style="margin-top:4px"><span class="cx-cap">' + esc(matchLabel(h.match)) + '</span></span>' +
|
|
129
130
|
'</span><span class="chev"></span></button>';
|
|
130
131
|
}
|
|
131
132
|
function list() {
|
|
@@ -12,17 +12,35 @@
|
|
|
12
12
|
|
|
13
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
14
|
html.ta-open { overflow: hidden; }
|
|
15
|
+
/* 6.99.30 (design QA): Today's own question card is drawn in the sheet; the one on the page under it waits. */
|
|
16
|
+
html.ta-open #sq-host { display: none; }
|
|
15
17
|
|
|
16
18
|
.ta-layer {
|
|
19
|
+
/* 6.99.30: the layer is the VISIBLE part of the page, keyboard up or down, in any browser: top and height from
|
|
20
|
+
visualViewport (today-actions.js applyFit, app.js viewportFit), the dynamic viewport where there is none. Whatever
|
|
21
|
+
covers the bottom (a keyboard, its strip, Comet's or any browser's bottom bar) stays below the reply box and Send.
|
|
22
|
+
Only the backdrop and the sheet's colour reach on down to the physical bottom (::before and .ta-sheet::after,
|
|
23
|
+
--ta-below plus the bleed), so no band of Today shows (IMG_0503). The home indicator is cleared once, by the padding
|
|
24
|
+
inside the sheet (.ta-foot, or the sheet when there is no reply box), never by the layer or the sheet. */
|
|
17
25
|
position: fixed; left: 0; right: 0; z-index: 45;
|
|
18
|
-
|
|
19
|
-
|
|
26
|
+
top: var(--ta-vt, 0px); height: var(--ta-vh, 100vh);
|
|
27
|
+
--ta-bleed: 120px;
|
|
20
28
|
--run-green: rgb(56, 145, 99);
|
|
21
29
|
}
|
|
30
|
+
/* Before the first reading (or with script failing), the dynamic viewport where the browser has it. A var() fallback
|
|
31
|
+
the browser cannot parse would void the whole height, so the dvh one sits behind @supports. */
|
|
32
|
+
@supports (height: 100dvh) { .ta-layer { height: var(--ta-vh, 100dvh); } }
|
|
22
33
|
:root[data-theme='dark'] .ta-layer { --run-green: rgb(108, 207, 143); }
|
|
23
34
|
@media (prefers-color-scheme: dark) { :root:not([data-theme='light']) .ta-layer { --run-green: rgb(108, 207, 143); } }
|
|
24
35
|
.ta-layer[hidden] { display: none; }
|
|
25
36
|
.ta-scrim { position: absolute; inset: 0; background: rgba(23, 18, 14, 0.45); touch-action: none; animation: fade 160ms ease; }
|
|
37
|
+
/* Below the visible part, down to the physical bottom and past it: whatever lies there (a browser's bar the page shows
|
|
38
|
+
through, a keyboard, a fixed viewport that stops short) shows the dim and the sheet's own colour, never Today.
|
|
39
|
+
--ta-below is the layout viewport under the layer; the bleed covers a bottom edge reported short.
|
|
40
|
+
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
|
|
41
|
+
band lands on the layer (no data-ta, so nothing happens) instead of closing the overlay or reaching Today below. */
|
|
42
|
+
.ta-layer::before { content: ''; position: absolute; left: 0; right: 0; top: 100%; height: calc(var(--ta-below, 0px) + var(--ta-bleed)); background: rgba(23, 18, 14, 0.45); touch-action: none; animation: fade 160ms ease; }
|
|
43
|
+
.ta-layer .ta-sheet::after { content: ''; position: absolute; left: 0; right: 0; top: 100%; height: calc(var(--ta-below, 0px) + var(--ta-bleed)); background: var(--page); pointer-events: none; }
|
|
26
44
|
|
|
27
45
|
.ta-sheet {
|
|
28
46
|
position: absolute; left: 0; right: 0; bottom: 0;
|
|
@@ -37,16 +55,23 @@ html.ta-open { overflow: hidden; }
|
|
|
37
55
|
}
|
|
38
56
|
.ta-grip { flex: none; width: 36px; height: 5px; border-radius: 3px; background: var(--panel-border); margin: 8px auto 0; }
|
|
39
57
|
.ta-head { flex: none; display: flex; align-items: flex-start; gap: 10px; padding: 6px var(--gutter) 4px; }
|
|
58
|
+
.ta-head-main { flex: 1; min-width: 0; }
|
|
59
|
+
.ta-back { display: inline-flex; align-items: center; min-height: 44px; margin: 0 0 -6px; padding: 0; }
|
|
60
|
+
.ta-back[hidden] { display: none; }
|
|
40
61
|
.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
62
|
.ta-x { flex: none; display: grid; place-items: center; width: 44px; height: 44px; margin-right: -8px; border-radius: 12px; color: var(--ink); }
|
|
42
63
|
.ta-x .icon { width: 20px; height: 20px; stroke-width: 1.8; }
|
|
43
64
|
.ta-body { flex: 1 1 auto; min-height: 0; overflow: auto; overscroll-behavior: contain; -webkit-overflow-scrolling: touch; padding: 2px var(--gutter) 16px; }
|
|
44
65
|
.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; }
|
|
66
|
+
.ta-foot:empty, .ta-foot[hidden] { display: none; }
|
|
46
67
|
.ta-foot .composer { margin-top: 0; position: static; }
|
|
47
68
|
.ta-foot .status-line { margin: 6px 4px 0; min-height: 0; }
|
|
48
69
|
.ta-foot .status-line:empty { display: none; }
|
|
49
70
|
.ta-foot .sx-box, .ta-foot .sx-park { margin: 0 0 10px; }
|
|
71
|
+
/* No reply box (the session list, a question answered in its card): the sheet itself keeps the home indicator's strip,
|
|
72
|
+
so the end of the list and the question card's footer stop above it and nothing scrolls under it. With a keyboard or
|
|
73
|
+
its strip up, that covers the strip. */
|
|
74
|
+
.ta-layer:not(.ta-kb):not(.ta-kb-strip) .ta-sheet:has(> .ta-foot:empty), .ta-layer:not(.ta-kb):not(.ta-kb-strip) .ta-sheet:has(> .ta-foot[hidden]) { padding-bottom: var(--safe-bottom); }
|
|
50
75
|
|
|
51
76
|
.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
77
|
.ta-note { margin: 6px 0 0; color: var(--hot); font-size: 13.5px; line-height: 1.4; }
|
|
@@ -56,6 +81,19 @@ html.ta-open { overflow: hidden; }
|
|
|
56
81
|
.ta-links { display: flex; flex-wrap: wrap; gap: 0 18px; }
|
|
57
82
|
.ta-q { display: grid; gap: 12px; margin: 8px 0 12px; }
|
|
58
83
|
.ta-q[hidden] { display: none; }
|
|
84
|
+
/* 6.99.26: Needs you says where a waiting session can be answered; a pet with nothing in play does nothing */
|
|
85
|
+
.need-where[data-where="here"] { color: var(--hot); font-weight: 600; }
|
|
86
|
+
.pet-btn[aria-disabled="true"] { cursor: default; }
|
|
87
|
+
/* 6.99.26: a question only the Mac's own prompt can answer */
|
|
88
|
+
.ta-mac-line { color: var(--ink); }
|
|
89
|
+
/* 6.99.26: the pet's sessions: waiting on you, running, done today */
|
|
90
|
+
.ta-pet-group { margin: 4px 0 14px; }
|
|
91
|
+
.ta-pet-head { margin: 10px 0 2px; font: 600 15px/1.3 var(--body); color: var(--ink); }
|
|
92
|
+
.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; }
|
|
93
|
+
.ta-pet-head + .ta-pet-row { border-top: 0; }
|
|
94
|
+
.ta-pet-top { display: flex; align-items: center; gap: 8px; min-width: 0; }
|
|
95
|
+
.ta-pet-title { font-size: 15.5px; font-weight: 600; line-height: 1.3; overflow-wrap: anywhere; }
|
|
96
|
+
.ta-pet-line { font-size: 13.5px; line-height: 1.4; color: var(--sub); overflow-wrap: anywhere; }
|
|
59
97
|
.ta-activity { margin: 8px 0 4px; }
|
|
60
98
|
.ta-activity .sx-act-state { margin-top: 0; }
|
|
61
99
|
.ta-act-prompt { margin: 2px 0 0; font-size: 13.5px; line-height: 1.4; color: var(--ink); overflow-wrap: anywhere; }
|
|
@@ -85,6 +123,8 @@ html.ta-open { overflow: hidden; }
|
|
|
85
123
|
@media (min-width: 600px) {
|
|
86
124
|
.ta-sheet {
|
|
87
125
|
left: auto; top: 0; width: clamp(360px, 60vw, 480px); max-height: none;
|
|
126
|
+
/* At the right edge only the right inset (a landscape notch or rounded corner) applies. */
|
|
127
|
+
padding-left: 0;
|
|
88
128
|
border-radius: 20px 0 0 20px; box-shadow: -16px 0 48px rgba(0, 0, 0, 0.22);
|
|
89
129
|
animation-name: ta-slide;
|
|
90
130
|
}
|
|
@@ -92,6 +132,25 @@ html.ta-open { overflow: hidden; }
|
|
|
92
132
|
.ta-head { padding-top: calc(var(--safe-top) + 14px); }
|
|
93
133
|
.ta-foot { padding-bottom: calc(var(--safe-bottom) + 14px); }
|
|
94
134
|
}
|
|
135
|
+
/* 6.99.29 (Miles's unfolded Duo, 11/07): the on-screen keyboard is up, at any width (today-actions.js applyFit marks the
|
|
136
|
+
layer ta-kb when 120 px or more of the page is covered at the bottom). The sheet fills what is visible, the header
|
|
137
|
+
folds to one compact row, the reply box stays pinned at the bottom just above the keyboard, and the conversation
|
|
138
|
+
takes the rest and scrolls. The keyboard covers the home indicator, so the foot drops that inset. */
|
|
139
|
+
.ta-layer.ta-kb .ta-sheet { top: 0; bottom: 0; max-height: none; overflow: hidden; }
|
|
140
|
+
.ta-layer.ta-kb .ta-grip { display: none; }
|
|
141
|
+
.ta-layer.ta-kb .ta-head { align-items: center; padding-top: calc(var(--safe-top) + 6px); padding-bottom: 4px; }
|
|
142
|
+
.ta-layer.ta-kb .ta-head-main { display: flex; align-items: center; gap: 12px; }
|
|
143
|
+
.ta-layer.ta-kb .ta-back { flex: none; min-height: 36px; margin: 0; }
|
|
144
|
+
.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; }
|
|
145
|
+
.ta-layer.ta-kb .ta-x { width: 36px; height: 36px; margin-right: -6px; }
|
|
146
|
+
.ta-layer.ta-kb .ta-body { padding-bottom: 8px; }
|
|
147
|
+
.ta-layer.ta-kb .ta-foot { padding-top: 8px; padding-bottom: 8px; }
|
|
148
|
+
.ta-layer.ta-kb .ta-foot .composer textarea { max-height: 96px; }
|
|
149
|
+
/* 6.99.30: a hardware keyboard's strip with a field focused (ta-kb-strip): the header stays, the strip covers the home
|
|
150
|
+
indicator, so the reply box drops that inset. The layer already ends above the strip. */
|
|
151
|
+
.ta-layer.ta-kb-strip .ta-foot { padding-bottom: 8px; }
|
|
152
|
+
/* Under about three lines left for the conversation: the "Working right now" note waits until the keyboard closes. */
|
|
153
|
+
.ta-layer.ta-kb-tight .ta-foot .sx-park { display: none; }
|
|
95
154
|
@keyframes ta-rise { from { transform: translateY(32px); opacity: 0.6; } }
|
|
96
155
|
@keyframes ta-slide { from { transform: translateX(32px); opacity: 0.6; } }
|
|
97
156
|
/* Dismiss armed: the second tap removes it (no Undo on this server). */
|
|
@@ -107,3 +166,22 @@ html.ta-open { overflow: hidden; }
|
|
|
107
166
|
.nav-row .note.file-error { color: var(--hot); white-space: normal; }
|
|
108
167
|
/* Running card when the session list could not be read (app.js renderRunning). */
|
|
109
168
|
.run-more.run-failed { color: var(--run-open); }
|
|
169
|
+
|
|
170
|
+
/* 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
|
|
171
|
+
the reply box. The chips take their own row above the words; the clip sits beside Send. */
|
|
172
|
+
.ta-composer { flex-wrap: wrap; }
|
|
173
|
+
/* 6.99.30 (canary BLOCKER): five files pushed the reply box and Send off the sheet at every size (the foot never
|
|
174
|
+
shrinks and the keyboard's sheet clips). The chips scroll inside a capped box, so the words and Send always fit: about
|
|
175
|
+
two chip rows with room, one with the keyboard up. */
|
|
176
|
+
.ta-composer .ta-files { flex: 1 1 100%; padding: 2px 0 4px; max-height: min(30vh, 132px); overflow-y: auto; overscroll-behavior: contain; }
|
|
177
|
+
.ta-layer.ta-kb .ta-composer .ta-files { max-height: 60px; }
|
|
178
|
+
.ta-composer .ta-files.hidden { display: none; }
|
|
179
|
+
.ta-clip:empty { display: none; }
|
|
180
|
+
.ta-clip .cc-attach { height: 38px; width: 42px; border-radius: var(--radius-sm); }
|
|
181
|
+
/* 6.99.30 (canary): with the keyboard up a sticky question footer covered all but part of one option; it scrolls with
|
|
182
|
+
the card instead while typing. */
|
|
183
|
+
.ta-layer.ta-kb .ta-q .sq-foot, .ta-layer.ta-kb-strip .ta-q .sq-foot { position: static; }
|
|
184
|
+
/* The "Not sent" line for a refused queued message (today-actions.js loadNotSent). */
|
|
185
|
+
.ta-notsent:empty { display: none; }
|
|
186
|
+
.ta-notsent { margin: 4px 0 8px; }
|
|
187
|
+
.ta-composer.dragging { outline: 2px dashed var(--accent); outline-offset: 4px; }
|