@gotcos/glasses-server 6.99.20 → 6.99.25
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 +91 -0
- package/docs/work-orchestrator.md +20 -0
- package/package.json +1 -1
- package/server/index.ts +2 -1
- package/server/lib/web-control-static.ts +226 -0
- package/server/lib/web-memories-bridge.ts +9 -8
- package/server/lib/web-pet-choice.ts +225 -0
- package/server/lib/web-pet.ts +226 -0
- package/server/lib/work-lens-rounds.ts +29 -4
- package/server/routes/web-control.ts +110 -7
- package/server/routes/work-lens-rounds.ts +19 -5
- package/server/web-control/app.css +24 -5
- package/server/web-control/app.js +646 -108
- package/server/web-control/control.css +1 -1
- package/server/web-control/index.html +27 -13
- package/server/web-control/memories/memory-workspace.js +1 -1
- package/server/web-control/panes/cos-chat.css +4 -0
- package/server/web-control/panes/cos-chat.js +26 -4
- package/server/web-control/panes/meetings.css +15 -2
- package/server/web-control/panes/memories.css +291 -0
- package/server/web-control/panes/memories.js +2208 -0
- package/server/web-control/panes/questions.js +59 -21
- package/server/web-control/panes/settings.js +102 -4
- package/server/web-control/panes/today-actions.css +109 -0
- package/server/web-control/panes/today-actions.js +972 -0
- package/server/web-control/panes/work.css +54 -0
- package/server/web-control/panes/work.js +1037 -18
- package/server/web-control/shell.js +6 -2
- package/server/web-control/theme.js +0 -2
- package/server/web-control/memories/memories-app.js +0 -1561
- package/server/web-control/memories/memories-theme.css +0 -73
- package/server/web-control/memories/memories.html +0 -72
- package/server/web-control/memories/memory-stewardship.js +0 -63
|
@@ -133,7 +133,7 @@
|
|
|
133
133
|
var state = {
|
|
134
134
|
pane: 'today', serverName: '', tailnetUrl: '',
|
|
135
135
|
sessions: [], sessionsMode: 'words', workTasks: [], workOpenStages: { complete: false },
|
|
136
|
-
stream: null,
|
|
136
|
+
stream: null, lastLoad: {}, livePoll: null
|
|
137
137
|
};
|
|
138
138
|
// Control's order (ActivitySection.visibleSections): Today, then Messages to Dictation. Usage is a
|
|
139
139
|
// view inside Sessions on the Mac; its route stays so old links work, and the rail marks Sessions.
|
|
@@ -142,6 +142,53 @@
|
|
|
142
142
|
// Pane modules (panes/*.js) register here: { load(args, ctx), leave(), up(args) }. A registered
|
|
143
143
|
// pane owns its section; route() only shows it and hands it the hash arguments.
|
|
144
144
|
var PANES = {};
|
|
145
|
+
// Heavy panes load when first opened, then the rest load quietly once the page is idle.
|
|
146
|
+
// index.html lists them in #lazy-panes so the server can version each URL (?v=<hash>).
|
|
147
|
+
// Read on first use, not at parse time: #lazy-panes sits after this script in index.html.
|
|
148
|
+
var lazyList = null;
|
|
149
|
+
function LAZY() {
|
|
150
|
+
if (lazyList) return lazyList;
|
|
151
|
+
var el = document.getElementById('lazy-panes');
|
|
152
|
+
if (!el) return {};
|
|
153
|
+
try { lazyList = JSON.parse(el.textContent || '{}'); } catch (_e) { lazyList = {}; }
|
|
154
|
+
return lazyList;
|
|
155
|
+
}
|
|
156
|
+
var lazyLoads = {};
|
|
157
|
+
// ensurePane(name, waitMs): the pane's script, loaded once. With waitMs, this caller stops waiting after that
|
|
158
|
+
// long (the load itself carries on, and a later call reuses it), so a stalled download can never hang a tap.
|
|
159
|
+
function ensurePane(name, waitMs) {
|
|
160
|
+
var load = loadPane(name);
|
|
161
|
+
if (!waitMs) return load;
|
|
162
|
+
return new Promise(function (resolve, reject) {
|
|
163
|
+
var timer = setTimeout(function () {
|
|
164
|
+
var late = new Error('The ' + (TITLES[name] || name) + ' section is taking too long to load. Check the connection to your Mac.');
|
|
165
|
+
late.code = 'pane_timeout'; reject(late);
|
|
166
|
+
}, waitMs);
|
|
167
|
+
load.then(function (v) { clearTimeout(timer); resolve(v); }, function (e) { clearTimeout(timer); reject(e); });
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
function loadPane(name) {
|
|
171
|
+
if (PANES[name] || !LAZY()[name]) return Promise.resolve(PANES[name] || null);
|
|
172
|
+
if (lazyLoads[name]) return lazyLoads[name];
|
|
173
|
+
lazyLoads[name] = new Promise(function (resolve, reject) {
|
|
174
|
+
var s = document.createElement('script');
|
|
175
|
+
s.src = LAZY()[name];
|
|
176
|
+
s.async = true;
|
|
177
|
+
s.onload = function () { if (PANES[name]) resolve(PANES[name]); else { delete lazyLoads[name]; reject(new Error('The ' + (TITLES[name] || name) + ' section did not start.')); } };
|
|
178
|
+
s.onerror = function () { delete lazyLoads[name]; s.remove(); reject(new Error('Could not load the ' + (TITLES[name] || name) + ' section. Check the connection to your Mac.')); };
|
|
179
|
+
document.body.appendChild(s);
|
|
180
|
+
});
|
|
181
|
+
return lazyLoads[name];
|
|
182
|
+
}
|
|
183
|
+
function prefetchPanes() {
|
|
184
|
+
var names = Object.keys(LAZY()).filter(function (n) { return !PANES[n]; });
|
|
185
|
+
var idle = window.requestIdleCallback || function (fn) { return setTimeout(fn, 200); };
|
|
186
|
+
(function next() {
|
|
187
|
+
var n = names.shift();
|
|
188
|
+
if (!n) return;
|
|
189
|
+
idle(function () { ensurePane(n).then(next, next); });
|
|
190
|
+
})();
|
|
191
|
+
}
|
|
145
192
|
var routeHooks = [];
|
|
146
193
|
var shellHooks = [];
|
|
147
194
|
var crumbDetail = '';
|
|
@@ -157,9 +204,9 @@
|
|
|
157
204
|
if (location.hash === hash) route(); else location.hash = hash;
|
|
158
205
|
}
|
|
159
206
|
function isPhone() { return !window.matchMedia('(min-width: 600px)').matches; }
|
|
160
|
-
function route() {
|
|
207
|
+
function route(forceChanged) {
|
|
161
208
|
var r = parseRoute();
|
|
162
|
-
var changedPane = r.pane !== state.pane || !document.querySelector('#pane-' + r.pane + ':not(.hidden)');
|
|
209
|
+
var changedPane = forceChanged === true || r.pane !== state.pane || !document.querySelector('#pane-' + r.pane + ':not(.hidden)');
|
|
163
210
|
if (state.pane !== r.pane && PANES[state.pane] && PANES[state.pane].leave) { try { PANES[state.pane].leave(); } catch (err) { console.error(err); } }
|
|
164
211
|
state.pane = r.pane;
|
|
165
212
|
crumbDetail = '';
|
|
@@ -171,7 +218,7 @@
|
|
|
171
218
|
if (!(r.pane === 'sessions' && r.args.length === 2)) { clearInterval(state.turnPoll); if (state.reading) clearInterval(state.reading.heldTimer); state.reading = null; }
|
|
172
219
|
if (r.pane !== 'meetings' && state.livePoll) { clearInterval(state.livePoll); state.livePoll = null; }
|
|
173
220
|
|
|
174
|
-
var reading = r.args.length > 0;
|
|
221
|
+
var reading = r.args.length > 0 && !isTabRoute(r.args);
|
|
175
222
|
var paneEl = $('pane-' + r.pane);
|
|
176
223
|
if (paneEl) paneEl.classList.toggle('reading', reading);
|
|
177
224
|
var split = paneEl && paneEl.querySelector('.split');
|
|
@@ -187,10 +234,31 @@
|
|
|
187
234
|
$('topbar').classList.remove('tucked');
|
|
188
235
|
onScroll();
|
|
189
236
|
|
|
237
|
+
if (!PANES[r.pane] && LAZY()[r.pane]) {
|
|
238
|
+
var lazyPane = r.pane, lazyEl = $('pane-' + lazyPane), wasEmpty = lazyEl && !lazyEl.getAttribute('data-lazy-ready');
|
|
239
|
+
if (wasEmpty && lazyEl && !lazyEl.querySelector('.lazy-wait')) lazyEl.insertAdjacentHTML('afterbegin', '<div class="lazy-wait">' + skeletonRows(4) + '</div>');
|
|
240
|
+
ensurePane(lazyPane, 15000).then(function () {
|
|
241
|
+
if (lazyEl) { lazyEl.setAttribute('data-lazy-ready', '1'); lazyEl.querySelectorAll('.lazy-wait').forEach(function (n) { n.remove(); }); }
|
|
242
|
+
// Pass changed=true, the value this pane got on its first open when it loaded at boot. Headless Chrome
|
|
243
|
+
// probes on 11/06 rendered Work, Meetings, Settings and Speakers with either value; this keeps the old contract.
|
|
244
|
+
if (state.pane === lazyPane) route(true);
|
|
245
|
+
}, function (err) {
|
|
246
|
+
if (!lazyEl) return;
|
|
247
|
+
lazyEl.querySelectorAll('.lazy-wait').forEach(function (n) { n.remove(); });
|
|
248
|
+
lazyEl.insertAdjacentHTML('afterbegin', '<div class="lazy-wait"><div class="list"><p class="empty"><strong>' + esc(err.message) + '</strong></p><button type="button" class="btn" data-lazy-retry="' + esc(lazyPane) + '">Try again</button></div></div>');
|
|
249
|
+
// Only the wait ran out: if the load still finishes, the card clears itself and the pane shows.
|
|
250
|
+
if (err && err.code === 'pane_timeout') loadPane(lazyPane).then(function () {
|
|
251
|
+
lazyEl.querySelectorAll('.lazy-wait').forEach(function (n) { n.remove(); });
|
|
252
|
+
lazyEl.setAttribute('data-lazy-ready', '1');
|
|
253
|
+
if (state.pane === lazyPane) route(true);
|
|
254
|
+
}, function () {});
|
|
255
|
+
});
|
|
256
|
+
runHooks(routeHooks, r);
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
190
259
|
if (PANES[r.pane]) { try { PANES[r.pane].load(r.args, { changed: changedPane }); } catch (err) { console.error(err); } }
|
|
191
260
|
else if (r.pane === 'today') loadToday();
|
|
192
261
|
else if (r.pane === 'sessions') { if (!reading || !state.sessions.length || changedPane) loadSessions(); if (reading) openSession(r.args[0], r.args[1]); }
|
|
193
|
-
else if (r.pane === 'memories') loadMemories();
|
|
194
262
|
else if (r.pane === 'usage') loadUsage(false);
|
|
195
263
|
state.lastLoad[r.pane] = Date.now();
|
|
196
264
|
runHooks(routeHooks, r);
|
|
@@ -202,9 +270,12 @@
|
|
|
202
270
|
var r = parseRoute();
|
|
203
271
|
var up = PANES[r.pane] && PANES[r.pane].up ? PANES[r.pane].up(r.args) : null;
|
|
204
272
|
if (up) go(r.pane, up);
|
|
205
|
-
else if (r.args.length) go(r.pane);
|
|
273
|
+
else if (r.args.length && !isTabRoute(r.args)) go(r.pane);
|
|
206
274
|
else go('today');
|
|
207
275
|
}
|
|
276
|
+
// '#/<pane>/t/<tab>' is the pane's list on one of its tabs, not a reader: the phone keeps the section switcher and Back
|
|
277
|
+
// leaves the pane. Memories keeps its tab in the address this way, so a reload or a new browser lands on the same tab.
|
|
278
|
+
function isTabRoute(args) { return args.length === 2 && args[0] === 't'; }
|
|
208
279
|
// On the closed Duo every vertical pixel counts: the top bar slides away while you scroll
|
|
209
280
|
// down and comes back the moment you scroll up.
|
|
210
281
|
var lastY = 0;
|
|
@@ -238,18 +309,209 @@
|
|
|
238
309
|
loadTailnetUrl();
|
|
239
310
|
runHooks(shellHooks, info);
|
|
240
311
|
route();
|
|
312
|
+
// Reloaded with Today's overlay open: its history entry is still current. The overlay opens the same thing again
|
|
313
|
+
// (panes/today-actions.js restore), so Back closes it; if it cannot load, the entry is unmarked.
|
|
314
|
+
var held = history.state;
|
|
315
|
+
if (held && held.cosTodayOverlay) {
|
|
316
|
+
ensurePane('todayActions', 4000).then(function (m) { if (m && m.restore) m.restore(held); },
|
|
317
|
+
function () { try { history.replaceState(null, ''); } catch (_e) {} });
|
|
318
|
+
}
|
|
319
|
+
setTimeout(prefetchPanes, 1500);
|
|
320
|
+
}
|
|
321
|
+
document.addEventListener('click', function (e) {
|
|
322
|
+
var b = e.target && e.target.closest && e.target.closest('[data-lazy-retry]');
|
|
323
|
+
if (!b) return;
|
|
324
|
+
var el = $('pane-' + b.getAttribute('data-lazy-retry'));
|
|
325
|
+
if (el) el.querySelectorAll('.lazy-wait').forEach(function (n) { n.remove(); });
|
|
326
|
+
route(true);
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
// ── one session list for every pane ────────────────────
|
|
330
|
+
// /api/agent-sessions walks every session folder and runs a 2-3 s lock probe on the Mac (measured
|
|
331
|
+
// 11/06 in the browser: 5.1 s on Sessions, 2.1 s on Work). Today, Sessions, Work and the section
|
|
332
|
+
// stories all asked for it separately. Now there is one copy: a request younger than maxAgeMs is
|
|
333
|
+
// answered from it, a request already on the wire is shared, and panes can paint the last copy at
|
|
334
|
+
// once while a fresh one loads (sessionListCached).
|
|
335
|
+
var SL = { data: null, at: 0, inflight: null };
|
|
336
|
+
function sessionList(maxAgeMs) {
|
|
337
|
+
if (SL.data && Date.now() - SL.at < (maxAgeMs || 0)) return Promise.resolve(SL.data);
|
|
338
|
+
if (SL.inflight) return SL.inflight;
|
|
339
|
+
SL.inflight = api('/api/agent-sessions?limit=80&sort=updated').then(function (d) {
|
|
340
|
+
SL.data = d; SL.at = Date.now(); SL.inflight = null; return d;
|
|
341
|
+
}, function (err) { SL.inflight = null; throw err; });
|
|
342
|
+
return SL.inflight;
|
|
343
|
+
}
|
|
344
|
+
function sessionListCached() { return SL.data ? { data: SL.data, at: SL.at } : null; }
|
|
345
|
+
|
|
346
|
+
// ── the session pet (Today) ─────────────────────────────
|
|
347
|
+
// Same pet and the same pose rule as COS Control (PetSpritePose.resolve): waiting with nothing running
|
|
348
|
+
// is "waiting", then one, two, three, four or more sessions in play are working, duel, trio, swarm, an open
|
|
349
|
+
// quiet session is "patrol", none is "idle". Strips come from /api/web-pet, resized and cut for a phone.
|
|
350
|
+
var PET = { manifest: null, fetchedAt: 0, pose: null, timer: null, frame: 0, token: 0 };
|
|
351
|
+
// This browser's pet settings (Settings > Pet): which pet, and whether Today shows it. Per browser, like the theme.
|
|
352
|
+
var PET_KEY = 'cos-web-pet';
|
|
353
|
+
function petPrefs() {
|
|
354
|
+
var p = readStore(PET_KEY);
|
|
355
|
+
var pet = typeof p.pet === 'string' && /^(mac|(shipped|gallery):[a-z0-9][a-z0-9-]{0,80})$/.test(p.pet) ? p.pet : 'mac';
|
|
356
|
+
return { pet: pet, show: p.show !== false };
|
|
357
|
+
}
|
|
358
|
+
function setPetPrefs(next) {
|
|
359
|
+
var cur = petPrefs();
|
|
360
|
+
var merged = { pet: next.pet != null ? next.pet : cur.pet, show: next.show != null ? !!next.show : cur.show };
|
|
361
|
+
writeStore(PET_KEY, merged);
|
|
362
|
+
// Forget the old pet's manifest and loop; Today repaints with the new choice next time it shows.
|
|
363
|
+
PET.manifest = null; PET.fetchedAt = 0; PET.pose = null; PET.token++; petStop();
|
|
364
|
+
var box = $('today-pet');
|
|
365
|
+
if (box) { box.innerHTML = ''; box.classList.add('hidden'); }
|
|
366
|
+
if (state.pane === 'today' && state.sessions) renderPet(state.sessions);
|
|
367
|
+
return merged;
|
|
368
|
+
}
|
|
369
|
+
var PET_HEIGHT = 140;
|
|
370
|
+
function petPose(sessions) {
|
|
371
|
+
var working = 0, waiting = 0, alive = 0;
|
|
372
|
+
sessions.forEach(function (s) {
|
|
373
|
+
var st = sxState(s);
|
|
374
|
+
// Working is the Running card's own test (running_active), so the caption under the pet never
|
|
375
|
+
// disagrees with the Running count beside it (11/06: the pet said 3 while the card said 2).
|
|
376
|
+
if (s.running_active) working++;
|
|
377
|
+
else if (st === 'waiting') waiting++;
|
|
378
|
+
if (s.running || s.running_active) alive++;
|
|
379
|
+
});
|
|
380
|
+
var pose;
|
|
381
|
+
if (waiting > 0 && working === 0) pose = 'waiting';
|
|
382
|
+
else if (working + waiting >= 4) pose = 'swarm';
|
|
383
|
+
else if (working + waiting === 3) pose = 'trio';
|
|
384
|
+
else if (working + waiting === 2) pose = 'duel';
|
|
385
|
+
else if (working + waiting === 1) pose = 'working';
|
|
386
|
+
else pose = alive > 0 ? 'patrol' : 'idle';
|
|
387
|
+
return { pose: pose, working: working, waiting: waiting };
|
|
388
|
+
}
|
|
389
|
+
function petCaption(c) {
|
|
390
|
+
var parts = [];
|
|
391
|
+
if (c.working) parts.push(c.working + ' running');
|
|
392
|
+
if (c.waiting) parts.push(c.waiting + ' waiting on you');
|
|
393
|
+
return parts.length ? parts.join(', ') : 'Nothing running';
|
|
394
|
+
}
|
|
395
|
+
function petStop() { clearTimeout(PET.timer); PET.timer = null; }
|
|
396
|
+
function petPaint(el, spec, frame, scale) {
|
|
397
|
+
var chunk = Math.floor(frame / spec.perChunk), inChunk = frame % spec.perChunk;
|
|
398
|
+
var count = Math.min(spec.perChunk, spec.frames - chunk * spec.perChunk);
|
|
399
|
+
el.style.backgroundImage = 'url("' + spec.chunks[chunk] + '")';
|
|
400
|
+
el.style.backgroundSize = Math.round(count * spec.frameWidth * scale) + 'px ' + PET_HEIGHT + 'px';
|
|
401
|
+
el.style.backgroundPosition = '-' + Math.round(inChunk * spec.frameWidth * scale) + 'px 0';
|
|
402
|
+
}
|
|
403
|
+
function petLoadImages(spec) {
|
|
404
|
+
return Promise.all(spec.chunks.map(function (url) {
|
|
405
|
+
return new Promise(function (resolve, reject) { var im = new Image(); im.onload = function () { resolve(im); }; im.onerror = reject; im.src = url; });
|
|
406
|
+
}));
|
|
407
|
+
}
|
|
408
|
+
// Folded (one column) the pet sits above the story. Unfolded (two columns, the same 760px line as
|
|
409
|
+
// .today-cols in app.css) it moves to the top of the right-hand column, above Needs you (Miles 11/06).
|
|
410
|
+
var PET_WIDE = window.matchMedia ? window.matchMedia('(min-width: 760px)') : null;
|
|
411
|
+
function placePet() {
|
|
412
|
+
var box = $('today-pet'), side = document.querySelector('#pane-today .today-side'), rec = $('today-rec');
|
|
413
|
+
if (!box || !side || !rec) return;
|
|
414
|
+
if (PET_WIDE && PET_WIDE.matches) { if (side.firstElementChild !== box) side.insertBefore(box, side.firstElementChild); }
|
|
415
|
+
else if (rec.previousElementSibling !== box) rec.parentNode.insertBefore(box, rec);
|
|
416
|
+
}
|
|
417
|
+
if (PET_WIDE) { if (PET_WIDE.addEventListener) PET_WIDE.addEventListener('change', placePet); else if (PET_WIDE.addListener) PET_WIDE.addListener(placePet); }
|
|
418
|
+
var PET_CHANGE = '<button type="button" class="pet-change" data-goto="settings/pet">Change pet</button>';
|
|
419
|
+
// A still pet (a gallery pick, Cloud Puff, or a single image on the Mac): one picture for every state.
|
|
420
|
+
function renderPetStill(box, m, sessions) {
|
|
421
|
+
var c = petPose(sessions), label = petCaption(c);
|
|
422
|
+
petStop(); PET.pose = 'still:' + m.still;
|
|
423
|
+
var img = box.querySelector('.pet-still');
|
|
424
|
+
if (!img || img.getAttribute('src') !== m.still) {
|
|
425
|
+
box.innerHTML = '<button type="button" class="pet-btn" data-goto="sessions"><img class="pet-still" alt="" src="' + esc(m.still) + '"><span class="pet-caption"></span></button>' + PET_CHANGE;
|
|
426
|
+
img = box.querySelector('.pet-still');
|
|
427
|
+
img.addEventListener('error', function () { box.classList.add('hidden'); });
|
|
428
|
+
img.addEventListener('load', function () { img.classList.toggle('pixel', img.naturalHeight > 0 && img.naturalHeight < 140); });
|
|
429
|
+
}
|
|
430
|
+
box.querySelector('.pet-caption').textContent = label;
|
|
431
|
+
box.querySelector('.pet-btn').setAttribute('aria-label', 'Sessions: ' + label);
|
|
432
|
+
box.classList.remove('hidden');
|
|
433
|
+
}
|
|
434
|
+
function renderPet(sessions) {
|
|
435
|
+
var box = $('today-pet');
|
|
436
|
+
if (!box) return;
|
|
437
|
+
placePet();
|
|
438
|
+
var prefs = petPrefs();
|
|
439
|
+
if (!prefs.show) { box.classList.add('hidden'); box.innerHTML = ''; petStop(); PET.pose = null; return; }
|
|
440
|
+
// The session list could not be read: the pet would say "Nothing running" about sessions nobody saw. Hide it.
|
|
441
|
+
if (state.sessionsUnread) { box.classList.add('hidden'); petStop(); PET.pose = null; return; }
|
|
442
|
+
var fresh = PET.manifest && PET.manifest.pet === prefs.pet && Date.now() - PET.fetchedAt < 10 * 60000;
|
|
443
|
+
(fresh ? Promise.resolve(PET.manifest) : api('/api/web-pet?pet=' + encodeURIComponent(prefs.pet)).then(function (m) { PET.manifest = m; PET.fetchedAt = Date.now(); return m; }))
|
|
444
|
+
.catch(function () { return PET.manifest && PET.manifest.pet === prefs.pet ? PET.manifest : null; })
|
|
445
|
+
.then(function (m) {
|
|
446
|
+
// A slow answer for a pet that is no longer chosen (or a pet now switched off) paints nothing.
|
|
447
|
+
var now = petPrefs();
|
|
448
|
+
if (!now.show || now.pet !== prefs.pet) return;
|
|
449
|
+
if (!m || !m.available) { box.classList.add('hidden'); petStop(); return; }
|
|
450
|
+
if (m.kind === 'still') { renderPetStill(box, m, sessions); return; }
|
|
451
|
+
var c = petPose(sessions);
|
|
452
|
+
var pose = m.poses[c.pose] ? c.pose : (m.poses.idle ? 'idle' : Object.keys(m.poses)[0]);
|
|
453
|
+
var spec = m.poses[pose];
|
|
454
|
+
var label = petCaption(c);
|
|
455
|
+
box.classList.remove('hidden');
|
|
456
|
+
var cap = box.querySelector('.pet-caption');
|
|
457
|
+
if (cap) cap.textContent = label;
|
|
458
|
+
var btn = box.querySelector('.pet-btn');
|
|
459
|
+
if (btn) btn.setAttribute('aria-label', 'Sessions: ' + label);
|
|
460
|
+
if (PET.pose === pose && PET.timer) return;
|
|
461
|
+
var token = ++PET.token;
|
|
462
|
+
petLoadImages(spec).then(function () {
|
|
463
|
+
if (token !== PET.token) return;
|
|
464
|
+
petStop();
|
|
465
|
+
PET.pose = pose; PET.frame = 0;
|
|
466
|
+
if (!box.querySelector('.pet-sprite')) {
|
|
467
|
+
box.innerHTML = '<button type="button" class="pet-btn" data-goto="sessions"><span class="pet-sprite" aria-hidden="true"></span><span class="pet-caption"></span></button>' + PET_CHANGE;
|
|
468
|
+
}
|
|
469
|
+
box.querySelector('.pet-caption').textContent = label;
|
|
470
|
+
box.querySelector('.pet-btn').setAttribute('aria-label', 'Sessions: ' + label);
|
|
471
|
+
var el = box.querySelector('.pet-sprite'), scale = PET_HEIGHT / spec.height;
|
|
472
|
+
el.style.width = Math.round(spec.frameWidth * scale) + 'px';
|
|
473
|
+
petPaint(el, spec, 0, scale);
|
|
474
|
+
var still = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
475
|
+
if (still || spec.frames < 2) return;
|
|
476
|
+
var step = function () {
|
|
477
|
+
// Runs only while Today is on screen: leaving the pane or hiding the page stops the loop.
|
|
478
|
+
if (token !== PET.token || document.hidden || state.pane !== 'today') { PET.timer = null; return; }
|
|
479
|
+
PET.frame = (PET.frame + 1) % spec.frames;
|
|
480
|
+
petPaint(el, spec, PET.frame, scale);
|
|
481
|
+
PET.timer = setTimeout(step, Math.max(20, spec.interval * 1000));
|
|
482
|
+
};
|
|
483
|
+
PET.timer = setTimeout(step, Math.max(20, spec.interval * 1000));
|
|
484
|
+
}, function () {
|
|
485
|
+
if (token === PET.token && !box.querySelector('.pet-sprite')) box.classList.add('hidden');
|
|
486
|
+
});
|
|
487
|
+
});
|
|
241
488
|
}
|
|
242
489
|
|
|
243
490
|
// ── Today ───────────────────────────────────────────────
|
|
244
491
|
function loadToday() {
|
|
245
492
|
var now = new Date();
|
|
246
493
|
$('today-date').textContent = now.toLocaleDateString(undefined, { weekday: 'long', month: 'long', day: 'numeric' }) + ', ' + clock(now);
|
|
494
|
+
var gen = ++TODAY.gen;
|
|
495
|
+
// Voices and memories answer on their own time (the memories status runs a CLI on the Mac): Today paints what it
|
|
496
|
+
// has and says it is still reading, then repaints as each one lands.
|
|
497
|
+
if (Date.now() - TODAY.extraAt > 60000 || TODAY.voices.state === 'failed' || TODAY.memories.state === 'failed') {
|
|
498
|
+
TODAY.extraAt = Date.now();
|
|
499
|
+
TODAY.voices = { state: 'pending', groups: [] }; TODAY.memories = { state: 'pending', n: 0 };
|
|
500
|
+
api('/api/voice/held-groups').then(function (d) {
|
|
501
|
+
TODAY.voices = { state: 'answered', groups: (d && Array.isArray(d.groups)) ? d.groups : [] };
|
|
502
|
+
}, function () { TODAY.voices = { state: 'failed', groups: [] }; }).then(function () { paintNeeds(); });
|
|
503
|
+
api('/api/web-bridge/memories', { method: 'POST', body: { id: 'today-status', op: 'status', args: {} } }).then(function (d) {
|
|
504
|
+
var n = d && d.details ? d.details.learningToReview : null;
|
|
505
|
+
TODAY.memories = typeof n === 'number' ? { state: 'answered', n: Math.max(0, n) } : { state: 'failed', n: 0 };
|
|
506
|
+
}, function () { TODAY.memories = { state: 'failed', n: 0 }; }).then(function () { paintNeeds(); });
|
|
507
|
+
}
|
|
247
508
|
Promise.all([
|
|
248
|
-
|
|
509
|
+
sessionList(15000).catch(function () { return null; }),
|
|
249
510
|
api('/api/work-board/your-move').catch(function () { return null; }),
|
|
250
511
|
api('/api/welcome-context').catch(function () { return null; }),
|
|
251
512
|
api('/api/meetings/live-chunks').catch(function () { return null; })
|
|
252
513
|
]).then(function (res) {
|
|
514
|
+
if (gen !== TODAY.gen) return;
|
|
253
515
|
var sessions = (res[0] && res[0].sessions) || [];
|
|
254
516
|
state.sessions = sessions;
|
|
255
517
|
var needs = (res[1] && res[1].items) || [];
|
|
@@ -259,18 +521,24 @@
|
|
|
259
521
|
var open = sessions.filter(function (s) { return s.running && !s.running_active; });
|
|
260
522
|
var recent = live.filter(function (s) { return Date.now() - (s.mtimeMs || 0) < LIVE_FRESH_MS; });
|
|
261
523
|
var offline = !res[0] && !res[1];
|
|
524
|
+
TODAY.needs = needs; TODAY.working = working; TODAY.open = open; TODAY.welcome = welcome; TODAY.offline = offline; TODAY.at = Date.now();
|
|
525
|
+
// your-move answers {available:false} when the board cannot be read: that is a failed read, not "nothing".
|
|
526
|
+
TODAY.sessionsState = res[0] ? 'answered' : 'failed';
|
|
527
|
+
state.sessionsUnread = !res[0]; // the pet reads this (renderPet)
|
|
528
|
+
TODAY.workState = res[1] && res[1].available !== false ? 'answered' : 'failed';
|
|
529
|
+
TODAY.recording = false;
|
|
262
530
|
|
|
263
|
-
renderStory(needs.length, working.length, open.length, welcome.nextEvent, false, offline);
|
|
264
531
|
renderRunning(working, open);
|
|
265
|
-
|
|
532
|
+
if (!offline) renderPet(sessions);
|
|
533
|
+
paintNeeds();
|
|
266
534
|
renderNext(welcome);
|
|
267
535
|
loadTodayIntake();
|
|
268
|
-
setBadge('today', needs.length > 0);
|
|
269
536
|
setBadge('sessions', working.length > 0);
|
|
270
537
|
liveSession(recent).then(function (rec) {
|
|
538
|
+
if (gen !== TODAY.gen) return;
|
|
271
539
|
renderRecording(rec);
|
|
272
540
|
setBadge('meetings', !!rec);
|
|
273
|
-
if (rec)
|
|
541
|
+
if (rec) { TODAY.recording = true; paintNeeds(); }
|
|
274
542
|
});
|
|
275
543
|
});
|
|
276
544
|
}
|
|
@@ -295,37 +563,44 @@
|
|
|
295
563
|
'<span class="rec-copy"><strong>Recording now</strong><span>' + esc(rec.source + (rec.startTime ? ', ' + span(rec.startTime) + ' so far' : '')) + '</span></span>' +
|
|
296
564
|
'<span class="btn small">Live view</span></button>';
|
|
297
565
|
}
|
|
298
|
-
|
|
566
|
+
// The headline: COS Control's brief (Models.swift TodayHome.brief) over the same rows Needs you lists.
|
|
567
|
+
function renderStory(m) {
|
|
299
568
|
var el = $('today-story');
|
|
300
569
|
el.className = 'story';
|
|
301
|
-
if (offline) { el.textContent = 'COS on ' + state.serverName + ' is not answering right now. Today fills in when it does.'; return; }
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
: 'Nothing is waiting on you';
|
|
309
|
-
var nextBit = '';
|
|
310
|
-
var nd = next && toDate(next.time);
|
|
570
|
+
if (TODAY.offline) { el.textContent = 'COS on ' + state.serverName + ' is not answering right now. Today fills in when it does.'; return; }
|
|
571
|
+
var first = '';
|
|
572
|
+
if (m.sessionsWaiting > 0) first = countWord(m.sessionsWaiting) + ' ' + plural(m.sessionsWaiting, 'session is', 'sessions are') + ' waiting on you';
|
|
573
|
+
else if (m.now.length > 0) first = countWord(m.now.length) + ' ' + plural(m.now.length, 'thing needs', 'things need') + ' you' + (m.settled ? '' : ' so far');
|
|
574
|
+
else if (m.later.length > 0) first = countWord(m.later.length) + ' ' + plural(m.later.length, 'thing', 'things') + ' can wait until tomorrow' + (m.settled ? '' : ' so far');
|
|
575
|
+
else if (m.settled) first = 'Nothing is waiting on you';
|
|
576
|
+
var next = TODAY.welcome && TODAY.welcome.nextEvent, nd = next && toDate(next.time);
|
|
311
577
|
if (nd && next.title && (nd.getTime() - Date.now()) < 18 * 3600000 && nd.getTime() > Date.now() - 600000) {
|
|
312
|
-
var dw = dayWord(nd);
|
|
313
|
-
|
|
578
|
+
var dw = dayWord(nd), words = next.title + ' is at ' + clock(nd) + (dw === 'today' ? '' : ' ' + dw);
|
|
579
|
+
first = first ? first + ', and ' + words : words.charAt(0).toUpperCase() + words.slice(1);
|
|
314
580
|
}
|
|
315
|
-
var second;
|
|
316
|
-
|
|
581
|
+
var working = TODAY.working.length, open = TODAY.open.length, second;
|
|
582
|
+
var unread = TODAY.sessionsState === 'failed';
|
|
583
|
+
// The sessions sentence speaks only about a list that was read (W3-r2: "No agents are running" while two worked).
|
|
584
|
+
if (unread) second = 'Couldn\u2019t read your sessions';
|
|
585
|
+
else if (working > 0) second = countWord(working) + ' ' + plural(working, 'agent is', 'agents are') + ' working' + (open > 0 ? ', with ' + countWord(open, true) + ' more ' + plural(open, 'session', 'sessions') + ' open' : '');
|
|
317
586
|
else if (open > 0) second = countWord(open) + ' ' + plural(open, 'session is', 'sessions are') + ' open and quiet';
|
|
318
587
|
else second = 'No agents are running';
|
|
319
|
-
if (recording) second += (working > 0 || open > 0 ? ', and ' : '. ') + 'COS is recording a meeting';
|
|
320
|
-
|
|
321
|
-
|
|
588
|
+
if (TODAY.recording) second += (unread || working > 0 || open > 0 ? ', and ' : '. ') + 'COS is recording a meeting';
|
|
589
|
+
var out = (first ? first + '. ' : '') + second + '.';
|
|
590
|
+
var others = m.failed.filter(function (n) { return n !== 'sessions'; });
|
|
591
|
+
if (!m.settled && !m.now.length && !m.later.length) out += (unread ? others : m.failed).length ? ' Couldn\u2019t read ' + listWords(unread ? others : m.failed) + ', so this may not be everything.' : unread ? ' This may not be everything that needs you.' : ' Still reading what needs you.';
|
|
592
|
+
else if (unread) out += ' This may not be everything that needs you.';
|
|
593
|
+
el.textContent = out;
|
|
594
|
+
}
|
|
595
|
+
function listWords(names) { return names.length < 2 ? names.join('') : names.slice(0, -1).join(', ') + ' and ' + names[names.length - 1]; }
|
|
322
596
|
function runRow(s, activeNow) {
|
|
323
597
|
var key = splitSessionId(s.provider, s.session_id);
|
|
324
598
|
var meta = activeNow
|
|
325
599
|
? 'Working for ' + span(s.state_since || s.last_activity_at)
|
|
326
600
|
: 'Open, quiet for ' + span(s.last_activity_at || s.state_since);
|
|
327
601
|
if (s.project && s.project !== usualProject()) meta += ', in ' + String(s.project).replace(/^-Users-[^-]+-/, '');
|
|
328
|
-
|
|
602
|
+
// A tap opens the peek on Today (panes/today-actions.js); it falls back to the reader if the peek cannot load.
|
|
603
|
+
return '<button type="button" class="run-row" data-today="run" data-key="' + esc(key.provider) + '|' + esc(key.id) + '">' +
|
|
329
604
|
'<span class="run-who">' + badge(key.provider) + (activeNow ? '<span class="pulse" aria-label="working"></span>' : '') + '</span>' +
|
|
330
605
|
'<span class="run-title">' + esc(sessionTitle(s)) + '</span>' +
|
|
331
606
|
'<span class="run-meta">' + esc(meta) + '</span></button>';
|
|
@@ -333,6 +608,8 @@
|
|
|
333
608
|
function renderRunning(working, open) {
|
|
334
609
|
$('run-count').textContent = working.length ? String(working.length) : '';
|
|
335
610
|
var list = $('run-list');
|
|
611
|
+
// Never "Nothing running" about a list that was not read.
|
|
612
|
+
if (TODAY.sessionsState === 'failed') { list.innerHTML = '<button type="button" class="run-more run-failed" data-today-needs="retry">Couldn\u2019t read sessions. Try again</button>'; return; }
|
|
336
613
|
var shown = working.slice(0, 3);
|
|
337
614
|
var html = shown.map(function (s) { return runRow(s, true); }).join('');
|
|
338
615
|
if (!shown.length) {
|
|
@@ -350,21 +627,121 @@
|
|
|
350
627
|
if (item.status === 'delivered') return 'Handed to you';
|
|
351
628
|
return 'Needs a look';
|
|
352
629
|
}
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
630
|
+
// ── Needs you (COS Control's TodayHome.things, sourceStates and fold; Models.swift 14291, 14512, 14474) ──
|
|
631
|
+
// Every source Control's Today reads that the web can: sessions waiting on you (the session list), Work handoffs
|
|
632
|
+
// (your-move), voices to name (/api/voice/held-groups) and memories to review (the memories bridge's status). One row
|
|
633
|
+
// per thing. The card's count, the headline and the rail say the same number, from the same rows. "Nothing is
|
|
634
|
+
// waiting on you" only once every source answered; a source that failed is named instead. Under quiet hours (after
|
|
635
|
+
// 6 PM, before 6 AM, weekends) everything but a waiting session folds under "can wait until tomorrow".
|
|
636
|
+
var NEEDS_SHOWN = 6;
|
|
637
|
+
var NEED_NAMES = { sessions: 'sessions', work: 'Work', voices: 'voices', memories: 'memories' };
|
|
638
|
+
function quietNowToday(d) { var h = d.getHours(), day = d.getDay(); return day === 0 || day === 6 || h >= 18 || h < 6; }
|
|
639
|
+
// Control's askedTitle and askedWhy for a session waiting on you.
|
|
640
|
+
// Control's TodayHome: guarded(title) ?? "A session". Never the workspace path or the session id: this line is shown
|
|
641
|
+
// on Today and goes into the Today summary sent to the model, which carries no ids.
|
|
642
|
+
function askedTitle(s) {
|
|
643
|
+
var t = plain(s.display_label || s.custom_title || '').replace(/\s+/g, ' ').trim() || 'A session';
|
|
644
|
+
if (sxProvider(s.provider) !== 'claude') return t + ' is waiting for your reply';
|
|
645
|
+
if (s.waiting_kind === 'plan') return t + ' has a plan for you to approve';
|
|
646
|
+
if (s.waiting_kind === 'permission') return t + ' wants your permission';
|
|
647
|
+
if (s.waiting_kind === 'mcp_input') return t + ' needs your input';
|
|
648
|
+
return t + ' asked you a question';
|
|
649
|
+
}
|
|
650
|
+
// Control never shows a bare tool name (TodayHome.toolNames): "AskUserQuestion" says nothing to a person.
|
|
651
|
+
var TOOL_NAMES = ['AskUserQuestion', 'ExitPlanMode', 'Bash', 'BashOutput', 'KillShell', 'Edit', 'Write', 'Read', 'Grep', 'Glob', 'LS', 'Task', 'Agent',
|
|
652
|
+
'WebFetch', 'WebSearch', 'TodoWrite', 'NotebookEdit', 'MultiEdit', 'SlashCommand', 'Skill', 'ToolSearch'];
|
|
653
|
+
function askedWhy(s) {
|
|
654
|
+
if (s.waiting_kind === 'plan') return 'Has a plan for you to approve';
|
|
655
|
+
var d = String(s.waiting_detail || '').trim();
|
|
656
|
+
if (TOOL_NAMES.indexOf(d) >= 0) d = '';
|
|
657
|
+
if (s.waiting_kind === 'permission' && d) return 'Wants to run ' + (d.length > 80 ? d.slice(0, 79) + '…' : d);
|
|
658
|
+
if (s.waiting_kind === 'question' && d) return d.length > 160 ? d.slice(0, 159) + '…' : d;
|
|
659
|
+
return 'Waiting for you' + (sxProvider(s.provider) === 'claude' ? '. Paused until you answer.' : '');
|
|
660
|
+
}
|
|
661
|
+
function needsModel() {
|
|
662
|
+
var states = { sessions: TODAY.sessionsState, work: TODAY.workState, voices: TODAY.voices.state, memories: TODAY.memories.state };
|
|
663
|
+
var rows = [], seen = {};
|
|
664
|
+
(state.sessions || []).forEach(function (s) {
|
|
665
|
+
if (sxKeepWarm(s) || sxState(s) !== 'waiting') return;
|
|
666
|
+
var k = splitSessionId(s.provider, s.session_id || s.id);
|
|
667
|
+
seen[k.provider + '|' + String(k.id).toLowerCase()] = 1;
|
|
668
|
+
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 });
|
|
669
|
+
});
|
|
670
|
+
(TODAY.needs || []).forEach(function (it) {
|
|
671
|
+
var k = splitSessionId(it.provider, it.sessionId);
|
|
672
|
+
if (k.id && seen[k.provider + '|' + String(k.id).toLowerCase()]) return; // Control: a Work row for a waiting session is the session's row
|
|
673
|
+
rows.push({ kind: 'work', tier: 3, item: it, key: k.id ? k.provider + '|' + k.id : '' });
|
|
674
|
+
});
|
|
675
|
+
var groups = TODAY.voices.groups || [];
|
|
676
|
+
if (groups.length) {
|
|
677
|
+
var named = groups.filter(function (g) { return g && g.suggestion && g.suggestion.name; })[0];
|
|
678
|
+
rows.push({ kind: 'voices', tier: 2, title: groups.length === 1 ? 'Name a held voice' : 'Name ' + groups.length + ' held voices',
|
|
679
|
+
why: named ? 'One sounds like ' + named.suggestion.name + '.' : 'Held from your recordings until they have a name.' });
|
|
680
|
+
}
|
|
681
|
+
if (TODAY.memories.n > 0) {
|
|
682
|
+
var n = TODAY.memories.n;
|
|
683
|
+
rows.push({ kind: 'memories', tier: 4, title: n === 1 ? 'Review a memory' : 'Review ' + n + ' memories', why: 'Learned from your meetings and sessions, waiting for a yes or no.' });
|
|
684
|
+
}
|
|
685
|
+
var quiet = quietNowToday(new Date());
|
|
686
|
+
var nowRows = rows.filter(function (r) { return !quiet || r.tier === 1; }), later = rows.filter(function (r) { return quiet && r.tier !== 1; });
|
|
687
|
+
var failed = Object.keys(states).filter(function (k) { return states[k] === 'failed'; }).map(function (k) { return NEED_NAMES[k]; });
|
|
688
|
+
var settled = Object.keys(states).every(function (k) { return states[k] === 'answered'; });
|
|
689
|
+
return { rows: rows, now: nowRows, later: later, failed: failed, settled: settled, states: states,
|
|
690
|
+
sessionsWaiting: rows.filter(function (r) { return r.kind === 'session'; }).length };
|
|
691
|
+
}
|
|
692
|
+
function paintNeeds() {
|
|
693
|
+
if (!TODAY.at) return;
|
|
694
|
+
var m = TODAY.model = needsModel();
|
|
695
|
+
renderStory(m);
|
|
696
|
+
renderNeeds(m);
|
|
697
|
+
setBadge('today', m.now.length > 0);
|
|
698
|
+
if (window.COSWeb && window.COSWeb.setStory) window.COSWeb.setStory('today', todayRailLine(m), m.now.length > 0);
|
|
699
|
+
}
|
|
700
|
+
// The rail's Today line, from the same rows (shell.js keeps its own count off Today).
|
|
701
|
+
function todayRailLine(m) {
|
|
702
|
+
var n = m.now.length, bits = [];
|
|
703
|
+
if (n > 0) bits.push(n === 1 ? '1 needs you' : n + ' need you');
|
|
704
|
+
else if (m.later.length) bits.push(m.later.length + ' can wait until tomorrow');
|
|
705
|
+
else if (m.settled) bits.push('Nothing needs you');
|
|
706
|
+
else if (m.failed.length) bits.push('Couldn’t read ' + listWords(m.failed));
|
|
707
|
+
else bits.push('Reading');
|
|
708
|
+
if (TODAY.working.length) bits.push(TODAY.working.length + ' running');
|
|
709
|
+
return bits.join(', ');
|
|
710
|
+
}
|
|
711
|
+
function needRowHtml(r, i) {
|
|
712
|
+
var age = r.since ? ago(r.since) : r.item ? ago(r.item.updatedAt || r.item.createdAt) : '';
|
|
713
|
+
var top = function (title) { return '<span class="need-top"><span class="need-title">' + esc(title) + '</span>' + (age ? '<span class="need-age">' + esc(age) + '</span>' : '') + '</span>'; };
|
|
714
|
+
if (r.kind === 'work') {
|
|
715
|
+
var it = r.item, k = splitSessionId(it.provider, it.sessionId);
|
|
361
716
|
var why = needWhy(it) + (it.domain ? ' in ' + domainLabel(it.domain) : '');
|
|
717
|
+
// The row opens it in an overlay on Today (panes/today-actions.js); Open session still goes to the reader.
|
|
362
718
|
return '<div class="need-row">' +
|
|
363
|
-
'<
|
|
364
|
-
'<
|
|
365
|
-
(
|
|
366
|
-
|
|
367
|
-
|
|
719
|
+
'<button type="button" class="need-open" data-today="need" data-key="' + esc(it.id) + '"' + (k.id ? ' data-session-key="' + esc(k.provider) + '|' + esc(k.id) + '"' : '') + '>' +
|
|
720
|
+
top(it.sessionTitle || it.title || 'Handoff') + '<span class="need-foot">' + esc(why) + '</span></button>' +
|
|
721
|
+
(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
|
+
}
|
|
723
|
+
if (r.kind === 'session') {
|
|
724
|
+
// The session's question shows in the overlay's peek, with its cards to answer.
|
|
725
|
+
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></button>' +
|
|
727
|
+
'<div class="need-actions"><button type="button" class="btn small' + (i === 0 ? ' primary' : '') + '" data-session="' + esc(r.key) + '">Open session</button></div></div>';
|
|
728
|
+
}
|
|
729
|
+
var go2 = r.kind === 'voices' ? 'speakers' : 'memories/view/review', label = r.kind === 'voices' ? 'Name' : 'Review';
|
|
730
|
+
return '<div class="need-row">' +
|
|
731
|
+
'<button type="button" class="need-open" data-goto="' + go2 + '">' + top(r.title) + '<span class="need-foot">' + esc(r.why) + '</span></button>' +
|
|
732
|
+
'<div class="need-actions"><button type="button" class="btn small' + (i === 0 ? ' primary' : '') + '" data-goto="' + go2 + '">' + label + '</button></div></div>';
|
|
733
|
+
}
|
|
734
|
+
function renderNeeds(m) {
|
|
735
|
+
var shown = TODAY.showLater ? m.now.concat(m.later) : m.now;
|
|
736
|
+
$('needs-count').textContent = m.now.length ? String(m.now.length) : '';
|
|
737
|
+
var list = $('needs-list');
|
|
738
|
+
var visible = TODAY.showAllNeeds ? shown : shown.slice(0, NEEDS_SHOWN);
|
|
739
|
+
var html = visible.map(needRowHtml).join('');
|
|
740
|
+
if (shown.length > visible.length) html += '<button type="button" class="need-more" data-today-needs="all">Show ' + (shown.length - visible.length) + ' more</button>';
|
|
741
|
+
if (m.later.length && !TODAY.showLater) html += '<button type="button" class="need-more" data-today-needs="later">' + esc(m.later.length + (m.later.length === 1 ? ' thing can' : ' things can') + ' wait until tomorrow. Show') + '</button>';
|
|
742
|
+
if (!m.rows.length) html = '<p class="card-empty">' + esc(m.settled ? 'Nothing is waiting on you.' : m.failed.length ? '' : 'Reading sessions, Work, voices and memories.') + '</p>';
|
|
743
|
+
m.failed.forEach(function (name) { html += '<button type="button" class="need-more need-failed" data-today-needs="retry">' + esc('Couldn’t read ' + name + '. Try again') + '</button>'; });
|
|
744
|
+
list.innerHTML = html;
|
|
368
745
|
}
|
|
369
746
|
function renderNext(w) {
|
|
370
747
|
var card = $('next-card');
|
|
@@ -374,30 +751,73 @@
|
|
|
374
751
|
var dw = dayWord(d);
|
|
375
752
|
$('next-body').innerHTML = '<span class="when">' + esc(clock(d)) + '</span><span><span class="what">' + esc(n.title) + '</span>' +
|
|
376
753
|
'<span class="sub" style="display:block">' + esc((dw.charAt(0).toUpperCase() + dw.slice(1)) + (weather ? '. Now ' + weather : '')) + '</span></span>';
|
|
754
|
+
// Control's Next up (todayPrepButtons): Prep me opens the Prep composer in the COS chat (panes/cos-chat.js prepFor),
|
|
755
|
+
// Open meeting opens Meetings. Without a server to answer, Prep me is not offered.
|
|
756
|
+
var prep = !TODAY.offline && window.COSWeb && window.COSWeb.cosChat && window.COSWeb.cosChat.prepFor;
|
|
757
|
+
$('next-actions').innerHTML = (prep ? '<button type="button" class="btn small primary" data-today-next="prep">Prep me</button>' : '') +
|
|
758
|
+
'<button type="button" class="link-btn" data-goto="meetings">Open meeting</button>';
|
|
377
759
|
card.classList.remove('hidden');
|
|
378
760
|
}
|
|
761
|
+
function prepNext() {
|
|
762
|
+
var n = TODAY.welcome && TODAY.welcome.nextEvent, d = n && toDate(n.time), chat = window.COSWeb && window.COSWeb.cosChat;
|
|
763
|
+
if (!n || !n.title || !chat || !chat.prepFor) return;
|
|
764
|
+
go('messages', ['cos']);
|
|
765
|
+
chat.prepFor(n.title, d ? d.toISOString() : null).catch(function (err) { toast((err && err.message) || 'Prep me did not start.'); });
|
|
766
|
+
}
|
|
379
767
|
// From today's meetings: Work's suggestions from meetings held today. They stay here until
|
|
380
768
|
// midnight, then join Sort in Work; Review opens them in Work's Sort.
|
|
381
769
|
function localDay(d) { return d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0') + '-' + String(d.getDate()).padStart(2, '0'); }
|
|
382
770
|
function loadTodayIntake() {
|
|
383
|
-
var card = $('intake-card'); if (!card) return;
|
|
384
|
-
api('/api/work-intake').then(function (d) {
|
|
771
|
+
var card = $('intake-card'); if (!card) return Promise.resolve();
|
|
772
|
+
return api('/api/work-intake').then(function (d) {
|
|
385
773
|
var today = localDay(new Date());
|
|
386
774
|
var items = ((d && d.items) || []).filter(function (it) {
|
|
387
775
|
if (['suggested', 'review', 'ask'].indexOf(it.status) < 0) return false;
|
|
388
776
|
var day = String((it.meeting && (it.meeting.filename || it.meeting.date)) || '').match(/\d{4}-\d{2}-\d{2}/);
|
|
389
777
|
return day && day[0] === today;
|
|
390
778
|
});
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
$('intake-body').innerHTML = Object.keys(meetings).slice(0, 4).map(function (t) {
|
|
396
|
-
return '<div class="intake-row"><span class="intake-meeting">' + esc(t) + '</span><span class="intake-n">' + meetings[t] + '</span></div>';
|
|
397
|
-
}).join('') + '<p class="intake-foot">Moves to Work at midnight.</p>';
|
|
398
|
-
card.classList.remove('hidden');
|
|
779
|
+
TODAY.intake = items; TODAY.intakeCaps = (d && d.capabilities) || {};
|
|
780
|
+
// A decision made on Today hides its row at once; a read that no longer lists it ends the hiding.
|
|
781
|
+
Object.keys(TODAY.gone).forEach(function (id) { if (!items.some(function (it) { return it.id === id; })) delete TODAY.gone[id]; });
|
|
782
|
+
paintTodayIntake();
|
|
399
783
|
}, function () { card.classList.add('hidden'); });
|
|
400
784
|
}
|
|
785
|
+
// Today's overlay (panes/today-actions.js) reads what the cards show from here and repaints the intake card after a
|
|
786
|
+
// decision, so the counts move the moment Work takes it.
|
|
787
|
+
var TODAY = { needs: [], working: [], open: [], welcome: {}, offline: false, at: 0, intake: null, intakeCaps: {}, gone: {},
|
|
788
|
+
gen: 0, sessionsState: 'pending', workState: 'pending', voices: { state: 'pending', groups: [] }, memories: { state: 'pending', n: 0 }, extraAt: 0,
|
|
789
|
+
recording: false, model: null, showAllNeeds: false, showLater: false };
|
|
790
|
+
function todayIntakeOpen() { return (TODAY.intake || []).filter(function (it) { return !TODAY.gone[it.id]; }); }
|
|
791
|
+
function paintTodayIntake() {
|
|
792
|
+
var card = $('intake-card'); if (!card) return;
|
|
793
|
+
var items = todayIntakeOpen();
|
|
794
|
+
if (!items.length) { card.classList.add('hidden'); return; }
|
|
795
|
+
var meetings = {};
|
|
796
|
+
items.forEach(function (it) { var t = (it.meeting && it.meeting.title) || 'A meeting'; meetings[t] = (meetings[t] || 0) + 1; });
|
|
797
|
+
$('intake-count').textContent = String(items.length);
|
|
798
|
+
$('intake-body').innerHTML = Object.keys(meetings).slice(0, 4).map(function (t) {
|
|
799
|
+
return '<button type="button" class="intake-row" data-today="meeting" data-key="' + esc(t) + '"><span class="intake-meeting">' + esc(t) + '</span><span class="intake-n">' + meetings[t] + '</span></button>';
|
|
800
|
+
}).join('') + '<p class="intake-foot">Moves to Work at midnight.</p>';
|
|
801
|
+
card.classList.remove('hidden');
|
|
802
|
+
}
|
|
803
|
+
function openTodayAction(t) {
|
|
804
|
+
var kind = t.getAttribute('data-today'), key = t.getAttribute('data-key') || '';
|
|
805
|
+
ensurePane('todayActions', 4000).then(function (m) {
|
|
806
|
+
if (!m || !m.open) throw new Error('The Today overlay did not start.');
|
|
807
|
+
m.open(kind, key, t);
|
|
808
|
+
}).catch(function (err) {
|
|
809
|
+
// Without the overlay, the tap goes where it went before: the session reader, or Sort in Work. Web has no client
|
|
810
|
+
// log route, so the failure is said on screen and in the console first. An overlay that half opened leaves its
|
|
811
|
+
// history entry, which the fallback replaces, so Back from there still lands on Today.
|
|
812
|
+
var s = String(kind === 'run' || kind === 'wait' ? key : t.getAttribute('data-session-key') || '').split('|');
|
|
813
|
+
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']);
|
|
815
|
+
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.');
|
|
817
|
+
if (history.state && history.state.cosTodayOverlay) location.replace('#/' + dest.map(encodeURIComponent).join('/'));
|
|
818
|
+
else go(dest[0], dest.slice(1));
|
|
819
|
+
});
|
|
820
|
+
}
|
|
401
821
|
function setBadge(name, on) { document.querySelectorAll('[data-badge="' + name + '"]').forEach(function (b) { b.classList.toggle('hidden', !on); }); }
|
|
402
822
|
|
|
403
823
|
// ── Sessions ────────────────────────────────────────────
|
|
@@ -583,9 +1003,21 @@
|
|
|
583
1003
|
if (SX.view !== 'sessions') { renderUsageView(force === true); return Promise.resolve(); }
|
|
584
1004
|
loadSessionHooks();
|
|
585
1005
|
if (force !== true && SX.rows && Date.now() - SX.loadedAt < 15000) { renderSessionGroups(); return Promise.resolve(); }
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
1006
|
+
// Paint the shared copy at once (Today usually fetched it) and refresh in place behind it.
|
|
1007
|
+
var cached = !SX.rows && sessionListCached();
|
|
1008
|
+
if (cached && cached.data && Array.isArray(cached.data.sessions)) {
|
|
1009
|
+
SX.rows = cached.data.sessions; SX.dropped = cached.data.dropped || null; SX.enabled = cached.data.enabled !== false;
|
|
1010
|
+
SX.loadedAt = cached.at;
|
|
1011
|
+
if (sxQueryActive()) SX.instant = sxInstant(SX.q);
|
|
1012
|
+
var fresh = force !== true && Date.now() - cached.at < 15000;
|
|
1013
|
+
SX.loading = !fresh; // rows stay on screen; the header reads "Refreshing…" until the new list lands
|
|
1014
|
+
renderSessionGroups();
|
|
1015
|
+
if (fresh) return Promise.resolve();
|
|
1016
|
+
} else {
|
|
1017
|
+
SX.loading = true;
|
|
1018
|
+
renderSessionGroups();
|
|
1019
|
+
}
|
|
1020
|
+
return sessionList(force === true ? 0 : 15000).then(function (d) {
|
|
589
1021
|
SX.rows = (d && d.sessions) || [];
|
|
590
1022
|
SX.dropped = (d && d.dropped) || null;
|
|
591
1023
|
SX.enabled = !(d && d.enabled === false);
|
|
@@ -1041,7 +1473,8 @@
|
|
|
1041
1473
|
if (f.tools.length > 8) f.tools.splice(0, f.tools.length - 8);
|
|
1042
1474
|
}
|
|
1043
1475
|
}
|
|
1044
|
-
|
|
1476
|
+
// `other` is the stream's verb for every tool it has no verb for; its target names the tool ("Used SendMessage").
|
|
1477
|
+
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' };
|
|
1045
1478
|
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; }
|
|
1046
1479
|
// The word follows the page's own working flag, which a Stop or a finished turn settles before
|
|
1047
1480
|
// the stream's status catches up; otherwise the server's derived state, as Control reads it.
|
|
@@ -1220,14 +1653,15 @@
|
|
|
1220
1653
|
// probeChatAttachability): a busy thread parks a send, another app holding it idle asks first,
|
|
1221
1654
|
// and anything else is the server's refusal, word for word.
|
|
1222
1655
|
function sxProbeAttach(r) {
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
if (
|
|
1656
|
+
// Today's overlay probes its own session object (r.view): the same verdict, painted by its view.
|
|
1657
|
+
return apiAnswer(sessionPath(r.provider, r.id, '/attachability')).then(function (a) {
|
|
1658
|
+
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; }
|
|
1226
1660
|
var b = a.body || {};
|
|
1227
1661
|
if (a.status !== 200 || typeof b.attachable !== 'boolean') return;
|
|
1228
1662
|
var owners = Number(b.ownerCount) || 0;
|
|
1229
1663
|
r.verdict = { attachable: b.attachable, reason: String(b.reason || ''), reasonCopy: String(b.reasonCopy || ''), ownerCount: owners, caution: b.attachable === true && owners > 0 };
|
|
1230
|
-
|
|
1664
|
+
viewOf(r).verdict(r);
|
|
1231
1665
|
}).catch(function () {});
|
|
1232
1666
|
}
|
|
1233
1667
|
var SX_PARK = {
|
|
@@ -1278,8 +1712,8 @@
|
|
|
1278
1712
|
el.className = 'status-line' + (kind === 'error' ? ' error' : '');
|
|
1279
1713
|
el.innerHTML = esc(text || '') + (actionHtml || '');
|
|
1280
1714
|
}
|
|
1281
|
-
function draftFor(text) {
|
|
1282
|
-
|
|
1715
|
+
function draftFor(text, r) {
|
|
1716
|
+
r = r || state.reading;
|
|
1283
1717
|
if (!r.draft || r.draft.text !== text) r.draft = { text: text, key: newTurnKey() };
|
|
1284
1718
|
return r.draft;
|
|
1285
1719
|
}
|
|
@@ -1312,6 +1746,21 @@
|
|
|
1312
1746
|
r.held.forEach(function (h) { if (!h.cancelled && !heldBubble(h.key)) showPendingTurn(h.text, 'sends after this reply', h.key); });
|
|
1313
1747
|
}
|
|
1314
1748
|
|
|
1749
|
+
// Where the send path below reports: the reader's own page by default. Today's overlay (panes/today-actions.js)
|
|
1750
|
+
// hands continueSession a session object with its own view, so a reply or a steer from Today runs this same attach,
|
|
1751
|
+
// turn and queue path (one binding store, one key per draft, the same refusals). The overlay keeps its own copies of
|
|
1752
|
+
// the gates around it (sendSessionTurn), the turn watch (watchTurn) and the refusal box (renderVerdict).
|
|
1753
|
+
var READER_VIEW = {
|
|
1754
|
+
status: function (r, text, kind, actionHtml) { sessionStatus(text, kind, actionHtml); },
|
|
1755
|
+
pending: function (r, text, note) { showPendingTurn(text, note); },
|
|
1756
|
+
clear: function (r, text) { clearBoxOf(text); },
|
|
1757
|
+
verdict: function (r) { renderVerdict(r); },
|
|
1758
|
+
hold: function (r, draft) { holdUntilReplyEnds(r, draft); },
|
|
1759
|
+
watch: function (r, bindingId, key) { watchTurn(bindingId, key, r.provider, r.id); },
|
|
1760
|
+
queue: function (r) { loadQueue(r.provider, r.id); }
|
|
1761
|
+
};
|
|
1762
|
+
function viewOf(r) { return (r && r.view) || READER_VIEW; }
|
|
1763
|
+
|
|
1315
1764
|
function sendSessionTurn(fork, opts) {
|
|
1316
1765
|
var r = state.reading; var form = $('session-form');
|
|
1317
1766
|
if (!r || !form) return;
|
|
@@ -1345,8 +1794,8 @@
|
|
|
1345
1794
|
// Resolves to 'sent' | 'parked' | 'held' | 'refused' | 'changed'.
|
|
1346
1795
|
function continueSession(r, text, opts) {
|
|
1347
1796
|
var provider = r.provider, id = r.id;
|
|
1348
|
-
var draft = opts.held ? { text: opts.held.text, key: opts.held.key } : draftFor(text);
|
|
1349
|
-
if (!opts.held)
|
|
1797
|
+
var draft = opts.held ? { text: opts.held.text, key: opts.held.key } : draftFor(text, r), view = viewOf(r);
|
|
1798
|
+
if (!opts.held) view.status(r, 'Sending');
|
|
1350
1799
|
var bound = bindingFor(provider, id);
|
|
1351
1800
|
var attach = bound ? Promise.resolve({ ok: true, binding: bound }) : apiAnswer(sessionPath(provider, id, '/attach'), { method: 'POST', body: { cosSessionId: cosSessionId() }, timeoutMs: 20000 }).then(function (a) {
|
|
1352
1801
|
// 201 Created on success; any 2xx that says attached counts.
|
|
@@ -1368,9 +1817,9 @@
|
|
|
1368
1817
|
if (opts.held) settleHeld(r, opts.held, 'refused', copy);
|
|
1369
1818
|
else {
|
|
1370
1819
|
// Control's refusal box: the server's words, then Fork with this message.
|
|
1371
|
-
|
|
1820
|
+
view.status(r, '');
|
|
1372
1821
|
r.refusal = { copy: (at.refusal && at.refusal.reasonCopy) || 'COS cannot write to this session right now.', supplement: sxSupplement(why), fork: true };
|
|
1373
|
-
|
|
1822
|
+
view.verdict(r);
|
|
1374
1823
|
}
|
|
1375
1824
|
return 'refused';
|
|
1376
1825
|
}
|
|
@@ -1381,9 +1830,9 @@
|
|
|
1381
1830
|
var b = t.body || {};
|
|
1382
1831
|
if (t.status >= 200 && t.status < 300 && (b.outcome === 'queued' || b.outcome === 'completed')) {
|
|
1383
1832
|
if (opts.held) settleHeld(r, opts.held, 'sent');
|
|
1384
|
-
else {
|
|
1385
|
-
|
|
1386
|
-
|
|
1833
|
+
else { view.pending(r, draft.text, b.replayed ? 'already sent' : 'sent'); view.clear(r, draft.text); }
|
|
1834
|
+
view.status(r, b.replayed ? 'Already sent. Watching for the reply.' : 'Sent. Watching for the reply.');
|
|
1835
|
+
view.watch(r, at.binding.bindingId, draft.key);
|
|
1387
1836
|
return 'sent';
|
|
1388
1837
|
}
|
|
1389
1838
|
var reason = b.reason || '';
|
|
@@ -1391,15 +1840,15 @@
|
|
|
1391
1840
|
// COS's own reply is still running on this thread. The server's queue counts that
|
|
1392
1841
|
// thread as free (the running child is COS's), so it cannot park this; the page holds
|
|
1393
1842
|
// it under the same key and sends it the moment the reply ends.
|
|
1394
|
-
if (reason === 'native_turn_in_progress') { if (!opts.held)
|
|
1843
|
+
if (reason === 'native_turn_in_progress') { if (!opts.held) view.hold(r, draft); return 'held'; }
|
|
1395
1844
|
if (reason === 'native_thread_changed' && b.revision) {
|
|
1396
1845
|
if (opts.held) settleHeld(r, opts.held, 'refused', 'This session changed on your Mac while your message waited.');
|
|
1397
1846
|
else {
|
|
1398
1847
|
// Control's refusal box for a changed thread: Refresh, Continue anyway, Fork with this message.
|
|
1399
1848
|
r.sendAnyway = b.revision;
|
|
1400
|
-
|
|
1849
|
+
view.status(r, '');
|
|
1401
1850
|
r.refusal = { copy: b.reasonCopy || 'This session changed on your Mac since you opened it.', changed: true, fork: true };
|
|
1402
|
-
|
|
1851
|
+
view.verdict(r);
|
|
1403
1852
|
}
|
|
1404
1853
|
return 'changed';
|
|
1405
1854
|
}
|
|
@@ -1409,7 +1858,7 @@
|
|
|
1409
1858
|
}
|
|
1410
1859
|
var refusedCopy = b.reasonCopy || 'The session did not take that message.';
|
|
1411
1860
|
if (opts.held) settleHeld(r, opts.held, 'refused', refusedCopy);
|
|
1412
|
-
else
|
|
1861
|
+
else view.status(r, refusedCopy + ' It is still in the box.', 'error');
|
|
1413
1862
|
return 'refused';
|
|
1414
1863
|
});
|
|
1415
1864
|
});
|
|
@@ -1469,6 +1918,7 @@
|
|
|
1469
1918
|
}
|
|
1470
1919
|
|
|
1471
1920
|
function parkTurn(r, draft, refusal, opts) {
|
|
1921
|
+
var view = viewOf(r);
|
|
1472
1922
|
return apiAnswer(sessionPath(r.provider, r.id, '/queued-turns'), {
|
|
1473
1923
|
method: 'POST', timeoutMs: 15000,
|
|
1474
1924
|
body: { cosSessionId: cosSessionId(), clientTurnId: draft.key, prompt: draft.text },
|
|
@@ -1477,14 +1927,14 @@
|
|
|
1477
1927
|
if (b.queued === true || b.error === 'duplicate_turn') {
|
|
1478
1928
|
rememberParked(r, draft.key);
|
|
1479
1929
|
if (opts.held) settleHeld(r, opts.held, 'parked');
|
|
1480
|
-
else {
|
|
1481
|
-
|
|
1482
|
-
|
|
1930
|
+
else { view.pending(r, draft.text, 'waiting in line'); view.clear(r, draft.text); }
|
|
1931
|
+
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
|
+
view.queue(r);
|
|
1483
1933
|
return 'parked';
|
|
1484
1934
|
}
|
|
1485
1935
|
if (b.error === 'thread_free' && !opts.freeRetry) return continueSession(r, draft.text, Object.assign({}, opts, { freeRetry: true }));
|
|
1486
1936
|
var copy = (refusal && refusal.reasonCopy) || (b.error === 'queue_full' ? 'That session already has a full line of waiting messages.' : 'COS could not send or queue that.');
|
|
1487
|
-
if (opts.held) settleHeld(r, opts.held, 'refused', copy); else
|
|
1937
|
+
if (opts.held) settleHeld(r, opts.held, 'refused', copy); else view.status(r, copy + ' It is still in the box.', 'error');
|
|
1488
1938
|
return 'refused';
|
|
1489
1939
|
});
|
|
1490
1940
|
}
|
|
@@ -1749,21 +2199,31 @@
|
|
|
1749
2199
|
var h = Array.prototype.map.call(b, function (x) { return (x + 0x100).toString(16).slice(1); }).join('');
|
|
1750
2200
|
return h.slice(0, 8) + '-' + h.slice(8, 12) + '-' + h.slice(12, 16) + '-' + h.slice(16, 20) + '-' + h.slice(20);
|
|
1751
2201
|
}
|
|
2202
|
+
var TODAY_REFERENCE = 'What Today in COS Control shows right now';
|
|
1752
2203
|
function askCos(prompt, statusEl) {
|
|
1753
2204
|
// 6.99.12: "it shows Sent but never shows up" (Miles, 11/04). With Ask COS in Messages, the sheet hands its words to
|
|
1754
2205
|
// that composer (panes/cos-chat.js submitAsk), so the answer streams on screen and lands as a numbered turn. The
|
|
1755
2206
|
// request below is the fallback for a page without it; it resolves to the Messages route to open.
|
|
1756
2207
|
var chat = window.COSWeb && window.COSWeb.cosChat;
|
|
2208
|
+
// Asked from Today: COS gets a short summary of what Today shows (panes/today-actions.js summary), as the Ask's
|
|
2209
|
+
// quoted reference, the query-jobs field for context. Asked from any other pane, nothing extra is sent.
|
|
2210
|
+
var context = state.pane === 'today' ? ensurePane('todayActions', 1500).then(function (m) {
|
|
2211
|
+
// A broken summary never costs the Ask: it goes without it, and the console says why.
|
|
2212
|
+
try { return m && m.summary ? m.summary() : ''; } catch (err) { console.error('[today] the Today summary failed; asking without it.', err); return ''; }
|
|
2213
|
+
}, function () { return ''; }) : Promise.resolve('');
|
|
1757
2214
|
if (chat && chat.askFromSheet) {
|
|
1758
2215
|
setStatus(statusEl, 'Sending…');
|
|
1759
|
-
return chat.askFromSheet(prompt).then(function (where) { setStatus(statusEl, ''); return where; }, function (err) {
|
|
2216
|
+
return context.then(function (ctx) { return chat.askFromSheet(prompt, ctx ? { context: ctx } : undefined); }).then(function (where) { setStatus(statusEl, ''); return where; }, function (err) {
|
|
1760
2217
|
setStatus(statusEl, err.message, true);
|
|
1761
2218
|
throw err;
|
|
1762
2219
|
});
|
|
1763
2220
|
}
|
|
1764
2221
|
setStatus(statusEl, 'Sent. COS is answering.');
|
|
1765
2222
|
var body = { clientJobId: webJobId(), generation: 1, query: prompt, source: 'web', activityToolMode: 'status' };
|
|
1766
|
-
return
|
|
2223
|
+
return context.then(function (ctx) {
|
|
2224
|
+
if (ctx) body.reference = { query: TODAY_REFERENCE, response: ctx };
|
|
2225
|
+
return api('/api/query-jobs', { method: 'POST', body: body, timeoutMs: 30000 });
|
|
2226
|
+
}).then(function () {
|
|
1767
2227
|
// The answer lands in Messages; refresh it there if it is open.
|
|
1768
2228
|
var again = function () { if (state.pane === 'messages' && PANES.messages && PANES.messages.refresh) PANES.messages.refresh(); };
|
|
1769
2229
|
setTimeout(again, 4000);
|
|
@@ -1774,27 +2234,6 @@
|
|
|
1774
2234
|
throw err;
|
|
1775
2235
|
});
|
|
1776
2236
|
}
|
|
1777
|
-
// ── Memories (the native page, hosted) ──────────────────
|
|
1778
|
-
function syncFrameTheme() {
|
|
1779
|
-
var frame = $('memories-frame'); if (!frame) return;
|
|
1780
|
-
var mode = window.CosTheme ? window.CosTheme.mode() : 'light';
|
|
1781
|
-
frame.style.colorScheme = mode;
|
|
1782
|
-
try {
|
|
1783
|
-
var root = frame.contentDocument && frame.contentDocument.documentElement;
|
|
1784
|
-
if (root) {
|
|
1785
|
-
root.setAttribute('data-theme', mode);
|
|
1786
|
-
if (isPhone()) root.setAttribute('data-host', 'web-phone'); else root.removeAttribute('data-host');
|
|
1787
|
-
}
|
|
1788
|
-
} catch (_e) {}
|
|
1789
|
-
}
|
|
1790
|
-
function loadMemories() {
|
|
1791
|
-
var frame = $('memories-frame');
|
|
1792
|
-
if (state.memoriesLoaded) { syncFrameTheme(); return; }
|
|
1793
|
-
state.memoriesLoaded = true;
|
|
1794
|
-
frame.addEventListener('load', syncFrameTheme);
|
|
1795
|
-
frame.src = '/control/memories/memories.html';
|
|
1796
|
-
}
|
|
1797
|
-
|
|
1798
2237
|
// ── Usage ───────────────────────────────────────────────
|
|
1799
2238
|
// Usage and COS cost are views inside Sessions, as on the Mac (ActivityWindow.swift 2306-2325);
|
|
1800
2239
|
// an old #/usage link opens the Usage view there. Data: GET /api/usage/summary, the helper's
|
|
@@ -3392,7 +3831,7 @@
|
|
|
3392
3831
|
var left = exp ? Math.round((exp.getTime() - Date.now()) / 1000) : 0;
|
|
3393
3832
|
var el = $('device-code-left');
|
|
3394
3833
|
if (!el) { clearInterval(codeTimer); return; }
|
|
3395
|
-
if (left <= 0) { clearInterval(codeTimer); panel.innerHTML = '<p>That code expired. Choose
|
|
3834
|
+
if (left <= 0) { clearInterval(codeTimer); panel.innerHTML = '<p>That code expired. Choose Use COS on another phone or browser for a new one.</p>'; return; }
|
|
3396
3835
|
el.textContent = 'Works once, for ' + (left >= 60 ? Math.ceil(left / 60) + ' more min' : left + ' more sec') + '.';
|
|
3397
3836
|
};
|
|
3398
3837
|
tick(); codeTimer = setInterval(tick, 1000);
|
|
@@ -3413,7 +3852,57 @@
|
|
|
3413
3852
|
['today', 'Today'], ['messages', 'Messages'], ['speakers', 'Speakers'], ['meetings', 'Meetings'], ['memories', 'Memories'],
|
|
3414
3853
|
['threads', 'Threads'], ['sessions', 'Sessions'], ['work', 'Work'], ['dictation', 'Dictation'], ['usage', 'Usage'], ['settings', 'Settings']
|
|
3415
3854
|
];
|
|
3855
|
+
// ⌘K's File to Work inbox (Control's todaySubmitAsk, ActivityWindow.swift 8810): the words become a task in the Work
|
|
3856
|
+
// inbox, in Control's taskDomain (quilt when the server has it, else the first domain it lists: reconcileTaskDomain),
|
|
3857
|
+
// through POST /api/tasks/capture,
|
|
3858
|
+
// the route Work's Add task uses. One captureId per text: a second tap, or a retry after a lost answer, files it once.
|
|
3859
|
+
var ASK_FILE = { text: '', id: '', busy: false, domains: null, note: null };
|
|
3860
|
+
// The result shows on the row itself, so it is in view wherever the sheet is (W5-r2: a status line under the sheet
|
|
3861
|
+
// at 1000x703 hid a refusal), and a toast says it too.
|
|
3862
|
+
function fileNoteHtml(q) {
|
|
3863
|
+
var n = ASK_FILE.note && ASK_FILE.note.text === q ? ASK_FILE.note : null;
|
|
3864
|
+
if (n) return '<span class="note' + (n.error ? ' file-error' : '') + '" role="' + (n.error ? 'alert' : 'status') + '">' + esc(n.line) + '</span>';
|
|
3865
|
+
return '<span class="note">' + esc('As a task in ' + fileDomainLabel()) + '</span>';
|
|
3866
|
+
}
|
|
3867
|
+
function fileDomainEntry() {
|
|
3868
|
+
var d = ASK_FILE.domains || [];
|
|
3869
|
+
return d.filter(function (x) { return x && x.name === 'quilt'; })[0] || d.filter(function (x) { return x && x.name; })[0] || null;
|
|
3870
|
+
}
|
|
3871
|
+
function fileDomain() { var e = fileDomainEntry(); return e ? e.name : 'quilt'; }
|
|
3872
|
+
function fileDomainLabel() { var e = fileDomainEntry(); return (e && e.label) || domainLabel(fileDomain()); }
|
|
3873
|
+
function loadFileDomains() {
|
|
3874
|
+
if (ASK_FILE.domains) return Promise.resolve();
|
|
3875
|
+
return api('/api/domains').then(function (d) { ASK_FILE.domains = (d && Array.isArray(d.domains)) ? d.domains : []; }, function () { ASK_FILE.domains = null; });
|
|
3876
|
+
}
|
|
3877
|
+
function fileToInbox(text) {
|
|
3878
|
+
if (ASK_FILE.busy || !text) return;
|
|
3879
|
+
if (ASK_FILE.text !== text) { ASK_FILE.text = text; ASK_FILE.id = webJobId(); }
|
|
3880
|
+
ASK_FILE.busy = true; ASK_FILE.note = null; renderAskFile();
|
|
3881
|
+
loadFileDomains().then(function () {
|
|
3882
|
+
return api('/api/tasks/capture', { method: 'POST', body: { domain: fileDomain(), text: text, section: 'inbox', captureId: ASK_FILE.id }, timeoutMs: 20000 });
|
|
3883
|
+
}).then(function () {
|
|
3884
|
+
ASK_FILE.busy = false; ASK_FILE.text = ''; ASK_FILE.note = null;
|
|
3885
|
+
var box = $('ask-q'); if (box && box.value.trim() === text) { box.value = ''; renderAsk(); }
|
|
3886
|
+
renderAskFile();
|
|
3887
|
+
toast('Filed to the Work inbox.');
|
|
3888
|
+
if (PANES.work && PANES.work.intakeChanged) PANES.work.intakeChanged(); // Work reads its board again when it opens
|
|
3889
|
+
}, function (err) {
|
|
3890
|
+
ASK_FILE.busy = false;
|
|
3891
|
+
var line = 'Not filed: ' + ((err && err.message) || 'your Mac did not answer') + ' Filing again is safe.';
|
|
3892
|
+
ASK_FILE.note = { text: text, line: line, error: true };
|
|
3893
|
+
renderAskFile();
|
|
3894
|
+
toast(line);
|
|
3895
|
+
});
|
|
3896
|
+
}
|
|
3897
|
+
function renderAskFile() {
|
|
3898
|
+
var b = document.querySelector('#ask-results [data-action="ask-file"]'); if (!b) return;
|
|
3899
|
+
b.disabled = ASK_FILE.busy;
|
|
3900
|
+
var g = b.querySelector('.grow'); if (!g) return;
|
|
3901
|
+
var q = ($('ask-q') && $('ask-q').value.trim()) || '';
|
|
3902
|
+
g.innerHTML = esc(ASK_FILE.busy ? 'Filing\u2026' : 'File to Work inbox') + fileNoteHtml(q);
|
|
3903
|
+
}
|
|
3416
3904
|
function openAsk(prefill) {
|
|
3905
|
+
loadFileDomains().then(function () { if (!$('ask-sheet').classList.contains('hidden') && $('ask-q').value.trim()) renderAsk(); });
|
|
3417
3906
|
$('ask-sheet').classList.remove('hidden');
|
|
3418
3907
|
document.body.style.overflow = 'hidden';
|
|
3419
3908
|
var input = $('ask-q'); input.value = prefill || '';
|
|
@@ -3434,7 +3923,8 @@
|
|
|
3434
3923
|
}
|
|
3435
3924
|
var jumps = JUMPS.filter(function (j) { return j[1].toLowerCase().indexOf(q.toLowerCase()) === 0; });
|
|
3436
3925
|
out.innerHTML = '<div class="list" style="margin-top:14px">' +
|
|
3437
|
-
'<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(q) + '
|
|
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">“' + esc(q) + '”' + (state.pane === 'today' ? ' with what Today shows' : '') + '</span></span></button>' +
|
|
3927
|
+
'<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>' +
|
|
3438
3928
|
'<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>' +
|
|
3439
3929
|
'<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>' +
|
|
3440
3930
|
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('') +
|
|
@@ -3451,7 +3941,7 @@
|
|
|
3451
3941
|
|
|
3452
3942
|
// ── events (one delegated listener) ─────────────────────
|
|
3453
3943
|
document.addEventListener('click', function (e) {
|
|
3454
|
-
var t = e.target.closest('[data-goto],[data-pane],[data-session],[data-meeting],[data-speakers],[data-action],[data-open-ask],[data-theme-choice],#topbar-back,#section-btn,#sign-out,#copy-link,#ask-close,#auth-switch,#auth-token-show,#device-code-btn');
|
|
3944
|
+
var t = e.target.closest('[data-today-next],[data-today-needs],[data-today],[data-goto],[data-pane],[data-session],[data-meeting],[data-speakers],[data-action],[data-open-ask],[data-theme-choice],#topbar-back,#section-btn,#sign-out,#copy-link,#ask-close,#auth-switch,#auth-token-show,#device-code-btn');
|
|
3455
3945
|
if (!t) { if (e.target.id === 'ask-sheet') closeAsk(); if (e.target.id === 'section-menu') closeMenu(); return; }
|
|
3456
3946
|
var inSheet = !!t.closest('#ask-sheet');
|
|
3457
3947
|
if (t.id === 'topbar-back') return goBack();
|
|
@@ -3482,7 +3972,16 @@
|
|
|
3482
3972
|
return;
|
|
3483
3973
|
}
|
|
3484
3974
|
if (t.hasAttribute('data-open-ask')) return openAsk();
|
|
3485
|
-
if (t.hasAttribute('data-
|
|
3975
|
+
if (t.hasAttribute('data-today')) return openTodayAction(t);
|
|
3976
|
+
if (t.hasAttribute('data-today-next')) return prepNext();
|
|
3977
|
+
if (t.hasAttribute('data-today-needs')) {
|
|
3978
|
+
var which = t.getAttribute('data-today-needs');
|
|
3979
|
+
if (which === 'all') TODAY.showAllNeeds = true;
|
|
3980
|
+
else if (which === 'later') TODAY.showLater = true;
|
|
3981
|
+
else { TODAY.extraAt = 0; return loadToday(); }
|
|
3982
|
+
return paintNeeds();
|
|
3983
|
+
}
|
|
3984
|
+
if (t.hasAttribute('data-theme-choice')) { window.CosTheme.set(t.getAttribute('data-theme-choice')); paintAppearance(); return; }
|
|
3486
3985
|
if (t.hasAttribute('data-goto') || t.hasAttribute('data-pane')) {
|
|
3487
3986
|
if (inSheet) closeAsk(); closeMenu();
|
|
3488
3987
|
// "work/sort" is a pane and its arguments, not one name.
|
|
@@ -3506,11 +4005,12 @@
|
|
|
3506
4005
|
if (action === 'expand-turn') { var tr = document.querySelector('.turn[data-turn="' + t.getAttribute('data-turn') + '"]'); if (tr) tr.classList.remove('clamped'); t.remove(); return; }
|
|
3507
4006
|
var q = $('ask-q').value.trim();
|
|
3508
4007
|
if (action === 'ask-cos' && q) { askCos(q, $('ask-status')).then(function (where) { closeAsk(); go('messages', Array.isArray(where) ? where : []); }).catch(function () {}); return; }
|
|
4008
|
+
if (action === 'ask-file' && q) return fileToInbox(q);
|
|
3509
4009
|
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; }
|
|
3510
4010
|
if (action === 'ask-work' && q) {
|
|
3511
4011
|
closeAsk(); go('work');
|
|
3512
4012
|
// The Work pane (panes/work.js) owns its search box; hand it the query.
|
|
3513
|
-
if (PANES.work && PANES.work.search) PANES.work.search(q);
|
|
4013
|
+
ensurePane('work').then(function () { if (PANES.work && PANES.work.search) PANES.work.search(q); }, function () {});
|
|
3514
4014
|
}
|
|
3515
4015
|
});
|
|
3516
4016
|
|
|
@@ -3554,14 +4054,14 @@
|
|
|
3554
4054
|
});
|
|
3555
4055
|
});
|
|
3556
4056
|
window.addEventListener('hashchange', route);
|
|
3557
|
-
// Folding or unfolding the Duo crosses the 600px line: keep the hosted Memories page in step.
|
|
3558
|
-
window.matchMedia('(min-width: 600px)').addEventListener('change', syncFrameTheme);
|
|
3559
4057
|
window.addEventListener('scroll', onScroll, { passive: true });
|
|
3560
4058
|
document.addEventListener('visibilitychange', function () {
|
|
3561
4059
|
if (document.hidden || $('app-shell').classList.contains('hidden')) return;
|
|
3562
4060
|
// Coming back to the page refreshes Today in place. Other panes keep what is on screen:
|
|
3563
4061
|
// rebuilding them here moved the page under Miles's finger ("it reloads the section").
|
|
3564
4062
|
if (state.pane === 'today' && Date.now() - (state.lastLoad.today || 0) > 60000) { state.lastLoad.today = Date.now(); loadToday(); }
|
|
4063
|
+
// Back within the minute: Today keeps its data, but the pet's loop stopped while hidden, so restart it.
|
|
4064
|
+
else if (state.pane === 'today' && PET.pose && !PET.timer) renderPet(state.sessions || []);
|
|
3565
4065
|
});
|
|
3566
4066
|
window.addEventListener('message', function (e) {
|
|
3567
4067
|
if (e.origin !== location.origin) return;
|
|
@@ -3603,6 +4103,9 @@
|
|
|
3603
4103
|
readStore: readStore, writeStore: writeStore, copyText: copyText, isPhone: isPhone,
|
|
3604
4104
|
state: state, titles: TITLES, go: go, parseRoute: parseRoute, goBack: goBack,
|
|
3605
4105
|
registerPane: function (name, def) { PANES[name] = def; },
|
|
4106
|
+
sessionList: sessionList,
|
|
4107
|
+
petPrefs: petPrefs,
|
|
4108
|
+
setPetPrefs: setPetPrefs,
|
|
3606
4109
|
onRoute: function (fn) { routeHooks.push(fn); },
|
|
3607
4110
|
onShell: function (fn) { shellHooks.push(fn); },
|
|
3608
4111
|
// The detail title after "COS Control > Section" (Control's breadcrumb). Pane modules set it
|
|
@@ -3616,6 +4119,41 @@
|
|
|
3616
4119
|
sessionState: function (s) { return sxState(s); },
|
|
3617
4120
|
isKeepWarm: function (s) { return sxKeepWarm(s); }
|
|
3618
4121
|
};
|
|
4122
|
+
// Today as a place to act (panes/today-actions.js, Miles 11/06): what the Today cards show, a repaint after a
|
|
4123
|
+
// decision, and the Sessions reader's own pieces, so the overlay sends and reads through the reader's code.
|
|
4124
|
+
window.COSWeb.pane = function (name) { return PANES[name] || null; };
|
|
4125
|
+
// Tap, then tap again (Today's Dismiss, Stop and Send anyway; a question card's Allow, Deny and Send): the second tap
|
|
4126
|
+
// counts only once the first has been on screen ARM_CONFIRM_MS, so a double tap or a finger bounce arms and never
|
|
4127
|
+
// confirms (W2-r2). The one rule both panes/today-actions.js and panes/questions.js read.
|
|
4128
|
+
var ARM_CONFIRM_MS = 400;
|
|
4129
|
+
window.COSWeb.armConfirmMs = ARM_CONFIRM_MS;
|
|
4130
|
+
window.COSWeb.confirmReady = function (armedAt) { return typeof armedAt === 'number' && Date.now() - armedAt >= ARM_CONFIRM_MS; };
|
|
4131
|
+
window.COSWeb.today = {
|
|
4132
|
+
data: function () {
|
|
4133
|
+
return { needs: TODAY.needs, working: TODAY.working, open: TODAY.open, welcome: TODAY.welcome, offline: TODAY.offline, at: TODAY.at,
|
|
4134
|
+
sessions: state.sessions || [], intake: todayIntakeOpen(), intakeCaps: TODAY.intakeCaps, intakeLoaded: TODAY.intake !== null };
|
|
4135
|
+
},
|
|
4136
|
+
needWhy: needWhy, sessionTitle: sessionTitle, badge: badge, span: span, splitSessionId: splitSessionId,
|
|
4137
|
+
// Needs you as Today lists it (all four sources), for the overlay's summary and the rail.
|
|
4138
|
+
needs: function () { return TODAY.model; },
|
|
4139
|
+
railLine: function () { return TODAY.model ? { text: todayRailLine(TODAY.model), hot: TODAY.model.now.length > 0 } : null; },
|
|
4140
|
+
markGone: function (id, gone) { if (gone) TODAY.gone[id] = 1; else delete TODAY.gone[id]; paintTodayIntake(); },
|
|
4141
|
+
reloadIntake: function () { return loadTodayIntake(); },
|
|
4142
|
+
refresh: function () { if (state.pane === 'today') loadToday(); }
|
|
4143
|
+
};
|
|
4144
|
+
window.COSWeb.sessionKit = {
|
|
4145
|
+
// A session object for continueSession, with the overlay's own view (READER_VIEW's methods).
|
|
4146
|
+
session: function (provider, id, view) {
|
|
4147
|
+
return { provider: provider, id: id, draft: null, held: [], view: view, verdict: null, refusal: null, cautionAck: false, feed: newFeed(), live: null, queued: 0 };
|
|
4148
|
+
},
|
|
4149
|
+
send: function (r, text, opts) { return continueSession(r, text, opts || {}); },
|
|
4150
|
+
newKey: newTurnKey,
|
|
4151
|
+
probe: function (r) { return sxProbeAttach(r); },
|
|
4152
|
+
path: sessionPath, answer: apiAnswer,
|
|
4153
|
+
feed: { fresh: newFeed, apply: applyFeed, word: sxFeedWord, detail: sxFeedDetail, tool: sxToolLine, elapsed: sxElapsed, fallback: sxFallbackLine },
|
|
4154
|
+
title: sxTitle, stateOf: sxState, stateLabel: function (s) { return sxStateLabel(s, sxState(s)); },
|
|
4155
|
+
parkCopy: function (p) { return SX_PARK[sxProvider(p)]; }, providerLabel: sxProviderLabel, markdown: sxMd
|
|
4156
|
+
};
|
|
3619
4157
|
|
|
3620
4158
|
// Every pane module is a plain script after this one; boot waits for all of them.
|
|
3621
4159
|
function boot() {
|