@gotcos/glasses-server 6.99.25 → 6.99.29
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +166 -0
- package/package.json +1 -1
- package/server/index.ts +46 -4
- package/server/lib/agent-session-output-images.ts +280 -0
- package/server/lib/agent-session-store.ts +3 -1
- package/server/lib/agent-session-turns.ts +18 -7
- package/server/lib/client-liveness.ts +38 -3
- package/server/lib/codex-subagent-activity.ts +36 -0
- package/server/lib/g2-turn-provenance.ts +679 -0
- package/server/lib/media-store.ts +41 -2
- package/server/lib/permission-broker.ts +94 -17
- package/server/lib/queue-turn-evidence.ts +19 -2
- package/server/lib/session-halt.ts +15 -1
- package/server/lib/session-stream-events.ts +4 -0
- package/server/lib/session-subagents.ts +155 -0
- package/server/lib/session-turn-attachments.ts +245 -0
- package/server/lib/thread-turn-queue-deliver.ts +17 -2
- package/server/lib/thread-turn-queue.ts +10 -0
- package/server/lib/today-brief.ts +731 -0
- package/server/lib/turn-file-store.ts +202 -0
- package/server/lib/turn-footer.ts +81 -0
- package/server/routes/agent-session-bindings.ts +164 -16
- package/server/routes/agent-sessions.ts +77 -8
- package/server/routes/client-instance.ts +12 -1
- package/server/routes/cursor-stop-followup.ts +30 -1
- package/server/routes/g2-turns.ts +46 -0
- package/server/routes/health.ts +9 -0
- package/server/routes/media.ts +32 -8
- package/server/routes/permission-broker.ts +47 -4
- package/server/routes/thread-turn-queue.ts +148 -8
- package/server/routes/today-brief.ts +148 -0
- package/server/web-control/app.css +2 -0
- package/server/web-control/app.js +738 -102
- package/server/web-control/index.html +5 -1
- package/server/web-control/panes/cos-chat.css +18 -0
- package/server/web-control/panes/cos-chat.js +239 -45
- package/server/web-control/panes/meetings.js +20 -9
- package/server/web-control/panes/questions.css +39 -1
- package/server/web-control/panes/questions.js +368 -51
- package/server/web-control/panes/reader-media.js +154 -0
- package/server/web-control/panes/sessions.css +2 -0
- package/server/web-control/panes/settings.js +34 -2
- package/server/web-control/panes/today-actions.css +62 -4
- package/server/web-control/panes/today-actions.js +384 -53
|
@@ -101,8 +101,10 @@
|
|
|
101
101
|
var ctrl = limit && window.AbortController ? new AbortController() : null;
|
|
102
102
|
var timer = ctrl ? setTimeout(function () { ctrl.abort(); }, limit) : null;
|
|
103
103
|
return fetch(path, { method: method, headers: headers, credentials: 'same-origin', body: body, signal: ctrl ? ctrl.signal : undefined }).then(function (res) {
|
|
104
|
-
|
|
104
|
+
// Blob bodies keep their deadline after headers: a stalled download must release its queue slot.
|
|
105
|
+
if (timer && o.responseType !== 'blob') clearTimeout(timer);
|
|
105
106
|
if (res.status === 401 && path.indexOf('/api/web-session/') !== 0) { showAuth(); var gone = new Error('Signed out. Connect this browser again.'); gone.status = 401; throw gone; }
|
|
107
|
+
if (o.responseType === 'blob' && res.ok) return res.blob();
|
|
106
108
|
return res.text().then(function (text) {
|
|
107
109
|
var data = {};
|
|
108
110
|
try { data = text ? JSON.parse(text) : {}; } catch (_e) { data = {}; }
|
|
@@ -118,6 +120,9 @@
|
|
|
118
120
|
}
|
|
119
121
|
return data;
|
|
120
122
|
});
|
|
123
|
+
}).then(function (value) {
|
|
124
|
+
if (timer) clearTimeout(timer);
|
|
125
|
+
return value;
|
|
121
126
|
}, function (err) {
|
|
122
127
|
if (timer) clearTimeout(timer);
|
|
123
128
|
if (err && err.name === 'AbortError' && ctrl && ctrl.signal.aborted) {
|
|
@@ -215,7 +220,7 @@
|
|
|
215
220
|
var railPane = r.pane === 'usage' ? 'sessions' : r.pane;
|
|
216
221
|
document.querySelectorAll('.rail-btn,.rail-settings').forEach(function (b) { if (b.getAttribute('data-pane') === railPane) b.setAttribute('aria-current', 'page'); else b.removeAttribute('aria-current'); });
|
|
217
222
|
if (state.stream && !(r.pane === 'sessions' && r.args.length === 2)) { state.stream.close(); state.stream = null; }
|
|
218
|
-
if (!(r.pane === 'sessions' && r.args.length === 2)) { clearInterval(state.turnPoll); if (state.reading) clearInterval(state.reading.heldTimer); state.reading = null; }
|
|
223
|
+
if (!(r.pane === 'sessions' && r.args.length === 2)) { readerImagesStop($('session-turns')); clearInterval(state.turnPoll); if (state.reading) clearInterval(state.reading.heldTimer); state.reading = null; }
|
|
219
224
|
if (r.pane !== 'meetings' && state.livePoll) { clearInterval(state.livePoll); state.livePoll = null; }
|
|
220
225
|
|
|
221
226
|
var reading = r.args.length > 0 && !isTabRoute(r.args);
|
|
@@ -292,6 +297,7 @@
|
|
|
292
297
|
|
|
293
298
|
// ── auth ────────────────────────────────────────────────
|
|
294
299
|
function showAuth(message) {
|
|
300
|
+
readerImagesStop($('session-turns')); readerImagesStop($('ta-last'));
|
|
295
301
|
var alreadyOpen = !$('auth-gate').classList.contains('hidden');
|
|
296
302
|
$('app-shell').classList.add('hidden');
|
|
297
303
|
$('auth-gate').classList.remove('hidden');
|
|
@@ -332,16 +338,35 @@
|
|
|
332
338
|
// stories all asked for it separately. Now there is one copy: a request younger than maxAgeMs is
|
|
333
339
|
// answered from it, a request already on the wire is shared, and panes can paint the last copy at
|
|
334
340
|
// once while a fresh one loads (sessionListCached).
|
|
335
|
-
var SL = { data: null, at: 0, inflight: null };
|
|
341
|
+
var SL = { data: null, at: 0, inflight: null, waiting: null };
|
|
336
342
|
function sessionList(maxAgeMs) {
|
|
337
343
|
if (SL.data && Date.now() - SL.at < (maxAgeMs || 0)) return Promise.resolve(SL.data);
|
|
338
344
|
if (SL.inflight) return SL.inflight;
|
|
339
345
|
SL.inflight = api('/api/agent-sessions?limit=80&sort=updated').then(function (d) {
|
|
340
|
-
SL.data = d; SL.at = Date.now(); SL.inflight = null;
|
|
346
|
+
SL.data = d; SL.at = Date.now(); SL.inflight = null;
|
|
347
|
+
noteWaiting(d);
|
|
348
|
+
return d;
|
|
341
349
|
}, function (err) { SL.inflight = null; throw err; });
|
|
342
350
|
return SL.inflight;
|
|
343
351
|
}
|
|
344
352
|
function sessionListCached() { return SL.data ? { data: SL.data, at: SL.at } : null; }
|
|
353
|
+
// 6.99.27 (Miles 11/07 09:29, "a big delay to see the questions"): a session that has just gone to waiting may have
|
|
354
|
+
// parked a question for this page. The question poll (panes/questions.js) reads at once rather than at its next turn.
|
|
355
|
+
function questionsNudge() {
|
|
356
|
+
var q = window.COSWeb && window.COSWeb.sessionQuestions;
|
|
357
|
+
if (q && typeof q.nudge === 'function') { try { q.nudge(); } catch (_e) {} }
|
|
358
|
+
}
|
|
359
|
+
function noteWaiting(d) {
|
|
360
|
+
var now = {}, fresh = false;
|
|
361
|
+
((d && d.sessions) || []).forEach(function (s) {
|
|
362
|
+
if (!s || sxState(s) !== 'waiting') return;
|
|
363
|
+
var k = splitSessionId(s.provider, s.session_id || s.id), key = k.provider + '|' + String(k.id).toLowerCase();
|
|
364
|
+
now[key] = 1;
|
|
365
|
+
if (!SL.waiting || !SL.waiting[key]) fresh = true;
|
|
366
|
+
});
|
|
367
|
+
SL.waiting = now;
|
|
368
|
+
if (fresh) questionsNudge();
|
|
369
|
+
}
|
|
345
370
|
|
|
346
371
|
// ── the session pet (Today) ─────────────────────────────
|
|
347
372
|
// Same pet and the same pose rule as COS Control (PetSpritePose.resolve): waiting with nothing running
|
|
@@ -367,27 +392,99 @@
|
|
|
367
392
|
return merged;
|
|
368
393
|
}
|
|
369
394
|
var PET_HEIGHT = 140;
|
|
370
|
-
|
|
371
|
-
|
|
395
|
+
// 6.99.27 (Miles 11/07 09:29, "there wasn't an option for the pet to interact with the questions"): the questions
|
|
396
|
+
// this page holds (panes/questions.js), the ones that can be answered here. One key per session, as Needs you keys
|
|
397
|
+
// them.
|
|
398
|
+
function petQuestions() {
|
|
399
|
+
var q = window.COSWeb && window.COSWeb.sessionQuestions;
|
|
400
|
+
var items = q && typeof q.itemsFor === 'function' ? q.itemsFor('') : [];
|
|
401
|
+
return (items || []).filter(function (it) { return it && it.sessionId; });
|
|
402
|
+
}
|
|
403
|
+
function petQKey(it) { var k = splitSessionId(it.provider, it.sessionId); return k.provider + '|' + k.id; }
|
|
404
|
+
function petPose(sessions, questions) {
|
|
405
|
+
var working = 0, waiting = 0, alive = 0, asked = {};
|
|
406
|
+
var qs = questions || [];
|
|
407
|
+
qs.forEach(function (it) { asked[petQKey(it).toLowerCase()] = 1; });
|
|
372
408
|
sessions.forEach(function (s) {
|
|
373
409
|
var st = sxState(s);
|
|
374
410
|
// Working is the Running card's own test (running_active), so the caption under the pet never
|
|
375
411
|
// disagrees with the Running count beside it (11/06: the pet said 3 while the card said 2).
|
|
376
412
|
if (s.running_active) working++;
|
|
377
|
-
|
|
413
|
+
// A session whose question the caption already leads with is not counted again as waiting on you.
|
|
414
|
+
else if (st === 'waiting' && !asked[petKey(s).toLowerCase()]) waiting++;
|
|
378
415
|
if (s.running || s.running_active) alive++;
|
|
379
416
|
});
|
|
380
417
|
var pose;
|
|
381
|
-
|
|
418
|
+
// COS Control's rule (PetSpritePose.resolve): attention outranks every count pose. A question waiting here is the
|
|
419
|
+
// web's attention: it needs you, and this page can answer it.
|
|
420
|
+
if (qs.length) pose = 'attention';
|
|
421
|
+
else if (waiting > 0 && working === 0) pose = 'waiting';
|
|
382
422
|
else if (working + waiting >= 4) pose = 'swarm';
|
|
383
423
|
else if (working + waiting === 3) pose = 'trio';
|
|
384
424
|
else if (working + waiting === 2) pose = 'duel';
|
|
385
425
|
else if (working + waiting === 1) pose = 'working';
|
|
386
426
|
else pose = alive > 0 ? 'patrol' : 'idle';
|
|
387
|
-
|
|
427
|
+
// QA 6.99.27 (User 6): a permission request is an approval, counted apart from the questions.
|
|
428
|
+
var approvals = qs.filter(function (it) { return it.kind === 'approval'; }).length;
|
|
429
|
+
return { pose: pose, working: working, waiting: waiting, questions: qs.length - approvals, approvals: approvals };
|
|
430
|
+
}
|
|
431
|
+
// COS Control's fallbacks (PetSpritePose.fallbackPoses) for a pet that has no strip for the pose.
|
|
432
|
+
var PET_FALLBACK = { attention: ['error', 'waiting', 'idle'], patrol: ['idle'], duel: ['swarm', 'working', 'idle'], trio: ['swarm', 'duel', 'working', 'idle'],
|
|
433
|
+
swarm: ['duel', 'trio', 'working', 'idle'], working: ['idle'], waiting: ['idle'] };
|
|
434
|
+
function petPoseIn(poses, want) {
|
|
435
|
+
if (poses[want]) return want;
|
|
436
|
+
var chain = PET_FALLBACK[want] || [];
|
|
437
|
+
for (var i = 0; i < chain.length; i++) if (poses[chain[i]]) return chain[i];
|
|
438
|
+
return poses.idle ? 'idle' : Object.keys(poses)[0];
|
|
439
|
+
}
|
|
440
|
+
// 6.99.26 (Miles 11/07, "Open what matters"): the sessions behind the pet's caption, by the same test petPose
|
|
441
|
+
// counts with, so a tap opens what the caption says. In play: waiting on you, then running. Done today rides along in
|
|
442
|
+
// the list, never in the count.
|
|
443
|
+
function petPlay(sessions) {
|
|
444
|
+
var waiting = [], running = [], done = [], today = new Date().toDateString();
|
|
445
|
+
(sessions || []).forEach(function (s) {
|
|
446
|
+
var st = sxState(s);
|
|
447
|
+
if (s.running_active) running.push(s);
|
|
448
|
+
else if (st === 'waiting') waiting.push(s);
|
|
449
|
+
else if ((st === 'recent' || st === 'stale') && !sxKeepWarm(s)) {
|
|
450
|
+
var d = toDate(s.last_activity_at || s.state_since);
|
|
451
|
+
if (d && d.toDateString() === today) done.push(s);
|
|
452
|
+
}
|
|
453
|
+
});
|
|
454
|
+
return { waiting: waiting, running: running, done: done };
|
|
455
|
+
}
|
|
456
|
+
function petKey(s) { var k = splitSessionId(s.provider, s.session_id || s.id); return k.provider + '|' + k.id; }
|
|
457
|
+
// One session in play opens its own sheet (waiting: the question card or reply box; else the Running peek); several
|
|
458
|
+
// open the short list; none does nothing.
|
|
459
|
+
function petTap(t) {
|
|
460
|
+
// A question waiting here comes first: one session's question opens its card in the overlay; questions from
|
|
461
|
+
// several sessions open the list, question rows first.
|
|
462
|
+
var qs = petQuestions(), qkeys = {};
|
|
463
|
+
qs.forEach(function (it) { qkeys[petQKey(it)] = 1; });
|
|
464
|
+
var qk = Object.keys(qkeys);
|
|
465
|
+
if (qk.length === 1) return openTodayAction(t, 'wait', qk[0]);
|
|
466
|
+
if (qk.length > 1) return openTodayAction(t, 'pet', '');
|
|
467
|
+
var p = petPlay(state.sessions || []), n = p.waiting.length + p.running.length;
|
|
468
|
+
if (!n) return;
|
|
469
|
+
if (n > 1) return openTodayAction(t, 'pet', '');
|
|
470
|
+
var s = p.waiting[0] || p.running[0];
|
|
471
|
+
openTodayAction(t, sxState(s) === 'waiting' ? 'wait' : 'run', petKey(s));
|
|
472
|
+
}
|
|
473
|
+
function petButtonState(box, c, label) {
|
|
474
|
+
var btn = box.querySelector('.pet-btn');
|
|
475
|
+
if (!btn) return;
|
|
476
|
+
var any = c.working + c.waiting + (c.questions || 0) + (c.approvals || 0) > 0;
|
|
477
|
+
btn.setAttribute('aria-label', any ? label + '. Open' : label);
|
|
478
|
+
btn.setAttribute('aria-disabled', any ? 'false' : 'true');
|
|
388
479
|
}
|
|
389
480
|
function petCaption(c) {
|
|
390
481
|
var parts = [];
|
|
482
|
+
// A question or an approval waiting here leads (6.99.27): "1 question for you", "1 approval for you", or
|
|
483
|
+
// "1 question and 1 approval for you" (QA 6.99.27, User 6: approvals were counted as questions).
|
|
484
|
+
var asks = [];
|
|
485
|
+
if (c.questions) asks.push(c.questions === 1 ? '1 question' : c.questions + ' questions');
|
|
486
|
+
if (c.approvals) asks.push(c.approvals === 1 ? '1 approval' : c.approvals + ' approvals');
|
|
487
|
+
if (asks.length) parts.push(asks.join(' and ') + ' for you');
|
|
391
488
|
if (c.working) parts.push(c.working + ' running');
|
|
392
489
|
if (c.waiting) parts.push(c.waiting + ' waiting on you');
|
|
393
490
|
return parts.length ? parts.join(', ') : 'Nothing running';
|
|
@@ -418,17 +515,17 @@
|
|
|
418
515
|
var PET_CHANGE = '<button type="button" class="pet-change" data-goto="settings/pet">Change pet</button>';
|
|
419
516
|
// A still pet (a gallery pick, Cloud Puff, or a single image on the Mac): one picture for every state.
|
|
420
517
|
function renderPetStill(box, m, sessions) {
|
|
421
|
-
var c = petPose(sessions), label = petCaption(c);
|
|
518
|
+
var c = petPose(sessions, petQuestions()), label = petCaption(c);
|
|
422
519
|
petStop(); PET.pose = 'still:' + m.still;
|
|
423
520
|
var img = box.querySelector('.pet-still');
|
|
424
521
|
if (!img || img.getAttribute('src') !== m.still) {
|
|
425
|
-
box.innerHTML = '<button type="button" class="pet-btn" data-
|
|
522
|
+
box.innerHTML = '<button type="button" class="pet-btn" data-today="pet"><img class="pet-still" alt="" src="' + esc(m.still) + '"><span class="pet-caption"></span></button>' + PET_CHANGE;
|
|
426
523
|
img = box.querySelector('.pet-still');
|
|
427
524
|
img.addEventListener('error', function () { box.classList.add('hidden'); });
|
|
428
525
|
img.addEventListener('load', function () { img.classList.toggle('pixel', img.naturalHeight > 0 && img.naturalHeight < 140); });
|
|
429
526
|
}
|
|
430
527
|
box.querySelector('.pet-caption').textContent = label;
|
|
431
|
-
box
|
|
528
|
+
petButtonState(box, c, label);
|
|
432
529
|
box.classList.remove('hidden');
|
|
433
530
|
}
|
|
434
531
|
function renderPet(sessions) {
|
|
@@ -448,15 +545,14 @@
|
|
|
448
545
|
if (!now.show || now.pet !== prefs.pet) return;
|
|
449
546
|
if (!m || !m.available) { box.classList.add('hidden'); petStop(); return; }
|
|
450
547
|
if (m.kind === 'still') { renderPetStill(box, m, sessions); return; }
|
|
451
|
-
var c = petPose(sessions);
|
|
452
|
-
var pose = m.poses
|
|
548
|
+
var c = petPose(sessions, petQuestions());
|
|
549
|
+
var pose = petPoseIn(m.poses, c.pose);
|
|
453
550
|
var spec = m.poses[pose];
|
|
454
551
|
var label = petCaption(c);
|
|
455
552
|
box.classList.remove('hidden');
|
|
456
553
|
var cap = box.querySelector('.pet-caption');
|
|
457
554
|
if (cap) cap.textContent = label;
|
|
458
|
-
|
|
459
|
-
if (btn) btn.setAttribute('aria-label', 'Sessions: ' + label);
|
|
555
|
+
petButtonState(box, c, label);
|
|
460
556
|
if (PET.pose === pose && PET.timer) return;
|
|
461
557
|
var token = ++PET.token;
|
|
462
558
|
petLoadImages(spec).then(function () {
|
|
@@ -464,10 +560,10 @@
|
|
|
464
560
|
petStop();
|
|
465
561
|
PET.pose = pose; PET.frame = 0;
|
|
466
562
|
if (!box.querySelector('.pet-sprite')) {
|
|
467
|
-
box.innerHTML = '<button type="button" class="pet-btn" data-
|
|
563
|
+
box.innerHTML = '<button type="button" class="pet-btn" data-today="pet"><span class="pet-sprite" aria-hidden="true"></span><span class="pet-caption"></span></button>' + PET_CHANGE;
|
|
468
564
|
}
|
|
469
565
|
box.querySelector('.pet-caption').textContent = label;
|
|
470
|
-
box
|
|
566
|
+
petButtonState(box, c, label);
|
|
471
567
|
var el = box.querySelector('.pet-sprite'), scale = PET_HEIGHT / spec.height;
|
|
472
568
|
el.style.width = Math.round(spec.frameWidth * scale) + 'px';
|
|
473
569
|
petPaint(el, spec, 0, scale);
|
|
@@ -563,11 +659,94 @@
|
|
|
563
659
|
'<span class="rec-copy"><strong>Recording now</strong><span>' + esc(rec.source + (rec.startTime ? ', ' + span(rec.startTime) + ' so far' : '')) + '</span></span>' +
|
|
564
660
|
'<span class="btn small">Live view</span></button>';
|
|
565
661
|
}
|
|
662
|
+
// ── The Today summary (6.99.26 candidate) ───────────────
|
|
663
|
+
// "The orchestrator's eyes" (Miles, 11/07): two or three sentences on what the sessions add up to, written by Haiku on
|
|
664
|
+
// the server (lib/today-brief.ts). Today sends what it already read: session titles, states, how long, each session's
|
|
665
|
+
// latest line and what needs you. The server decides whether a new one is written (off unless switched on, only when
|
|
666
|
+
// the inputs changed, 10 minutes apart, 60 a day, a breaker). Today never waits for it: the counts line paints at once
|
|
667
|
+
// and the summary replaces it when one arrives. Never shown over a session list that was not read.
|
|
668
|
+
var BRIEF = { view: null, inflight: false, sentKey: '', sentAt: 0 };
|
|
669
|
+
// 6.99.26 QA: the same rows are asked about again only after five minutes (a Today that sits open still learns what
|
|
670
|
+
// the server reads itself: sub-agents and Work cards); changed rows are asked about at once, one ask at a time.
|
|
671
|
+
var BRIEF_RESEND_MS = 5 * 60000, BRIEF_OFF_RECHECK_MS = 5 * 60000, BRIEF_TICK_MS = 60000;
|
|
672
|
+
function briefMinutes(v) { var d = toDate(v); return d ? Math.max(0, Math.round((Date.now() - d.getTime()) / 60000)) : 0; }
|
|
673
|
+
function briefInputs(m) {
|
|
674
|
+
var sessions = [];
|
|
675
|
+
(state.sessions || []).forEach(function (s) {
|
|
676
|
+
if (sxKeepWarm(s)) return;
|
|
677
|
+
var kind = sxState(s) === 'waiting' ? 'waiting' : s.running_active ? 'working' : s.running ? 'open' : '';
|
|
678
|
+
if (!kind) return; // only what Today counts: waiting, working and open
|
|
679
|
+
var k = splitSessionId(s.provider, s.session_id || s.id);
|
|
680
|
+
sessions.push({ title: sessionTitle(s), provider: k.provider, state: kind,
|
|
681
|
+
minutes: briefMinutes(kind === 'open' ? (s.last_activity_at || s.state_since) : (s.state_since || s.last_activity_at)),
|
|
682
|
+
line: String(s.discussion_summary || '').slice(0, 400), waiting: kind === 'waiting' ? askedWhy(s) : '',
|
|
683
|
+
// Only so the server can read this session's sub-agents (names, tasks, status); it never reaches the model.
|
|
684
|
+
key: k.provider + '|' + k.id,
|
|
685
|
+
agents: s.subagent_activity ? { active: s.subagent_activity.active, completed: s.subagent_activity.completed, total: s.subagent_activity.total } : undefined });
|
|
686
|
+
});
|
|
687
|
+
var needs = m.rows.filter(function (r) { return r.kind !== 'session'; }).slice(0, 12).map(function (r) {
|
|
688
|
+
if (r.kind === 'work') return { title: r.item.sessionTitle || r.item.title || 'Handoff', why: needWhy(r.item) };
|
|
689
|
+
return { title: r.title, why: r.why || '' };
|
|
690
|
+
});
|
|
691
|
+
// Today folds all but waiting sessions under "can wait until tomorrow" in quiet hours; the summary says the same.
|
|
692
|
+
var later = m.later.filter(function (r) { return r.kind !== 'session'; }).length;
|
|
693
|
+
return { sessions: sessions.slice(0, 40), needs: needs, later: Math.min(later, needs.length), sessionsRead: TODAY.sessionsState === 'answered', needsComplete: !!m.settled,
|
|
694
|
+
unread: m.failed.slice(), recording: !!TODAY.recording };
|
|
695
|
+
}
|
|
696
|
+
// Asked once every source answered or failed: at once when what Today read changed, else at most every five minutes
|
|
697
|
+
// (the server gates the spend; this only saves requests). When the switch is off, every five minutes.
|
|
698
|
+
function requestBrief(m) {
|
|
699
|
+
if (BRIEF.inflight || state.pane !== 'today') return; // renderStory already answered an offline Today
|
|
700
|
+
if (Object.keys(m.states).some(function (k) { return m.states[k] === 'pending'; })) return;
|
|
701
|
+
var body = briefInputs(m);
|
|
702
|
+
var key = JSON.stringify([body.sessions.map(function (s) { return [s.title, s.state, s.line, s.waiting, s.agents]; }), body.needs, body.later, body.sessionsRead, body.needsComplete, body.unread, body.recording]);
|
|
703
|
+
var since = Date.now() - BRIEF.sentAt;
|
|
704
|
+
if (BRIEF.view && BRIEF.view.state === 'off' ? since < BRIEF_OFF_RECHECK_MS : key === BRIEF.sentKey && since < BRIEF_RESEND_MS) return;
|
|
705
|
+
BRIEF.inflight = true; BRIEF.sentKey = key; BRIEF.sentAt = Date.now();
|
|
706
|
+
api('/api/today/brief', { method: 'POST', body: body, timeoutMs: 45000 }).then(function (v) {
|
|
707
|
+
BRIEF.view = v && typeof v === 'object' ? v : null;
|
|
708
|
+
}, function () { /* keep what was shown: the counts line or the last summary */ }).then(function () {
|
|
709
|
+
BRIEF.inflight = false;
|
|
710
|
+
if (TODAY.model && state.pane === 'today') renderStory(TODAY.model);
|
|
711
|
+
});
|
|
712
|
+
}
|
|
713
|
+
/** The summary to show, or null: only with text, only over a session list that was read. */
|
|
714
|
+
function briefShown() {
|
|
715
|
+
var v = BRIEF.view;
|
|
716
|
+
if (!v || !v.text || (v.state !== 'fresh' && v.state !== 'stale') || TODAY.sessionsState !== 'answered') return null;
|
|
717
|
+
return v;
|
|
718
|
+
}
|
|
719
|
+
// Forget the summary (Settings turned it on or off): the next Today paint asks at once.
|
|
720
|
+
function resetBrief() { BRIEF.view = null; BRIEF.sentKey = ''; BRIEF.sentAt = 0; }
|
|
721
|
+
// Every minute while Today is on screen: "Updated N min ago" moves on, and an unchanged Today asks again once its
|
|
722
|
+
// five minutes are up (requestBrief's own throttle). Hidden or another pane: nothing.
|
|
723
|
+
setInterval(function () {
|
|
724
|
+
if (document.hidden || state.pane !== 'today' || !TODAY.model || TODAY.offline) return;
|
|
725
|
+
requestBrief(TODAY.model);
|
|
726
|
+
var v = briefShown(), when = document.querySelector('#today-story .story-when');
|
|
727
|
+
if (v && when) when.textContent = briefWhen(v, TODAY.model);
|
|
728
|
+
}, BRIEF_TICK_MS);
|
|
729
|
+
function briefWhen(v, m) {
|
|
730
|
+
var mins = briefMinutes(v.at);
|
|
731
|
+
var out = 'Updated ' + (mins < 1 ? 'just now' : span(v.at) + ' ago');
|
|
732
|
+
// Stale says so once it is five minutes old: a summary from a minute ago over one new reply is still the picture.
|
|
733
|
+
if (v.state === 'stale' && mins >= 5) out += '. Things have moved since';
|
|
734
|
+
if (m.failed.length) out += '. Couldn’t read ' + listWords(m.failed) + ', so this may not be everything';
|
|
735
|
+
return out + '.';
|
|
736
|
+
}
|
|
737
|
+
|
|
566
738
|
// The headline: COS Control's brief (Models.swift TodayHome.brief) over the same rows Needs you lists.
|
|
567
739
|
function renderStory(m) {
|
|
568
740
|
var el = $('today-story');
|
|
569
741
|
el.className = 'story';
|
|
570
742
|
if (TODAY.offline) { el.textContent = 'COS on ' + state.serverName + ' is not answering right now. Today fills in when it does.'; return; }
|
|
743
|
+
// The model's summary when there is one (it never blocks: the counts line below shows until it lands). The test
|
|
744
|
+
// harnesses that run this block alone have no requestBrief, so it is looked up, never assumed.
|
|
745
|
+
if (typeof requestBrief === 'function') {
|
|
746
|
+
requestBrief(m);
|
|
747
|
+
var brief = briefShown();
|
|
748
|
+
if (brief) { el.innerHTML = esc(brief.text) + '<span class="story-when">' + esc(briefWhen(brief, m)) + '</span>'; return; }
|
|
749
|
+
}
|
|
571
750
|
var first = '';
|
|
572
751
|
if (m.sessionsWaiting > 0) first = countWord(m.sessionsWaiting) + ' ' + plural(m.sessionsWaiting, 'session is', 'sessions are') + ' waiting on you';
|
|
573
752
|
else if (m.now.length > 0) first = countWord(m.now.length) + ' ' + plural(m.now.length, 'thing needs', 'things need') + ' you' + (m.settled ? '' : ' so far');
|
|
@@ -658,6 +837,29 @@
|
|
|
658
837
|
if (s.waiting_kind === 'question' && d) return d.length > 160 ? d.slice(0, 159) + '…' : d;
|
|
659
838
|
return 'Waiting for you' + (sxProvider(s.provider) === 'claude' ? '. Paused until you answer.' : '');
|
|
660
839
|
}
|
|
840
|
+
// 6.99.26: where a waiting session's question can be answered. 'here': this page holds it (panes/questions.js), as
|
|
841
|
+
// cards in its waiting sheet. 'mac': a Claude session's own prompt (or a held one that went back to the Mac), which
|
|
842
|
+
// only the Mac can answer, in person or through Screens. 'answered': answered on some screen in the last two minutes,
|
|
843
|
+
// the list not caught up yet. '': a reply answers it (a Codex or Cursor session).
|
|
844
|
+
function sessionQs() { var q = window.COSWeb && window.COSWeb.sessionQuestions; return q && q.itemsFor ? q : null; }
|
|
845
|
+
function askedHere(items) {
|
|
846
|
+
var it = items[0];
|
|
847
|
+
if (!it) return '';
|
|
848
|
+
if (it.kind === 'approval') return it.approval && it.approval.summary ? 'Wants to run ' + clipWords(it.approval.summary, 80) : '';
|
|
849
|
+
var q = (it.questions || [])[0];
|
|
850
|
+
return q && q.question ? clipWords(q.question, 160) : '';
|
|
851
|
+
}
|
|
852
|
+
function clipWords(text, n) { var t = String(text || '').replace(/\s+/g, ' ').trim(); return t.length > n ? t.slice(0, n - 1) + '…' : t; }
|
|
853
|
+
function whereFor(provider, id) {
|
|
854
|
+
var q = sessionQs();
|
|
855
|
+
var held = q ? q.itemsFor(id) : [], mac = q && q.atMacFor ? q.atMacFor(id) : [];
|
|
856
|
+
if (held.length) return { where: 'here', ask: askedHere(held) };
|
|
857
|
+
if (q && q.answeredFor && q.answeredFor(id)) return { where: 'answered', ask: '' };
|
|
858
|
+
var words = mac.length && q.wordsOf ? q.wordsOf(mac[0].id) : null;
|
|
859
|
+
var ask = words ? clipWords(words.questions[0] || (words.summary ? 'Wants to run ' + words.summary : ''), 160) : '';
|
|
860
|
+
if (mac.length || sxProvider(provider) === 'claude') return { where: 'mac', ask: ask };
|
|
861
|
+
return { where: '', ask: '' };
|
|
862
|
+
}
|
|
661
863
|
function needsModel() {
|
|
662
864
|
var states = { sessions: TODAY.sessionsState, work: TODAY.workState, voices: TODAY.voices.state, memories: TODAY.memories.state };
|
|
663
865
|
var rows = [], seen = {};
|
|
@@ -665,7 +867,19 @@
|
|
|
665
867
|
if (sxKeepWarm(s) || sxState(s) !== 'waiting') return;
|
|
666
868
|
var k = splitSessionId(s.provider, s.session_id || s.id);
|
|
667
869
|
seen[k.provider + '|' + String(k.id).toLowerCase()] = 1;
|
|
668
|
-
|
|
870
|
+
var w = whereFor(k.provider, k.id);
|
|
871
|
+
rows.push({ kind: 'session', tier: 1, title: askedTitle(s), why: askedWhy(s), since: s.state_since || s.last_activity_at, key: k.provider + '|' + k.id, where: w.where, ask: w.ask });
|
|
872
|
+
});
|
|
873
|
+
// A question this page holds for a session the list has not shown waiting yet: its own row, so every held
|
|
874
|
+
// question is in front of you (two at once each get one).
|
|
875
|
+
var qs = sessionQs();
|
|
876
|
+
(qs ? qs.itemsFor('') : []).forEach(function (it) {
|
|
877
|
+
var k = splitSessionId(it.provider, it.sessionId), sk = k.provider + '|' + String(k.id).toLowerCase();
|
|
878
|
+
if (!k.id || seen[sk]) return;
|
|
879
|
+
seen[sk] = 1;
|
|
880
|
+
var s = (state.sessions || []).filter(function (x) { var xk = splitSessionId(x.provider, x.session_id || x.id); return xk.provider === k.provider && String(xk.id).toLowerCase() === String(k.id).toLowerCase(); })[0];
|
|
881
|
+
var title = s ? askedTitle(s) : it.kind === 'approval' ? 'A session wants your permission' : 'A session asked you a question';
|
|
882
|
+
rows.push({ kind: 'session', tier: 1, title: title, why: 'Waiting for you. Paused until you answer.', since: it.createdAt, key: k.provider + '|' + k.id, where: 'here', ask: askedHere([it]) });
|
|
669
883
|
});
|
|
670
884
|
(TODAY.needs || []).forEach(function (it) {
|
|
671
885
|
var k = splitSessionId(it.provider, it.sessionId);
|
|
@@ -721,9 +935,11 @@
|
|
|
721
935
|
(k.id ? '<div class="need-actions"><button type="button" class="btn small' + (i === 0 ? ' primary' : '') + '" data-session="' + esc(k.provider) + '|' + esc(k.id) + '">Open session</button></div>' : '') + '</div>';
|
|
722
936
|
}
|
|
723
937
|
if (r.kind === 'session') {
|
|
724
|
-
// The session's question shows in the overlay's peek, with its cards to answer.
|
|
938
|
+
// The session's question shows in the overlay's peek, with its cards to answer. Under it, where it can be answered.
|
|
939
|
+
var q = sessionQs(), whereLine = r.where === 'here' ? 'Answer it here.' : r.where === 'mac' ? (q && q.macLine) || '' : r.where === 'answered' ? (q && q.answeredLine) || '' : '';
|
|
725
940
|
return '<div class="need-row">' +
|
|
726
|
-
'<button type="button" class="need-open" data-today="wait" data-key="' + esc(r.key) + '">' + top(r.title) + '<span class="need-foot">' + esc(r.why) + '</span
|
|
941
|
+
'<button type="button" class="need-open" data-today="wait" data-key="' + esc(r.key) + '">' + top(r.title) + '<span class="need-foot">' + esc(r.ask || r.why) + '</span>' +
|
|
942
|
+
(whereLine ? '<span class="need-foot need-where" data-where="' + esc(r.where) + '">' + esc(whereLine) + '</span>' : '') + '</button>' +
|
|
727
943
|
'<div class="need-actions"><button type="button" class="btn small' + (i === 0 ? ' primary' : '') + '" data-session="' + esc(r.key) + '">Open session</button></div></div>';
|
|
728
944
|
}
|
|
729
945
|
var go2 = r.kind === 'voices' ? 'speakers' : 'memories/view/review', label = r.kind === 'voices' ? 'Name' : 'Review';
|
|
@@ -800,8 +1016,8 @@
|
|
|
800
1016
|
}).join('') + '<p class="intake-foot">Moves to Work at midnight.</p>';
|
|
801
1017
|
card.classList.remove('hidden');
|
|
802
1018
|
}
|
|
803
|
-
function openTodayAction(t) {
|
|
804
|
-
var kind = t.getAttribute('data-today'), key = t.getAttribute('data-key') || '';
|
|
1019
|
+
function openTodayAction(t, kindIn, keyIn) {
|
|
1020
|
+
var kind = kindIn || t.getAttribute('data-today'), key = keyIn != null ? keyIn : t.getAttribute('data-key') || '';
|
|
805
1021
|
ensurePane('todayActions', 4000).then(function (m) {
|
|
806
1022
|
if (!m || !m.open) throw new Error('The Today overlay did not start.');
|
|
807
1023
|
m.open(kind, key, t);
|
|
@@ -811,9 +1027,9 @@
|
|
|
811
1027
|
// history entry, which the fallback replaces, so Back from there still lands on Today.
|
|
812
1028
|
var s = String(kind === 'run' || kind === 'wait' ? key : t.getAttribute('data-session-key') || '').split('|');
|
|
813
1029
|
var session = (kind === 'run' || kind === 'wait' || kind === 'need') && !!s[1];
|
|
814
|
-
var dest = session ? ['sessions', s[0], s.slice(1).join('|')] : ['work'].concat(kind === 'need' ? [] : ['sort']);
|
|
1030
|
+
var dest = session ? ['sessions', s[0], s.slice(1).join('|')] : kind === 'pet' ? ['sessions'] : ['work'].concat(kind === 'need' ? [] : ['sort']);
|
|
815
1031
|
console.error('[today] the overlay did not open (' + kind + '); opening ' + dest.join('/') + ' instead.', err);
|
|
816
|
-
toast(session ? 'Could not open that here. Opening the session.' : 'Could not open that here. Opening it in Work.');
|
|
1032
|
+
toast(session ? 'Could not open that here. Opening the session.' : kind === 'pet' ? 'Could not open that here. Opening Sessions.' : 'Could not open that here. Opening it in Work.');
|
|
817
1033
|
if (history.state && history.state.cosTodayOverlay) location.replace('#/' + dest.map(encodeURIComponent).join('/'));
|
|
818
1034
|
else go(dest[0], dest.slice(1));
|
|
819
1035
|
});
|
|
@@ -1358,7 +1574,11 @@
|
|
|
1358
1574
|
function writeStore(key, v) { try { localStorage.setItem(key, JSON.stringify(v)); } catch (_e) {} }
|
|
1359
1575
|
function saveHeld(r) {
|
|
1360
1576
|
var all = readStore(HELD_KEY), k = r.provider + '|' + r.id;
|
|
1361
|
-
var keep = r.held.filter(function (h) { return !h.cancelled; }).map(function (h) {
|
|
1577
|
+
var keep = r.held.filter(function (h) { return !h.cancelled; }).map(function (h) {
|
|
1578
|
+
var o = { text: h.text, key: h.key, since: h.since };
|
|
1579
|
+
if (h.ids && h.ids.length) { o.ids = h.ids; if (h.files && h.files.length) o.files = h.files; }
|
|
1580
|
+
return o;
|
|
1581
|
+
});
|
|
1362
1582
|
if (keep.length) all[k] = keep; else delete all[k];
|
|
1363
1583
|
writeStore(HELD_KEY, all);
|
|
1364
1584
|
}
|
|
@@ -1393,6 +1613,7 @@
|
|
|
1393
1613
|
function openSession(provider, id) {
|
|
1394
1614
|
var reader = $('session-reader');
|
|
1395
1615
|
if (!provider || !id) return;
|
|
1616
|
+
readerImagesStop($('session-turns'));
|
|
1396
1617
|
if (state.stream) { state.stream.close(); state.stream = null; }
|
|
1397
1618
|
clearInterval(state.turnPoll);
|
|
1398
1619
|
if (state.reading) { clearInterval(state.reading.heldTimer); clearInterval(state.reading.feedTimer); }
|
|
@@ -1422,15 +1643,21 @@
|
|
|
1422
1643
|
'<p class="sx-copy-note hidden" id="session-copy-note" role="status"></p>' +
|
|
1423
1644
|
'<div class="sx-chat">' +
|
|
1424
1645
|
'<div id="session-verdict"></div>' +
|
|
1425
|
-
'<form class="composer" id="session-form" data-provider="' + esc(provider) + '" data-id="' + esc(id) + '">' +
|
|
1646
|
+
'<form class="composer att-composer" id="session-form" data-provider="' + esc(provider) + '" data-id="' + esc(id) + '">' +
|
|
1647
|
+
'<div class="cc-chips att-files hidden" id="session-files"></div>' +
|
|
1426
1648
|
'<textarea id="session-input" rows="1" placeholder="Continue this session…" enterkeyhint="send" aria-label="Continue this session"></textarea>' +
|
|
1427
|
-
'<div class="composer-actions"><button type="button" class="btn small" data-action="fork-session" title="Copy this session and send the message to the copy">Fork</button>' +
|
|
1428
|
-
'<button type="submit" class="btn primary small">Send</button></div>' +
|
|
1649
|
+
'<div class="composer-actions"><span id="session-clip" class="att-clip"></span><button type="button" class="btn small" data-action="fork-session" title="Copy this session and send the message to the copy">Fork</button>' +
|
|
1650
|
+
'<button type="submit" class="btn primary small" id="session-send">Send</button></div>' +
|
|
1429
1651
|
'</form><div class="status-line" id="session-status"></div>' +
|
|
1430
1652
|
'<p class="sx-caution hidden" id="session-caution"></p>' +
|
|
1431
1653
|
'<div id="session-queue"></div>' +
|
|
1432
1654
|
'</div>';
|
|
1433
|
-
|
|
1655
|
+
readerImagesMount($('session-turns'), r);
|
|
1656
|
+
sxNoteList(r, d);
|
|
1657
|
+
setLive({});
|
|
1658
|
+
paintReaderFiles();
|
|
1659
|
+
// The Mac's answer about files may not be in yet: ask once, then draw the clip.
|
|
1660
|
+
var kitR = readerKit(); if (kitR && !kitR.turnFiles()) kitR.ensureCaps().then(paintReaderFiles, function () {});
|
|
1434
1661
|
renderHeld(state.reading);
|
|
1435
1662
|
loadQueue(provider, id);
|
|
1436
1663
|
sxProbeAttach(r);
|
|
@@ -1449,20 +1676,30 @@
|
|
|
1449
1676
|
// ACTIVITY (ActivityWindow.swift SessionActivityFeed 6800-6890, SessionLiveFeed.swift): the state
|
|
1450
1677
|
// word with its clock, the current prompt, the last eight tool lines, and the honest counters.
|
|
1451
1678
|
// The reducer follows SessionLiveFeed.applying: a reseed (seq 1 after anything) starts over.
|
|
1452
|
-
function newFeed() { return { transport: 'idle', agentState: null, stateSince: null, waitingKind: null, waitingDetail: null, failure: null, lastReply: null, prompt: '', tools: [], lastSeq: 0, lastEventAt: null, missed: 0, connected: false, fallback: null }; }
|
|
1679
|
+
function newFeed() { return { transport: 'idle', transportSeenAt: null, agentState: null, stateSince: null, stateSeenAt: null, seenMemo: null, statusAt: null, waitingKind: null, waitingDetail: null, failure: null, lastReply: null, prompt: '', tools: [], lastSeq: 0, lastEventAt: null, missed: 0, connected: false, fallback: null }; }
|
|
1453
1680
|
function applyFeed(r, e) {
|
|
1454
1681
|
if (typeof e.seq !== 'number') return;
|
|
1455
1682
|
var f = r.feed;
|
|
1456
|
-
if (e.seq === 1 && f.lastSeq > 0) { var keep = f.connected; f = r.feed = newFeed(); f.connected = keep; }
|
|
1683
|
+
if (e.seq === 1 && f.lastSeq > 0) { var keep = f.connected, seenMemo = f.seenMemo; f = r.feed = newFeed(); f.connected = keep; f.seenMemo = seenMemo; }
|
|
1457
1684
|
else if (f.lastSeq > 0 && e.seq > f.lastSeq + 1) f.missed += e.seq - f.lastSeq - 1;
|
|
1458
1685
|
f.lastSeq = e.seq;
|
|
1459
1686
|
f.lastEventAt = typeof e.at === 'number' ? new Date(e.at) : new Date();
|
|
1460
1687
|
f.connected = true;
|
|
1461
1688
|
if (f.fallback === 'reconnecting') f.fallback = null;
|
|
1462
1689
|
if (e.kind === 'status') {
|
|
1463
|
-
|
|
1690
|
+
f.statusAt = f.lastEventAt;
|
|
1691
|
+
// When this page first saw each state (its own clock): the Stop's echo window is measured by it. The
|
|
1692
|
+
// memo outlives a reseed, so a reconnect that repeats a state seen before a Stop is not news.
|
|
1693
|
+
var memo = f.seenMemo || (f.seenMemo = {});
|
|
1694
|
+
if (e.state) {
|
|
1695
|
+
if (!memo.stream || memo.stream.key !== e.state) memo.stream = { key: e.state, seen: Date.now() };
|
|
1696
|
+
f.transport = e.state; f.transportSeenAt = memo.stream.seen;
|
|
1697
|
+
}
|
|
1464
1698
|
if (e.agent_state) {
|
|
1465
|
-
|
|
1699
|
+
var since = toDate(e.state_since), hookKey = e.agent_state + '|' + sxMs(since);
|
|
1700
|
+
if (!memo.hook || memo.hook.key !== hookKey) memo.hook = { key: hookKey, seen: Date.now() };
|
|
1701
|
+
f.stateSeenAt = memo.hook.seen;
|
|
1702
|
+
f.agentState = e.agent_state; f.stateSince = since;
|
|
1466
1703
|
f.waitingKind = e.waiting_kind || null; f.waitingDetail = e.waiting_detail || null; f.failure = e.failure || null;
|
|
1467
1704
|
if (e.last_reply) f.lastReply = e.last_reply;
|
|
1468
1705
|
}
|
|
@@ -1476,16 +1713,105 @@
|
|
|
1476
1713
|
// `other` is the stream's verb for every tool it has no verb for; its target names the tool ("Used SendMessage").
|
|
1477
1714
|
var SX_VERBS = { read: 'Read', edit: 'Edited', write: 'Edited', bash: 'Ran', run: 'Ran', search: 'Searched', grep: 'Searched', glob: 'Searched', fetch: 'Fetched', web: 'Fetched', agent: 'Delegated', task: 'Delegated', other: 'Used' };
|
|
1478
1715
|
function sxToolLine(t) { var v = String(t.verb || 'tool'); var w = SX_VERBS[v] || v.charAt(0).toUpperCase() + v.slice(1); return t.target ? w + ' ' + t.target : w; }
|
|
1479
|
-
//
|
|
1480
|
-
//
|
|
1716
|
+
// ONE STATE FOR THE SESSION CARD (6.99.29). Miles, 11/07 11:01: Today's session overlay showed
|
|
1717
|
+
// "Idle 40s" over "Working on: Yes install and proceed", beside "Working right now" and "The
|
|
1718
|
+
// session is busy" while the session ran its turn. Each piece read its own source: the badge the
|
|
1719
|
+
// hooks' derived state (which a turn delivered through the peer inbox does not move), the composer
|
|
1720
|
+
// the server's write gate, the send note the park answer, and a delivered live turn was read as a
|
|
1721
|
+
// finished one. Now every piece of the card (the badge word and clock, the prompt line's label,
|
|
1722
|
+
// Stop, the composer's park note and the reader's state line) reads sxTruth, and each signal says
|
|
1723
|
+
// what it saw and WHEN that became true; the freshest wins:
|
|
1724
|
+
// hook the stream's derived agent_state, since its state_since
|
|
1725
|
+
// stream the stream's own state, when no derived state rides it, as of that status line
|
|
1726
|
+
// list the session row or detail: running_active as of its last activity, else since state_since
|
|
1727
|
+
// gate the server's write gate: native_thread_working on the attachability probe, a refused
|
|
1728
|
+
// turn or a parked send, or a turn delivered live into the open session, as of the answer;
|
|
1729
|
+
// a later free answer, or a COS child's turn that finished, clears it
|
|
1730
|
+
// stop this page's Stop (and a finished COS child turn): idle as of then, and no working signal
|
|
1731
|
+
// that ARRIVED in the next SX_STOP_ECHO_MS counts (the stream's status trails a Stop). The
|
|
1732
|
+
// window is the page's own clock (`seen`, when this page observed each signal), never the
|
|
1733
|
+
// signal's server time, and a turn this page sends ends it (continueSession clears stoppedAt).
|
|
1734
|
+
// Waiting, failed and ended, from the hook or the session row, are the session's own words and stay:
|
|
1735
|
+
// a gate or stream that sees a busy file does not unseat a question (QA 6.99.29: the row and the
|
|
1736
|
+
// hook carry the same state_since, so the row's Waiting must hold as the hook's does). Signals with
|
|
1737
|
+
// the same time are ordered gate, stream, list, hook, stop: the hook's word wins a tie. Idle yields
|
|
1738
|
+
// only to working seen more than SX_IDLE_GRACE_MS after it, the server's rule for the flush that
|
|
1739
|
+
// trails a Stop (TRANSCRIPT_AFTER_IDLE_GRACE_MS).
|
|
1740
|
+
var SX_IDLE_GRACE_MS = 5000, SX_STOP_ECHO_MS = 8000, SX_GATE_RECHECK_MS = 20000;
|
|
1741
|
+
var SX_TIE_RANK = { gate: 0, stream: 1, list: 2, hook: 3, stop: 4 };
|
|
1742
|
+
function sxMs(v) {
|
|
1743
|
+
var t = v instanceof Date ? v.getTime() : typeof v === 'number' ? v : typeof v === 'string' && v ? Date.parse(v) : NaN;
|
|
1744
|
+
return isFinite(t) ? t : null;
|
|
1745
|
+
}
|
|
1746
|
+
var SX_HOOK_WORD = { running: 'Working', waiting: 'Waiting', failed: 'Failed', ended: 'Done', idle: 'Idle' };
|
|
1747
|
+
function sxSignals(r) {
|
|
1748
|
+
var f = r.feed || {}, out = [];
|
|
1749
|
+
// `seen`: when this page observed the signal (its own clock), for the Stop's echo window.
|
|
1750
|
+
if (f.agentState && SX_HOOK_WORD[f.agentState]) out.push({ word: SX_HOOK_WORD[f.agentState], at: sxMs(f.stateSince) || sxMs(f.statusAt) || 0, src: 'hook', seen: sxMs(f.stateSeenAt) });
|
|
1751
|
+
else if (f.statusAt) out.push({ word: f.transport === 'working' ? 'Working' : f.transport === 'done' ? 'Done' : 'Idle', at: sxMs(f.statusAt), src: 'stream', seen: sxMs(f.transportSeenAt) });
|
|
1752
|
+
if (r.list && r.list.at != null) out.push({ word: r.list.word, at: r.list.at, src: 'list', seen: sxMs(r.list.seen) });
|
|
1753
|
+
if (r.gate && r.gate.busy && r.gate.at != null) out.push({ word: 'Working', at: r.gate.at, src: 'gate', seen: r.gate.at });
|
|
1754
|
+
var stop = sxMs(r.stoppedAt);
|
|
1755
|
+
if (stop !== null) out.push({ word: 'Idle', at: stop, src: 'stop', seen: stop });
|
|
1756
|
+
return out;
|
|
1757
|
+
}
|
|
1758
|
+
/** { word, at, src }: the one state the whole card shows. Null before any signal. */
|
|
1759
|
+
function sxTruth(r) {
|
|
1760
|
+
var stop = sxMs(r.stoppedAt);
|
|
1761
|
+
var all = sxSignals(r).sort(function (a, b) { return a.at - b.at || SX_TIE_RANK[a.src] - SX_TIE_RANK[b.src]; });
|
|
1762
|
+
var best = null;
|
|
1763
|
+
all.forEach(function (c) {
|
|
1764
|
+
if (!best) { best = c; return; }
|
|
1765
|
+
if (c.word === 'Working') {
|
|
1766
|
+
// The Stop's echo, judged by when this page saw the signal (a signal with no arrival time: by its own).
|
|
1767
|
+
var seen = c.seen != null ? c.seen : c.at;
|
|
1768
|
+
if (stop !== null && seen >= stop && seen - stop < SX_STOP_ECHO_MS) return;
|
|
1769
|
+
if (stop !== null && seen < stop) return;
|
|
1770
|
+
if ((best.src === 'hook' || best.src === 'list') && (c.src === 'gate' || c.src === 'stream') && /^(Waiting|Failed|Done)$/.test(best.word)) return;
|
|
1771
|
+
if (best.word === 'Idle' && best.src !== 'stop' && c.src !== best.src && c.at <= best.at + SX_IDLE_GRACE_MS) return;
|
|
1772
|
+
}
|
|
1773
|
+
best = c;
|
|
1774
|
+
});
|
|
1775
|
+
return best;
|
|
1776
|
+
}
|
|
1777
|
+
/** Settle the page's working flag from the one state; true when it changed. */
|
|
1778
|
+
function sxSettle(r) {
|
|
1779
|
+
var t = sxTruth(r), working = !!(t && t.word === 'Working');
|
|
1780
|
+
r.live = r.live || {};
|
|
1781
|
+
var was = !!r.live.working;
|
|
1782
|
+
r.live.working = working;
|
|
1783
|
+
r.live.waiting = !!(t && t.word === 'Waiting');
|
|
1784
|
+
if (!working) { r.live.step = null; r.live.prose = null; }
|
|
1785
|
+
return was !== working;
|
|
1786
|
+
}
|
|
1787
|
+
/** What the server's write gate said, and when. A free answer clears an earlier busy one. */
|
|
1788
|
+
function sxNoteGate(r, busy) { r.gate = { busy: !!busy, at: Date.now() }; }
|
|
1789
|
+
/** The list or detail row's view, timed by its own evidence. */
|
|
1790
|
+
function sxNoteList(r, row) {
|
|
1791
|
+
if (!row) return;
|
|
1792
|
+
var waiting = sxState(row) === 'waiting', working = !!row.running_active && !waiting;
|
|
1793
|
+
var at = working ? (sxMs(row.last_activity_at) || sxMs(row.state_since)) : (sxMs(row.state_since) || sxMs(row.last_activity_at));
|
|
1794
|
+
// A row with no time of its own is the oldest word: the live feed outranks it the moment it speaks.
|
|
1795
|
+
var word = working ? 'Working' : waiting ? 'Waiting' : 'Idle', was = r.list;
|
|
1796
|
+
// Arrival is the first time this page saw THIS word at THIS time: a re-read of the same row is not news.
|
|
1797
|
+
r.list = { word: word, at: at != null ? at : 0, seen: was && was.word === word && was.at === (at != null ? at : 0) && was.seen != null ? was.seen : Date.now() };
|
|
1798
|
+
}
|
|
1799
|
+
/** A gate busy answer that decides the card is re-asked after a while, so it cannot stand forever. Every
|
|
1800
|
+
* attempt is stamped (gate.triedAt), and each failed one doubles the wait, up to 16 times (QA 6.99.29: a failing
|
|
1801
|
+
* probe was re-asked every second). `at` stays the time of the last ANSWER: a failure is no evidence of busy. */
|
|
1802
|
+
function sxGateStale(r) {
|
|
1803
|
+
var t = sxTruth(r);
|
|
1804
|
+
if (!(t && t.src === 'gate' && r.gate) || r.probing) return false;
|
|
1805
|
+
var last = Math.max(r.gate.at || 0, r.gate.triedAt || 0), fails = r.gate.failures || 0;
|
|
1806
|
+
return Date.now() - last > SX_GATE_RECHECK_MS * Math.min(Math.pow(2, fails), 16);
|
|
1807
|
+
}
|
|
1481
1808
|
function sxFeedWord(r) {
|
|
1482
|
-
var
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
return 'Idle';
|
|
1809
|
+
var t = sxTruth(r);
|
|
1810
|
+
return t ? t.word : 'Idle';
|
|
1811
|
+
}
|
|
1812
|
+
/** The prompt line: what the session works on, or what it was last asked. */
|
|
1813
|
+
function sxPromptLine(r, text) {
|
|
1814
|
+
return (sxFeedWord(r) === 'Working' ? 'Working on: ' : 'Last asked: ') + text;
|
|
1489
1815
|
}
|
|
1490
1816
|
function sxFeedDetail(f, word) {
|
|
1491
1817
|
if (word === 'Waiting' && f.agentState === 'waiting') {
|
|
@@ -1500,8 +1826,9 @@
|
|
|
1500
1826
|
if (word === 'Idle' && f.agentState === 'idle') { var reply = String(f.lastReply || '').trim(); return reply ? 'last reply: ' + reply : ''; }
|
|
1501
1827
|
return '';
|
|
1502
1828
|
}
|
|
1503
|
-
function sxElapsed(f) {
|
|
1504
|
-
var
|
|
1829
|
+
function sxElapsed(f, r) {
|
|
1830
|
+
var t = r ? sxTruth(r) : null;
|
|
1831
|
+
var since = t && t.at ? new Date(t.at) : f.stateSince || f.lastEventAt;
|
|
1505
1832
|
if (!since) return '';
|
|
1506
1833
|
var s = Math.max(0, Math.floor((Date.now() - since.getTime()) / 1000));
|
|
1507
1834
|
if (s < 60) return s + 's';
|
|
@@ -1533,7 +1860,7 @@
|
|
|
1533
1860
|
var word = sxFeedWord(r), detail = sxFeedDetail(f, word);
|
|
1534
1861
|
html += '<div class="sx-act-state"><span class="sx-dot ' + word.toLowerCase() + '" aria-hidden="true"></span><strong>' + word + '</strong>' +
|
|
1535
1862
|
'<span class="sx-act-detail">' + esc(detail) + '</span>' +
|
|
1536
|
-
'<span class="sx-act-clock" id="session-elapsed">' + esc(sxElapsed(f)) + '</span>' +
|
|
1863
|
+
'<span class="sx-act-clock" id="session-elapsed">' + esc(sxElapsed(f, r)) + '</span>' +
|
|
1537
1864
|
(live.working ? '<button type="button" class="cx-btn quiet sx-act-stop" data-action="stop-session">Stop</button>' : '') + '</div>' +
|
|
1538
1865
|
'<p class="sx-act-prompt">' + esc(f.prompt) + '</p>' +
|
|
1539
1866
|
'<div class="sx-act-tail">' + f.tools.map(function (t) { return '<div class="sx-act-tool">' + esc(sxToolLine(t)) + '</div>'; }).join('') +
|
|
@@ -1542,13 +1869,15 @@
|
|
|
1542
1869
|
html += '<p class="sx-act-fallback">' + esc(f.fallback ? sxFallbackLine(f.fallback) : 'Connecting to the live feed…') + '</p>';
|
|
1543
1870
|
}
|
|
1544
1871
|
box.innerHTML = html;
|
|
1545
|
-
// The clock ticks only while the session works or waits (Control 0.5.337).
|
|
1872
|
+
// The clock ticks only while the session works or waits (Control 0.5.337). A Working that only the write gate
|
|
1873
|
+
// says is re-asked even with no live feed (QA 6.99.29), so it cannot stand forever once the feed is lost.
|
|
1546
1874
|
clearInterval(r.feedTimer); r.feedTimer = null;
|
|
1547
|
-
var
|
|
1548
|
-
if ((f.connected || f.lastSeq > 0) && (w === 'Working' || w === 'Waiting')) {
|
|
1875
|
+
var t = sxTruth(r), w = t ? t.word : 'Idle';
|
|
1876
|
+
if (((f.connected || f.lastSeq > 0) && (w === 'Working' || w === 'Waiting')) || (t && t.src === 'gate' && w === 'Working')) {
|
|
1549
1877
|
r.feedTimer = setInterval(function () {
|
|
1550
1878
|
if (state.reading !== r) { clearInterval(r.feedTimer); return; }
|
|
1551
|
-
var el = $('session-elapsed'); if (el) el.textContent = sxElapsed(r.feed);
|
|
1879
|
+
var el = $('session-elapsed'); if (el) el.textContent = sxElapsed(r.feed, r);
|
|
1880
|
+
if (sxGateStale(r)) sxProbeAttach(r);
|
|
1552
1881
|
}, 1000);
|
|
1553
1882
|
}
|
|
1554
1883
|
}
|
|
@@ -1571,7 +1900,7 @@
|
|
|
1571
1900
|
var es;
|
|
1572
1901
|
try { es = new EventSource(sessionPath(provider, id, '/stream?seed=turn')); } catch (_e) { if (r0) { r0.feed.fallback = 'unsupported'; paintActivity(r0); } return; }
|
|
1573
1902
|
state.stream = es;
|
|
1574
|
-
var wasWorking = false;
|
|
1903
|
+
var wasWorking = false, wasWaiting = false;
|
|
1575
1904
|
es.onerror = function () {
|
|
1576
1905
|
var r = state.reading;
|
|
1577
1906
|
if (!r || r.id !== id || state.stream !== es) return;
|
|
@@ -1586,15 +1915,17 @@
|
|
|
1586
1915
|
if (!r || r.id !== id) return;
|
|
1587
1916
|
applyFeed(r, e);
|
|
1588
1917
|
if (e.kind === 'status') {
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
var waiting = e.state === 'waiting' || e.state === 'needs_input' || e.agent_state === 'waiting';
|
|
1918
|
+
// 6.99.29: the one state (sxTruth), never this line's own reading of itself.
|
|
1919
|
+
setLive({});
|
|
1920
|
+
var working = !!(r.live && r.live.working);
|
|
1921
|
+
var waiting = !working && (e.state === 'waiting' || e.state === 'needs_input' || e.agent_state === 'waiting');
|
|
1593
1922
|
var el = $('session-state');
|
|
1594
1923
|
if (el) el.textContent = working ? 'Working now' : waiting ? 'Waiting for you' : 'Open';
|
|
1595
|
-
setLive({ working: working });
|
|
1596
1924
|
if (wasWorking && !working) { refreshTurns(provider, id); replyEnded(r); }
|
|
1597
1925
|
wasWorking = working;
|
|
1926
|
+
// Gone to waiting: its question may be parked for this page now (6.99.27).
|
|
1927
|
+
if (waiting && !working && !wasWaiting) questionsNudge();
|
|
1928
|
+
wasWaiting = waiting && !working;
|
|
1598
1929
|
} else if (e.kind === 'tool') {
|
|
1599
1930
|
setLive({ step: [e.verb, e.target].filter(Boolean).join(' ') });
|
|
1600
1931
|
} else if (e.kind === 'prose' && e.text) {
|
|
@@ -1606,16 +1937,20 @@
|
|
|
1606
1937
|
}
|
|
1607
1938
|
function setLive(patch) {
|
|
1608
1939
|
var r = state.reading; if (!r) return;
|
|
1609
|
-
|
|
1610
|
-
|
|
1940
|
+
// The working flag is never set here: it is the one state's (sxSettle).
|
|
1941
|
+
var p = Object.assign({}, patch); delete p.working;
|
|
1942
|
+
r.live = Object.assign(r.live || {}, p);
|
|
1943
|
+
var moved = sxSettle(r);
|
|
1611
1944
|
paintActivity(r);
|
|
1945
|
+
if (moved) renderVerdict(r);
|
|
1612
1946
|
}
|
|
1613
1947
|
function refreshTurns(provider, id) {
|
|
1948
|
+
var reading = state.reading;
|
|
1614
1949
|
api(sessionPath(provider, id, '?turns=' + SX_TURNS)).then(function (d) {
|
|
1615
1950
|
var r = state.reading;
|
|
1616
|
-
if (!r || r.id !== id) return;
|
|
1951
|
+
if (!r || r !== reading || r.id !== id || r.provider !== provider) return;
|
|
1617
1952
|
var turns = Array.isArray(d && d.recent_turns) ? d.recent_turns : [];
|
|
1618
|
-
var box = $('session-turns'); if (box) sxMergeTurns(box, r.turns || [], turns, d);
|
|
1953
|
+
var box = $('session-turns'); if (box) { sxMergeTurns(box, r.turns || [], turns, d); readerImagesMount(box, r); }
|
|
1619
1954
|
// The header keeps the subtitle it opened with: a rewrap above the turns would move them.
|
|
1620
1955
|
r.detail = d; r.turns = turns; r.more = d.recent_turns_more === true;
|
|
1621
1956
|
renderHeld(r);
|
|
@@ -1638,13 +1973,20 @@
|
|
|
1638
1973
|
var box = $('session-queue'); if (!box || !r || r.id !== id) return;
|
|
1639
1974
|
var rows = (q.status === 200 && q.body && q.body.turns) || [];
|
|
1640
1975
|
var waiting = rows.filter(function (t) { return t.status === 'waiting' || t.status === 'queued' || t.status === 'pending'; });
|
|
1976
|
+
// QA 6.99.29: a queued message the server refused (or let expire) stays in view with the server's reason,
|
|
1977
|
+
// instead of vanishing from the line.
|
|
1978
|
+
var notSent = rows.filter(function (t) { return t.status === 'refused' || t.status === 'expired'; });
|
|
1641
1979
|
if (q.status === 200 && r.queued !== waiting.length) { r.queued = waiting.length; paintActivity(r); }
|
|
1642
|
-
if (!waiting.length) { box.innerHTML = ''; return; }
|
|
1643
|
-
box.innerHTML = '<div class="sx-queue"
|
|
1980
|
+
if (!waiting.length && !notSent.length) { box.innerHTML = ''; return; }
|
|
1981
|
+
box.innerHTML = '<div class="sx-queue">' + (waiting.length ? '<p class="sx-queue-head">' + (waiting.length === 1 ? '1 follow-up queued. It sends when this session is available.' : waiting.length + ' follow-ups queued. They send in order when this session is available.') + '</p>' : '') +
|
|
1644
1982
|
waiting.map(function (t) {
|
|
1645
1983
|
var mine = t.owner !== 'codex' && parkedByMe(r, t.clientTurnId);
|
|
1646
1984
|
return '<div class="sx-queue-row"><span class="sx-queue-state">' + esc(sxQueueLine(t)) + '</span><span class="sx-queue-text">' + esc(t.preview || t.prompt || 'Message') + '</span>' +
|
|
1647
1985
|
(mine ? '<button type="button" class="cx-btn quiet" data-action="unqueue" data-turn-key="' + esc(t.clientTurnId) + '" title="Cancel this queued message. It will not be sent.">Cancel</button>' : '') + '</div>';
|
|
1986
|
+
}).join('') +
|
|
1987
|
+
notSent.map(function (t) {
|
|
1988
|
+
return '<div class="sx-queue-row sx-queue-refused"><span class="sx-queue-state">Not sent</span><span class="sx-queue-text">' + esc(t.preview || t.prompt || 'Message') +
|
|
1989
|
+
'<span class="sx-queue-reason">' + esc(t.reasonCopy || (t.status === 'expired' ? 'Not sent: the thread stayed busy too long.' : 'Not sent: the session refused it.')) + '</span></span></div>';
|
|
1648
1990
|
}).join('') + '</div>';
|
|
1649
1991
|
}).catch(function () {});
|
|
1650
1992
|
}
|
|
@@ -1654,21 +1996,33 @@
|
|
|
1654
1996
|
// and anything else is the server's refusal, word for word.
|
|
1655
1997
|
function sxProbeAttach(r) {
|
|
1656
1998
|
// Today's overlay probes its own session object (r.view): the same verdict, painted by its view.
|
|
1999
|
+
r.probing = true;
|
|
2000
|
+
// Every attempt is stamped; a failed one backs the next off (sxGateStale). A gate answer resets both.
|
|
2001
|
+
if (r.gate) r.gate.triedAt = Date.now();
|
|
2002
|
+
var failed = function () { if (r.gate) r.gate.failures = (r.gate.failures || 0) + 1; };
|
|
1657
2003
|
return apiAnswer(sessionPath(r.provider, r.id, '/attachability')).then(function (a) {
|
|
1658
2004
|
if (state.reading !== r && !r.view) return;
|
|
1659
|
-
if (a.status === 404) { r.verdict = { attachable: false, reason: 'route_absent', reasonCopy: 'Update the COS server to continue a thread from here.', ownerCount: 0 }; viewOf(r).verdict(r); return; }
|
|
2005
|
+
if (a.status === 404) { failed(); r.verdict = { attachable: false, reason: 'route_absent', reasonCopy: 'Update the COS server to continue a thread from here.', ownerCount: 0 }; viewOf(r).verdict(r); return; }
|
|
1660
2006
|
var b = a.body || {};
|
|
1661
|
-
if (a.status !== 200 || typeof b.attachable !== 'boolean') return;
|
|
2007
|
+
if (a.status !== 200 || typeof b.attachable !== 'boolean') { failed(); return; }
|
|
1662
2008
|
var owners = Number(b.ownerCount) || 0;
|
|
1663
2009
|
r.verdict = { attachable: b.attachable, reason: String(b.reason || ''), reasonCopy: String(b.reasonCopy || ''), ownerCount: owners, caution: b.attachable === true && owners > 0 };
|
|
1664
|
-
|
|
1665
|
-
|
|
2010
|
+
// 6.99.29: the gate's answer is a signal of the one state, as of now.
|
|
2011
|
+
sxNoteGate(r, b.attachable === false && b.reason === 'native_thread_working');
|
|
2012
|
+
sxRepaint(r);
|
|
2013
|
+
}).catch(failed).finally(function () { r.probing = false; });
|
|
1666
2014
|
}
|
|
1667
2015
|
var SX_PARK = {
|
|
1668
2016
|
cursor: 'Working right now. A message you send goes into this Cursor chat when its turn ends.',
|
|
1669
2017
|
codex: 'Working right now. A message you send joins the Codex app’s queue for this thread.',
|
|
1670
2018
|
claude: 'Working right now. A message you send waits and lands when this turn ends.'
|
|
1671
2019
|
};
|
|
2020
|
+
/** The composer's "Working right now" note: a busy gate verdict, and the one state agrees. */
|
|
2021
|
+
function sxParkNoteShows(r) {
|
|
2022
|
+
var v = r.verdict;
|
|
2023
|
+
if (!v || v.attachable !== false || !/^(native_thread_working|native_target_busy)$/.test(v.reason)) return false;
|
|
2024
|
+
return sxFeedWord(r) === 'Working' || v.reason === 'native_target_busy';
|
|
2025
|
+
}
|
|
1672
2026
|
function sxSupplement(reason) {
|
|
1673
2027
|
if (reason === 'native_target_busy') return 'It frees itself within 30 minutes. Detaching is not available in Control yet.';
|
|
1674
2028
|
if (reason === 'native_target_fenced') return 'Fences can be released from the Fences card in this panel.';
|
|
@@ -1694,7 +2048,8 @@
|
|
|
1694
2048
|
(f.retry ? '<button type="button" class="cx-btn quiet" data-sx="retry">Retry</button>' : '') +
|
|
1695
2049
|
(f.fork ? forkBtn : ''));
|
|
1696
2050
|
} else if (v && v.attachable === false && /^(native_thread_working|native_target_busy)$/.test(v.reason)) {
|
|
1697
|
-
|
|
2051
|
+
// 6.99.29: "Working right now" only while the one state says so.
|
|
2052
|
+
if (sxParkNoteShows(r)) html = '<div class="sx-park"><p>' + esc(SX_PARK[sxProvider(r.provider)]) + '</p>' + (/fork/i.test(v.reasonCopy) ? '<div class="sx-box-actions">' + forkBtn + '</div>' : '') + '</div>';
|
|
1698
2053
|
} else if (v && v.attachable === false && v.reasonCopy) {
|
|
1699
2054
|
html = refusalBox(v.reasonCopy, sxSupplement(v.reason), /fork/i.test(v.reasonCopy) ? forkBtn : '');
|
|
1700
2055
|
}
|
|
@@ -1707,14 +2062,23 @@
|
|
|
1707
2062
|
}
|
|
1708
2063
|
}
|
|
1709
2064
|
|
|
2065
|
+
/** Repaint whichever card shows this session after its one state moved (the reader or a view). */
|
|
2066
|
+
function sxRepaint(r) {
|
|
2067
|
+
sxSettle(r);
|
|
2068
|
+
if (r.view) { if (typeof r.view.stateChanged === 'function') r.view.stateChanged(r); else r.view.verdict(r); return; }
|
|
2069
|
+
if (state.reading === r) { paintActivity(r); renderVerdict(r); }
|
|
2070
|
+
}
|
|
1710
2071
|
function sessionStatus(text, kind, actionHtml) {
|
|
1711
2072
|
var el = $('session-status'); if (!el) return;
|
|
1712
2073
|
el.className = 'status-line' + (kind === 'error' ? ' error' : '');
|
|
1713
2074
|
el.innerHTML = esc(text || '') + (actionHtml || '');
|
|
1714
2075
|
}
|
|
1715
|
-
|
|
2076
|
+
// 11/07: the files sent with the words (media ids) are part of the draft: other files are another message, with a
|
|
2077
|
+
// key of its own, so the server never answers a new message with an old one's replay.
|
|
2078
|
+
function draftFor(text, r, ids) {
|
|
1716
2079
|
r = r || state.reading;
|
|
1717
|
-
|
|
2080
|
+
var files = Array.isArray(ids) ? ids.slice() : [];
|
|
2081
|
+
if (!r.draft || r.draft.text !== text || (r.draft.ids || []).join(',') !== files.join(',')) r.draft = { text: text, key: newTurnKey(), ids: files };
|
|
1718
2082
|
return r.draft;
|
|
1719
2083
|
}
|
|
1720
2084
|
// The box is cleared only of the very words that were sent; anything typed since stays.
|
|
@@ -1723,11 +2087,12 @@
|
|
|
1723
2087
|
if (input && input.value.trim() === String(text).trim()) { input.value = ''; autoGrow(input); }
|
|
1724
2088
|
if (state.reading && state.reading.draft && state.reading.draft.text === text) state.reading.draft = null;
|
|
1725
2089
|
}
|
|
1726
|
-
// Words go back in the box only if the box is empty; otherwise the status line keeps them.
|
|
1727
|
-
|
|
2090
|
+
// Words go back in the box only if the box is empty; otherwise the status line keeps them. `files` says what became
|
|
2091
|
+
// of the message's files (heldFilesBack).
|
|
2092
|
+
function returnToBox(text, why, files) {
|
|
1728
2093
|
var input = $('session-input');
|
|
1729
|
-
if (input && !input.value.trim()) { input.value = text; autoGrow(input); sessionStatus(why + ' Your message is back in the box.', 'error'); }
|
|
1730
|
-
else sessionStatus(why + ' Not sent: “' + String(text).slice(0, 80) + (text.length > 80 ? '…' : '') + '”', 'error');
|
|
2094
|
+
if (input && !input.value.trim()) { input.value = text; autoGrow(input); sessionStatus(why + ' Your message is back in the box.' + (files || ''), 'error'); }
|
|
2095
|
+
else sessionStatus(why + ' Not sent: “' + String(text).slice(0, 80) + (text.length > 80 ? '…' : '') + '”' + (files || ''), 'error');
|
|
1731
2096
|
}
|
|
1732
2097
|
// Your words show in the conversation the moment the Mac accepts them; the next refresh of
|
|
1733
2098
|
// the transcript replaces the bubble with the real turn.
|
|
@@ -1761,11 +2126,96 @@
|
|
|
1761
2126
|
};
|
|
1762
2127
|
function viewOf(r) { return (r && r.view) || READER_VIEW; }
|
|
1763
2128
|
|
|
2129
|
+
// ── files with a message to a session (6.99.27, Miles: "We still don't have the ability to take picture, or add photo
|
|
2130
|
+
// or files from the web") ──
|
|
2131
|
+
// The COS chat's attach kit (panes/cos-chat.js, W.cosChat.attach), as on Today's reply box: Take photo and the clip,
|
|
2132
|
+
// any file the Mac takes on a turn (threadTurnFileBytes, 64 MB; a video 100 MB), up to 5, sent as it is. The ids go
|
|
2133
|
+
// with the words; the server writes where each file is on the Mac under them. One box per session, kept in this tab.
|
|
2134
|
+
var READER_FILES = {};
|
|
2135
|
+
function readerKit() { var c = window.COSWeb && window.COSWeb.cosChat; return c && c.attach ? c.attach : null; }
|
|
2136
|
+
function readerFilesOn() { var k = readerKit(); return !!(k && k.turnFiles() > 0 && state.reading); }
|
|
2137
|
+
function readerBox(r) {
|
|
2138
|
+
var kit = readerKit(); if (!kit || !r) return null;
|
|
2139
|
+
var key = r.provider + '|' + r.id;
|
|
2140
|
+
if (!READER_FILES[key]) READER_FILES[key] = kit.box({
|
|
2141
|
+
changed: function (box) { if (state.reading && READER_FILES[state.reading.provider + '|' + state.reading.id] === box) paintReaderFiles(); },
|
|
2142
|
+
removed: function () { sessionStatus(''); },
|
|
2143
|
+
signedOut: function () { sessionStatus('This browser was signed out. Your message and files are kept. Sign in, then tap Retry on each file.', 'error'); }
|
|
2144
|
+
}, { anyBytes: kit.turnFileBytes() });
|
|
2145
|
+
if (!READER_FILES[key].anyBytes && !READER_FILES[key].atts.length && kit.turnFileBytes()) READER_FILES[key].anyBytes = kit.turnFileBytes();
|
|
2146
|
+
return READER_FILES[key];
|
|
2147
|
+
}
|
|
2148
|
+
function readerFilesState(box) {
|
|
2149
|
+
var kit = readerKit();
|
|
2150
|
+
if (!kit || !box || !box.atts.length) return 'none';
|
|
2151
|
+
return kit.pending(box) ? 'waiting' : kit.failed(box) ? 'failed' : 'ready';
|
|
2152
|
+
}
|
|
2153
|
+
function paintReaderFiles() {
|
|
2154
|
+
var kit = readerKit(), r = state.reading, slot = $('session-clip'), list = $('session-files');
|
|
2155
|
+
if (!slot || !list) return;
|
|
2156
|
+
if (!readerFilesOn()) { slot.innerHTML = ''; list.innerHTML = ''; list.classList.add('hidden'); return; }
|
|
2157
|
+
var box = readerBox(r), full = box.atts.length >= kit.max, mode = String(box.anyBytes || 0);
|
|
2158
|
+
if (slot.getAttribute('data-mode') !== mode) { slot.innerHTML = kit.cameraHtml('data-sx-file') + kit.clipHtml('data-sx-file', { any: box.anyBytes }); slot.setAttribute('data-mode', mode); }
|
|
2159
|
+
Array.prototype.forEach.call(slot.querySelectorAll('.cc-attach'), function (l) { l.classList.toggle('full', full); });
|
|
2160
|
+
Array.prototype.forEach.call(slot.querySelectorAll('[data-sx-file]'), function (i) { i.disabled = full || !!r.sending; });
|
|
2161
|
+
list.innerHTML = kit.chipsHtml(box, 'data-action') + (box.atts.length ? '<p class="att-limit">' + esc(kit.limitLine(box)) + '</p>' : '');
|
|
2162
|
+
list.classList.toggle('hidden', !box.atts.length);
|
|
2163
|
+
// Send and Fork wait while a file uploads. A failed file never greys them out (QA 6.99.27): a tap says why.
|
|
2164
|
+
var st = readerFilesState(box), form = $('session-form');
|
|
2165
|
+
if (!form || r.sending) return;
|
|
2166
|
+
var send = $('session-send'), forkBtn = form.querySelector('[data-action="fork-session"]');
|
|
2167
|
+
if (send) { send.disabled = st === 'waiting'; send.textContent = st === 'waiting' ? 'Uploading' : 'Send'; }
|
|
2168
|
+
if (forkBtn) forkBtn.disabled = st === 'waiting';
|
|
2169
|
+
}
|
|
2170
|
+
function addReaderFiles(list) {
|
|
2171
|
+
var kit = readerKit(), r = state.reading; if (!kit || !r || !readerFilesOn() || !list || !list.length) return;
|
|
2172
|
+
var refusals = kit.add(readerBox(r), list);
|
|
2173
|
+
if (refusals.length) sessionStatus(refusals.join(' '), 'error');
|
|
2174
|
+
paintReaderFiles();
|
|
2175
|
+
}
|
|
2176
|
+
// The files that went with a message leave the box; any added since stay. Taken, not removed: the status line stays
|
|
2177
|
+
// (a held message's Don't send is on it). Answers what went, for a held message to keep.
|
|
2178
|
+
function readerFilesSent(box, ids) {
|
|
2179
|
+
var kit = readerKit(); if (!kit || !box || !ids || !ids.length) return [];
|
|
2180
|
+
return kit.take(box, ids);
|
|
2181
|
+
}
|
|
2182
|
+
// QA 6.99.27: a held message keeps what its files were, so if it does not go, it comes back whole (heldFilesBack).
|
|
2183
|
+
function keepHeldFiles(r, text, ids, went) {
|
|
2184
|
+
if (!went || !went.length) return;
|
|
2185
|
+
r.held.forEach(function (h) { if (!h.files && h.text === text && (h.ids || []).join(',') === ids.join(',')) h.files = went; });
|
|
2186
|
+
saveHeld(r);
|
|
2187
|
+
}
|
|
2188
|
+
// The Mac keeps a file no turn took for 4 hours (lib/turn-file-store.ts, and the media store's own rule); this
|
|
2189
|
+
// trusts a file for 5 minutes less. Past that it says the file has gone rather than offer one the send would refuse.
|
|
2190
|
+
var HELD_FILE_KEEP_MS = 4 * 60 * 60000 - 5 * 60000;
|
|
2191
|
+
function heldFilesBack(r, h) {
|
|
2192
|
+
var ids = h.ids || []; if (!ids.length) return '';
|
|
2193
|
+
var kit = readerKit(), box = readerFilesOn() ? readerBox(r) : null, now = Date.now();
|
|
2194
|
+
var known = (h.files || []).filter(function (f) { return f && ids.indexOf(f.mediaId) >= 0; });
|
|
2195
|
+
var kept = known.filter(function (f) { return f.keptAt > 0 && now - f.keptAt < HELD_FILE_KEEP_MS; });
|
|
2196
|
+
var back = box && kept.length ? kit.restore(box, kept) : 0;
|
|
2197
|
+
var lost = ids.length - back, expired = known.length - kept.length;
|
|
2198
|
+
var said = !back ? '' : back === ids.length ? (back === 1 ? ' Its file is attached again.' : ' Its ' + back + ' files are attached again.')
|
|
2199
|
+
: ' ' + back + ' of its files ' + (back === 1 ? 'is' : 'are') + ' attached again.';
|
|
2200
|
+
if (!lost) return said;
|
|
2201
|
+
var gone = lost === expired
|
|
2202
|
+
? (lost === 1 ? ' The file it carried was kept for 4 hours and has gone.' : ' The ' + lost + ' files it carried were kept for 4 hours and have gone.')
|
|
2203
|
+
: (lost === 1 ? ' A file it carried did not come back.' : ' ' + lost + ' files it carried did not come back.');
|
|
2204
|
+
return said + gone + ' Attach ' + (lost === 1 ? 'it' : 'them') + ' again.';
|
|
2205
|
+
}
|
|
2206
|
+
|
|
1764
2207
|
function sendSessionTurn(fork, opts) {
|
|
1765
2208
|
var r = state.reading; var form = $('session-form');
|
|
1766
2209
|
if (!r || !form) return;
|
|
1767
2210
|
var input = $('session-input'), text = input.value.trim();
|
|
1768
2211
|
if (!text) { sessionStatus(fork ? 'Write what the copy should do first.' : 'Write a message first.', 'error'); return; }
|
|
2212
|
+
// 6.99.27: files. Never before every upload has finished, never with one that failed. A fork carries them too: its
|
|
2213
|
+
// first turn is this message, so the copy gets the same files (the fork route writes their paths as a turn does).
|
|
2214
|
+
var fb = readerFilesOn() ? readerBox(r) : null, fst = readerFilesState(fb);
|
|
2215
|
+
if (fst === 'waiting') { sessionStatus('A file is still uploading. ' + (fork ? 'Fork' : 'Send') + ' when it finishes.'); return; }
|
|
2216
|
+
if (fst === 'failed') { sessionStatus('A file did not upload. Retry it or remove it.', 'error'); return; }
|
|
2217
|
+
var fileIds = fst === 'ready' ? readerKit().ids(fb) : [];
|
|
2218
|
+
if (fileIds.length && !fork) opts = Object.assign({}, opts || {}, { attachmentIds: fileIds });
|
|
1769
2219
|
// Control's caution (SessionChatVerdict.caution): another app holds this session and looks
|
|
1770
2220
|
// idle, so the first send asks before it writes. A fork writes to a copy and never asks.
|
|
1771
2221
|
if (!fork && r.verdict && r.verdict.caution && !r.cautionAck) { r.cautionAsk = true; renderVerdict(r); return; }
|
|
@@ -1775,8 +2225,12 @@
|
|
|
1775
2225
|
if (r.refusal || r.cautionAsk) { r.refusal = null; r.cautionAsk = false; renderVerdict(r); }
|
|
1776
2226
|
r.sending = true;
|
|
1777
2227
|
var btns = form.querySelectorAll('button'); btns.forEach(function (b) { b.disabled = true; });
|
|
1778
|
-
var done = function () { r.sending = false; btns.forEach(function (b) { b.disabled = false; }); };
|
|
1779
|
-
(fork ? forkSession(r, text) : continueSession(r, text, opts || {})
|
|
2228
|
+
var done = function () { r.sending = false; btns.forEach(function (b) { b.disabled = false; }); paintReaderFiles(); };
|
|
2229
|
+
(fork ? forkSession(r, text, fileIds) : continueSession(r, text, opts || {}).then(function (how) {
|
|
2230
|
+
// The Mac took the message (sent, waiting in line, or held until the reply ends): its files went with it.
|
|
2231
|
+
if (/^(sent|parked|held)$/.test(how)) { var went = readerFilesSent(fb, fileIds); if (how === 'held') keepHeldFiles(r, text, fileIds, went); }
|
|
2232
|
+
return how;
|
|
2233
|
+
})).then(done, function (err) {
|
|
1780
2234
|
done();
|
|
1781
2235
|
if (state.reading !== r) return;
|
|
1782
2236
|
var why = err && err.name === 'AbortError'
|
|
@@ -1794,7 +2248,9 @@
|
|
|
1794
2248
|
// Resolves to 'sent' | 'parked' | 'held' | 'refused' | 'changed'.
|
|
1795
2249
|
function continueSession(r, text, opts) {
|
|
1796
2250
|
var provider = r.provider, id = r.id;
|
|
1797
|
-
var draft = opts.held ? { text: opts.held.text, key: opts.held.key } : draftFor(text, r), view = viewOf(r);
|
|
2251
|
+
var draft = opts.held ? { text: opts.held.text, key: opts.held.key, ids: opts.held.ids || [] } : draftFor(text, r, opts.attachmentIds), view = viewOf(r);
|
|
2252
|
+
// Files go only to a server that said it takes them (threadTurnAttachments); the caller shows no clip otherwise.
|
|
2253
|
+
var files = draft.ids && draft.ids.length ? { attachmentIds: draft.ids } : {};
|
|
1798
2254
|
if (!opts.held) view.status(r, 'Sending');
|
|
1799
2255
|
var bound = bindingFor(provider, id);
|
|
1800
2256
|
var attach = bound ? Promise.resolve({ ok: true, binding: bound }) : apiAnswer(sessionPath(provider, id, '/attach'), { method: 'POST', body: { cosSessionId: cosSessionId() }, timeoutMs: 20000 }).then(function (a) {
|
|
@@ -1823,9 +2279,12 @@
|
|
|
1823
2279
|
}
|
|
1824
2280
|
return 'refused';
|
|
1825
2281
|
}
|
|
2282
|
+
// QA 6.99.29: a turn this page sends starts new work, so the last Stop (or finished COS turn) no longer
|
|
2283
|
+
// mutes the working signals that follow (a held follow-up goes 800 ms after the end, inside the echo window).
|
|
2284
|
+
r.stoppedAt = null;
|
|
1826
2285
|
return apiAnswer('/api/agent-sessions/bindings/' + encodeURIComponent(at.binding.bindingId) + '/turns', {
|
|
1827
2286
|
method: 'POST', timeoutMs: 25000,
|
|
1828
|
-
body: Object.assign({ prompt: draft.text, epoch: at.binding.epoch, targetKey: targetKey(provider, id), clientTurnId: draft.key }, opts.acknowledgedRevision ? { acknowledgedRevision: opts.acknowledgedRevision } : {}),
|
|
2287
|
+
body: Object.assign({ prompt: draft.text, epoch: at.binding.epoch, targetKey: targetKey(provider, id), clientTurnId: draft.key }, files, opts.acknowledgedRevision ? { acknowledgedRevision: opts.acknowledgedRevision } : {}),
|
|
1829
2288
|
}).then(function (t) {
|
|
1830
2289
|
var b = t.body || {};
|
|
1831
2290
|
if (t.status >= 200 && t.status < 300 && (b.outcome === 'queued' || b.outcome === 'completed')) {
|
|
@@ -1833,6 +2292,8 @@
|
|
|
1833
2292
|
else { view.pending(r, draft.text, b.replayed ? 'already sent' : 'sent'); view.clear(r, draft.text); }
|
|
1834
2293
|
view.status(r, b.replayed ? 'Already sent. Watching for the reply.' : 'Sent. Watching for the reply.');
|
|
1835
2294
|
view.watch(r, at.binding.bindingId, draft.key);
|
|
2295
|
+
// 6.99.29: a turn that went live is in the open session, which now works on it.
|
|
2296
|
+
if (b.outcome === 'completed' && b.via === 'live') { sxNoteGate(r, true); try { sxRepaint(r); } catch (_e) {} }
|
|
1836
2297
|
return 'sent';
|
|
1837
2298
|
}
|
|
1838
2299
|
var reason = b.reason || '';
|
|
@@ -1867,7 +2328,7 @@
|
|
|
1867
2328
|
var HELD_MAX_MS = 5 * 60000, HELD_RETRY_MS = 8000;
|
|
1868
2329
|
function holdUntilReplyEnds(r, draft) {
|
|
1869
2330
|
if (!r.held.some(function (h) { return h.key === draft.key; })) {
|
|
1870
|
-
r.held.push({ text: draft.text, key: draft.key, since: Date.now() });
|
|
2331
|
+
r.held.push({ text: draft.text, key: draft.key, since: Date.now(), ids: draft.ids || [] });
|
|
1871
2332
|
showPendingTurn(draft.text, 'sends after this reply', draft.key);
|
|
1872
2333
|
saveHeld(r);
|
|
1873
2334
|
}
|
|
@@ -1903,8 +2364,8 @@
|
|
|
1903
2364
|
if (bubble) { bubble.removeAttribute('data-held-key'); bubble.querySelector('.who').textContent = 'You, waiting in line'; }
|
|
1904
2365
|
} else {
|
|
1905
2366
|
if (bubble) bubble.remove();
|
|
1906
|
-
|
|
1907
|
-
|
|
2367
|
+
// The whole message comes back: its words, and its files where the Mac still keeps them.
|
|
2368
|
+
returnToBox(h.text, how === 'cancelled' ? 'Not sent.' : copy || 'Not sent.', heldFilesBack(r, h));
|
|
1908
2369
|
}
|
|
1909
2370
|
if (!r.held.some(function (x) { return !x.cancelled; })) clearInterval(r.heldTimer);
|
|
1910
2371
|
}
|
|
@@ -1921,7 +2382,7 @@
|
|
|
1921
2382
|
var view = viewOf(r);
|
|
1922
2383
|
return apiAnswer(sessionPath(r.provider, r.id, '/queued-turns'), {
|
|
1923
2384
|
method: 'POST', timeoutMs: 15000,
|
|
1924
|
-
body: { cosSessionId: cosSessionId(), clientTurnId: draft.key, prompt: draft.text },
|
|
2385
|
+
body: Object.assign({ cosSessionId: cosSessionId(), clientTurnId: draft.key, prompt: draft.text }, draft.ids && draft.ids.length ? { attachmentIds: draft.ids } : {}),
|
|
1925
2386
|
}).then(function (q) {
|
|
1926
2387
|
var b = q.body || {};
|
|
1927
2388
|
if (b.queued === true || b.error === 'duplicate_turn') {
|
|
@@ -1930,10 +2391,14 @@
|
|
|
1930
2391
|
else { view.pending(r, draft.text, 'waiting in line'); view.clear(r, draft.text); }
|
|
1931
2392
|
view.status(r, b.error === 'duplicate_turn' ? 'Already waiting to send.' : (b.position ? 'The session is busy. Yours is #' + (b.position + 1) + ' in line and sends on its own.' : 'The session is busy. Yours sends next, on its own.'));
|
|
1932
2393
|
view.queue(r);
|
|
2394
|
+
// 6.99.29: "The session is busy" is the gate's word as of now; the card shows the same. Last, and
|
|
2395
|
+
// never allowed to cost the parked message its settlement.
|
|
2396
|
+
if (b.queued === true && refusal && refusal.reason === 'native_thread_working') { sxNoteGate(r, true); try { sxRepaint(r); } catch (_e) {} }
|
|
1933
2397
|
return 'parked';
|
|
1934
2398
|
}
|
|
1935
2399
|
if (b.error === 'thread_free' && !opts.freeRetry) return continueSession(r, draft.text, Object.assign({}, opts, { freeRetry: true }));
|
|
1936
|
-
|
|
2400
|
+
// 11/07: a file that could not be kept refuses the park with its own words, which say which step to take.
|
|
2401
|
+
var copy = (b.attachmentError && b.reasonCopy) || (refusal && refusal.reasonCopy) || (b.error === 'queue_full' ? 'That session already has a full line of waiting messages.' : 'COS could not send or queue that.');
|
|
1937
2402
|
if (opts.held) settleHeld(r, opts.held, 'refused', copy); else view.status(r, copy + ' It is still in the box.', 'error');
|
|
1938
2403
|
return 'refused';
|
|
1939
2404
|
});
|
|
@@ -1949,10 +2414,13 @@
|
|
|
1949
2414
|
apiAnswer('/api/agent-sessions/bindings/' + encodeURIComponent(bindingId) + '/turns/' + encodeURIComponent(key)).then(function (s) {
|
|
1950
2415
|
var o = s.body && s.body.outcome;
|
|
1951
2416
|
if (o === 'completed') {
|
|
1952
|
-
clearInterval(state.turnPoll);
|
|
1953
|
-
//
|
|
1954
|
-
|
|
1955
|
-
|
|
2417
|
+
clearInterval(state.turnPoll); refreshTurns(provider, id);
|
|
2418
|
+
// 6.99.29: a turn delivered LIVE is in the open session, which is now working on it; only a
|
|
2419
|
+
// COS child's finished turn is the end of the work. Either way the one state settles it.
|
|
2420
|
+
if (s.body.via === 'live') { sxNoteGate(r, true); sessionStatus('Sent into the open session.'); }
|
|
2421
|
+
else { r.stoppedAt = Date.now(); sessionStatus('Done.'); }
|
|
2422
|
+
setLive({}); var st = $('session-state'); if (st) st.textContent = r.live && r.live.working ? 'Working now' : 'Open';
|
|
2423
|
+
if (s.body.via !== 'live') replyEnded(r);
|
|
1956
2424
|
} else if (o === 'refused') { clearInterval(state.turnPoll); sessionStatus(s.body.reasonCopy || 'The session refused that message.', 'error'); replyEnded(r); }
|
|
1957
2425
|
else if (o === 'ambiguous') { clearInterval(state.turnPoll); sessionStatus(s.body.reasonCopy || 'COS cannot tell whether that landed. Look at the session before sending it again.', 'error'); }
|
|
1958
2426
|
}).catch(function () {});
|
|
@@ -1961,13 +2429,15 @@
|
|
|
1961
2429
|
|
|
1962
2430
|
// Fork copies the session and runs the message there. One key per draft, so a second tap or
|
|
1963
2431
|
// a resend after a timeout finds the same job instead of making a second copy.
|
|
1964
|
-
function forkSession(r, text) {
|
|
1965
|
-
|
|
2432
|
+
function forkSession(r, text, ids) {
|
|
2433
|
+
ids = Array.isArray(ids) ? ids : [];
|
|
2434
|
+
// Other files are another fork, with its own key.
|
|
2435
|
+
if (!r.forkDraft || r.forkDraft.text !== text || (r.forkDraft.ids || []).join(',') !== ids.join(',')) r.forkDraft = { text: text, key: 'fk' + newTurnKey().slice(2), ids: ids.slice() };
|
|
1966
2436
|
var forkId = r.forkDraft.key;
|
|
1967
2437
|
sessionStatus('Copying the session. This can take up to a minute.');
|
|
1968
2438
|
var settle = function (b) {
|
|
1969
2439
|
if (b.forked === true) {
|
|
1970
|
-
r.forkDraft = null; clearBoxOf(text);
|
|
2440
|
+
r.forkDraft = null; clearBoxOf(text); readerFilesSent(readerFilesOn() ? readerBox(r) : null, ids);
|
|
1971
2441
|
sessionStatus('Forked. The copy has your message and shows in Sessions in a moment.');
|
|
1972
2442
|
setTimeout(function () { if (state.pane === 'sessions') loadSessions(true); }, 4000);
|
|
1973
2443
|
return true;
|
|
@@ -1975,7 +2445,7 @@
|
|
|
1975
2445
|
if (b.forked === false || b.state === 'failed' || b.state === 'refused') { sessionStatus((b.reasonCopy || 'COS could not copy that session.'), 'error'); return true; }
|
|
1976
2446
|
return false;
|
|
1977
2447
|
};
|
|
1978
|
-
return apiAnswer(sessionPath(r.provider, r.id, '/fork'), { method: 'POST', timeoutMs: 65000, body: { prompt: text, cosSessionId: cosSessionId(), clientForkId: forkId } }).then(function (f) {
|
|
2448
|
+
return apiAnswer(sessionPath(r.provider, r.id, '/fork'), { method: 'POST', timeoutMs: 65000, body: Object.assign({ prompt: text, cosSessionId: cosSessionId(), clientForkId: forkId }, ids.length ? { attachmentIds: ids } : {}) }).then(function (f) {
|
|
1979
2449
|
if (settle(f.body || {})) return;
|
|
1980
2450
|
if (f.status !== 202) { sessionStatus('COS could not copy that session.', 'error'); return; }
|
|
1981
2451
|
// 202: running in the background. Follow its job for up to three minutes.
|
|
@@ -2000,7 +2470,7 @@
|
|
|
2000
2470
|
if (b.cancelled) {
|
|
2001
2471
|
r.stoppedAt = Date.now();
|
|
2002
2472
|
clearInterval(state.turnPoll);
|
|
2003
|
-
setLive({
|
|
2473
|
+
setLive({}); var st = $('session-state'); if (st) st.textContent = 'Open';
|
|
2004
2474
|
setTimeout(function () { if (state.reading === r) refreshTurns(r.provider, r.id); }, 1500);
|
|
2005
2475
|
}
|
|
2006
2476
|
}).catch(function (err) { sessionStatus(err.message, 'error'); });
|
|
@@ -2014,6 +2484,36 @@
|
|
|
2014
2484
|
});
|
|
2015
2485
|
}
|
|
2016
2486
|
|
|
2487
|
+
// Output previews are separate from composer attachments. Only the DTO fields reach the lazy viewer.
|
|
2488
|
+
function readerImagesHtml(turn) {
|
|
2489
|
+
var images = turn && Array.isArray(turn.outputImages) ? turn.outputImages.filter(function (x) {
|
|
2490
|
+
return x && /^ai_[a-f0-9]{64}$/.test(x.id || '') && (x.state === 'available' || x.state === 'unavailable');
|
|
2491
|
+
}).map(function (x) { return { id: x.id, label: String(x.label || 'Output image').slice(0, 180), state: x.state, reason: x.reason }; }) : [];
|
|
2492
|
+
return images.length ? '<div data-reader-images="' + esc(JSON.stringify(images)) + '"><p class="rm-status" role="status">Loading image previews…</p></div>' : '';
|
|
2493
|
+
}
|
|
2494
|
+
function readerImagesStop(host) {
|
|
2495
|
+
if (!host) return;
|
|
2496
|
+
host.__cosImageOwner = null;
|
|
2497
|
+
if (window.COSWeb.readerMedia) window.COSWeb.readerMedia.unmount(host);
|
|
2498
|
+
}
|
|
2499
|
+
function readerImagesMount(host, r) {
|
|
2500
|
+
if (!host || !r) return;
|
|
2501
|
+
if (!host.querySelectorAll('[data-reader-images]').length) { readerImagesStop(host); return; }
|
|
2502
|
+
var owner = host.__cosImageOwner = {};
|
|
2503
|
+
ensurePane('readerMedia', 4000).then(function (kit) {
|
|
2504
|
+
if (host.__cosImageOwner !== owner || host.isConnected === false) return;
|
|
2505
|
+
if (!kit) throw new Error('Image previews could not load.');
|
|
2506
|
+
kit.mount(host, r.provider, r.id);
|
|
2507
|
+
}).catch(function () {
|
|
2508
|
+
if (host.__cosImageOwner !== owner || host.isConnected === false) return;
|
|
2509
|
+
host.querySelectorAll('[data-reader-images]').forEach(function (slot) {
|
|
2510
|
+
slot.textContent = 'Image previews could not load. ';
|
|
2511
|
+
var retry = document.createElement('button'); retry.type = 'button'; retry.className = 'link-btn'; retry.textContent = 'Retry images';
|
|
2512
|
+
retry.addEventListener('click', function () { readerImagesMount(host, r); }); slot.appendChild(retry);
|
|
2513
|
+
});
|
|
2514
|
+
});
|
|
2515
|
+
}
|
|
2516
|
+
|
|
2017
2517
|
// Copy session (Control's Kickstart brief, HelperSources/main.swift claudeKickstartCopy). Control
|
|
2018
2518
|
// builds it from the Mac's transcript; the web builds it from the turns the server serves and
|
|
2019
2519
|
// the session's first prompt, so it keeps the original request and the newest context.
|
|
@@ -2146,7 +2646,7 @@
|
|
|
2146
2646
|
// One turn's identity, for telling which turns a refresh brought that the page already shows.
|
|
2147
2647
|
function sxTurnSig(t) {
|
|
2148
2648
|
var text = String(t.text || t.content || '').trim();
|
|
2149
|
-
return (String(t.role || '').toLowerCase() === 'user' ? 'u' : 'a') + '|' + (t.at || '') + '|' + text.length + '|' + text.slice(0, 48);
|
|
2649
|
+
return (String(t.role || '').toLowerCase() === 'user' ? 'u' : 'a') + '|' + (t.at || '') + '|' + text.length + '|' + text.slice(0, 48) + '|' + readerImagesHtml(t);
|
|
2150
2650
|
}
|
|
2151
2651
|
function sxTurnHtml(t) {
|
|
2152
2652
|
var role = String(t.role || '').toLowerCase() === 'user' ? 'user' : 'assistant';
|
|
@@ -2155,7 +2655,7 @@
|
|
|
2155
2655
|
var when = at ? (Date.now() - at.getTime() < 86400000 ? clock(at) : sxShortDate(at)) : '';
|
|
2156
2656
|
return '<div class="turn ' + role + ' sx-turn" data-sig="' + esc(sxTurnSig(t)) + '"><span class="who"><span>' + (role === 'user' ? 'YOU' : 'ASSISTANT') + '</span>' +
|
|
2157
2657
|
(when ? '<span class="sx-turn-when">' + esc(when) + '</span>' : '') + '</span>' +
|
|
2158
|
-
(role === 'user' ? '<div class="sx-plain">' + esc(text) + '</div>' : '<div class="sx-md">' + sxMd(text) + '</div>') + '</div>';
|
|
2658
|
+
(role === 'user' ? '<div class="sx-plain">' + esc(text) + '</div>' : '<div class="sx-md">' + sxMd(text) + '</div>' + readerImagesHtml(t)) + '</div>';
|
|
2159
2659
|
}
|
|
2160
2660
|
// A refresh never rebuilds what you are reading (Miles, /qa: "start to scroll and then it reloads
|
|
2161
2661
|
// the section"). The server's window slides as a session grows, so the turns the page already
|
|
@@ -2200,7 +2700,8 @@
|
|
|
2200
2700
|
return h.slice(0, 8) + '-' + h.slice(8, 12) + '-' + h.slice(12, 16) + '-' + h.slice(16, 20) + '-' + h.slice(20);
|
|
2201
2701
|
}
|
|
2202
2702
|
var TODAY_REFERENCE = 'What Today in COS Control shows right now';
|
|
2203
|
-
|
|
2703
|
+
// files: { attachmentIds, atts } from the Ask sheet's clip (askSheetFiles), or nothing.
|
|
2704
|
+
function askCos(prompt, statusEl, files) {
|
|
2204
2705
|
// 6.99.12: "it shows Sent but never shows up" (Miles, 11/04). With Ask COS in Messages, the sheet hands its words to
|
|
2205
2706
|
// that composer (panes/cos-chat.js submitAsk), so the answer streams on screen and lands as a numbered turn. The
|
|
2206
2707
|
// request below is the fallback for a page without it; it resolves to the Messages route to open.
|
|
@@ -2209,17 +2710,27 @@
|
|
|
2209
2710
|
// quoted reference, the query-jobs field for context. Asked from any other pane, nothing extra is sent.
|
|
2210
2711
|
var context = state.pane === 'today' ? ensurePane('todayActions', 1500).then(function (m) {
|
|
2211
2712
|
// A broken summary never costs the Ask: it goes without it, and the console says why.
|
|
2212
|
-
|
|
2713
|
+
var ctx;
|
|
2714
|
+
try { ctx = m && m.summary ? m.summary() : ''; } catch (err) { console.error('[today] the Today summary failed; asking without it.', err); return ''; }
|
|
2715
|
+
// The model's summary of the sessions leads it when there is one, inside the same cap; past the cap it is left out.
|
|
2716
|
+
var b = typeof briefShown === 'function' ? briefShown() : null;
|
|
2717
|
+
var lead = b ? 'Summary of the sessions, written by a model ' + (briefMinutes(b.at) < 1 ? 'just now' : span(b.at) + ' ago') + (b.state === 'stale' ? ' (things have moved since)' : '') + ': ' + b.text : '';
|
|
2718
|
+
return ctx && lead && lead.length + 1 + ctx.length <= ((m && m.summaryMax) || 4000) ? lead + '\n' + ctx : ctx;
|
|
2213
2719
|
}, function () { return ''; }) : Promise.resolve('');
|
|
2214
2720
|
if (chat && chat.askFromSheet) {
|
|
2215
2721
|
setStatus(statusEl, 'Sending…');
|
|
2216
|
-
return context.then(function (ctx) {
|
|
2722
|
+
return context.then(function (ctx) {
|
|
2723
|
+
var extra = files ? { attachmentIds: files.attachmentIds, atts: files.atts } : {};
|
|
2724
|
+
if (ctx) extra.context = ctx;
|
|
2725
|
+
return chat.askFromSheet(prompt, ctx || files ? extra : undefined);
|
|
2726
|
+
}).then(function (where) { setStatus(statusEl, ''); return where; }, function (err) {
|
|
2217
2727
|
setStatus(statusEl, err.message, true);
|
|
2218
2728
|
throw err;
|
|
2219
2729
|
});
|
|
2220
2730
|
}
|
|
2221
2731
|
setStatus(statusEl, 'Sent. COS is answering.');
|
|
2222
2732
|
var body = { clientJobId: webJobId(), generation: 1, query: prompt, source: 'web', activityToolMode: 'status' };
|
|
2733
|
+
if (files && files.attachmentIds && files.attachmentIds.length) body.attachmentIds = files.attachmentIds;
|
|
2223
2734
|
return context.then(function (ctx) {
|
|
2224
2735
|
if (ctx) body.reference = { query: TODAY_REFERENCE, response: ctx };
|
|
2225
2736
|
return api('/api/query-jobs', { method: 'POST', body: body, timeoutMs: 30000 });
|
|
@@ -3901,12 +4412,87 @@
|
|
|
3901
4412
|
var q = ($('ask-q') && $('ask-q').value.trim()) || '';
|
|
3902
4413
|
g.innerHTML = esc(ASK_FILE.busy ? 'Filing\u2026' : 'File to Work inbox') + fileNoteHtml(q);
|
|
3903
4414
|
}
|
|
4415
|
+
// ── files with Ask COS (11/07, Miles: "Sometimes I need to pass context via a screenshot, photo or file") ──
|
|
4416
|
+
// The COS chat's own clip, chips, limits and upload (panes/cos-chat.js, W.cosChat.attach): photos, PDF, TXT, MD, CSV,
|
|
4417
|
+
// JSON, XLSX, MP4 and MOV, up to 5, pasted, dropped or picked. They go with Ask COS only; the sheet's other rows take
|
|
4418
|
+
// words. Ask COS waits for every upload, and a file that failed has to be retried or removed first. Closing the sheet
|
|
4419
|
+
// keeps them for the next Ask in this tab.
|
|
4420
|
+
var ASK_FILES = null, ASK_SENDING = false;
|
|
4421
|
+
function askKit() { var c = window.COSWeb && window.COSWeb.cosChat; return c && c.attach ? c.attach : null; }
|
|
4422
|
+
function askFiles() {
|
|
4423
|
+
var kit = askKit(); if (!kit) return null;
|
|
4424
|
+
if (!ASK_FILES) ASK_FILES = kit.box({
|
|
4425
|
+
changed: paintAskFiles,
|
|
4426
|
+
removed: function () { setStatus($('ask-status'), ''); },
|
|
4427
|
+
signedOut: function () { setStatus($('ask-status'), 'This browser was signed out. Your files are kept. Sign in, then tap Retry on each file.', true); }
|
|
4428
|
+
});
|
|
4429
|
+
return ASK_FILES;
|
|
4430
|
+
}
|
|
4431
|
+
function askFilesState() {
|
|
4432
|
+
var kit = askKit(), b = ASK_FILES;
|
|
4433
|
+
if (!kit || !b || !b.atts.length) return 'none';
|
|
4434
|
+
return kit.pending(b) ? 'waiting' : kit.failed(b) ? 'failed' : 'ready';
|
|
4435
|
+
}
|
|
4436
|
+
function paintAskClip() {
|
|
4437
|
+
var slot = $('ask-clip'), kit = askKit(); if (!slot) return;
|
|
4438
|
+
var on = !!(kit && kit.askOn());
|
|
4439
|
+
if (!on) { slot.innerHTML = ''; return; }
|
|
4440
|
+
// 6.99.27: Take photo beside the clip (a phone or tablet only; kit.cameraHtml).
|
|
4441
|
+
if (!slot.querySelector('[data-ask-file]')) slot.innerHTML = kit.cameraHtml('data-ask-file') + kit.clipHtml('data-ask-file');
|
|
4442
|
+
var full = !!(ASK_FILES && ASK_FILES.atts.length >= kit.max);
|
|
4443
|
+
Array.prototype.forEach.call(slot.querySelectorAll ? slot.querySelectorAll('.cc-attach') : [], function (l) { l.classList.toggle('full', full); });
|
|
4444
|
+
Array.prototype.forEach.call(slot.querySelectorAll ? slot.querySelectorAll('[data-ask-file]') : [], function (i) { i.disabled = full || ASK_SENDING; });
|
|
4445
|
+
}
|
|
4446
|
+
function paintAskFiles() {
|
|
4447
|
+
var kit = askKit(), box = $('ask-files'), b = ASK_FILES; if (!box || !kit) return;
|
|
4448
|
+
var st = askFilesState();
|
|
4449
|
+
box.innerHTML = st === 'none' ? '' : kit.chipsHtml(b, 'data-action') + '<p class="ask-files-note">' + esc(
|
|
4450
|
+
st === 'waiting' ? 'Uploading. Ask COS waits for it.' : st === 'failed' ? 'A file did not upload. Retry it or remove it, then Ask COS.'
|
|
4451
|
+
: (b.atts.length === 1 ? 'This file goes' : 'These files go') + ' with Ask COS. Write the question above.') + '</p>';
|
|
4452
|
+
box.classList.toggle('hidden', st === 'none');
|
|
4453
|
+
paintAskClip(); paintAskRow();
|
|
4454
|
+
}
|
|
4455
|
+
function addAskFiles(list) {
|
|
4456
|
+
var kit = askKit(), b = askFiles(); if (!kit || !b || !list || !list.length) return;
|
|
4457
|
+
var refusals = kit.add(b, list);
|
|
4458
|
+
setStatus($('ask-status'), refusals.join(' '), !!refusals.length);
|
|
4459
|
+
paintAskFiles();
|
|
4460
|
+
}
|
|
4461
|
+
// The Ask COS row says what it sends, and waits while a file uploads.
|
|
4462
|
+
function askRowNote(q) {
|
|
4463
|
+
var n = ASK_FILES ? ASK_FILES.atts.length : 0;
|
|
4464
|
+
// The files first: on the folded phone the line is cut, and they are what changed.
|
|
4465
|
+
return '“' + q + '”' + (n ? ' with ' + (n === 1 ? '1 file' : n + ' files') : '') + (state.pane === 'today' ? (n ? ' and' : ' with') + ' what Today shows' : '');
|
|
4466
|
+
}
|
|
4467
|
+
function paintAskRow() {
|
|
4468
|
+
var row = document.querySelector('#ask-results [data-action="ask-cos"]'); if (!row) return;
|
|
4469
|
+
var st = askFilesState();
|
|
4470
|
+
row.disabled = ASK_SENDING || st === 'waiting' || st === 'failed';
|
|
4471
|
+
var note = row.querySelector('.note'), q = ($('ask-q') && $('ask-q').value.trim()) || '';
|
|
4472
|
+
if (note) note.textContent = ASK_SENDING ? 'Sending…' : st === 'waiting' ? 'Waiting for the upload' : askRowNote(q);
|
|
4473
|
+
}
|
|
4474
|
+
function askSheetSend(q) {
|
|
4475
|
+
if (ASK_SENDING) return;
|
|
4476
|
+
var kit = askKit(), b = ASK_FILES, st = askFilesState();
|
|
4477
|
+
if (st === 'waiting') { setStatus($('ask-status'), 'A file is still uploading. Ask COS when it finishes.'); return; }
|
|
4478
|
+
if (st === 'failed') { setStatus($('ask-status'), 'A file did not upload. Retry it or remove it, then Ask COS.', true); return; }
|
|
4479
|
+
var files = st === 'ready' ? { attachmentIds: kit.ids(b), atts: kit.sent(b) } : null;
|
|
4480
|
+
ASK_SENDING = true; paintAskRow(); paintAskClip();
|
|
4481
|
+
var done = function () { ASK_SENDING = false; paintAskRow(); paintAskClip(); };
|
|
4482
|
+
askCos(q, $('ask-status'), files).then(function (where) {
|
|
4483
|
+
done();
|
|
4484
|
+
// Sent: these files went with it. Anything added while it was sending stays for the next Ask.
|
|
4485
|
+
if (files && b) b.atts.filter(function (t) { return files.attachmentIds.indexOf(t.mediaId) >= 0; }).forEach(function (t) { kit.remove(b, t.id); });
|
|
4486
|
+
closeAsk(); go('messages', Array.isArray(where) ? where : []);
|
|
4487
|
+
}).catch(done);
|
|
4488
|
+
}
|
|
3904
4489
|
function openAsk(prefill) {
|
|
3905
4490
|
loadFileDomains().then(function () { if (!$('ask-sheet').classList.contains('hidden') && $('ask-q').value.trim()) renderAsk(); });
|
|
3906
4491
|
$('ask-sheet').classList.remove('hidden');
|
|
3907
4492
|
document.body.style.overflow = 'hidden';
|
|
3908
4493
|
var input = $('ask-q'); input.value = prefill || '';
|
|
3909
|
-
renderAsk();
|
|
4494
|
+
renderAsk(); paintAskFiles(); paintAskClip();
|
|
4495
|
+
var kit = askKit(); if (kit && !kit.askOn()) kit.ensureCaps().then(paintAskClip, function () {});
|
|
3910
4496
|
setTimeout(function () { input.focus(); }, 30);
|
|
3911
4497
|
}
|
|
3912
4498
|
function closeAsk() { $('ask-sheet').classList.add('hidden'); document.body.style.overflow = ''; }
|
|
@@ -3923,12 +4509,13 @@
|
|
|
3923
4509
|
}
|
|
3924
4510
|
var jumps = JUMPS.filter(function (j) { return j[1].toLowerCase().indexOf(q.toLowerCase()) === 0; });
|
|
3925
4511
|
out.innerHTML = '<div class="list" style="margin-top:14px">' +
|
|
3926
|
-
'<button type="button" class="nav-row" data-action="ask-cos"><svg class="icon"><use href="#i-ask"/></svg><span class="grow">Ask COS<span class="note"
|
|
4512
|
+
'<button type="button" class="nav-row" data-action="ask-cos"><svg class="icon"><use href="#i-ask"/></svg><span class="grow">Ask COS<span class="note">' + esc(askRowNote(q)) + '</span></span></button>' +
|
|
3927
4513
|
'<button type="button" class="nav-row" data-action="ask-file"' + (ASK_FILE.busy ? ' disabled' : '') + '><svg class="icon"><use href="#i-work"/></svg><span class="grow">' + (ASK_FILE.busy ? 'Filing\u2026' : 'File to Work inbox') + fileNoteHtml(q) + '</span></button>' +
|
|
3928
4514
|
'<button type="button" class="nav-row" data-action="ask-meaning"><svg class="icon"><use href="#i-sessions"/></svg><span class="grow">Find a session about this<span class="note">Ranks every session by meaning</span></span></button>' +
|
|
3929
4515
|
'<button type="button" class="nav-row" data-action="ask-work"><svg class="icon"><use href="#i-work"/></svg><span class="grow">Find a task about this<span class="note">Searches Work by meaning</span></span></button>' +
|
|
3930
4516
|
jumps.map(function (j) { return '<button type="button" class="nav-row" data-goto="' + j[0] + '"><svg class="icon"><use href="#i-' + j[0] + '"/></svg><span class="grow">Go to ' + esc(j[1]) + '</span><span class="chev"></span></button>'; }).join('') +
|
|
3931
4517
|
'</div><div id="ask-hits"></div><p class="status-line" id="ask-status"></p>';
|
|
4518
|
+
paintAskRow();
|
|
3932
4519
|
clearTimeout(renderAsk.t);
|
|
3933
4520
|
if (q.length >= 2) renderAsk.t = setTimeout(function () {
|
|
3934
4521
|
api('/api/agent-sessions/search?q=' + encodeURIComponent(q) + '&limit=6').then(function (d) {
|
|
@@ -3972,7 +4559,7 @@
|
|
|
3972
4559
|
return;
|
|
3973
4560
|
}
|
|
3974
4561
|
if (t.hasAttribute('data-open-ask')) return openAsk();
|
|
3975
|
-
if (t.hasAttribute('data-today')) return openTodayAction(t);
|
|
4562
|
+
if (t.hasAttribute('data-today')) return t.getAttribute('data-today') === 'pet' ? petTap(t) : openTodayAction(t);
|
|
3976
4563
|
if (t.hasAttribute('data-today-next')) return prepNext();
|
|
3977
4564
|
if (t.hasAttribute('data-today-needs')) {
|
|
3978
4565
|
var which = t.getAttribute('data-today-needs');
|
|
@@ -4004,7 +4591,18 @@
|
|
|
4004
4591
|
}
|
|
4005
4592
|
if (action === 'expand-turn') { var tr = document.querySelector('.turn[data-turn="' + t.getAttribute('data-turn') + '"]'); if (tr) tr.classList.remove('clamped'); t.remove(); return; }
|
|
4006
4593
|
var q = $('ask-q').value.trim();
|
|
4007
|
-
if (action === '
|
|
4594
|
+
if ((action === 'att-remove' || action === 'att-retry') && t.closest('#session-form') && state.reading) {
|
|
4595
|
+
var kitR = readerKit(), boxR = kitR && readerBox(state.reading); if (!boxR) return;
|
|
4596
|
+
sessionStatus('');
|
|
4597
|
+
if (action === 'att-remove') kitR.remove(boxR, t.getAttribute('data-att')); else kitR.retry(boxR, t.getAttribute('data-att'));
|
|
4598
|
+
return;
|
|
4599
|
+
}
|
|
4600
|
+
if (inSheet && (action === 'att-remove' || action === 'att-retry') && ASK_FILES) {
|
|
4601
|
+
var kitA = askKit(); setStatus($('ask-status'), '');
|
|
4602
|
+
if (action === 'att-remove') kitA.remove(ASK_FILES, t.getAttribute('data-att')); else kitA.retry(ASK_FILES, t.getAttribute('data-att'));
|
|
4603
|
+
return;
|
|
4604
|
+
}
|
|
4605
|
+
if (action === 'ask-cos' && q) return askSheetSend(q);
|
|
4008
4606
|
if (action === 'ask-file' && q) return fileToInbox(q);
|
|
4009
4607
|
if (action === 'ask-meaning' && q) { $('ask-hits').innerHTML = skeletonRows(3); findByMeaning(q).then(function (html) { var el = $('ask-hits'); if (el) el.innerHTML = html; }); return; }
|
|
4010
4608
|
if (action === 'ask-work' && q) {
|
|
@@ -4035,12 +4633,43 @@
|
|
|
4035
4633
|
}
|
|
4036
4634
|
});
|
|
4037
4635
|
|
|
4636
|
+
// The Ask sheet's clip, a pasted screenshot in its box, and files dropped on it (askFiles above).
|
|
4637
|
+
document.addEventListener('change', function (e) {
|
|
4638
|
+
if (e.target && e.target.hasAttribute && e.target.hasAttribute('data-ask-file')) { addAskFiles(e.target.files); e.target.value = ''; }
|
|
4639
|
+
if (e.target && e.target.hasAttribute && e.target.hasAttribute('data-sx-file')) { addReaderFiles(e.target.files); e.target.value = ''; }
|
|
4640
|
+
});
|
|
4641
|
+
// The Sessions reader's box: a pasted screenshot, and files dropped on it.
|
|
4642
|
+
document.addEventListener('paste', function (e) {
|
|
4643
|
+
if (!e.target || e.target.id !== 'session-input') return;
|
|
4644
|
+
var kit = readerKit(), files = kit && readerFilesOn() ? kit.pasted(e) : null;
|
|
4645
|
+
if (files) { e.preventDefault(); addReaderFiles(files); }
|
|
4646
|
+
});
|
|
4647
|
+
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(function (type) {
|
|
4648
|
+
document.addEventListener(type, function (e) {
|
|
4649
|
+
var form = e.target && e.target.closest ? e.target.closest('#session-form') : null, kit = readerKit();
|
|
4650
|
+
if (!form || !kit || !readerFilesOn()) return;
|
|
4651
|
+
var files = kit.dragged(e, form); if (files) addReaderFiles(files);
|
|
4652
|
+
});
|
|
4653
|
+
});
|
|
4654
|
+
document.addEventListener('paste', function (e) {
|
|
4655
|
+
if (!e.target || e.target.id !== 'ask-q') return;
|
|
4656
|
+
var kit = askKit(), files = kit && kit.askOn() ? kit.pasted(e) : null;
|
|
4657
|
+
if (files) { e.preventDefault(); addAskFiles(files); }
|
|
4658
|
+
});
|
|
4659
|
+
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(function (type) {
|
|
4660
|
+
var sheet = $('ask-sheet'); if (!sheet) return;
|
|
4661
|
+
sheet.addEventListener(type, function (e) {
|
|
4662
|
+
var kit = askKit(), zone = sheet.querySelector('.sheet'); if (!kit || !zone || !kit.askOn()) return;
|
|
4663
|
+
var files = kit.dragged(e, zone); if (files) addAskFiles(files);
|
|
4664
|
+
});
|
|
4665
|
+
});
|
|
4038
4666
|
document.addEventListener('input', function (e) {
|
|
4039
4667
|
if (e.target.tagName === 'TEXTAREA') autoGrow(e.target);
|
|
4040
4668
|
if (e.target.id === 'ask-q') renderAsk();
|
|
4041
4669
|
if (e.target.id === 'sessions-q' && !e.target.value.trim()) renderSessionGroups(state.sessions);
|
|
4042
4670
|
});
|
|
4043
4671
|
document.addEventListener('keydown', function (e) {
|
|
4672
|
+
if (window.COSWeb.readerMedia && window.COSWeb.readerMedia.isOpen()) return;
|
|
4044
4673
|
if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === 'k') { e.preventDefault(); openAsk(); return; }
|
|
4045
4674
|
if (e.key === 'Escape' && !$('ask-sheet').classList.contains('hidden')) { closeAsk(); return; }
|
|
4046
4675
|
if (e.key === 'Escape' && !$('section-menu').classList.contains('hidden')) { closeMenu(); $('section-btn').focus(); return; }
|
|
@@ -4106,6 +4735,7 @@
|
|
|
4106
4735
|
sessionList: sessionList,
|
|
4107
4736
|
petPrefs: petPrefs,
|
|
4108
4737
|
setPetPrefs: setPetPrefs,
|
|
4738
|
+
resetTodayBrief: resetBrief,
|
|
4109
4739
|
onRoute: function (fn) { routeHooks.push(fn); },
|
|
4110
4740
|
onShell: function (fn) { shellHooks.push(fn); },
|
|
4111
4741
|
// The detail title after "COS Control > Section" (Control's breadcrumb). Pane modules set it
|
|
@@ -4139,7 +4769,11 @@
|
|
|
4139
4769
|
railLine: function () { return TODAY.model ? { text: todayRailLine(TODAY.model), hot: TODAY.model.now.length > 0 } : null; },
|
|
4140
4770
|
markGone: function (id, gone) { if (gone) TODAY.gone[id] = 1; else delete TODAY.gone[id]; paintTodayIntake(); },
|
|
4141
4771
|
reloadIntake: function () { return loadTodayIntake(); },
|
|
4142
|
-
refresh: function () { if (state.pane === 'today') loadToday(); }
|
|
4772
|
+
refresh: function () { if (state.pane === 'today') loadToday(); },
|
|
4773
|
+
// 6.99.26: the pet's sessions (petPlay), and a repaint of Needs you when the held questions change.
|
|
4774
|
+
petPlay: function () { return petPlay(state.sessions || []); },
|
|
4775
|
+
// 6.99.27: the pet's caption and pose follow the held questions too.
|
|
4776
|
+
questionsChanged: function () { if (TODAY.at) { paintNeeds(); if (state.pane === 'today' && !TODAY.offline) renderPet(state.sessions || []); } }
|
|
4143
4777
|
};
|
|
4144
4778
|
window.COSWeb.sessionKit = {
|
|
4145
4779
|
// A session object for continueSession, with the overlay's own view (READER_VIEW's methods).
|
|
@@ -4150,7 +4784,9 @@
|
|
|
4150
4784
|
newKey: newTurnKey,
|
|
4151
4785
|
probe: function (r) { return sxProbeAttach(r); },
|
|
4152
4786
|
path: sessionPath, answer: apiAnswer,
|
|
4153
|
-
|
|
4787
|
+
imageHtml: readerImagesHtml, imagesMount: readerImagesMount, imagesStop: readerImagesStop,
|
|
4788
|
+
feed: { fresh: newFeed, apply: applyFeed, word: sxFeedWord, detail: sxFeedDetail, tool: sxToolLine, elapsed: sxElapsed, fallback: sxFallbackLine,
|
|
4789
|
+
truth: sxTruth, settle: sxSettle, noteList: sxNoteList, noteGate: sxNoteGate, gateStale: sxGateStale, promptLine: sxPromptLine, parkNoteShows: sxParkNoteShows },
|
|
4154
4790
|
title: sxTitle, stateOf: sxState, stateLabel: function (s) { return sxStateLabel(s, sxState(s)); },
|
|
4155
4791
|
parkCopy: function (p) { return SX_PARK[sxProvider(p)]; }, providerLabel: sxProviderLabel, markdown: sxMd
|
|
4156
4792
|
};
|