@gotcos/glasses-server 6.99.25 → 6.99.33
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 +224 -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 +42 -9
- package/server/web-control/app.js +975 -114
- package/server/web-control/control.css +12 -6
- package/server/web-control/index.html +6 -1
- package/server/web-control/panes/cos-chat.css +27 -3
- package/server/web-control/panes/cos-chat.js +240 -46
- package/server/web-control/panes/meetings.css +5 -2
- package/server/web-control/panes/meetings.js +21 -10
- package/server/web-control/panes/memories.css +1 -1
- package/server/web-control/panes/memories.js +3 -8
- package/server/web-control/panes/questions.css +56 -2
- package/server/web-control/panes/questions.js +454 -56
- package/server/web-control/panes/reader-media.js +154 -0
- package/server/web-control/panes/sessions.css +20 -7
- package/server/web-control/panes/settings.js +34 -2
- package/server/web-control/panes/speakers.css +3 -5
- package/server/web-control/panes/speakers.js +7 -4
- package/server/web-control/panes/threads.js +3 -2
- package/server/web-control/panes/today-actions.css +81 -3
- package/server/web-control/panes/today-actions.js +527 -62
- package/server/web-control/panes/work.css +1 -1
- package/server/web-control/shell.js +3 -0
|
@@ -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
|
-
pushed: false, ignorePops: 0,
|
|
78
|
+
pushed: false, ignorePops: 0, pops: [], // pops: our own Back steps still to arrive (quietBack)
|
|
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>' +
|
|
@@ -127,21 +144,164 @@
|
|
|
127
144
|
layer.addEventListener('click', onClick);
|
|
128
145
|
layer.addEventListener('submit', onSubmit);
|
|
129
146
|
layer.addEventListener('focusin', onFocusIn);
|
|
147
|
+
// Focus leaving a field (the keyboard's Hide key, a tap outside) refits on the next frame, once it has moved.
|
|
148
|
+
layer.addEventListener('focusout', function () { fit(); });
|
|
130
149
|
layer.addEventListener('input', function (e) {
|
|
131
150
|
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
151
|
});
|
|
152
|
+
// Files: the clip, a pasted screenshot in the box, and files dropped on the reply box.
|
|
153
|
+
layer.addEventListener('change', function (e) {
|
|
154
|
+
if (e.target && e.target.hasAttribute && e.target.hasAttribute('data-ta-file')) { addFiles(e.target.files); e.target.value = ''; }
|
|
155
|
+
});
|
|
156
|
+
layer.addEventListener('paste', function (e) {
|
|
157
|
+
if (!e.target || e.target.id !== 'ta-input' || !filesOn()) return;
|
|
158
|
+
var files = KIT().pasted(e);
|
|
159
|
+
if (files) { e.preventDefault(); addFiles(files); }
|
|
160
|
+
});
|
|
161
|
+
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(function (type) {
|
|
162
|
+
layer.addEventListener(type, function (e) {
|
|
163
|
+
var form = $('ta-form');
|
|
164
|
+
if (!form || !filesOn() || !e.target || !e.target.closest || !e.target.closest('#ta-form')) return;
|
|
165
|
+
var files = KIT().dragged(e, form);
|
|
166
|
+
if (files) addFiles(files);
|
|
167
|
+
});
|
|
168
|
+
});
|
|
133
169
|
// A drag on the dimmed page never scrolls what is behind it.
|
|
134
|
-
|
|
170
|
+
// The bleed below the bottom edge is the layer's own ::before (today-actions.css): a drag there is the layer's.
|
|
171
|
+
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
172
|
return layer;
|
|
136
173
|
}
|
|
137
|
-
//
|
|
174
|
+
// 6.99.30 (Miles's Duo, in Comet as well as Safari): the layer is ALWAYS the visible part of the page, read the one
|
|
175
|
+
// way Control Web reads it (app.js COSWeb.viewportFit): top and height from visualViewport, 100dvh where there is
|
|
176
|
+
// none. Nothing assumes a browser: whatever covers the bottom (an on-screen keyboard, a hardware keyboard's strip,
|
|
177
|
+
// Comet's or any browser's bottom bar) is what the browser reports, and the reply box, Send and a question card's
|
|
178
|
+
// Submit stay above it. Only the backdrop and the sheet's own colour reach on down to the physical bottom
|
|
179
|
+
// (--ta-below plus the bleed, today-actions.css), so no band of Today shows under a bar that lets the page through.
|
|
180
|
+
// (6.99.29 made the closed-keyboard layer the whole layout viewport to cover IMG_0503's 106 px band; that band may be
|
|
181
|
+
// Comet's bottom bar, real covered space, and the composer must not sit under it.)
|
|
182
|
+
// The layout on top of that box:
|
|
183
|
+
// ta-kb an on-screen keyboard: what covers the bottom is KB_MIN_PX or more. The header folds to one
|
|
184
|
+
// compact row, the sheet fills the box, the reply box stays pinned at its bottom, and the
|
|
185
|
+
// conversation takes what is left. Under about three lines left, ta-kb-tight also hides the
|
|
186
|
+
// "Working right now" note until the keyboard closes (or the screen turns).
|
|
187
|
+
// ta-kb-strip a field in the sheet has focus and something under KB_MIN_PX covers the bottom (a hardware
|
|
188
|
+
// keyboard's 55 to 70 px strip): the header stays as it is; the strip covers the home indicator,
|
|
189
|
+
// so the reply box drops that inset.
|
|
190
|
+
// KB_MIN_PX applies whether or not a field has focus: a bottom bar under it (Comet's folded bar measured about 110 px)
|
|
191
|
+
// never folds the header, one of 120 px or more does. A strip counts from KB_STRIP_MIN_PX, so desktop zoom or a
|
|
192
|
+
// scrollbar (1 to 17 px of rounding, QA 6.99.30) never reads as a keyboard strip. Read on visualViewport resize and scroll while the overlay is open, and on window resize and
|
|
193
|
+
// orientationchange, pageshow, the page coming back into view, and focus moving in the sheet: at most once a frame.
|
|
194
|
+
var KB_MIN_PX = 120;
|
|
195
|
+
var KB_STRIP_MIN_PX = 24;
|
|
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 };
|
|
198
|
+
function tall(el) { return (el && el.offsetHeight) || 0; }
|
|
199
|
+
// What the conversation gets once the compact header and the reply box have theirs.
|
|
200
|
+
function bodyRoom(height) {
|
|
201
|
+
var sheet = $('ta-sheet'), grip = sheet && sheet.querySelector ? sheet.querySelector('.ta-grip') : null;
|
|
202
|
+
return height - tall($('ta-head')) - tall($('ta-foot')) - tall(grip);
|
|
203
|
+
}
|
|
204
|
+
// A field that types (the reply box, a question card's Other) has focus inside the sheet.
|
|
205
|
+
function typing() {
|
|
206
|
+
var a = document.activeElement;
|
|
207
|
+
if (!a || !layer || a === layer || !layer.contains || !layer.contains(a)) return false;
|
|
208
|
+
var tag = String(a.tagName || '').toUpperCase();
|
|
209
|
+
if (tag === 'TEXTAREA' || tag === 'SELECT') return true;
|
|
210
|
+
if (tag === 'INPUT') return !/^(checkbox|radio|button|submit|reset|range|color|file|image|hidden)$/i.test(String(a.type || 'text'));
|
|
211
|
+
return !!a.isContentEditable;
|
|
212
|
+
}
|
|
213
|
+
function viewport() { return W.viewportFit ? W.viewportFit() : null; }
|
|
214
|
+
function crumb(what, v, focus) {
|
|
215
|
+
if (!W.layoutCrumb || !O.open || !v) return;
|
|
216
|
+
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: !phone() });
|
|
218
|
+
}
|
|
219
|
+
function applyFit() {
|
|
220
|
+
KB.frame = 0;
|
|
221
|
+
if (!layer || !O.open) return null;
|
|
222
|
+
var v = viewport();
|
|
223
|
+
if (!v) return null;
|
|
224
|
+
layer.style.setProperty('--ta-vh', v.height + 'px');
|
|
225
|
+
layer.style.setProperty('--ta-vt', v.top + 'px');
|
|
226
|
+
layer.style.setProperty('--ta-below', v.below + 'px');
|
|
227
|
+
var focus = typing(), on = v.inset >= KB_MIN_PX, strip = !on && focus && v.inset >= KB_STRIP_MIN_PX;
|
|
228
|
+
// A turned screen measures the room again: what was tight in landscape is not in portrait (QA 6.99.29).
|
|
229
|
+
var w = window.innerWidth || 0, turned = !!(KB.width && w && w !== KB.width);
|
|
230
|
+
if (w) KB.width = w;
|
|
231
|
+
var was = KB.on + '|' + KB.strip + '|' + KB.tight;
|
|
232
|
+
if (turned) KB.tight = false;
|
|
233
|
+
// A rise is what covers the bottom growing, by any amount, while the keyboard layout is on: not a class flip (a
|
|
234
|
+
// 120 px bar can already hold ta-kb before the keyboard comes, QA 6.99.32 Skeptic N-3), and not a jump past a step
|
|
235
|
+
// (a keyboard animating up in 20 px frames never crossed one, Codex's root regression on 98f3cde). The reveal is
|
|
236
|
+
// scrollIntoView 'nearest', so a frame where the field is already in view moves nothing. Shrinking never reveals.
|
|
237
|
+
var rose = (on || strip) && v.inset > (KB.inset || 0);
|
|
238
|
+
KB.inset = v.inset;
|
|
239
|
+
if (on !== KB.on) { KB.on = on; KB.tight = false; layer.classList.toggle('ta-kb', on); }
|
|
240
|
+
if (strip !== KB.strip) { KB.strip = strip; layer.classList.toggle('ta-kb-strip', strip); }
|
|
241
|
+
// 6.99.32 (QA 6.99.30 canary W2): the keyboard rises after the focus, so onFocusIn's scroll (250 ms) can land before
|
|
242
|
+
// the layer shrinks and leave a question card's Other field under the keyboard. Once the layout has the keyboard,
|
|
243
|
+
// bring the focused field into view again.
|
|
244
|
+
if (rose && focus) revealFocused();
|
|
245
|
+
// Measured with the compact header in place. Once tight it stays so while the keyboard is up: showing the note
|
|
246
|
+
// again would take the room back and flicker.
|
|
247
|
+
if (on && !KB.tight) KB.tight = bodyRoom(v.height) < BODY_MIN_PX;
|
|
248
|
+
layer.classList.toggle('ta-kb-tight', on && KB.tight);
|
|
249
|
+
if (was !== KB.on + '|' + KB.strip + '|' + KB.tight) crumb('kb', v, focus);
|
|
250
|
+
return v;
|
|
251
|
+
}
|
|
252
|
+
function revealFocused() {
|
|
253
|
+
// What has focus when the frame runs, not when it was asked for (QA 6.99.32 Skeptic N-2).
|
|
254
|
+
var go = function () {
|
|
255
|
+
if (!O.open || !typing()) return;
|
|
256
|
+
var a = document.activeElement, f = a && a.id === 'ta-input' ? $('ta-form') : a;
|
|
257
|
+
if (f && f.scrollIntoView) f.scrollIntoView({ block: 'nearest' });
|
|
258
|
+
};
|
|
259
|
+
// The next frame, so the scroll measures the layout with ta-kb (or ta-kb-strip) applied.
|
|
260
|
+
if (window.requestAnimationFrame) window.requestAnimationFrame(go); else go();
|
|
261
|
+
}
|
|
138
262
|
function fit() {
|
|
263
|
+
if (KB.frame || !O.open) return;
|
|
264
|
+
if (!window.requestAnimationFrame) return applyFit(); // every browser with visualViewport has frames
|
|
265
|
+
KB.frame = window.requestAnimationFrame(applyFit);
|
|
266
|
+
}
|
|
267
|
+
function listenViewport(on) {
|
|
139
268
|
var vv = window.visualViewport;
|
|
140
|
-
if (!
|
|
141
|
-
|
|
142
|
-
|
|
269
|
+
if (!vv || KB.listening === on) return;
|
|
270
|
+
KB.listening = on;
|
|
271
|
+
var m = on ? 'addEventListener' : 'removeEventListener';
|
|
272
|
+
if (typeof vv[m] === 'function') { vv[m]('resize', fit); vv[m]('scroll', fit); }
|
|
143
273
|
}
|
|
144
|
-
|
|
274
|
+
function unfit() {
|
|
275
|
+
listenViewport(false);
|
|
276
|
+
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;
|
|
278
|
+
if (!layer) return;
|
|
279
|
+
layer.classList.remove('ta-kb'); layer.classList.remove('ta-kb-strip'); layer.classList.remove('ta-kb-tight');
|
|
280
|
+
}
|
|
281
|
+
// The rest of the moments the visible viewport can change without a visualViewport event of its own (QA 6.99.29: the
|
|
282
|
+
// layer stayed sized for a keyboard that had gone). Each is a no-op while the overlay is closed.
|
|
283
|
+
window.addEventListener('resize', function () { fit(); });
|
|
284
|
+
window.addEventListener('orientationchange', function () { if (O.open) { KB.tight = false; fit(); } });
|
|
285
|
+
document.addEventListener('visibilitychange', function () {
|
|
286
|
+
if (document.hidden || !O.open) return;
|
|
287
|
+
fit();
|
|
288
|
+
if (O.r && O.open.kind === 'run') paintLive(); // the clock and the state, at once rather than on the next tick
|
|
289
|
+
});
|
|
290
|
+
// Back from the back-forward cache (a swipe back, the app switcher): the live stream is closed and everything shown
|
|
291
|
+
// is as old as the visit away. Open the stream again and read the session again.
|
|
292
|
+
window.addEventListener('pageshow', function (e) {
|
|
293
|
+
if (!O.open) return;
|
|
294
|
+
fit();
|
|
295
|
+
if (e && e.persisted) revive();
|
|
296
|
+
});
|
|
297
|
+
// 6.99.30 (QA): the Mac going away or coming back shows in an open sheet at once, not after the stream gives up: the
|
|
298
|
+
// live line says the Mac is not answering, and back up the stream opens again and the session is read again.
|
|
299
|
+
if (W.onServer) W.onServer(function (up) {
|
|
300
|
+
var r = O.r;
|
|
301
|
+
if (!O.open || !r) return;
|
|
302
|
+
if (!up) { r.feed.connected = false; r.feed.fallback = 'unreachable'; if (r.live) r.live.working = false; paintLive(); paintVerdict(); }
|
|
303
|
+
else revive();
|
|
304
|
+
});
|
|
145
305
|
|
|
146
306
|
function openerSelector(t) {
|
|
147
307
|
if (!t || !t.getAttribute) return '';
|
|
@@ -151,9 +311,14 @@
|
|
|
151
311
|
return '[data-today=' + q(kind) + ']' + (key != null ? '[data-key=' + q(key) + ']' : '');
|
|
152
312
|
}
|
|
153
313
|
|
|
154
|
-
|
|
314
|
+
// opt.from: a session opened from the pet's list ({ scroll }), which gets Back to sessions and its own history entry,
|
|
315
|
+
// so the phone's Back returns to the list too (Miles's iPhone, 11/07). opt.again: the list shown again from there,
|
|
316
|
+
// its entry already current.
|
|
317
|
+
function open(kind, key, opener, opt) {
|
|
318
|
+
opt = opt || {};
|
|
155
319
|
build();
|
|
156
320
|
if (O.open) stopSession();
|
|
321
|
+
O.back = opt.from || null;
|
|
157
322
|
O.open = { kind: kind, key: String(key || ''), all: kind === 'intake' };
|
|
158
323
|
O.opener = opener || document.activeElement; O.openerSel = openerSelector(opener);
|
|
159
324
|
O.note = { text: '', kind: '' }; O.cautionAsk = false; O.needGone = false; O.armedAt = {};
|
|
@@ -161,14 +326,20 @@
|
|
|
161
326
|
layer.hidden = false;
|
|
162
327
|
document.documentElement.classList.add('ta-open');
|
|
163
328
|
var shell = $('app-shell'); if (shell) shell.setAttribute('inert', '');
|
|
164
|
-
|
|
329
|
+
listenViewport(true); applyFit();
|
|
165
330
|
// The entry names what is open, so a reload can open it again and Back still closes it (restore below).
|
|
166
331
|
var entry = { cosTodayOverlay: 1, kind: kind, key: O.open.key };
|
|
332
|
+
if (O.back) entry.from = 'pet';
|
|
167
333
|
try {
|
|
168
|
-
if (
|
|
334
|
+
if (opt.again) { /* the list's own entry is current again */ }
|
|
335
|
+
else if (!O.pushed) { history.pushState(entry, ''); O.pushed = true; O.depth = 1; }
|
|
336
|
+
else if (opt.from && O.depth === 1) { history.pushState(entry, ''); O.depth = 2; }
|
|
337
|
+
else history.replaceState(entry, '');
|
|
169
338
|
} catch (_e) { /* no history API: Back simply leaves Today */ }
|
|
339
|
+
var backBtn = $('ta-back'); if (backBtn) backBtn.hidden = !O.back;
|
|
170
340
|
try {
|
|
171
341
|
if (kind === 'need' || kind === 'run' || kind === 'wait') openSession(kind, O.open.key);
|
|
342
|
+
else if (kind === 'pet') openPet();
|
|
172
343
|
else openIntake();
|
|
173
344
|
} catch (err) {
|
|
174
345
|
// A bug here must not leave a half-drawn sheet over an inert page: take it down, keep the history entry for
|
|
@@ -179,6 +350,8 @@
|
|
|
179
350
|
var sheet = $('ta-sheet');
|
|
180
351
|
O.settleAt = Date.now() + settleMs(sheet);
|
|
181
352
|
if (sheet && sheet.focus) sheet.focus({ preventScroll: true });
|
|
353
|
+
// Measured once the sheet is drawn, and sent home as a breadcrumb (app.js layoutCrumb, 6.99.30).
|
|
354
|
+
crumb('open', applyFit(), typing());
|
|
182
355
|
}
|
|
183
356
|
// How long taps inside are ignored after opening: the longest open motion of the sheet and its backdrop (their computed
|
|
184
357
|
// animation and transition, delay included: today-actions.css ta-rise or ta-slide, 220 ms, and fade, 160 ms), plus
|
|
@@ -212,9 +385,15 @@
|
|
|
212
385
|
stopSession();
|
|
213
386
|
O.open = null;
|
|
214
387
|
if (layer) layer.hidden = true;
|
|
388
|
+
unfit();
|
|
215
389
|
document.documentElement.classList.remove('ta-open');
|
|
216
390
|
var shell = $('app-shell'); if (shell) shell.removeAttribute('inert');
|
|
217
|
-
|
|
391
|
+
// X from a session opened off the pet's list unwinds both entries in one step, so the next Back leaves Today as before.
|
|
392
|
+
if (O.pushed && how !== 'pop' && how !== 'leave') {
|
|
393
|
+
var n = O.depth > 1 ? O.depth : 1;
|
|
394
|
+
O.pushed = false;
|
|
395
|
+
quietBack(n);
|
|
396
|
+
}
|
|
218
397
|
if (how === 'pop' || how === 'leave') O.pushed = false;
|
|
219
398
|
clearTimeout(O.expireTimer);
|
|
220
399
|
// Settled lines are done once the overlay closes; Today reads the server again if anything changed here.
|
|
@@ -231,17 +410,35 @@
|
|
|
231
410
|
if (el && el.focus) { try { el.focus({ preventScroll: true }); } catch (_e) { el.focus(); } }
|
|
232
411
|
}
|
|
233
412
|
// Leaving for a tab from inside the overlay replaces the overlay's history entry, so Back from there returns to Today.
|
|
413
|
+
// A session opened off the pet's list holds two entries: the list's comes off first (6.99.30, QA: it stayed under
|
|
414
|
+
// the new tab, and Back from there landed on it and showed nothing), then the tab replaces the overlay's own.
|
|
234
415
|
function leaveTo(pane, args) {
|
|
235
416
|
var hash = '#/' + [pane].concat(args || []).map(encodeURIComponent).join('/');
|
|
236
|
-
var replace = O.pushed;
|
|
417
|
+
var replace = O.pushed, extra = O.pushed && O.depth > 1 ? O.depth - 1 : 0;
|
|
237
418
|
close('leave');
|
|
238
|
-
|
|
419
|
+
O.depth = 1;
|
|
420
|
+
if (!replace) { W.go(pane, args || []); return; }
|
|
421
|
+
if (extra) { quietBack(extra, function () { location.replace(hash); }); return; }
|
|
422
|
+
location.replace(hash);
|
|
423
|
+
}
|
|
424
|
+
// A Back step the overlay takes itself (X, Back to sessions, leaving for a tab): its popstate is ours, not the phone's
|
|
425
|
+
// Back, and `then` runs once it lands. If it never arrives (history.go with nothing to go back to, or a browser that
|
|
426
|
+
// drops it) the wait lapses after POP_WAIT_MS and `then` runs anyway, so a real Back later is never swallowed
|
|
427
|
+
// (6.99.30, QA: ignorePops stayed counted and ate the next Back).
|
|
428
|
+
var POP_WAIT_MS = 700;
|
|
429
|
+
function quietBack(n, then) {
|
|
430
|
+
var w = { then: then || null, timer: 0 };
|
|
431
|
+
var drop = function () { var i = O.pops.indexOf(w); if (i < 0) return false; O.pops.splice(i, 1); O.ignorePops = O.pops.length; return true; };
|
|
432
|
+
w.timer = setTimeout(function () { if (drop() && w.then) w.then(); }, POP_WAIT_MS);
|
|
433
|
+
O.pops.push(w); O.ignorePops = O.pops.length;
|
|
434
|
+
try { if (n > 1 && typeof history.go === 'function') history.go(-n); else history.back(); }
|
|
435
|
+
catch (_e) { clearTimeout(w.timer); if (drop() && w.then) w.then(); }
|
|
239
436
|
}
|
|
240
437
|
// A reload with the overlay open keeps its history entry, and the entry before it is Today at the same address, so
|
|
241
438
|
// Back from there showed nothing (measured in Chrome 11/07: a same-document popstate). On load, app.js hands that
|
|
242
439
|
// entry here: open the same thing again, so the first Back closes it as it did before the reload. What cannot be
|
|
243
440
|
// 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 };
|
|
441
|
+
var KINDS = { need: 1, run: 1, wait: 1, pet: 1, meeting: 1, intake: 1 };
|
|
245
442
|
function restore(st) {
|
|
246
443
|
if (O.open || !st || !st.cosTodayOverlay) return Promise.resolve(false);
|
|
247
444
|
var forget = function () { try { history.replaceState(null, ''); } catch (_e) {} return false; };
|
|
@@ -249,23 +446,36 @@
|
|
|
249
446
|
var started = Date.now();
|
|
250
447
|
return new Promise(function (resolve) {
|
|
251
448
|
(function wait() {
|
|
252
|
-
var d = T.data(), ready = d.at > 0 && (st.kind === 'need' || st.kind === 'run' || st.kind === 'wait' || d.intakeLoaded);
|
|
449
|
+
var d = T.data(), ready = d.at > 0 && (st.kind === 'need' || st.kind === 'run' || st.kind === 'wait' || st.kind === 'pet' || d.intakeLoaded);
|
|
253
450
|
if (W.parseRoute().pane !== 'today' || O.open) return resolve(forget());
|
|
254
451
|
if (!ready) { if (Date.now() - started > 15000) return resolve(forget()); return setTimeout(wait, 150); }
|
|
255
452
|
O.pushed = true; // this entry is the overlay's own
|
|
256
|
-
|
|
453
|
+
// A session opened off the pet's list keeps its Back to sessions: the list's entry is still under it.
|
|
454
|
+
if (st.from === 'pet' && (st.kind === 'wait' || st.kind === 'run')) O.depth = 2;
|
|
455
|
+
try { open(st.kind, String(st.key || ''), null, st.from === 'pet' && O.depth === 2 ? { from: { scroll: 0 }, again: true } : undefined); resolve(true); }
|
|
257
456
|
catch (err) { console.error('[today] could not reopen the overlay after a reload.', err); O.pushed = false; resolve(forget()); }
|
|
258
457
|
})();
|
|
259
458
|
});
|
|
260
459
|
}
|
|
261
460
|
window.addEventListener('popstate', function () {
|
|
262
|
-
if (O.
|
|
461
|
+
if (O.pops.length) { var w = O.pops.shift(); clearTimeout(w.timer); O.ignorePops = O.pops.length; if (w.then) w.then(); return; }
|
|
462
|
+
// The phone's Back from a session opened off the pet's list goes back to that list, as Back to sessions does.
|
|
463
|
+
if (O.open && O.back) { backToList(); return; }
|
|
263
464
|
if (O.open) close('pop');
|
|
264
465
|
});
|
|
466
|
+
// The pet's list again, where it was scrolled to. From the button the session's entry is taken off history too.
|
|
467
|
+
function backToList(fromButton) {
|
|
468
|
+
var b = O.back;
|
|
469
|
+
if (!b) return;
|
|
470
|
+
if (fromButton && O.depth > 1) quietBack(1);
|
|
471
|
+
O.depth = 1;
|
|
472
|
+
open('pet', '', O.opener, { again: true });
|
|
473
|
+
var body = $('ta-body'); if (body) body.scrollTop = b.scroll || 0;
|
|
474
|
+
}
|
|
265
475
|
W.onRoute(function (r) { if (O.open && r.pane !== 'today') close('leave'); });
|
|
266
476
|
// Escape closes the overlay, unless the Ask sheet is open over it (app.js closes that first).
|
|
267
477
|
document.addEventListener('keydown', function (e) {
|
|
268
|
-
if (!O.open || e.key !== 'Escape') return;
|
|
478
|
+
if (!O.open || e.key !== 'Escape' || (W.readerMedia && W.readerMedia.isOpen())) return;
|
|
269
479
|
var ask = $('ask-sheet');
|
|
270
480
|
if (ask && !ask.classList.contains('hidden')) return;
|
|
271
481
|
e.preventDefault();
|
|
@@ -295,6 +505,7 @@
|
|
|
295
505
|
if (O.open && (O.open.kind === 'meeting' || O.open.kind === 'intake')) paintIntake(); else paintLive();
|
|
296
506
|
}
|
|
297
507
|
if (what === 'close') return close('user');
|
|
508
|
+
if (what === 'back') return backToList(true);
|
|
298
509
|
if (what === 'create' || what === 'link') return decide(id, 'accept');
|
|
299
510
|
if (what === 'dismiss') {
|
|
300
511
|
// Without Undo on this server a Dismiss cannot be taken back, so it asks once: tap, then tap again.
|
|
@@ -306,7 +517,20 @@
|
|
|
306
517
|
if (what === 'open-work') return leaveTo('work', ['sort']);
|
|
307
518
|
if (what === 'open-card') return leaveTo('work', id ? [id] : []);
|
|
308
519
|
if (what === 'open-session') return O.r ? leaveTo('sessions', [O.r.provider, O.r.id]) : null;
|
|
520
|
+
if (what === 'open-sessions') return leaveTo('sessions', []);
|
|
521
|
+
// 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
|
+
// returning to the list where it was scrolled; X still closes the overlay.
|
|
523
|
+
if (what === 'pet-open') {
|
|
524
|
+
var pk = b.getAttribute('data-kind'), lb = $('ta-body');
|
|
525
|
+
return open(pk === 'wait' ? 'wait' : 'run', id, O.opener, { from: { scroll: lb ? lb.scrollTop || 0 : 0 } });
|
|
526
|
+
}
|
|
309
527
|
if (what === 'send') return sendReply();
|
|
528
|
+
if (what === 'att-remove' || what === 'att-retry') {
|
|
529
|
+
var fb = filesBox(); if (!fb) return;
|
|
530
|
+
O.note = { text: '', kind: '' }; paintNote();
|
|
531
|
+
if (what === 'att-remove') KIT().remove(fb, b.getAttribute('data-att')); else KIT().retry(fb, b.getAttribute('data-att'));
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
310
534
|
if (what === 'stop') {
|
|
311
535
|
// A tap arms it (Stop ends the agent's turn on your Mac); the second sends it.
|
|
312
536
|
if (!confirmTap('stop', paintLive)) return;
|
|
@@ -325,10 +549,18 @@
|
|
|
325
549
|
function onSubmit(e) {
|
|
326
550
|
if (e.target && e.target.id === 'ta-form') { e.preventDefault(); sendReply(); }
|
|
327
551
|
}
|
|
328
|
-
// The keyboard rises over the bottom of the sheet: keep the box in view as it does.
|
|
552
|
+
// The keyboard rises over the bottom of the sheet: keep the box in view as it does. Any field that types counts
|
|
553
|
+
// (the reply box, a question card's Other): a hardware keyboard's strip is known only by focus (6.99.30).
|
|
329
554
|
function onFocusIn(e) {
|
|
330
|
-
if (!e.target ||
|
|
331
|
-
|
|
555
|
+
if (!e.target || !typing()) return;
|
|
556
|
+
fit();
|
|
557
|
+
var reply = e.target.id === 'ta-input';
|
|
558
|
+
setTimeout(function () {
|
|
559
|
+
if (!O.open) return;
|
|
560
|
+
applyFit();
|
|
561
|
+
var f = reply ? $('ta-form') : e.target;
|
|
562
|
+
if (f && f.scrollIntoView) f.scrollIntoView({ block: 'nearest' });
|
|
563
|
+
}, 250);
|
|
332
564
|
}
|
|
333
565
|
|
|
334
566
|
// ═════════════ From today's meetings ═════════════
|
|
@@ -359,7 +591,8 @@
|
|
|
359
591
|
probeLoop().then(function () { if (O.open && (O.open.kind === 'meeting' || O.open.kind === 'intake')) paintIntake(); });
|
|
360
592
|
paintIntake();
|
|
361
593
|
var foot = $('ta-foot');
|
|
362
|
-
|
|
594
|
+
// A waiting sheet opened before may have hidden the foot (its reply box); this one always shows (QA round 1).
|
|
595
|
+
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
596
|
// Read Intake again: what was decided on another screen since Today loaded shows as handled, never as a phantom.
|
|
364
597
|
var asked = T.reloadIntake();
|
|
365
598
|
if (asked && asked.then) asked.then(function () { if (O.open && (O.open.kind === 'meeting' || O.open.kind === 'intake')) { noteVanished(); paintIntake(); } });
|
|
@@ -562,37 +795,84 @@
|
|
|
562
795
|
O.row = id ? sessionRowFor(provider, id) : null;
|
|
563
796
|
O.r = id ? K.session(provider, id, VIEW) : null;
|
|
564
797
|
// Working only when the session is not waiting on you: a session paused on a question is not at work (W4-r2).
|
|
565
|
-
|
|
798
|
+
// 6.99.29: the row is one signal of the card's one state (app.js sxTruth), timed by its own evidence.
|
|
799
|
+
if (O.r) { O.r.live = {}; K.feed.noteList(O.r, O.row); K.feed.settle(O.r); }
|
|
566
800
|
// What was typed, and a send whose answer never came, survive a close, Back or reload: the same words resend under
|
|
567
801
|
// the same key (skeptic W3, qa2-user N1).
|
|
568
802
|
var kept = O.r ? draftOf(O.r) : null;
|
|
569
|
-
if (kept && kept.unsettled)
|
|
803
|
+
if (kept && kept.unsettled) {
|
|
804
|
+
O.r.draft = { text: kept.unsettled.text, key: kept.unsettled.key };
|
|
805
|
+
if (Array.isArray(kept.unsettled.ids) && kept.unsettled.ids.length) { O.r.draft.ids = kept.unsettled.ids.slice(); O.r.draft.restored = true; }
|
|
806
|
+
}
|
|
570
807
|
O.last = null; O.lastState = 'idle';
|
|
571
808
|
setHead(kind === 'need' ? (O.item ? str(O.item.sessionTitle) || str(O.item.title) || 'Handoff' : 'Needs you')
|
|
572
809
|
: kind === 'wait' ? (O.wait ? O.wait.title : 'A session is waiting for you') : (O.row ? K.title(O.row) : 'Session'));
|
|
573
810
|
var body = $('ta-body');
|
|
574
|
-
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>';
|
|
811
|
+
if (body) body.innerHTML = '<div id="ta-meta"></div><div id="ta-notsent" class="ta-notsent" role="status"></div><div id="ta-q" class="ta-q" hidden></div><div id="ta-live"></div><div id="ta-last"></div>';
|
|
575
812
|
var foot = $('ta-foot');
|
|
576
|
-
if (foot) foot.innerHTML = O.r ? composerHtml(kind) : '';
|
|
813
|
+
if (foot) { foot.innerHTML = O.r ? composerHtml(kind) : ''; foot.hidden = false; }
|
|
577
814
|
var input = $('ta-input');
|
|
578
815
|
if (input && kept && kept.text) { input.value = kept.text; W.autoGrow(input); }
|
|
816
|
+
if (O.r) {
|
|
817
|
+
paintFiles();
|
|
818
|
+
// The Mac's answer about files may not be in yet: ask once, then draw the clip.
|
|
819
|
+
var kit = KIT(), r0 = O.r;
|
|
820
|
+
if (kit && !kit.turnFiles()) kit.ensureCaps().then(function () { if (O.r === r0) paintFiles(); }, function () {});
|
|
821
|
+
// Files of a send a reload cut off go again with the same words; say so, since their chips are gone.
|
|
822
|
+
if (O.r.draft && O.r.draft.restored) {
|
|
823
|
+
var nf = O.r.draft.ids.length;
|
|
824
|
+
O.note = { text: 'Your last message did not finish sending. Send sends it again with the ' + (nf === 1 ? 'file' : nf + ' files') + ' you attached.', kind: '' };
|
|
825
|
+
paintNote();
|
|
826
|
+
}
|
|
827
|
+
}
|
|
579
828
|
paintMeta();
|
|
580
829
|
if (!O.r) return;
|
|
581
830
|
if (W.sessionQuestions && W.sessionQuestions.mount) {
|
|
582
|
-
// mount() draws at once and calls back before it returns, so the
|
|
583
|
-
|
|
831
|
+
// mount() draws at once and calls back before it returns, so the counts come from the callback, not O.qMount:
|
|
832
|
+
// the questions to answer here, and those the Mac's own prompt waits on (drawn read-only).
|
|
833
|
+
O.qMount = W.sessionQuestions.mount($('ta-q'), id, function (n, mac) { O.qCount = n; O.macCount = mac || 0; paintMeta(); });
|
|
584
834
|
}
|
|
585
835
|
if (kind === 'run') { paintLive(); watchStream(O.r); }
|
|
586
836
|
loadLast(O.r);
|
|
587
837
|
K.probe(O.r);
|
|
838
|
+
loadNotSent(O.r);
|
|
588
839
|
if (kind === 'need') recheckNeed(key);
|
|
589
840
|
}
|
|
841
|
+
// 6.99.30: a queued message for this session that the server refused or let expire, said here as in the reader
|
|
842
|
+
// ("Not sent: <reason>"), from the same queue read (app.js sessionKit.notSent). The Sessions list chip follows it.
|
|
843
|
+
function loadNotSent(r) {
|
|
844
|
+
if (!r || !K.notSent || !K.answer) return;
|
|
845
|
+
K.answer(K.path(r.provider, r.id, '/queued-turns')).then(function (q) {
|
|
846
|
+
if (O.r !== r) return;
|
|
847
|
+
var rows = (q && q.status === 200 && q.body && q.body.turns) || [];
|
|
848
|
+
if (q && q.status === 200) K.notSent.note(r.provider, r.id, rows);
|
|
849
|
+
var box = $('ta-notsent'); if (!box) return;
|
|
850
|
+
box.innerHTML = K.notSent.rows(rows).map(function (t) {
|
|
851
|
+
return '<p class="ta-note ta-notsent-line"><strong>' + esc(clip(t.preview || t.prompt || 'A queued message', 80)) + '</strong> ' + esc(K.notSent.line(t)) + '</p>';
|
|
852
|
+
}).join('');
|
|
853
|
+
}, function () {});
|
|
854
|
+
}
|
|
855
|
+
// Back from the back-forward cache with a session open (pageshow persisted): the stream again, the last reply, the
|
|
856
|
+
// write gate and the questions, as on opening.
|
|
857
|
+
function revive() {
|
|
858
|
+
var r = O.r;
|
|
859
|
+
if (!r || !O.open) return;
|
|
860
|
+
if (O.open.kind === 'run') {
|
|
861
|
+
if (O.stream) { try { O.stream.close(); } catch (_e) {} O.stream = null; }
|
|
862
|
+
watchStream(r); paintLive();
|
|
863
|
+
}
|
|
864
|
+
loadLast(r);
|
|
865
|
+
K.probe(r);
|
|
866
|
+
loadNotSent(r);
|
|
867
|
+
if (W.sessionQuestions && W.sessionQuestions.nudge) W.sessionQuestions.nudge();
|
|
868
|
+
}
|
|
590
869
|
function stopSession() {
|
|
870
|
+
if (K.imagesStop) K.imagesStop($('ta-last'));
|
|
591
871
|
if (O.stream) { try { O.stream.close(); } catch (_e) {} O.stream = null; }
|
|
592
872
|
clearInterval(O.clock); O.clock = null;
|
|
593
873
|
clearInterval(O.watch); O.watch = null;
|
|
594
874
|
if (O.qMount) { O.qMount.unmount(); O.qMount = null; }
|
|
595
|
-
O.qCount = 0;
|
|
875
|
+
O.qCount = 0; O.macCount = 0;
|
|
596
876
|
O.r = null; O.row = null; O.item = null;
|
|
597
877
|
}
|
|
598
878
|
// The Needs you list may be minutes old: read it again, and say so if this one is no longer waiting.
|
|
@@ -612,12 +892,73 @@
|
|
|
612
892
|
var label = kind === 'run' ? 'Send a note to this session' : 'Reply to this session';
|
|
613
893
|
return '<div id="ta-verdict"></div>' +
|
|
614
894
|
'<form class="composer ta-composer" id="ta-form">' +
|
|
895
|
+
'<div class="cc-chips ta-files hidden" id="ta-files"></div>' +
|
|
615
896
|
'<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>' +
|
|
897
|
+
'<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
898
|
'</form>' +
|
|
618
899
|
'<p class="status-line" id="ta-status" role="status" aria-live="polite"></p>';
|
|
619
900
|
}
|
|
620
901
|
|
|
902
|
+
// ── files with a reply or a note (the COS chat's attach kit) ──
|
|
903
|
+
// One box per session, kept while this tab is open, so closing and reopening the sheet keeps what was attached.
|
|
904
|
+
var FILES = {};
|
|
905
|
+
function KIT() { return W.cosChat && W.cosChat.attach ? W.cosChat.attach : null; }
|
|
906
|
+
// Only where the Mac takes files on a session turn: on an older server the words go alone, and no clip says otherwise.
|
|
907
|
+
function filesOn() { var k = KIT(); return !!(k && k.turnFiles() > 0 && O.r); }
|
|
908
|
+
function filesBox(r) {
|
|
909
|
+
r = r || O.r; var kit = KIT();
|
|
910
|
+
if (!r || !kit) return null;
|
|
911
|
+
var key = r.provider + '|' + r.id;
|
|
912
|
+
// 6.99.27: any file the Mac takes on a turn (threadTurnFileBytes), sent as it is; else the chat's list.
|
|
913
|
+
if (!FILES[key]) FILES[key] = kit.box({
|
|
914
|
+
changed: function (box) { if (O.r && FILES[O.r.provider + '|' + O.r.id] === box) paintFiles(); },
|
|
915
|
+
removed: function () {},
|
|
916
|
+
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(); } }
|
|
917
|
+
}, { anyBytes: kit.turnFileBytes() });
|
|
918
|
+
// The Mac's answer can come after the box was made: a box with nothing in it yet takes it then.
|
|
919
|
+
if (!FILES[key].anyBytes && !FILES[key].atts.length && kit.turnFileBytes()) FILES[key].anyBytes = kit.turnFileBytes();
|
|
920
|
+
return FILES[key];
|
|
921
|
+
}
|
|
922
|
+
function filesState(box) {
|
|
923
|
+
var kit = KIT();
|
|
924
|
+
if (!kit || !box || !box.atts.length) return 'none';
|
|
925
|
+
return kit.pending(box) ? 'waiting' : kit.failed(box) ? 'failed' : 'ready';
|
|
926
|
+
}
|
|
927
|
+
function paintClip() {
|
|
928
|
+
var slot = $('ta-clip'); if (!slot) return;
|
|
929
|
+
var kit = KIT();
|
|
930
|
+
if (!filesOn()) { slot.innerHTML = ''; return; }
|
|
931
|
+
var box = filesBox(), full = !!(box && box.atts.length >= kit.max);
|
|
932
|
+
var mode = String(box && box.anyBytes || 0);
|
|
933
|
+
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); }
|
|
934
|
+
var lbls = slot.querySelectorAll ? slot.querySelectorAll('.cc-attach') : [], inps = slot.querySelectorAll ? slot.querySelectorAll('[data-ta-file]') : [];
|
|
935
|
+
Array.prototype.forEach.call(lbls, function (l) { l.classList.toggle('full', full); });
|
|
936
|
+
Array.prototype.forEach.call(inps, function (i) { i.disabled = full || !!(O.r && O.r.sending); });
|
|
937
|
+
}
|
|
938
|
+
function paintFiles() {
|
|
939
|
+
var el = $('ta-files'), kit = KIT(), box = filesBox();
|
|
940
|
+
if (el && kit && box) {
|
|
941
|
+
el.innerHTML = kit.chipsHtml(box, 'data-ta') + (box.atts.length ? '<p class="att-limit">' + esc(kit.limitLine(box)) + '</p>' : '');
|
|
942
|
+
el.classList.toggle('hidden', !box.atts.length);
|
|
943
|
+
}
|
|
944
|
+
paintClip(); paintSendButton();
|
|
945
|
+
}
|
|
946
|
+
// 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
|
+
// card does.
|
|
948
|
+
function paintSendButton() {
|
|
949
|
+
var send = $('ta-send'); if (!send) return;
|
|
950
|
+
var sending = !!(O.r && O.r.sending), st = filesState(filesBox());
|
|
951
|
+
send.disabled = sending || st === 'waiting';
|
|
952
|
+
send.textContent = sending ? 'Sending' : st === 'waiting' ? 'Uploading' : 'Send';
|
|
953
|
+
}
|
|
954
|
+
function addFiles(list) {
|
|
955
|
+
var kit = KIT(), box = filesBox();
|
|
956
|
+
if (!kit || !box || !list || !list.length || !filesOn()) return;
|
|
957
|
+
var refusals = kit.add(box, list);
|
|
958
|
+
if (refusals.length) { O.note = { text: refusals.join(' '), kind: 'error' }; paintNote(); }
|
|
959
|
+
paintFiles();
|
|
960
|
+
}
|
|
961
|
+
|
|
621
962
|
function needLine(it) {
|
|
622
963
|
var why = T.needWhy(it) + (it.domain ? ' in ' + W.domainLabel(it.domain) : '');
|
|
623
964
|
var age = W.ago(it.updatedAt || it.createdAt);
|
|
@@ -638,7 +979,7 @@
|
|
|
638
979
|
function runWords() {
|
|
639
980
|
var r = O.r, f = r && r.feed;
|
|
640
981
|
if (f && (f.connected || f.lastSeq > 0)) {
|
|
641
|
-
var w = K.feed.word(r), since = K.feed.elapsed(f);
|
|
982
|
+
var w = K.feed.word(r), since = K.feed.elapsed(f, r);
|
|
642
983
|
if (w === 'Working') return 'Working now' + (since ? ', for ' + since : '');
|
|
643
984
|
if (w === 'Waiting') return 'Waiting for you' + (since ? ', for ' + since : '');
|
|
644
985
|
if (w === 'Failed') return 'Stopped with an error';
|
|
@@ -662,10 +1003,15 @@
|
|
|
662
1003
|
var p = it.progress && !it.progress.unreadable ? it.progress : null;
|
|
663
1004
|
if (p && p.evidence) html += '<blockquote class="ta-quote">' + esc(p.evidence) + '</blockquote>';
|
|
664
1005
|
} 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
|
-
//
|
|
1006
|
+
// A session waiting on you: what it waits on, its question to answer here when the Mac holds one for this page;
|
|
1007
|
+
// when the Mac's own prompt has it, that, read-only; else (a reply is the answer) the reply box below. No live
|
|
1008
|
+
// peek and no Stop: it is not at work (W4-r2).
|
|
667
1009
|
html += '<p class="ta-sub">' + T.badge(O.r ? O.r.provider : 'claude') + ' <span>' + esc(O.wait ? O.wait.why : 'Waiting for you') + '</span></p>';
|
|
668
|
-
|
|
1010
|
+
var where = waitWhere();
|
|
1011
|
+
// The Mac card says it itself when there is one; say it here only when there is none.
|
|
1012
|
+
if (where === 'mac' && !O.macCount) html += '<p class="ta-sub ta-mac-line">' + esc(macLine()) + '</p>';
|
|
1013
|
+
if (where === 'answered') html += '<p class="ta-sub ta-mac-line">' + esc((W.sessionQuestions && W.sessionQuestions.answeredLine) || 'Answered. The session carries on.') + '</p>';
|
|
1014
|
+
if (where === 'reply') html += '<p class="ta-sub">' + esc('Answer it in a reply below, or open the session.') + '</p>';
|
|
669
1015
|
} else if (O.row || O.r) {
|
|
670
1016
|
// Once the live feed speaks, the line says what it says, so the header and the peek never disagree (qa2-user N3).
|
|
671
1017
|
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 +1024,108 @@
|
|
|
678
1024
|
if (links) html += '<div class="ta-links">' + links + '</div>';
|
|
679
1025
|
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
1026
|
box.innerHTML = html;
|
|
1027
|
+
// The reply box shows only where a reply can be the answer; what was typed stays in it, hidden.
|
|
1028
|
+
var foot = $('ta-foot');
|
|
1029
|
+
if (foot && O.open.kind === 'wait') foot.hidden = waitWhere() !== 'reply';
|
|
1030
|
+
}
|
|
1031
|
+
// Where a waiting session's question is answered: 'here' (its cards in this sheet), 'answered' (on some screen just
|
|
1032
|
+
// now), 'mac' (a Claude session's own prompt, or a held question that went back to the Mac), or 'reply' (a Codex or
|
|
1033
|
+
// Cursor session asking in words).
|
|
1034
|
+
function waitWhere() {
|
|
1035
|
+
if (O.qCount) return 'here';
|
|
1036
|
+
if (O.r && W.sessionQuestions && W.sessionQuestions.answeredFor && W.sessionQuestions.answeredFor(O.r.id)) return 'answered';
|
|
1037
|
+
if (O.macCount || (O.r && O.r.provider === 'claude') || (O.wait && O.wait.where === 'mac')) return 'mac';
|
|
1038
|
+
return 'reply';
|
|
1039
|
+
}
|
|
1040
|
+
function macLine() { return (W.sessionQuestions && W.sessionQuestions.macLine) || "Waiting on your Mac's prompt. Answer it on the Mac (or through Screens)."; }
|
|
1041
|
+
|
|
1042
|
+
// ═════════════ The pet's sessions ═════════════
|
|
1043
|
+
// The same sessions the pet's caption counts (app.js petPlay): waiting on you, running, then done today. Each row
|
|
1044
|
+
// opens that session's own sheet.
|
|
1045
|
+
var PET_DONE_SHOWN = 5;
|
|
1046
|
+
function openPet() {
|
|
1047
|
+
setHead('Sessions');
|
|
1048
|
+
var p = T.petPlay ? T.petPlay() : { waiting: [], running: [], done: [] };
|
|
1049
|
+
var row = function (s, kind, line) {
|
|
1050
|
+
var k = T.splitSessionId(s.provider, s.session_id || s.id);
|
|
1051
|
+
return '<button type="button" class="ta-pet-row" data-ta="pet-open" data-kind="' + kind + '" data-id="' + esc(k.provider + '|' + k.id) + '">' +
|
|
1052
|
+
'<span class="ta-pet-top">' + T.badge(k.provider) + '<span class="ta-pet-title">' + esc(T.sessionTitle(s)) + '</span></span>' +
|
|
1053
|
+
'<span class="ta-pet-line">' + esc(line) + '</span></button>';
|
|
1054
|
+
};
|
|
1055
|
+
var group = function (title, rows) { return rows.length ? '<section class="ta-pet-group"><h3 class="ta-pet-head">' + esc(title) + '</h3>' + rows.join('') + '</section>' : ''; };
|
|
1056
|
+
// 6.99.27: the questions this page holds come first, one row per session, each opening that session's cards.
|
|
1057
|
+
var asked = petAsked(), keyOf = function (s) { var k = T.splitSessionId(s.provider, s.session_id || s.id); return (k.provider + '|' + k.id).toLowerCase(); };
|
|
1058
|
+
var questions = asked.map(function (a) {
|
|
1059
|
+
var s = sessionRowFor(a.provider, a.id);
|
|
1060
|
+
var k = esc(a.provider + '|' + a.id);
|
|
1061
|
+
return '<button type="button" class="ta-pet-row" data-ta="pet-open" data-kind="wait" data-id="' + k + '">' +
|
|
1062
|
+
'<span class="ta-pet-top">' + T.badge(a.provider) + '<span class="ta-pet-title">' + esc(s ? T.sessionTitle(s) : a.title) + '</span></span>' +
|
|
1063
|
+
'<span class="ta-pet-line">' + esc(a.line) + '</span></button>';
|
|
1064
|
+
});
|
|
1065
|
+
var held = {}; asked.forEach(function (a) { held[(a.provider + '|' + a.id).toLowerCase()] = 1; });
|
|
1066
|
+
var waiting = p.waiting.filter(function (s) { return !held[keyOf(s)]; }).map(function (s) { return row(s, 'wait', stateWords(s)); });
|
|
1067
|
+
// The pet counts a session as running from the Running card's test; one of those paused on you opens as waiting.
|
|
1068
|
+
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)); });
|
|
1069
|
+
var done = p.done.slice(0, PET_DONE_SHOWN).map(function (s) { return row(s, 'run', 'Last active ' + doneWhen(s)); });
|
|
1070
|
+
var body = $('ta-body');
|
|
1071
|
+
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) +
|
|
1072
|
+
(!questions.length && !waiting.length && !running.length ? '<p class="ta-note">Nothing is running or waiting on you now.</p>' : '') +
|
|
1073
|
+
'<div class="ta-links"><button type="button" class="link-btn" data-ta="open-sessions">All sessions</button></div>';
|
|
1074
|
+
var foot = $('ta-foot'); if (foot) { foot.innerHTML = ''; foot.hidden = false; }
|
|
1075
|
+
}
|
|
1076
|
+
// The held questions by session (panes/questions.js), oldest first: the words of each session's first question.
|
|
1077
|
+
function petAsked() {
|
|
1078
|
+
var q = W.sessionQuestions, items = q && q.itemsFor ? q.itemsFor('') : [], out = [], at = {};
|
|
1079
|
+
(items || []).forEach(function (it) {
|
|
1080
|
+
if (!it || !it.sessionId) return;
|
|
1081
|
+
var k = T.splitSessionId(it.provider, it.sessionId), key = (k.provider + '|' + k.id).toLowerCase();
|
|
1082
|
+
if (at[key] !== undefined) { out[at[key]].n++; return; }
|
|
1083
|
+
var first = it.kind === 'approval' ? (it.approval && it.approval.summary ? 'Wants to run ' + it.approval.summary : 'Wants your permission')
|
|
1084
|
+
: String(((it.questions || [])[0] || {}).question || 'Asked you a question');
|
|
1085
|
+
at[key] = out.length;
|
|
1086
|
+
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' });
|
|
1087
|
+
});
|
|
1088
|
+
out.forEach(function (a) { if (a.n > 1) a.line = a.n + ' questions. ' + a.line; });
|
|
1089
|
+
return out;
|
|
1090
|
+
}
|
|
1091
|
+
function doneWhen(s) {
|
|
1092
|
+
var a = W.ago(s.last_activity_at || s.state_since);
|
|
1093
|
+
if (!a || a === 'now') return 'just now';
|
|
1094
|
+
return /( min| h)$/.test(a) ? a + ' ago' : a;
|
|
681
1095
|
}
|
|
682
1096
|
|
|
683
1097
|
// The session's last reply, from the same read the reader makes (?turns=N, lib/agent-session-turns.ts).
|
|
684
1098
|
function loadLast(r) {
|
|
685
|
-
|
|
1099
|
+
var request = r.lastRead = {};
|
|
1100
|
+
O.lastState = 'loading'; O.lastWhy = ''; paintLast();
|
|
686
1101
|
return K.answer(K.path(r.provider, r.id, '?turns=6'), { timeoutMs: 20000 }).then(function (a) {
|
|
687
|
-
if (O.r !== r) return;
|
|
1102
|
+
if (O.r !== r || r.lastRead !== request) return;
|
|
688
1103
|
if (a.status === 404) { O.lastState = 'none'; O.lastWhy = 'This session is not on your Mac any more.'; paintLast(); return; }
|
|
689
1104
|
if (a.status !== 200) { O.lastState = 'error'; paintLast(); return; }
|
|
690
1105
|
var d = a.body || {};
|
|
691
1106
|
if (!O.row) { O.row = d; paintMeta(); if (O.open && O.open.kind === 'run') setHead(K.title(d)); }
|
|
692
1107
|
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;
|
|
1108
|
+
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, outputImages: lastA.outputImages } : null;
|
|
695
1110
|
O.lastState = O.last ? 'ready' : 'none';
|
|
696
1111
|
O.lastWhy = '';
|
|
697
1112
|
paintLast();
|
|
698
|
-
}, function () { if (O.r === r) { O.lastState = 'error'; paintLast(); } });
|
|
1113
|
+
}, function () { if (O.r === r && r.lastRead === request) { O.lastState = 'error'; paintLast(); } });
|
|
699
1114
|
}
|
|
700
1115
|
function paintLast() {
|
|
701
1116
|
var box = $('ta-last'); if (!box) return;
|
|
1117
|
+
// Retain the actual reply nodes (and its open image) during refreshes and transient errors.
|
|
1118
|
+
// 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) : '']) : '';
|
|
1120
|
+
if (signature && box.__taLastSignature === signature) {
|
|
1121
|
+
var refresh = $('ta-last-refresh');
|
|
1122
|
+
if (refresh) refresh.textContent = O.lastState === 'loading' ? 'Checking for a newer reply…'
|
|
1123
|
+
: O.lastState === 'error' ? 'The latest reply could not load. Showing the previous reply.'
|
|
1124
|
+
: O.lastState === 'none' ? (O.lastWhy || '') : '';
|
|
1125
|
+
return;
|
|
1126
|
+
}
|
|
1127
|
+
box.__taLastSignature = signature;
|
|
1128
|
+
if (K.imagesStop) K.imagesStop(box);
|
|
702
1129
|
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
1130
|
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
1131
|
if (O.lastState === 'none') { box.innerHTML = O.lastWhy ? '<p class="ta-sub">' + esc(O.lastWhy) + '</p>' : ''; return; }
|
|
@@ -706,7 +1133,8 @@
|
|
|
706
1133
|
var at = W.toDate(O.last.at), long = O.last.text.length > 700;
|
|
707
1134
|
box.innerHTML = '<section class="ta-last" aria-label="Last reply"><p class="ta-last-head">' + esc('Last reply' + (at ? ', ' + W.clock(at) : '')) + '</p>' +
|
|
708
1135
|
'<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>' : '') + '
|
|
1136
|
+
(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>';
|
|
1137
|
+
if (K.imagesMount) K.imagesMount(box, O.r);
|
|
710
1138
|
}
|
|
711
1139
|
|
|
712
1140
|
// Running: the same live stream the reader follows, read with its own reducer (applyFeed, sxFeedWord and friends).
|
|
@@ -721,15 +1149,17 @@
|
|
|
721
1149
|
K.feed.apply(r, e);
|
|
722
1150
|
r.live = r.live || {};
|
|
723
1151
|
if (e.kind === 'status') {
|
|
724
|
-
//
|
|
725
|
-
|
|
726
|
-
//
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
r.live.working
|
|
730
|
-
|
|
1152
|
+
// 6.99.29: the card's one state (app.js sxTruth) decides working and waiting: the agent's own
|
|
1153
|
+
// waiting still wins over a busy file (W4-r2), and the stream's echo of a Stop still cannot
|
|
1154
|
+
// reopen it, but a stale idle no longer outranks fresher working evidence.
|
|
1155
|
+
var wasWorking = !!r.live.working, wasWaiting = !!r.live.waiting;
|
|
1156
|
+
K.feed.settle(r);
|
|
1157
|
+
if (wasWorking && !r.live.working) loadLast(r);
|
|
1158
|
+
// Gone to waiting: its question may be parked for this page now, so the card reads at once (6.99.27).
|
|
1159
|
+
if (r.live.waiting && !wasWaiting && W.sessionQuestions && W.sessionQuestions.nudge) W.sessionQuestions.nudge();
|
|
731
1160
|
} else if (e.kind === 'prose' && e.text) r.live.prose = String(e.text);
|
|
732
1161
|
paintLive();
|
|
1162
|
+
paintVerdict();
|
|
733
1163
|
};
|
|
734
1164
|
es.onerror = function () {
|
|
735
1165
|
if (O.r !== r || O.stream !== es) return;
|
|
@@ -759,10 +1189,10 @@
|
|
|
759
1189
|
// Idle: the reader's line repeats the last reply, which the peek shows in full just below.
|
|
760
1190
|
var word = K.feed.word(r), detail = word === 'Idle' ? '' : K.feed.detail(f, word);
|
|
761
1191
|
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>' +
|
|
1192
|
+
'<span class="sx-act-detail">' + esc(detail) + '</span><span class="sx-act-clock" id="ta-clock">' + esc(K.feed.elapsed(f, r)) + '</span>' +
|
|
763
1193
|
(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
1194
|
(O.stopping ? 'Stopping' : O.armedAt.stop != null ? 'Tap again to stop' : 'Stop') + '</button>' : '') + '</div>' +
|
|
765
|
-
(f.prompt ? '<p class="ta-act-prompt">' + esc(
|
|
1195
|
+
(f.prompt ? '<p class="ta-act-prompt">' + esc(K.feed.promptLine(r, clip(f.prompt, 240))) + '</p>' : '') +
|
|
766
1196
|
'<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
1197
|
(live.working && live.prose ? '<div class="sx-act-prose">' + esc(String(live.prose).slice(-500)) + '</div>' : '') + '</div>';
|
|
768
1198
|
// The feed stopped or was refused: say why under the last state it showed.
|
|
@@ -775,7 +1205,9 @@
|
|
|
775
1205
|
if (w === 'Working' || w === 'Waiting') {
|
|
776
1206
|
O.clock = setInterval(function () {
|
|
777
1207
|
if (O.r !== r) { clearInterval(O.clock); return; }
|
|
778
|
-
|
|
1208
|
+
if (document.hidden) return; // a hidden page spends nothing; coming back repaints at once (visibilitychange)
|
|
1209
|
+
var c = $('ta-clock'); if (c) c.textContent = K.feed.elapsed(r.feed, r);
|
|
1210
|
+
if (K.feed.gateStale(r)) K.probe(r);
|
|
779
1211
|
var rs = $('ta-run-state'); if (rs) rs.textContent = runWords();
|
|
780
1212
|
}, 1000);
|
|
781
1213
|
}
|
|
@@ -795,14 +1227,26 @@
|
|
|
795
1227
|
paintNote();
|
|
796
1228
|
},
|
|
797
1229
|
clear: function (r, text) {
|
|
798
|
-
// The Mac took it: the send is settled, and the kept draft goes with the words that were sent.
|
|
1230
|
+
// The Mac took it: the send is settled, and the kept draft goes with the words that were sent. So do the files
|
|
1231
|
+
// that went with them; any added since stay for the next message.
|
|
1232
|
+
var sent = r.draft && r.draft.text === text ? r.draft.ids || [] : [];
|
|
799
1233
|
if (r.draft && r.draft.text === text) r.draft = null;
|
|
1234
|
+
var fb = sent.length ? filesBox(r) : null, kit = KIT();
|
|
1235
|
+
if (fb && kit) fb.atts.filter(function (t) { return sent.indexOf(t.mediaId) >= 0; }).forEach(function (t) { kit.remove(fb, t.id); });
|
|
800
1236
|
var box = r === O.r ? $('ta-input') : null;
|
|
801
1237
|
var left = box ? (box.value.trim() === String(text).trim() ? '' : box.value) : '';
|
|
802
1238
|
if (box && !left) { box.value = ''; W.autoGrow(box); }
|
|
803
1239
|
keepDraft(r, left, null);
|
|
804
1240
|
},
|
|
805
1241
|
verdict: function (r) { if (r === O.r) paintVerdict(); },
|
|
1242
|
+
// 6.99.29: the card's one state moved (a gate answer, a parked send, a live delivery): repaint all of it.
|
|
1243
|
+
stateChanged: function (r) {
|
|
1244
|
+
if (r !== O.r) return;
|
|
1245
|
+
var wasWorking = !!r.live && !!r.live.working;
|
|
1246
|
+
K.feed.settle(r);
|
|
1247
|
+
if (wasWorking && !r.live.working) loadLast(r);
|
|
1248
|
+
paintLive(); paintVerdict();
|
|
1249
|
+
},
|
|
806
1250
|
hold: function (r) {
|
|
807
1251
|
if (r !== O.r) return;
|
|
808
1252
|
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 +1255,7 @@
|
|
|
811
1255
|
watch: function (r, bindingId, key) { watchTurn(r, bindingId, key); },
|
|
812
1256
|
queue: function () {}
|
|
813
1257
|
};
|
|
814
|
-
function paintNote() { W.setStatus($('ta-status'), O.note.text, O.note.kind === 'error'); }
|
|
1258
|
+
function paintNote() { W.setStatus($('ta-status'), O.note.text, O.note.kind === 'error'); if (KB.on) fit(); }
|
|
815
1259
|
// A copy of the reader's renderVerdict (app.js): the same boxes and words, with Open session where the reader offers
|
|
816
1260
|
// Fork and Refresh, which live only in the reader.
|
|
817
1261
|
function paintVerdict() {
|
|
@@ -830,7 +1274,8 @@
|
|
|
830
1274
|
((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
1275
|
'</div></div>';
|
|
832
1276
|
} else if (v && v.attachable === false && /^(native_thread_working|native_target_busy)$/.test(v.reason)) {
|
|
833
|
-
|
|
1277
|
+
// 6.99.29: "Working right now" only while the card's one state says so.
|
|
1278
|
+
if (K.feed.parkNoteShows(r)) html = '<p class="sx-park">' + esc(K.parkCopy(r.provider)) + '</p>';
|
|
834
1279
|
} else if (v && v.attachable === false && v.reasonCopy) {
|
|
835
1280
|
html = '<div class="sx-box refusal" role="status"><p>' + esc(v.reasonCopy) + '</p></div>';
|
|
836
1281
|
} else if (v && v.caution) {
|
|
@@ -838,16 +1283,26 @@
|
|
|
838
1283
|
: 'Another app on this Mac has this session open. COS asks before the first send.') + '</p>';
|
|
839
1284
|
}
|
|
840
1285
|
box.innerHTML = html;
|
|
1286
|
+
if (KB.on) fit(); // the reply box's height changed: the conversation's room is measured again
|
|
841
1287
|
}
|
|
842
1288
|
function setSending(on) {
|
|
843
|
-
var
|
|
844
|
-
if (send) { send.disabled = on; send.textContent = on ? 'Sending' : 'Send'; }
|
|
1289
|
+
var input = $('ta-input');
|
|
845
1290
|
if (input) input.readOnly = on;
|
|
1291
|
+
paintSendButton(); paintClip();
|
|
846
1292
|
}
|
|
847
1293
|
function sendReply(opts) {
|
|
848
1294
|
var r = O.r; if (!r) return;
|
|
849
1295
|
var input = $('ta-input'), text = input ? input.value.trim() : '';
|
|
850
1296
|
if (!text) { O.note = { text: 'Write a message first.', kind: 'error' }; paintNote(); return; }
|
|
1297
|
+
// Files: never before every upload has finished, and never with one that failed.
|
|
1298
|
+
var box = filesOn() ? filesBox(r) : null, st = filesState(box);
|
|
1299
|
+
if (st === 'waiting') { O.note = { text: 'A file is still uploading. Send when it finishes.', kind: '' }; paintNote(); return; }
|
|
1300
|
+
if (st === 'failed') { O.note = { text: 'A file did not upload. Retry it or remove it.', kind: 'error' }; paintNote(); return; }
|
|
1301
|
+
var ids = st === 'ready' ? KIT().ids(box) : [];
|
|
1302
|
+
// A send with files cut off by a reload: the chips are gone, the draft kept their ids. The same words go again with
|
|
1303
|
+
// the same files under the same key, so a send that did land is answered, never repeated.
|
|
1304
|
+
if (!ids.length && r.draft && r.draft.restored && r.draft.text === text && r.draft.ids && r.draft.ids.length) ids = r.draft.ids.slice();
|
|
1305
|
+
if (ids.length) opts = Object.assign({}, opts || {}, { attachmentIds: ids });
|
|
851
1306
|
// Control's caution: another app holds this session and looks idle, so the first send asks.
|
|
852
1307
|
if (r.verdict && r.verdict.caution && !r.cautionAck) { O.cautionAsk = true; O.armedAt = { caution: Date.now() }; paintVerdict(); return; }
|
|
853
1308
|
if (r.sending) return;
|
|
@@ -881,7 +1336,7 @@
|
|
|
881
1336
|
var b = c.body || {};
|
|
882
1337
|
O.stopKey = '';
|
|
883
1338
|
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();
|
|
1339
|
+
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
1340
|
paintNote(); paintLive();
|
|
886
1341
|
}, function (err) {
|
|
887
1342
|
O.stopping = false;
|
|
@@ -896,11 +1351,19 @@
|
|
|
896
1351
|
clearInterval(O.watch);
|
|
897
1352
|
var started = Date.now();
|
|
898
1353
|
O.watch = setInterval(function () {
|
|
1354
|
+
// Hidden: no reads, and the three minutes count only while the page is in view (6.99.30).
|
|
1355
|
+
if (document.hidden) { started += 3000; return; }
|
|
899
1356
|
if (O.r !== r || Date.now() - started > WATCH_MS) { clearInterval(O.watch); return; }
|
|
900
1357
|
K.answer('/api/agent-sessions/bindings/' + encodeURIComponent(bindingId) + '/turns/' + encodeURIComponent(key)).then(function (s) {
|
|
901
1358
|
if (O.r !== r) return;
|
|
902
1359
|
var o = s.body && s.body.outcome;
|
|
903
|
-
if (o === 'completed') {
|
|
1360
|
+
if (o === 'completed') {
|
|
1361
|
+
clearInterval(O.watch);
|
|
1362
|
+
// 6.99.29: a turn delivered LIVE is in the open session, which now works on it; it is not done.
|
|
1363
|
+
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: '' }; }
|
|
1364
|
+
else { O.note = { text: 'Done. The reply is below.', kind: '' }; loadLast(r); }
|
|
1365
|
+
paintNote(); VIEW.stateChanged(r);
|
|
1366
|
+
}
|
|
904
1367
|
else if (o === 'refused') { clearInterval(O.watch); O.note = { text: s.body.reasonCopy || 'The session refused that message.', kind: 'error' }; paintNote(); }
|
|
905
1368
|
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
1369
|
}).catch(function () {});
|
|
@@ -924,7 +1387,9 @@
|
|
|
924
1387
|
lines.push(rows.length ? 'Needs you (' + rows.length + (later ? ', ' + later + ' of them can wait until tomorrow' : '') + '):'
|
|
925
1388
|
: m && !m.settled ? 'Needs you: nothing found so far.' : 'Needs you: nothing.');
|
|
926
1389
|
rows.slice(0, 6).forEach(function (r) {
|
|
927
|
-
|
|
1390
|
+
// 6.99.26: where a waiting session can be answered, as the row says it.
|
|
1391
|
+
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.' : '';
|
|
1392
|
+
if (r.kind !== 'work') { lines.push('- ' + end(clip(r.title, 140)) + (r.why ? ' ' + end(clip(r.why, 160)) : '') + where); return; }
|
|
928
1393
|
var it = r.item;
|
|
929
1394
|
var title = str(it.sessionTitle) || str(it.title) || 'Handoff';
|
|
930
1395
|
var bits = [end(clip(title, 120)) + ' ' + end(needLine(it))];
|
|
@@ -967,6 +1432,6 @@
|
|
|
967
1432
|
open: open, close: close, restore: restore, summary: summary, summaryMax: SUMMARY_MAX,
|
|
968
1433
|
isOpen: function () { return !!O.open; },
|
|
969
1434
|
_state: O, _decide: decide, _undo: undo, _intakeHtml: intakeHtml, _send: sendReply, _view: VIEW, _paintVerdict: paintVerdict,
|
|
970
|
-
_paintLive: paintLive, _paintMeta: paintMeta, _onClick: onClick, _expire: expireLater
|
|
1435
|
+
_paintLive: paintLive, _paintMeta: paintMeta, _onClick: onClick, _expire: expireLater, _kb: KB, _fitNow: applyFit, _revive: revive
|
|
971
1436
|
});
|
|
972
1437
|
})();
|