@gotcos/glasses-server 6.99.25 → 6.99.29
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +166 -0
- package/package.json +1 -1
- package/server/index.ts +46 -4
- package/server/lib/agent-session-output-images.ts +280 -0
- package/server/lib/agent-session-store.ts +3 -1
- package/server/lib/agent-session-turns.ts +18 -7
- package/server/lib/client-liveness.ts +38 -3
- package/server/lib/codex-subagent-activity.ts +36 -0
- package/server/lib/g2-turn-provenance.ts +679 -0
- package/server/lib/media-store.ts +41 -2
- package/server/lib/permission-broker.ts +94 -17
- package/server/lib/queue-turn-evidence.ts +19 -2
- package/server/lib/session-halt.ts +15 -1
- package/server/lib/session-stream-events.ts +4 -0
- package/server/lib/session-subagents.ts +155 -0
- package/server/lib/session-turn-attachments.ts +245 -0
- package/server/lib/thread-turn-queue-deliver.ts +17 -2
- package/server/lib/thread-turn-queue.ts +10 -0
- package/server/lib/today-brief.ts +731 -0
- package/server/lib/turn-file-store.ts +202 -0
- package/server/lib/turn-footer.ts +81 -0
- package/server/routes/agent-session-bindings.ts +164 -16
- package/server/routes/agent-sessions.ts +77 -8
- package/server/routes/client-instance.ts +12 -1
- package/server/routes/cursor-stop-followup.ts +30 -1
- package/server/routes/g2-turns.ts +46 -0
- package/server/routes/health.ts +9 -0
- package/server/routes/media.ts +32 -8
- package/server/routes/permission-broker.ts +47 -4
- package/server/routes/thread-turn-queue.ts +148 -8
- package/server/routes/today-brief.ts +148 -0
- package/server/web-control/app.css +2 -0
- package/server/web-control/app.js +738 -102
- package/server/web-control/index.html +5 -1
- package/server/web-control/panes/cos-chat.css +18 -0
- package/server/web-control/panes/cos-chat.js +239 -45
- package/server/web-control/panes/meetings.js +20 -9
- package/server/web-control/panes/questions.css +39 -1
- package/server/web-control/panes/questions.js +368 -51
- package/server/web-control/panes/reader-media.js +154 -0
- package/server/web-control/panes/sessions.css +2 -0
- package/server/web-control/panes/settings.js +34 -2
- package/server/web-control/panes/today-actions.css +62 -4
- package/server/web-control/panes/today-actions.js +384 -53
|
@@ -8,6 +8,12 @@
|
|
|
8
8
|
status line and its last reply. Open session stays a link to the full reader.
|
|
9
9
|
- Running: a peek at what the session is doing now, read with the reader's own activity reducer (applyFeed) from the
|
|
10
10
|
same live stream, and a short note box. Open session goes to the reader.
|
|
11
|
+
- A waiting session (6.99.26): its question cards when this page holds the question; when the Mac's own prompt has
|
|
12
|
+
it, that prompt read-only with Miles's words for it, and no reply box (a reply never answers a prompt); a reply box
|
|
13
|
+
only where a reply is the answer (Codex and Cursor).
|
|
14
|
+
- The pet (6.99.26, Miles 11/07: "Open what matters"): one session in play opens its sheet straight away (app.js
|
|
15
|
+
petTap); several open the short list here, waiting on you first, then running, then done today, each row opening
|
|
16
|
+
its own sheet.
|
|
11
17
|
What a reply or a note shares with the Sessions reader, and what is this file's own copy:
|
|
12
18
|
- Shared (app.js, through COSWeb.sessionKit): the attach, turn and queue path itself (continueSession and parkTurn,
|
|
13
19
|
run with this overlay as their view), the per-browser binding store, one key per draft, the attachability probe
|
|
@@ -22,6 +28,12 @@
|
|
|
22
28
|
Close with the X, a tap outside, Escape, or Back (the overlay holds one history entry of its own). Focus moves in and
|
|
23
29
|
comes back to the row. The page behind is inert and does not scroll.
|
|
24
30
|
|
|
31
|
+
Files with a reply or a note (11/07, Miles: "Sometimes I need to pass context via a screenshot, photo or file"): the
|
|
32
|
+
COS chat's own clip, chips, limits and upload (W.cosChat.attach), shown only where the Mac says a session turn takes
|
|
33
|
+
files (threadTurnAttachments). The ids go with the words through the same send path; the server writes where each
|
|
34
|
+
file is on the Mac under the words (lib/session-turn-attachments.ts). Send waits for every upload, and a failed one has
|
|
35
|
+
to be retried or removed first. The files are part of the draft's key, so other files are another message.
|
|
36
|
+
|
|
25
37
|
Also the summary COS gets when Miles asks from Today (summary(): app.js askCos passes it as the Ask's reference).
|
|
26
38
|
Loaded on the first tap or once the page is idle (app.js ensurePane, #lazy-panes). */
|
|
27
39
|
(function () {
|
|
@@ -61,9 +73,11 @@
|
|
|
61
73
|
var CREATE_LABEL = 'Create task', DISMISS_LABEL = 'Dismiss';
|
|
62
74
|
|
|
63
75
|
var O = {
|
|
64
|
-
open: null, // { kind: 'need' | 'run' | 'meeting' | 'intake', key, all }
|
|
76
|
+
open: null, // { kind: 'need' | 'run' | 'wait' | 'pet' | 'meeting' | 'intake', key, all }
|
|
65
77
|
opener: null, openerSel: '',
|
|
66
78
|
pushed: false, ignorePops: 0,
|
|
79
|
+
depth: 1, // history entries the overlay holds: 2 while a session opened from the pet's list shows
|
|
80
|
+
back: null, // { scroll } the pet's list that session came from, for Back to sessions
|
|
67
81
|
dirty: false, // something was decided or sent: Today reloads on close
|
|
68
82
|
// From today's meetings
|
|
69
83
|
busy: '', // the one suggestion being decided (Control: one at a time)
|
|
@@ -75,7 +89,7 @@
|
|
|
75
89
|
r: null, // the session object the reader's send path takes (sessionKit.session)
|
|
76
90
|
row: null, // the session's row in the session list
|
|
77
91
|
item: null, // the Needs you item
|
|
78
|
-
stream: null, clock: null, watch: null, qMount: null, cautionAsk: false, note: { text: '', kind: '' },
|
|
92
|
+
stream: null, clock: null, watch: null, qMount: null, qCount: 0, macCount: 0, cautionAsk: false, note: { text: '', kind: '' },
|
|
79
93
|
last: null, // { text, at } the session's last reply
|
|
80
94
|
lastState: 'idle', // idle | loading | ready | error | none
|
|
81
95
|
needGone: false
|
|
@@ -95,6 +109,8 @@
|
|
|
95
109
|
var all = W.readStore(DRAFTS), k = draftKey(r), now = Date.now(), prev = all[k] || {};
|
|
96
110
|
Object.keys(all).forEach(function (x) { if (!all[x] || now - (Number(all[x].at) || 0) >= DRAFT_DAYS * 86400000) delete all[x]; });
|
|
97
111
|
var u = unsettled === undefined ? prev.unsettled || null : unsettled ? { text: unsettled.text, key: unsettled.key } : null;
|
|
112
|
+
// A send with files that never settled keeps their ids, so after a reload it goes again exactly as it was.
|
|
113
|
+
if (u && unsettled && unsettled.ids && unsettled.ids.length) u.ids = unsettled.ids.slice();
|
|
98
114
|
var t = String(text || '');
|
|
99
115
|
if (!t.trim() && !u) delete all[k]; else all[k] = { text: t, unsettled: u, at: now };
|
|
100
116
|
W.writeStore(DRAFTS, all);
|
|
@@ -115,7 +131,8 @@
|
|
|
115
131
|
layer.innerHTML = '<div class="ta-scrim" data-ta="close" aria-hidden="true"></div>' +
|
|
116
132
|
'<section class="ta-sheet" id="ta-sheet" role="dialog" aria-modal="true" aria-labelledby="ta-title" tabindex="-1">' +
|
|
117
133
|
'<div class="ta-grip" aria-hidden="true"></div>' +
|
|
118
|
-
'<header class="ta-head"><
|
|
134
|
+
'<header class="ta-head" id="ta-head"><div class="ta-head-main"><button type="button" class="link-btn ta-back" id="ta-back" data-ta="back" hidden>Back to sessions</button>' +
|
|
135
|
+
'<h2 class="ta-title" id="ta-title"></h2></div>' +
|
|
119
136
|
'<button type="button" class="ta-x" data-ta="close" aria-label="Close">' + X_ICON + '</button></header>' +
|
|
120
137
|
'<div class="ta-body" id="ta-body"></div>' +
|
|
121
138
|
'<div class="ta-foot" id="ta-foot"></div>' +
|
|
@@ -130,18 +147,86 @@
|
|
|
130
147
|
layer.addEventListener('input', function (e) {
|
|
131
148
|
if (e.target && e.target.id === 'ta-input' && O.r) keepDraft(O.r, e.target.value, O.r.draft && O.r.draft.text === e.target.value.trim() ? O.r.draft : undefined);
|
|
132
149
|
});
|
|
150
|
+
// Files: the clip, a pasted screenshot in the box, and files dropped on the reply box.
|
|
151
|
+
layer.addEventListener('change', function (e) {
|
|
152
|
+
if (e.target && e.target.hasAttribute && e.target.hasAttribute('data-ta-file')) { addFiles(e.target.files); e.target.value = ''; }
|
|
153
|
+
});
|
|
154
|
+
layer.addEventListener('paste', function (e) {
|
|
155
|
+
if (!e.target || e.target.id !== 'ta-input' || !filesOn()) return;
|
|
156
|
+
var files = KIT().pasted(e);
|
|
157
|
+
if (files) { e.preventDefault(); addFiles(files); }
|
|
158
|
+
});
|
|
159
|
+
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(function (type) {
|
|
160
|
+
layer.addEventListener(type, function (e) {
|
|
161
|
+
var form = $('ta-form');
|
|
162
|
+
if (!form || !filesOn() || !e.target || !e.target.closest || !e.target.closest('#ta-form')) return;
|
|
163
|
+
var files = KIT().dragged(e, form);
|
|
164
|
+
if (files) addFiles(files);
|
|
165
|
+
});
|
|
166
|
+
});
|
|
133
167
|
// A drag on the dimmed page never scrolls what is behind it.
|
|
134
|
-
|
|
168
|
+
// The bleed below the bottom edge is the layer's own ::before (today-actions.css): a drag there is the layer's.
|
|
169
|
+
layer.addEventListener('touchmove', function (e) { if (e.target && (e.target === layer || (e.target.classList && e.target.classList.contains('ta-scrim')))) e.preventDefault(); }, { passive: false });
|
|
135
170
|
return layer;
|
|
136
171
|
}
|
|
137
|
-
//
|
|
138
|
-
|
|
172
|
+
// With the keyboard up the layer follows the visible viewport, so the keyboard never covers the reply box or Send.
|
|
173
|
+
// With it closed the layer is the whole fixed viewport (today-actions.css): --ta-vh and --ta-vt are set but unused, so
|
|
174
|
+
// a visible viewport that reports less than the screen (IMG_0503, 11/07 12:48: about 106 px) never leaves a band.
|
|
175
|
+
// 6.99.29 (Miles's unfolded Duo, 11/07 11:50): on a wide screen the side sheet kept its tall header and the reply box
|
|
176
|
+
// went under the keyboard while the conversation shrank to one line. With the keyboard up, at any width, the layer is
|
|
177
|
+
// marked ta-kb (today-actions.css): the sheet fills the visible viewport, the header folds to one compact row, the
|
|
178
|
+
// reply box stays pinned at the sheet's bottom just above the keyboard, and the conversation takes what is left. If
|
|
179
|
+
// that leaves under about three lines, ta-kb-tight also hides the "Working right now" note until the keyboard closes.
|
|
180
|
+
// Keyboard: the visible viewport, at its zoom, is KB_MIN_PX or more shorter than the layout one (a form bar alone is
|
|
181
|
+
// about 45 px). Read on visualViewport resize and scroll, at most once a frame, only while the overlay is open.
|
|
182
|
+
var KB_MIN_PX = 120;
|
|
183
|
+
var BODY_MIN_PX = 72; // about three lines of the conversation (15 px text at 1.5)
|
|
184
|
+
var KB = { on: false, tight: false, frame: 0, listening: false };
|
|
185
|
+
function keyboardInset(vv) {
|
|
186
|
+
var layoutH = window.innerHeight || (document.documentElement && document.documentElement.clientHeight) || 0;
|
|
187
|
+
var inset = Math.round(layoutH - vv.height * (vv.scale || 1));
|
|
188
|
+
return inset > 0 ? inset : 0;
|
|
189
|
+
}
|
|
190
|
+
function tall(el) { return (el && el.offsetHeight) || 0; }
|
|
191
|
+
// What the conversation gets once the compact header and the reply box have theirs.
|
|
192
|
+
function bodyRoom(vv) {
|
|
193
|
+
var sheet = $('ta-sheet'), grip = sheet && sheet.querySelector ? sheet.querySelector('.ta-grip') : null;
|
|
194
|
+
return vv.height - tall($('ta-head')) - tall($('ta-foot')) - tall(grip);
|
|
195
|
+
}
|
|
196
|
+
function applyFit() {
|
|
197
|
+
KB.frame = 0;
|
|
139
198
|
var vv = window.visualViewport;
|
|
140
199
|
if (!layer || !vv) return;
|
|
141
200
|
layer.style.setProperty('--ta-vh', Math.round(vv.height) + 'px');
|
|
142
201
|
layer.style.setProperty('--ta-vt', Math.round(vv.offsetTop) + 'px');
|
|
202
|
+
var inset = keyboardInset(vv), on = inset >= KB_MIN_PX;
|
|
203
|
+
layer.style.setProperty('--ta-kb', (on ? inset : 0) + 'px');
|
|
204
|
+
if (on !== KB.on) { KB.on = on; KB.tight = false; layer.classList.toggle('ta-kb', on); }
|
|
205
|
+
// Measured with the compact header in place. Once tight it stays so while the keyboard is up: showing the note
|
|
206
|
+
// again would take the room back and flicker.
|
|
207
|
+
if (on && !KB.tight) KB.tight = bodyRoom(vv) < BODY_MIN_PX;
|
|
208
|
+
layer.classList.toggle('ta-kb-tight', on && KB.tight);
|
|
209
|
+
}
|
|
210
|
+
function fit() {
|
|
211
|
+
if (KB.frame) return;
|
|
212
|
+
if (!window.requestAnimationFrame) return applyFit(); // every browser with visualViewport has frames
|
|
213
|
+
KB.frame = window.requestAnimationFrame(applyFit);
|
|
214
|
+
}
|
|
215
|
+
function listenViewport(on) {
|
|
216
|
+
var vv = window.visualViewport;
|
|
217
|
+
if (!vv || KB.listening === on) return;
|
|
218
|
+
KB.listening = on;
|
|
219
|
+
var m = on ? 'addEventListener' : 'removeEventListener';
|
|
220
|
+
if (typeof vv[m] === 'function') { vv[m]('resize', fit); vv[m]('scroll', fit); }
|
|
221
|
+
}
|
|
222
|
+
function unfit() {
|
|
223
|
+
listenViewport(false);
|
|
224
|
+
if (KB.frame && window.cancelAnimationFrame) window.cancelAnimationFrame(KB.frame);
|
|
225
|
+
KB.frame = 0; KB.on = false; KB.tight = false;
|
|
226
|
+
if (!layer) return;
|
|
227
|
+
layer.classList.remove('ta-kb'); layer.classList.remove('ta-kb-tight');
|
|
228
|
+
layer.style.setProperty('--ta-kb', '0px');
|
|
143
229
|
}
|
|
144
|
-
if (window.visualViewport) { window.visualViewport.addEventListener('resize', fit); window.visualViewport.addEventListener('scroll', fit); }
|
|
145
230
|
|
|
146
231
|
function openerSelector(t) {
|
|
147
232
|
if (!t || !t.getAttribute) return '';
|
|
@@ -151,9 +236,14 @@
|
|
|
151
236
|
return '[data-today=' + q(kind) + ']' + (key != null ? '[data-key=' + q(key) + ']' : '');
|
|
152
237
|
}
|
|
153
238
|
|
|
154
|
-
|
|
239
|
+
// opt.from: a session opened from the pet's list ({ scroll }), which gets Back to sessions and its own history entry,
|
|
240
|
+
// so the phone's Back returns to the list too (Miles's iPhone, 11/07). opt.again: the list shown again from there,
|
|
241
|
+
// its entry already current.
|
|
242
|
+
function open(kind, key, opener, opt) {
|
|
243
|
+
opt = opt || {};
|
|
155
244
|
build();
|
|
156
245
|
if (O.open) stopSession();
|
|
246
|
+
O.back = opt.from || null;
|
|
157
247
|
O.open = { kind: kind, key: String(key || ''), all: kind === 'intake' };
|
|
158
248
|
O.opener = opener || document.activeElement; O.openerSel = openerSelector(opener);
|
|
159
249
|
O.note = { text: '', kind: '' }; O.cautionAsk = false; O.needGone = false; O.armedAt = {};
|
|
@@ -161,14 +251,20 @@
|
|
|
161
251
|
layer.hidden = false;
|
|
162
252
|
document.documentElement.classList.add('ta-open');
|
|
163
253
|
var shell = $('app-shell'); if (shell) shell.setAttribute('inert', '');
|
|
164
|
-
|
|
254
|
+
listenViewport(true); applyFit();
|
|
165
255
|
// The entry names what is open, so a reload can open it again and Back still closes it (restore below).
|
|
166
256
|
var entry = { cosTodayOverlay: 1, kind: kind, key: O.open.key };
|
|
257
|
+
if (O.back) entry.from = 'pet';
|
|
167
258
|
try {
|
|
168
|
-
if (
|
|
259
|
+
if (opt.again) { /* the list's own entry is current again */ }
|
|
260
|
+
else if (!O.pushed) { history.pushState(entry, ''); O.pushed = true; O.depth = 1; }
|
|
261
|
+
else if (opt.from && O.depth === 1) { history.pushState(entry, ''); O.depth = 2; }
|
|
262
|
+
else history.replaceState(entry, '');
|
|
169
263
|
} catch (_e) { /* no history API: Back simply leaves Today */ }
|
|
264
|
+
var backBtn = $('ta-back'); if (backBtn) backBtn.hidden = !O.back;
|
|
170
265
|
try {
|
|
171
266
|
if (kind === 'need' || kind === 'run' || kind === 'wait') openSession(kind, O.open.key);
|
|
267
|
+
else if (kind === 'pet') openPet();
|
|
172
268
|
else openIntake();
|
|
173
269
|
} catch (err) {
|
|
174
270
|
// A bug here must not leave a half-drawn sheet over an inert page: take it down, keep the history entry for
|
|
@@ -212,9 +308,15 @@
|
|
|
212
308
|
stopSession();
|
|
213
309
|
O.open = null;
|
|
214
310
|
if (layer) layer.hidden = true;
|
|
311
|
+
unfit();
|
|
215
312
|
document.documentElement.classList.remove('ta-open');
|
|
216
313
|
var shell = $('app-shell'); if (shell) shell.removeAttribute('inert');
|
|
217
|
-
|
|
314
|
+
// X from a session opened off the pet's list unwinds both entries in one step, so the next Back leaves Today as before.
|
|
315
|
+
if (O.pushed && how !== 'pop' && how !== 'leave') {
|
|
316
|
+
var n = O.depth > 1 ? O.depth : 1;
|
|
317
|
+
O.pushed = false; O.ignorePops++;
|
|
318
|
+
try { if (n > 1 && typeof history.go === 'function') history.go(-n); else history.back(); } catch (_e) { O.ignorePops--; }
|
|
319
|
+
}
|
|
218
320
|
if (how === 'pop' || how === 'leave') O.pushed = false;
|
|
219
321
|
clearTimeout(O.expireTimer);
|
|
220
322
|
// Settled lines are done once the overlay closes; Today reads the server again if anything changed here.
|
|
@@ -241,7 +343,7 @@
|
|
|
241
343
|
// Back from there showed nothing (measured in Chrome 11/07: a same-document popstate). On load, app.js hands that
|
|
242
344
|
// entry here: open the same thing again, so the first Back closes it as it did before the reload. What cannot be
|
|
243
345
|
// opened (gone, or Today left meanwhile) just clears the entry's mark.
|
|
244
|
-
var KINDS = { need: 1, run: 1, wait: 1, meeting: 1, intake: 1 };
|
|
346
|
+
var KINDS = { need: 1, run: 1, wait: 1, pet: 1, meeting: 1, intake: 1 };
|
|
245
347
|
function restore(st) {
|
|
246
348
|
if (O.open || !st || !st.cosTodayOverlay) return Promise.resolve(false);
|
|
247
349
|
var forget = function () { try { history.replaceState(null, ''); } catch (_e) {} return false; };
|
|
@@ -249,23 +351,36 @@
|
|
|
249
351
|
var started = Date.now();
|
|
250
352
|
return new Promise(function (resolve) {
|
|
251
353
|
(function wait() {
|
|
252
|
-
var d = T.data(), ready = d.at > 0 && (st.kind === 'need' || st.kind === 'run' || st.kind === 'wait' || d.intakeLoaded);
|
|
354
|
+
var d = T.data(), ready = d.at > 0 && (st.kind === 'need' || st.kind === 'run' || st.kind === 'wait' || st.kind === 'pet' || d.intakeLoaded);
|
|
253
355
|
if (W.parseRoute().pane !== 'today' || O.open) return resolve(forget());
|
|
254
356
|
if (!ready) { if (Date.now() - started > 15000) return resolve(forget()); return setTimeout(wait, 150); }
|
|
255
357
|
O.pushed = true; // this entry is the overlay's own
|
|
256
|
-
|
|
358
|
+
// A session opened off the pet's list keeps its Back to sessions: the list's entry is still under it.
|
|
359
|
+
if (st.from === 'pet' && (st.kind === 'wait' || st.kind === 'run')) O.depth = 2;
|
|
360
|
+
try { open(st.kind, String(st.key || ''), null, st.from === 'pet' && O.depth === 2 ? { from: { scroll: 0 }, again: true } : undefined); resolve(true); }
|
|
257
361
|
catch (err) { console.error('[today] could not reopen the overlay after a reload.', err); O.pushed = false; resolve(forget()); }
|
|
258
362
|
})();
|
|
259
363
|
});
|
|
260
364
|
}
|
|
261
365
|
window.addEventListener('popstate', function () {
|
|
262
366
|
if (O.ignorePops > 0) { O.ignorePops--; return; }
|
|
367
|
+
// The phone's Back from a session opened off the pet's list goes back to that list, as Back to sessions does.
|
|
368
|
+
if (O.open && O.back) { backToList(); return; }
|
|
263
369
|
if (O.open) close('pop');
|
|
264
370
|
});
|
|
371
|
+
// The pet's list again, where it was scrolled to. From the button the session's entry is taken off history too.
|
|
372
|
+
function backToList(fromButton) {
|
|
373
|
+
var b = O.back;
|
|
374
|
+
if (!b) return;
|
|
375
|
+
if (fromButton && O.depth > 1) { O.ignorePops++; try { history.back(); } catch (_e) { O.ignorePops--; } }
|
|
376
|
+
O.depth = 1;
|
|
377
|
+
open('pet', '', O.opener, { again: true });
|
|
378
|
+
var body = $('ta-body'); if (body) body.scrollTop = b.scroll || 0;
|
|
379
|
+
}
|
|
265
380
|
W.onRoute(function (r) { if (O.open && r.pane !== 'today') close('leave'); });
|
|
266
381
|
// Escape closes the overlay, unless the Ask sheet is open over it (app.js closes that first).
|
|
267
382
|
document.addEventListener('keydown', function (e) {
|
|
268
|
-
if (!O.open || e.key !== 'Escape') return;
|
|
383
|
+
if (!O.open || e.key !== 'Escape' || (W.readerMedia && W.readerMedia.isOpen())) return;
|
|
269
384
|
var ask = $('ask-sheet');
|
|
270
385
|
if (ask && !ask.classList.contains('hidden')) return;
|
|
271
386
|
e.preventDefault();
|
|
@@ -295,6 +410,7 @@
|
|
|
295
410
|
if (O.open && (O.open.kind === 'meeting' || O.open.kind === 'intake')) paintIntake(); else paintLive();
|
|
296
411
|
}
|
|
297
412
|
if (what === 'close') return close('user');
|
|
413
|
+
if (what === 'back') return backToList(true);
|
|
298
414
|
if (what === 'create' || what === 'link') return decide(id, 'accept');
|
|
299
415
|
if (what === 'dismiss') {
|
|
300
416
|
// Without Undo on this server a Dismiss cannot be taken back, so it asks once: tap, then tap again.
|
|
@@ -306,7 +422,20 @@
|
|
|
306
422
|
if (what === 'open-work') return leaveTo('work', ['sort']);
|
|
307
423
|
if (what === 'open-card') return leaveTo('work', id ? [id] : []);
|
|
308
424
|
if (what === 'open-session') return O.r ? leaveTo('sessions', [O.r.provider, O.r.id]) : null;
|
|
425
|
+
if (what === 'open-sessions') return leaveTo('sessions', []);
|
|
426
|
+
// 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)
|
|
427
|
+
// returning to the list where it was scrolled; X still closes the overlay.
|
|
428
|
+
if (what === 'pet-open') {
|
|
429
|
+
var pk = b.getAttribute('data-kind'), lb = $('ta-body');
|
|
430
|
+
return open(pk === 'wait' ? 'wait' : 'run', id, O.opener, { from: { scroll: lb ? lb.scrollTop || 0 : 0 } });
|
|
431
|
+
}
|
|
309
432
|
if (what === 'send') return sendReply();
|
|
433
|
+
if (what === 'att-remove' || what === 'att-retry') {
|
|
434
|
+
var fb = filesBox(); if (!fb) return;
|
|
435
|
+
O.note = { text: '', kind: '' }; paintNote();
|
|
436
|
+
if (what === 'att-remove') KIT().remove(fb, b.getAttribute('data-att')); else KIT().retry(fb, b.getAttribute('data-att'));
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
310
439
|
if (what === 'stop') {
|
|
311
440
|
// A tap arms it (Stop ends the agent's turn on your Mac); the second sends it.
|
|
312
441
|
if (!confirmTap('stop', paintLive)) return;
|
|
@@ -328,7 +457,8 @@
|
|
|
328
457
|
// The keyboard rises over the bottom of the sheet: keep the box in view as it does.
|
|
329
458
|
function onFocusIn(e) {
|
|
330
459
|
if (!e.target || e.target.id !== 'ta-input') return;
|
|
331
|
-
|
|
460
|
+
fit();
|
|
461
|
+
setTimeout(function () { if (!O.open) return; applyFit(); var f = $('ta-form'); if (f && f.scrollIntoView) f.scrollIntoView({ block: 'nearest' }); }, 250);
|
|
332
462
|
}
|
|
333
463
|
|
|
334
464
|
// ═════════════ From today's meetings ═════════════
|
|
@@ -359,7 +489,8 @@
|
|
|
359
489
|
probeLoop().then(function () { if (O.open && (O.open.kind === 'meeting' || O.open.kind === 'intake')) paintIntake(); });
|
|
360
490
|
paintIntake();
|
|
361
491
|
var foot = $('ta-foot');
|
|
362
|
-
|
|
492
|
+
// A waiting sheet opened before may have hidden the foot (its reply box); this one always shows (QA round 1).
|
|
493
|
+
if (foot) { foot.innerHTML = '<div class="ta-links"><button type="button" class="link-btn" data-ta="open-work">Open Sort in Work</button></div>'; foot.hidden = false; }
|
|
363
494
|
// Read Intake again: what was decided on another screen since Today loaded shows as handled, never as a phantom.
|
|
364
495
|
var asked = T.reloadIntake();
|
|
365
496
|
if (asked && asked.then) asked.then(function () { if (O.open && (O.open.kind === 'meeting' || O.open.kind === 'intake')) { noteVanished(); paintIntake(); } });
|
|
@@ -562,25 +693,42 @@
|
|
|
562
693
|
O.row = id ? sessionRowFor(provider, id) : null;
|
|
563
694
|
O.r = id ? K.session(provider, id, VIEW) : null;
|
|
564
695
|
// Working only when the session is not waiting on you: a session paused on a question is not at work (W4-r2).
|
|
565
|
-
|
|
696
|
+
// 6.99.29: the row is one signal of the card's one state (app.js sxTruth), timed by its own evidence.
|
|
697
|
+
if (O.r) { O.r.live = {}; K.feed.noteList(O.r, O.row); K.feed.settle(O.r); }
|
|
566
698
|
// What was typed, and a send whose answer never came, survive a close, Back or reload: the same words resend under
|
|
567
699
|
// the same key (skeptic W3, qa2-user N1).
|
|
568
700
|
var kept = O.r ? draftOf(O.r) : null;
|
|
569
|
-
if (kept && kept.unsettled)
|
|
701
|
+
if (kept && kept.unsettled) {
|
|
702
|
+
O.r.draft = { text: kept.unsettled.text, key: kept.unsettled.key };
|
|
703
|
+
if (Array.isArray(kept.unsettled.ids) && kept.unsettled.ids.length) { O.r.draft.ids = kept.unsettled.ids.slice(); O.r.draft.restored = true; }
|
|
704
|
+
}
|
|
570
705
|
O.last = null; O.lastState = 'idle';
|
|
571
706
|
setHead(kind === 'need' ? (O.item ? str(O.item.sessionTitle) || str(O.item.title) || 'Handoff' : 'Needs you')
|
|
572
707
|
: kind === 'wait' ? (O.wait ? O.wait.title : 'A session is waiting for you') : (O.row ? K.title(O.row) : 'Session'));
|
|
573
708
|
var body = $('ta-body');
|
|
574
709
|
if (body) body.innerHTML = '<div id="ta-meta"></div><div id="ta-q" class="ta-q" hidden></div><div id="ta-live"></div><div id="ta-last"></div>';
|
|
575
710
|
var foot = $('ta-foot');
|
|
576
|
-
if (foot) foot.innerHTML = O.r ? composerHtml(kind) : '';
|
|
711
|
+
if (foot) { foot.innerHTML = O.r ? composerHtml(kind) : ''; foot.hidden = false; }
|
|
577
712
|
var input = $('ta-input');
|
|
578
713
|
if (input && kept && kept.text) { input.value = kept.text; W.autoGrow(input); }
|
|
714
|
+
if (O.r) {
|
|
715
|
+
paintFiles();
|
|
716
|
+
// The Mac's answer about files may not be in yet: ask once, then draw the clip.
|
|
717
|
+
var kit = KIT(), r0 = O.r;
|
|
718
|
+
if (kit && !kit.turnFiles()) kit.ensureCaps().then(function () { if (O.r === r0) paintFiles(); }, function () {});
|
|
719
|
+
// Files of a send a reload cut off go again with the same words; say so, since their chips are gone.
|
|
720
|
+
if (O.r.draft && O.r.draft.restored) {
|
|
721
|
+
var nf = O.r.draft.ids.length;
|
|
722
|
+
O.note = { text: 'Your last message did not finish sending. Send sends it again with the ' + (nf === 1 ? 'file' : nf + ' files') + ' you attached.', kind: '' };
|
|
723
|
+
paintNote();
|
|
724
|
+
}
|
|
725
|
+
}
|
|
579
726
|
paintMeta();
|
|
580
727
|
if (!O.r) return;
|
|
581
728
|
if (W.sessionQuestions && W.sessionQuestions.mount) {
|
|
582
|
-
// mount() draws at once and calls back before it returns, so the
|
|
583
|
-
|
|
729
|
+
// mount() draws at once and calls back before it returns, so the counts come from the callback, not O.qMount:
|
|
730
|
+
// the questions to answer here, and those the Mac's own prompt waits on (drawn read-only).
|
|
731
|
+
O.qMount = W.sessionQuestions.mount($('ta-q'), id, function (n, mac) { O.qCount = n; O.macCount = mac || 0; paintMeta(); });
|
|
584
732
|
}
|
|
585
733
|
if (kind === 'run') { paintLive(); watchStream(O.r); }
|
|
586
734
|
loadLast(O.r);
|
|
@@ -588,11 +736,12 @@
|
|
|
588
736
|
if (kind === 'need') recheckNeed(key);
|
|
589
737
|
}
|
|
590
738
|
function stopSession() {
|
|
739
|
+
if (K.imagesStop) K.imagesStop($('ta-last'));
|
|
591
740
|
if (O.stream) { try { O.stream.close(); } catch (_e) {} O.stream = null; }
|
|
592
741
|
clearInterval(O.clock); O.clock = null;
|
|
593
742
|
clearInterval(O.watch); O.watch = null;
|
|
594
743
|
if (O.qMount) { O.qMount.unmount(); O.qMount = null; }
|
|
595
|
-
O.qCount = 0;
|
|
744
|
+
O.qCount = 0; O.macCount = 0;
|
|
596
745
|
O.r = null; O.row = null; O.item = null;
|
|
597
746
|
}
|
|
598
747
|
// The Needs you list may be minutes old: read it again, and say so if this one is no longer waiting.
|
|
@@ -612,12 +761,73 @@
|
|
|
612
761
|
var label = kind === 'run' ? 'Send a note to this session' : 'Reply to this session';
|
|
613
762
|
return '<div id="ta-verdict"></div>' +
|
|
614
763
|
'<form class="composer ta-composer" id="ta-form">' +
|
|
764
|
+
'<div class="cc-chips ta-files hidden" id="ta-files"></div>' +
|
|
615
765
|
'<textarea id="ta-input" rows="1" placeholder="' + esc(label) + '" aria-label="' + esc(label) + '" enterkeyhint="send"></textarea>' +
|
|
616
|
-
'<div class="composer-actions"><button type="submit" class="btn primary small" id="ta-send">Send</button></div>' +
|
|
766
|
+
'<div class="composer-actions"><span id="ta-clip" class="ta-clip"></span><button type="submit" class="btn primary small" id="ta-send">Send</button></div>' +
|
|
617
767
|
'</form>' +
|
|
618
768
|
'<p class="status-line" id="ta-status" role="status" aria-live="polite"></p>';
|
|
619
769
|
}
|
|
620
770
|
|
|
771
|
+
// ── files with a reply or a note (the COS chat's attach kit) ──
|
|
772
|
+
// One box per session, kept while this tab is open, so closing and reopening the sheet keeps what was attached.
|
|
773
|
+
var FILES = {};
|
|
774
|
+
function KIT() { return W.cosChat && W.cosChat.attach ? W.cosChat.attach : null; }
|
|
775
|
+
// Only where the Mac takes files on a session turn: on an older server the words go alone, and no clip says otherwise.
|
|
776
|
+
function filesOn() { var k = KIT(); return !!(k && k.turnFiles() > 0 && O.r); }
|
|
777
|
+
function filesBox(r) {
|
|
778
|
+
r = r || O.r; var kit = KIT();
|
|
779
|
+
if (!r || !kit) return null;
|
|
780
|
+
var key = r.provider + '|' + r.id;
|
|
781
|
+
// 6.99.27: any file the Mac takes on a turn (threadTurnFileBytes), sent as it is; else the chat's list.
|
|
782
|
+
if (!FILES[key]) FILES[key] = kit.box({
|
|
783
|
+
changed: function (box) { if (O.r && FILES[O.r.provider + '|' + O.r.id] === box) paintFiles(); },
|
|
784
|
+
removed: function () {},
|
|
785
|
+
signedOut: function (box) { if (O.r && FILES[O.r.provider + '|' + O.r.id] === box) { O.note = { text: 'This browser was signed out. Your message and files are kept. Sign in, then tap Retry on each file.', kind: 'error' }; paintNote(); } }
|
|
786
|
+
}, { anyBytes: kit.turnFileBytes() });
|
|
787
|
+
// The Mac's answer can come after the box was made: a box with nothing in it yet takes it then.
|
|
788
|
+
if (!FILES[key].anyBytes && !FILES[key].atts.length && kit.turnFileBytes()) FILES[key].anyBytes = kit.turnFileBytes();
|
|
789
|
+
return FILES[key];
|
|
790
|
+
}
|
|
791
|
+
function filesState(box) {
|
|
792
|
+
var kit = KIT();
|
|
793
|
+
if (!kit || !box || !box.atts.length) return 'none';
|
|
794
|
+
return kit.pending(box) ? 'waiting' : kit.failed(box) ? 'failed' : 'ready';
|
|
795
|
+
}
|
|
796
|
+
function paintClip() {
|
|
797
|
+
var slot = $('ta-clip'); if (!slot) return;
|
|
798
|
+
var kit = KIT();
|
|
799
|
+
if (!filesOn()) { slot.innerHTML = ''; return; }
|
|
800
|
+
var box = filesBox(), full = !!(box && box.atts.length >= kit.max);
|
|
801
|
+
var mode = String(box && box.anyBytes || 0);
|
|
802
|
+
if (slot.getAttribute('data-mode') !== mode) { slot.innerHTML = kit.cameraHtml('data-ta-file') + kit.clipHtml('data-ta-file', { any: box && box.anyBytes }); slot.setAttribute('data-mode', mode); }
|
|
803
|
+
var lbls = slot.querySelectorAll ? slot.querySelectorAll('.cc-attach') : [], inps = slot.querySelectorAll ? slot.querySelectorAll('[data-ta-file]') : [];
|
|
804
|
+
Array.prototype.forEach.call(lbls, function (l) { l.classList.toggle('full', full); });
|
|
805
|
+
Array.prototype.forEach.call(inps, function (i) { i.disabled = full || !!(O.r && O.r.sending); });
|
|
806
|
+
}
|
|
807
|
+
function paintFiles() {
|
|
808
|
+
var el = $('ta-files'), kit = KIT(), box = filesBox();
|
|
809
|
+
if (el && kit && box) {
|
|
810
|
+
el.innerHTML = kit.chipsHtml(box, 'data-ta') + (box.atts.length ? '<p class="att-limit">' + esc(kit.limitLine(box)) + '</p>' : '');
|
|
811
|
+
el.classList.toggle('hidden', !box.atts.length);
|
|
812
|
+
}
|
|
813
|
+
paintClip(); paintSendButton();
|
|
814
|
+
}
|
|
815
|
+
// Send waits while a file uploads. A failed file never greys it out (QA 6.99.27): a tap says why, as the question
|
|
816
|
+
// card does.
|
|
817
|
+
function paintSendButton() {
|
|
818
|
+
var send = $('ta-send'); if (!send) return;
|
|
819
|
+
var sending = !!(O.r && O.r.sending), st = filesState(filesBox());
|
|
820
|
+
send.disabled = sending || st === 'waiting';
|
|
821
|
+
send.textContent = sending ? 'Sending' : st === 'waiting' ? 'Uploading' : 'Send';
|
|
822
|
+
}
|
|
823
|
+
function addFiles(list) {
|
|
824
|
+
var kit = KIT(), box = filesBox();
|
|
825
|
+
if (!kit || !box || !list || !list.length || !filesOn()) return;
|
|
826
|
+
var refusals = kit.add(box, list);
|
|
827
|
+
if (refusals.length) { O.note = { text: refusals.join(' '), kind: 'error' }; paintNote(); }
|
|
828
|
+
paintFiles();
|
|
829
|
+
}
|
|
830
|
+
|
|
621
831
|
function needLine(it) {
|
|
622
832
|
var why = T.needWhy(it) + (it.domain ? ' in ' + W.domainLabel(it.domain) : '');
|
|
623
833
|
var age = W.ago(it.updatedAt || it.createdAt);
|
|
@@ -638,7 +848,7 @@
|
|
|
638
848
|
function runWords() {
|
|
639
849
|
var r = O.r, f = r && r.feed;
|
|
640
850
|
if (f && (f.connected || f.lastSeq > 0)) {
|
|
641
|
-
var w = K.feed.word(r), since = K.feed.elapsed(f);
|
|
851
|
+
var w = K.feed.word(r), since = K.feed.elapsed(f, r);
|
|
642
852
|
if (w === 'Working') return 'Working now' + (since ? ', for ' + since : '');
|
|
643
853
|
if (w === 'Waiting') return 'Waiting for you' + (since ? ', for ' + since : '');
|
|
644
854
|
if (w === 'Failed') return 'Stopped with an error';
|
|
@@ -662,10 +872,15 @@
|
|
|
662
872
|
var p = it.progress && !it.progress.unreadable ? it.progress : null;
|
|
663
873
|
if (p && p.evidence) html += '<blockquote class="ta-quote">' + esc(p.evidence) + '</blockquote>';
|
|
664
874
|
} else if (O.open.kind === 'wait') {
|
|
665
|
-
// A session waiting on you: what it waits on, its question to answer here when the Mac holds one for this page
|
|
666
|
-
//
|
|
875
|
+
// A session waiting on you: what it waits on, its question to answer here when the Mac holds one for this page;
|
|
876
|
+
// when the Mac's own prompt has it, that, read-only; else (a reply is the answer) the reply box below. No live
|
|
877
|
+
// peek and no Stop: it is not at work (W4-r2).
|
|
667
878
|
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>';
|
|
668
|
-
|
|
879
|
+
var where = waitWhere();
|
|
880
|
+
// The Mac card says it itself when there is one; say it here only when there is none.
|
|
881
|
+
if (where === 'mac' && !O.macCount) html += '<p class="ta-sub ta-mac-line">' + esc(macLine()) + '</p>';
|
|
882
|
+
if (where === 'answered') html += '<p class="ta-sub ta-mac-line">' + esc((W.sessionQuestions && W.sessionQuestions.answeredLine) || 'Answered. The session carries on.') + '</p>';
|
|
883
|
+
if (where === 'reply') html += '<p class="ta-sub">' + esc('Answer it in a reply below, or open the session.') + '</p>';
|
|
669
884
|
} else if (O.row || O.r) {
|
|
670
885
|
// Once the live feed speaks, the line says what it says, so the header and the peek never disagree (qa2-user N3).
|
|
671
886
|
html += '<p class="ta-sub">' + T.badge(O.r ? O.r.provider : O.row.provider) + ' <span id="ta-run-state">' + esc(runWords()) + '</span></p>';
|
|
@@ -678,27 +893,108 @@
|
|
|
678
893
|
if (links) html += '<div class="ta-links">' + links + '</div>';
|
|
679
894
|
if (!O.r && O.open.kind === 'need') html += '<p class="ta-sub">This handoff has no session to reply to. Open it in Work.</p>';
|
|
680
895
|
box.innerHTML = html;
|
|
896
|
+
// The reply box shows only where a reply can be the answer; what was typed stays in it, hidden.
|
|
897
|
+
var foot = $('ta-foot');
|
|
898
|
+
if (foot && O.open.kind === 'wait') foot.hidden = waitWhere() !== 'reply';
|
|
899
|
+
}
|
|
900
|
+
// Where a waiting session's question is answered: 'here' (its cards in this sheet), 'answered' (on some screen just
|
|
901
|
+
// now), 'mac' (a Claude session's own prompt, or a held question that went back to the Mac), or 'reply' (a Codex or
|
|
902
|
+
// Cursor session asking in words).
|
|
903
|
+
function waitWhere() {
|
|
904
|
+
if (O.qCount) return 'here';
|
|
905
|
+
if (O.r && W.sessionQuestions && W.sessionQuestions.answeredFor && W.sessionQuestions.answeredFor(O.r.id)) return 'answered';
|
|
906
|
+
if (O.macCount || (O.r && O.r.provider === 'claude') || (O.wait && O.wait.where === 'mac')) return 'mac';
|
|
907
|
+
return 'reply';
|
|
908
|
+
}
|
|
909
|
+
function macLine() { return (W.sessionQuestions && W.sessionQuestions.macLine) || "Waiting on your Mac's prompt. Answer it on the Mac (or through Screens)."; }
|
|
910
|
+
|
|
911
|
+
// ═════════════ The pet's sessions ═════════════
|
|
912
|
+
// The same sessions the pet's caption counts (app.js petPlay): waiting on you, running, then done today. Each row
|
|
913
|
+
// opens that session's own sheet.
|
|
914
|
+
var PET_DONE_SHOWN = 5;
|
|
915
|
+
function openPet() {
|
|
916
|
+
setHead('Sessions');
|
|
917
|
+
var p = T.petPlay ? T.petPlay() : { waiting: [], running: [], done: [] };
|
|
918
|
+
var row = function (s, kind, line) {
|
|
919
|
+
var k = T.splitSessionId(s.provider, s.session_id || s.id);
|
|
920
|
+
return '<button type="button" class="ta-pet-row" data-ta="pet-open" data-kind="' + kind + '" data-id="' + esc(k.provider + '|' + k.id) + '">' +
|
|
921
|
+
'<span class="ta-pet-top">' + T.badge(k.provider) + '<span class="ta-pet-title">' + esc(T.sessionTitle(s)) + '</span></span>' +
|
|
922
|
+
'<span class="ta-pet-line">' + esc(line) + '</span></button>';
|
|
923
|
+
};
|
|
924
|
+
var group = function (title, rows) { return rows.length ? '<section class="ta-pet-group"><h3 class="ta-pet-head">' + esc(title) + '</h3>' + rows.join('') + '</section>' : ''; };
|
|
925
|
+
// 6.99.27: the questions this page holds come first, one row per session, each opening that session's cards.
|
|
926
|
+
var asked = petAsked(), keyOf = function (s) { var k = T.splitSessionId(s.provider, s.session_id || s.id); return (k.provider + '|' + k.id).toLowerCase(); };
|
|
927
|
+
var questions = asked.map(function (a) {
|
|
928
|
+
var s = sessionRowFor(a.provider, a.id);
|
|
929
|
+
var k = esc(a.provider + '|' + a.id);
|
|
930
|
+
return '<button type="button" class="ta-pet-row" data-ta="pet-open" data-kind="wait" data-id="' + k + '">' +
|
|
931
|
+
'<span class="ta-pet-top">' + T.badge(a.provider) + '<span class="ta-pet-title">' + esc(s ? T.sessionTitle(s) : a.title) + '</span></span>' +
|
|
932
|
+
'<span class="ta-pet-line">' + esc(a.line) + '</span></button>';
|
|
933
|
+
});
|
|
934
|
+
var held = {}; asked.forEach(function (a) { held[(a.provider + '|' + a.id).toLowerCase()] = 1; });
|
|
935
|
+
var waiting = p.waiting.filter(function (s) { return !held[keyOf(s)]; }).map(function (s) { return row(s, 'wait', stateWords(s)); });
|
|
936
|
+
// The pet counts a session as running from the Running card's test; one of those paused on you opens as waiting.
|
|
937
|
+
var running = p.running.filter(function (s) { return !held[keyOf(s)]; }).map(function (s) { return row(s, K.stateOf(s) === 'waiting' ? 'wait' : 'run', stateWords(s)); });
|
|
938
|
+
var done = p.done.slice(0, PET_DONE_SHOWN).map(function (s) { return row(s, 'run', 'Last active ' + doneWhen(s)); });
|
|
939
|
+
var body = $('ta-body');
|
|
940
|
+
if (body) body.innerHTML = group(questions.length === 1 ? 'A question for you' : 'Questions for you', questions) + group('Waiting on you', waiting) + group('Running', running) + group('Done today', done) +
|
|
941
|
+
(!questions.length && !waiting.length && !running.length ? '<p class="ta-note">Nothing is running or waiting on you now.</p>' : '') +
|
|
942
|
+
'<div class="ta-links"><button type="button" class="link-btn" data-ta="open-sessions">All sessions</button></div>';
|
|
943
|
+
var foot = $('ta-foot'); if (foot) { foot.innerHTML = ''; foot.hidden = false; }
|
|
944
|
+
}
|
|
945
|
+
// The held questions by session (panes/questions.js), oldest first: the words of each session's first question.
|
|
946
|
+
function petAsked() {
|
|
947
|
+
var q = W.sessionQuestions, items = q && q.itemsFor ? q.itemsFor('') : [], out = [], at = {};
|
|
948
|
+
(items || []).forEach(function (it) {
|
|
949
|
+
if (!it || !it.sessionId) return;
|
|
950
|
+
var k = T.splitSessionId(it.provider, it.sessionId), key = (k.provider + '|' + k.id).toLowerCase();
|
|
951
|
+
if (at[key] !== undefined) { out[at[key]].n++; return; }
|
|
952
|
+
var first = it.kind === 'approval' ? (it.approval && it.approval.summary ? 'Wants to run ' + it.approval.summary : 'Wants your permission')
|
|
953
|
+
: String(((it.questions || [])[0] || {}).question || 'Asked you a question');
|
|
954
|
+
at[key] = out.length;
|
|
955
|
+
out.push({ provider: k.provider, id: k.id, n: 1, line: clip(first, 160), title: it.kind === 'approval' ? 'A session wants your permission' : 'A session asked you a question' });
|
|
956
|
+
});
|
|
957
|
+
out.forEach(function (a) { if (a.n > 1) a.line = a.n + ' questions. ' + a.line; });
|
|
958
|
+
return out;
|
|
959
|
+
}
|
|
960
|
+
function doneWhen(s) {
|
|
961
|
+
var a = W.ago(s.last_activity_at || s.state_since);
|
|
962
|
+
if (!a || a === 'now') return 'just now';
|
|
963
|
+
return /( min| h)$/.test(a) ? a + ' ago' : a;
|
|
681
964
|
}
|
|
682
965
|
|
|
683
966
|
// The session's last reply, from the same read the reader makes (?turns=N, lib/agent-session-turns.ts).
|
|
684
967
|
function loadLast(r) {
|
|
685
|
-
|
|
968
|
+
var request = r.lastRead = {};
|
|
969
|
+
O.lastState = 'loading'; O.lastWhy = ''; paintLast();
|
|
686
970
|
return K.answer(K.path(r.provider, r.id, '?turns=6'), { timeoutMs: 20000 }).then(function (a) {
|
|
687
|
-
if (O.r !== r) return;
|
|
971
|
+
if (O.r !== r || r.lastRead !== request) return;
|
|
688
972
|
if (a.status === 404) { O.lastState = 'none'; O.lastWhy = 'This session is not on your Mac any more.'; paintLast(); return; }
|
|
689
973
|
if (a.status !== 200) { O.lastState = 'error'; paintLast(); return; }
|
|
690
974
|
var d = a.body || {};
|
|
691
975
|
if (!O.row) { O.row = d; paintMeta(); if (O.open && O.open.kind === 'run') setHead(K.title(d)); }
|
|
692
976
|
var turns = Array.isArray(d.recent_turns) ? d.recent_turns : [];
|
|
693
|
-
var lastA = turns.filter(function (t) { return lower(t.role) !== 'user' && String(t.text || t.content || '').trim(); }).pop();
|
|
694
|
-
O.last = lastA ? { text: String(lastA.text || lastA.content).trim(), at: lastA.at } : null;
|
|
977
|
+
var lastA = turns.filter(function (t) { return lower(t.role) !== 'user' && (String(t.text || t.content || '').trim() || (K.imageHtml && K.imageHtml(t))); }).pop();
|
|
978
|
+
O.last = lastA ? { text: String(lastA.text || lastA.content || '').trim(), at: lastA.at, outputImages: lastA.outputImages } : null;
|
|
695
979
|
O.lastState = O.last ? 'ready' : 'none';
|
|
696
980
|
O.lastWhy = '';
|
|
697
981
|
paintLast();
|
|
698
|
-
}, function () { if (O.r === r) { O.lastState = 'error'; paintLast(); } });
|
|
982
|
+
}, function () { if (O.r === r && r.lastRead === request) { O.lastState = 'error'; paintLast(); } });
|
|
699
983
|
}
|
|
700
984
|
function paintLast() {
|
|
701
985
|
var box = $('ta-last'); if (!box) return;
|
|
986
|
+
// Retain the actual reply nodes (and its open image) during refreshes and transient errors.
|
|
987
|
+
// Only a successful changed reply replaces them. Metadata changes count as changed too.
|
|
988
|
+
var signature = O.last ? JSON.stringify([O.last.text, O.last.at, K.imageHtml ? K.imageHtml(O.last) : '']) : '';
|
|
989
|
+
if (signature && box.__taLastSignature === signature) {
|
|
990
|
+
var refresh = $('ta-last-refresh');
|
|
991
|
+
if (refresh) refresh.textContent = O.lastState === 'loading' ? 'Checking for a newer reply…'
|
|
992
|
+
: O.lastState === 'error' ? 'The latest reply could not load. Showing the previous reply.'
|
|
993
|
+
: O.lastState === 'none' ? (O.lastWhy || '') : '';
|
|
994
|
+
return;
|
|
995
|
+
}
|
|
996
|
+
box.__taLastSignature = signature;
|
|
997
|
+
if (K.imagesStop) K.imagesStop(box);
|
|
702
998
|
if (O.lastState === 'loading') { box.innerHTML = '<div class="ta-last"><div class="skeleton" style="width:74%"></div><div class="skeleton" style="width:52%;margin-top:8px"></div></div>'; return; }
|
|
703
999
|
if (O.lastState === 'error') { box.innerHTML = '<p class="ta-sub">The last reply did not load. Open the session to read it.</p>'; return; }
|
|
704
1000
|
if (O.lastState === 'none') { box.innerHTML = O.lastWhy ? '<p class="ta-sub">' + esc(O.lastWhy) + '</p>' : ''; return; }
|
|
@@ -706,7 +1002,8 @@
|
|
|
706
1002
|
var at = W.toDate(O.last.at), long = O.last.text.length > 700;
|
|
707
1003
|
box.innerHTML = '<section class="ta-last" aria-label="Last reply"><p class="ta-last-head">' + esc('Last reply' + (at ? ', ' + W.clock(at) : '')) + '</p>' +
|
|
708
1004
|
'<div class="sx-md ta-last-text' + (long ? ' clamped' : '') + '" id="ta-last-text">' + K.markdown(O.last.text.slice(0, 12000)) + '</div>' +
|
|
709
|
-
(long ? '<button type="button" class="link-btn" data-ta="more">Show all of it</button>' : '') + '
|
|
1005
|
+
(long ? '<button type="button" class="link-btn" data-ta="more">Show all of it</button>' : '') + (K.imageHtml ? K.imageHtml(O.last) : '') + '<p id="ta-last-refresh" class="ta-sub" role="status"></p></section>';
|
|
1006
|
+
if (K.imagesMount) K.imagesMount(box, O.r);
|
|
710
1007
|
}
|
|
711
1008
|
|
|
712
1009
|
// Running: the same live stream the reader follows, read with its own reducer (applyFeed, sxFeedWord and friends).
|
|
@@ -721,15 +1018,17 @@
|
|
|
721
1018
|
K.feed.apply(r, e);
|
|
722
1019
|
r.live = r.live || {};
|
|
723
1020
|
if (e.kind === 'status') {
|
|
724
|
-
//
|
|
725
|
-
|
|
726
|
-
//
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
r.live.working
|
|
730
|
-
|
|
1021
|
+
// 6.99.29: the card's one state (app.js sxTruth) decides working and waiting: the agent's own
|
|
1022
|
+
// waiting still wins over a busy file (W4-r2), and the stream's echo of a Stop still cannot
|
|
1023
|
+
// reopen it, but a stale idle no longer outranks fresher working evidence.
|
|
1024
|
+
var wasWorking = !!r.live.working, wasWaiting = !!r.live.waiting;
|
|
1025
|
+
K.feed.settle(r);
|
|
1026
|
+
if (wasWorking && !r.live.working) loadLast(r);
|
|
1027
|
+
// Gone to waiting: its question may be parked for this page now, so the card reads at once (6.99.27).
|
|
1028
|
+
if (r.live.waiting && !wasWaiting && W.sessionQuestions && W.sessionQuestions.nudge) W.sessionQuestions.nudge();
|
|
731
1029
|
} else if (e.kind === 'prose' && e.text) r.live.prose = String(e.text);
|
|
732
1030
|
paintLive();
|
|
1031
|
+
paintVerdict();
|
|
733
1032
|
};
|
|
734
1033
|
es.onerror = function () {
|
|
735
1034
|
if (O.r !== r || O.stream !== es) return;
|
|
@@ -759,10 +1058,10 @@
|
|
|
759
1058
|
// Idle: the reader's line repeats the last reply, which the peek shows in full just below.
|
|
760
1059
|
var word = K.feed.word(r), detail = word === 'Idle' ? '' : K.feed.detail(f, word);
|
|
761
1060
|
html = '<div class="sx-act-state"><span class="sx-dot ' + word.toLowerCase() + '" aria-hidden="true"></span><strong>' + esc(word) + '</strong>' +
|
|
762
|
-
'<span class="sx-act-detail">' + esc(detail) + '</span><span class="sx-act-clock" id="ta-clock">' + esc(K.feed.elapsed(f)) + '</span>' +
|
|
1061
|
+
'<span class="sx-act-detail">' + esc(detail) + '</span><span class="sx-act-clock" id="ta-clock">' + esc(K.feed.elapsed(f, r)) + '</span>' +
|
|
763
1062
|
(live.working || O.stopping ? '<button type="button" class="cx-btn quiet sx-act-stop' + (O.armedAt.stop != null ? ' danger' : '') + '" data-ta="stop"' + (O.stopping ? ' disabled' : '') + '>' +
|
|
764
1063
|
(O.stopping ? 'Stopping' : O.armedAt.stop != null ? 'Tap again to stop' : 'Stop') + '</button>' : '') + '</div>' +
|
|
765
|
-
(f.prompt ? '<p class="ta-act-prompt">' + esc(
|
|
1064
|
+
(f.prompt ? '<p class="ta-act-prompt">' + esc(K.feed.promptLine(r, clip(f.prompt, 240))) + '</p>' : '') +
|
|
766
1065
|
'<div class="ta-act-tail">' + f.tools.slice(-5).map(function (t) { return '<div class="sx-act-tool">' + esc(K.feed.tool(t)) + '</div>'; }).join('') +
|
|
767
1066
|
(live.working && live.prose ? '<div class="sx-act-prose">' + esc(String(live.prose).slice(-500)) + '</div>' : '') + '</div>';
|
|
768
1067
|
// The feed stopped or was refused: say why under the last state it showed.
|
|
@@ -775,7 +1074,8 @@
|
|
|
775
1074
|
if (w === 'Working' || w === 'Waiting') {
|
|
776
1075
|
O.clock = setInterval(function () {
|
|
777
1076
|
if (O.r !== r) { clearInterval(O.clock); return; }
|
|
778
|
-
var c = $('ta-clock'); if (c) c.textContent = K.feed.elapsed(r.feed);
|
|
1077
|
+
var c = $('ta-clock'); if (c) c.textContent = K.feed.elapsed(r.feed, r);
|
|
1078
|
+
if (K.feed.gateStale(r)) K.probe(r);
|
|
779
1079
|
var rs = $('ta-run-state'); if (rs) rs.textContent = runWords();
|
|
780
1080
|
}, 1000);
|
|
781
1081
|
}
|
|
@@ -795,14 +1095,26 @@
|
|
|
795
1095
|
paintNote();
|
|
796
1096
|
},
|
|
797
1097
|
clear: function (r, text) {
|
|
798
|
-
// The Mac took it: the send is settled, and the kept draft goes with the words that were sent.
|
|
1098
|
+
// The Mac took it: the send is settled, and the kept draft goes with the words that were sent. So do the files
|
|
1099
|
+
// that went with them; any added since stay for the next message.
|
|
1100
|
+
var sent = r.draft && r.draft.text === text ? r.draft.ids || [] : [];
|
|
799
1101
|
if (r.draft && r.draft.text === text) r.draft = null;
|
|
1102
|
+
var fb = sent.length ? filesBox(r) : null, kit = KIT();
|
|
1103
|
+
if (fb && kit) fb.atts.filter(function (t) { return sent.indexOf(t.mediaId) >= 0; }).forEach(function (t) { kit.remove(fb, t.id); });
|
|
800
1104
|
var box = r === O.r ? $('ta-input') : null;
|
|
801
1105
|
var left = box ? (box.value.trim() === String(text).trim() ? '' : box.value) : '';
|
|
802
1106
|
if (box && !left) { box.value = ''; W.autoGrow(box); }
|
|
803
1107
|
keepDraft(r, left, null);
|
|
804
1108
|
},
|
|
805
1109
|
verdict: function (r) { if (r === O.r) paintVerdict(); },
|
|
1110
|
+
// 6.99.29: the card's one state moved (a gate answer, a parked send, a live delivery): repaint all of it.
|
|
1111
|
+
stateChanged: function (r) {
|
|
1112
|
+
if (r !== O.r) return;
|
|
1113
|
+
var wasWorking = !!r.live && !!r.live.working;
|
|
1114
|
+
K.feed.settle(r);
|
|
1115
|
+
if (wasWorking && !r.live.working) loadLast(r);
|
|
1116
|
+
paintLive(); paintVerdict();
|
|
1117
|
+
},
|
|
806
1118
|
hold: function (r) {
|
|
807
1119
|
if (r !== O.r) return;
|
|
808
1120
|
O.note = { text: 'COS is still answering in this session. Send again when it finishes; it will not post twice. Or open the session, where it sends on its own.', kind: 'error' };
|
|
@@ -811,7 +1123,7 @@
|
|
|
811
1123
|
watch: function (r, bindingId, key) { watchTurn(r, bindingId, key); },
|
|
812
1124
|
queue: function () {}
|
|
813
1125
|
};
|
|
814
|
-
function paintNote() { W.setStatus($('ta-status'), O.note.text, O.note.kind === 'error'); }
|
|
1126
|
+
function paintNote() { W.setStatus($('ta-status'), O.note.text, O.note.kind === 'error'); if (KB.on) fit(); }
|
|
815
1127
|
// A copy of the reader's renderVerdict (app.js): the same boxes and words, with Open session where the reader offers
|
|
816
1128
|
// Fork and Refresh, which live only in the reader.
|
|
817
1129
|
function paintVerdict() {
|
|
@@ -830,7 +1142,8 @@
|
|
|
830
1142
|
((f.changed || f.fork) ? '<button type="button" class="cx-btn quiet" data-ta="open-session">' + (f.fork ? 'Open session to fork it' : 'Open session') + '</button>' : '') +
|
|
831
1143
|
'</div></div>';
|
|
832
1144
|
} else if (v && v.attachable === false && /^(native_thread_working|native_target_busy)$/.test(v.reason)) {
|
|
833
|
-
|
|
1145
|
+
// 6.99.29: "Working right now" only while the card's one state says so.
|
|
1146
|
+
if (K.feed.parkNoteShows(r)) html = '<p class="sx-park">' + esc(K.parkCopy(r.provider)) + '</p>';
|
|
834
1147
|
} else if (v && v.attachable === false && v.reasonCopy) {
|
|
835
1148
|
html = '<div class="sx-box refusal" role="status"><p>' + esc(v.reasonCopy) + '</p></div>';
|
|
836
1149
|
} else if (v && v.caution) {
|
|
@@ -838,16 +1151,26 @@
|
|
|
838
1151
|
: 'Another app on this Mac has this session open. COS asks before the first send.') + '</p>';
|
|
839
1152
|
}
|
|
840
1153
|
box.innerHTML = html;
|
|
1154
|
+
if (KB.on) fit(); // the reply box's height changed: the conversation's room is measured again
|
|
841
1155
|
}
|
|
842
1156
|
function setSending(on) {
|
|
843
|
-
var
|
|
844
|
-
if (send) { send.disabled = on; send.textContent = on ? 'Sending' : 'Send'; }
|
|
1157
|
+
var input = $('ta-input');
|
|
845
1158
|
if (input) input.readOnly = on;
|
|
1159
|
+
paintSendButton(); paintClip();
|
|
846
1160
|
}
|
|
847
1161
|
function sendReply(opts) {
|
|
848
1162
|
var r = O.r; if (!r) return;
|
|
849
1163
|
var input = $('ta-input'), text = input ? input.value.trim() : '';
|
|
850
1164
|
if (!text) { O.note = { text: 'Write a message first.', kind: 'error' }; paintNote(); return; }
|
|
1165
|
+
// Files: never before every upload has finished, and never with one that failed.
|
|
1166
|
+
var box = filesOn() ? filesBox(r) : null, st = filesState(box);
|
|
1167
|
+
if (st === 'waiting') { O.note = { text: 'A file is still uploading. Send when it finishes.', kind: '' }; paintNote(); return; }
|
|
1168
|
+
if (st === 'failed') { O.note = { text: 'A file did not upload. Retry it or remove it.', kind: 'error' }; paintNote(); return; }
|
|
1169
|
+
var ids = st === 'ready' ? KIT().ids(box) : [];
|
|
1170
|
+
// A send with files cut off by a reload: the chips are gone, the draft kept their ids. The same words go again with
|
|
1171
|
+
// the same files under the same key, so a send that did land is answered, never repeated.
|
|
1172
|
+
if (!ids.length && r.draft && r.draft.restored && r.draft.text === text && r.draft.ids && r.draft.ids.length) ids = r.draft.ids.slice();
|
|
1173
|
+
if (ids.length) opts = Object.assign({}, opts || {}, { attachmentIds: ids });
|
|
851
1174
|
// Control's caution: another app holds this session and looks idle, so the first send asks.
|
|
852
1175
|
if (r.verdict && r.verdict.caution && !r.cautionAck) { O.cautionAsk = true; O.armedAt = { caution: Date.now() }; paintVerdict(); return; }
|
|
853
1176
|
if (r.sending) return;
|
|
@@ -881,7 +1204,7 @@
|
|
|
881
1204
|
var b = c.body || {};
|
|
882
1205
|
O.stopKey = '';
|
|
883
1206
|
O.note = { text: b.reasonCopy || (b.cancelled ? 'Stopped.' : 'COS could not stop it.'), kind: b.cancelled ? '' : 'error' };
|
|
884
|
-
if (b.cancelled) { r.stoppedAt = Date.now();
|
|
1207
|
+
if (b.cancelled) { r.stoppedAt = Date.now(); K.feed.settle(r); O.dirty = true; paintLive(); paintVerdict(); setTimeout(function () { if (O.r === r) loadLast(r); }, 1500); }
|
|
885
1208
|
paintNote(); paintLive();
|
|
886
1209
|
}, function (err) {
|
|
887
1210
|
O.stopping = false;
|
|
@@ -900,7 +1223,13 @@
|
|
|
900
1223
|
K.answer('/api/agent-sessions/bindings/' + encodeURIComponent(bindingId) + '/turns/' + encodeURIComponent(key)).then(function (s) {
|
|
901
1224
|
if (O.r !== r) return;
|
|
902
1225
|
var o = s.body && s.body.outcome;
|
|
903
|
-
if (o === 'completed') {
|
|
1226
|
+
if (o === 'completed') {
|
|
1227
|
+
clearInterval(O.watch);
|
|
1228
|
+
// 6.99.29: a turn delivered LIVE is in the open session, which now works on it; it is not done.
|
|
1229
|
+
if (s.body.via === 'live') { K.feed.noteGate(r, true); O.note = { text: 'Sent into the open session. It is working on it now.', kind: '' }; }
|
|
1230
|
+
else { O.note = { text: 'Done. The reply is below.', kind: '' }; loadLast(r); }
|
|
1231
|
+
paintNote(); VIEW.stateChanged(r);
|
|
1232
|
+
}
|
|
904
1233
|
else if (o === 'refused') { clearInterval(O.watch); O.note = { text: s.body.reasonCopy || 'The session refused that message.', kind: 'error' }; paintNote(); }
|
|
905
1234
|
else if (o === 'ambiguous') { clearInterval(O.watch); O.note = { text: s.body.reasonCopy || 'COS cannot tell whether that landed. Look at the session before sending it again.', kind: 'error' }; paintNote(); }
|
|
906
1235
|
}).catch(function () {});
|
|
@@ -924,7 +1253,9 @@
|
|
|
924
1253
|
lines.push(rows.length ? 'Needs you (' + rows.length + (later ? ', ' + later + ' of them can wait until tomorrow' : '') + '):'
|
|
925
1254
|
: m && !m.settled ? 'Needs you: nothing found so far.' : 'Needs you: nothing.');
|
|
926
1255
|
rows.slice(0, 6).forEach(function (r) {
|
|
927
|
-
|
|
1256
|
+
// 6.99.26: where a waiting session can be answered, as the row says it.
|
|
1257
|
+
var where = r.where === 'here' ? ' It can be answered from Today.' : r.where === 'mac' ? ' It waits on the Mac\u2019s own prompt.' : r.where === 'answered' ? ' It was just answered.' : '';
|
|
1258
|
+
if (r.kind !== 'work') { lines.push('- ' + end(clip(r.title, 140)) + (r.why ? ' ' + end(clip(r.why, 160)) : '') + where); return; }
|
|
928
1259
|
var it = r.item;
|
|
929
1260
|
var title = str(it.sessionTitle) || str(it.title) || 'Handoff';
|
|
930
1261
|
var bits = [end(clip(title, 120)) + ' ' + end(needLine(it))];
|
|
@@ -967,6 +1298,6 @@
|
|
|
967
1298
|
open: open, close: close, restore: restore, summary: summary, summaryMax: SUMMARY_MAX,
|
|
968
1299
|
isOpen: function () { return !!O.open; },
|
|
969
1300
|
_state: O, _decide: decide, _undo: undo, _intakeHtml: intakeHtml, _send: sendReply, _view: VIEW, _paintVerdict: paintVerdict,
|
|
970
|
-
_paintLive: paintLive, _paintMeta: paintMeta, _onClick: onClick, _expire: expireLater
|
|
1301
|
+
_paintLive: paintLive, _paintMeta: paintMeta, _onClick: onClick, _expire: expireLater, _kb: KB, _fitNow: applyFit
|
|
971
1302
|
});
|
|
972
1303
|
})();
|