@gotcos/glasses-server 7.0.7 → 7.0.27
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 +19 -0
- package/CHANGELOG.md +305 -0
- package/docs/web-key-verifier-contract.md +378 -0
- package/managed-runtime-contract.json +9 -1
- package/package.json +10 -3
- package/server/bootstrap.ts +10 -0
- package/server/index.ts +129 -23
- package/server/lib/agent-session-older.ts +218 -0
- package/server/lib/agent-session-recent.ts +321 -0
- package/server/lib/agent-session-store.ts +102 -20
- package/server/lib/api-auth.ts +37 -1
- package/server/lib/calendar-meetings.ts +70 -0
- package/server/lib/calendar-source.ts +54 -9
- package/server/lib/claude-hooks-installer.ts +111 -12
- package/server/lib/conversation.ts +56 -28
- package/server/lib/cors-reject-log.ts +51 -0
- package/server/lib/cos-started-sessions.ts +101 -0
- package/server/lib/disk-io-meter.ts +202 -0
- package/server/lib/drop-service.ts +47 -12
- package/server/lib/drop-store.ts +21 -1
- package/server/lib/event-loop-stall.ts +62 -0
- package/server/lib/full-hold-engine.ts +114 -0
- package/server/lib/full-hold.ts +124 -0
- package/server/lib/g2-turn-provenance.ts +128 -9
- package/server/lib/list-model-memo.ts +85 -0
- package/server/lib/listener-startup.ts +18 -0
- package/server/lib/meeting-batch-transcribe.ts +32 -5
- package/server/lib/model-efforts.ts +40 -0
- package/server/lib/permission-broker.ts +443 -11
- package/server/lib/secret-path-guard.ts +4 -1
- package/server/lib/session-cache-writer.ts +8 -1
- package/server/lib/session-hook-events.ts +23 -1
- package/server/lib/session-hooks-runtime.ts +34 -0
- package/server/lib/session-log.ts +313 -4
- package/server/lib/session-organizer.ts +305 -0
- package/server/lib/session-state-derive.ts +36 -0
- package/server/lib/session-stream-events.ts +39 -3
- package/server/lib/thread-turn-queue.ts +24 -0
- package/server/lib/today-brief.ts +522 -58
- package/server/lib/turn-end-ask.ts +320 -0
- package/server/lib/turn-footer.ts +3 -3
- package/server/lib/web-control-integrity.ts +160 -0
- package/server/lib/web-control-offnet-page.ts +58 -0
- package/server/lib/web-control-static.ts +53 -9
- package/server/lib/web-key-authority.ts +372 -0
- package/server/lib/web-key-envelope.ts +98 -0
- package/server/lib/web-key-handoff.ts +135 -0
- package/server/lib/web-key-origin.ts +21 -0
- package/server/lib/web-key-passkey.ts +155 -0
- package/server/lib/web-key-registry.ts +825 -0
- package/server/lib/web-key-signer-app.ts +152 -0
- package/server/lib/web-session-auth.ts +133 -15
- package/server/lib/whisper-local.ts +93 -7
- package/server/lib/whisper-preview.ts +5 -3
- package/server/lib/work-orchestrator/snapshot.ts +17 -3
- package/server/lib/work-review-runtime.ts +17 -4
- package/server/routes/agent-session-bindings.ts +16 -1
- package/server/routes/agent-session-stream.ts +7 -0
- package/server/routes/agent-sessions.ts +106 -2
- package/server/routes/drops.ts +7 -2
- package/server/routes/g2-turns.ts +35 -0
- package/server/routes/health.ts +14 -1
- package/server/routes/live-events.ts +6 -1
- package/server/routes/permission-broker.ts +26 -0
- package/server/routes/query-jobs.ts +3 -0
- package/server/routes/session-organizer.ts +149 -0
- package/server/routes/sessions.ts +15 -24
- package/server/routes/thread-turn-queue.ts +50 -14
- package/server/routes/today-brief.ts +91 -36
- package/server/routes/web-control.ts +37 -4
- package/server/routes/web-key.ts +284 -0
- package/server/routes/welcome-context.ts +6 -2
- package/server/routes/work-orchestrator.ts +12 -7
- package/server/scripts/write-web-control-manifest.mjs +43 -0
- package/server/web-control/.manifest.json +44 -0
- package/server/web-control/app.css +45 -0
- package/server/web-control/app.js +1680 -108
- package/server/web-control/apple-touch-icon.png +0 -0
- package/server/web-control/index.html +54 -17
- package/server/web-control/panes/cos-chat.css +52 -6
- package/server/web-control/panes/cos-chat.js +364 -32
- package/server/web-control/panes/questions.js +135 -14
- package/server/web-control/panes/sessions.css +280 -0
- package/server/web-control/panes/settings.js +1 -1
- package/server/web-control/panes/today-actions.css +35 -0
- package/server/web-control/panes/today-actions.js +121 -6
- package/server/web-control/panes/work.css +3 -0
- package/server/web-control/panes/work.js +185 -38
- package/server/web-control/theme.js +33 -0
- package/server/web-signer/.manifest.json +11 -0
- package/server/web-signer/index.html +74 -0
- package/server/web-signer/signer-proof.js +348 -0
- package/server/web-signer/signer-store.js +39 -0
- package/server/web-signer/signer.css +23 -0
- package/server/web-signer/signer.js +341 -0
- package/shared/web-approval-packets.golden.json +124 -0
- package/shared/web-turn-envelope.ts +271 -0
|
@@ -267,6 +267,7 @@
|
|
|
267
267
|
if (window.requestAnimationFrame) window.requestAnimationFrame(go); else go();
|
|
268
268
|
}
|
|
269
269
|
function fit() {
|
|
270
|
+
if (O.open) panCheck();
|
|
270
271
|
if (KB.frame || !O.open) return;
|
|
271
272
|
if (!window.requestAnimationFrame) return applyFit(); // every browser with visualViewport has frames
|
|
272
273
|
KB.frame = window.requestAnimationFrame(applyFit);
|
|
@@ -321,6 +322,49 @@
|
|
|
321
322
|
// opt.from: a session opened from the pet's list ({ scroll }), which gets Back to sessions and its own history entry,
|
|
322
323
|
// so the phone's Back returns to the list too (Miles's iPhone, 11/07). opt.again: the list shown again from there,
|
|
323
324
|
// its entry already current.
|
|
325
|
+
// Miles 11/10 20:16 (IMG_0570): with the panel open on his Duo, the panel and the page under it moved together. Two
|
|
326
|
+
// stories fit: the page scrolled behind the panel, or the visible area panned past the layout viewport (the page
|
|
327
|
+
// drawn at 0.85). These numbers tell them apart, so nothing is fixed on a guess (lead 20:26): while the panel is open,
|
|
328
|
+
// ONE layout report per open when window.scrollY, visualViewport.pageTop or visualViewport.offsetTop first changes,
|
|
329
|
+
// with the values at open and now, the panel's top and bottom, and whether a keyboard was up. layoutCrumb adds
|
|
330
|
+
// innerHeight (ih), the visible height (vvh) and scale (vvs) itself. Numbers only, inside layoutCrumb's caps.
|
|
331
|
+
var PAN = { at: null, sent: false, fn: null };
|
|
332
|
+
function panNum(v) { return typeof v === 'number' && isFinite(v) ? Math.round(v) : null; }
|
|
333
|
+
function panOffsets() {
|
|
334
|
+
var vv = window.visualViewport;
|
|
335
|
+
return { sy: panNum(window.scrollY), pt: vv ? panNum(vv.pageTop) : null, ot: vv ? panNum(vv.offsetTop) : null,
|
|
336
|
+
sx: panNum(window.scrollX), pl: vv ? panNum(vv.pageLeft) : null, ol: vv ? panNum(vv.offsetLeft) : null };
|
|
337
|
+
}
|
|
338
|
+
function panState() {
|
|
339
|
+
var r = layer && typeof layer.getBoundingClientRect === 'function' ? layer.getBoundingClientRect() : null, o = panOffsets();
|
|
340
|
+
var rail = document.querySelector ? document.querySelector('.rail') : null, rr = rail && rail.getBoundingClientRect ? rail.getBoundingClientRect() : null;
|
|
341
|
+
o.pTop = r ? panNum(r.top) : null; o.pBot = r ? panNum(r.bottom) : null; o.pL = r ? panNum(r.left) : null; o.pR = r ? panNum(r.right) : null;
|
|
342
|
+
o.railL = rr ? panNum(rr.left) : null; o.railR = rr ? panNum(rr.right) : null;
|
|
343
|
+
return o;
|
|
344
|
+
}
|
|
345
|
+
// Runs on the window's scroll and, through fit, on every visualViewport resize and scroll while the panel is open.
|
|
346
|
+
// The panel's box is read only once something has moved, and only once per open.
|
|
347
|
+
function panCheck() {
|
|
348
|
+
// PAN.at is set only between open and close (close stops the watch before it clears O.open).
|
|
349
|
+
if (PAN.sent || !PAN.at) return;
|
|
350
|
+
var moved = panOffsets(), was = PAN.at;
|
|
351
|
+
// Up and down (sy, pt, ot) and, like split-drag, sideways (sx, pl, ol: Q1 20:44, lead 20:4x).
|
|
352
|
+
if (moved.sy === was.sy && moved.pt === was.pt && moved.ot === was.ot && moved.sx === was.sx && moved.pl === was.pl && moved.ol === was.ol) return;
|
|
353
|
+
PAN.sent = true;
|
|
354
|
+
var now = panState();
|
|
355
|
+
if (W.layoutCrumb) W.layoutCrumb('ta-pan', { kind: O.open.kind, kb: !!KB.on, sy0: was.sy, pt0: was.pt, ot0: was.ot, sy: now.sy, pt: now.pt, ot: now.ot,
|
|
356
|
+
sx0: was.sx, pl0: was.pl, ol0: was.ol, sx: now.sx, pl: now.pl, ol: now.ol,
|
|
357
|
+
pTop: now.pTop, pBot: now.pBot, pL: now.pL, pR: now.pR, railL: now.railL, railR: now.railR });
|
|
358
|
+
}
|
|
359
|
+
function panWatch(on) {
|
|
360
|
+
if (on) {
|
|
361
|
+
PAN.at = panOffsets(); PAN.sent = false; PAN.fn = panCheck;
|
|
362
|
+
window.addEventListener('scroll', panCheck, { passive: true });
|
|
363
|
+
} else if (PAN.fn) {
|
|
364
|
+
if (window.removeEventListener) window.removeEventListener('scroll', panCheck, { passive: true });
|
|
365
|
+
PAN.fn = null; PAN.at = null;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
324
368
|
function open(kind, key, opener, opt) {
|
|
325
369
|
opt = opt || {};
|
|
326
370
|
build();
|
|
@@ -334,6 +378,7 @@
|
|
|
334
378
|
document.documentElement.classList.add('ta-open');
|
|
335
379
|
var shell = $('app-shell'); if (shell) shell.setAttribute('inert', '');
|
|
336
380
|
listenViewport(true); applyFit();
|
|
381
|
+
panWatch(true);
|
|
337
382
|
// The entry names what is open, so a reload can open it again and Back still closes it (restore below).
|
|
338
383
|
var entry = { cosTodayOverlay: 1, kind: kind, key: O.open.key };
|
|
339
384
|
if (O.back) entry.from = 'pet';
|
|
@@ -390,6 +435,7 @@
|
|
|
390
435
|
function close(how) {
|
|
391
436
|
if (!O.open) return;
|
|
392
437
|
stopSession();
|
|
438
|
+
panWatch(false);
|
|
393
439
|
O.open = null;
|
|
394
440
|
if (layer) layer.hidden = true;
|
|
395
441
|
unfit();
|
|
@@ -537,6 +583,8 @@
|
|
|
537
583
|
return open(pk === 'wait' ? 'wait' : 'run', id, O.opener, { from: { scroll: lb ? lb.scrollTop || 0 : 0 } });
|
|
538
584
|
}
|
|
539
585
|
if (what === 'send') return sendReply();
|
|
586
|
+
if (what === 'turnend') return sendChoice(Number(id));
|
|
587
|
+
if (what === 'approve-phone' && K.approvePhone && O.r) return K.approvePhone(phoneApprovalOptions(O.r));
|
|
540
588
|
if (what === 'att-remove' || what === 'att-retry') {
|
|
541
589
|
var fb = filesBox(); if (!fb) return;
|
|
542
590
|
O.note = { text: '', kind: '' }; paintNote();
|
|
@@ -772,7 +820,7 @@
|
|
|
772
820
|
}, function (err) {
|
|
773
821
|
row.undoing = false;
|
|
774
822
|
if (err && err.status === 404) O.loop = false;
|
|
775
|
-
row.undoNote = err && err.status === 404 ? 'Undo
|
|
823
|
+
row.undoNote = err && err.status === 404 ? 'Undo did not go through. Change it in Work.' : (err && err.message) || 'Undo did not go through.';
|
|
776
824
|
if (row.state === 'dismissed') { row.until = Date.now() + UNDO_SECONDS * 1000; expireLater(); }
|
|
777
825
|
}).then(paintIntake);
|
|
778
826
|
}
|
|
@@ -829,7 +877,8 @@
|
|
|
829
877
|
paintFiles();
|
|
830
878
|
// The Mac's answer about files may not be in yet: ask once, then draw the clip.
|
|
831
879
|
var kit = KIT(), r0 = O.r;
|
|
832
|
-
|
|
880
|
+
paintPhoneOffer(r0);
|
|
881
|
+
if (kit && !kit.turnFiles()) kit.ensureCaps().then(function () { if (O.r === r0) { paintFiles(); paintPhoneOffer(r0); } }, function () {});
|
|
833
882
|
// Files of a send a reload cut off go again with the same words; say so, since their chips are gone.
|
|
834
883
|
if (O.r.draft && O.r.draft.restored) {
|
|
835
884
|
var nf = O.r.draft.ids.length;
|
|
@@ -911,6 +960,14 @@
|
|
|
911
960
|
'<p class="status-line" id="ta-status" role="status" aria-live="polite"></p>';
|
|
912
961
|
}
|
|
913
962
|
|
|
963
|
+
// The same exact packet picker and handoff as Sessions. This does not upgrade an ordinary Send.
|
|
964
|
+
function phoneApprovalOptions(r) {
|
|
965
|
+
return { r: r, prefix: 'ta', live: function () { return O.r === r && !!O.open; },
|
|
966
|
+
status: function (text, kind) { if (O.r === r) { O.note = { text: text, kind: kind || '' }; paintNote(); } },
|
|
967
|
+
files: function () { return filesState(filesBox(r)); } };
|
|
968
|
+
}
|
|
969
|
+
function paintPhoneOffer(r) { if (K.phoneOffer) K.phoneOffer(r, phoneApprovalOptions(r)); }
|
|
970
|
+
|
|
914
971
|
// ── files with a reply or a note (the COS chat's attach kit) ──
|
|
915
972
|
// One box per session, kept while this tab is open, so closing and reopening the sheet keeps what was attached.
|
|
916
973
|
var FILES = {};
|
|
@@ -1020,12 +1077,14 @@
|
|
|
1020
1077
|
// A session waiting on you: what it waits on, its question to answer here when the Mac holds one for this page;
|
|
1021
1078
|
// when the Mac's own prompt has it, that, read-only; else (a reply is the answer) the reply box below. No live
|
|
1022
1079
|
// peek and no Stop: it is not at work (W4-r2).
|
|
1023
|
-
html += '<p class="ta-sub">' + T.badge(O.r ? O.r.provider : 'claude') + ' <span>' + esc(O.wait ? O.wait.why : 'Waiting for you') + '</span></p>';
|
|
1024
1080
|
var where = waitWhere();
|
|
1081
|
+
html += '<p class="ta-sub">' + T.badge(O.r ? O.r.provider : 'claude') + ' <span>' + esc(waitWhy(where)) + '</span></p>';
|
|
1025
1082
|
// W13 (QA week of 11/01-11/08): the words Today's row showed for it (its `ask`) stay here, read-only, when no card
|
|
1026
1083
|
// draws them: the Mac line and a failed read alike. Never twice: not when the line above already says them.
|
|
1027
1084
|
var ask = waitAsk();
|
|
1028
1085
|
if (ask && (where === 'mac' || where === 'failed') && !O.macCount) html += '<blockquote class="ta-quote ta-mac-ask">' + esc(ask) + '</blockquote>';
|
|
1086
|
+
// W1 A5: no words anywhere (a question that went straight to the Mac): say that, rather than a card with nothing in it.
|
|
1087
|
+
if (!ask && where === 'mac' && !O.macCount && !askInWhy()) html += '<p class="ta-sub ta-mac-nowords">' + esc(macNoWords()) + '</p>';
|
|
1029
1088
|
// The Mac card says it itself when there is one; say it here only when there is none.
|
|
1030
1089
|
if (where === 'mac' && !O.macCount) html += '<p class="ta-sub ta-mac-line">' + esc(macLine()) + '</p>';
|
|
1031
1090
|
// W13: the questions could not be read, so whether one waits here is unknown. Say so, never "at the Mac", and read again.
|
|
@@ -1033,6 +1092,7 @@
|
|
|
1033
1092
|
' <button type="button" class="link-btn" data-ta="q-retry">Retry</button></p>';
|
|
1034
1093
|
if (where === 'answered') html += '<p class="ta-sub ta-mac-line">' + esc((W.sessionQuestions && W.sessionQuestions.answeredLine) || 'Answered. The session carries on.') + '</p>';
|
|
1035
1094
|
if (where === 'reply') html += '<p class="ta-sub">' + esc('Answer it in a reply below, or open the session.') + '</p>';
|
|
1095
|
+
if (where === 'turnend') html += turnEndHtml();
|
|
1036
1096
|
} else if (O.row || O.r) {
|
|
1037
1097
|
// Once the live feed speaks, the line says what it says, so the header and the peek never disagree (qa2-user N3).
|
|
1038
1098
|
html += '<p class="ta-sub">' + T.badge(O.r ? O.r.provider : O.row.provider) + ' <span id="ta-run-state">' + esc(runWords()) + '</span></p>';
|
|
@@ -1047,7 +1107,7 @@
|
|
|
1047
1107
|
box.innerHTML = html;
|
|
1048
1108
|
// The reply box shows only where a reply can be the answer; what was typed stays in it, hidden.
|
|
1049
1109
|
var foot = $('ta-foot');
|
|
1050
|
-
if (foot && O.open.kind === 'wait') { var w = waitWhere(); foot.hidden = w === 'failed' ? !!(O.r && O.r.provider === 'claude') : w !== 'reply'; }
|
|
1110
|
+
if (foot && O.open.kind === 'wait') { var w = waitWhere(); foot.hidden = w === 'failed' ? !!(O.r && O.r.provider === 'claude') : w !== 'reply' && w !== 'turnend'; }
|
|
1051
1111
|
}
|
|
1052
1112
|
// Where a waiting session's question is answered: 'here' (its cards in this sheet), 'answered' (on some screen just
|
|
1053
1113
|
// now), 'mac' (a Claude session's own prompt, or a held question that went back to the Mac), or 'reply' (a Codex or
|
|
@@ -1056,6 +1116,8 @@
|
|
|
1056
1116
|
// reply box stays a Codex or Cursor one.
|
|
1057
1117
|
function waitWhere() {
|
|
1058
1118
|
if (O.qCount) return 'here';
|
|
1119
|
+
// 7.0.18: a turn that ended asking him is answered by a reply (a choice or his own words), so the reply box shows.
|
|
1120
|
+
if (O.wait && O.wait.where === 'turnend') return 'turnend';
|
|
1059
1121
|
if (O.r && W.sessionQuestions && W.sessionQuestions.answeredFor && W.sessionQuestions.answeredFor(O.r.id)) return 'answered';
|
|
1060
1122
|
if (!O.macCount && W.sessionQuestions && W.sessionQuestions.loadState && W.sessionQuestions.loadState() === 'failed') return 'failed';
|
|
1061
1123
|
if (O.macCount || (O.r && O.r.provider === 'claude') || (O.wait && O.wait.where === 'mac')) return 'mac';
|
|
@@ -1066,7 +1128,57 @@
|
|
|
1066
1128
|
var a = O.wait && O.wait.ask ? String(O.wait.ask).trim() : '';
|
|
1067
1129
|
return a && !(O.wait.why && String(O.wait.why).trim() === a) ? a : '';
|
|
1068
1130
|
}
|
|
1069
|
-
|
|
1131
|
+
// 7.0.18: a turn that ended asking him (X1 contract v1). The question as the row has it, a button per choice, and the
|
|
1132
|
+
// required honest line. A tap sends the contract's exact text through the reader's Continue (K.send); his own words
|
|
1133
|
+
// in the box below are sent the same way.
|
|
1134
|
+
var TURN_END_LINE = 'A choice or a reply here reaches the session as a message from COS, not typed in the session, and the session may ask you to confirm.';
|
|
1135
|
+
var TURN_END_PREFIX = 'Answer sent from COS Control Web through COS, not typed in this session: ';
|
|
1136
|
+
function turnEndAnswer(text) { var t = String(text || '').trim(); return TURN_END_PREFIX + (t.indexOf(TURN_END_PREFIX) === 0 ? t.slice(TURN_END_PREFIX.length).trim() : t); }
|
|
1137
|
+
function turnEndHtml() {
|
|
1138
|
+
var w = O.wait || {}, choices = Array.isArray(w.choices) ? w.choices : [], busy = !!(O.r && O.r.sending);
|
|
1139
|
+
return (w.ask ? '<blockquote class="ta-quote ta-te-ask">' + esc(w.ask) + '</blockquote>' : '') +
|
|
1140
|
+
(choices.length ? '<div class="ta-te-choices">' + choices.map(function (c, i) {
|
|
1141
|
+
return '<button type="button" class="cx-btn' + (i === 0 ? ' primary' : '') + '" data-ta="turnend" data-id="' + i + '"' + (busy ? ' disabled' : '') + '>' + esc(c) + '</button>';
|
|
1142
|
+
}).join('') + '</div>' : '') +
|
|
1143
|
+
'<p class="ta-sub ta-te-line">' + esc(TURN_END_LINE) + '</p>';
|
|
1144
|
+
}
|
|
1145
|
+
/** Still waiting on this answer? The latest session list decides; a reply from anywhere clears it on the server. */
|
|
1146
|
+
function turnEndStill() {
|
|
1147
|
+
var nm = T.needs ? T.needs() : null, key = O.wait && O.wait.key;
|
|
1148
|
+
return !!(nm && key && nm.rows.some(function (x) { return x.kind === 'session' && x.key === key && x.where === 'turnend'; }));
|
|
1149
|
+
}
|
|
1150
|
+
function sendChoice(i) {
|
|
1151
|
+
var r = O.r, w = O.wait; if (!r || !w || r.sending) return;
|
|
1152
|
+
var label = (Array.isArray(w.choices) ? w.choices : [])[i];
|
|
1153
|
+
if (!label) return;
|
|
1154
|
+
if (!turnEndStill()) { O.note = { text: 'Already answered on another screen, or the session moved on. Nothing was sent.', kind: '' }; paintNote(); return; }
|
|
1155
|
+
r.sending = true; setSending(true); paintMeta();
|
|
1156
|
+
var done = function () { r.sending = false; if (O.r === r) { setSending(false); paintMeta(); } };
|
|
1157
|
+
return K.send(r, turnEndAnswer(label), {}).then(function (how) {
|
|
1158
|
+
done();
|
|
1159
|
+
if (O.r !== r) return;
|
|
1160
|
+
if (how === 'sent') O.note = { text: 'Sent \u201c' + label + '\u201d. It reaches the session as a message from COS; it may ask you to confirm.', kind: '' };
|
|
1161
|
+
else if (how === 'changed') { r.refusal = null; r.sendAnyway = null; paintVerdict(); O.note = { text: 'The session moved on before this reached it. \u201c' + label + '\u201d was not sent.', kind: 'error' }; }
|
|
1162
|
+
else if (how === 'refused') O.note = { text: 'The session did not take \u201c' + label + '\u201d. Try again, or answer it on your Mac.', kind: 'error' };
|
|
1163
|
+
paintNote();
|
|
1164
|
+
}, function (err) {
|
|
1165
|
+
done();
|
|
1166
|
+
if (O.r !== r) return;
|
|
1167
|
+
O.note = { text: 'Not sent: ' + (err && err.name === 'AbortError' ? 'no answer from your Mac yet. It may still land; tapping again is safe, it will not post twice.' : (err && err.message) || 'could not reach your Mac.'), kind: 'error' };
|
|
1168
|
+
paintNote();
|
|
1169
|
+
});
|
|
1170
|
+
}
|
|
1171
|
+
function macLine() { return (W.sessionQuestions && W.sessionQuestions.macLine) || "Answer this at your Mac's prompt. A message sent from here does not answer it; it waits until the question is answered at the Mac."; }
|
|
1172
|
+
function macNoWords() { return (W.sessionQuestions && W.sessionQuestions.macNoWords) || "COS cannot read this question's words from here. They show at the Mac's prompt."; }
|
|
1173
|
+
// W1 A5: the head line says the state once. At the Mac's prompt the line below says where; the head does not repeat it.
|
|
1174
|
+
var GENERIC_WAIT = /^Waiting for (you|your answer)(\. Paused until you answer\.)?$/;
|
|
1175
|
+
function waitWhy(where) {
|
|
1176
|
+
var why = O.wait ? String(O.wait.why || '') : '';
|
|
1177
|
+
if (!why) return 'Waiting for you';
|
|
1178
|
+
return where === 'mac' && GENERIC_WAIT.test(why) ? 'Waiting for your answer' : why;
|
|
1179
|
+
}
|
|
1180
|
+
// The head line already carries the question's words (the server named them in the waiting line).
|
|
1181
|
+
function askInWhy() { return !!(O.wait && O.wait.why && !GENERIC_WAIT.test(String(O.wait.why))); }
|
|
1070
1182
|
|
|
1071
1183
|
// ═════════════ The pet's sessions ═════════════
|
|
1072
1184
|
// The same sessions the pet's caption counts (app.js petPlay): waiting on you, running, then done today. Each row
|
|
@@ -1266,7 +1378,8 @@
|
|
|
1266
1378
|
var fb = sent.length ? filesBox(r) : null, kit = KIT();
|
|
1267
1379
|
if (fb && kit) fb.atts.filter(function (t) { return sent.indexOf(t.mediaId) >= 0; }).forEach(function (t) { kit.remove(fb, t.id); });
|
|
1268
1380
|
var box = r === O.r ? $('ta-input') : null;
|
|
1269
|
-
|
|
1381
|
+
// 7.0.18: his answer to a turn-end ask went with the attribution in front; the box holds his words without it.
|
|
1382
|
+
var left = box ? (box.value.trim() === String(text).trim() || (box.value.trim() && turnEndAnswer(box.value) === String(text).trim()) ? '' : box.value) : '';
|
|
1270
1383
|
if (box && !left) { box.value = ''; W.autoGrow(box); }
|
|
1271
1384
|
keepDraft(r, left, null);
|
|
1272
1385
|
},
|
|
@@ -1342,6 +1455,8 @@
|
|
|
1342
1455
|
if (r.refusal || O.cautionAsk) { r.refusal = null; O.cautionAsk = false; paintVerdict(); }
|
|
1343
1456
|
r.sending = true; setSending(true);
|
|
1344
1457
|
var done = function () { r.sending = false; if (O.r === r) setSending(false); };
|
|
1458
|
+
// 7.0.18: his own words in answer to a turn-end ask carry the same attribution as a choice ('Other' in the contract).
|
|
1459
|
+
if (O.open && O.open.kind === 'wait' && waitWhere() === 'turnend' && turnEndStill()) text = turnEndAnswer(text);
|
|
1345
1460
|
var sending = K.send(r, text, opts || {});
|
|
1346
1461
|
// continueSession picks the key at once (draftFor): keep it until the Mac takes the words.
|
|
1347
1462
|
if (r.draft) keepDraft(r, text, r.draft);
|
|
@@ -323,3 +323,6 @@
|
|
|
323
323
|
.wk-detail.split-side > .split-handle { display: flex; }
|
|
324
324
|
}
|
|
325
325
|
@media (min-width: 1300px) { .wk-detail.split-side { grid-template-columns: minmax(0, 1fr) 22px 452px; } }
|
|
326
|
+
|
|
327
|
+
/* W1 (0.5.386 parity): the empty card's one-tap New session button wraps on a folded phone instead of running off the card. */
|
|
328
|
+
.wk-start-new { white-space: normal; max-width: 100%; height: auto; min-height: 40px; text-align: center; line-height: 1.3; padding-top: 8px; padding-bottom: 8px; }
|