@gotcos/glasses-server 6.99.29 → 7.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +18 -0
- package/CHANGELOG.md +475 -0
- package/README.md +3 -1
- package/managed-runtime-contract.json +3 -0
- package/package.json +1 -1
- package/server/index.ts +29 -7
- package/server/lib/agent-session-output-images.ts +627 -49
- package/server/lib/agent-session-store.ts +24 -11
- package/server/lib/agent-session-tool-images.ts +224 -0
- package/server/lib/agent-session-turns.ts +19 -2
- package/server/lib/api-auth.ts +6 -0
- package/server/lib/ask-work-plan.ts +3 -1
- package/server/lib/calendar-source.ts +491 -0
- package/server/lib/codex-bridge.ts +4 -1
- package/server/lib/codex-run-ledger.ts +29 -0
- package/server/lib/context-builder.ts +2 -1
- package/server/lib/drop-p2-runtime.ts +1 -1
- package/server/lib/drop-store.ts +10 -4
- package/server/lib/learning-needs-you.ts +24 -7
- package/server/lib/live-events.ts +533 -0
- package/server/lib/local-path-redact.ts +272 -0
- package/server/lib/meeting-ask-orientation.ts +40 -6
- package/server/lib/meeting-ask-runner.ts +30 -10
- package/server/lib/morning-brief-runtime.ts +4 -2
- package/server/lib/native-mac-caller.ts +29 -0
- package/server/lib/ping-engine.ts +1 -1
- package/server/lib/ping-sources.ts +11 -6
- package/server/lib/preview-content.ts +172 -0
- package/server/lib/preview-error-code.ts +10 -0
- package/server/lib/preview-folders-setting.ts +133 -0
- package/server/lib/preview-log-throttle.ts +18 -0
- package/server/lib/preview-thumbs.ts +139 -0
- package/server/lib/secret-path-guard.ts +80 -0
- package/server/lib/session-files-setting.ts +88 -0
- package/server/lib/session-links.ts +280 -0
- package/server/lib/thread-turn-queue.ts +10 -0
- package/server/lib/today-brief.ts +119 -28
- package/server/lib/web-memories-bridge.ts +3 -1
- package/server/lib/work-activity.ts +56 -6
- package/server/lib/work-session-activity.ts +260 -0
- package/server/lib/your-move-count.ts +9 -3
- package/server/routes/agent-session-bindings.ts +13 -0
- package/server/routes/agent-session-stream.ts +16 -0
- package/server/routes/agent-sessions.ts +146 -11
- package/server/routes/ask-work-plan.ts +4 -1
- package/server/routes/claude-sessions.ts +5 -1
- package/server/routes/health.ts +35 -0
- package/server/routes/live-events.ts +329 -0
- package/server/routes/permission-broker.ts +69 -5
- package/server/routes/session-preview.ts +200 -0
- package/server/routes/thread-turn-queue.ts +207 -3
- package/server/routes/today-brief.ts +44 -9
- package/server/routes/welcome-context.ts +23 -3
- package/server/routes/work-board.ts +9 -2
- package/server/web-control/app.css +79 -9
- package/server/web-control/app.js +1543 -65
- package/server/web-control/control.css +37 -6
- package/server/web-control/index.html +8 -2
- package/server/web-control/memory-review.js +100 -0
- package/server/web-control/panes/cos-chat.css +51 -6
- package/server/web-control/panes/cos-chat.js +268 -16
- package/server/web-control/panes/meetings.css +23 -4
- package/server/web-control/panes/meetings.js +25 -9
- package/server/web-control/panes/memories.css +18 -3
- package/server/web-control/panes/memories.js +130 -50
- package/server/web-control/panes/messages.js +21 -2
- package/server/web-control/panes/preview.js +318 -0
- package/server/web-control/panes/questions.css +20 -4
- package/server/web-control/panes/questions.js +127 -26
- package/server/web-control/panes/reader-media.js +409 -23
- package/server/web-control/panes/sessions.css +50 -10
- package/server/web-control/panes/settings.js +96 -5
- 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 +49 -18
- package/server/web-control/panes/today-actions.js +217 -50
- package/server/web-control/panes/work.css +11 -1
- package/server/web-control/panes/work.js +10 -2
- package/server/web-control/shell.js +20 -14
|
@@ -54,7 +54,9 @@
|
|
|
54
54
|
onAnswered: null, // messages.js: re-read Recent when a web Ask lands
|
|
55
55
|
thread: null, // 6.99.16: the last GET /api/cos-thread answer
|
|
56
56
|
threadCarried: '', // the thread's newest session started fresh: 'summary' (it carried a digest) or 'plain'
|
|
57
|
-
threadBusy: false
|
|
57
|
+
threadBusy: false,
|
|
58
|
+
threadSession: '', // 7.0.2: the session the COS thread's next Ask continues ('' when it starts fresh)
|
|
59
|
+
defaultGen: 0 // 7.0.2: the newest /api/models read for the Default label; older answers are dropped
|
|
58
60
|
};
|
|
59
61
|
var saved = W.readStore(STORE);
|
|
60
62
|
C.model = typeof saved.model === 'string' ? saved.model : '';
|
|
@@ -221,11 +223,46 @@
|
|
|
221
223
|
});
|
|
222
224
|
return out;
|
|
223
225
|
}
|
|
226
|
+
// 7.0.2 (A7): what "Default" resolves to on this Mac (/api/models defaultModel: the session's preference, then
|
|
227
|
+
// COS_G2_DEFAULT_MODEL, then the built-in default), named in the menu: "Default (Codex Frontier)". On 11/07 Default
|
|
228
|
+
// was Codex while Codex was out of usage, and nothing on the page said so. An older server sends no field: plain Default.
|
|
229
|
+
// A session bound to a key profile reads "Default (key profile)".
|
|
230
|
+
function defaultLabel(d) {
|
|
231
|
+
if (d && d.source === 'profile') return 'key profile';
|
|
232
|
+
var pref = d && typeof d.preference === 'string' ? d.preference : '';
|
|
233
|
+
if (!pref) return '';
|
|
234
|
+
for (var i = 0; i < CLAUDE_MENU.length; i++) if (CLAUDE_MENU[i][0] === pref) return CLAUDE_MENU[i][1];
|
|
235
|
+
if (pref === 'haiku') return 'Claude Haiku';
|
|
236
|
+
return ENGINE[pref] ? (ENGINE[pref] + (FALLBACK[pref] ? ' ' + FALLBACK[pref] : '')) : '';
|
|
237
|
+
}
|
|
238
|
+
// The session an Ask from the chat continues, whose preference "Default" follows: the COS thread's newest session in
|
|
239
|
+
// thread mode, else the chat's own (C.session).
|
|
240
|
+
function defaultSession() { return threadOn() ? String(C.threadSession || '') : C.session && C.session.id ? String(C.session.id) : ''; }
|
|
241
|
+
// Each read is stamped with a generation and the session it asked about. An answer is used for the label only while
|
|
242
|
+
// it is still the newest read and the chat still continues that session, so a slow answer for an older session never
|
|
243
|
+
// overwrites a newer label (skeptic, 7.0.2).
|
|
244
|
+
function modelsRead() {
|
|
245
|
+
var sid = defaultSession();
|
|
246
|
+
return { gen: ++C.defaultGen, sid: sid, path: '/api/models' + (sid ? '?sessionId=' + encodeURIComponent(sid) : '') };
|
|
247
|
+
}
|
|
248
|
+
function currentRead(r) { return r.gen === C.defaultGen && r.sid === defaultSession(); }
|
|
249
|
+
// QA W5: the label is read again after each Ask finishes and whenever that session changes, so it never goes stale.
|
|
250
|
+
function refreshDefault() {
|
|
251
|
+
if (!C.models) return null;
|
|
252
|
+
var r = modelsRead();
|
|
253
|
+
return api(r.path).then(function (m) {
|
|
254
|
+
if (!currentRead(r)) return;
|
|
255
|
+
var label = defaultLabel(m && m.defaultModel);
|
|
256
|
+
if (label !== C.defaultLabel) { C.defaultLabel = label; paintModelMenu(); }
|
|
257
|
+
}, function () {});
|
|
258
|
+
}
|
|
224
259
|
function loadModels() {
|
|
225
260
|
if (C.models) return;
|
|
226
261
|
C.models = [];
|
|
227
|
-
|
|
262
|
+
var r = modelsRead();
|
|
263
|
+
api(r.path).then(function (m) {
|
|
228
264
|
C.models = modelsFrom(m);
|
|
265
|
+
if (currentRead(r)) C.defaultLabel = defaultLabel(m && m.defaultModel);
|
|
229
266
|
if (C.model && !C.models.some(function (o) { return o.value === C.model; })) { C.model = ''; save(); }
|
|
230
267
|
// Steer exists only where the server says so (/api/models capabilities.durableQueryJobs.steer).
|
|
231
268
|
var j = m && m.capabilities && m.capabilities.durableQueryJobs;
|
|
@@ -235,7 +272,7 @@
|
|
|
235
272
|
}
|
|
236
273
|
function modelMenuHtml() {
|
|
237
274
|
var list = C.models || [];
|
|
238
|
-
var opts = ['<option value="">Default</option>'].concat(list.map(function (m) {
|
|
275
|
+
var opts = ['<option value="">' + esc(C.defaultLabel ? 'Default (' + C.defaultLabel + ')' : 'Default') + '</option>'].concat(list.map(function (m) {
|
|
239
276
|
return '<option value="' + esc(m.value) + '"' + (m.value === C.model ? ' selected' : '') + (m.available ? '' : ' disabled') + '>' + esc(m.label + (m.available ? '' : ' (not available)')) + '</option>';
|
|
240
277
|
}));
|
|
241
278
|
var efforts = ['<option value="">Default</option>'].concat(EFFORTS.map(function (e) {
|
|
@@ -427,9 +464,10 @@
|
|
|
427
464
|
(item.title ? '<p class="cc-card-title">' + esc(item.title) + '</p>' : '') +
|
|
428
465
|
'<p class="cc-card-line">' + esc(item.line) + '</p>' + fileLinksHtml(item, false) + actionsHtml(item) + '</div></div>';
|
|
429
466
|
}
|
|
430
|
-
// The finished answer as Markdown
|
|
467
|
+
// The finished answer as Markdown, its lens-plain inline lists on their own lines first (structureLensAnswer); while it
|
|
468
|
+
// streams, plain words, so a half-written "**bold" never shows its stars.
|
|
431
469
|
function answerHtml(a) {
|
|
432
|
-
if (a.response) return W.md(a.response);
|
|
470
|
+
if (a.response) return W.md(W.structureLensAnswer(a.response));
|
|
433
471
|
if (!a.partial) return '';
|
|
434
472
|
return '<p>' + esc(a.partial.replace(/\*\*|__|`|^#+\s*/gm, '')).replace(/\n{2,}/g, '</p><p>').replace(/\n/g, '<br>') + '</p>';
|
|
435
473
|
}
|
|
@@ -454,6 +492,10 @@
|
|
|
454
492
|
'<button type="button" class="link-btn cc-text-btn" data-cos="cancel" data-cid="' + a.cid + '">Cancel</button>';
|
|
455
493
|
} else if (a.status === 'completed' && a.no) {
|
|
456
494
|
acts = '<button type="button" class="cx-btn primary" data-cos="open-msg" data-no="' + a.no + '">Open in Messages</button>';
|
|
495
|
+
} else if ((a.status === 'failed' || a.status === 'interrupted') && a.jobId && !a.retried && !a.prepDraftId) {
|
|
496
|
+
acts = a.againBlocked
|
|
497
|
+
? '<button type="button" class="cx-btn" disabled aria-disabled="true">Ask again</button><span class="cc-fine">' + esc(a.againBlocked) + '</span>'
|
|
498
|
+
: '<button type="button" class="cx-btn primary" data-cos="again" data-cid="' + a.cid + '">Ask again</button>';
|
|
457
499
|
}
|
|
458
500
|
// A drop that became a question already shows its words as the drop: only the answer card is added.
|
|
459
501
|
return (linked ? '' : '<div class="cc-line you"><div class="cc-bubble">' + esc(a.query) + '</div>' + sentAttsHtml(a.atts) + '<div class="cc-src">' + (a.replyKey ? 'Replied on the web' : 'Asked on the web') + '</div></div>') +
|
|
@@ -953,8 +995,18 @@
|
|
|
953
995
|
var line = threadLine();
|
|
954
996
|
document.querySelectorAll('[data-cos-thread]').forEach(function (el) { el.textContent = line; });
|
|
955
997
|
}
|
|
998
|
+
// Every thread answer (read, a turn recorded, New thread) also says which session the next Ask continues, so the
|
|
999
|
+
// Default label follows the thread's own session (skeptic, 7.0.2). A fresh start continues none.
|
|
1000
|
+
function threadIdOf(t) { return t && t.thread && typeof t.thread.id === 'string' ? t.thread.id : ''; }
|
|
1001
|
+
function setThread(t) {
|
|
1002
|
+
C.thread = t;
|
|
1003
|
+
var n = t && t.next;
|
|
1004
|
+
var sid = n ? (n.mode === 'continue' && typeof n.sessionId === 'string' ? n.sessionId : '') : (t && t.thread && typeof t.thread.sessionId === 'string' ? t.thread.sessionId : '');
|
|
1005
|
+
if (sid !== C.threadSession) { C.threadSession = sid; if (threadOn()) refreshDefault(); }
|
|
1006
|
+
paintThread();
|
|
1007
|
+
}
|
|
956
1008
|
function loadThread() {
|
|
957
|
-
return api('/api/cos-thread').then(function (t) {
|
|
1009
|
+
return api('/api/cos-thread').then(function (t) { setThread(t); return t; }, function () { return null; });
|
|
958
1010
|
}
|
|
959
1011
|
// What the next Ask in the thread sends: a sessionId to continue, or none and the digest to carry.
|
|
960
1012
|
function threadPlan() {
|
|
@@ -979,7 +1031,7 @@
|
|
|
979
1031
|
if (Number.isInteger(a.no) && a.no > 0) body.messageNo = a.no;
|
|
980
1032
|
api('/api/cos-thread/turn', { method: 'POST', body: body }).then(function (t) {
|
|
981
1033
|
a.turnState = 'done';
|
|
982
|
-
if (t && t.thread)
|
|
1034
|
+
if (t && t.thread) setThread(t);
|
|
983
1035
|
}, function () { a.turnState = 'done'; });
|
|
984
1036
|
}
|
|
985
1037
|
function newThread() {
|
|
@@ -987,8 +1039,8 @@
|
|
|
987
1039
|
C.threadBusy = true;
|
|
988
1040
|
api('/api/cos-thread/new', { method: 'POST', body: {} }).then(function (t) {
|
|
989
1041
|
C.threadBusy = false;
|
|
990
|
-
C.
|
|
991
|
-
|
|
1042
|
+
C.threadCarried = '';
|
|
1043
|
+
setThread(t);
|
|
992
1044
|
}, function (err) {
|
|
993
1045
|
C.threadBusy = false;
|
|
994
1046
|
W.toast((err && err.message) || 'New thread did not go through.');
|
|
@@ -1014,16 +1066,23 @@
|
|
|
1014
1066
|
function submitAsk(text, dropId, done, opts) {
|
|
1015
1067
|
opts = opts || {};
|
|
1016
1068
|
var a = { cid: uuid(), query: text, at: Date.now(), status: 'sending', partial: '', dropId: dropId || '', atts: opts.atts || [], replyKey: opts.replyKey || '' };
|
|
1069
|
+
// 7.0.2 (A6): what Ask again sends: the same words, files and conversation.
|
|
1070
|
+
a.again = { attachmentIds: (opts.attachmentIds || []).slice(0, MAX_ATTS), atts: (opts.atts || []).slice(0, MAX_ATTS), sessionId: opts.sessionId || '', replyKey: opts.replyKey || '', context: opts.context || '' };
|
|
1017
1071
|
C.asks.push(a);
|
|
1018
1072
|
if (a.replyKey) paintReplyFeed(a.replyKey); else paintFeed(true);
|
|
1019
1073
|
var body = { clientJobId: a.cid, generation: 1, query: text, source: 'web', activityToolMode: 'status' };
|
|
1020
1074
|
if (C.model) body.model = C.model;
|
|
1021
1075
|
if (C.effort) body.effort = C.effort;
|
|
1022
1076
|
// 6.99.16: an Ask from the chat's own composer belongs to the COS thread (a Reply keeps its turn's conversation).
|
|
1077
|
+
// An Ask again only reaches here once askAgain has confirmed the conversation is still this one (skeptic, 7.0.2).
|
|
1023
1078
|
a.threaded = threadOn() && !opts.replyKey;
|
|
1024
|
-
|
|
1079
|
+
// A Reply never takes the chat's session: with no session of its own it starts a conversation, as its card says.
|
|
1080
|
+
if (!a.threaded && !opts.replyKey && C.session && C.session.id) body.sessionId = C.session.id;
|
|
1025
1081
|
// A reply continues the turn's own conversation (kept when it is still live; else the server starts a new one).
|
|
1026
1082
|
if (opts.sessionId) body.sessionId = opts.sessionId;
|
|
1083
|
+
// 7.0.2: which conversation this Ask is for, as Ask again needs it: the replied-to turn's session, the COS thread
|
|
1084
|
+
// (its id, read when the plan is made), or the chat's own session ('' for a new one).
|
|
1085
|
+
a.conv = a.replyKey ? { kind: 'reply', sessionId: opts.sessionId || '' } : a.threaded ? { kind: 'thread', threadId: '' } : { kind: 'chat', sessionId: body.sessionId || '' };
|
|
1027
1086
|
if (opts.attachmentIds && opts.attachmentIds.length) body.attachmentIds = opts.attachmentIds.slice(0, MAX_ATTS);
|
|
1028
1087
|
if (C.caps && C.caps.steer) body.steerable = true;
|
|
1029
1088
|
var admit = function (retry) {
|
|
@@ -1036,22 +1095,41 @@
|
|
|
1036
1095
|
});
|
|
1037
1096
|
};
|
|
1038
1097
|
var prepare = (a.threaded ? threadPlan().then(function (plan) {
|
|
1039
|
-
|
|
1098
|
+
a.conv.threadId = threadIdOf(C.thread);
|
|
1099
|
+
if (plan.sessionId) { body.sessionId = plan.sessionId; C.threadSession = plan.sessionId; }
|
|
1040
1100
|
else if (plan.digest) body.reference = { query: THREAD_REF, response: plan.digest };
|
|
1041
1101
|
a.carried = plan.sessionId ? '' : plan.digest ? 'summary' : 'plain';
|
|
1042
|
-
}) : Promise.resolve()).then(function () {
|
|
1102
|
+
}) : Promise.resolve()).then(function () {
|
|
1103
|
+
// An Ask again: the conversation its first try was for must still be the one this send resolved to, checked here,
|
|
1104
|
+
// after the plan, immediately before admission (root review, 7.0.2: the thread can change between askAgain's check
|
|
1105
|
+
// and this send's own thread read, and a retry carries old files and context).
|
|
1106
|
+
if (opts.expect && !sameConversation(opts.expect, a)) throw Object.assign(new Error(MOVED_ON), { code: 'conversation_moved' });
|
|
1107
|
+
withContext(body, opts.context);
|
|
1108
|
+
});
|
|
1043
1109
|
prepare.then(function () { return admit(true); }).then(function (d) {
|
|
1044
1110
|
var job = d && d.job; if (!job) throw new Error('The Mac did not take the question.');
|
|
1045
1111
|
take(a, job);
|
|
1046
|
-
|
|
1112
|
+
var before = defaultSession();
|
|
1113
|
+
if (a.threaded) { C.threadCarried = a.carried; if (job.sessionId) C.threadSession = job.sessionId; paintThread(); }
|
|
1047
1114
|
else if (job.sessionId && !a.replyKey) { C.session = { id: job.sessionId, day: today() }; save(); }
|
|
1115
|
+
if (defaultSession() !== before) refreshDefault();
|
|
1048
1116
|
if (dropId) api('/api/drops/' + encodeURIComponent(dropId) + '/ask', { method: 'POST', body: { clientJobId: a.cid, generation: 1 } }).catch(function () {});
|
|
1049
1117
|
if (done) done(true);
|
|
1050
1118
|
poll(a);
|
|
1051
1119
|
}, function (err) {
|
|
1120
|
+
if (err && err.code === 'conversation_moved' && opts.retryOf) {
|
|
1121
|
+
// Nothing was sent: this try leaves the chat, and the failed card it came from says why, its button disabled.
|
|
1122
|
+
C.asks = C.asks.filter(function (x) { return x !== a; });
|
|
1123
|
+
opts.retryOf.retried = false; opts.retryOf.againBlocked = MOVED_ON;
|
|
1124
|
+
if (a.replyKey) paintReplyFeed(a.replyKey); else paintFeed(false);
|
|
1125
|
+
if (done) done(false);
|
|
1126
|
+
return;
|
|
1127
|
+
}
|
|
1052
1128
|
a.status = 'failed';
|
|
1053
1129
|
a.error = err && err.code === 'durable_query_jobs_disabled' ? 'Ask needs the durable query jobs switch on your Mac.'
|
|
1054
|
-
: err && /attachment/.test(String(err.code || '')) ? 'An attachment could not be read: ' + ((err && err.message) || err.code) +
|
|
1130
|
+
: err && /attachment/.test(String(err.code || '')) ? 'An attachment could not be read: ' + ((err && err.message) || err.code) +
|
|
1131
|
+
// An Ask again never came from the box (QA): say what to do instead of pointing at it.
|
|
1132
|
+
(opts.reask ? ' Attach the file again in the box and ask again.' : ' Your words and files are still in the box.')
|
|
1055
1133
|
: 'Not sent: ' + ((err && err.message) || 'the Mac did not answer');
|
|
1056
1134
|
if (a.replyKey) paintReplyFeed(a.replyKey); else paintFeed(false);
|
|
1057
1135
|
if (done) done(false);
|
|
@@ -1074,7 +1152,99 @@
|
|
|
1074
1152
|
if (typeof job.partialText === 'string') a.partial = job.partialText;
|
|
1075
1153
|
if (typeof job.response === 'string') a.response = job.response;
|
|
1076
1154
|
if (job.steerable === false) a.steerable = false;
|
|
1077
|
-
if (job.error && (a.status === 'failed' || a.status === 'interrupted')) a.error =
|
|
1155
|
+
if (job.error && (a.status === 'failed' || a.status === 'interrupted')) a.error = errorLine(job);
|
|
1156
|
+
}
|
|
1157
|
+
// 7.0.2 (A6, the Duo audit): the line on a failed card says why and what to do, by the server's error.code. On 11/07
|
|
1158
|
+
// Codex was out of usage until Nov 13 and the card said only "COS could not finish this answer". The reset time comes
|
|
1159
|
+
// from the provider's own words in error.message when they carry one, else from retryAfterMs, else it is left out.
|
|
1160
|
+
var GENERIC_FAIL = 'COS could not finish this answer. Ask again when you are ready.';
|
|
1161
|
+
var PROVIDER_NAME = { claude: 'Claude', codex: 'Codex', cursor: 'Cursor', ollama: 'Ollama' };
|
|
1162
|
+
function providerName(job) {
|
|
1163
|
+
if (job && PROVIDER_NAME[job.provider]) return PROVIDER_NAME[job.provider];
|
|
1164
|
+
var m = String((job && (job.resolvedModel || job.requestedModel)) || '');
|
|
1165
|
+
if (/^codex/.test(m)) return 'Codex';
|
|
1166
|
+
if (/^cursor/.test(m)) return 'Cursor';
|
|
1167
|
+
if (/^ollama/.test(m)) return 'Ollama';
|
|
1168
|
+
if (/^(opus|fable|sonnet|haiku)/.test(m)) return 'Claude';
|
|
1169
|
+
return '';
|
|
1170
|
+
}
|
|
1171
|
+
// "until <when>" or "for <how long>" from the provider's words or retryAfterMs; '' when neither says. A machine time
|
|
1172
|
+
// (ISO 8601, or one that names UTC or GMT) is shown in this browser's own time zone.
|
|
1173
|
+
var RESET_ISO = /\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?(?:Z|[+-]\d{2}:?\d{2})?/;
|
|
1174
|
+
function localWhen(d) {
|
|
1175
|
+
try { return d.toLocaleString(undefined, { month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit' }); } catch (_e) { return ''; }
|
|
1176
|
+
}
|
|
1177
|
+
function resetTime(err) {
|
|
1178
|
+
var msg = String((err && err.message) || '').slice(0, 2000);
|
|
1179
|
+
var m = /\b(?:try again|available again|resets?)\s+(at|on|after|in)?\s*([^.!\n·(]{2,48})/i.exec(msg), prep = m ? String(m[1] || '').toLowerCase() : '';
|
|
1180
|
+
if (!m) { m = /\buntil\s+()([^.!\n·(]{2,48})/i.exec(msg); prep = ''; }
|
|
1181
|
+
if (m) {
|
|
1182
|
+
var iso = RESET_ISO.exec(msg.slice(m.index));
|
|
1183
|
+
var when = iso && iso.index < 24 ? new Date(iso[0].replace(' ', 'T')) : /\b(?:UTC|GMT)\b/.test(m[2]) ? new Date(m[2].trim()) : null;
|
|
1184
|
+
if (when && !isNaN(when.getTime())) { var local = localWhen(when); if (local) return 'until ' + local; }
|
|
1185
|
+
var t = m[2].replace(/[\s,;:]+$/, '').trim();
|
|
1186
|
+
if (/\d/.test(t)) return (prep === 'in' ? 'for ' : 'until ') + t;
|
|
1187
|
+
}
|
|
1188
|
+
var ms = err && Number(err.retryAfterMs);
|
|
1189
|
+
if (ms > 0 && isFinite(ms)) { var later = localWhen(new Date(Date.now() + ms)); return later ? 'until ' + later : ''; }
|
|
1190
|
+
return '';
|
|
1191
|
+
}
|
|
1192
|
+
function errorLine(job) {
|
|
1193
|
+
var err = (job && job.error) || {}, code = String(err.code || ''), who = providerName(job);
|
|
1194
|
+
if (code === 'provider_limit') {
|
|
1195
|
+
var until = resetTime(err);
|
|
1196
|
+
return (who || 'This model') + ' is out of usage ' + (until || 'for now') + '. Pick another model under Model and ask again.';
|
|
1197
|
+
}
|
|
1198
|
+
if (code === 'provider_auth') return (who || 'This model') + ' is not signed in on your Mac. Sign in there, or pick another model under Model and ask again.';
|
|
1199
|
+
if (code === 'provider_context_too_long') return 'This conversation is too long for ' + (who || 'this model') + '. Start a new chat, or pick a model with a larger context under Model.';
|
|
1200
|
+
if (code === 'provider_timeout') return 'The model took longer than the time limit, so COS stopped waiting. Ask again, or pick a lower Effort for a faster answer.';
|
|
1201
|
+
if (code === 'interrupted' || (job && job.status === 'interrupted' && !code)) {
|
|
1202
|
+
if (err.cause === 'server_shutdown') return 'COS on your Mac shut down before this answer finished. When it is back, ask again.';
|
|
1203
|
+
if (err.cause === 'crash_loop_parked') return 'COS on your Mac stopped after crashing several times, so it set this question aside. Open COS Control on the Mac, then ask again.';
|
|
1204
|
+
return 'COS on your Mac restarted before this answer finished. Ask again when you are ready.';
|
|
1205
|
+
}
|
|
1206
|
+
return GENERIC_FAIL;
|
|
1207
|
+
}
|
|
1208
|
+
// Ask again: the same words and files as a new Ask (a new client job id), with the model and effort picked now, to the
|
|
1209
|
+
// SAME conversation as the first try. If that conversation has moved on (New thread, another thread now current, the
|
|
1210
|
+
// chat on another session, thread mode switched), nothing is sent: the button stays, disabled, with why (skeptic,
|
|
1211
|
+
// 7.0.2). A Reply goes to its turn's session as before. The failed card keeps its line and, once resent, loses the
|
|
1212
|
+
// button. Resolves to the new Ask, or null.
|
|
1213
|
+
var MOVED_ON = 'This conversation has moved on. Copy your words into a new Ask.';
|
|
1214
|
+
// Whether the Ask `a` (being sent now) resolved to the conversation `c`: the same thread, the chat's same session, or
|
|
1215
|
+
// the same Reply session. Read after the send's own plan, so it sees the thread that send will really use.
|
|
1216
|
+
function sameConversation(c, a) {
|
|
1217
|
+
if (!c || !a.conv || c.kind !== a.conv.kind) return false;
|
|
1218
|
+
if (c.kind === 'thread') return threadOn() && !!c.threadId && a.conv.threadId === c.threadId;
|
|
1219
|
+
return a.conv.sessionId === c.sessionId;
|
|
1220
|
+
}
|
|
1221
|
+
function conversationGone(a) {
|
|
1222
|
+
var c = a.conv;
|
|
1223
|
+
if (!c || c.kind === 'reply') return Promise.resolve('');
|
|
1224
|
+
if (c.kind === 'chat') {
|
|
1225
|
+
var now = C.session && C.session.id ? String(C.session.id) : '';
|
|
1226
|
+
return Promise.resolve(threadOn() || now !== c.sessionId ? MOVED_ON : '');
|
|
1227
|
+
}
|
|
1228
|
+
if (!threadOn() || !c.threadId) return Promise.resolve(MOVED_ON);
|
|
1229
|
+
return loadThread().then(function (t) { return threadIdOf(t) === c.threadId ? '' : MOVED_ON; });
|
|
1230
|
+
}
|
|
1231
|
+
function askAgain(cid) {
|
|
1232
|
+
var a = C.asks.filter(function (x) { return x.cid === cid; })[0];
|
|
1233
|
+
// A Prep card is never resent as a plain Ask (QA W4): Prep has its own Run.
|
|
1234
|
+
if (!a || a.retried || a.checking || a.againBlocked || a.prepDraftId || !(a.status === 'failed' || a.status === 'interrupted') || !a.jobId) return Promise.resolve(null);
|
|
1235
|
+
a.checking = true;
|
|
1236
|
+
return conversationGone(a).then(function (why) {
|
|
1237
|
+
a.checking = false;
|
|
1238
|
+
if (why) { a.againBlocked = why; if (a.replyKey) paintReplyFeed(a.replyKey); else paintFeed(false); return null; }
|
|
1239
|
+
a.retried = true;
|
|
1240
|
+
var g = a.again || {}, o = {};
|
|
1241
|
+
if (g.attachmentIds && g.attachmentIds.length) { o.attachmentIds = g.attachmentIds.slice(0, MAX_ATTS); o.atts = (g.atts || []).slice(0, MAX_ATTS); }
|
|
1242
|
+
if (g.replyKey) { o.replyKey = g.replyKey; o.sessionId = g.sessionId; }
|
|
1243
|
+
if (g.context) o.context = g.context;
|
|
1244
|
+
o.reask = true;
|
|
1245
|
+
if (a.conv) { o.expect = a.conv; o.retryOf = a; }
|
|
1246
|
+
return submitAsk(a.query, null, null, o);
|
|
1247
|
+
});
|
|
1078
1248
|
}
|
|
1079
1249
|
function poll(a) {
|
|
1080
1250
|
var started = Date.now();
|
|
@@ -1091,6 +1261,7 @@
|
|
|
1091
1261
|
}
|
|
1092
1262
|
function finished(a) {
|
|
1093
1263
|
postTurn(a);
|
|
1264
|
+
refreshDefault();
|
|
1094
1265
|
if (a.replyKey) paintReplyFeed(a.replyKey); else paintFeed(false);
|
|
1095
1266
|
if (C.onAnswered) C.onAnswered();
|
|
1096
1267
|
// A drop that became a question gets its answer as a follow-up: read the chat again.
|
|
@@ -1811,6 +1982,7 @@
|
|
|
1811
1982
|
return;
|
|
1812
1983
|
}
|
|
1813
1984
|
if (what === 'cancel') return cancel(t.getAttribute('data-cid'));
|
|
1985
|
+
if (what === 'again') return askAgain(t.getAttribute('data-cid'));
|
|
1814
1986
|
if (what === 'att-remove') return removeAtt(kOf(t), t.getAttribute('data-att'));
|
|
1815
1987
|
if (what === 'att-retry') {
|
|
1816
1988
|
var k = kOf(t);
|
|
@@ -1826,7 +1998,7 @@
|
|
|
1826
1998
|
}
|
|
1827
1999
|
function isComposerInput(el) { return !!(el && el.classList && el.classList.contains('cc-input')); }
|
|
1828
2000
|
function onKey(e) {
|
|
1829
|
-
if (!isComposerInput(e.target) || e.key !== 'Enter' || e.isComposing || e.shiftKey) return;
|
|
2001
|
+
if (!isComposerInput(e.target) || e.key !== 'Enter' || e.isComposing || e.keyCode === 229 || e.shiftKey) return;
|
|
1830
2002
|
e.preventDefault();
|
|
1831
2003
|
var k = kOf(e.target);
|
|
1832
2004
|
if (e.altKey) { if (k === CHAT && dropOn()) doDrop(); return; }
|
|
@@ -1880,6 +2052,85 @@
|
|
|
1880
2052
|
form.classList.remove('dragging');
|
|
1881
2053
|
if (e.type === 'drop') addFiles(composer(form.getAttribute('data-cos-form')), e.dataTransfer.files);
|
|
1882
2054
|
}
|
|
2055
|
+
// 7.0.2 (A2, the Duo audit): after the keyboard closes on the chat (one column, the page scrolls), iOS can leave the
|
|
2056
|
+
// page scrolled past its end, which showed as blank page under Model and Effort. Once it has closed (app.js
|
|
2057
|
+
// COSWeb.onKeyboard 'after'), scrollY comes back inside the page's range, and when the chat's own box had the
|
|
2058
|
+
// keyboard the composer goes back to the bottom of the screen.
|
|
2059
|
+
var LAST = { composer: false };
|
|
2060
|
+
document.addEventListener('focusin', function (e) {
|
|
2061
|
+
var f = e && e.target && e.target.closest ? e.target.closest('[data-cos-form]') : null;
|
|
2062
|
+
LAST.composer = !!(f && f.getAttribute('data-cos-form') === CHAT.name);
|
|
2063
|
+
});
|
|
2064
|
+
function settleAfterKeyboard() {
|
|
2065
|
+
var r = W.parseRoute();
|
|
2066
|
+
if (wide() || r.pane !== 'messages' || r.args[0] !== 'cos') return false;
|
|
2067
|
+
var se = document.scrollingElement || document.documentElement;
|
|
2068
|
+
if (!se) return false;
|
|
2069
|
+
var max = Math.max(0, Math.round((se.scrollHeight || 0) - (window.innerHeight || 0))), y = Math.round(window.scrollY || 0);
|
|
2070
|
+
if (y > max) { window.scrollTo(0, max); return true; }
|
|
2071
|
+
var form = formOf(CHAT);
|
|
2072
|
+
if (LAST.composer && form && form.getBoundingClientRect && (window.innerHeight || 0) - form.getBoundingClientRect().bottom > 1) { window.scrollTo(0, max); return true; }
|
|
2073
|
+
return false;
|
|
2074
|
+
}
|
|
2075
|
+
// 7.0.2 (A4): two columns (860 px and up, the unfolded Duo) with the keyboard up for a field in the chat column.
|
|
2076
|
+
// cos-chat.css makes the column the visible box (html.kb-chat), but a sticky column only reaches its top once the page
|
|
2077
|
+
// has scrolled to it, so on open the page scrolls it there, and where the page is too short to, the column is capped
|
|
2078
|
+
// to what is left above the keyboard. The cap follows each change of the visible box while the keyboard is up (a
|
|
2079
|
+
// keyboard rising in steps, QA W6) and goes when it closes, when focus leaves the column, or below 860 px (a fold
|
|
2080
|
+
// with the keyboard up never keeps it).
|
|
2081
|
+
var DOCK_MIN_PX = 120;
|
|
2082
|
+
function dockWide(what, v) {
|
|
2083
|
+
var mr = C.host;
|
|
2084
|
+
if (!mr || !mr.style) return false;
|
|
2085
|
+
var a = document.activeElement;
|
|
2086
|
+
var keep = (what === 'open' || what === 'move') && wide() && !!a && !!mr.contains && mr.contains(a) && W.parseRoute().pane === 'messages';
|
|
2087
|
+
v = v || (W.viewportFit ? W.viewportFit() : null);
|
|
2088
|
+
if (!keep || !v || !mr.getBoundingClientRect) { mr.style.maxHeight = ''; return false; }
|
|
2089
|
+
var want = v.top + 8, top = mr.getBoundingClientRect().top;
|
|
2090
|
+
// On open, and on any later move that left the column's top too low to fit a usable column (an unfold with the
|
|
2091
|
+
// keyboard up, canary 7.0.2 WARNING 2), the page scrolls the column up to the visible box.
|
|
2092
|
+
var low = top > v.top + v.height - DOCK_MIN_PX - 8;
|
|
2093
|
+
if ((what === 'open' || low) && top > want + 1) { window.scrollBy(0, Math.round(top - want)); top = mr.getBoundingClientRect().top; }
|
|
2094
|
+
mr.style.maxHeight = Math.max(DOCK_MIN_PX, Math.round(v.top + v.height - Math.max(top, v.top) - 8)) + 'px';
|
|
2095
|
+
return true;
|
|
2096
|
+
}
|
|
2097
|
+
// 7.0.2 (QA W7): which composer the keyboard is for. html.kb-up (app.js) says only that a keyboard is up on the page;
|
|
2098
|
+
// html.kb-chat says it is for a field in the chat column (cos-chat.css docks the column and the one-column composer on
|
|
2099
|
+
// it), and .cc-kb marks the one composer that has it (only that one turns compact). The Messages search box at 860 px
|
|
2100
|
+
// and up, or a field in another pane, leaves the chat as it is.
|
|
2101
|
+
function markKeyboard(up) {
|
|
2102
|
+
var a = up ? document.activeElement : null, de = document.documentElement;
|
|
2103
|
+
var form = a && a.closest ? a.closest('[data-cos-form]') : null;
|
|
2104
|
+
document.querySelectorAll('.cc-composer.cc-kb').forEach(function (f) { if (f !== form) f.classList.remove('cc-kb'); });
|
|
2105
|
+
if (form && form.classList) form.classList.add('cc-kb');
|
|
2106
|
+
var inChat = !!(a && C.host && C.host.contains && C.host.contains(a));
|
|
2107
|
+
if (de && de.classList) de.classList.toggle('kb-chat', inChat);
|
|
2108
|
+
return inChat;
|
|
2109
|
+
}
|
|
2110
|
+
// The composer that has the keyboard, brought into view on open and on every later change of the visible box (a fold
|
|
2111
|
+
// or an unfold with the keyboard up, canary 7.0.2). Only while the keyboard is for the chat, and only for a composer
|
|
2112
|
+
// (a steer box or the search box is left where it is).
|
|
2113
|
+
function revealComposer() {
|
|
2114
|
+
var a = document.activeElement, de = document.documentElement;
|
|
2115
|
+
if (!a || !de || !de.classList || !de.classList.contains('kb-chat')) return false;
|
|
2116
|
+
var form = a.closest ? a.closest('[data-cos-form]') : null;
|
|
2117
|
+
if (!form || !form.scrollIntoView) return false;
|
|
2118
|
+
form.scrollIntoView({ block: 'nearest' });
|
|
2119
|
+
return true;
|
|
2120
|
+
}
|
|
2121
|
+
if (W.onKeyboard) W.onKeyboard(function (what, v) {
|
|
2122
|
+
if (what === 'after') { settleAfterKeyboard(); return; }
|
|
2123
|
+
markKeyboard(what !== 'close');
|
|
2124
|
+
dockWide(what, v);
|
|
2125
|
+
if (what !== 'close') revealComposer();
|
|
2126
|
+
});
|
|
2127
|
+
// Focus moving between fields while the keyboard stays up: mark (and dock) for the new one.
|
|
2128
|
+
document.addEventListener('focusin', function () {
|
|
2129
|
+
if (!W.keyboardUp || !W.keyboardUp()) return;
|
|
2130
|
+
markKeyboard(true);
|
|
2131
|
+
dockWide('move');
|
|
2132
|
+
});
|
|
2133
|
+
|
|
1883
2134
|
// Crossing 860 px shows the chat beside the list: load it then.
|
|
1884
2135
|
window.matchMedia('(min-width: 860px)').addEventListener('change', function (m) {
|
|
1885
2136
|
if (m.matches && on() && C.host && W.parseRoute().pane === 'messages') mount(C.host, true);
|
|
@@ -1942,6 +2193,7 @@
|
|
|
1942
2193
|
prepInstance: function () { return PREP_INST; }, notePrepInstance: function (id) { if (typeof id === 'string' && id) PREP_INST = id; },
|
|
1943
2194
|
_prep: function () { return P; }, _openPrep: openPrep, _prepHtml: function () { return P ? prepPanelHtml(P) : ''; }, _addPrepFiles: addPrepFiles,
|
|
1944
2195
|
_runPrep: function () { runPrep(P); }, _sha256Js: sha256Js, _closePrep: closePrep, _onPrepInput: onPrepInput, _savePrepSteer: function () { return P ? savePrepSteer(P) : null; },
|
|
2196
|
+
_errorLine: errorLine, _askAgain: askAgain, _askHtml: askHtml, _settle: settleAfterKeyboard, _dockWide: dockWide, _revealComposer: revealComposer, _markKeyboard: markKeyboard, _refreshDefault: refreshDefault, _defaultLabel: defaultLabel, _modelMenuHtml: modelMenuHtml,
|
|
1945
2197
|
_state: C, _composer: composer, _classify: classify, _modelsFrom: modelsFrom, _addFiles: addFiles, _itemHtml: itemHtml, _setCaps: setCaps, _threadLine: threadLine, _postTurn: postTurn
|
|
1946
2198
|
};
|
|
1947
2199
|
})();
|
|
@@ -21,7 +21,10 @@
|
|
|
21
21
|
.mt-wd { text-align: center; font: 600 10px/1 var(--mono); letter-spacing: 0.04em; color: var(--tertiary); padding: 2px 0 4px; }
|
|
22
22
|
.mt-day, .mt-blank { min-height: 42px; }
|
|
23
23
|
.mt-day { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 4px; border-radius: 8px; color: var(--ink); font: 400 14px/1 var(--body); }
|
|
24
|
-
.mt-day:disabled { opacity:
|
|
24
|
+
.mt-day:disabled { opacity: 1; }
|
|
25
|
+
/* 6.99.30 (design QA): a day with nothing to open read at 2.35:1 at 38% ink; the secondary colour keeps it quiet and
|
|
26
|
+
readable. */
|
|
27
|
+
.mt-day:disabled .mt-dnum { color: var(--sub); }
|
|
25
28
|
.mt-day:not(:disabled):hover { background: var(--qopen); }
|
|
26
29
|
.mt-day[aria-pressed='true'] { background: color-mix(in srgb, var(--tint) 18%, transparent); font-weight: 600; }
|
|
27
30
|
.mt-day:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
|
|
@@ -158,9 +161,12 @@
|
|
|
158
161
|
.md-check { display: inline-block; width: 12px; height: 12px; border: 1.5px solid var(--sub); border-radius: 3px; margin-right: 7px; vertical-align: -1px; }
|
|
159
162
|
.md-check.on { background: var(--green); border-color: var(--green); }
|
|
160
163
|
.md-table-wrap { overflow-x: auto; margin: 0 0 1em; -webkit-overflow-scrolling: touch; }
|
|
161
|
-
|
|
164
|
+
/* Duo tables: separated borders so the held first column (control.css table.tf) keeps its line; the same look. */
|
|
165
|
+
.mt-md { --tf-bg: var(--page); }
|
|
166
|
+
.mt-answer .mt-md { --tf-bg: var(--qopen); }
|
|
167
|
+
.md-table { border-collapse: separate; border-spacing: 0; font-size: 14px; border: 1px solid var(--panel-border); border-radius: 6px; min-width: 60%; }
|
|
162
168
|
.md-table th, .md-table td { padding: 6px 12px; text-align: left; vertical-align: top; border-bottom: 1px solid var(--hairline); }
|
|
163
|
-
.md-table th { font-weight: 600; background: var(--qopen); }
|
|
169
|
+
.md-table th, .md-table.tf thead tr > :first-child { font-weight: 600; background: var(--qopen); }
|
|
164
170
|
.md-table tr:last-child td { border-bottom: 0; }
|
|
165
171
|
.md-table .md-key { color: var(--sub); white-space: nowrap; }
|
|
166
172
|
.md-code { overflow-x: auto; font: 400 12.5px/1.45 var(--mono); background: var(--qopen); border: 1px solid var(--panel-border); padding: 10px; border-radius: 8px; margin: 0 0 1em; }
|
|
@@ -353,5 +359,18 @@
|
|
|
353
359
|
@media (min-width: 860px) {
|
|
354
360
|
.mt-turns { height: clamp(320px, calc(100dvh - 400px), 1100px); }
|
|
355
361
|
.mt-lgrid { grid-template-columns: minmax(0, 1fr) minmax(280px, min(380px, 38%)); align-items: start; column-gap: 24px; }
|
|
356
|
-
.mt-lgrid > .mt-side-panel { position: sticky; top: calc(62px + var(--safe-top, 0px)); max-height: calc(100vh - 80px - var(--safe-top, 0px)); overflow: auto; }
|
|
362
|
+
.mt-lgrid > .mt-side-panel { position: sticky; top: calc(62px + var(--safe-top, 0px)); max-height: calc(100vh - 80px - var(--safe-top, 0px)); max-height: calc(100dvh - 80px - var(--safe-top, 0px)); overflow: auto; }
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/* Duo tables: the divider between the notes or live transcript and the Ask panel (app.js splitKit, styled in app.css;
|
|
366
|
+
meetings.js SPLIT). From 860 px only, where both show. Its 24 px column is the old gap, so the default layout is
|
|
367
|
+
unchanged; a dragged width sizes the Ask panel, clamped so it keeps 280 px and the notes 380 px. */
|
|
368
|
+
@media (min-width: 860px) {
|
|
369
|
+
.mt-detail .mt-dbody.split-side { grid-template-columns: minmax(0, 1fr) 24px minmax(280px, min(380px, 38%)); column-gap: 0; }
|
|
370
|
+
.mt-detail .mt-dbody.split-side.split-sized { grid-template-columns: minmax(0, 1fr) 24px clamp(280px, var(--split-side-share, 30%), calc(100% - 404px)); }
|
|
371
|
+
.mt-detail .mt-dbody.split-side > .split-handle { display: flex; grid-column: 2; grid-row: 1 / span 20; top: calc(62px + var(--safe-top, 0px)); height: calc(100dvh - 80px - var(--safe-top, 0px)); }
|
|
372
|
+
.mt-detail .mt-dbody.split-side > :not(.mt-doc):not(.split-handle) { grid-column: 3; }
|
|
373
|
+
.mt-lgrid.split-side { grid-template-columns: minmax(0, 1fr) 24px minmax(280px, min(380px, 38%)); column-gap: 0; }
|
|
374
|
+
.mt-lgrid.split-side.split-sized { grid-template-columns: minmax(0, 1fr) 24px clamp(280px, var(--split-side-share, 30%), calc(100% - 404px)); }
|
|
375
|
+
.mt-lgrid.split-side > .split-handle { display: flex; top: calc(62px + var(--safe-top, 0px)); height: calc(100dvh - 80px - var(--safe-top, 0px)); }
|
|
357
376
|
}
|
|
@@ -102,7 +102,7 @@
|
|
|
102
102
|
// "2026-11-03 · 9:05 PM · P · 60 minutes · Mac System Audio"
|
|
103
103
|
function subtitle(m) {
|
|
104
104
|
var parts = [];
|
|
105
|
-
if (m.date) parts.push(m.date);
|
|
105
|
+
if (m.date) parts.push(W.plainDay ? W.plainDay(m.date) : m.date); // "Today" or "Nov 7", never 2026-11-07 (6.99.30)
|
|
106
106
|
if (m.time) parts.push(fmtTime(m.time));
|
|
107
107
|
var d = domainLabel(m); if (d) parts.push(d);
|
|
108
108
|
if (m.duration) parts.push(m.duration); else if (num(m.durationMinutes) > 0) parts.push(num(m.durationMinutes) + ' min');
|
|
@@ -391,13 +391,18 @@
|
|
|
391
391
|
case 'table': {
|
|
392
392
|
var kv = b.header.length === 2 && b.rows.length && b.rows.every(function (r) { return r.length === 2 && r[0].indexOf('**') === 0; });
|
|
393
393
|
var cols = Math.max(b.header.length, b.rows.reduce(function (m, r) { return Math.max(m, r.length); }, 0));
|
|
394
|
-
|
|
394
|
+
// Duo tables: the shared column floors (app.js tableFit), measured on the rows that show.
|
|
395
|
+
var fit = W.tableFit(kv ? b.rows : [b.header].concat(b.rows));
|
|
396
|
+
function row(r, tag, key, first) {
|
|
395
397
|
var h = '<tr>';
|
|
396
|
-
for (var c = 0; c < cols; c++)
|
|
398
|
+
for (var c = 0; c < cols; c++) {
|
|
399
|
+
var w = fit.width(c, first);
|
|
400
|
+
h += '<' + tag + (key && c === 0 ? ' class="md-key"' : '') + (w ? ' style="' + w + '"' : '') + '>' + fit.cell(c, inline(r[c] || ''), first) + '</' + tag + '>';
|
|
401
|
+
}
|
|
397
402
|
return h + '</tr>';
|
|
398
403
|
}
|
|
399
|
-
return '<div class="md-table-wrap"><table class="md-table' + (kv ? ' kv' : '') + '">' + (kv ? '' : '<thead>' + row(b.header, 'th') + '</thead>') +
|
|
400
|
-
'<tbody>' + b.rows.map(function (r) { return row(r, 'td', kv); }).join('') + '</tbody></table></div>';
|
|
404
|
+
return '<div class="md-table-wrap tf-wrap"><table class="md-table ' + fit.tableClass + (kv ? ' kv' : '') + '">' + (kv ? '' : '<thead>' + row(b.header, 'th', false, true) + '</thead>') +
|
|
405
|
+
'<tbody>' + b.rows.map(function (r, n) { return row(r, 'td', kv, kv && n === 0); }).join('') + '</tbody></table></div>';
|
|
401
406
|
}
|
|
402
407
|
case 'details': {
|
|
403
408
|
var sp = b.blocks.filter(function (x) { return x.t === 'speakers'; })[0];
|
|
@@ -1245,7 +1250,9 @@
|
|
|
1245
1250
|
else if (!d) body = '<p class="cx-note mt-wait">Loading meeting…</p>' + W.skeletonRows(5);
|
|
1246
1251
|
else {
|
|
1247
1252
|
var sessions = detailSessions();
|
|
1248
|
-
|
|
1253
|
+
// Duo tables: the line between the notes and the Ask panel drags where both show (860 px and up).
|
|
1254
|
+
var divider = sessions.length ? SPLIT_KIT.handle({ id: 'mt-dhandle', controls: 'mt-ask-' + safeId(sessions[0]), label: SPLIT.label }) : '';
|
|
1255
|
+
body = detailBarHtml(row, d) + '<div class="mt-dbody' + (divider ? ' split-side' : '') + '" id="mt-dbody">' + divider +
|
|
1249
1256
|
(row.sourceSessionId && sessions.length ? '<p class="mt-note-line">Ask history covers the whole original capture, including its other split pieces.</p>' : '') +
|
|
1250
1257
|
sessions.map(function (sid, i) {
|
|
1251
1258
|
A(sid).ctx = { saved: true, meeting: row, captureLabel: 'Original recording ' + (i + 1) + '. Questions use its saved cutoff; each recording keeps its own history.' };
|
|
@@ -1255,7 +1262,14 @@
|
|
|
1255
1262
|
'<article class="mt-doc">' + documentHtml(d) + '</article></div>';
|
|
1256
1263
|
}
|
|
1257
1264
|
pane.innerHTML = '<div class="cx mt mt-detail" data-tint="meetings">' + head + body + '</div>';
|
|
1258
|
-
|
|
1265
|
+
SPLIT_KIT.mount($('mt-dbody'), $('mt-dhandle'), SPLIT);
|
|
1266
|
+
}
|
|
1267
|
+
// Duo tables: the divider between a meeting's notes or live transcript and its Ask panel (app.js splitKit, 860 px and
|
|
1268
|
+
// up), one width for both views. The Ask panel is the column on the right, so it grows as the line moves left; it keeps
|
|
1269
|
+
// 280 px and the notes 380 px, past a 24 px divider that is the old gap.
|
|
1270
|
+
// A page without the kit (a test's stand-in W) draws no divider and keeps working.
|
|
1271
|
+
var SPLIT_KIT = W.splitKit || { handle: function () { return ''; }, mount: function () { return null; } };
|
|
1272
|
+
var SPLIT = { key: 'cos_web_meetings_split', label: 'Ask panel', side: 'end', sized: '.mt-side-panel', cssVar: '--split-side-share', media: '(min-width: 860px)', listMin: 280, readerMin: 380, handleW: 24 };
|
|
1259
1273
|
// ── Prep in a saved meeting (6.99.19) ─────────────────
|
|
1260
1274
|
// GET /api/prep/by-session/:sessionId for each of the meeting's recordings: what the prep focused on and its files,
|
|
1261
1275
|
// each opening from the Prep store. Nothing shows when there is no prep, or on a server without Prep.
|
|
@@ -2001,12 +2015,14 @@
|
|
|
2001
2015
|
// 6.99.14: the transcript scrolls inside its own box, as Control's does, so a new line never moves the page: the
|
|
2002
2016
|
// title, the recording bar, the files and the Ask panel stay where the reader left them. Everything that changes
|
|
2003
2017
|
// height while it records (the stop line, the files, the confirm card) sits under the box, never above it.
|
|
2004
|
-
|
|
2018
|
+
var divider = SPLIT_KIT.handle({ id: 'mt-lhandle', controls: 'mt-ask-' + safeId(sid), label: SPLIT.label });
|
|
2019
|
+
pane.innerHTML = '<div class="cx mt mt-livev" data-tint="meetings"><div id="mt-lhead"></div><div class="mt-lgrid' + (divider ? ' split-side' : '') + '" id="mt-lgrid"><div class="mt-lmain" id="mt-lmain">' +
|
|
2005
2020
|
'<div class="mt-tbox"><div id="mt-turns" class="mt-turns" tabindex="0" role="log" aria-live="polite" aria-label="Live transcript"></div>' +
|
|
2006
2021
|
'<button type="button" class="cx-btn quiet mt-jump" id="mt-jump" data-mt="jump" hidden>Jump to live</button></div>' +
|
|
2007
2022
|
'<div id="mt-lbar" class="mt-lbarslot"></div><div id="mt-lfiles"></div><div id="mt-lprep"></div><div id="mt-lfoot"></div>' +
|
|
2008
|
-
'<input type="file" id="mt-lpick" class="mt-pick" multiple accept="' + esc(LIVE_ACCEPT) + '" tabindex="-1" aria-hidden="true"></div>' +
|
|
2023
|
+
'<input type="file" id="mt-lpick" class="mt-pick" multiple accept="' + esc(LIVE_ACCEPT) + '" tabindex="-1" aria-hidden="true"></div>' + divider +
|
|
2009
2024
|
'<aside class="mt-side-panel" id="mt-ask-' + safeId(sid) + '" data-sid="' + esc(sid) + '">' + sidePanelHtml(sid) + '</aside></div></div>';
|
|
2025
|
+
SPLIT_KIT.mount($('mt-lgrid'), $('mt-lhandle'), SPLIT);
|
|
2010
2026
|
S.headHtml = S.footHtml = S.barHtml = S.filesHtml = S.prepHtml = null;
|
|
2011
2027
|
paintLiveHead(); paintTurns(true); paintLiveFoot();
|
|
2012
2028
|
clearInterval(S.liveTimer); clearInterval(S.clockTimer);
|
|
@@ -28,6 +28,18 @@
|
|
|
28
28
|
.mm-warn { color: var(--amber); font-size: 13px; }
|
|
29
29
|
.mm-split { margin-top: 2px; }
|
|
30
30
|
.mm-split, .mm-list, .mm-reader { min-width: 0; }
|
|
31
|
+
/* Duo tables: the divider between the list and the reader (app.js splitKit, styled in app.css), from 760 px only. Its
|
|
32
|
+
18 px column replaces the split's 18 px gap, so the default layout is unchanged; a dragged width is a share of the
|
|
33
|
+
split, clamped so the list keeps 240 px and the reader 380 px (memories.js SPLIT), like Sessions. */
|
|
34
|
+
@media (min-width: 760px) {
|
|
35
|
+
.split.mm-split { grid-template-columns: minmax(260px, 340px) 18px minmax(0, 1fr); column-gap: 0; }
|
|
36
|
+
.split.mm-split.split-sized { grid-template-columns: clamp(240px, var(--split-list-share, 30%), calc(100% - 398px)) 18px minmax(0, 1fr); }
|
|
37
|
+
.split.mm-split > .split-handle { display: flex; }
|
|
38
|
+
.mm-reader .split-full-btn { display: inline-flex; }
|
|
39
|
+
/* Full width: the reader takes the split; the list and the divider come back with Show list. */
|
|
40
|
+
.split.mm-split.split-full.mm-fullable:not(.solo) { grid-template-columns: minmax(0, 1fr); }
|
|
41
|
+
.split.mm-split.split-full.mm-fullable:not(.solo) > .split-list, .split.mm-split.split-full.mm-fullable:not(.solo) > .split-handle { display: none; }
|
|
42
|
+
}
|
|
31
43
|
|
|
32
44
|
/* A reader on a narrow screen is a pushed page: the header, views and counts step aside. */
|
|
33
45
|
@media (max-width: 759px) {
|
|
@@ -128,14 +140,16 @@
|
|
|
128
140
|
.mm-try { margin: 8px 0 2px; }
|
|
129
141
|
.mm-tries { margin: 0; padding: 0; list-style: none; display: grid; gap: 4px; }
|
|
130
142
|
.mm-tries .mm-link { font-size: 13.5px; overflow-wrap: anywhere; }
|
|
131
|
-
.mm-answer { margin: 12px 0 8px; padding: 12px 14px; border-radius: 10px; background: var(--qopen); font-size: 14.5px; line-height: 1.55; overflow-wrap: anywhere; max-height: 60vh; overflow-y: auto; }
|
|
143
|
+
.mm-answer { margin: 12px 0 8px; padding: 12px 14px; border-radius: 10px; background: var(--qopen); font-size: 14.5px; line-height: 1.55; overflow-wrap: anywhere; max-height: 60vh; max-height: 60dvh; overflow-y: auto; }
|
|
132
144
|
.mm-answer h4 { margin: 10px 0 4px; font: 600 14.5px/1.3 var(--body); }
|
|
133
145
|
.mm-answer p { margin: 0 0 8px; }
|
|
134
146
|
.mm-answer ul { margin: 0 0 8px; padding-left: 1.2em; }
|
|
135
147
|
.mm-answer code { font: 400 13px/1.4 var(--mono); background: var(--panel); padding: 0 3px; border-radius: 4px; }
|
|
136
148
|
.mm-table { overflow-x: auto; max-width: 100%; }
|
|
137
|
-
.
|
|
138
|
-
.mm-answer
|
|
149
|
+
/* Duo tables: grid lines and the held first column are control.css (table.tf-grid), in this pane's border colour. */
|
|
150
|
+
.mm-answer { --tf-bg: var(--qopen); --tf-line: var(--panel-border); }
|
|
151
|
+
.mm-answer table { font-size: 13px; }
|
|
152
|
+
.mm-answer th, .mm-answer td { padding: 4px 8px; text-align: left; vertical-align: top; }
|
|
139
153
|
.mm-refs ol { margin: 0; padding-left: 1.4em; font-size: 13px; line-height: 1.45; }
|
|
140
154
|
.mm-refs li { margin-bottom: 4px; overflow-wrap: anywhere; }
|
|
141
155
|
.mm-ref { color: var(--accent); font-variant-numeric: tabular-nums; }
|
|
@@ -259,6 +273,7 @@ body.mm-modal-open { overflow: hidden; }
|
|
|
259
273
|
.split.mm-split.solo { display: block; }
|
|
260
274
|
.split.mm-split.solo .split-list { position: static; max-height: none; overflow: visible; max-width: 820px; }
|
|
261
275
|
.split.mm-split.solo .reader { display: none; }
|
|
276
|
+
.split.mm-split.solo > .split-handle { display: none; }
|
|
262
277
|
.mm-cue-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; margin: 6px 2px 12px; }
|
|
263
278
|
.mm-cue-head p { margin: 3px 0 0; }
|
|
264
279
|
.mm-cue-title { margin: 0; font: 500 24px/1.15 var(--display); color: var(--ink); letter-spacing: -0.01em; }
|