@gotcos/glasses-server 7.0.7 → 7.0.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +19 -0
- package/CHANGELOG.md +170 -0
- package/docs/web-key-verifier-contract.md +378 -0
- package/managed-runtime-contract.json +9 -1
- package/package.json +10 -3
- package/server/bootstrap.ts +10 -0
- package/server/index.ts +126 -23
- package/server/lib/agent-session-recent.ts +321 -0
- package/server/lib/agent-session-store.ts +3 -3
- package/server/lib/api-auth.ts +37 -1
- package/server/lib/calendar-meetings.ts +70 -0
- package/server/lib/calendar-source.ts +54 -9
- package/server/lib/claude-hooks-installer.ts +111 -12
- package/server/lib/conversation.ts +56 -28
- package/server/lib/cors-reject-log.ts +51 -0
- package/server/lib/disk-io-meter.ts +202 -0
- package/server/lib/drop-service.ts +47 -12
- package/server/lib/drop-store.ts +21 -1
- package/server/lib/event-loop-stall.ts +62 -0
- package/server/lib/full-hold-engine.ts +114 -0
- package/server/lib/full-hold.ts +124 -0
- package/server/lib/g2-turn-provenance.ts +128 -9
- package/server/lib/listener-startup.ts +18 -0
- package/server/lib/meeting-batch-transcribe.ts +32 -5
- package/server/lib/permission-broker.ts +443 -11
- package/server/lib/secret-path-guard.ts +4 -1
- package/server/lib/session-cache-writer.ts +8 -1
- package/server/lib/session-hook-events.ts +23 -1
- package/server/lib/session-hooks-runtime.ts +34 -0
- package/server/lib/session-log.ts +313 -4
- package/server/lib/session-state-derive.ts +36 -0
- package/server/lib/session-stream-events.ts +25 -1
- package/server/lib/today-brief.ts +522 -58
- package/server/lib/turn-end-ask.ts +320 -0
- package/server/lib/turn-footer.ts +3 -3
- package/server/lib/web-control-integrity.ts +160 -0
- package/server/lib/web-control-offnet-page.ts +58 -0
- package/server/lib/web-control-static.ts +53 -9
- package/server/lib/web-key-authority.ts +372 -0
- package/server/lib/web-key-envelope.ts +98 -0
- package/server/lib/web-key-handoff.ts +135 -0
- package/server/lib/web-key-origin.ts +21 -0
- package/server/lib/web-key-passkey.ts +155 -0
- package/server/lib/web-key-registry.ts +825 -0
- package/server/lib/web-key-signer-app.ts +152 -0
- package/server/lib/web-session-auth.ts +133 -15
- package/server/lib/whisper-local.ts +93 -7
- package/server/lib/whisper-preview.ts +5 -3
- package/server/lib/work-orchestrator/snapshot.ts +17 -3
- package/server/lib/work-review-runtime.ts +17 -4
- package/server/routes/agent-session-bindings.ts +16 -1
- package/server/routes/agent-session-stream.ts +7 -0
- package/server/routes/drops.ts +7 -2
- package/server/routes/g2-turns.ts +35 -0
- package/server/routes/health.ts +11 -1
- package/server/routes/live-events.ts +6 -1
- package/server/routes/permission-broker.ts +26 -0
- package/server/routes/query-jobs.ts +3 -0
- package/server/routes/sessions.ts +15 -24
- package/server/routes/thread-turn-queue.ts +16 -9
- package/server/routes/today-brief.ts +91 -36
- package/server/routes/web-control.ts +37 -4
- package/server/routes/web-key.ts +284 -0
- package/server/routes/welcome-context.ts +6 -2
- package/server/routes/work-orchestrator.ts +12 -7
- package/server/scripts/write-web-control-manifest.mjs +43 -0
- package/server/web-control/.manifest.json +44 -0
- package/server/web-control/app.css +36 -0
- package/server/web-control/app.js +953 -57
- package/server/web-control/apple-touch-icon.png +0 -0
- package/server/web-control/index.html +14 -1
- package/server/web-control/panes/cos-chat.css +52 -6
- package/server/web-control/panes/cos-chat.js +364 -32
- package/server/web-control/panes/questions.js +135 -14
- package/server/web-control/panes/settings.js +1 -1
- package/server/web-control/panes/today-actions.css +35 -0
- package/server/web-control/panes/today-actions.js +75 -6
- package/server/web-control/panes/work.css +3 -0
- package/server/web-control/panes/work.js +178 -35
- package/server/web-signer/.manifest.json +11 -0
- package/server/web-signer/index.html +74 -0
- package/server/web-signer/signer-proof.js +348 -0
- package/server/web-signer/signer-store.js +39 -0
- package/server/web-signer/signer.css +23 -0
- package/server/web-signer/signer.js +341 -0
- package/shared/web-approval-packets.golden.json +124 -0
- package/shared/web-turn-envelope.ts +271 -0
|
@@ -29,9 +29,12 @@
|
|
|
29
29
|
finishes, POST /api/cos-thread/turn once with its answer. "New thread" in the header posts /api/cos-thread/new.
|
|
30
30
|
Without the capability, the old rule stands: one conversation per day, kept in this browser.
|
|
31
31
|
|
|
32
|
-
The rail badge on Messages
|
|
33
|
-
|
|
34
|
-
|
|
32
|
+
The rail badge on Messages (7.0.9, Miles 11/08: "notifications would just be for net new messages") is a number for
|
|
33
|
+
`unread` only: what COS sent since the chat was last open on any device (the server keeps readAt, and the first
|
|
34
|
+
7.0.9 open marked everything older as read). A dot, with no number, says something in the chat waits on your reply
|
|
35
|
+
(`asked`). Work's Your move is not counted here: it has its own count on Today and in Work. It is read with the
|
|
36
|
+
shell's existing status read (shell.js checkServer), never on a timer of its own, and painting it touches only the
|
|
37
|
+
badge, never the pane. */
|
|
35
38
|
(function () {
|
|
36
39
|
'use strict';
|
|
37
40
|
var W = window.COSWeb;
|
|
@@ -40,6 +43,8 @@
|
|
|
40
43
|
var DRAFTS = 'cos.web.cosDrafts';
|
|
41
44
|
|
|
42
45
|
var C = {
|
|
46
|
+
archive: null, // 7.0.9: the older questions (GET /api/inbox/archive) once opened, newest first
|
|
47
|
+
archiveOpen: false,
|
|
43
48
|
caps: null, // null until /api/health is read: { dropIt, jobs, steer }
|
|
44
49
|
capsLoading: null,
|
|
45
50
|
summary: null, // { unread, waiting, lastLine, lastAt }
|
|
@@ -114,7 +119,9 @@
|
|
|
114
119
|
// reply box shows no clip, so a file can never be dropped from a turn in silence.
|
|
115
120
|
turnFiles: h && Number.isInteger(h.threadTurnAttachments) && h.threadTurnAttachments > 0 ? Math.min(h.threadTurnAttachments, MAX_ATTS) : 0,
|
|
116
121
|
// 6.99.27: any file for a session turn, up to this many bytes (threadTurnFileBytes). 0: the chat's list only.
|
|
117
|
-
turnFileBytes: h && Number.isInteger(h.threadTurnFileBytes) && h.threadTurnFileBytes > 0 ? Math.min(h.threadTurnFileBytes, MAX_DOC) : 0
|
|
122
|
+
turnFileBytes: h && Number.isInteger(h.threadTurnFileBytes) && h.threadTurnFileBytes > 0 ? Math.min(h.threadTurnFileBytes, MAX_DOC) : 0,
|
|
123
|
+
// 6.99.31: the Mac runs the phone key signer (webKeySigner 1, only with COS_WEB_KEY_AUTHORITY on). Absent: 0, nothing offered.
|
|
124
|
+
webKeySigner: h && h.webKeySigner === 1 ? 1 : 0
|
|
118
125
|
};
|
|
119
126
|
if (next.prep && next.prep.serverInstanceId) PREP_INST = next.prep.serverInstanceId;
|
|
120
127
|
var changed = !C.caps || C.caps.dropIt !== next.dropIt || C.caps.jobs !== next.jobs || C.caps.thread !== next.thread;
|
|
@@ -133,7 +140,7 @@
|
|
|
133
140
|
serverInstanceId: typeof p.serverInstanceId === 'string' ? p.serverInstanceId : '' };
|
|
134
141
|
}
|
|
135
142
|
// The shell's status read (shell.js checkServer) hands its /api/health answer here.
|
|
136
|
-
function fromHealth(h) { setCaps(h); }
|
|
143
|
+
function fromHealth(h) { setCaps(h); if (C.asks.some(relayPending)) loadRelayRows(15000); }
|
|
137
144
|
function ensureCaps() {
|
|
138
145
|
if (C.caps) return Promise.resolve(C.caps);
|
|
139
146
|
if (!C.capsLoading) C.capsLoading = api('/api/health').then(function (h) { setCaps(h); return C.caps; }, function () { C.capsLoading = null; return null; });
|
|
@@ -149,10 +156,12 @@
|
|
|
149
156
|
return null;
|
|
150
157
|
});
|
|
151
158
|
}
|
|
152
|
-
|
|
159
|
+
// `asked` is the chat's own part of `waiting` (7.0.9). An older server without it: the chat's part cannot be told
|
|
160
|
+
// apart from Work's, so nothing is claimed as waiting on a reply here.
|
|
161
|
+
function waitingOf(s) { var n = s && Number(s.asked); return Number.isInteger(n) && n > 0 ? n : 0; }
|
|
153
162
|
function unreadOf(s) { var n = s && Number(s.unread); return Number.isInteger(n) && n > 0 ? n : 0; }
|
|
154
|
-
// A number for what
|
|
155
|
-
// is untouched.
|
|
163
|
+
// A number for what is new, a dot for a reply the chat waits on. Created on first paint, so with Drop it off the
|
|
164
|
+
// rail's DOM is untouched.
|
|
156
165
|
function paintRail() {
|
|
157
166
|
var waiting = waitingOf(C.summary), unread = unreadOf(C.summary);
|
|
158
167
|
document.querySelectorAll('.rail-btn[data-pane="messages"], #section-menu [data-goto="messages"]').forEach(function (b) {
|
|
@@ -162,10 +171,10 @@
|
|
|
162
171
|
var dot = document.createElement('span'); dot.className = 'inbox-dot hidden'; dot.setAttribute('data-inbox-dot', '');
|
|
163
172
|
b.appendChild(count); b.appendChild(dot);
|
|
164
173
|
}
|
|
165
|
-
count.textContent =
|
|
166
|
-
count.classList.toggle('hidden', !
|
|
167
|
-
b.querySelector('[data-inbox-dot]').classList.toggle('hidden', !(
|
|
168
|
-
var label =
|
|
174
|
+
count.textContent = unread > 99 ? '99+' : String(unread);
|
|
175
|
+
count.classList.toggle('hidden', !unread);
|
|
176
|
+
b.querySelector('[data-inbox-dot]').classList.toggle('hidden', !(waiting && !unread));
|
|
177
|
+
var label = [unread ? unread + ' new in COS' : '', waiting ? waiting + ' waiting on your reply' : ''].filter(Boolean).join(', ');
|
|
169
178
|
b.setAttribute('aria-label', 'Messages' + (label ? ', ' + label : ''));
|
|
170
179
|
});
|
|
171
180
|
}
|
|
@@ -179,7 +188,7 @@
|
|
|
179
188
|
// ── the pinned row (first in Recent) ──────────────────
|
|
180
189
|
function pinStatus() {
|
|
181
190
|
var s = C.summary, w = waitingOf(s), u = unreadOf(s);
|
|
182
|
-
return [
|
|
191
|
+
return [u ? u + ' new' : '', w ? w + ' waiting on your reply' : ''].filter(Boolean).join(' · ');
|
|
183
192
|
}
|
|
184
193
|
function pinnedRowHtml(selected) {
|
|
185
194
|
var s = dropOn() ? C.summary : null;
|
|
@@ -345,7 +354,7 @@
|
|
|
345
354
|
(threadOn() ? '<button type="button" class="link-btn" data-cos="new-thread">New thread</button>' : '') + '</div>' +
|
|
346
355
|
(threadOn() ? '<p class="cc-thread" data-cos-thread role="status">' + esc(threadLine()) + '</p>' : '') + '</header>' +
|
|
347
356
|
'<div class="cc-feed" data-cos-feed aria-live="polite"></div>' +
|
|
348
|
-
composerHtml(CHAT, { drop: drop, placeholder: drop ? 'Ask COS
|
|
357
|
+
composerHtml(CHAT, { drop: drop, placeholder: drop ? 'Ask COS or drop a thought' : 'Ask COS anything', label: 'Message to COS' }) +
|
|
349
358
|
'</section>';
|
|
350
359
|
}
|
|
351
360
|
function mount(host, visible) {
|
|
@@ -393,12 +402,39 @@
|
|
|
393
402
|
});
|
|
394
403
|
C.items.sort(function (a, b) { return String(a.createdAt).localeCompare(String(b.createdAt)); });
|
|
395
404
|
}
|
|
405
|
+
// ── older questions (7.0.9, Miles 11/08: "Expire after 7 days") ──
|
|
406
|
+
// What still asks for a reply but was asked 7 days ago or more stops counting as waiting (server rule
|
|
407
|
+
// WAITING_EXPIRY_DAYS, drop-service.ts) and shows here, closed until opened. Each still answers as in the chat, and
|
|
408
|
+
// answering it takes it off this list. The count and the days come from the server, so no rule is kept here.
|
|
409
|
+
function archivedOf(s) { var n = s && Number(s.archived); return Number.isInteger(n) && n > 0 ? n : 0; }
|
|
410
|
+
function archiveHtml() {
|
|
411
|
+
var n = C.archive ? C.archive.length : archivedOf(C.summary);
|
|
412
|
+
if (!n) return '';
|
|
413
|
+
var days = (C.summary && Number(C.summary.waitingExpiryDays)) || 7;
|
|
414
|
+
var body = !C.archiveOpen ? '' : C.archive === null ? '<p class="cc-fine">Reading older questions.</p>'
|
|
415
|
+
: C.archive.map(itemHtml).join('');
|
|
416
|
+
return '<details class="cc-archive" data-cos-archive' + (C.archiveOpen ? ' open' : '') + '><summary data-cos="archive">' +
|
|
417
|
+
n + (n === 1 ? ' older question' : ' older questions') + '</summary>' +
|
|
418
|
+
'<p class="cc-fine">Asked more than ' + days + ' days ago, so ' + (n === 1 ? 'it no longer counts' : 'they no longer count') + ' as waiting on you. You can still answer here.</p>' +
|
|
419
|
+
body + '</details>';
|
|
420
|
+
}
|
|
421
|
+
function loadArchive() {
|
|
422
|
+
return api('/api/inbox/archive').then(function (d) {
|
|
423
|
+
C.archive = (d && Array.isArray(d.items)) ? d.items : [];
|
|
424
|
+
paintFeed(false);
|
|
425
|
+
}, function () { if (C.archive === null) C.archive = []; paintFeed(false); });
|
|
426
|
+
}
|
|
427
|
+
function toggleArchive() {
|
|
428
|
+
C.archiveOpen = !C.archiveOpen;
|
|
429
|
+
if (C.archiveOpen) { C.archive = null; loadArchive(); }
|
|
430
|
+
else paintFeed(false);
|
|
431
|
+
}
|
|
396
432
|
function markReadIfShown(force) {
|
|
397
433
|
if (!dropOn() || !chatVisible() || !C.items || !C.items.length) return;
|
|
398
434
|
if (!force && C.summary && !unreadOf(C.summary)) return;
|
|
399
435
|
var last = C.items[C.items.length - 1];
|
|
400
436
|
api('/api/inbox/read', { method: 'POST', body: { through: last.id } }).then(function (r) {
|
|
401
|
-
if (C.summary) { C.summary.unread = r.unread; if (typeof r.waiting === 'number') C.summary.waiting = r.waiting; }
|
|
437
|
+
if (C.summary) { C.summary.unread = r.unread; if (typeof r.waiting === 'number') C.summary.waiting = r.waiting; if (typeof r.asked === 'number') C.summary.asked = r.asked; }
|
|
402
438
|
paintRail(); paintPin();
|
|
403
439
|
}, function () {});
|
|
404
440
|
}
|
|
@@ -482,10 +518,159 @@
|
|
|
482
518
|
function askState(a) {
|
|
483
519
|
return a.status === 'completed' ? 'Answered' : a.status === 'canceled' ? 'Canceled' : a.status === 'failed' || a.status === 'interrupted' ? 'Did not finish' : a.status === 'sending' ? 'Sending' : 'Answering';
|
|
484
520
|
}
|
|
521
|
+
// ── what a running answer is doing, and where a relayed message went (7.0.9) ──
|
|
522
|
+
// Miles 11/08: "It's hard to see when a session is making progress, whereas when you're wearing it on the glasses, we
|
|
523
|
+
// can see the whole audit trail." The steps are the job's own trail (GET /api/query-jobs/:id?trail=1, the lens's
|
|
524
|
+
// data, redacted for secrets on the Mac at the journal), read by the poll this card already runs. While it runs: the
|
|
525
|
+
// newest few, newest last. Once it ends: a quiet "N steps" that opens.
|
|
526
|
+
var TERMINAL = { completed: 1, failed: 1, canceled: 1, interrupted: 1 };
|
|
527
|
+
function askRunning(a) { return !TERMINAL[a.status]; }
|
|
528
|
+
function hasText(a) { return !!(a.response || a.partial); }
|
|
529
|
+
var STEP_VERBS = { read: 'Read', edit: 'Edited', write: 'Edited', bash: 'Ran', search: 'Searched', task: 'Delegated', other: 'Used' };
|
|
530
|
+
var LIVE_STEPS = 6, DONE_STEPS = 40;
|
|
531
|
+
// A path on the Mac shows as its last part only. Paths are not secrets on the Mac, but the web never shows one.
|
|
532
|
+
var MAC_PATH = /(?:~|\/(?:Users|home|private|var|tmp|opt|Volumes|Library|System|Applications|etc))(?:\/[^\s'"`()<>\[\]{},;|]+)+\/?/g;
|
|
533
|
+
function scrubPaths(text) {
|
|
534
|
+
return String(text || '').replace(MAC_PATH, function (p) { var parts = p.replace(/\/+$/, '').split('/'); return parts[parts.length - 1] || 'a file'; });
|
|
535
|
+
}
|
|
536
|
+
function stepText(e) {
|
|
537
|
+
if (!e || typeof e !== 'object') return null;
|
|
538
|
+
if (e.kind === 'tool') {
|
|
539
|
+
var target = clip(scrubPaths(e.target), 90), detail = clip(scrubPaths(e.detail), 60);
|
|
540
|
+
if (target === 'SendMessage') return { kind: 'tool', text: 'Sent a message' + (detail ? ' ' + detail : '') };
|
|
541
|
+
var verb = STEP_VERBS[e.verb] || 'Used';
|
|
542
|
+
return { kind: 'tool', text: (target ? verb + ' ' + target : verb) + (detail ? ' (' + detail + ')' : '') };
|
|
543
|
+
}
|
|
544
|
+
if (e.kind === 'status' && (e.detail || e.needs_action)) return { kind: 'note', text: clip(scrubPaths(e.detail || e.needs_action), 140) };
|
|
545
|
+
if (e.kind === 'prose' && e.text) return { kind: 'prose', text: clip(scrubPaths(e.text), 160) };
|
|
546
|
+
return null;
|
|
547
|
+
}
|
|
548
|
+
function stepsOf(a) {
|
|
549
|
+
var out = [];
|
|
550
|
+
(a.trail || []).forEach(function (e) { var t = stepText(e); if (t && t.text) out.push(t); });
|
|
551
|
+
return out;
|
|
552
|
+
}
|
|
553
|
+
function stepLi(t) { return '<li class="cc-step ' + t.kind + '">' + esc(t.text) + '</li>'; }
|
|
554
|
+
function earlierLi(n) { return n > 0 ? '<li class="cc-step more">' + n + ' earlier ' + (n === 1 ? 'step' : 'steps') + '</li>' : ''; }
|
|
555
|
+
function stepsHtml(a) {
|
|
556
|
+
var steps = stepsOf(a);
|
|
557
|
+
if (!steps.length) return '';
|
|
558
|
+
if (askRunning(a)) {
|
|
559
|
+
var live = steps.slice(-LIVE_STEPS);
|
|
560
|
+
return '<ol class="cc-steps live" aria-label="What COS is doing">' + earlierLi(steps.length - live.length) + live.map(stepLi).join('') + '</ol>';
|
|
561
|
+
}
|
|
562
|
+
var kept = steps.slice(-DONE_STEPS);
|
|
563
|
+
return '<details class="cc-steps-done"' + (a.stepsOpen ? ' open' : '') + '><summary data-cos="steps" data-cid="' + esc(a.cid) + '">' +
|
|
564
|
+
steps.length + (steps.length === 1 ? ' step' : ' steps') + '</summary><ol class="cc-steps">' + earlierLi(steps.length - kept.length) + kept.map(stepLi).join('') + '</ol></details>';
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// Issue 4 (Miles 11/08, Message #465 "Why aren't you responding to the other session?"): an answer that sent a message
|
|
568
|
+
// to another session (SendMessage in its trail) gets that session's reply in that session, never here. The card says
|
|
569
|
+
// where it went, opens that session, and shows its newest reply once the session has answered. Read from the session
|
|
570
|
+
// list the shell already keeps (W.sessionList, cached), refreshed by the live stream and the shell's status read.
|
|
571
|
+
function relaysOf(a) {
|
|
572
|
+
var out = [];
|
|
573
|
+
(a.trail || []).forEach(function (e) {
|
|
574
|
+
if (!e || e.kind !== 'tool' || String(e.target) !== 'SendMessage') return;
|
|
575
|
+
var m = /^to (.+)$/.exec(String(e.detail || '').trim());
|
|
576
|
+
var name = m && m[1] !== 'another session' && m[1] !== 'every session' ? m[1].trim() : '';
|
|
577
|
+
if (out.indexOf(name) < 0) out.push(name);
|
|
578
|
+
});
|
|
579
|
+
return out.slice(-2);
|
|
580
|
+
}
|
|
581
|
+
function rowTitle(r) { return String(r.display_label || r.custom_title || r.title || '').replace(/\s+/g, ' ').trim(); }
|
|
582
|
+
function relayRow(name) {
|
|
583
|
+
if (!name || !Array.isArray(C.sessionRows)) return null;
|
|
584
|
+
var want = name.toLowerCase(), sid = /^session ([0-9a-f]{8})$/i.exec(name);
|
|
585
|
+
for (var i = 0; i < C.sessionRows.length; i++) {
|
|
586
|
+
var r = C.sessionRows[i], id = String(r.session_id || r.id || '').replace(/^(claude|codex|cursor):/, '');
|
|
587
|
+
if (sid ? id.toLowerCase().indexOf(sid[1].toLowerCase()) === 0 : rowTitle(r).toLowerCase() === want || id.toLowerCase() === want) return r;
|
|
588
|
+
}
|
|
589
|
+
return null;
|
|
590
|
+
}
|
|
591
|
+
function msOf(v) { var d = W.toDate(v); return d ? d.getTime() : 0; }
|
|
592
|
+
// The session's newest reply, only once it came after this Ask started.
|
|
593
|
+
function relayReply(r, a) {
|
|
594
|
+
var text = r && String(r.last_reply || '').trim();
|
|
595
|
+
if (!text) return '';
|
|
596
|
+
var at = msOf(r.state_since) || msOf(r.last_activity_at);
|
|
597
|
+
return at && at > (a.at || 0) ? text : '';
|
|
598
|
+
}
|
|
599
|
+
function relayHtml(a) {
|
|
600
|
+
var names = relaysOf(a);
|
|
601
|
+
if (!names.length) return '';
|
|
602
|
+
return '<div class="cc-relays" data-ask-relay>' + names.map(function (name) {
|
|
603
|
+
var r = relayRow(name), title = r ? rowTitle(r) || name : name, reply = relayReply(r, a);
|
|
604
|
+
var id = r ? String(r.session_id || r.id || '').replace(/^(claude|codex|cursor):/, '') : '';
|
|
605
|
+
var where = title ? 'Sent to ' + title + '.' : 'Sent to another session.';
|
|
606
|
+
return '<div class="cc-relay"><p class="cc-relay-line">' + esc(where) + ' Its answer shows in that session, not here.</p>' +
|
|
607
|
+
(reply ? '<p class="cc-relay-reply"><span class="cc-relay-who">' + esc(title || 'The session') + ' replied</span>' + esc(clip(scrubPaths(reply), 400)) + '</p>' :
|
|
608
|
+
askRunning(a) ? '' : '<p class="cc-fine">Its reply shows here when it answers.</p>') +
|
|
609
|
+
'<div class="cc-acts">' + (r ? '<button type="button" class="cx-btn" data-cos="open-session" data-provider="' + esc(r.provider || 'claude') + '" data-sid="' + esc(id) + '">Open session</button>'
|
|
610
|
+
: '<button type="button" class="cx-btn quiet" data-cos="open-sessions">Open Sessions</button>') + '</div></div>';
|
|
611
|
+
}).join('') + '</div>';
|
|
612
|
+
}
|
|
613
|
+
function relayPending(a) {
|
|
614
|
+
if (!relaysOf(a).length || Date.now() - (a.at || 0) > 2 * 3600000) return false;
|
|
615
|
+
return relaysOf(a).some(function (n) { var r = relayRow(n); return !relayReply(r, a); });
|
|
616
|
+
}
|
|
617
|
+
var relayHooked = false;
|
|
618
|
+
function loadRelayRows(maxAgeMs) {
|
|
619
|
+
if (!W.sessionList) return;
|
|
620
|
+
W.sessionList(maxAgeMs).then(function (d) {
|
|
621
|
+
if (!d || !Array.isArray(d.sessions)) return;
|
|
622
|
+
C.sessionRows = d.sessions;
|
|
623
|
+
C.asks.forEach(function (x) { if (relaysOf(x).length) paintRelay(x); });
|
|
624
|
+
}, function () {});
|
|
625
|
+
}
|
|
626
|
+
// No loop of its own: the first read when a relay shows up, then the live stream's frames and the shell's status read.
|
|
627
|
+
function watchRelays(a) {
|
|
628
|
+
if (!relaysOf(a).length || a.relayWatched) return;
|
|
629
|
+
a.relayWatched = true;
|
|
630
|
+
loadRelayRows(15000);
|
|
631
|
+
if (!relayHooked && W.live && typeof W.live.on === 'function') {
|
|
632
|
+
relayHooked = true;
|
|
633
|
+
W.live.on(function () { if (C.asks.some(relayPending)) loadRelayRows(15000); });
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
function paintRelay(a) {
|
|
637
|
+
var el = document.querySelector('[data-ask="' + a.cid + '"]');
|
|
638
|
+
var box = el && el.querySelector('[data-ask-relay-slot]');
|
|
639
|
+
if (!box) return;
|
|
640
|
+
var html = relayHtml(a);
|
|
641
|
+
if (box.getAttribute('data-html') !== html) { box.innerHTML = html; box.setAttribute('data-html', html); }
|
|
642
|
+
}
|
|
643
|
+
// Issue 2 (Message #463 said "Answered" over "COS is answering"): a finished Ask with no words. The card used to read
|
|
644
|
+
// "no words yet" as "still answering". Now it reads the turn from Messages once (the answer may have reached Messages
|
|
645
|
+
// after this card's last read), and if Messages has no words either, says so, with its steps and where it sent things.
|
|
646
|
+
function emptyAnswerHtml(a) {
|
|
647
|
+
if (a.recovering) return '<span class="cc-wait">Reading the answer from Messages.</span>';
|
|
648
|
+
if (relaysOf(a).length) return '';
|
|
649
|
+
return '<span class="cc-wait">COS finished without a written answer.' + (stepsOf(a).length ? ' What it did is under steps.' : '') + '</span>';
|
|
650
|
+
}
|
|
651
|
+
function recoverAnswer(a) {
|
|
652
|
+
if (a.recovered || a.recovering) return Promise.resolve();
|
|
653
|
+
a.recovered = true;
|
|
654
|
+
if (!a.no) return Promise.resolve();
|
|
655
|
+
a.recovering = true;
|
|
656
|
+
paintAsk(a);
|
|
657
|
+
return api('/api/sessions/today/all-messages?limit=100').then(function (d) {
|
|
658
|
+
var list = (d && d.messages) || [];
|
|
659
|
+
for (var i = list.length - 1; i >= 0; i--) {
|
|
660
|
+
var t = list[i], no = t.no != null ? t.no : t.globalMsgNum;
|
|
661
|
+
if (Number(no) === Number(a.no) && (!a.sessionId || !t.sessionId || t.sessionId === a.sessionId)) {
|
|
662
|
+
if (typeof t.text === 'string' && t.text.trim()) a.response = t.text;
|
|
663
|
+
break;
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
}, function () {}).then(function () { a.recovering = false; paintAsk(a, true); });
|
|
667
|
+
}
|
|
485
668
|
function askHtml(a, linked) {
|
|
486
669
|
var running = a.status === 'sending' || a.status === 'queued' || a.status === 'running';
|
|
487
670
|
var title = a.no ? 'Message #' + a.no : 'Asking COS';
|
|
488
671
|
var body = answerHtml(a);
|
|
672
|
+
// "COS is answering" only while it is; a finished Ask with no words says so (issue 2).
|
|
673
|
+
if (!body) body = a.status === 'completed' ? emptyAnswerHtml(a) : askRunning(a) ? '<span class="cc-wait">COS is answering. It also lands in Messages on the glasses.</span>' : '';
|
|
489
674
|
var acts = '';
|
|
490
675
|
if (running && a.jobId) {
|
|
491
676
|
acts = (C.caps && C.caps.steer && a.steerable !== false ? '<button type="button" class="link-btn cc-text-btn" data-cos="steer" data-cid="' + a.cid + '">Steer</button>' : '') +
|
|
@@ -501,7 +686,9 @@
|
|
|
501
686
|
return (linked ? '' : '<div class="cc-line you"><div class="cc-bubble">' + esc(a.query) + '</div>' + sentAttsHtml(a.atts) + '<div class="cc-src">' + (a.replyKey ? 'Replied on the web' : 'Asked on the web') + '</div></div>') +
|
|
502
687
|
'<div class="cc-line cos" data-ask="' + a.cid + '"><span class="cc-avatar">' + MARK + '</span><div class="cc-card">' +
|
|
503
688
|
'<p class="cc-card-title"><span data-ask-title>' + esc(title) + '</span><span class="cx-time" data-ask-state>' + esc(askState(a)) + '</span></p>' +
|
|
504
|
-
'<div class="cc-answer md" data-ask-body>' +
|
|
689
|
+
'<div class="cc-answer md" data-ask-body>' + body + '</div>' +
|
|
690
|
+
'<div class="cc-steps-slot" data-ask-steps>' + stepsHtml(a) + '</div>' +
|
|
691
|
+
'<div data-ask-relay-slot>' + relayHtml(a) + '</div>' +
|
|
505
692
|
(a.error ? '<p class="cc-fine warn">' + esc(a.error) + '</p>' : '') +
|
|
506
693
|
'<div class="cc-acts" data-ask-acts>' + acts + '</div>' +
|
|
507
694
|
(a.steering ? '<form class="cc-steer" data-cos-steer="' + a.cid + '"><input class="field" type="text" maxlength="2000" placeholder="Steer the answer" aria-label="Steer the answer"><button type="submit" class="cx-btn">Send</button></form>' : '') +
|
|
@@ -516,10 +703,12 @@
|
|
|
516
703
|
if (C.items === null && C.feedState !== 'error') html = W.skeletonRows(3);
|
|
517
704
|
else if (C.feedState === 'error' && C.items === null) html = '<div class="cx-notice warn"><span>' + esc(C.feedError) + '</span></div><button type="button" class="cx-btn quiet" data-cos="retry">Try again</button>';
|
|
518
705
|
else {
|
|
519
|
-
|
|
706
|
+
// 7.0.9: a question asked 7 days ago or more (the server marks it `archived`) lives under older questions.
|
|
707
|
+
var rows = (C.items || []).filter(function (it) { return !it.archived; }).map(function (it) { return { at: it.createdAt, html: itemHtml(it) }; });
|
|
520
708
|
C.asks.forEach(function (a) { if (!a.replyKey) rows.push({ at: new Date(a.at + (a.dropId ? 1 : 0)).toISOString(), html: askHtml(a, !!a.dropId) }); });
|
|
521
709
|
rows.sort(function (a, b) { return String(a.at).localeCompare(String(b.at)); });
|
|
522
|
-
|
|
710
|
+
var older = archiveHtml();
|
|
711
|
+
html = rows.length || older ? older + rows.map(function (r) { return r.html; }).join('')
|
|
523
712
|
: dropOn() ? '<p class="cc-empty">Nothing here yet. Ask COS something, or drop a thought and COS files it.</p>'
|
|
524
713
|
: '<p class="cc-empty">Ask COS anything. Attach photos or files with the clip.</p>';
|
|
525
714
|
}
|
|
@@ -535,7 +724,8 @@
|
|
|
535
724
|
else if (W.parseRoute().args[0] === 'cos') window.scrollTo(0, document.body.scrollHeight);
|
|
536
725
|
}
|
|
537
726
|
// A running Ask updates its own card in place, so a poll never rebuilds the chat (or the reader).
|
|
538
|
-
|
|
727
|
+
// `inPlace`: repaint this card only, never the chat around it (a late answer from recoverAnswer).
|
|
728
|
+
function paintAsk(a, inPlace) {
|
|
539
729
|
var el = document.querySelector('[data-ask="' + a.cid + '"]');
|
|
540
730
|
if (!el) return a.replyKey ? paintReplyFeed(a.replyKey) : paintFeed(false);
|
|
541
731
|
var running = a.status === 'sending' || a.status === 'queued' || a.status === 'running';
|
|
@@ -543,7 +733,12 @@
|
|
|
543
733
|
var st = el.querySelector('[data-ask-state]'); if (st) st.textContent = askState(a);
|
|
544
734
|
var body = el.querySelector('[data-ask-body]'); var text = a.response || a.partial;
|
|
545
735
|
if (body && text && body.getAttribute('data-len') !== String(text.length) + (a.response ? 'r' : '')) { body.innerHTML = answerHtml(a); body.setAttribute('data-len', String(text.length) + (a.response ? 'r' : '')); }
|
|
546
|
-
|
|
736
|
+
// No words and no longer running: never leave "COS is answering" under a finished card (issue 2).
|
|
737
|
+
else if (body && !text && !askRunning(a)) { var none = a.status === 'completed' ? emptyAnswerHtml(a) : ''; if (body.getAttribute('data-none') !== none) { body.innerHTML = none; body.setAttribute('data-none', none); body.removeAttribute('data-len'); } }
|
|
738
|
+
var steps = el.querySelector('[data-ask-steps]');
|
|
739
|
+
if (steps) { var sig = (a.trail ? a.trail.length + ':' + (a.trail.length ? a.trail[a.trail.length - 1].trailSeq : 0) : '0') + (askRunning(a) ? 'r' : 'd'); if (steps.getAttribute('data-sig') !== sig) { steps.innerHTML = stepsHtml(a); steps.setAttribute('data-sig', sig); } }
|
|
740
|
+
paintRelay(a);
|
|
741
|
+
if (!running && !inPlace) { if (a.replyKey) paintReplyFeed(a.replyKey); else paintFeed(false); }
|
|
547
742
|
}
|
|
548
743
|
|
|
549
744
|
function uploadsPending(k) { return k.atts.some(function (t) { return t.state === 'reading' || t.state === 'uploading'; }); }
|
|
@@ -1067,7 +1262,7 @@
|
|
|
1067
1262
|
opts = opts || {};
|
|
1068
1263
|
var a = { cid: uuid(), query: text, at: Date.now(), status: 'sending', partial: '', dropId: dropId || '', atts: opts.atts || [], replyKey: opts.replyKey || '' };
|
|
1069
1264
|
// 7.0.2 (A6): what Ask again sends: the same words, files and conversation.
|
|
1070
|
-
a.again = { attachmentIds: (opts.attachmentIds || []).slice(0, MAX_ATTS), atts: (opts.atts || []).slice(0, MAX_ATTS), sessionId: opts.sessionId || '', replyKey: opts.replyKey || '', context: opts.context || '' };
|
|
1265
|
+
a.again = { attachmentIds: (opts.attachmentIds || []).slice(0, MAX_ATTS), atts: (opts.atts || []).slice(0, MAX_ATTS), sessionId: opts.sessionId || '', replyKey: opts.replyKey || '', context: opts.context || '', contextRef: opts.contextRef || '' };
|
|
1071
1266
|
C.asks.push(a);
|
|
1072
1267
|
if (a.replyKey) paintReplyFeed(a.replyKey); else paintFeed(true);
|
|
1073
1268
|
var body = { clientJobId: a.cid, generation: 1, query: text, source: 'web', activityToolMode: 'status' };
|
|
@@ -1104,7 +1299,7 @@
|
|
|
1104
1299
|
// after the plan, immediately before admission (root review, 7.0.2: the thread can change between askAgain's check
|
|
1105
1300
|
// and this send's own thread read, and a retry carries old files and context).
|
|
1106
1301
|
if (opts.expect && !sameConversation(opts.expect, a)) throw Object.assign(new Error(MOVED_ON), { code: 'conversation_moved' });
|
|
1107
|
-
withContext(body, opts.context);
|
|
1302
|
+
withContext(body, opts.context, opts.contextRef);
|
|
1108
1303
|
});
|
|
1109
1304
|
prepare.then(function () { return admit(true); }).then(function (d) {
|
|
1110
1305
|
var job = d && d.job; if (!job) throw new Error('The Mac did not take the question.');
|
|
@@ -1138,12 +1333,63 @@
|
|
|
1138
1333
|
}
|
|
1139
1334
|
// An Ask from Today carries what Today shows (app.js askCos, panes/today-actions.js summary) as the quoted reference,
|
|
1140
1335
|
// after a carried thread summary when there is one. Bounded, and only on the Ask it was given to.
|
|
1336
|
+
// Today round 2: "Ask about this" names its own reference (ABOUT_REF).
|
|
1141
1337
|
var TODAY_REF = 'What Today in COS Control shows right now', CONTEXT_MAX = 6000;
|
|
1142
|
-
function withContext(body, context) {
|
|
1338
|
+
function withContext(body, context, ref) {
|
|
1143
1339
|
var text = typeof context === 'string' ? context.trim().slice(0, CONTEXT_MAX) : '';
|
|
1144
1340
|
if (!text) return;
|
|
1145
|
-
|
|
1146
|
-
|
|
1341
|
+
var name = typeof ref === 'string' && ref ? ref : TODAY_REF;
|
|
1342
|
+
if (body.reference) body.reference = { query: body.reference.query + ', then ' + name.charAt(0).toLowerCase() + name.slice(1), response: body.reference.response + '\n\n' + text };
|
|
1343
|
+
else body.reference = { query: name, response: text };
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
// ── Ask about this (Today round 2, Miles 11/09) ───────
|
|
1347
|
+
// Today's box under the model summary (app.js sendTodayAsk) asks here. The typed words are the question; the summary,
|
|
1348
|
+
// when it was written and the sessions it read go as the Ask's reference, which the server quotes as untrusted data
|
|
1349
|
+
// (lib/prompt-reference-boundary.ts), so nothing in them can act as an instruction. Each session is named by the title
|
|
1350
|
+
// its Sessions row shows, so COS can pass an instruction to it with its own relay (SendMessage to that title; the
|
|
1351
|
+
// answer card then says "Sent to <title>", relayHtml). Never transcript text: only the last message into each session,
|
|
1352
|
+
// as the summary already read it. The same lines as desktop Control 0.5.384's ```today-context block.
|
|
1353
|
+
var ABOUT_REF = 'The Today summary and the sessions it read', ABOUT_ROWS = 12;
|
|
1354
|
+
function aboutClock(v) { var d = W.toDate(v); return d ? W.clock(d) : ''; }
|
|
1355
|
+
function aboutAgo(v) {
|
|
1356
|
+
var d = W.toDate(v); if (!d) return '';
|
|
1357
|
+
var m = Math.max(0, Math.round((Date.now() - d.getTime()) / 60000));
|
|
1358
|
+
if (m < 1) return 'just now';
|
|
1359
|
+
if (m < 60) return m + ' min ago';
|
|
1360
|
+
var h = Math.floor(m / 60);
|
|
1361
|
+
return h < 48 ? h + ' h ago' : Math.round(h / 24) + ' days ago';
|
|
1362
|
+
}
|
|
1363
|
+
// A fence cannot be closed from inside: three backticks become three single quotes; one line per field.
|
|
1364
|
+
function aboutText(v, n) { return clip(String(v == null ? '' : v).replace(/```/g, "'''"), n); }
|
|
1365
|
+
function aboutContextText(ctx) {
|
|
1366
|
+
if (!ctx || typeof ctx !== 'object') return '';
|
|
1367
|
+
var summary = aboutText(ctx.summary, 1200);
|
|
1368
|
+
var rows = Array.isArray(ctx.sessions) ? ctx.sessions.filter(function (r) { return r && r.title; }) : [];
|
|
1369
|
+
if (!summary && !rows.length) return '';
|
|
1370
|
+
var when = aboutClock(ctx.at);
|
|
1371
|
+
var lines = [];
|
|
1372
|
+
if (summary) lines.push('Today summary' + (when ? ', written ' + when : '') + ':', summary, '');
|
|
1373
|
+
lines.push('Sessions Today read (' + rows.length + '):');
|
|
1374
|
+
rows.slice(0, ABOUT_ROWS).forEach(function (r) {
|
|
1375
|
+
var moved = aboutAgo(r.movedAt);
|
|
1376
|
+
lines.push('- ' + [aboutText(r.title, 120), aboutText(r.provider || 'claude', 12), aboutText(r.state, 12)].join(' | ') +
|
|
1377
|
+
(moved ? ' | last activity ' + moved : '') + (r.lastAsk ? ' | last ask: "' + aboutText(r.lastAsk, 160).replace(/"/g, "'") + '"' : ''));
|
|
1378
|
+
});
|
|
1379
|
+
if (rows.length > ABOUT_ROWS) lines.push('and ' + (rows.length - ABOUT_ROWS) + ' more');
|
|
1380
|
+
return lines.join('\n').slice(0, CONTEXT_MAX);
|
|
1381
|
+
}
|
|
1382
|
+
function askAbout(text, ctx) {
|
|
1383
|
+
return ensureCaps().then(function (caps) {
|
|
1384
|
+
if (!caps || !caps.jobs) { var off = new Error('Ask needs the durable query jobs switch on your Mac.'); off.code = 'durable_query_jobs_disabled'; throw off; }
|
|
1385
|
+
var q = String(text || '').trim();
|
|
1386
|
+
if (!q) throw new Error('Type what to ask first.');
|
|
1387
|
+
var body = aboutContextText(ctx);
|
|
1388
|
+
return new Promise(function (resolve, reject) {
|
|
1389
|
+
submitAsk(q, null, function (ok) { if (ok) resolve(['cos']); else reject(new Error('Not sent. Your words are still in the box on Today.')); },
|
|
1390
|
+
body ? { context: body, contextRef: ABOUT_REF } : undefined);
|
|
1391
|
+
});
|
|
1392
|
+
});
|
|
1147
1393
|
}
|
|
1148
1394
|
function take(a, job) {
|
|
1149
1395
|
a.jobId = job.jobId; a.status = job.status || a.status;
|
|
@@ -1151,6 +1397,8 @@
|
|
|
1151
1397
|
if (job.sessionId) a.sessionId = job.sessionId;
|
|
1152
1398
|
if (typeof job.partialText === 'string') a.partial = job.partialText;
|
|
1153
1399
|
if (typeof job.response === 'string') a.response = job.response;
|
|
1400
|
+
// 7.0.9: the steps, as the lens reads them (bounded on the Mac to 200 entries and 32,000 characters).
|
|
1401
|
+
if (Array.isArray(job.trail)) { a.trail = job.trail; watchRelays(a); }
|
|
1154
1402
|
if (job.steerable === false) a.steerable = false;
|
|
1155
1403
|
if (job.error && (a.status === 'failed' || a.status === 'interrupted')) a.error = errorLine(job);
|
|
1156
1404
|
}
|
|
@@ -1241,6 +1489,7 @@
|
|
|
1241
1489
|
if (g.attachmentIds && g.attachmentIds.length) { o.attachmentIds = g.attachmentIds.slice(0, MAX_ATTS); o.atts = (g.atts || []).slice(0, MAX_ATTS); }
|
|
1242
1490
|
if (g.replyKey) { o.replyKey = g.replyKey; o.sessionId = g.sessionId; }
|
|
1243
1491
|
if (g.context) o.context = g.context;
|
|
1492
|
+
if (g.contextRef) o.contextRef = g.contextRef;
|
|
1244
1493
|
o.reask = true;
|
|
1245
1494
|
if (a.conv) { o.expect = a.conv; o.retryOf = a; }
|
|
1246
1495
|
return submitAsk(a.query, null, null, o);
|
|
@@ -1250,7 +1499,7 @@
|
|
|
1250
1499
|
var started = Date.now();
|
|
1251
1500
|
var tick = function () {
|
|
1252
1501
|
if (!a.jobId) return;
|
|
1253
|
-
api('/api/query-jobs/' + a.jobId + '?generation=1').then(function (d) {
|
|
1502
|
+
api('/api/query-jobs/' + a.jobId + '?generation=1&trail=1').then(function (d) {
|
|
1254
1503
|
if (d && d.job) take(a, d.job);
|
|
1255
1504
|
paintAsk(a);
|
|
1256
1505
|
if (['completed', 'failed', 'canceled', 'interrupted'].indexOf(a.status) >= 0) return finished(a);
|
|
@@ -1260,6 +1509,7 @@
|
|
|
1260
1509
|
setTimeout(tick, 600);
|
|
1261
1510
|
}
|
|
1262
1511
|
function finished(a) {
|
|
1512
|
+
if (a.status === 'completed' && !hasText(a)) recoverAnswer(a);
|
|
1263
1513
|
postTurn(a);
|
|
1264
1514
|
refreshDefault();
|
|
1265
1515
|
if (a.replyKey) paintReplyFeed(a.replyKey); else paintFeed(false);
|
|
@@ -1322,6 +1572,9 @@
|
|
|
1322
1572
|
// ── actions on a line ─────────────────────────────────
|
|
1323
1573
|
function act(itemId, action) {
|
|
1324
1574
|
var item = (C.items || []).filter(function (i) { return i.id === itemId; })[0];
|
|
1575
|
+
// 7.0.9: an older question answers from its list exactly as from the chat.
|
|
1576
|
+
var fromArchive = !item || !!item.archived;
|
|
1577
|
+
if (!item) item = (C.archive || []).filter(function (i) { return i.id === itemId; })[0];
|
|
1325
1578
|
if (!item) return;
|
|
1326
1579
|
if (action === 'move') { C.moving = C.moving === itemId ? '' : itemId; return paintFeed(false); }
|
|
1327
1580
|
if (action === 'open_card') return W.go('work');
|
|
@@ -1350,6 +1603,8 @@
|
|
|
1350
1603
|
// A reminder action answers the finished item itself (merged above); dismiss answers counts only.
|
|
1351
1604
|
if (action === 'dismiss') item.state = 'done';
|
|
1352
1605
|
if (ans && ans.next && ans.next.kind === 'ask' && C.caps && C.caps.jobs) submitAsk(ans.next.prompt, ans.drop.id, null, askFilesOf(ans.next));
|
|
1606
|
+
// Answered: it is no longer an older question, wherever it was.
|
|
1607
|
+
if (fromArchive) { delete item.archived; (C.items || []).forEach(function (i) { if (i.id === itemId) delete i.archived; }); if (C.archive) C.archive = C.archive.filter(function (i) { return i.id !== itemId; }); if (C.archiveOpen) loadArchive(); }
|
|
1353
1608
|
paintFeed(false); refreshSummary();
|
|
1354
1609
|
}, function (err) { W.toast((err && err.message) || 'That did not go through.'); });
|
|
1355
1610
|
}
|
|
@@ -1512,14 +1767,17 @@
|
|
|
1512
1767
|
var act = ((item && item.actions) || []).filter(function (a) { return a && a.id === 'prep'; })[0] || {};
|
|
1513
1768
|
var info = (act.prep && typeof act.prep === 'object' ? act.prep : item.prep && typeof item.prep === 'object' ? item.prep : {}) || {};
|
|
1514
1769
|
var key = 'item:' + item.id;
|
|
1515
|
-
|
|
1770
|
+
// Resolves to this composer once its draft is read (or the read failed, its note set): Today's Start prep waits on it.
|
|
1771
|
+
if (P && P.key === key) { var same = P; paintPrep(); focusPrep(); return (same.loaded || Promise.resolve()).then(function () { return same; }); }
|
|
1516
1772
|
if (P) closePrep();
|
|
1517
1773
|
P = { key: key, title: clip(info.title || item.title || 'my next meeting', 200), startsAt: typeof info.startsAt === 'string' ? info.startsAt : null,
|
|
1518
1774
|
draft: null, steer: '', dirty: false, ups: [], note: '', warn: false, running: false, pending: null, chain: Promise.resolve(), timer: null, conflicts: 0, loading: true };
|
|
1519
1775
|
paintPrep(true); focusPrep();
|
|
1520
1776
|
// On a phone the page scrolls: bring the composer up to where the card was tapped.
|
|
1521
1777
|
var slot = prepSlot(false); if (slot && slot.scrollIntoView && !wide()) slot.scrollIntoView({ block: 'start' });
|
|
1522
|
-
|
|
1778
|
+
var p = P;
|
|
1779
|
+
p.loaded = loadPrep(p);
|
|
1780
|
+
return p.loaded.then(function () { return p; });
|
|
1523
1781
|
}
|
|
1524
1782
|
function closePrep() {
|
|
1525
1783
|
if (!P) return;
|
|
@@ -1615,9 +1873,9 @@
|
|
|
1615
1873
|
|
|
1616
1874
|
// Files: the composer's own upload (uploadMedia), then POST .../files with the draft's revision.
|
|
1617
1875
|
function addPrepFiles(list) {
|
|
1618
|
-
var p = P; if (!p) return;
|
|
1876
|
+
var p = P; if (!p) return 0;
|
|
1619
1877
|
var files = Array.prototype.slice.call(list || []), refusals = [];
|
|
1620
|
-
if (!p.draft) { setPrepNote(p, 'This prep is still opening. Add the files again in a moment.', true); return; }
|
|
1878
|
+
if (!p.draft) { setPrepNote(p, 'This prep is still opening. Add the files again in a moment.', true); return files.length; }
|
|
1621
1879
|
files.forEach(function (file) {
|
|
1622
1880
|
if (prepFiles(p.draft).length + p.ups.length >= prepMax()) { refusals.push('A prep holds ' + prepMax() + ' files. ' + (file.name || 'A file') + ' was not added.'); return; }
|
|
1623
1881
|
var c = classify(file);
|
|
@@ -1629,6 +1887,7 @@
|
|
|
1629
1887
|
});
|
|
1630
1888
|
setPrepNote(p, refusals.join(' '), !!refusals.length);
|
|
1631
1889
|
paintPrep();
|
|
1890
|
+
return refusals.length;
|
|
1632
1891
|
}
|
|
1633
1892
|
function sendPrepFile(p, t) {
|
|
1634
1893
|
t.state = t.mediaId ? 'adding' : 'reading'; t.progress = 0; t.error = '';
|
|
@@ -1654,7 +1913,7 @@
|
|
|
1654
1913
|
});
|
|
1655
1914
|
}).then(function (r) {
|
|
1656
1915
|
if (!r || p.ups.indexOf(t) < 0) return;
|
|
1657
|
-
if (r.status >= 200 && r.status < 300) { p.ups = p.ups.filter(function (x) { return x !== t; }); paintPrep(); return; }
|
|
1916
|
+
if (r.status >= 200 && r.status < 300) { p.ups = p.ups.filter(function (x) { return x !== t; }); paintPrep(); return prepUploadsSettled(p); }
|
|
1658
1917
|
// Refused (a 4xx), so nothing was recorded: Retry is a new operation. No answer or a 5xx: Retry is the same one.
|
|
1659
1918
|
if (r.status >= 400 && r.status < 500) t.opId = '';
|
|
1660
1919
|
// The staged copy is gone or differs: Retry uploads the bytes again.
|
|
@@ -1663,11 +1922,13 @@
|
|
|
1663
1922
|
t.state = 'failed';
|
|
1664
1923
|
t.error = r.mismatch ? 'Not added: the Mac\u2019s COS server changed. Retry.' : r.status === 409 && code === 'revision_conflict' ? 'Not added: the prep changed. Retry.' : prepErrorText(r);
|
|
1665
1924
|
paintPrep();
|
|
1925
|
+
prepUploadsSettled(p);
|
|
1666
1926
|
}).catch(function (err) {
|
|
1667
1927
|
if (p.ups.indexOf(t) < 0 || (err && err.code === 'aborted')) return;
|
|
1668
1928
|
t.state = 'failed';
|
|
1669
1929
|
t.error = err && err.code === 'kept' ? err.message : err && err.status === 401 ? 'Signed out. Sign in, then retry.' : uploadErrorText(err);
|
|
1670
1930
|
paintPrep();
|
|
1931
|
+
prepUploadsSettled(p);
|
|
1671
1932
|
});
|
|
1672
1933
|
}
|
|
1673
1934
|
function removePrepFile(p, fileId) {
|
|
@@ -1695,6 +1956,31 @@
|
|
|
1695
1956
|
return postRun(p, p.pending, true);
|
|
1696
1957
|
}).then(function (r) { return settleRun(p, r, true); });
|
|
1697
1958
|
}
|
|
1959
|
+
// Today's Start prep (prepStart): the run waits until every file it added is in the draft. A file that did not upload
|
|
1960
|
+
// stops it, with one line saying so, so a prep never runs without a file the person chose for it.
|
|
1961
|
+
var PREP_FILE_HELD = 'A file did not upload, so this prep has not started. Retry or remove it, then tap Prep me.';
|
|
1962
|
+
function prepUploadsSettled(p) {
|
|
1963
|
+
if (!p || !p.autoRun || P !== p || prepUploading(p)) return;
|
|
1964
|
+
p.autoRun = false;
|
|
1965
|
+
if (p.ups.length) { setPrepNote(p, PREP_FILE_HELD, true); return paintPrep(); }
|
|
1966
|
+
runPrep(p);
|
|
1967
|
+
}
|
|
1968
|
+
// The context typed on Today joins the draft's notes: kept as they are when they already hold it, else added after them.
|
|
1969
|
+
function mergeSteer(old, add) {
|
|
1970
|
+
var a = String(add || '').trim(), o = String(old || '');
|
|
1971
|
+
if (!a) return o;
|
|
1972
|
+
if (!o.trim()) return a.slice(0, prepChars());
|
|
1973
|
+
if (o.indexOf(a) >= 0) return o;
|
|
1974
|
+
return (o.replace(/\s+$/, '') + '\n\n' + a).slice(0, prepChars());
|
|
1975
|
+
}
|
|
1976
|
+
// Prep off on the Mac, Ask on: "Prep me for <title> at <time>", the context typed on Today as the Ask's quoted
|
|
1977
|
+
// reference (data, never instructions; ``` cannot close the fence), as askAbout sends the Today summary.
|
|
1978
|
+
var PREP_ASK_REF = 'Context added for this prep on Today';
|
|
1979
|
+
function prepAskContext(title, when, steer) {
|
|
1980
|
+
var text = aboutText(steer, 2000);
|
|
1981
|
+
if (!text) return '';
|
|
1982
|
+
return ['Meeting: ' + aboutText(title, 200), when ? 'Starts: ' + when : '', 'Added context: ' + text].filter(Boolean).join('\n');
|
|
1983
|
+
}
|
|
1698
1984
|
function runBody(p, run) {
|
|
1699
1985
|
var body = { clientRequestId: run.rid, revision: p.draft.revision };
|
|
1700
1986
|
if (run.model) body.model = run.model;
|
|
@@ -1951,6 +2237,7 @@
|
|
|
1951
2237
|
if (up.xhr) { try { up.xhr.abort(); } catch (_e) {} }
|
|
1952
2238
|
p.ups = p.ups.filter(function (x) { return x !== up; });
|
|
1953
2239
|
paintPrep();
|
|
2240
|
+
prepUploadsSettled(p);
|
|
1954
2241
|
}
|
|
1955
2242
|
}
|
|
1956
2243
|
function onPrepDrag(e, slot) {
|
|
@@ -1976,6 +2263,10 @@
|
|
|
1976
2263
|
if (what === 'move-to') return moveTo(t.getAttribute('data-item'), t.getAttribute('data-dest'), t.getAttribute('data-domain'));
|
|
1977
2264
|
if (what === 'move-cancel') { C.moving = ''; return paintFeed(false); }
|
|
1978
2265
|
if (what === 'open-msg') return W.go('messages', ['m', t.getAttribute('data-no')]);
|
|
2266
|
+
if (what === 'open-session') return W.go('sessions', [t.getAttribute('data-provider') || 'claude', t.getAttribute('data-sid')]);
|
|
2267
|
+
if (what === 'open-sessions') return W.go('sessions');
|
|
2268
|
+
if (what === 'archive') return toggleArchive();
|
|
2269
|
+
if (what === 'steps') { var sa = C.asks.filter(function (x) { return x.cid === t.getAttribute('data-cid'); })[0]; if (sa) sa.stepsOpen = !sa.stepsOpen; return; }
|
|
1979
2270
|
if (what === 'steer') {
|
|
1980
2271
|
var a = C.asks.filter(function (x) { return x.cid === t.getAttribute('data-cid'); })[0];
|
|
1981
2272
|
if (a) { a.steering = !a.steering; if (a.replyKey) paintReplyFeed(a.replyKey); else paintFeed(false); var f = document.querySelector('[data-cos-steer="' + a.cid + '"] input'); if (f) f.focus(); }
|
|
@@ -2143,6 +2434,8 @@
|
|
|
2143
2434
|
on: on, dropOn: dropOn, askOn: askOn, known: function () { return !!C.caps; }, ensureCaps: ensureCaps, fromHealth: fromHealth, refreshSummary: refreshSummary,
|
|
2144
2435
|
pinnedRowHtml: pinnedRowHtml, mount: mount, wide: wide,
|
|
2145
2436
|
replyHtml: replyHtml, mountReply: mountReply, askFromSheet: askFromSheet,
|
|
2437
|
+
// Today round 2: "Ask about this" under the Today summary (app.js sendTodayAsk).
|
|
2438
|
+
askAbout: askAbout, aboutContextText: aboutContextText,
|
|
2146
2439
|
onClick: onClick, onKey: onKey, onInput: onInput, onChange: onChangeEvent, onSubmit: onSubmit, onPaste: onPaste, onDrag: onDrag,
|
|
2147
2440
|
setOnChange: function (fn) { C.onChange = fn; }, setOnAnswered: function (fn) { C.onAnswered = fn; },
|
|
2148
2441
|
// Test and render hooks: what the chat holds right now.
|
|
@@ -2173,6 +2466,8 @@
|
|
|
2173
2466
|
// How many files a session turn takes on this Mac (threadTurnAttachments); 0 means words only.
|
|
2174
2467
|
turnFiles: function () { return C.caps && C.caps.turnFiles > 0 ? C.caps.turnFiles : 0; },
|
|
2175
2468
|
turnFileBytes: function () { return C.caps && C.caps.turnFiles > 0 && C.caps.turnFileBytes > 0 ? C.caps.turnFileBytes : 0; },
|
|
2469
|
+
// 6.99.31: 1 when the Mac offers "Approve on this phone" (the 3144 signer), else 0.
|
|
2470
|
+
webKeySigner: function () { return C.caps && C.caps.webKeySigner === 1 ? 1 : 0; },
|
|
2176
2471
|
askOn: askOn, ensureCaps: ensureCaps
|
|
2177
2472
|
},
|
|
2178
2473
|
// 6.99.19: Prep. meetings.js reads the capability from here (one /api/health read for the page).
|
|
@@ -2189,11 +2484,48 @@
|
|
|
2189
2484
|
return '';
|
|
2190
2485
|
});
|
|
2191
2486
|
},
|
|
2487
|
+
// Prep on every Next up row (Miles 11/09): Start prep under a row of Today. With Prep on the Mac, the Prep composer
|
|
2488
|
+
// opens for that meeting (the same draft prepFor opens), the typed context joins its notes, the files go up through
|
|
2489
|
+
// the composer's own path (addPrepFiles), and Prep me runs once they are in. Without Prep, the plain Ask "Prep me for
|
|
2490
|
+
// <title> at <time>" with the context as its quoted reference. Resolves to 'prep', 'ask' or '' (neither is on).
|
|
2491
|
+
prepStart: function (title, startsAt, extra) {
|
|
2492
|
+
var steer = String((extra && extra.steer) || '').trim(), files = (extra && extra.files) || [];
|
|
2493
|
+
return ensureCaps().then(function () {
|
|
2494
|
+
var t = String(title || '').trim() || 'my next meeting';
|
|
2495
|
+
if (prepOn()) {
|
|
2496
|
+
var item = { id: 'today-next:' + (startsAt || '') + ':' + t, title: t, prep: { title: t, startsAt: startsAt || null } };
|
|
2497
|
+
return openPrep(item).then(function (p) {
|
|
2498
|
+
if (!p || P !== p || !p.draft) return 'prep';
|
|
2499
|
+
var merged = mergeSteer(p.steer, steer);
|
|
2500
|
+
if (merged !== p.steer) {
|
|
2501
|
+
p.steer = merged; p.dirty = true;
|
|
2502
|
+
var box = prepBox(); if (box && box.value !== p.steer) { box.value = p.steer; W.autoGrow(box); }
|
|
2503
|
+
paintPrepParts(p);
|
|
2504
|
+
}
|
|
2505
|
+
if (files.length && addPrepFiles(files) > 0) return 'prep';
|
|
2506
|
+
p.autoRun = true;
|
|
2507
|
+
prepUploadsSettled(p);
|
|
2508
|
+
return 'prep';
|
|
2509
|
+
});
|
|
2510
|
+
}
|
|
2511
|
+
if (C.caps && C.caps.jobs) {
|
|
2512
|
+
var d = startsAt ? W.toDate(startsAt) : null, when = d ? W.clock(d) : '';
|
|
2513
|
+
var context = prepAskContext(t, when, steer);
|
|
2514
|
+
return new Promise(function (resolve, reject) {
|
|
2515
|
+
submitAsk('Prep me for ' + t + (when ? ' at ' + when : ''), null, function (ok) { if (ok) resolve('ask'); else reject(new Error('Prep did not start. Your context is still under the meeting on Today.')); },
|
|
2516
|
+
context ? { context: context, contextRef: PREP_ASK_REF } : undefined);
|
|
2517
|
+
});
|
|
2518
|
+
}
|
|
2519
|
+
return '';
|
|
2520
|
+
});
|
|
2521
|
+
},
|
|
2522
|
+
// The file types the Prep composer takes (Today's Add files uses the same list).
|
|
2523
|
+
prepAccept: PREP_ACCEPT,
|
|
2192
2524
|
// The Prep server id to name on a write: the newest from capabilities.prep or any Prep answer (meetings.js reports its own).
|
|
2193
2525
|
prepInstance: function () { return PREP_INST; }, notePrepInstance: function (id) { if (typeof id === 'string' && id) PREP_INST = id; },
|
|
2194
2526
|
_prep: function () { return P; }, _openPrep: openPrep, _prepHtml: function () { return P ? prepPanelHtml(P) : ''; }, _addPrepFiles: addPrepFiles,
|
|
2195
2527
|
_runPrep: function () { runPrep(P); }, _sha256Js: sha256Js, _closePrep: closePrep, _onPrepInput: onPrepInput, _savePrepSteer: function () { return P ? savePrepSteer(P) : null; },
|
|
2196
|
-
_errorLine: errorLine, _askAgain: askAgain, _askHtml: askHtml, _settle: settleAfterKeyboard, _dockWide: dockWide, _revealComposer: revealComposer, _markKeyboard: markKeyboard, _refreshDefault: refreshDefault, _defaultLabel: defaultLabel, _modelMenuHtml: modelMenuHtml,
|
|
2528
|
+
_errorLine: errorLine, _askAgain: askAgain, _askHtml: askHtml, _paintFeed: paintFeed, _paintAsk: paintAsk, _poll: poll, _finished: finished, _stepsHtml: stepsHtml, _relayHtml: relayHtml, _scrubPaths: scrubPaths, _settle: settleAfterKeyboard, _dockWide: dockWide, _revealComposer: revealComposer, _markKeyboard: markKeyboard, _refreshDefault: refreshDefault, _defaultLabel: defaultLabel, _modelMenuHtml: modelMenuHtml,
|
|
2197
2529
|
_state: C, _composer: composer, _classify: classify, _modelsFrom: modelsFrom, _addFiles: addFiles, _itemHtml: itemHtml, _setCaps: setCaps, _threadLine: threadLine, _postTurn: postTurn
|
|
2198
2530
|
};
|
|
2199
2531
|
})();
|