@gotcos/glasses-server 6.99.33 → 7.0.7
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 +18 -0
- package/CHANGELOG.md +417 -0
- package/README.md +3 -1
- package/managed-runtime-contract.json +3 -0
- package/package.json +1 -1
- package/server/index.ts +29 -7
- package/server/lib/agent-session-output-images.ts +627 -49
- package/server/lib/agent-session-store.ts +24 -11
- 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 +6 -0
- package/server/lib/ask-work-plan.ts +3 -1
- package/server/lib/calendar-source.ts +491 -0
- 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/drop-p2-runtime.ts +1 -1
- package/server/lib/drop-store.ts +10 -4
- package/server/lib/learning-needs-you.ts +24 -7
- 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/morning-brief-runtime.ts +4 -2
- package/server/lib/native-mac-caller.ts +29 -0
- 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 +80 -0
- package/server/lib/session-files-setting.ts +88 -0
- package/server/lib/session-links.ts +280 -0
- package/server/lib/thread-turn-queue.ts +10 -0
- package/server/lib/today-brief.ts +119 -28
- package/server/lib/web-memories-bridge.ts +3 -1
- package/server/lib/work-activity.ts +56 -6
- 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 +13 -0
- package/server/routes/agent-session-stream.ts +16 -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/health.ts +35 -0
- package/server/routes/live-events.ts +329 -0
- package/server/routes/permission-broker.ts +69 -5
- package/server/routes/session-preview.ts +200 -0
- package/server/routes/thread-turn-queue.ts +207 -3
- package/server/routes/today-brief.ts +44 -9
- package/server/routes/welcome-context.ts +23 -3
- package/server/routes/work-board.ts +9 -2
- package/server/web-control/app.css +40 -1
- package/server/web-control/app.js +1309 -56
- package/server/web-control/control.css +25 -0
- package/server/web-control/index.html +7 -2
- package/server/web-control/memory-review.js +100 -0
- package/server/web-control/panes/cos-chat.css +42 -3
- package/server/web-control/panes/cos-chat.js +267 -15
- 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 +28 -8
- 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 +96 -5
- package/server/web-control/panes/today-actions.css +11 -0
- package/server/web-control/panes/today-actions.js +45 -12
- package/server/web-control/panes/work.css +10 -0
- package/server/web-control/panes/work.js +10 -2
- package/server/web-control/shell.js +17 -14
|
@@ -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;
|
|
@@ -185,3 +185,14 @@ html.ta-open #sq-host { display: none; }
|
|
|
185
185
|
.ta-notsent:empty { display: none; }
|
|
186
186
|
.ta-notsent { margin: 4px 0 8px; }
|
|
187
187
|
.ta-composer.dragging { outline: 2px dashed var(--accent); outline-offset: 4px; }
|
|
188
|
+
/* 7.0.1 live state: Refresh and the line beside it under Today's date (app.js todayRefresh, liveNoteText). The button
|
|
189
|
+
keeps the 44 px target of every link button; the line says Updating, Reconnecting, "As of 2 min ago" or the server's
|
|
190
|
+
summary limit, in the date's own size and colour. */
|
|
191
|
+
.today-live { display: flex; align-items: center; flex-wrap: wrap; gap: 0 12px; margin: 2px 0 0; min-height: 44px; }
|
|
192
|
+
.today-refresh { min-height: 44px; min-width: 44px; padding: 0 2px; font-size: 15px; }
|
|
193
|
+
.today-refresh:disabled { opacity: 0.55; cursor: default; }
|
|
194
|
+
.today-live-note { color: var(--sub); font-size: 14px; line-height: 1.3; }
|
|
195
|
+
.today-live-note:empty { display: none; }
|
|
196
|
+
.today-live-note.warn { color: var(--warn, var(--sub)); }
|
|
197
|
+
/* A working row's clock ticks every second: tabular figures so the line does not shimmer. */
|
|
198
|
+
.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') {
|
|
@@ -942,6 +954,8 @@
|
|
|
942
954
|
el.classList.toggle('hidden', !box.atts.length);
|
|
943
955
|
}
|
|
944
956
|
paintClip(); paintSendButton();
|
|
957
|
+
// QA 7.0.0: the "Working right now" note says whether files go with the message.
|
|
958
|
+
if (O.r && O.r.verdict) paintVerdict();
|
|
945
959
|
}
|
|
946
960
|
// 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
961
|
// card does.
|
|
@@ -1008,8 +1022,15 @@
|
|
|
1008
1022
|
// peek and no Stop: it is not at work (W4-r2).
|
|
1009
1023
|
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
1024
|
var where = waitWhere();
|
|
1025
|
+
// 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
|
|
1026
|
+
// draws them: the Mac line and a failed read alike. Never twice: not when the line above already says them.
|
|
1027
|
+
var ask = waitAsk();
|
|
1028
|
+
if (ask && (where === 'mac' || where === 'failed') && !O.macCount) html += '<blockquote class="ta-quote ta-mac-ask">' + esc(ask) + '</blockquote>';
|
|
1011
1029
|
// The Mac card says it itself when there is one; say it here only when there is none.
|
|
1012
1030
|
if (where === 'mac' && !O.macCount) html += '<p class="ta-sub ta-mac-line">' + esc(macLine()) + '</p>';
|
|
1031
|
+
// W13: the questions could not be read, so whether one waits here is unknown. Say so, never "at the Mac", and read again.
|
|
1032
|
+
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.') +
|
|
1033
|
+
' <button type="button" class="link-btn" data-ta="q-retry">Retry</button></p>';
|
|
1013
1034
|
if (where === 'answered') html += '<p class="ta-sub ta-mac-line">' + esc((W.sessionQuestions && W.sessionQuestions.answeredLine) || 'Answered. The session carries on.') + '</p>';
|
|
1014
1035
|
if (where === 'reply') html += '<p class="ta-sub">' + esc('Answer it in a reply below, or open the session.') + '</p>';
|
|
1015
1036
|
} else if (O.row || O.r) {
|
|
@@ -1026,17 +1047,25 @@
|
|
|
1026
1047
|
box.innerHTML = html;
|
|
1027
1048
|
// The reply box shows only where a reply can be the answer; what was typed stays in it, hidden.
|
|
1028
1049
|
var foot = $('ta-foot');
|
|
1029
|
-
if (foot && O.open.kind === 'wait') foot.hidden =
|
|
1050
|
+
if (foot && O.open.kind === 'wait') { var w = waitWhere(); foot.hidden = w === 'failed' ? !!(O.r && O.r.provider === 'claude') : w !== 'reply'; }
|
|
1030
1051
|
}
|
|
1031
1052
|
// Where a waiting session's question is answered: 'here' (its cards in this sheet), 'answered' (on some screen just
|
|
1032
1053
|
// now), 'mac' (a Claude session's own prompt, or a held question that went back to the Mac), or 'reply' (a Codex or
|
|
1033
1054
|
// Cursor session asking in words).
|
|
1055
|
+
// W13: 'failed' when the last questions read failed and no card is drawn: a Claude reply still answers nothing, so the
|
|
1056
|
+
// reply box stays a Codex or Cursor one.
|
|
1034
1057
|
function waitWhere() {
|
|
1035
1058
|
if (O.qCount) return 'here';
|
|
1036
1059
|
if (O.r && W.sessionQuestions && W.sessionQuestions.answeredFor && W.sessionQuestions.answeredFor(O.r.id)) return 'answered';
|
|
1060
|
+
if (!O.macCount && W.sessionQuestions && W.sessionQuestions.loadState && W.sessionQuestions.loadState() === 'failed') return 'failed';
|
|
1037
1061
|
if (O.macCount || (O.r && O.r.provider === 'claude') || (O.wait && O.wait.where === 'mac')) return 'mac';
|
|
1038
1062
|
return 'reply';
|
|
1039
1063
|
}
|
|
1064
|
+
// The question in Today's own words (its Needs you row), unless the line above the sheet already reads exactly that.
|
|
1065
|
+
function waitAsk() {
|
|
1066
|
+
var a = O.wait && O.wait.ask ? String(O.wait.ask).trim() : '';
|
|
1067
|
+
return a && !(O.wait.why && String(O.wait.why).trim() === a) ? a : '';
|
|
1068
|
+
}
|
|
1040
1069
|
function macLine() { return (W.sessionQuestions && W.sessionQuestions.macLine) || "Waiting on your Mac's prompt. Answer it on the Mac (or through Screens)."; }
|
|
1041
1070
|
|
|
1042
1071
|
// ═════════════ The pet's sessions ═════════════
|
|
@@ -1098,7 +1127,7 @@
|
|
|
1098
1127
|
function loadLast(r) {
|
|
1099
1128
|
var request = r.lastRead = {};
|
|
1100
1129
|
O.lastState = 'loading'; O.lastWhy = ''; paintLast();
|
|
1101
|
-
return K.answer(K.path(r.provider, r.id, '?turns=6'), { timeoutMs: 20000 }).then(function (a) {
|
|
1130
|
+
return K.answer(K.path(r.provider, r.id, '?turns=6&cards=1'), { timeoutMs: 20000 }).then(function (a) {
|
|
1102
1131
|
if (O.r !== r || r.lastRead !== request) return;
|
|
1103
1132
|
if (a.status === 404) { O.lastState = 'none'; O.lastWhy = 'This session is not on your Mac any more.'; paintLast(); return; }
|
|
1104
1133
|
if (a.status !== 200) { O.lastState = 'error'; paintLast(); return; }
|
|
@@ -1106,7 +1135,8 @@
|
|
|
1106
1135
|
if (!O.row) { O.row = d; paintMeta(); if (O.open && O.open.kind === 'run') setHead(K.title(d)); }
|
|
1107
1136
|
var turns = Array.isArray(d.recent_turns) ? d.recent_turns : [];
|
|
1108
1137
|
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,
|
|
1138
|
+
O.last = lastA ? { text: String(lastA.text || lastA.content || '').trim(), at: lastA.at, toolImages: lastA.toolImages, toolImagesMore: lastA.toolImagesMore, outputLinks: lastA.outputLinks,
|
|
1139
|
+
outputImages: lastA.outputImages, outputFiles: lastA.outputFiles, previewsOff: lastA.previewsOff } : null;
|
|
1110
1140
|
O.lastState = O.last ? 'ready' : 'none';
|
|
1111
1141
|
O.lastWhy = '';
|
|
1112
1142
|
paintLast();
|
|
@@ -1116,7 +1146,7 @@
|
|
|
1116
1146
|
var box = $('ta-last'); if (!box) return;
|
|
1117
1147
|
// Retain the actual reply nodes (and its open image) during refreshes and transient errors.
|
|
1118
1148
|
// 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) : '']) : '';
|
|
1149
|
+
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
1150
|
if (signature && box.__taLastSignature === signature) {
|
|
1121
1151
|
var refresh = $('ta-last-refresh');
|
|
1122
1152
|
if (refresh) refresh.textContent = O.lastState === 'loading' ? 'Checking for a newer reply…'
|
|
@@ -1132,8 +1162,10 @@
|
|
|
1132
1162
|
if (!O.last) { box.innerHTML = ''; return; }
|
|
1133
1163
|
var at = W.toDate(O.last.at), long = O.last.text.length > 700;
|
|
1134
1164
|
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
|
-
|
|
1165
|
+
(K.imageHtml ? K.imageHtml(O.last, { part: 'before' }) : '') +
|
|
1166
|
+
// Preview engine phase 1: the reply's link cards, as in the reader.
|
|
1167
|
+
'<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>' +
|
|
1168
|
+
(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
1169
|
if (K.imagesMount) K.imagesMount(box, O.r);
|
|
1138
1170
|
}
|
|
1139
1171
|
|
|
@@ -1275,7 +1307,8 @@
|
|
|
1275
1307
|
'</div></div>';
|
|
1276
1308
|
} else if (v && v.attachable === false && /^(native_thread_working|native_target_busy)$/.test(v.reason)) {
|
|
1277
1309
|
// 6.99.29: "Working right now" only while the card's one state says so.
|
|
1278
|
-
|
|
1310
|
+
// QA 7.0.0: files in this sheet's box wait for the turn end, so the note says so (app.js sxParkLine).
|
|
1311
|
+
if (K.feed.parkNoteShows(r)) html = '<p class="sx-park">' + esc(K.parkCopy(r.provider, r, !!(filesOn() && filesState(filesBox(r)) !== 'none'))) + '</p>';
|
|
1279
1312
|
} else if (v && v.attachable === false && v.reasonCopy) {
|
|
1280
1313
|
html = '<div class="sx-box refusal" role="status"><p>' + esc(v.reasonCopy) + '</p></div>';
|
|
1281
1314
|
} else if (v && v.caution) {
|
|
@@ -313,3 +313,13 @@
|
|
|
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; } }
|
|
@@ -1126,6 +1126,13 @@
|
|
|
1126
1126
|
(str(t.source) ? fact('Source', t.source) : '') + (str(t.runAt) ? fact('Scheduled', t.runAt) : '') +
|
|
1127
1127
|
'<p class="wk-fine">Task editing, scheduling, and completion stay attached to the original task. Agent output does not change its completion state.</p></section>';
|
|
1128
1128
|
}
|
|
1129
|
+
// Duo tables: the divider between a card and its side panel (app.js splitKit, 900 px and up). The side panel is the
|
|
1130
|
+
// column on the right, so it grows as the line moves left; it keeps 320 px and the card 380 px, past a 22 px divider
|
|
1131
|
+
// that is the old gap. A repaint in place keeps the same divider. (DIVIDER, since SPLIT is the Sort split.)
|
|
1132
|
+
// A page without the kit (a test's stand-in W) draws no divider and keeps working.
|
|
1133
|
+
var SPLIT_KIT = W.splitKit || { handle: function () { return ''; }, mount: function () { return null; } };
|
|
1134
|
+
var DIVIDER = { key: 'cos_web_work_split', label: 'Side panel', side: 'end', sized: '.wk-detail-side', cssVar: '--split-side-share', media: '(min-width: 900px)', listMin: 320, readerMin: 380, handleW: 22 };
|
|
1135
|
+
var SPLIT_HANDLE = SPLIT_KIT.handle({ id: 'wk-handle', controls: 'wk-part-side', label: DIVIDER.label });
|
|
1129
1136
|
function renderDetail(arg) {
|
|
1130
1137
|
var it = itemFor(arg);
|
|
1131
1138
|
S.viewWaiting = !it && (S.loading || !S.tasks);
|
|
@@ -1151,11 +1158,12 @@
|
|
|
1151
1158
|
} else {
|
|
1152
1159
|
S.chatDeferred = false;
|
|
1153
1160
|
pane.innerHTML = '<div class="cx wk" data-tint="work">' + backBar(backLabel()) + '<div id="wk-part-notices" class="wk-part">' + parts.notices + '</div>' +
|
|
1154
|
-
'<div class="wk-detail" data-wid="' + esc(it.wid) + '"><div class="wk-detail-main"><div id="wk-part-main" class="wk-part">' + parts.main + '</div>' + chatSectionHtml(it) + '</div>' +
|
|
1155
|
-
'<aside class="wk-detail-side" id="wk-part-side">' + parts.side + '</aside></div></div>';
|
|
1161
|
+
'<div class="wk-detail' + (SPLIT_HANDLE ? ' split-side' : '') + '" data-wid="' + esc(it.wid) + '"><div class="wk-detail-main"><div id="wk-part-main" class="wk-part">' + parts.main + '</div>' + chatSectionHtml(it) + '</div>' +
|
|
1162
|
+
SPLIT_HANDLE + '<aside class="wk-detail-side" id="wk-part-side">' + parts.side + '</aside></div></div>';
|
|
1156
1163
|
[['wk-part-notices', parts.notices], ['wk-part-main', parts.main], ['wk-part-side', parts.side]].forEach(function (p) { var el = $(p[0]); if (el) el.__html = p[1]; });
|
|
1157
1164
|
var hist = $('wk-part-chat'); if (hist) hist.__html = hist.innerHTML;
|
|
1158
1165
|
}
|
|
1166
|
+
SPLIT_KIT.mount(pane.querySelector('.wk-detail'), $('wk-handle'), DIVIDER);
|
|
1159
1167
|
chatSyncComposer(it);
|
|
1160
1168
|
if (!S.activity[it.wid] && !S.activityError[it.wid] && !S.activityLoading[it.wid]) {
|
|
1161
1169
|
S.activityLoading[it.wid] = true;
|
|
@@ -77,10 +77,13 @@
|
|
|
77
77
|
}
|
|
78
78
|
// About 16 reads: never two runs at once, never more than one a minute, two at a time through storyApi, and never
|
|
79
79
|
// ahead of the pane the person just opened (Safari keeps 6 connections to the Mac over HTTP/1.1).
|
|
80
|
-
//
|
|
81
|
-
// when the menu opens rather than costing the pane its connections on every visit.
|
|
80
|
+
// Below 600 px wide (the folded Duo) the rail is hidden and the stories show only inside the section menu, so they
|
|
81
|
+
// load when the menu opens rather than costing the pane its connections on every visit. This is the WIDTH test on
|
|
82
|
+
// purpose, not W.isPhone: from 7.0.2 a short touch screen at any width counts as a phone (the unfolded Duo, 951x539),
|
|
83
|
+
// and it still has the rail, its story lines and hot dots.
|
|
84
|
+
function railHidden() { return !window.matchMedia('(min-width: 600px)').matches; }
|
|
82
85
|
function loadStories(force) {
|
|
83
|
-
if (!force &&
|
|
86
|
+
if (!force && railHidden()) return;
|
|
84
87
|
if (Date.now() - storiesAt < 60000) return;
|
|
85
88
|
storiesAt = Date.now();
|
|
86
89
|
var now = new Date();
|
|
@@ -210,17 +213,17 @@
|
|
|
210
213
|
setStory('threads', active + ' active' + (list[0] && list[0].name ? ', newest ' + clip(list[0].name, 40) : ''), false);
|
|
211
214
|
});
|
|
212
215
|
|
|
213
|
-
// Memories (TodayHome.memoriesStory): n is the
|
|
214
|
-
//
|
|
215
|
-
//
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
var n =
|
|
216
|
+
// Memories (TodayHome.memoriesStory): n is the needs-review count (capped, from the bridge's
|
|
217
|
+
// status, which runs the same CLI Control's helper runs, judged by memory-review.js); total is
|
|
218
|
+
// every item that needs review by the same predicate, never the To review list's length (that
|
|
219
|
+
// list also holds suggestions and proposals with no decision waiting). "Next n of total" when
|
|
220
|
+
// more need review than are shown, else "n to review".
|
|
221
|
+
soft(storyApi('/api/web-bridge/memories', { method: 'POST', body: { id: 'rail-status', op: 'status', args: {} } })).then(function (r) {
|
|
222
|
+
var d = r && r.details ? r.details : {};
|
|
223
|
+
var n = d.learningToReview;
|
|
221
224
|
if (typeof n !== 'number') return setStory('memories', FALLBACK.memories, false);
|
|
222
225
|
if (n <= 0) return setStory('memories', 'Nothing to review', false);
|
|
223
|
-
var total =
|
|
226
|
+
var total = typeof d.learningToReviewTotal === 'number' ? d.learningToReviewTotal : null;
|
|
224
227
|
setStory('memories', total != null && total > n ? 'Next ' + n + ' of ' + total : n + ' to review', true);
|
|
225
228
|
});
|
|
226
229
|
// Dictation lives on the Mac (its history and suggestions are local files).
|
|
@@ -260,9 +263,9 @@
|
|
|
260
263
|
}
|
|
261
264
|
crumbs.innerHTML = parts.join('');
|
|
262
265
|
$('nav-back').disabled = onToday;
|
|
263
|
-
// The phone top bar names the detail too, so a reader says where it is.
|
|
266
|
+
// The phone top bar (below 600 px, the same width test) names the detail too, so a reader says where it is.
|
|
264
267
|
var label = $('section-name');
|
|
265
|
-
if (label && detail &&
|
|
268
|
+
if (label && detail && railHidden()) label.textContent = detail;
|
|
266
269
|
}
|
|
267
270
|
|
|
268
271
|
document.addEventListener('click', function (e) {
|