@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
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
var CATEGORIES = [
|
|
17
17
|
{ id: 'general', title: 'General', summary: 'How COS Control opens, its shortcut, Today and the setup guide.', mac: ['App', 'Activity home', 'Setup guide'] },
|
|
18
18
|
{ id: 'ai', title: 'AI and accounts', summary: 'The AI apps COS works through, your keys and the models behind Work.', mac: ['Your AI apps', 'Local model', 'Your own key', 'Search'] },
|
|
19
|
-
{ id: 'glasses', title: 'Glasses and
|
|
19
|
+
{ id: 'glasses', title: 'Glasses, phone and web', summary: 'Pair your glasses and phone, and how videos reach this Mac.', mac: ['Devices', 'Video uploads'] },
|
|
20
20
|
{ id: 'meetings', title: 'Meetings', summary: 'Recording, the meeting assistant, live cues and your meeting library.', mac: ['Recording', 'Meeting assistant', 'Library and sync'] },
|
|
21
21
|
{ id: 'dictation', title: 'Dictation', summary: 'Type by voice into any Mac app: shortcuts, sounds, text and AI polish.', mac: ['Dictation', 'Microphone', 'Shortcuts', 'Sounds and audio', 'Text', 'AI polish', 'History'] },
|
|
22
22
|
{ id: 'voice', title: 'Transcription and voice', summary: 'The engines and models that turn speech into text on this Mac.', mac: ['Voice setup', 'Models', 'Performance'] },
|
|
@@ -91,9 +91,10 @@
|
|
|
91
91
|
Promise.all([
|
|
92
92
|
soft(api('/api/decision-provider')), soft(api('/api/live-cues/v2/settings')), soft(api('/api/morning-brief')),
|
|
93
93
|
soft(api('/api/domains')), soft(api('/api/agent-sessions/fences')), soft(api('/api/meeting-import/fireflies/status')),
|
|
94
|
-
soft(api('/api/health')), soft(api('/api/drops/settings/p2')), soft(api('/api/today/brief/settings'))
|
|
94
|
+
soft(api('/api/health')), soft(api('/api/drops/settings/p2')), soft(api('/api/today/brief/settings')),
|
|
95
|
+
soft(api('/api/session-files/settings')), soft(api('/api/preview-folders/settings'))
|
|
95
96
|
]).then(function (r) {
|
|
96
|
-
S.data = { dp: r[0], cues: r[1], mb: r[2], domains: r[3], fences: r[4], ff: r[5], health: r[6], p2: r[7], tb: r[8] };
|
|
97
|
+
S.data = { dp: r[0], cues: r[1], mb: r[2], domains: r[3], fences: r[4], ff: r[5], health: r[6], p2: r[7], tb: r[8], sf: r[9], pf: r[10] };
|
|
97
98
|
S.state = 'ready';
|
|
98
99
|
render();
|
|
99
100
|
});
|
|
@@ -211,22 +212,68 @@
|
|
|
211
212
|
if (!st) return '';
|
|
212
213
|
var at = function (iso) { var d = new Date(iso); return isNaN(d) ? '' : d.toLocaleTimeString(undefined, { hour: 'numeric', minute: '2-digit' }); };
|
|
213
214
|
if (st.kind === 'paused') return 'Paused after three failed tries. It tries again at ' + at(st.until) + '.';
|
|
214
|
-
|
|
215
|
+
// 7.0.5: the number is the "Summaries per day" setting (an older server sends only the count).
|
|
216
|
+
if (st.kind === 'daily_cap') { var n = typeof st.cap === 'number' ? st.cap : st.count; return 'Today\u2019s ' + n + (n === 1 ? ' summary is' : ' summaries are') + ' used. It writes again tomorrow.'; }
|
|
215
217
|
if (st.kind === 'failed') return 'The last try failed at ' + at(st.at) + (st.code === 'timeout' ? ' (it took too long).' : st.code === 'unavailable' ? ' (Claude Code was not found on your Mac).' : '.');
|
|
216
218
|
return '';
|
|
217
219
|
}
|
|
220
|
+
// 6.99.35: "Show files and images your sessions make" (GET/POST /api/session-files/settings, lib/session-files-setting.ts).
|
|
221
|
+
// On by default. COS_WEB_SESSION_FILES=0 on the Mac forces it off; the row then says so and cannot be changed here.
|
|
222
|
+
function sessionFilesGroup(d) {
|
|
223
|
+
var sf = d && d.sf;
|
|
224
|
+
if (!sf || typeof sf.enabled !== 'boolean') return '';
|
|
225
|
+
var env = sf.source === 'env';
|
|
226
|
+
// QA 7.0.0: a saved setting the Mac could not read fails closed (off). The row says so; turning it on saves it again.
|
|
227
|
+
return group('Session files', '',
|
|
228
|
+
// 7.0.4: the switch also covers screenshots, images a session opened and images attached to prompts, so it says so.
|
|
229
|
+
row('Show session files, screenshots and images',
|
|
230
|
+
env ? 'Turned off on the Mac by the server environment (COS_WEB_SESSION_FILES=0).'
|
|
231
|
+
: sf.source === 'damaged' ? 'Couldn’t read the saved setting, so previews are off. Turn it on to save it again.'
|
|
232
|
+
: 'Lets this page show the files and images your sessions send you, the screenshots and images they open, and images attached to your prompts. Files stay on your Mac.',
|
|
233
|
+
toggle(sf.enabled, 'data-set="session-files"' + (env ? ' disabled' : ''), 'Show session files, screenshots and images')) +
|
|
234
|
+
previewFoldersRows(d.pf));
|
|
235
|
+
}
|
|
236
|
+
// 7.0.5 (preview engine, Miles decision 2b): extra folders a session's linked or sent files may open from, besides its
|
|
237
|
+
// own project folder (GET/POST /api/preview-folders/settings). Empty by default; each folder is listed by name and
|
|
238
|
+
// removed one at a time. The server refuses broad folders (home, Documents, Library, /) and says why.
|
|
239
|
+
function previewFoldersRows(pf) {
|
|
240
|
+
if (!pf || !Array.isArray(pf.folders)) return '';
|
|
241
|
+
var list = pf.folders.slice(0, 20);
|
|
242
|
+
var blurb = pf.source === 'damaged' ? 'Couldn’t read the saved list, so no extra folder counts. Add a folder to save it again.'
|
|
243
|
+
: list.length ? 'Sessions can also open files from these folders.' : 'None. Sessions open files only from their own project folder.';
|
|
244
|
+
return '<div class="cx-set-row cx-set-folders"><div class="cx-set-text"><span class="cx-set-name">Extra folders</span><span class="cx-set-desc" id="pf-blurb">' + esc(blurb) + '</span></div></div>' +
|
|
245
|
+
list.map(function (f, i) {
|
|
246
|
+
return '<div class="cx-set-row cx-set-folder"><div class="cx-set-text"><span class="cx-set-name pf-name">' + esc(f) + '</span></div>' +
|
|
247
|
+
'<button type="button" class="cx-btn quiet" data-set="pf-remove" data-index="' + i + '" aria-label="' + esc('Remove ' + f) + '">Remove</button></div>';
|
|
248
|
+
}).join('') +
|
|
249
|
+
(list.length < 20 ? '<form class="cx-set-row pf-add" id="pf-add"><label class="cx-set-text" for="pf-input"><span class="cx-set-desc">Add a project folder, like ~/cos-worktrees. Not your whole home or Documents folder.</span></label>' +
|
|
250
|
+
'<span class="pf-add-line"><input id="pf-input" type="text" inputmode="url" autocapitalize="off" autocorrect="off" spellcheck="false" placeholder="~/cos-worktrees" aria-label="Folder to add">' +
|
|
251
|
+
'<button type="submit" class="cx-btn" data-set="pf-add">Add</button></span></form>' : '');
|
|
252
|
+
}
|
|
253
|
+
function savePreviewFolders(next, done) {
|
|
254
|
+
api('/api/preview-folders/settings', { method: 'POST', body: { folders: next } }).then(function (v) {
|
|
255
|
+
if (S.data && v && Array.isArray(v.folders)) S.data.pf = v;
|
|
256
|
+
W.toast('Saved');
|
|
257
|
+
render();
|
|
258
|
+
if (done) done(true);
|
|
259
|
+
}, function (err) { say((err && err.message) || 'That folder was not saved.', true); if (done) done(false); });
|
|
260
|
+
}
|
|
218
261
|
function categoryBody(cat) {
|
|
219
262
|
var d = S.data || {};
|
|
220
263
|
var out = '';
|
|
221
264
|
if (cat.id === 'reach') return reachBody();
|
|
222
265
|
if (cat.id === 'pet') return petBody() + onMac(cat.mac);
|
|
223
|
-
|
|
266
|
+
if (cat.id === 'privacy') out += sessionFilesGroup(d);
|
|
267
|
+
// The Today summary (routes/today-brief.ts): Sonnet on the Mac writes Today's top lines (7.0.1; Haiku before). Off unless switched on here or
|
|
224
268
|
// by COS_TODAY_BRIEF_LLM on the Mac, which wins when set.
|
|
225
269
|
if (cat.id === 'general' && d.tb && typeof d.tb.enabled === 'boolean') {
|
|
226
270
|
out += group('Today summary', '',
|
|
227
271
|
row('Write the Today summary with a model', d.tb.source === 'env' ? 'Set on the Mac by the server environment.' :
|
|
228
|
-
'
|
|
272
|
+
'Sonnet writes two or three sentences after the count of what needs you. It reads what Today shows (session titles and states, each session\u2019s latest reply, the questions sessions ask you, what needs you) and, on your Mac, each session\u2019s sub-agents and the Work cards in progress. It runs only while Today is open, only when something changed, never during a recording, at most every 10 minutes and no more than Summaries per day allows. Refresh on Today asks for a new one at once, at most every 30 seconds.',
|
|
229
273
|
toggle(d.tb.enabled, 'data-set="today-brief"' + (d.tb.source === 'env' ? ' disabled' : ''), 'Write the Today summary with a model')) +
|
|
274
|
+
// 7.0.5 "Summaries per day" (GET/PUT /api/today/brief/settings/cap), 1 to 100, default 100. Shown when the server has it.
|
|
275
|
+
(typeof d.tb.dailyCap === 'number' ? row('Summaries per day', 'At most this many new summaries a day, Refresh included. 1 to 100.',
|
|
276
|
+
'<input type="number" class="cx-set-num" id="today-brief-cap" data-set="today-brief-cap" min="1" max="100" step="1" inputmode="numeric" value="' + esc(String(d.tb.dailyCap)) + '" aria-label="Summaries per day">') : '') +
|
|
230
277
|
(briefStatus(d.tb) ? '<p class="cx-set-blurb" id="today-brief-status">' + esc(briefStatus(d.tb)) + '</p>' : ''));
|
|
231
278
|
}
|
|
232
279
|
if (cat.id === 'ai' && d.dp) {
|
|
@@ -346,6 +393,25 @@
|
|
|
346
393
|
.then(function () { W.toast(!on ? 'Live cues on' : 'Live cues off'); }, function (err) { t.setAttribute('aria-checked', String(on)); say(err.message, true); });
|
|
347
394
|
return;
|
|
348
395
|
}
|
|
396
|
+
if (act === 'session-files') {
|
|
397
|
+
var son = t.getAttribute('aria-checked') === 'true';
|
|
398
|
+
t.setAttribute('aria-checked', String(!son));
|
|
399
|
+
api('/api/session-files/settings', { method: 'POST', body: { enabled: !son } })
|
|
400
|
+
.then(function (v) {
|
|
401
|
+
if (S.data && v) S.data.sf = v;
|
|
402
|
+
W.toast(!son ? 'Session files and images on' : 'Session files and images off');
|
|
403
|
+
render();
|
|
404
|
+
}, function (err) { t.setAttribute('aria-checked', String(son)); say(err.message, true); });
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
if (act === 'pf-remove') {
|
|
408
|
+
var cur = (S.data && S.data.pf && S.data.pf.folders) || [], idx = Number(t.getAttribute('data-index'));
|
|
409
|
+
if (!(idx >= 0 && idx < cur.length)) return;
|
|
410
|
+
t.disabled = true;
|
|
411
|
+
savePreviewFolders(cur.filter(function (_f, i) { return i !== idx; }), function (ok) { if (!ok) t.disabled = false; });
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
if (act === 'pf-add') return; // the form's submit handler saves
|
|
349
415
|
if (act === 'today-brief') {
|
|
350
416
|
var ton = t.getAttribute('aria-checked') === 'true';
|
|
351
417
|
t.setAttribute('aria-checked', String(!ton));
|
|
@@ -380,6 +446,15 @@
|
|
|
380
446
|
}
|
|
381
447
|
});
|
|
382
448
|
|
|
449
|
+
pane.addEventListener('submit', function (e) {
|
|
450
|
+
if (!e.target || e.target.id !== 'pf-add') return;
|
|
451
|
+
e.preventDefault();
|
|
452
|
+
var input = $('pf-input'), v = input ? input.value.trim() : '';
|
|
453
|
+
if (!v) { say('Type a folder, like ~/cos-worktrees.', true); return; }
|
|
454
|
+
var cur = (S.data && S.data.pf && S.data.pf.folders) || [];
|
|
455
|
+
savePreviewFolders(cur.concat([v]));
|
|
456
|
+
});
|
|
457
|
+
|
|
383
458
|
// A gallery picture that will not load shows the pet's name alone, never a broken image.
|
|
384
459
|
pane.addEventListener('error', function (e) {
|
|
385
460
|
var img = e.target;
|
|
@@ -391,6 +466,22 @@
|
|
|
391
466
|
|
|
392
467
|
// Number, time, day and steer fields save when they change (a number is clamped by the server, then shown as saved).
|
|
393
468
|
pane.addEventListener('change', function (e) {
|
|
469
|
+
// 7.0.5 "Summaries per day": the server clamps to 1 to 100 and answers what it saved; the field shows that.
|
|
470
|
+
if (e.target && e.target.id === 'today-brief-cap') {
|
|
471
|
+
// QA 7.0.5: an emptied field (or letters, which a number field reads as '') is Number('') = 0, which the server
|
|
472
|
+
// clamps to 1. So an empty, non-numeric or below-1 value sends nothing, keeps the saved number and says why.
|
|
473
|
+
var capField = e.target, capRaw = String(capField.value == null ? '' : capField.value).trim(), wanted = Number(capRaw);
|
|
474
|
+
if (capRaw === '' || !isFinite(wanted) || wanted < 1) {
|
|
475
|
+
if (S.data && S.data.tb && typeof S.data.tb.dailyCap === 'number') capField.value = String(S.data.tb.dailyCap);
|
|
476
|
+
say('Type a number from 1 to 100.', true);
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
api('/api/today/brief/settings/cap', { method: 'PUT', body: { dailyCap: wanted } }).then(function (v) {
|
|
480
|
+
if (v && typeof v.dailyCap === 'number') { capField.value = String(v.dailyCap); if (S.data && S.data.tb) S.data.tb.dailyCap = v.dailyCap; }
|
|
481
|
+
W.toast('Saved');
|
|
482
|
+
}, function (err) { say((err && err.message) || 'That did not save.', true); });
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
394
485
|
var f = e.target && e.target.closest ? e.target.closest('[data-p2]') : null;
|
|
395
486
|
if (!f || f.getAttribute('data-set') === 'p2' || !S.data || !S.data.p2) return;
|
|
396
487
|
var path = f.getAttribute('data-p2'), raw = f.value;
|
|
@@ -162,6 +162,41 @@ html.ta-open #sq-host { display: none; }
|
|
|
162
162
|
.next-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 4px 18px; padding: 0 16px 14px; }
|
|
163
163
|
.next-actions:empty { display: none; }
|
|
164
164
|
.next-actions .btn.small { min-height: 44px; }
|
|
165
|
+
/* Prep on every Next up row (Miles 11/09, app.js paintCalendarLater and nextPrepPanelHtml): a quiet Prep at the end of
|
|
166
|
+
each later row, and one small panel under the row it opens (the COS chat's field and buttons, laid flat in the card).
|
|
167
|
+
Spacing (Miles 14:15, from the 382 px render): every later row is one height, with or without Prep (the hold row
|
|
168
|
+
too), and its padding is inside that height, so the Prep link never makes a row taller. The open row and its panel
|
|
169
|
+
read as ONE block: no rules, the panel attached under its row, and the space under the block equals the space
|
|
170
|
+
between rows. The gap after Prep me, and the gap above "+N more today", are the same as between rows. One pitch per
|
|
171
|
+
width: --np-row is the row height, --np-gap the space the rows' own height leaves above and below a line of text,
|
|
172
|
+
--np-after the space under a button row (a button's box is 2 px taller than its line), measured equal to the row gap. */
|
|
173
|
+
#next-card { --np-row: 36px; --np-gap: 8px; --np-after: calc(var(--np-gap) + 2px); }
|
|
174
|
+
#next-card:has(.next-later:not(.hidden)) .next-actions { padding-bottom: var(--np-after); }
|
|
175
|
+
.next-later .later-row { align-items: center; min-height: var(--np-row); padding: 0; }
|
|
176
|
+
.next-later .later-what { flex: 1; min-width: 0; }
|
|
177
|
+
.next-later .later-prep { flex: none; min-height: var(--np-row); font-size: 14px; font-weight: 500; padding: 0 2px; }
|
|
178
|
+
.next-later .later-more { margin-top: 0; padding: 0; min-height: var(--np-row); }
|
|
179
|
+
.next-first-prep { padding: 0 16px var(--np-after); }
|
|
180
|
+
.next-first-prep.hidden { display: none; }
|
|
181
|
+
.next-prep { display: grid; gap: 8px; margin: 0 0 var(--np-after); padding: 2px 0 0; }
|
|
182
|
+
.next-first-prep .next-prep { margin: 0; padding: 0; }
|
|
183
|
+
.next-prep .field { width: 100%; min-height: 64px; max-height: 140px; font-size: 15px; resize: none; }
|
|
184
|
+
.next-prep-files { list-style: none; margin: 0; padding: 0; display: grid; gap: 2px; }
|
|
185
|
+
.next-prep-files li { display: flex; align-items: center; gap: 10px; min-width: 0; font-size: 14px; }
|
|
186
|
+
.next-prep-file { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--sub); }
|
|
187
|
+
.next-prep-files .link-btn { min-height: 32px; font-size: 14px; font-weight: 500; }
|
|
188
|
+
.next-prep-acts { display: flex; align-items: center; gap: 8px 14px; flex-wrap: wrap; }
|
|
189
|
+
.next-prep-gap { flex: 1; }
|
|
190
|
+
.next-prep-add { position: relative; cursor: pointer; }
|
|
191
|
+
.next-prep-add:focus-within { outline: 2px solid var(--accent); outline-offset: 2px; }
|
|
192
|
+
.next-prep-acts .cx-btn { height: 40px; }
|
|
193
|
+
.next-prep-acts .link-btn { min-height: 40px; font-size: 14px; }
|
|
194
|
+
/* Phones and short touch screens: 44 px touch rows, on the same query as app.css's 44 px floor for .cx-btn and .field,
|
|
195
|
+
so the panel's buttons and the rows change height together. */
|
|
196
|
+
@media (max-width: 599px), (pointer: coarse) and (max-height: 720px) {
|
|
197
|
+
#next-card { --np-row: 44px; --np-gap: 12px; }
|
|
198
|
+
.next-prep-files .link-btn, .next-prep-acts .link-btn { min-height: 44px; }
|
|
199
|
+
}
|
|
165
200
|
/* The Ask sheet's File to Work inbox result, on its own row (app.js fileNoteHtml). */
|
|
166
201
|
.nav-row .note.file-error { color: var(--hot); white-space: normal; }
|
|
167
202
|
/* Running card when the session list could not be read (app.js renderRunning). */
|
|
@@ -185,3 +220,14 @@ html.ta-open #sq-host { display: none; }
|
|
|
185
220
|
.ta-notsent:empty { display: none; }
|
|
186
221
|
.ta-notsent { margin: 4px 0 8px; }
|
|
187
222
|
.ta-composer.dragging { outline: 2px dashed var(--accent); outline-offset: 4px; }
|
|
223
|
+
/* 7.0.1 live state: Refresh and the line beside it under Today's date (app.js todayRefresh, liveNoteText). The button
|
|
224
|
+
keeps the 44 px target of every link button; the line says Updating, Reconnecting, "As of 2 min ago" or the server's
|
|
225
|
+
summary limit, in the date's own size and colour. */
|
|
226
|
+
.today-live { display: flex; align-items: center; flex-wrap: wrap; gap: 0 12px; margin: 2px 0 0; min-height: 44px; }
|
|
227
|
+
.today-refresh { min-height: 44px; min-width: 44px; padding: 0 2px; font-size: 15px; }
|
|
228
|
+
.today-refresh:disabled { opacity: 0.55; cursor: default; }
|
|
229
|
+
.today-live-note { color: var(--sub); font-size: 14px; line-height: 1.3; }
|
|
230
|
+
.today-live-note:empty { display: none; }
|
|
231
|
+
.today-live-note.warn { color: var(--warn, var(--sub)); }
|
|
232
|
+
/* A working row's clock ticks every second: tabular figures so the line does not shimmer. */
|
|
233
|
+
.run-clock { font-variant-numeric: tabular-nums; }
|
|
@@ -194,7 +194,7 @@
|
|
|
194
194
|
var KB_MIN_PX = 120;
|
|
195
195
|
var KB_STRIP_MIN_PX = 24;
|
|
196
196
|
var BODY_MIN_PX = 72; // about three lines of the conversation (15 px text at 1.5)
|
|
197
|
-
var KB = { on: false, strip: false, tight: false, frame: 0, listening: false, width: 0, inset: 0 };
|
|
197
|
+
var KB = { on: false, strip: false, tight: false, frame: 0, listening: false, width: 0, inset: 0, typed: false };
|
|
198
198
|
function tall(el) { return (el && el.offsetHeight) || 0; }
|
|
199
199
|
// What the conversation gets once the compact header and the reply box have theirs.
|
|
200
200
|
function bodyRoom(height) {
|
|
@@ -211,10 +211,13 @@
|
|
|
211
211
|
return !!a.isContentEditable;
|
|
212
212
|
}
|
|
213
213
|
function viewport() { return W.viewportFit ? W.viewportFit() : null; }
|
|
214
|
+
// 7.0.2: `wide` keeps its 6.99.30 meaning, 600 px wide or more, so old and new reports compare. From 7.0.2 a short
|
|
215
|
+
// touch screen at that width counts as a phone (app.js isPhone, A8): that is shortTouch.
|
|
216
|
+
function crumbWide() { return window.matchMedia ? !!window.matchMedia('(min-width: 600px)').matches : !phone(); }
|
|
214
217
|
function crumb(what, v, focus) {
|
|
215
218
|
if (!W.layoutCrumb || !O.open || !v) return;
|
|
216
219
|
W.layoutCrumb(what, { kind: O.open.kind, kb: KB.on, strip: KB.strip, tight: KB.tight, focus: !!focus, inset: v.inset, below: v.below,
|
|
217
|
-
lh: v.layoutH, top: v.top, h: v.height, zoomed: v.zoomed, wide:
|
|
220
|
+
lh: v.layoutH, top: v.top, h: v.height, zoomed: v.zoomed, wide: crumbWide(), shortTouch: crumbWide() && phone() });
|
|
218
221
|
}
|
|
219
222
|
function applyFit() {
|
|
220
223
|
KB.frame = 0;
|
|
@@ -246,7 +249,11 @@
|
|
|
246
249
|
// again would take the room back and flicker.
|
|
247
250
|
if (on && !KB.tight) KB.tight = bodyRoom(v.height) < BODY_MIN_PX;
|
|
248
251
|
layer.classList.toggle('ta-kb-tight', on && KB.tight);
|
|
249
|
-
|
|
252
|
+
// 7.0.2 (A1): the breadcrumb goes on every keyboard open and close in the sheet (a field typing with KB_MIN_PX or more
|
|
253
|
+
// covered), not only when a class flips: a keyboard that came while ta-kb was already on used to send nothing.
|
|
254
|
+
var typed = focus && v.inset >= KB_MIN_PX, opened = typed !== KB.typed;
|
|
255
|
+
KB.typed = typed;
|
|
256
|
+
if (opened || was !== KB.on + '|' + KB.strip + '|' + KB.tight) crumb('kb', v, focus);
|
|
250
257
|
return v;
|
|
251
258
|
}
|
|
252
259
|
function revealFocused() {
|
|
@@ -274,7 +281,7 @@
|
|
|
274
281
|
function unfit() {
|
|
275
282
|
listenViewport(false);
|
|
276
283
|
if (KB.frame && window.cancelAnimationFrame) window.cancelAnimationFrame(KB.frame);
|
|
277
|
-
KB.frame = 0; KB.on = false; KB.strip = false; KB.tight = false; KB.width = 0; KB.inset = 0;
|
|
284
|
+
KB.frame = 0; KB.on = false; KB.strip = false; KB.tight = false; KB.width = 0; KB.inset = 0; KB.typed = false;
|
|
278
285
|
if (!layer) return;
|
|
279
286
|
layer.classList.remove('ta-kb'); layer.classList.remove('ta-kb-strip'); layer.classList.remove('ta-kb-tight');
|
|
280
287
|
}
|
|
@@ -457,8 +464,12 @@
|
|
|
457
464
|
})();
|
|
458
465
|
});
|
|
459
466
|
}
|
|
460
|
-
|
|
467
|
+
// Back from an enlarged image (panes/reader-media.js pushes one entry over the sheet's) is the image's Back: the
|
|
468
|
+
// viewer closes and the sheet, its scroll and a half-typed note stay (11/08). The viewer says which pops are its own.
|
|
469
|
+
function imagePop(e) { return !!(W.readerMedia && typeof W.readerMedia.popIsMine === 'function' && W.readerMedia.popIsMine(e)); }
|
|
470
|
+
window.addEventListener('popstate', function (e) {
|
|
461
471
|
if (O.pops.length) { var w = O.pops.shift(); clearTimeout(w.timer); O.ignorePops = O.pops.length; if (w.then) w.then(); return; }
|
|
472
|
+
if (imagePop(e)) return;
|
|
462
473
|
// The phone's Back from a session opened off the pet's list goes back to that list, as Back to sessions does.
|
|
463
474
|
if (O.open && O.back) { backToList(); return; }
|
|
464
475
|
if (O.open) close('pop');
|
|
@@ -518,6 +529,7 @@
|
|
|
518
529
|
if (what === 'open-card') return leaveTo('work', id ? [id] : []);
|
|
519
530
|
if (what === 'open-session') return O.r ? leaveTo('sessions', [O.r.provider, O.r.id]) : null;
|
|
520
531
|
if (what === 'open-sessions') return leaveTo('sessions', []);
|
|
532
|
+
if (what === 'q-retry') { if (W.sessionQuestions && W.sessionQuestions.retry) W.sessionQuestions.retry(); return; }
|
|
521
533
|
// A row of the pet's list opens that session's own sheet in its place, with Back to sessions (and the phone's Back)
|
|
522
534
|
// returning to the list where it was scrolled; X still closes the overlay.
|
|
523
535
|
if (what === 'pet-open') {
|
|
@@ -525,6 +537,8 @@
|
|
|
525
537
|
return open(pk === 'wait' ? 'wait' : 'run', id, O.opener, { from: { scroll: lb ? lb.scrollTop || 0 : 0 } });
|
|
526
538
|
}
|
|
527
539
|
if (what === 'send') return sendReply();
|
|
540
|
+
if (what === 'turnend') return sendChoice(Number(id));
|
|
541
|
+
if (what === 'approve-phone' && K.approvePhone && O.r) return K.approvePhone(phoneApprovalOptions(O.r));
|
|
528
542
|
if (what === 'att-remove' || what === 'att-retry') {
|
|
529
543
|
var fb = filesBox(); if (!fb) return;
|
|
530
544
|
O.note = { text: '', kind: '' }; paintNote();
|
|
@@ -760,7 +774,7 @@
|
|
|
760
774
|
}, function (err) {
|
|
761
775
|
row.undoing = false;
|
|
762
776
|
if (err && err.status === 404) O.loop = false;
|
|
763
|
-
row.undoNote = err && err.status === 404 ? 'Undo
|
|
777
|
+
row.undoNote = err && err.status === 404 ? 'Undo did not go through. Change it in Work.' : (err && err.message) || 'Undo did not go through.';
|
|
764
778
|
if (row.state === 'dismissed') { row.until = Date.now() + UNDO_SECONDS * 1000; expireLater(); }
|
|
765
779
|
}).then(paintIntake);
|
|
766
780
|
}
|
|
@@ -817,7 +831,8 @@
|
|
|
817
831
|
paintFiles();
|
|
818
832
|
// The Mac's answer about files may not be in yet: ask once, then draw the clip.
|
|
819
833
|
var kit = KIT(), r0 = O.r;
|
|
820
|
-
|
|
834
|
+
paintPhoneOffer(r0);
|
|
835
|
+
if (kit && !kit.turnFiles()) kit.ensureCaps().then(function () { if (O.r === r0) { paintFiles(); paintPhoneOffer(r0); } }, function () {});
|
|
821
836
|
// Files of a send a reload cut off go again with the same words; say so, since their chips are gone.
|
|
822
837
|
if (O.r.draft && O.r.draft.restored) {
|
|
823
838
|
var nf = O.r.draft.ids.length;
|
|
@@ -899,6 +914,14 @@
|
|
|
899
914
|
'<p class="status-line" id="ta-status" role="status" aria-live="polite"></p>';
|
|
900
915
|
}
|
|
901
916
|
|
|
917
|
+
// The same exact packet picker and handoff as Sessions. This does not upgrade an ordinary Send.
|
|
918
|
+
function phoneApprovalOptions(r) {
|
|
919
|
+
return { r: r, prefix: 'ta', live: function () { return O.r === r && !!O.open; },
|
|
920
|
+
status: function (text, kind) { if (O.r === r) { O.note = { text: text, kind: kind || '' }; paintNote(); } },
|
|
921
|
+
files: function () { return filesState(filesBox(r)); } };
|
|
922
|
+
}
|
|
923
|
+
function paintPhoneOffer(r) { if (K.phoneOffer) K.phoneOffer(r, phoneApprovalOptions(r)); }
|
|
924
|
+
|
|
902
925
|
// ── files with a reply or a note (the COS chat's attach kit) ──
|
|
903
926
|
// One box per session, kept while this tab is open, so closing and reopening the sheet keeps what was attached.
|
|
904
927
|
var FILES = {};
|
|
@@ -942,6 +965,8 @@
|
|
|
942
965
|
el.classList.toggle('hidden', !box.atts.length);
|
|
943
966
|
}
|
|
944
967
|
paintClip(); paintSendButton();
|
|
968
|
+
// QA 7.0.0: the "Working right now" note says whether files go with the message.
|
|
969
|
+
if (O.r && O.r.verdict) paintVerdict();
|
|
945
970
|
}
|
|
946
971
|
// Send waits while a file uploads. A failed file never greys it out (QA 6.99.27): a tap says why, as the question
|
|
947
972
|
// card does.
|
|
@@ -1006,12 +1031,22 @@
|
|
|
1006
1031
|
// A session waiting on you: what it waits on, its question to answer here when the Mac holds one for this page;
|
|
1007
1032
|
// when the Mac's own prompt has it, that, read-only; else (a reply is the answer) the reply box below. No live
|
|
1008
1033
|
// peek and no Stop: it is not at work (W4-r2).
|
|
1009
|
-
html += '<p class="ta-sub">' + T.badge(O.r ? O.r.provider : 'claude') + ' <span>' + esc(O.wait ? O.wait.why : 'Waiting for you') + '</span></p>';
|
|
1010
1034
|
var where = waitWhere();
|
|
1035
|
+
html += '<p class="ta-sub">' + T.badge(O.r ? O.r.provider : 'claude') + ' <span>' + esc(waitWhy(where)) + '</span></p>';
|
|
1036
|
+
// W13 (QA week of 11/01-11/08): the words Today's row showed for it (its `ask`) stay here, read-only, when no card
|
|
1037
|
+
// draws them: the Mac line and a failed read alike. Never twice: not when the line above already says them.
|
|
1038
|
+
var ask = waitAsk();
|
|
1039
|
+
if (ask && (where === 'mac' || where === 'failed') && !O.macCount) html += '<blockquote class="ta-quote ta-mac-ask">' + esc(ask) + '</blockquote>';
|
|
1040
|
+
// W1 A5: no words anywhere (a question that went straight to the Mac): say that, rather than a card with nothing in it.
|
|
1041
|
+
if (!ask && where === 'mac' && !O.macCount && !askInWhy()) html += '<p class="ta-sub ta-mac-nowords">' + esc(macNoWords()) + '</p>';
|
|
1011
1042
|
// The Mac card says it itself when there is one; say it here only when there is none.
|
|
1012
1043
|
if (where === 'mac' && !O.macCount) html += '<p class="ta-sub ta-mac-line">' + esc(macLine()) + '</p>';
|
|
1044
|
+
// W13: the questions could not be read, so whether one waits here is unknown. Say so, never "at the Mac", and read again.
|
|
1045
|
+
if (where === 'failed') html += '<p class="ta-note ta-q-failed" role="status">' + esc('Couldn\u2019t load this session\u2019s questions, so one may be waiting here.') +
|
|
1046
|
+
' <button type="button" class="link-btn" data-ta="q-retry">Retry</button></p>';
|
|
1013
1047
|
if (where === 'answered') html += '<p class="ta-sub ta-mac-line">' + esc((W.sessionQuestions && W.sessionQuestions.answeredLine) || 'Answered. The session carries on.') + '</p>';
|
|
1014
1048
|
if (where === 'reply') html += '<p class="ta-sub">' + esc('Answer it in a reply below, or open the session.') + '</p>';
|
|
1049
|
+
if (where === 'turnend') html += turnEndHtml();
|
|
1015
1050
|
} else if (O.row || O.r) {
|
|
1016
1051
|
// Once the live feed speaks, the line says what it says, so the header and the peek never disagree (qa2-user N3).
|
|
1017
1052
|
html += '<p class="ta-sub">' + T.badge(O.r ? O.r.provider : O.row.provider) + ' <span id="ta-run-state">' + esc(runWords()) + '</span></p>';
|
|
@@ -1026,18 +1061,78 @@
|
|
|
1026
1061
|
box.innerHTML = html;
|
|
1027
1062
|
// The reply box shows only where a reply can be the answer; what was typed stays in it, hidden.
|
|
1028
1063
|
var foot = $('ta-foot');
|
|
1029
|
-
if (foot && O.open.kind === 'wait') foot.hidden =
|
|
1064
|
+
if (foot && O.open.kind === 'wait') { var w = waitWhere(); foot.hidden = w === 'failed' ? !!(O.r && O.r.provider === 'claude') : w !== 'reply' && w !== 'turnend'; }
|
|
1030
1065
|
}
|
|
1031
1066
|
// Where a waiting session's question is answered: 'here' (its cards in this sheet), 'answered' (on some screen just
|
|
1032
1067
|
// now), 'mac' (a Claude session's own prompt, or a held question that went back to the Mac), or 'reply' (a Codex or
|
|
1033
1068
|
// Cursor session asking in words).
|
|
1069
|
+
// W13: 'failed' when the last questions read failed and no card is drawn: a Claude reply still answers nothing, so the
|
|
1070
|
+
// reply box stays a Codex or Cursor one.
|
|
1034
1071
|
function waitWhere() {
|
|
1035
1072
|
if (O.qCount) return 'here';
|
|
1073
|
+
// 7.0.18: a turn that ended asking him is answered by a reply (a choice or his own words), so the reply box shows.
|
|
1074
|
+
if (O.wait && O.wait.where === 'turnend') return 'turnend';
|
|
1036
1075
|
if (O.r && W.sessionQuestions && W.sessionQuestions.answeredFor && W.sessionQuestions.answeredFor(O.r.id)) return 'answered';
|
|
1076
|
+
if (!O.macCount && W.sessionQuestions && W.sessionQuestions.loadState && W.sessionQuestions.loadState() === 'failed') return 'failed';
|
|
1037
1077
|
if (O.macCount || (O.r && O.r.provider === 'claude') || (O.wait && O.wait.where === 'mac')) return 'mac';
|
|
1038
1078
|
return 'reply';
|
|
1039
1079
|
}
|
|
1040
|
-
|
|
1080
|
+
// The question in Today's own words (its Needs you row), unless the line above the sheet already reads exactly that.
|
|
1081
|
+
function waitAsk() {
|
|
1082
|
+
var a = O.wait && O.wait.ask ? String(O.wait.ask).trim() : '';
|
|
1083
|
+
return a && !(O.wait.why && String(O.wait.why).trim() === a) ? a : '';
|
|
1084
|
+
}
|
|
1085
|
+
// 7.0.18: a turn that ended asking him (X1 contract v1). The question as the row has it, a button per choice, and the
|
|
1086
|
+
// required honest line. A tap sends the contract's exact text through the reader's Continue (K.send); his own words
|
|
1087
|
+
// in the box below are sent the same way.
|
|
1088
|
+
var TURN_END_LINE = 'A choice or a reply here reaches the session as a message from COS, not typed in the session, and the session may ask you to confirm.';
|
|
1089
|
+
var TURN_END_PREFIX = 'Answer sent from COS Control Web through COS, not typed in this session: ';
|
|
1090
|
+
function turnEndAnswer(text) { var t = String(text || '').trim(); return TURN_END_PREFIX + (t.indexOf(TURN_END_PREFIX) === 0 ? t.slice(TURN_END_PREFIX.length).trim() : t); }
|
|
1091
|
+
function turnEndHtml() {
|
|
1092
|
+
var w = O.wait || {}, choices = Array.isArray(w.choices) ? w.choices : [], busy = !!(O.r && O.r.sending);
|
|
1093
|
+
return (w.ask ? '<blockquote class="ta-quote ta-te-ask">' + esc(w.ask) + '</blockquote>' : '') +
|
|
1094
|
+
(choices.length ? '<div class="ta-te-choices">' + choices.map(function (c, i) {
|
|
1095
|
+
return '<button type="button" class="cx-btn' + (i === 0 ? ' primary' : '') + '" data-ta="turnend" data-id="' + i + '"' + (busy ? ' disabled' : '') + '>' + esc(c) + '</button>';
|
|
1096
|
+
}).join('') + '</div>' : '') +
|
|
1097
|
+
'<p class="ta-sub ta-te-line">' + esc(TURN_END_LINE) + '</p>';
|
|
1098
|
+
}
|
|
1099
|
+
/** Still waiting on this answer? The latest session list decides; a reply from anywhere clears it on the server. */
|
|
1100
|
+
function turnEndStill() {
|
|
1101
|
+
var nm = T.needs ? T.needs() : null, key = O.wait && O.wait.key;
|
|
1102
|
+
return !!(nm && key && nm.rows.some(function (x) { return x.kind === 'session' && x.key === key && x.where === 'turnend'; }));
|
|
1103
|
+
}
|
|
1104
|
+
function sendChoice(i) {
|
|
1105
|
+
var r = O.r, w = O.wait; if (!r || !w || r.sending) return;
|
|
1106
|
+
var label = (Array.isArray(w.choices) ? w.choices : [])[i];
|
|
1107
|
+
if (!label) return;
|
|
1108
|
+
if (!turnEndStill()) { O.note = { text: 'Already answered on another screen, or the session moved on. Nothing was sent.', kind: '' }; paintNote(); return; }
|
|
1109
|
+
r.sending = true; setSending(true); paintMeta();
|
|
1110
|
+
var done = function () { r.sending = false; if (O.r === r) { setSending(false); paintMeta(); } };
|
|
1111
|
+
return K.send(r, turnEndAnswer(label), {}).then(function (how) {
|
|
1112
|
+
done();
|
|
1113
|
+
if (O.r !== r) return;
|
|
1114
|
+
if (how === 'sent') O.note = { text: 'Sent \u201c' + label + '\u201d. It reaches the session as a message from COS; it may ask you to confirm.', kind: '' };
|
|
1115
|
+
else if (how === 'changed') { r.refusal = null; r.sendAnyway = null; paintVerdict(); O.note = { text: 'The session moved on before this reached it. \u201c' + label + '\u201d was not sent.', kind: 'error' }; }
|
|
1116
|
+
else if (how === 'refused') O.note = { text: 'The session did not take \u201c' + label + '\u201d. Try again, or answer it on your Mac.', kind: 'error' };
|
|
1117
|
+
paintNote();
|
|
1118
|
+
}, function (err) {
|
|
1119
|
+
done();
|
|
1120
|
+
if (O.r !== r) return;
|
|
1121
|
+
O.note = { text: 'Not sent: ' + (err && err.name === 'AbortError' ? 'no answer from your Mac yet. It may still land; tapping again is safe, it will not post twice.' : (err && err.message) || 'could not reach your Mac.'), kind: 'error' };
|
|
1122
|
+
paintNote();
|
|
1123
|
+
});
|
|
1124
|
+
}
|
|
1125
|
+
function macLine() { return (W.sessionQuestions && W.sessionQuestions.macLine) || "Answer this at your Mac's prompt. A message sent from here does not answer it; it waits until the question is answered at the Mac."; }
|
|
1126
|
+
function macNoWords() { return (W.sessionQuestions && W.sessionQuestions.macNoWords) || "COS cannot read this question's words from here. They show at the Mac's prompt."; }
|
|
1127
|
+
// W1 A5: the head line says the state once. At the Mac's prompt the line below says where; the head does not repeat it.
|
|
1128
|
+
var GENERIC_WAIT = /^Waiting for (you|your answer)(\. Paused until you answer\.)?$/;
|
|
1129
|
+
function waitWhy(where) {
|
|
1130
|
+
var why = O.wait ? String(O.wait.why || '') : '';
|
|
1131
|
+
if (!why) return 'Waiting for you';
|
|
1132
|
+
return where === 'mac' && GENERIC_WAIT.test(why) ? 'Waiting for your answer' : why;
|
|
1133
|
+
}
|
|
1134
|
+
// The head line already carries the question's words (the server named them in the waiting line).
|
|
1135
|
+
function askInWhy() { return !!(O.wait && O.wait.why && !GENERIC_WAIT.test(String(O.wait.why))); }
|
|
1041
1136
|
|
|
1042
1137
|
// ═════════════ The pet's sessions ═════════════
|
|
1043
1138
|
// The same sessions the pet's caption counts (app.js petPlay): waiting on you, running, then done today. Each row
|
|
@@ -1098,7 +1193,7 @@
|
|
|
1098
1193
|
function loadLast(r) {
|
|
1099
1194
|
var request = r.lastRead = {};
|
|
1100
1195
|
O.lastState = 'loading'; O.lastWhy = ''; paintLast();
|
|
1101
|
-
return K.answer(K.path(r.provider, r.id, '?turns=6'), { timeoutMs: 20000 }).then(function (a) {
|
|
1196
|
+
return K.answer(K.path(r.provider, r.id, '?turns=6&cards=1'), { timeoutMs: 20000 }).then(function (a) {
|
|
1102
1197
|
if (O.r !== r || r.lastRead !== request) return;
|
|
1103
1198
|
if (a.status === 404) { O.lastState = 'none'; O.lastWhy = 'This session is not on your Mac any more.'; paintLast(); return; }
|
|
1104
1199
|
if (a.status !== 200) { O.lastState = 'error'; paintLast(); return; }
|
|
@@ -1106,7 +1201,8 @@
|
|
|
1106
1201
|
if (!O.row) { O.row = d; paintMeta(); if (O.open && O.open.kind === 'run') setHead(K.title(d)); }
|
|
1107
1202
|
var turns = Array.isArray(d.recent_turns) ? d.recent_turns : [];
|
|
1108
1203
|
var lastA = turns.filter(function (t) { return lower(t.role) !== 'user' && (String(t.text || t.content || '').trim() || (K.imageHtml && K.imageHtml(t))); }).pop();
|
|
1109
|
-
O.last = lastA ? { text: String(lastA.text || lastA.content || '').trim(), at: lastA.at,
|
|
1204
|
+
O.last = lastA ? { text: String(lastA.text || lastA.content || '').trim(), at: lastA.at, toolImages: lastA.toolImages, toolImagesMore: lastA.toolImagesMore, outputLinks: lastA.outputLinks,
|
|
1205
|
+
outputImages: lastA.outputImages, outputFiles: lastA.outputFiles, previewsOff: lastA.previewsOff } : null;
|
|
1110
1206
|
O.lastState = O.last ? 'ready' : 'none';
|
|
1111
1207
|
O.lastWhy = '';
|
|
1112
1208
|
paintLast();
|
|
@@ -1116,7 +1212,7 @@
|
|
|
1116
1212
|
var box = $('ta-last'); if (!box) return;
|
|
1117
1213
|
// Retain the actual reply nodes (and its open image) during refreshes and transient errors.
|
|
1118
1214
|
// Only a successful changed reply replaces them. Metadata changes count as changed too.
|
|
1119
|
-
var signature = O.last ? JSON.stringify([O.last.text, O.last.at, K.imageHtml ? K.imageHtml(O.last) : '']) : '';
|
|
1215
|
+
var signature = O.last ? JSON.stringify([O.last.text, O.last.at, K.imageHtml ? K.imageHtml(O.last) : '', K.linksAttr ? K.linksAttr(O.last) : '']) : '';
|
|
1120
1216
|
if (signature && box.__taLastSignature === signature) {
|
|
1121
1217
|
var refresh = $('ta-last-refresh');
|
|
1122
1218
|
if (refresh) refresh.textContent = O.lastState === 'loading' ? 'Checking for a newer reply…'
|
|
@@ -1132,8 +1228,10 @@
|
|
|
1132
1228
|
if (!O.last) { box.innerHTML = ''; return; }
|
|
1133
1229
|
var at = W.toDate(O.last.at), long = O.last.text.length > 700;
|
|
1134
1230
|
box.innerHTML = '<section class="ta-last" aria-label="Last reply"><p class="ta-last-head">' + esc('Last reply' + (at ? ', ' + W.clock(at) : '')) + '</p>' +
|
|
1135
|
-
|
|
1136
|
-
|
|
1231
|
+
(K.imageHtml ? K.imageHtml(O.last, { part: 'before' }) : '') +
|
|
1232
|
+
// Preview engine phase 1: the reply's link cards, as in the reader.
|
|
1233
|
+
'<div class="sx-md ta-last-text' + (long ? ' clamped' : '') + '" id="ta-last-text"' + (K.linksAttr ? K.linksAttr(O.last) : '') + '>' + K.markdown(O.last.text.slice(0, 12000), O.last.outputLinks) + '</div>' +
|
|
1234
|
+
(long ? '<button type="button" class="link-btn" data-ta="more">Show all of it</button>' : '') + (K.imageHtml ? K.imageHtml(O.last, { part: 'after' }) : '') + '<p id="ta-last-refresh" class="ta-sub" role="status"></p></section>';
|
|
1137
1235
|
if (K.imagesMount) K.imagesMount(box, O.r);
|
|
1138
1236
|
}
|
|
1139
1237
|
|
|
@@ -1234,7 +1332,8 @@
|
|
|
1234
1332
|
var fb = sent.length ? filesBox(r) : null, kit = KIT();
|
|
1235
1333
|
if (fb && kit) fb.atts.filter(function (t) { return sent.indexOf(t.mediaId) >= 0; }).forEach(function (t) { kit.remove(fb, t.id); });
|
|
1236
1334
|
var box = r === O.r ? $('ta-input') : null;
|
|
1237
|
-
|
|
1335
|
+
// 7.0.18: his answer to a turn-end ask went with the attribution in front; the box holds his words without it.
|
|
1336
|
+
var left = box ? (box.value.trim() === String(text).trim() || (box.value.trim() && turnEndAnswer(box.value) === String(text).trim()) ? '' : box.value) : '';
|
|
1238
1337
|
if (box && !left) { box.value = ''; W.autoGrow(box); }
|
|
1239
1338
|
keepDraft(r, left, null);
|
|
1240
1339
|
},
|
|
@@ -1275,7 +1374,8 @@
|
|
|
1275
1374
|
'</div></div>';
|
|
1276
1375
|
} else if (v && v.attachable === false && /^(native_thread_working|native_target_busy)$/.test(v.reason)) {
|
|
1277
1376
|
// 6.99.29: "Working right now" only while the card's one state says so.
|
|
1278
|
-
|
|
1377
|
+
// QA 7.0.0: files in this sheet's box wait for the turn end, so the note says so (app.js sxParkLine).
|
|
1378
|
+
if (K.feed.parkNoteShows(r)) html = '<p class="sx-park">' + esc(K.parkCopy(r.provider, r, !!(filesOn() && filesState(filesBox(r)) !== 'none'))) + '</p>';
|
|
1279
1379
|
} else if (v && v.attachable === false && v.reasonCopy) {
|
|
1280
1380
|
html = '<div class="sx-box refusal" role="status"><p>' + esc(v.reasonCopy) + '</p></div>';
|
|
1281
1381
|
} else if (v && v.caution) {
|
|
@@ -1309,6 +1409,8 @@
|
|
|
1309
1409
|
if (r.refusal || O.cautionAsk) { r.refusal = null; O.cautionAsk = false; paintVerdict(); }
|
|
1310
1410
|
r.sending = true; setSending(true);
|
|
1311
1411
|
var done = function () { r.sending = false; if (O.r === r) setSending(false); };
|
|
1412
|
+
// 7.0.18: his own words in answer to a turn-end ask carry the same attribution as a choice ('Other' in the contract).
|
|
1413
|
+
if (O.open && O.open.kind === 'wait' && waitWhere() === 'turnend' && turnEndStill()) text = turnEndAnswer(text);
|
|
1312
1414
|
var sending = K.send(r, text, opts || {});
|
|
1313
1415
|
// continueSession picks the key at once (draftFor): keep it until the Mac takes the words.
|
|
1314
1416
|
if (r.draft) keepDraft(r, text, r.draft);
|
|
@@ -313,3 +313,16 @@
|
|
|
313
313
|
.wk-pick-row { display: flex; align-items: center; gap: 10px; padding: 8px 2px; border-top: 1px solid var(--hairline); }
|
|
314
314
|
.wk-pick-row > div { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
|
|
315
315
|
.wk-pick-row strong { font-size: 13.5px; }
|
|
316
|
+
|
|
317
|
+
/* Duo tables: the divider between a card and its side panel (app.js splitKit, styled in app.css; work.js DIVIDER). From
|
|
318
|
+
900 px only, where both show. Its 22 px column is the old gap, so the default layout is unchanged; a dragged width
|
|
319
|
+
sizes the side panel, clamped so it keeps 320 px and the card 380 px. */
|
|
320
|
+
@media (min-width: 900px) {
|
|
321
|
+
.wk-detail.split-side { grid-template-columns: minmax(0, 1fr) 22px minmax(320px, 400px); column-gap: 0; }
|
|
322
|
+
.wk-detail.split-side.split-sized { grid-template-columns: minmax(0, 1fr) 22px clamp(320px, var(--split-side-share, 30%), calc(100% - 402px)); }
|
|
323
|
+
.wk-detail.split-side > .split-handle { display: flex; }
|
|
324
|
+
}
|
|
325
|
+
@media (min-width: 1300px) { .wk-detail.split-side { grid-template-columns: minmax(0, 1fr) 22px 452px; } }
|
|
326
|
+
|
|
327
|
+
/* W1 (0.5.386 parity): the empty card's one-tap New session button wraps on a folded phone instead of running off the card. */
|
|
328
|
+
.wk-start-new { white-space: normal; max-width: 100%; height: auto; min-height: 40px; text-align: center; line-height: 1.3; padding-top: 8px; padding-bottom: 8px; }
|