@gotcos/glasses-server 6.99.33 → 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 +37 -0
- package/CHANGELOG.md +587 -0
- package/README.md +3 -1
- package/docs/web-key-verifier-contract.md +378 -0
- package/managed-runtime-contract.json +12 -1
- package/package.json +10 -3
- package/server/bootstrap.ts +10 -0
- package/server/index.ts +155 -30
- package/server/lib/agent-session-output-images.ts +627 -49
- package/server/lib/agent-session-recent.ts +321 -0
- package/server/lib/agent-session-store.ts +27 -14
- package/server/lib/agent-session-tool-images.ts +224 -0
- package/server/lib/agent-session-turns.ts +19 -2
- package/server/lib/api-auth.ts +43 -1
- package/server/lib/ask-work-plan.ts +3 -1
- package/server/lib/calendar-meetings.ts +70 -0
- package/server/lib/calendar-source.ts +536 -0
- package/server/lib/claude-hooks-installer.ts +111 -12
- package/server/lib/codex-bridge.ts +4 -1
- package/server/lib/codex-run-ledger.ts +29 -0
- package/server/lib/context-builder.ts +2 -1
- package/server/lib/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-p2-runtime.ts +1 -1
- package/server/lib/drop-service.ts +47 -12
- package/server/lib/drop-store.ts +31 -5
- 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/learning-needs-you.ts +24 -7
- package/server/lib/listener-startup.ts +18 -0
- package/server/lib/live-events.ts +533 -0
- package/server/lib/local-path-redact.ts +272 -0
- package/server/lib/meeting-ask-orientation.ts +40 -6
- package/server/lib/meeting-ask-runner.ts +30 -10
- package/server/lib/meeting-batch-transcribe.ts +32 -5
- package/server/lib/morning-brief-runtime.ts +4 -2
- package/server/lib/native-mac-caller.ts +29 -0
- package/server/lib/permission-broker.ts +443 -11
- package/server/lib/ping-engine.ts +1 -1
- package/server/lib/ping-sources.ts +11 -6
- package/server/lib/preview-content.ts +172 -0
- package/server/lib/preview-error-code.ts +10 -0
- package/server/lib/preview-folders-setting.ts +133 -0
- package/server/lib/preview-log-throttle.ts +18 -0
- package/server/lib/preview-thumbs.ts +139 -0
- package/server/lib/secret-path-guard.ts +83 -0
- package/server/lib/session-cache-writer.ts +8 -1
- package/server/lib/session-files-setting.ts +88 -0
- package/server/lib/session-hook-events.ts +23 -1
- package/server/lib/session-hooks-runtime.ts +34 -0
- package/server/lib/session-links.ts +280 -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/thread-turn-queue.ts +10 -0
- package/server/lib/today-brief.ts +640 -85
- 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-memories-bridge.ts +3 -1
- 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-activity.ts +56 -6
- package/server/lib/work-orchestrator/snapshot.ts +17 -3
- package/server/lib/work-review-runtime.ts +17 -4
- package/server/lib/work-session-activity.ts +260 -0
- package/server/lib/your-move-count.ts +9 -3
- package/server/routes/agent-session-bindings.ts +29 -1
- package/server/routes/agent-session-stream.ts +23 -0
- package/server/routes/agent-sessions.ts +146 -11
- package/server/routes/ask-work-plan.ts +4 -1
- package/server/routes/claude-sessions.ts +5 -1
- package/server/routes/drops.ts +7 -2
- package/server/routes/g2-turns.ts +35 -0
- package/server/routes/health.ts +46 -1
- package/server/routes/live-events.ts +334 -0
- package/server/routes/permission-broker.ts +95 -5
- package/server/routes/query-jobs.ts +3 -0
- package/server/routes/session-preview.ts +200 -0
- package/server/routes/sessions.ts +15 -24
- package/server/routes/thread-turn-queue.ts +223 -12
- package/server/routes/today-brief.ts +134 -44
- package/server/routes/web-control.ts +37 -4
- package/server/routes/web-key.ts +284 -0
- package/server/routes/welcome-context.ts +28 -4
- package/server/routes/work-board.ts +9 -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 +76 -1
- package/server/web-control/app.js +2247 -98
- package/server/web-control/apple-touch-icon.png +0 -0
- package/server/web-control/control.css +25 -0
- package/server/web-control/index.html +21 -3
- package/server/web-control/memory-review.js +100 -0
- package/server/web-control/panes/cos-chat.css +94 -9
- package/server/web-control/panes/cos-chat.js +628 -44
- package/server/web-control/panes/meetings.css +18 -2
- package/server/web-control/panes/meetings.js +24 -8
- package/server/web-control/panes/memories.css +17 -2
- package/server/web-control/panes/memories.js +127 -42
- package/server/web-control/panes/messages.js +21 -2
- package/server/web-control/panes/preview.js +318 -0
- package/server/web-control/panes/questions.js +162 -21
- package/server/web-control/panes/reader-media.js +409 -23
- package/server/web-control/panes/sessions.css +32 -3
- package/server/web-control/panes/settings.js +97 -6
- package/server/web-control/panes/today-actions.css +46 -0
- package/server/web-control/panes/today-actions.js +119 -17
- package/server/web-control/panes/work.css +13 -0
- package/server/web-control/panes/work.js +188 -37
- package/server/web-control/shell.js +17 -14
- 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 }
|
|
@@ -54,7 +59,9 @@
|
|
|
54
59
|
onAnswered: null, // messages.js: re-read Recent when a web Ask lands
|
|
55
60
|
thread: null, // 6.99.16: the last GET /api/cos-thread answer
|
|
56
61
|
threadCarried: '', // the thread's newest session started fresh: 'summary' (it carried a digest) or 'plain'
|
|
57
|
-
threadBusy: false
|
|
62
|
+
threadBusy: false,
|
|
63
|
+
threadSession: '', // 7.0.2: the session the COS thread's next Ask continues ('' when it starts fresh)
|
|
64
|
+
defaultGen: 0 // 7.0.2: the newest /api/models read for the Default label; older answers are dropped
|
|
58
65
|
};
|
|
59
66
|
var saved = W.readStore(STORE);
|
|
60
67
|
C.model = typeof saved.model === 'string' ? saved.model : '';
|
|
@@ -112,7 +119,9 @@
|
|
|
112
119
|
// reply box shows no clip, so a file can never be dropped from a turn in silence.
|
|
113
120
|
turnFiles: h && Number.isInteger(h.threadTurnAttachments) && h.threadTurnAttachments > 0 ? Math.min(h.threadTurnAttachments, MAX_ATTS) : 0,
|
|
114
121
|
// 6.99.27: any file for a session turn, up to this many bytes (threadTurnFileBytes). 0: the chat's list only.
|
|
115
|
-
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
|
|
116
125
|
};
|
|
117
126
|
if (next.prep && next.prep.serverInstanceId) PREP_INST = next.prep.serverInstanceId;
|
|
118
127
|
var changed = !C.caps || C.caps.dropIt !== next.dropIt || C.caps.jobs !== next.jobs || C.caps.thread !== next.thread;
|
|
@@ -131,7 +140,7 @@
|
|
|
131
140
|
serverInstanceId: typeof p.serverInstanceId === 'string' ? p.serverInstanceId : '' };
|
|
132
141
|
}
|
|
133
142
|
// The shell's status read (shell.js checkServer) hands its /api/health answer here.
|
|
134
|
-
function fromHealth(h) { setCaps(h); }
|
|
143
|
+
function fromHealth(h) { setCaps(h); if (C.asks.some(relayPending)) loadRelayRows(15000); }
|
|
135
144
|
function ensureCaps() {
|
|
136
145
|
if (C.caps) return Promise.resolve(C.caps);
|
|
137
146
|
if (!C.capsLoading) C.capsLoading = api('/api/health').then(function (h) { setCaps(h); return C.caps; }, function () { C.capsLoading = null; return null; });
|
|
@@ -147,10 +156,12 @@
|
|
|
147
156
|
return null;
|
|
148
157
|
});
|
|
149
158
|
}
|
|
150
|
-
|
|
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; }
|
|
151
162
|
function unreadOf(s) { var n = s && Number(s.unread); return Number.isInteger(n) && n > 0 ? n : 0; }
|
|
152
|
-
// A number for what
|
|
153
|
-
// 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.
|
|
154
165
|
function paintRail() {
|
|
155
166
|
var waiting = waitingOf(C.summary), unread = unreadOf(C.summary);
|
|
156
167
|
document.querySelectorAll('.rail-btn[data-pane="messages"], #section-menu [data-goto="messages"]').forEach(function (b) {
|
|
@@ -160,10 +171,10 @@
|
|
|
160
171
|
var dot = document.createElement('span'); dot.className = 'inbox-dot hidden'; dot.setAttribute('data-inbox-dot', '');
|
|
161
172
|
b.appendChild(count); b.appendChild(dot);
|
|
162
173
|
}
|
|
163
|
-
count.textContent =
|
|
164
|
-
count.classList.toggle('hidden', !
|
|
165
|
-
b.querySelector('[data-inbox-dot]').classList.toggle('hidden', !(
|
|
166
|
-
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(', ');
|
|
167
178
|
b.setAttribute('aria-label', 'Messages' + (label ? ', ' + label : ''));
|
|
168
179
|
});
|
|
169
180
|
}
|
|
@@ -177,7 +188,7 @@
|
|
|
177
188
|
// ── the pinned row (first in Recent) ──────────────────
|
|
178
189
|
function pinStatus() {
|
|
179
190
|
var s = C.summary, w = waitingOf(s), u = unreadOf(s);
|
|
180
|
-
return [
|
|
191
|
+
return [u ? u + ' new' : '', w ? w + ' waiting on your reply' : ''].filter(Boolean).join(' · ');
|
|
181
192
|
}
|
|
182
193
|
function pinnedRowHtml(selected) {
|
|
183
194
|
var s = dropOn() ? C.summary : null;
|
|
@@ -221,11 +232,46 @@
|
|
|
221
232
|
});
|
|
222
233
|
return out;
|
|
223
234
|
}
|
|
235
|
+
// 7.0.2 (A7): what "Default" resolves to on this Mac (/api/models defaultModel: the session's preference, then
|
|
236
|
+
// COS_G2_DEFAULT_MODEL, then the built-in default), named in the menu: "Default (Codex Frontier)". On 11/07 Default
|
|
237
|
+
// was Codex while Codex was out of usage, and nothing on the page said so. An older server sends no field: plain Default.
|
|
238
|
+
// A session bound to a key profile reads "Default (key profile)".
|
|
239
|
+
function defaultLabel(d) {
|
|
240
|
+
if (d && d.source === 'profile') return 'key profile';
|
|
241
|
+
var pref = d && typeof d.preference === 'string' ? d.preference : '';
|
|
242
|
+
if (!pref) return '';
|
|
243
|
+
for (var i = 0; i < CLAUDE_MENU.length; i++) if (CLAUDE_MENU[i][0] === pref) return CLAUDE_MENU[i][1];
|
|
244
|
+
if (pref === 'haiku') return 'Claude Haiku';
|
|
245
|
+
return ENGINE[pref] ? (ENGINE[pref] + (FALLBACK[pref] ? ' ' + FALLBACK[pref] : '')) : '';
|
|
246
|
+
}
|
|
247
|
+
// The session an Ask from the chat continues, whose preference "Default" follows: the COS thread's newest session in
|
|
248
|
+
// thread mode, else the chat's own (C.session).
|
|
249
|
+
function defaultSession() { return threadOn() ? String(C.threadSession || '') : C.session && C.session.id ? String(C.session.id) : ''; }
|
|
250
|
+
// Each read is stamped with a generation and the session it asked about. An answer is used for the label only while
|
|
251
|
+
// it is still the newest read and the chat still continues that session, so a slow answer for an older session never
|
|
252
|
+
// overwrites a newer label (skeptic, 7.0.2).
|
|
253
|
+
function modelsRead() {
|
|
254
|
+
var sid = defaultSession();
|
|
255
|
+
return { gen: ++C.defaultGen, sid: sid, path: '/api/models' + (sid ? '?sessionId=' + encodeURIComponent(sid) : '') };
|
|
256
|
+
}
|
|
257
|
+
function currentRead(r) { return r.gen === C.defaultGen && r.sid === defaultSession(); }
|
|
258
|
+
// QA W5: the label is read again after each Ask finishes and whenever that session changes, so it never goes stale.
|
|
259
|
+
function refreshDefault() {
|
|
260
|
+
if (!C.models) return null;
|
|
261
|
+
var r = modelsRead();
|
|
262
|
+
return api(r.path).then(function (m) {
|
|
263
|
+
if (!currentRead(r)) return;
|
|
264
|
+
var label = defaultLabel(m && m.defaultModel);
|
|
265
|
+
if (label !== C.defaultLabel) { C.defaultLabel = label; paintModelMenu(); }
|
|
266
|
+
}, function () {});
|
|
267
|
+
}
|
|
224
268
|
function loadModels() {
|
|
225
269
|
if (C.models) return;
|
|
226
270
|
C.models = [];
|
|
227
|
-
|
|
271
|
+
var r = modelsRead();
|
|
272
|
+
api(r.path).then(function (m) {
|
|
228
273
|
C.models = modelsFrom(m);
|
|
274
|
+
if (currentRead(r)) C.defaultLabel = defaultLabel(m && m.defaultModel);
|
|
229
275
|
if (C.model && !C.models.some(function (o) { return o.value === C.model; })) { C.model = ''; save(); }
|
|
230
276
|
// Steer exists only where the server says so (/api/models capabilities.durableQueryJobs.steer).
|
|
231
277
|
var j = m && m.capabilities && m.capabilities.durableQueryJobs;
|
|
@@ -235,7 +281,7 @@
|
|
|
235
281
|
}
|
|
236
282
|
function modelMenuHtml() {
|
|
237
283
|
var list = C.models || [];
|
|
238
|
-
var opts = ['<option value="">Default</option>'].concat(list.map(function (m) {
|
|
284
|
+
var opts = ['<option value="">' + esc(C.defaultLabel ? 'Default (' + C.defaultLabel + ')' : 'Default') + '</option>'].concat(list.map(function (m) {
|
|
239
285
|
return '<option value="' + esc(m.value) + '"' + (m.value === C.model ? ' selected' : '') + (m.available ? '' : ' disabled') + '>' + esc(m.label + (m.available ? '' : ' (not available)')) + '</option>';
|
|
240
286
|
}));
|
|
241
287
|
var efforts = ['<option value="">Default</option>'].concat(EFFORTS.map(function (e) {
|
|
@@ -308,7 +354,7 @@
|
|
|
308
354
|
(threadOn() ? '<button type="button" class="link-btn" data-cos="new-thread">New thread</button>' : '') + '</div>' +
|
|
309
355
|
(threadOn() ? '<p class="cc-thread" data-cos-thread role="status">' + esc(threadLine()) + '</p>' : '') + '</header>' +
|
|
310
356
|
'<div class="cc-feed" data-cos-feed aria-live="polite"></div>' +
|
|
311
|
-
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' }) +
|
|
312
358
|
'</section>';
|
|
313
359
|
}
|
|
314
360
|
function mount(host, visible) {
|
|
@@ -356,12 +402,39 @@
|
|
|
356
402
|
});
|
|
357
403
|
C.items.sort(function (a, b) { return String(a.createdAt).localeCompare(String(b.createdAt)); });
|
|
358
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
|
+
}
|
|
359
432
|
function markReadIfShown(force) {
|
|
360
433
|
if (!dropOn() || !chatVisible() || !C.items || !C.items.length) return;
|
|
361
434
|
if (!force && C.summary && !unreadOf(C.summary)) return;
|
|
362
435
|
var last = C.items[C.items.length - 1];
|
|
363
436
|
api('/api/inbox/read', { method: 'POST', body: { through: last.id } }).then(function (r) {
|
|
364
|
-
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; }
|
|
365
438
|
paintRail(); paintPin();
|
|
366
439
|
}, function () {});
|
|
367
440
|
}
|
|
@@ -427,9 +500,10 @@
|
|
|
427
500
|
(item.title ? '<p class="cc-card-title">' + esc(item.title) + '</p>' : '') +
|
|
428
501
|
'<p class="cc-card-line">' + esc(item.line) + '</p>' + fileLinksHtml(item, false) + actionsHtml(item) + '</div></div>';
|
|
429
502
|
}
|
|
430
|
-
// The finished answer as Markdown
|
|
503
|
+
// The finished answer as Markdown, its lens-plain inline lists on their own lines first (structureLensAnswer); while it
|
|
504
|
+
// streams, plain words, so a half-written "**bold" never shows its stars.
|
|
431
505
|
function answerHtml(a) {
|
|
432
|
-
if (a.response) return W.md(a.response);
|
|
506
|
+
if (a.response) return W.md(W.structureLensAnswer(a.response));
|
|
433
507
|
if (!a.partial) return '';
|
|
434
508
|
return '<p>' + esc(a.partial.replace(/\*\*|__|`|^#+\s*/gm, '')).replace(/\n{2,}/g, '</p><p>').replace(/\n/g, '<br>') + '</p>';
|
|
435
509
|
}
|
|
@@ -444,22 +518,177 @@
|
|
|
444
518
|
function askState(a) {
|
|
445
519
|
return a.status === 'completed' ? 'Answered' : a.status === 'canceled' ? 'Canceled' : a.status === 'failed' || a.status === 'interrupted' ? 'Did not finish' : a.status === 'sending' ? 'Sending' : 'Answering';
|
|
446
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
|
+
}
|
|
447
668
|
function askHtml(a, linked) {
|
|
448
669
|
var running = a.status === 'sending' || a.status === 'queued' || a.status === 'running';
|
|
449
670
|
var title = a.no ? 'Message #' + a.no : 'Asking COS';
|
|
450
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>' : '';
|
|
451
674
|
var acts = '';
|
|
452
675
|
if (running && a.jobId) {
|
|
453
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>' : '') +
|
|
454
677
|
'<button type="button" class="link-btn cc-text-btn" data-cos="cancel" data-cid="' + a.cid + '">Cancel</button>';
|
|
455
678
|
} else if (a.status === 'completed' && a.no) {
|
|
456
679
|
acts = '<button type="button" class="cx-btn primary" data-cos="open-msg" data-no="' + a.no + '">Open in Messages</button>';
|
|
680
|
+
} else if ((a.status === 'failed' || a.status === 'interrupted') && a.jobId && !a.retried && !a.prepDraftId) {
|
|
681
|
+
acts = a.againBlocked
|
|
682
|
+
? '<button type="button" class="cx-btn" disabled aria-disabled="true">Ask again</button><span class="cc-fine">' + esc(a.againBlocked) + '</span>'
|
|
683
|
+
: '<button type="button" class="cx-btn primary" data-cos="again" data-cid="' + a.cid + '">Ask again</button>';
|
|
457
684
|
}
|
|
458
685
|
// A drop that became a question already shows its words as the drop: only the answer card is added.
|
|
459
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>') +
|
|
460
687
|
'<div class="cc-line cos" data-ask="' + a.cid + '"><span class="cc-avatar">' + MARK + '</span><div class="cc-card">' +
|
|
461
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>' +
|
|
462
|
-
'<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>' +
|
|
463
692
|
(a.error ? '<p class="cc-fine warn">' + esc(a.error) + '</p>' : '') +
|
|
464
693
|
'<div class="cc-acts" data-ask-acts>' + acts + '</div>' +
|
|
465
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>' : '') +
|
|
@@ -474,10 +703,12 @@
|
|
|
474
703
|
if (C.items === null && C.feedState !== 'error') html = W.skeletonRows(3);
|
|
475
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>';
|
|
476
705
|
else {
|
|
477
|
-
|
|
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) }; });
|
|
478
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) }); });
|
|
479
709
|
rows.sort(function (a, b) { return String(a.at).localeCompare(String(b.at)); });
|
|
480
|
-
|
|
710
|
+
var older = archiveHtml();
|
|
711
|
+
html = rows.length || older ? older + rows.map(function (r) { return r.html; }).join('')
|
|
481
712
|
: dropOn() ? '<p class="cc-empty">Nothing here yet. Ask COS something, or drop a thought and COS files it.</p>'
|
|
482
713
|
: '<p class="cc-empty">Ask COS anything. Attach photos or files with the clip.</p>';
|
|
483
714
|
}
|
|
@@ -493,7 +724,8 @@
|
|
|
493
724
|
else if (W.parseRoute().args[0] === 'cos') window.scrollTo(0, document.body.scrollHeight);
|
|
494
725
|
}
|
|
495
726
|
// A running Ask updates its own card in place, so a poll never rebuilds the chat (or the reader).
|
|
496
|
-
|
|
727
|
+
// `inPlace`: repaint this card only, never the chat around it (a late answer from recoverAnswer).
|
|
728
|
+
function paintAsk(a, inPlace) {
|
|
497
729
|
var el = document.querySelector('[data-ask="' + a.cid + '"]');
|
|
498
730
|
if (!el) return a.replyKey ? paintReplyFeed(a.replyKey) : paintFeed(false);
|
|
499
731
|
var running = a.status === 'sending' || a.status === 'queued' || a.status === 'running';
|
|
@@ -501,7 +733,12 @@
|
|
|
501
733
|
var st = el.querySelector('[data-ask-state]'); if (st) st.textContent = askState(a);
|
|
502
734
|
var body = el.querySelector('[data-ask-body]'); var text = a.response || a.partial;
|
|
503
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' : '')); }
|
|
504
|
-
|
|
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); }
|
|
505
742
|
}
|
|
506
743
|
|
|
507
744
|
function uploadsPending(k) { return k.atts.some(function (t) { return t.state === 'reading' || t.state === 'uploading'; }); }
|
|
@@ -953,8 +1190,18 @@
|
|
|
953
1190
|
var line = threadLine();
|
|
954
1191
|
document.querySelectorAll('[data-cos-thread]').forEach(function (el) { el.textContent = line; });
|
|
955
1192
|
}
|
|
1193
|
+
// Every thread answer (read, a turn recorded, New thread) also says which session the next Ask continues, so the
|
|
1194
|
+
// Default label follows the thread's own session (skeptic, 7.0.2). A fresh start continues none.
|
|
1195
|
+
function threadIdOf(t) { return t && t.thread && typeof t.thread.id === 'string' ? t.thread.id : ''; }
|
|
1196
|
+
function setThread(t) {
|
|
1197
|
+
C.thread = t;
|
|
1198
|
+
var n = t && t.next;
|
|
1199
|
+
var sid = n ? (n.mode === 'continue' && typeof n.sessionId === 'string' ? n.sessionId : '') : (t && t.thread && typeof t.thread.sessionId === 'string' ? t.thread.sessionId : '');
|
|
1200
|
+
if (sid !== C.threadSession) { C.threadSession = sid; if (threadOn()) refreshDefault(); }
|
|
1201
|
+
paintThread();
|
|
1202
|
+
}
|
|
956
1203
|
function loadThread() {
|
|
957
|
-
return api('/api/cos-thread').then(function (t) {
|
|
1204
|
+
return api('/api/cos-thread').then(function (t) { setThread(t); return t; }, function () { return null; });
|
|
958
1205
|
}
|
|
959
1206
|
// What the next Ask in the thread sends: a sessionId to continue, or none and the digest to carry.
|
|
960
1207
|
function threadPlan() {
|
|
@@ -979,7 +1226,7 @@
|
|
|
979
1226
|
if (Number.isInteger(a.no) && a.no > 0) body.messageNo = a.no;
|
|
980
1227
|
api('/api/cos-thread/turn', { method: 'POST', body: body }).then(function (t) {
|
|
981
1228
|
a.turnState = 'done';
|
|
982
|
-
if (t && t.thread)
|
|
1229
|
+
if (t && t.thread) setThread(t);
|
|
983
1230
|
}, function () { a.turnState = 'done'; });
|
|
984
1231
|
}
|
|
985
1232
|
function newThread() {
|
|
@@ -987,8 +1234,8 @@
|
|
|
987
1234
|
C.threadBusy = true;
|
|
988
1235
|
api('/api/cos-thread/new', { method: 'POST', body: {} }).then(function (t) {
|
|
989
1236
|
C.threadBusy = false;
|
|
990
|
-
C.
|
|
991
|
-
|
|
1237
|
+
C.threadCarried = '';
|
|
1238
|
+
setThread(t);
|
|
992
1239
|
}, function (err) {
|
|
993
1240
|
C.threadBusy = false;
|
|
994
1241
|
W.toast((err && err.message) || 'New thread did not go through.');
|
|
@@ -1014,16 +1261,23 @@
|
|
|
1014
1261
|
function submitAsk(text, dropId, done, opts) {
|
|
1015
1262
|
opts = opts || {};
|
|
1016
1263
|
var a = { cid: uuid(), query: text, at: Date.now(), status: 'sending', partial: '', dropId: dropId || '', atts: opts.atts || [], replyKey: opts.replyKey || '' };
|
|
1264
|
+
// 7.0.2 (A6): what Ask again sends: the same words, files and conversation.
|
|
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 || '' };
|
|
1017
1266
|
C.asks.push(a);
|
|
1018
1267
|
if (a.replyKey) paintReplyFeed(a.replyKey); else paintFeed(true);
|
|
1019
1268
|
var body = { clientJobId: a.cid, generation: 1, query: text, source: 'web', activityToolMode: 'status' };
|
|
1020
1269
|
if (C.model) body.model = C.model;
|
|
1021
1270
|
if (C.effort) body.effort = C.effort;
|
|
1022
1271
|
// 6.99.16: an Ask from the chat's own composer belongs to the COS thread (a Reply keeps its turn's conversation).
|
|
1272
|
+
// An Ask again only reaches here once askAgain has confirmed the conversation is still this one (skeptic, 7.0.2).
|
|
1023
1273
|
a.threaded = threadOn() && !opts.replyKey;
|
|
1024
|
-
|
|
1274
|
+
// A Reply never takes the chat's session: with no session of its own it starts a conversation, as its card says.
|
|
1275
|
+
if (!a.threaded && !opts.replyKey && C.session && C.session.id) body.sessionId = C.session.id;
|
|
1025
1276
|
// A reply continues the turn's own conversation (kept when it is still live; else the server starts a new one).
|
|
1026
1277
|
if (opts.sessionId) body.sessionId = opts.sessionId;
|
|
1278
|
+
// 7.0.2: which conversation this Ask is for, as Ask again needs it: the replied-to turn's session, the COS thread
|
|
1279
|
+
// (its id, read when the plan is made), or the chat's own session ('' for a new one).
|
|
1280
|
+
a.conv = a.replyKey ? { kind: 'reply', sessionId: opts.sessionId || '' } : a.threaded ? { kind: 'thread', threadId: '' } : { kind: 'chat', sessionId: body.sessionId || '' };
|
|
1027
1281
|
if (opts.attachmentIds && opts.attachmentIds.length) body.attachmentIds = opts.attachmentIds.slice(0, MAX_ATTS);
|
|
1028
1282
|
if (C.caps && C.caps.steer) body.steerable = true;
|
|
1029
1283
|
var admit = function (retry) {
|
|
@@ -1036,22 +1290,41 @@
|
|
|
1036
1290
|
});
|
|
1037
1291
|
};
|
|
1038
1292
|
var prepare = (a.threaded ? threadPlan().then(function (plan) {
|
|
1039
|
-
|
|
1293
|
+
a.conv.threadId = threadIdOf(C.thread);
|
|
1294
|
+
if (plan.sessionId) { body.sessionId = plan.sessionId; C.threadSession = plan.sessionId; }
|
|
1040
1295
|
else if (plan.digest) body.reference = { query: THREAD_REF, response: plan.digest };
|
|
1041
1296
|
a.carried = plan.sessionId ? '' : plan.digest ? 'summary' : 'plain';
|
|
1042
|
-
}) : Promise.resolve()).then(function () {
|
|
1297
|
+
}) : Promise.resolve()).then(function () {
|
|
1298
|
+
// An Ask again: the conversation its first try was for must still be the one this send resolved to, checked here,
|
|
1299
|
+
// after the plan, immediately before admission (root review, 7.0.2: the thread can change between askAgain's check
|
|
1300
|
+
// and this send's own thread read, and a retry carries old files and context).
|
|
1301
|
+
if (opts.expect && !sameConversation(opts.expect, a)) throw Object.assign(new Error(MOVED_ON), { code: 'conversation_moved' });
|
|
1302
|
+
withContext(body, opts.context, opts.contextRef);
|
|
1303
|
+
});
|
|
1043
1304
|
prepare.then(function () { return admit(true); }).then(function (d) {
|
|
1044
1305
|
var job = d && d.job; if (!job) throw new Error('The Mac did not take the question.');
|
|
1045
1306
|
take(a, job);
|
|
1046
|
-
|
|
1307
|
+
var before = defaultSession();
|
|
1308
|
+
if (a.threaded) { C.threadCarried = a.carried; if (job.sessionId) C.threadSession = job.sessionId; paintThread(); }
|
|
1047
1309
|
else if (job.sessionId && !a.replyKey) { C.session = { id: job.sessionId, day: today() }; save(); }
|
|
1310
|
+
if (defaultSession() !== before) refreshDefault();
|
|
1048
1311
|
if (dropId) api('/api/drops/' + encodeURIComponent(dropId) + '/ask', { method: 'POST', body: { clientJobId: a.cid, generation: 1 } }).catch(function () {});
|
|
1049
1312
|
if (done) done(true);
|
|
1050
1313
|
poll(a);
|
|
1051
1314
|
}, function (err) {
|
|
1315
|
+
if (err && err.code === 'conversation_moved' && opts.retryOf) {
|
|
1316
|
+
// Nothing was sent: this try leaves the chat, and the failed card it came from says why, its button disabled.
|
|
1317
|
+
C.asks = C.asks.filter(function (x) { return x !== a; });
|
|
1318
|
+
opts.retryOf.retried = false; opts.retryOf.againBlocked = MOVED_ON;
|
|
1319
|
+
if (a.replyKey) paintReplyFeed(a.replyKey); else paintFeed(false);
|
|
1320
|
+
if (done) done(false);
|
|
1321
|
+
return;
|
|
1322
|
+
}
|
|
1052
1323
|
a.status = 'failed';
|
|
1053
1324
|
a.error = err && err.code === 'durable_query_jobs_disabled' ? 'Ask needs the durable query jobs switch on your Mac.'
|
|
1054
|
-
: err && /attachment/.test(String(err.code || '')) ? 'An attachment could not be read: ' + ((err && err.message) || err.code) +
|
|
1325
|
+
: err && /attachment/.test(String(err.code || '')) ? 'An attachment could not be read: ' + ((err && err.message) || err.code) +
|
|
1326
|
+
// An Ask again never came from the box (QA): say what to do instead of pointing at it.
|
|
1327
|
+
(opts.reask ? ' Attach the file again in the box and ask again.' : ' Your words and files are still in the box.')
|
|
1055
1328
|
: 'Not sent: ' + ((err && err.message) || 'the Mac did not answer');
|
|
1056
1329
|
if (a.replyKey) paintReplyFeed(a.replyKey); else paintFeed(false);
|
|
1057
1330
|
if (done) done(false);
|
|
@@ -1060,12 +1333,63 @@
|
|
|
1060
1333
|
}
|
|
1061
1334
|
// An Ask from Today carries what Today shows (app.js askCos, panes/today-actions.js summary) as the quoted reference,
|
|
1062
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).
|
|
1063
1337
|
var TODAY_REF = 'What Today in COS Control shows right now', CONTEXT_MAX = 6000;
|
|
1064
|
-
function withContext(body, context) {
|
|
1338
|
+
function withContext(body, context, ref) {
|
|
1065
1339
|
var text = typeof context === 'string' ? context.trim().slice(0, CONTEXT_MAX) : '';
|
|
1066
1340
|
if (!text) return;
|
|
1067
|
-
|
|
1068
|
-
|
|
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
|
+
});
|
|
1069
1393
|
}
|
|
1070
1394
|
function take(a, job) {
|
|
1071
1395
|
a.jobId = job.jobId; a.status = job.status || a.status;
|
|
@@ -1073,14 +1397,109 @@
|
|
|
1073
1397
|
if (job.sessionId) a.sessionId = job.sessionId;
|
|
1074
1398
|
if (typeof job.partialText === 'string') a.partial = job.partialText;
|
|
1075
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); }
|
|
1076
1402
|
if (job.steerable === false) a.steerable = false;
|
|
1077
|
-
if (job.error && (a.status === 'failed' || a.status === 'interrupted')) a.error =
|
|
1403
|
+
if (job.error && (a.status === 'failed' || a.status === 'interrupted')) a.error = errorLine(job);
|
|
1404
|
+
}
|
|
1405
|
+
// 7.0.2 (A6, the Duo audit): the line on a failed card says why and what to do, by the server's error.code. On 11/07
|
|
1406
|
+
// Codex was out of usage until Nov 13 and the card said only "COS could not finish this answer". The reset time comes
|
|
1407
|
+
// from the provider's own words in error.message when they carry one, else from retryAfterMs, else it is left out.
|
|
1408
|
+
var GENERIC_FAIL = 'COS could not finish this answer. Ask again when you are ready.';
|
|
1409
|
+
var PROVIDER_NAME = { claude: 'Claude', codex: 'Codex', cursor: 'Cursor', ollama: 'Ollama' };
|
|
1410
|
+
function providerName(job) {
|
|
1411
|
+
if (job && PROVIDER_NAME[job.provider]) return PROVIDER_NAME[job.provider];
|
|
1412
|
+
var m = String((job && (job.resolvedModel || job.requestedModel)) || '');
|
|
1413
|
+
if (/^codex/.test(m)) return 'Codex';
|
|
1414
|
+
if (/^cursor/.test(m)) return 'Cursor';
|
|
1415
|
+
if (/^ollama/.test(m)) return 'Ollama';
|
|
1416
|
+
if (/^(opus|fable|sonnet|haiku)/.test(m)) return 'Claude';
|
|
1417
|
+
return '';
|
|
1418
|
+
}
|
|
1419
|
+
// "until <when>" or "for <how long>" from the provider's words or retryAfterMs; '' when neither says. A machine time
|
|
1420
|
+
// (ISO 8601, or one that names UTC or GMT) is shown in this browser's own time zone.
|
|
1421
|
+
var RESET_ISO = /\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?(?:Z|[+-]\d{2}:?\d{2})?/;
|
|
1422
|
+
function localWhen(d) {
|
|
1423
|
+
try { return d.toLocaleString(undefined, { month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit' }); } catch (_e) { return ''; }
|
|
1424
|
+
}
|
|
1425
|
+
function resetTime(err) {
|
|
1426
|
+
var msg = String((err && err.message) || '').slice(0, 2000);
|
|
1427
|
+
var m = /\b(?:try again|available again|resets?)\s+(at|on|after|in)?\s*([^.!\n·(]{2,48})/i.exec(msg), prep = m ? String(m[1] || '').toLowerCase() : '';
|
|
1428
|
+
if (!m) { m = /\buntil\s+()([^.!\n·(]{2,48})/i.exec(msg); prep = ''; }
|
|
1429
|
+
if (m) {
|
|
1430
|
+
var iso = RESET_ISO.exec(msg.slice(m.index));
|
|
1431
|
+
var when = iso && iso.index < 24 ? new Date(iso[0].replace(' ', 'T')) : /\b(?:UTC|GMT)\b/.test(m[2]) ? new Date(m[2].trim()) : null;
|
|
1432
|
+
if (when && !isNaN(when.getTime())) { var local = localWhen(when); if (local) return 'until ' + local; }
|
|
1433
|
+
var t = m[2].replace(/[\s,;:]+$/, '').trim();
|
|
1434
|
+
if (/\d/.test(t)) return (prep === 'in' ? 'for ' : 'until ') + t;
|
|
1435
|
+
}
|
|
1436
|
+
var ms = err && Number(err.retryAfterMs);
|
|
1437
|
+
if (ms > 0 && isFinite(ms)) { var later = localWhen(new Date(Date.now() + ms)); return later ? 'until ' + later : ''; }
|
|
1438
|
+
return '';
|
|
1439
|
+
}
|
|
1440
|
+
function errorLine(job) {
|
|
1441
|
+
var err = (job && job.error) || {}, code = String(err.code || ''), who = providerName(job);
|
|
1442
|
+
if (code === 'provider_limit') {
|
|
1443
|
+
var until = resetTime(err);
|
|
1444
|
+
return (who || 'This model') + ' is out of usage ' + (until || 'for now') + '. Pick another model under Model and ask again.';
|
|
1445
|
+
}
|
|
1446
|
+
if (code === 'provider_auth') return (who || 'This model') + ' is not signed in on your Mac. Sign in there, or pick another model under Model and ask again.';
|
|
1447
|
+
if (code === 'provider_context_too_long') return 'This conversation is too long for ' + (who || 'this model') + '. Start a new chat, or pick a model with a larger context under Model.';
|
|
1448
|
+
if (code === 'provider_timeout') return 'The model took longer than the time limit, so COS stopped waiting. Ask again, or pick a lower Effort for a faster answer.';
|
|
1449
|
+
if (code === 'interrupted' || (job && job.status === 'interrupted' && !code)) {
|
|
1450
|
+
if (err.cause === 'server_shutdown') return 'COS on your Mac shut down before this answer finished. When it is back, ask again.';
|
|
1451
|
+
if (err.cause === 'crash_loop_parked') return 'COS on your Mac stopped after crashing several times, so it set this question aside. Open COS Control on the Mac, then ask again.';
|
|
1452
|
+
return 'COS on your Mac restarted before this answer finished. Ask again when you are ready.';
|
|
1453
|
+
}
|
|
1454
|
+
return GENERIC_FAIL;
|
|
1455
|
+
}
|
|
1456
|
+
// Ask again: the same words and files as a new Ask (a new client job id), with the model and effort picked now, to the
|
|
1457
|
+
// SAME conversation as the first try. If that conversation has moved on (New thread, another thread now current, the
|
|
1458
|
+
// chat on another session, thread mode switched), nothing is sent: the button stays, disabled, with why (skeptic,
|
|
1459
|
+
// 7.0.2). A Reply goes to its turn's session as before. The failed card keeps its line and, once resent, loses the
|
|
1460
|
+
// button. Resolves to the new Ask, or null.
|
|
1461
|
+
var MOVED_ON = 'This conversation has moved on. Copy your words into a new Ask.';
|
|
1462
|
+
// Whether the Ask `a` (being sent now) resolved to the conversation `c`: the same thread, the chat's same session, or
|
|
1463
|
+
// the same Reply session. Read after the send's own plan, so it sees the thread that send will really use.
|
|
1464
|
+
function sameConversation(c, a) {
|
|
1465
|
+
if (!c || !a.conv || c.kind !== a.conv.kind) return false;
|
|
1466
|
+
if (c.kind === 'thread') return threadOn() && !!c.threadId && a.conv.threadId === c.threadId;
|
|
1467
|
+
return a.conv.sessionId === c.sessionId;
|
|
1468
|
+
}
|
|
1469
|
+
function conversationGone(a) {
|
|
1470
|
+
var c = a.conv;
|
|
1471
|
+
if (!c || c.kind === 'reply') return Promise.resolve('');
|
|
1472
|
+
if (c.kind === 'chat') {
|
|
1473
|
+
var now = C.session && C.session.id ? String(C.session.id) : '';
|
|
1474
|
+
return Promise.resolve(threadOn() || now !== c.sessionId ? MOVED_ON : '');
|
|
1475
|
+
}
|
|
1476
|
+
if (!threadOn() || !c.threadId) return Promise.resolve(MOVED_ON);
|
|
1477
|
+
return loadThread().then(function (t) { return threadIdOf(t) === c.threadId ? '' : MOVED_ON; });
|
|
1478
|
+
}
|
|
1479
|
+
function askAgain(cid) {
|
|
1480
|
+
var a = C.asks.filter(function (x) { return x.cid === cid; })[0];
|
|
1481
|
+
// A Prep card is never resent as a plain Ask (QA W4): Prep has its own Run.
|
|
1482
|
+
if (!a || a.retried || a.checking || a.againBlocked || a.prepDraftId || !(a.status === 'failed' || a.status === 'interrupted') || !a.jobId) return Promise.resolve(null);
|
|
1483
|
+
a.checking = true;
|
|
1484
|
+
return conversationGone(a).then(function (why) {
|
|
1485
|
+
a.checking = false;
|
|
1486
|
+
if (why) { a.againBlocked = why; if (a.replyKey) paintReplyFeed(a.replyKey); else paintFeed(false); return null; }
|
|
1487
|
+
a.retried = true;
|
|
1488
|
+
var g = a.again || {}, o = {};
|
|
1489
|
+
if (g.attachmentIds && g.attachmentIds.length) { o.attachmentIds = g.attachmentIds.slice(0, MAX_ATTS); o.atts = (g.atts || []).slice(0, MAX_ATTS); }
|
|
1490
|
+
if (g.replyKey) { o.replyKey = g.replyKey; o.sessionId = g.sessionId; }
|
|
1491
|
+
if (g.context) o.context = g.context;
|
|
1492
|
+
if (g.contextRef) o.contextRef = g.contextRef;
|
|
1493
|
+
o.reask = true;
|
|
1494
|
+
if (a.conv) { o.expect = a.conv; o.retryOf = a; }
|
|
1495
|
+
return submitAsk(a.query, null, null, o);
|
|
1496
|
+
});
|
|
1078
1497
|
}
|
|
1079
1498
|
function poll(a) {
|
|
1080
1499
|
var started = Date.now();
|
|
1081
1500
|
var tick = function () {
|
|
1082
1501
|
if (!a.jobId) return;
|
|
1083
|
-
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) {
|
|
1084
1503
|
if (d && d.job) take(a, d.job);
|
|
1085
1504
|
paintAsk(a);
|
|
1086
1505
|
if (['completed', 'failed', 'canceled', 'interrupted'].indexOf(a.status) >= 0) return finished(a);
|
|
@@ -1090,7 +1509,9 @@
|
|
|
1090
1509
|
setTimeout(tick, 600);
|
|
1091
1510
|
}
|
|
1092
1511
|
function finished(a) {
|
|
1512
|
+
if (a.status === 'completed' && !hasText(a)) recoverAnswer(a);
|
|
1093
1513
|
postTurn(a);
|
|
1514
|
+
refreshDefault();
|
|
1094
1515
|
if (a.replyKey) paintReplyFeed(a.replyKey); else paintFeed(false);
|
|
1095
1516
|
if (C.onAnswered) C.onAnswered();
|
|
1096
1517
|
// A drop that became a question gets its answer as a follow-up: read the chat again.
|
|
@@ -1151,6 +1572,9 @@
|
|
|
1151
1572
|
// ── actions on a line ─────────────────────────────────
|
|
1152
1573
|
function act(itemId, action) {
|
|
1153
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];
|
|
1154
1578
|
if (!item) return;
|
|
1155
1579
|
if (action === 'move') { C.moving = C.moving === itemId ? '' : itemId; return paintFeed(false); }
|
|
1156
1580
|
if (action === 'open_card') return W.go('work');
|
|
@@ -1179,6 +1603,8 @@
|
|
|
1179
1603
|
// A reminder action answers the finished item itself (merged above); dismiss answers counts only.
|
|
1180
1604
|
if (action === 'dismiss') item.state = 'done';
|
|
1181
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(); }
|
|
1182
1608
|
paintFeed(false); refreshSummary();
|
|
1183
1609
|
}, function (err) { W.toast((err && err.message) || 'That did not go through.'); });
|
|
1184
1610
|
}
|
|
@@ -1341,14 +1767,17 @@
|
|
|
1341
1767
|
var act = ((item && item.actions) || []).filter(function (a) { return a && a.id === 'prep'; })[0] || {};
|
|
1342
1768
|
var info = (act.prep && typeof act.prep === 'object' ? act.prep : item.prep && typeof item.prep === 'object' ? item.prep : {}) || {};
|
|
1343
1769
|
var key = 'item:' + item.id;
|
|
1344
|
-
|
|
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; }); }
|
|
1345
1772
|
if (P) closePrep();
|
|
1346
1773
|
P = { key: key, title: clip(info.title || item.title || 'my next meeting', 200), startsAt: typeof info.startsAt === 'string' ? info.startsAt : null,
|
|
1347
1774
|
draft: null, steer: '', dirty: false, ups: [], note: '', warn: false, running: false, pending: null, chain: Promise.resolve(), timer: null, conflicts: 0, loading: true };
|
|
1348
1775
|
paintPrep(true); focusPrep();
|
|
1349
1776
|
// On a phone the page scrolls: bring the composer up to where the card was tapped.
|
|
1350
1777
|
var slot = prepSlot(false); if (slot && slot.scrollIntoView && !wide()) slot.scrollIntoView({ block: 'start' });
|
|
1351
|
-
|
|
1778
|
+
var p = P;
|
|
1779
|
+
p.loaded = loadPrep(p);
|
|
1780
|
+
return p.loaded.then(function () { return p; });
|
|
1352
1781
|
}
|
|
1353
1782
|
function closePrep() {
|
|
1354
1783
|
if (!P) return;
|
|
@@ -1444,9 +1873,9 @@
|
|
|
1444
1873
|
|
|
1445
1874
|
// Files: the composer's own upload (uploadMedia), then POST .../files with the draft's revision.
|
|
1446
1875
|
function addPrepFiles(list) {
|
|
1447
|
-
var p = P; if (!p) return;
|
|
1876
|
+
var p = P; if (!p) return 0;
|
|
1448
1877
|
var files = Array.prototype.slice.call(list || []), refusals = [];
|
|
1449
|
-
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; }
|
|
1450
1879
|
files.forEach(function (file) {
|
|
1451
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; }
|
|
1452
1881
|
var c = classify(file);
|
|
@@ -1458,6 +1887,7 @@
|
|
|
1458
1887
|
});
|
|
1459
1888
|
setPrepNote(p, refusals.join(' '), !!refusals.length);
|
|
1460
1889
|
paintPrep();
|
|
1890
|
+
return refusals.length;
|
|
1461
1891
|
}
|
|
1462
1892
|
function sendPrepFile(p, t) {
|
|
1463
1893
|
t.state = t.mediaId ? 'adding' : 'reading'; t.progress = 0; t.error = '';
|
|
@@ -1483,7 +1913,7 @@
|
|
|
1483
1913
|
});
|
|
1484
1914
|
}).then(function (r) {
|
|
1485
1915
|
if (!r || p.ups.indexOf(t) < 0) return;
|
|
1486
|
-
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); }
|
|
1487
1917
|
// Refused (a 4xx), so nothing was recorded: Retry is a new operation. No answer or a 5xx: Retry is the same one.
|
|
1488
1918
|
if (r.status >= 400 && r.status < 500) t.opId = '';
|
|
1489
1919
|
// The staged copy is gone or differs: Retry uploads the bytes again.
|
|
@@ -1492,11 +1922,13 @@
|
|
|
1492
1922
|
t.state = 'failed';
|
|
1493
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);
|
|
1494
1924
|
paintPrep();
|
|
1925
|
+
prepUploadsSettled(p);
|
|
1495
1926
|
}).catch(function (err) {
|
|
1496
1927
|
if (p.ups.indexOf(t) < 0 || (err && err.code === 'aborted')) return;
|
|
1497
1928
|
t.state = 'failed';
|
|
1498
1929
|
t.error = err && err.code === 'kept' ? err.message : err && err.status === 401 ? 'Signed out. Sign in, then retry.' : uploadErrorText(err);
|
|
1499
1930
|
paintPrep();
|
|
1931
|
+
prepUploadsSettled(p);
|
|
1500
1932
|
});
|
|
1501
1933
|
}
|
|
1502
1934
|
function removePrepFile(p, fileId) {
|
|
@@ -1524,6 +1956,31 @@
|
|
|
1524
1956
|
return postRun(p, p.pending, true);
|
|
1525
1957
|
}).then(function (r) { return settleRun(p, r, true); });
|
|
1526
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
|
+
}
|
|
1527
1984
|
function runBody(p, run) {
|
|
1528
1985
|
var body = { clientRequestId: run.rid, revision: p.draft.revision };
|
|
1529
1986
|
if (run.model) body.model = run.model;
|
|
@@ -1780,6 +2237,7 @@
|
|
|
1780
2237
|
if (up.xhr) { try { up.xhr.abort(); } catch (_e) {} }
|
|
1781
2238
|
p.ups = p.ups.filter(function (x) { return x !== up; });
|
|
1782
2239
|
paintPrep();
|
|
2240
|
+
prepUploadsSettled(p);
|
|
1783
2241
|
}
|
|
1784
2242
|
}
|
|
1785
2243
|
function onPrepDrag(e, slot) {
|
|
@@ -1805,12 +2263,17 @@
|
|
|
1805
2263
|
if (what === 'move-to') return moveTo(t.getAttribute('data-item'), t.getAttribute('data-dest'), t.getAttribute('data-domain'));
|
|
1806
2264
|
if (what === 'move-cancel') { C.moving = ''; return paintFeed(false); }
|
|
1807
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; }
|
|
1808
2270
|
if (what === 'steer') {
|
|
1809
2271
|
var a = C.asks.filter(function (x) { return x.cid === t.getAttribute('data-cid'); })[0];
|
|
1810
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(); }
|
|
1811
2273
|
return;
|
|
1812
2274
|
}
|
|
1813
2275
|
if (what === 'cancel') return cancel(t.getAttribute('data-cid'));
|
|
2276
|
+
if (what === 'again') return askAgain(t.getAttribute('data-cid'));
|
|
1814
2277
|
if (what === 'att-remove') return removeAtt(kOf(t), t.getAttribute('data-att'));
|
|
1815
2278
|
if (what === 'att-retry') {
|
|
1816
2279
|
var k = kOf(t);
|
|
@@ -1880,6 +2343,85 @@
|
|
|
1880
2343
|
form.classList.remove('dragging');
|
|
1881
2344
|
if (e.type === 'drop') addFiles(composer(form.getAttribute('data-cos-form')), e.dataTransfer.files);
|
|
1882
2345
|
}
|
|
2346
|
+
// 7.0.2 (A2, the Duo audit): after the keyboard closes on the chat (one column, the page scrolls), iOS can leave the
|
|
2347
|
+
// page scrolled past its end, which showed as blank page under Model and Effort. Once it has closed (app.js
|
|
2348
|
+
// COSWeb.onKeyboard 'after'), scrollY comes back inside the page's range, and when the chat's own box had the
|
|
2349
|
+
// keyboard the composer goes back to the bottom of the screen.
|
|
2350
|
+
var LAST = { composer: false };
|
|
2351
|
+
document.addEventListener('focusin', function (e) {
|
|
2352
|
+
var f = e && e.target && e.target.closest ? e.target.closest('[data-cos-form]') : null;
|
|
2353
|
+
LAST.composer = !!(f && f.getAttribute('data-cos-form') === CHAT.name);
|
|
2354
|
+
});
|
|
2355
|
+
function settleAfterKeyboard() {
|
|
2356
|
+
var r = W.parseRoute();
|
|
2357
|
+
if (wide() || r.pane !== 'messages' || r.args[0] !== 'cos') return false;
|
|
2358
|
+
var se = document.scrollingElement || document.documentElement;
|
|
2359
|
+
if (!se) return false;
|
|
2360
|
+
var max = Math.max(0, Math.round((se.scrollHeight || 0) - (window.innerHeight || 0))), y = Math.round(window.scrollY || 0);
|
|
2361
|
+
if (y > max) { window.scrollTo(0, max); return true; }
|
|
2362
|
+
var form = formOf(CHAT);
|
|
2363
|
+
if (LAST.composer && form && form.getBoundingClientRect && (window.innerHeight || 0) - form.getBoundingClientRect().bottom > 1) { window.scrollTo(0, max); return true; }
|
|
2364
|
+
return false;
|
|
2365
|
+
}
|
|
2366
|
+
// 7.0.2 (A4): two columns (860 px and up, the unfolded Duo) with the keyboard up for a field in the chat column.
|
|
2367
|
+
// cos-chat.css makes the column the visible box (html.kb-chat), but a sticky column only reaches its top once the page
|
|
2368
|
+
// has scrolled to it, so on open the page scrolls it there, and where the page is too short to, the column is capped
|
|
2369
|
+
// to what is left above the keyboard. The cap follows each change of the visible box while the keyboard is up (a
|
|
2370
|
+
// keyboard rising in steps, QA W6) and goes when it closes, when focus leaves the column, or below 860 px (a fold
|
|
2371
|
+
// with the keyboard up never keeps it).
|
|
2372
|
+
var DOCK_MIN_PX = 120;
|
|
2373
|
+
function dockWide(what, v) {
|
|
2374
|
+
var mr = C.host;
|
|
2375
|
+
if (!mr || !mr.style) return false;
|
|
2376
|
+
var a = document.activeElement;
|
|
2377
|
+
var keep = (what === 'open' || what === 'move') && wide() && !!a && !!mr.contains && mr.contains(a) && W.parseRoute().pane === 'messages';
|
|
2378
|
+
v = v || (W.viewportFit ? W.viewportFit() : null);
|
|
2379
|
+
if (!keep || !v || !mr.getBoundingClientRect) { mr.style.maxHeight = ''; return false; }
|
|
2380
|
+
var want = v.top + 8, top = mr.getBoundingClientRect().top;
|
|
2381
|
+
// On open, and on any later move that left the column's top too low to fit a usable column (an unfold with the
|
|
2382
|
+
// keyboard up, canary 7.0.2 WARNING 2), the page scrolls the column up to the visible box.
|
|
2383
|
+
var low = top > v.top + v.height - DOCK_MIN_PX - 8;
|
|
2384
|
+
if ((what === 'open' || low) && top > want + 1) { window.scrollBy(0, Math.round(top - want)); top = mr.getBoundingClientRect().top; }
|
|
2385
|
+
mr.style.maxHeight = Math.max(DOCK_MIN_PX, Math.round(v.top + v.height - Math.max(top, v.top) - 8)) + 'px';
|
|
2386
|
+
return true;
|
|
2387
|
+
}
|
|
2388
|
+
// 7.0.2 (QA W7): which composer the keyboard is for. html.kb-up (app.js) says only that a keyboard is up on the page;
|
|
2389
|
+
// html.kb-chat says it is for a field in the chat column (cos-chat.css docks the column and the one-column composer on
|
|
2390
|
+
// it), and .cc-kb marks the one composer that has it (only that one turns compact). The Messages search box at 860 px
|
|
2391
|
+
// and up, or a field in another pane, leaves the chat as it is.
|
|
2392
|
+
function markKeyboard(up) {
|
|
2393
|
+
var a = up ? document.activeElement : null, de = document.documentElement;
|
|
2394
|
+
var form = a && a.closest ? a.closest('[data-cos-form]') : null;
|
|
2395
|
+
document.querySelectorAll('.cc-composer.cc-kb').forEach(function (f) { if (f !== form) f.classList.remove('cc-kb'); });
|
|
2396
|
+
if (form && form.classList) form.classList.add('cc-kb');
|
|
2397
|
+
var inChat = !!(a && C.host && C.host.contains && C.host.contains(a));
|
|
2398
|
+
if (de && de.classList) de.classList.toggle('kb-chat', inChat);
|
|
2399
|
+
return inChat;
|
|
2400
|
+
}
|
|
2401
|
+
// The composer that has the keyboard, brought into view on open and on every later change of the visible box (a fold
|
|
2402
|
+
// or an unfold with the keyboard up, canary 7.0.2). Only while the keyboard is for the chat, and only for a composer
|
|
2403
|
+
// (a steer box or the search box is left where it is).
|
|
2404
|
+
function revealComposer() {
|
|
2405
|
+
var a = document.activeElement, de = document.documentElement;
|
|
2406
|
+
if (!a || !de || !de.classList || !de.classList.contains('kb-chat')) return false;
|
|
2407
|
+
var form = a.closest ? a.closest('[data-cos-form]') : null;
|
|
2408
|
+
if (!form || !form.scrollIntoView) return false;
|
|
2409
|
+
form.scrollIntoView({ block: 'nearest' });
|
|
2410
|
+
return true;
|
|
2411
|
+
}
|
|
2412
|
+
if (W.onKeyboard) W.onKeyboard(function (what, v) {
|
|
2413
|
+
if (what === 'after') { settleAfterKeyboard(); return; }
|
|
2414
|
+
markKeyboard(what !== 'close');
|
|
2415
|
+
dockWide(what, v);
|
|
2416
|
+
if (what !== 'close') revealComposer();
|
|
2417
|
+
});
|
|
2418
|
+
// Focus moving between fields while the keyboard stays up: mark (and dock) for the new one.
|
|
2419
|
+
document.addEventListener('focusin', function () {
|
|
2420
|
+
if (!W.keyboardUp || !W.keyboardUp()) return;
|
|
2421
|
+
markKeyboard(true);
|
|
2422
|
+
dockWide('move');
|
|
2423
|
+
});
|
|
2424
|
+
|
|
1883
2425
|
// Crossing 860 px shows the chat beside the list: load it then.
|
|
1884
2426
|
window.matchMedia('(min-width: 860px)').addEventListener('change', function (m) {
|
|
1885
2427
|
if (m.matches && on() && C.host && W.parseRoute().pane === 'messages') mount(C.host, true);
|
|
@@ -1892,6 +2434,8 @@
|
|
|
1892
2434
|
on: on, dropOn: dropOn, askOn: askOn, known: function () { return !!C.caps; }, ensureCaps: ensureCaps, fromHealth: fromHealth, refreshSummary: refreshSummary,
|
|
1893
2435
|
pinnedRowHtml: pinnedRowHtml, mount: mount, wide: wide,
|
|
1894
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,
|
|
1895
2439
|
onClick: onClick, onKey: onKey, onInput: onInput, onChange: onChangeEvent, onSubmit: onSubmit, onPaste: onPaste, onDrag: onDrag,
|
|
1896
2440
|
setOnChange: function (fn) { C.onChange = fn; }, setOnAnswered: function (fn) { C.onAnswered = fn; },
|
|
1897
2441
|
// Test and render hooks: what the chat holds right now.
|
|
@@ -1922,6 +2466,8 @@
|
|
|
1922
2466
|
// How many files a session turn takes on this Mac (threadTurnAttachments); 0 means words only.
|
|
1923
2467
|
turnFiles: function () { return C.caps && C.caps.turnFiles > 0 ? C.caps.turnFiles : 0; },
|
|
1924
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; },
|
|
1925
2471
|
askOn: askOn, ensureCaps: ensureCaps
|
|
1926
2472
|
},
|
|
1927
2473
|
// 6.99.19: Prep. meetings.js reads the capability from here (one /api/health read for the page).
|
|
@@ -1938,10 +2484,48 @@
|
|
|
1938
2484
|
return '';
|
|
1939
2485
|
});
|
|
1940
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,
|
|
1941
2524
|
// The Prep server id to name on a write: the newest from capabilities.prep or any Prep answer (meetings.js reports its own).
|
|
1942
2525
|
prepInstance: function () { return PREP_INST; }, notePrepInstance: function (id) { if (typeof id === 'string' && id) PREP_INST = id; },
|
|
1943
2526
|
_prep: function () { return P; }, _openPrep: openPrep, _prepHtml: function () { return P ? prepPanelHtml(P) : ''; }, _addPrepFiles: addPrepFiles,
|
|
1944
2527
|
_runPrep: function () { runPrep(P); }, _sha256Js: sha256Js, _closePrep: closePrep, _onPrepInput: onPrepInput, _savePrepSteer: function () { return P ? savePrepSteer(P) : null; },
|
|
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,
|
|
1945
2529
|
_state: C, _composer: composer, _classify: classify, _modelsFrom: modelsFrom, _addFiles: addFiles, _itemHtml: itemHtml, _setCaps: setCaps, _threadLine: threadLine, _postTurn: postTurn
|
|
1946
2530
|
};
|
|
1947
2531
|
})();
|