@gotcos/glasses-server 6.99.29 → 7.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +18 -0
- package/CHANGELOG.md +475 -0
- package/README.md +3 -1
- package/managed-runtime-contract.json +3 -0
- package/package.json +1 -1
- package/server/index.ts +29 -7
- package/server/lib/agent-session-output-images.ts +627 -49
- package/server/lib/agent-session-store.ts +24 -11
- package/server/lib/agent-session-tool-images.ts +224 -0
- package/server/lib/agent-session-turns.ts +19 -2
- package/server/lib/api-auth.ts +6 -0
- package/server/lib/ask-work-plan.ts +3 -1
- package/server/lib/calendar-source.ts +491 -0
- package/server/lib/codex-bridge.ts +4 -1
- package/server/lib/codex-run-ledger.ts +29 -0
- package/server/lib/context-builder.ts +2 -1
- package/server/lib/drop-p2-runtime.ts +1 -1
- package/server/lib/drop-store.ts +10 -4
- package/server/lib/learning-needs-you.ts +24 -7
- package/server/lib/live-events.ts +533 -0
- package/server/lib/local-path-redact.ts +272 -0
- package/server/lib/meeting-ask-orientation.ts +40 -6
- package/server/lib/meeting-ask-runner.ts +30 -10
- package/server/lib/morning-brief-runtime.ts +4 -2
- package/server/lib/native-mac-caller.ts +29 -0
- package/server/lib/ping-engine.ts +1 -1
- package/server/lib/ping-sources.ts +11 -6
- package/server/lib/preview-content.ts +172 -0
- package/server/lib/preview-error-code.ts +10 -0
- package/server/lib/preview-folders-setting.ts +133 -0
- package/server/lib/preview-log-throttle.ts +18 -0
- package/server/lib/preview-thumbs.ts +139 -0
- package/server/lib/secret-path-guard.ts +80 -0
- package/server/lib/session-files-setting.ts +88 -0
- package/server/lib/session-links.ts +280 -0
- package/server/lib/thread-turn-queue.ts +10 -0
- package/server/lib/today-brief.ts +119 -28
- package/server/lib/web-memories-bridge.ts +3 -1
- package/server/lib/work-activity.ts +56 -6
- package/server/lib/work-session-activity.ts +260 -0
- package/server/lib/your-move-count.ts +9 -3
- package/server/routes/agent-session-bindings.ts +13 -0
- package/server/routes/agent-session-stream.ts +16 -0
- package/server/routes/agent-sessions.ts +146 -11
- package/server/routes/ask-work-plan.ts +4 -1
- package/server/routes/claude-sessions.ts +5 -1
- package/server/routes/health.ts +35 -0
- package/server/routes/live-events.ts +329 -0
- package/server/routes/permission-broker.ts +69 -5
- package/server/routes/session-preview.ts +200 -0
- package/server/routes/thread-turn-queue.ts +207 -3
- package/server/routes/today-brief.ts +44 -9
- package/server/routes/welcome-context.ts +23 -3
- package/server/routes/work-board.ts +9 -2
- package/server/web-control/app.css +79 -9
- package/server/web-control/app.js +1543 -65
- package/server/web-control/control.css +37 -6
- package/server/web-control/index.html +8 -2
- package/server/web-control/memory-review.js +100 -0
- package/server/web-control/panes/cos-chat.css +51 -6
- package/server/web-control/panes/cos-chat.js +268 -16
- package/server/web-control/panes/meetings.css +23 -4
- package/server/web-control/panes/meetings.js +25 -9
- package/server/web-control/panes/memories.css +18 -3
- package/server/web-control/panes/memories.js +130 -50
- package/server/web-control/panes/messages.js +21 -2
- package/server/web-control/panes/preview.js +318 -0
- package/server/web-control/panes/questions.css +20 -4
- package/server/web-control/panes/questions.js +127 -26
- package/server/web-control/panes/reader-media.js +409 -23
- package/server/web-control/panes/sessions.css +50 -10
- package/server/web-control/panes/settings.js +96 -5
- 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 +49 -18
- package/server/web-control/panes/today-actions.js +217 -50
- package/server/web-control/panes/work.css +11 -1
- package/server/web-control/panes/work.js +10 -2
- package/server/web-control/shell.js +20 -14
|
@@ -75,7 +75,7 @@
|
|
|
75
75
|
var O = {
|
|
76
76
|
open: null, // { kind: 'need' | 'run' | 'wait' | 'pet' | 'meeting' | 'intake', key, all }
|
|
77
77
|
opener: null, openerSel: '',
|
|
78
|
-
pushed: false, ignorePops: 0,
|
|
78
|
+
pushed: false, ignorePops: 0, pops: [], // pops: our own Back steps still to arrive (quietBack)
|
|
79
79
|
depth: 1, // history entries the overlay holds: 2 while a session opened from the pet's list shows
|
|
80
80
|
back: null, // { scroll } the pet's list that session came from, for Back to sessions
|
|
81
81
|
dirty: false, // something was decided or sent: Today reloads on close
|
|
@@ -144,6 +144,8 @@
|
|
|
144
144
|
layer.addEventListener('click', onClick);
|
|
145
145
|
layer.addEventListener('submit', onSubmit);
|
|
146
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(); });
|
|
147
149
|
layer.addEventListener('input', function (e) {
|
|
148
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);
|
|
149
151
|
});
|
|
@@ -169,46 +171,103 @@
|
|
|
169
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 });
|
|
170
172
|
return layer;
|
|
171
173
|
}
|
|
172
|
-
//
|
|
173
|
-
//
|
|
174
|
-
//
|
|
175
|
-
//
|
|
176
|
-
//
|
|
177
|
-
//
|
|
178
|
-
//
|
|
179
|
-
//
|
|
180
|
-
//
|
|
181
|
-
//
|
|
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.
|
|
182
194
|
var KB_MIN_PX = 120;
|
|
195
|
+
var KB_STRIP_MIN_PX = 24;
|
|
183
196
|
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
|
-
}
|
|
197
|
+
var KB = { on: false, strip: false, tight: false, frame: 0, listening: false, width: 0, inset: 0, typed: false };
|
|
190
198
|
function tall(el) { return (el && el.offsetHeight) || 0; }
|
|
191
199
|
// What the conversation gets once the compact header and the reply box have theirs.
|
|
192
|
-
function bodyRoom(
|
|
200
|
+
function bodyRoom(height) {
|
|
193
201
|
var sheet = $('ta-sheet'), grip = sheet && sheet.querySelector ? sheet.querySelector('.ta-grip') : null;
|
|
194
|
-
return
|
|
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
|
+
// 7.0.2: `wide` keeps its 6.99.30 meaning, 600 px wide or more, so old and new reports compare. From 7.0.2 a short
|
|
215
|
+
// touch screen at that width counts as a phone (app.js isPhone, A8): that is shortTouch.
|
|
216
|
+
function crumbWide() { return window.matchMedia ? !!window.matchMedia('(min-width: 600px)').matches : !phone(); }
|
|
217
|
+
function crumb(what, v, focus) {
|
|
218
|
+
if (!W.layoutCrumb || !O.open || !v) return;
|
|
219
|
+
W.layoutCrumb(what, { kind: O.open.kind, kb: KB.on, strip: KB.strip, tight: KB.tight, focus: !!focus, inset: v.inset, below: v.below,
|
|
220
|
+
lh: v.layoutH, top: v.top, h: v.height, zoomed: v.zoomed, wide: crumbWide(), shortTouch: crumbWide() && phone() });
|
|
195
221
|
}
|
|
196
222
|
function applyFit() {
|
|
197
223
|
KB.frame = 0;
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
layer.style.setProperty('--ta-
|
|
202
|
-
|
|
203
|
-
layer.style.setProperty('--ta-
|
|
224
|
+
if (!layer || !O.open) return null;
|
|
225
|
+
var v = viewport();
|
|
226
|
+
if (!v) return null;
|
|
227
|
+
layer.style.setProperty('--ta-vh', v.height + 'px');
|
|
228
|
+
layer.style.setProperty('--ta-vt', v.top + 'px');
|
|
229
|
+
layer.style.setProperty('--ta-below', v.below + 'px');
|
|
230
|
+
var focus = typing(), on = v.inset >= KB_MIN_PX, strip = !on && focus && v.inset >= KB_STRIP_MIN_PX;
|
|
231
|
+
// A turned screen measures the room again: what was tight in landscape is not in portrait (QA 6.99.29).
|
|
232
|
+
var w = window.innerWidth || 0, turned = !!(KB.width && w && w !== KB.width);
|
|
233
|
+
if (w) KB.width = w;
|
|
234
|
+
var was = KB.on + '|' + KB.strip + '|' + KB.tight;
|
|
235
|
+
if (turned) KB.tight = false;
|
|
236
|
+
// A rise is what covers the bottom growing, by any amount, while the keyboard layout is on: not a class flip (a
|
|
237
|
+
// 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
|
|
238
|
+
// (a keyboard animating up in 20 px frames never crossed one, Codex's root regression on 98f3cde). The reveal is
|
|
239
|
+
// scrollIntoView 'nearest', so a frame where the field is already in view moves nothing. Shrinking never reveals.
|
|
240
|
+
var rose = (on || strip) && v.inset > (KB.inset || 0);
|
|
241
|
+
KB.inset = v.inset;
|
|
204
242
|
if (on !== KB.on) { KB.on = on; KB.tight = false; layer.classList.toggle('ta-kb', on); }
|
|
243
|
+
if (strip !== KB.strip) { KB.strip = strip; layer.classList.toggle('ta-kb-strip', strip); }
|
|
244
|
+
// 6.99.32 (QA 6.99.30 canary W2): the keyboard rises after the focus, so onFocusIn's scroll (250 ms) can land before
|
|
245
|
+
// the layer shrinks and leave a question card's Other field under the keyboard. Once the layout has the keyboard,
|
|
246
|
+
// bring the focused field into view again.
|
|
247
|
+
if (rose && focus) revealFocused();
|
|
205
248
|
// Measured with the compact header in place. Once tight it stays so while the keyboard is up: showing the note
|
|
206
249
|
// again would take the room back and flicker.
|
|
207
|
-
if (on && !KB.tight) KB.tight = bodyRoom(
|
|
250
|
+
if (on && !KB.tight) KB.tight = bodyRoom(v.height) < BODY_MIN_PX;
|
|
208
251
|
layer.classList.toggle('ta-kb-tight', on && KB.tight);
|
|
252
|
+
// 7.0.2 (A1): the breadcrumb goes on every keyboard open and close in the sheet (a field typing with KB_MIN_PX or more
|
|
253
|
+
// covered), not only when a class flips: a keyboard that came while ta-kb was already on used to send nothing.
|
|
254
|
+
var typed = focus && v.inset >= KB_MIN_PX, opened = typed !== KB.typed;
|
|
255
|
+
KB.typed = typed;
|
|
256
|
+
if (opened || was !== KB.on + '|' + KB.strip + '|' + KB.tight) crumb('kb', v, focus);
|
|
257
|
+
return v;
|
|
258
|
+
}
|
|
259
|
+
function revealFocused() {
|
|
260
|
+
// What has focus when the frame runs, not when it was asked for (QA 6.99.32 Skeptic N-2).
|
|
261
|
+
var go = function () {
|
|
262
|
+
if (!O.open || !typing()) return;
|
|
263
|
+
var a = document.activeElement, f = a && a.id === 'ta-input' ? $('ta-form') : a;
|
|
264
|
+
if (f && f.scrollIntoView) f.scrollIntoView({ block: 'nearest' });
|
|
265
|
+
};
|
|
266
|
+
// The next frame, so the scroll measures the layout with ta-kb (or ta-kb-strip) applied.
|
|
267
|
+
if (window.requestAnimationFrame) window.requestAnimationFrame(go); else go();
|
|
209
268
|
}
|
|
210
269
|
function fit() {
|
|
211
|
-
if (KB.frame) return;
|
|
270
|
+
if (KB.frame || !O.open) return;
|
|
212
271
|
if (!window.requestAnimationFrame) return applyFit(); // every browser with visualViewport has frames
|
|
213
272
|
KB.frame = window.requestAnimationFrame(applyFit);
|
|
214
273
|
}
|
|
@@ -222,11 +281,34 @@
|
|
|
222
281
|
function unfit() {
|
|
223
282
|
listenViewport(false);
|
|
224
283
|
if (KB.frame && window.cancelAnimationFrame) window.cancelAnimationFrame(KB.frame);
|
|
225
|
-
KB.frame = 0; KB.on = false; KB.tight = false;
|
|
284
|
+
KB.frame = 0; KB.on = false; KB.strip = false; KB.tight = false; KB.width = 0; KB.inset = 0; KB.typed = false;
|
|
226
285
|
if (!layer) return;
|
|
227
|
-
layer.classList.remove('ta-kb'); layer.classList.remove('ta-kb-tight');
|
|
228
|
-
|
|
229
|
-
|
|
286
|
+
layer.classList.remove('ta-kb'); layer.classList.remove('ta-kb-strip'); layer.classList.remove('ta-kb-tight');
|
|
287
|
+
}
|
|
288
|
+
// The rest of the moments the visible viewport can change without a visualViewport event of its own (QA 6.99.29: the
|
|
289
|
+
// layer stayed sized for a keyboard that had gone). Each is a no-op while the overlay is closed.
|
|
290
|
+
window.addEventListener('resize', function () { fit(); });
|
|
291
|
+
window.addEventListener('orientationchange', function () { if (O.open) { KB.tight = false; fit(); } });
|
|
292
|
+
document.addEventListener('visibilitychange', function () {
|
|
293
|
+
if (document.hidden || !O.open) return;
|
|
294
|
+
fit();
|
|
295
|
+
if (O.r && O.open.kind === 'run') paintLive(); // the clock and the state, at once rather than on the next tick
|
|
296
|
+
});
|
|
297
|
+
// Back from the back-forward cache (a swipe back, the app switcher): the live stream is closed and everything shown
|
|
298
|
+
// is as old as the visit away. Open the stream again and read the session again.
|
|
299
|
+
window.addEventListener('pageshow', function (e) {
|
|
300
|
+
if (!O.open) return;
|
|
301
|
+
fit();
|
|
302
|
+
if (e && e.persisted) revive();
|
|
303
|
+
});
|
|
304
|
+
// 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
|
|
305
|
+
// live line says the Mac is not answering, and back up the stream opens again and the session is read again.
|
|
306
|
+
if (W.onServer) W.onServer(function (up) {
|
|
307
|
+
var r = O.r;
|
|
308
|
+
if (!O.open || !r) return;
|
|
309
|
+
if (!up) { r.feed.connected = false; r.feed.fallback = 'unreachable'; if (r.live) r.live.working = false; paintLive(); paintVerdict(); }
|
|
310
|
+
else revive();
|
|
311
|
+
});
|
|
230
312
|
|
|
231
313
|
function openerSelector(t) {
|
|
232
314
|
if (!t || !t.getAttribute) return '';
|
|
@@ -275,6 +357,8 @@
|
|
|
275
357
|
var sheet = $('ta-sheet');
|
|
276
358
|
O.settleAt = Date.now() + settleMs(sheet);
|
|
277
359
|
if (sheet && sheet.focus) sheet.focus({ preventScroll: true });
|
|
360
|
+
// Measured once the sheet is drawn, and sent home as a breadcrumb (app.js layoutCrumb, 6.99.30).
|
|
361
|
+
crumb('open', applyFit(), typing());
|
|
278
362
|
}
|
|
279
363
|
// How long taps inside are ignored after opening: the longest open motion of the sheet and its backdrop (their computed
|
|
280
364
|
// animation and transition, delay included: today-actions.css ta-rise or ta-slide, 220 ms, and fade, 160 ms), plus
|
|
@@ -314,8 +398,8 @@
|
|
|
314
398
|
// 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
399
|
if (O.pushed && how !== 'pop' && how !== 'leave') {
|
|
316
400
|
var n = O.depth > 1 ? O.depth : 1;
|
|
317
|
-
O.pushed = false;
|
|
318
|
-
|
|
401
|
+
O.pushed = false;
|
|
402
|
+
quietBack(n);
|
|
319
403
|
}
|
|
320
404
|
if (how === 'pop' || how === 'leave') O.pushed = false;
|
|
321
405
|
clearTimeout(O.expireTimer);
|
|
@@ -333,11 +417,29 @@
|
|
|
333
417
|
if (el && el.focus) { try { el.focus({ preventScroll: true }); } catch (_e) { el.focus(); } }
|
|
334
418
|
}
|
|
335
419
|
// Leaving for a tab from inside the overlay replaces the overlay's history entry, so Back from there returns to Today.
|
|
420
|
+
// A session opened off the pet's list holds two entries: the list's comes off first (6.99.30, QA: it stayed under
|
|
421
|
+
// the new tab, and Back from there landed on it and showed nothing), then the tab replaces the overlay's own.
|
|
336
422
|
function leaveTo(pane, args) {
|
|
337
423
|
var hash = '#/' + [pane].concat(args || []).map(encodeURIComponent).join('/');
|
|
338
|
-
var replace = O.pushed;
|
|
424
|
+
var replace = O.pushed, extra = O.pushed && O.depth > 1 ? O.depth - 1 : 0;
|
|
339
425
|
close('leave');
|
|
340
|
-
|
|
426
|
+
O.depth = 1;
|
|
427
|
+
if (!replace) { W.go(pane, args || []); return; }
|
|
428
|
+
if (extra) { quietBack(extra, function () { location.replace(hash); }); return; }
|
|
429
|
+
location.replace(hash);
|
|
430
|
+
}
|
|
431
|
+
// A Back step the overlay takes itself (X, Back to sessions, leaving for a tab): its popstate is ours, not the phone's
|
|
432
|
+
// Back, and `then` runs once it lands. If it never arrives (history.go with nothing to go back to, or a browser that
|
|
433
|
+
// drops it) the wait lapses after POP_WAIT_MS and `then` runs anyway, so a real Back later is never swallowed
|
|
434
|
+
// (6.99.30, QA: ignorePops stayed counted and ate the next Back).
|
|
435
|
+
var POP_WAIT_MS = 700;
|
|
436
|
+
function quietBack(n, then) {
|
|
437
|
+
var w = { then: then || null, timer: 0 };
|
|
438
|
+
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; };
|
|
439
|
+
w.timer = setTimeout(function () { if (drop() && w.then) w.then(); }, POP_WAIT_MS);
|
|
440
|
+
O.pops.push(w); O.ignorePops = O.pops.length;
|
|
441
|
+
try { if (n > 1 && typeof history.go === 'function') history.go(-n); else history.back(); }
|
|
442
|
+
catch (_e) { clearTimeout(w.timer); if (drop() && w.then) w.then(); }
|
|
341
443
|
}
|
|
342
444
|
// A reload with the overlay open keeps its history entry, and the entry before it is Today at the same address, so
|
|
343
445
|
// Back from there showed nothing (measured in Chrome 11/07: a same-document popstate). On load, app.js hands that
|
|
@@ -362,8 +464,12 @@
|
|
|
362
464
|
})();
|
|
363
465
|
});
|
|
364
466
|
}
|
|
365
|
-
|
|
366
|
-
|
|
467
|
+
// Back from an enlarged image (panes/reader-media.js pushes one entry over the sheet's) is the image's Back: the
|
|
468
|
+
// viewer closes and the sheet, its scroll and a half-typed note stay (11/08). The viewer says which pops are its own.
|
|
469
|
+
function imagePop(e) { return !!(W.readerMedia && typeof W.readerMedia.popIsMine === 'function' && W.readerMedia.popIsMine(e)); }
|
|
470
|
+
window.addEventListener('popstate', function (e) {
|
|
471
|
+
if (O.pops.length) { var w = O.pops.shift(); clearTimeout(w.timer); O.ignorePops = O.pops.length; if (w.then) w.then(); return; }
|
|
472
|
+
if (imagePop(e)) return;
|
|
367
473
|
// The phone's Back from a session opened off the pet's list goes back to that list, as Back to sessions does.
|
|
368
474
|
if (O.open && O.back) { backToList(); return; }
|
|
369
475
|
if (O.open) close('pop');
|
|
@@ -372,7 +478,7 @@
|
|
|
372
478
|
function backToList(fromButton) {
|
|
373
479
|
var b = O.back;
|
|
374
480
|
if (!b) return;
|
|
375
|
-
if (fromButton && O.depth > 1)
|
|
481
|
+
if (fromButton && O.depth > 1) quietBack(1);
|
|
376
482
|
O.depth = 1;
|
|
377
483
|
open('pet', '', O.opener, { again: true });
|
|
378
484
|
var body = $('ta-body'); if (body) body.scrollTop = b.scroll || 0;
|
|
@@ -423,6 +529,7 @@
|
|
|
423
529
|
if (what === 'open-card') return leaveTo('work', id ? [id] : []);
|
|
424
530
|
if (what === 'open-session') return O.r ? leaveTo('sessions', [O.r.provider, O.r.id]) : null;
|
|
425
531
|
if (what === 'open-sessions') return leaveTo('sessions', []);
|
|
532
|
+
if (what === 'q-retry') { if (W.sessionQuestions && W.sessionQuestions.retry) W.sessionQuestions.retry(); return; }
|
|
426
533
|
// A row of the pet's list opens that session's own sheet in its place, with Back to sessions (and the phone's Back)
|
|
427
534
|
// returning to the list where it was scrolled; X still closes the overlay.
|
|
428
535
|
if (what === 'pet-open') {
|
|
@@ -454,11 +561,18 @@
|
|
|
454
561
|
function onSubmit(e) {
|
|
455
562
|
if (e.target && e.target.id === 'ta-form') { e.preventDefault(); sendReply(); }
|
|
456
563
|
}
|
|
457
|
-
// The keyboard rises over the bottom of the sheet: keep the box in view as it does.
|
|
564
|
+
// The keyboard rises over the bottom of the sheet: keep the box in view as it does. Any field that types counts
|
|
565
|
+
// (the reply box, a question card's Other): a hardware keyboard's strip is known only by focus (6.99.30).
|
|
458
566
|
function onFocusIn(e) {
|
|
459
|
-
if (!e.target ||
|
|
567
|
+
if (!e.target || !typing()) return;
|
|
460
568
|
fit();
|
|
461
|
-
|
|
569
|
+
var reply = e.target.id === 'ta-input';
|
|
570
|
+
setTimeout(function () {
|
|
571
|
+
if (!O.open) return;
|
|
572
|
+
applyFit();
|
|
573
|
+
var f = reply ? $('ta-form') : e.target;
|
|
574
|
+
if (f && f.scrollIntoView) f.scrollIntoView({ block: 'nearest' });
|
|
575
|
+
}, 250);
|
|
462
576
|
}
|
|
463
577
|
|
|
464
578
|
// ═════════════ From today's meetings ═════════════
|
|
@@ -706,7 +820,7 @@
|
|
|
706
820
|
setHead(kind === 'need' ? (O.item ? str(O.item.sessionTitle) || str(O.item.title) || 'Handoff' : 'Needs you')
|
|
707
821
|
: kind === 'wait' ? (O.wait ? O.wait.title : 'A session is waiting for you') : (O.row ? K.title(O.row) : 'Session'));
|
|
708
822
|
var body = $('ta-body');
|
|
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>';
|
|
823
|
+
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>';
|
|
710
824
|
var foot = $('ta-foot');
|
|
711
825
|
if (foot) { foot.innerHTML = O.r ? composerHtml(kind) : ''; foot.hidden = false; }
|
|
712
826
|
var input = $('ta-input');
|
|
@@ -733,8 +847,37 @@
|
|
|
733
847
|
if (kind === 'run') { paintLive(); watchStream(O.r); }
|
|
734
848
|
loadLast(O.r);
|
|
735
849
|
K.probe(O.r);
|
|
850
|
+
loadNotSent(O.r);
|
|
736
851
|
if (kind === 'need') recheckNeed(key);
|
|
737
852
|
}
|
|
853
|
+
// 6.99.30: a queued message for this session that the server refused or let expire, said here as in the reader
|
|
854
|
+
// ("Not sent: <reason>"), from the same queue read (app.js sessionKit.notSent). The Sessions list chip follows it.
|
|
855
|
+
function loadNotSent(r) {
|
|
856
|
+
if (!r || !K.notSent || !K.answer) return;
|
|
857
|
+
K.answer(K.path(r.provider, r.id, '/queued-turns')).then(function (q) {
|
|
858
|
+
if (O.r !== r) return;
|
|
859
|
+
var rows = (q && q.status === 200 && q.body && q.body.turns) || [];
|
|
860
|
+
if (q && q.status === 200) K.notSent.note(r.provider, r.id, rows);
|
|
861
|
+
var box = $('ta-notsent'); if (!box) return;
|
|
862
|
+
box.innerHTML = K.notSent.rows(rows).map(function (t) {
|
|
863
|
+
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>';
|
|
864
|
+
}).join('');
|
|
865
|
+
}, function () {});
|
|
866
|
+
}
|
|
867
|
+
// Back from the back-forward cache with a session open (pageshow persisted): the stream again, the last reply, the
|
|
868
|
+
// write gate and the questions, as on opening.
|
|
869
|
+
function revive() {
|
|
870
|
+
var r = O.r;
|
|
871
|
+
if (!r || !O.open) return;
|
|
872
|
+
if (O.open.kind === 'run') {
|
|
873
|
+
if (O.stream) { try { O.stream.close(); } catch (_e) {} O.stream = null; }
|
|
874
|
+
watchStream(r); paintLive();
|
|
875
|
+
}
|
|
876
|
+
loadLast(r);
|
|
877
|
+
K.probe(r);
|
|
878
|
+
loadNotSent(r);
|
|
879
|
+
if (W.sessionQuestions && W.sessionQuestions.nudge) W.sessionQuestions.nudge();
|
|
880
|
+
}
|
|
738
881
|
function stopSession() {
|
|
739
882
|
if (K.imagesStop) K.imagesStop($('ta-last'));
|
|
740
883
|
if (O.stream) { try { O.stream.close(); } catch (_e) {} O.stream = null; }
|
|
@@ -811,6 +954,8 @@
|
|
|
811
954
|
el.classList.toggle('hidden', !box.atts.length);
|
|
812
955
|
}
|
|
813
956
|
paintClip(); paintSendButton();
|
|
957
|
+
// QA 7.0.0: the "Working right now" note says whether files go with the message.
|
|
958
|
+
if (O.r && O.r.verdict) paintVerdict();
|
|
814
959
|
}
|
|
815
960
|
// Send waits while a file uploads. A failed file never greys it out (QA 6.99.27): a tap says why, as the question
|
|
816
961
|
// card does.
|
|
@@ -877,8 +1022,15 @@
|
|
|
877
1022
|
// peek and no Stop: it is not at work (W4-r2).
|
|
878
1023
|
html += '<p class="ta-sub">' + T.badge(O.r ? O.r.provider : 'claude') + ' <span>' + esc(O.wait ? O.wait.why : 'Waiting for you') + '</span></p>';
|
|
879
1024
|
var where = waitWhere();
|
|
1025
|
+
// W13 (QA week of 11/01-11/08): the words Today's row showed for it (its `ask`) stay here, read-only, when no card
|
|
1026
|
+
// draws them: the Mac line and a failed read alike. Never twice: not when the line above already says them.
|
|
1027
|
+
var ask = waitAsk();
|
|
1028
|
+
if (ask && (where === 'mac' || where === 'failed') && !O.macCount) html += '<blockquote class="ta-quote ta-mac-ask">' + esc(ask) + '</blockquote>';
|
|
880
1029
|
// The Mac card says it itself when there is one; say it here only when there is none.
|
|
881
1030
|
if (where === 'mac' && !O.macCount) html += '<p class="ta-sub ta-mac-line">' + esc(macLine()) + '</p>';
|
|
1031
|
+
// W13: the questions could not be read, so whether one waits here is unknown. Say so, never "at the Mac", and read again.
|
|
1032
|
+
if (where === 'failed') html += '<p class="ta-note ta-q-failed" role="status">' + esc('Couldn\u2019t load this session\u2019s questions, so one may be waiting here.') +
|
|
1033
|
+
' <button type="button" class="link-btn" data-ta="q-retry">Retry</button></p>';
|
|
882
1034
|
if (where === 'answered') html += '<p class="ta-sub ta-mac-line">' + esc((W.sessionQuestions && W.sessionQuestions.answeredLine) || 'Answered. The session carries on.') + '</p>';
|
|
883
1035
|
if (where === 'reply') html += '<p class="ta-sub">' + esc('Answer it in a reply below, or open the session.') + '</p>';
|
|
884
1036
|
} else if (O.row || O.r) {
|
|
@@ -895,17 +1047,25 @@
|
|
|
895
1047
|
box.innerHTML = html;
|
|
896
1048
|
// The reply box shows only where a reply can be the answer; what was typed stays in it, hidden.
|
|
897
1049
|
var foot = $('ta-foot');
|
|
898
|
-
if (foot && O.open.kind === 'wait') foot.hidden =
|
|
1050
|
+
if (foot && O.open.kind === 'wait') { var w = waitWhere(); foot.hidden = w === 'failed' ? !!(O.r && O.r.provider === 'claude') : w !== 'reply'; }
|
|
899
1051
|
}
|
|
900
1052
|
// Where a waiting session's question is answered: 'here' (its cards in this sheet), 'answered' (on some screen just
|
|
901
1053
|
// now), 'mac' (a Claude session's own prompt, or a held question that went back to the Mac), or 'reply' (a Codex or
|
|
902
1054
|
// Cursor session asking in words).
|
|
1055
|
+
// W13: 'failed' when the last questions read failed and no card is drawn: a Claude reply still answers nothing, so the
|
|
1056
|
+
// reply box stays a Codex or Cursor one.
|
|
903
1057
|
function waitWhere() {
|
|
904
1058
|
if (O.qCount) return 'here';
|
|
905
1059
|
if (O.r && W.sessionQuestions && W.sessionQuestions.answeredFor && W.sessionQuestions.answeredFor(O.r.id)) return 'answered';
|
|
1060
|
+
if (!O.macCount && W.sessionQuestions && W.sessionQuestions.loadState && W.sessionQuestions.loadState() === 'failed') return 'failed';
|
|
906
1061
|
if (O.macCount || (O.r && O.r.provider === 'claude') || (O.wait && O.wait.where === 'mac')) return 'mac';
|
|
907
1062
|
return 'reply';
|
|
908
1063
|
}
|
|
1064
|
+
// The question in Today's own words (its Needs you row), unless the line above the sheet already reads exactly that.
|
|
1065
|
+
function waitAsk() {
|
|
1066
|
+
var a = O.wait && O.wait.ask ? String(O.wait.ask).trim() : '';
|
|
1067
|
+
return a && !(O.wait.why && String(O.wait.why).trim() === a) ? a : '';
|
|
1068
|
+
}
|
|
909
1069
|
function macLine() { return (W.sessionQuestions && W.sessionQuestions.macLine) || "Waiting on your Mac's prompt. Answer it on the Mac (or through Screens)."; }
|
|
910
1070
|
|
|
911
1071
|
// ═════════════ The pet's sessions ═════════════
|
|
@@ -967,7 +1127,7 @@
|
|
|
967
1127
|
function loadLast(r) {
|
|
968
1128
|
var request = r.lastRead = {};
|
|
969
1129
|
O.lastState = 'loading'; O.lastWhy = ''; paintLast();
|
|
970
|
-
return K.answer(K.path(r.provider, r.id, '?turns=6'), { timeoutMs: 20000 }).then(function (a) {
|
|
1130
|
+
return K.answer(K.path(r.provider, r.id, '?turns=6&cards=1'), { timeoutMs: 20000 }).then(function (a) {
|
|
971
1131
|
if (O.r !== r || r.lastRead !== request) return;
|
|
972
1132
|
if (a.status === 404) { O.lastState = 'none'; O.lastWhy = 'This session is not on your Mac any more.'; paintLast(); return; }
|
|
973
1133
|
if (a.status !== 200) { O.lastState = 'error'; paintLast(); return; }
|
|
@@ -975,7 +1135,8 @@
|
|
|
975
1135
|
if (!O.row) { O.row = d; paintMeta(); if (O.open && O.open.kind === 'run') setHead(K.title(d)); }
|
|
976
1136
|
var turns = Array.isArray(d.recent_turns) ? d.recent_turns : [];
|
|
977
1137
|
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,
|
|
1138
|
+
O.last = lastA ? { text: String(lastA.text || lastA.content || '').trim(), at: lastA.at, toolImages: lastA.toolImages, toolImagesMore: lastA.toolImagesMore, outputLinks: lastA.outputLinks,
|
|
1139
|
+
outputImages: lastA.outputImages, outputFiles: lastA.outputFiles, previewsOff: lastA.previewsOff } : null;
|
|
979
1140
|
O.lastState = O.last ? 'ready' : 'none';
|
|
980
1141
|
O.lastWhy = '';
|
|
981
1142
|
paintLast();
|
|
@@ -985,7 +1146,7 @@
|
|
|
985
1146
|
var box = $('ta-last'); if (!box) return;
|
|
986
1147
|
// Retain the actual reply nodes (and its open image) during refreshes and transient errors.
|
|
987
1148
|
// 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) : '']) : '';
|
|
1149
|
+
var signature = O.last ? JSON.stringify([O.last.text, O.last.at, K.imageHtml ? K.imageHtml(O.last) : '', K.linksAttr ? K.linksAttr(O.last) : '']) : '';
|
|
989
1150
|
if (signature && box.__taLastSignature === signature) {
|
|
990
1151
|
var refresh = $('ta-last-refresh');
|
|
991
1152
|
if (refresh) refresh.textContent = O.lastState === 'loading' ? 'Checking for a newer reply…'
|
|
@@ -1001,8 +1162,10 @@
|
|
|
1001
1162
|
if (!O.last) { box.innerHTML = ''; return; }
|
|
1002
1163
|
var at = W.toDate(O.last.at), long = O.last.text.length > 700;
|
|
1003
1164
|
box.innerHTML = '<section class="ta-last" aria-label="Last reply"><p class="ta-last-head">' + esc('Last reply' + (at ? ', ' + W.clock(at) : '')) + '</p>' +
|
|
1004
|
-
|
|
1005
|
-
|
|
1165
|
+
(K.imageHtml ? K.imageHtml(O.last, { part: 'before' }) : '') +
|
|
1166
|
+
// Preview engine phase 1: the reply's link cards, as in the reader.
|
|
1167
|
+
'<div class="sx-md ta-last-text' + (long ? ' clamped' : '') + '" id="ta-last-text"' + (K.linksAttr ? K.linksAttr(O.last) : '') + '>' + K.markdown(O.last.text.slice(0, 12000), O.last.outputLinks) + '</div>' +
|
|
1168
|
+
(long ? '<button type="button" class="link-btn" data-ta="more">Show all of it</button>' : '') + (K.imageHtml ? K.imageHtml(O.last, { part: 'after' }) : '') + '<p id="ta-last-refresh" class="ta-sub" role="status"></p></section>';
|
|
1006
1169
|
if (K.imagesMount) K.imagesMount(box, O.r);
|
|
1007
1170
|
}
|
|
1008
1171
|
|
|
@@ -1074,6 +1237,7 @@
|
|
|
1074
1237
|
if (w === 'Working' || w === 'Waiting') {
|
|
1075
1238
|
O.clock = setInterval(function () {
|
|
1076
1239
|
if (O.r !== r) { clearInterval(O.clock); return; }
|
|
1240
|
+
if (document.hidden) return; // a hidden page spends nothing; coming back repaints at once (visibilitychange)
|
|
1077
1241
|
var c = $('ta-clock'); if (c) c.textContent = K.feed.elapsed(r.feed, r);
|
|
1078
1242
|
if (K.feed.gateStale(r)) K.probe(r);
|
|
1079
1243
|
var rs = $('ta-run-state'); if (rs) rs.textContent = runWords();
|
|
@@ -1143,7 +1307,8 @@
|
|
|
1143
1307
|
'</div></div>';
|
|
1144
1308
|
} else if (v && v.attachable === false && /^(native_thread_working|native_target_busy)$/.test(v.reason)) {
|
|
1145
1309
|
// 6.99.29: "Working right now" only while the card's one state says so.
|
|
1146
|
-
|
|
1310
|
+
// QA 7.0.0: files in this sheet's box wait for the turn end, so the note says so (app.js sxParkLine).
|
|
1311
|
+
if (K.feed.parkNoteShows(r)) html = '<p class="sx-park">' + esc(K.parkCopy(r.provider, r, !!(filesOn() && filesState(filesBox(r)) !== 'none'))) + '</p>';
|
|
1147
1312
|
} else if (v && v.attachable === false && v.reasonCopy) {
|
|
1148
1313
|
html = '<div class="sx-box refusal" role="status"><p>' + esc(v.reasonCopy) + '</p></div>';
|
|
1149
1314
|
} else if (v && v.caution) {
|
|
@@ -1219,6 +1384,8 @@
|
|
|
1219
1384
|
clearInterval(O.watch);
|
|
1220
1385
|
var started = Date.now();
|
|
1221
1386
|
O.watch = setInterval(function () {
|
|
1387
|
+
// Hidden: no reads, and the three minutes count only while the page is in view (6.99.30).
|
|
1388
|
+
if (document.hidden) { started += 3000; return; }
|
|
1222
1389
|
if (O.r !== r || Date.now() - started > WATCH_MS) { clearInterval(O.watch); return; }
|
|
1223
1390
|
K.answer('/api/agent-sessions/bindings/' + encodeURIComponent(bindingId) + '/turns/' + encodeURIComponent(key)).then(function (s) {
|
|
1224
1391
|
if (O.r !== r) return;
|
|
@@ -1298,6 +1465,6 @@
|
|
|
1298
1465
|
open: open, close: close, restore: restore, summary: summary, summaryMax: SUMMARY_MAX,
|
|
1299
1466
|
isOpen: function () { return !!O.open; },
|
|
1300
1467
|
_state: O, _decide: decide, _undo: undo, _intakeHtml: intakeHtml, _send: sendReply, _view: VIEW, _paintVerdict: paintVerdict,
|
|
1301
|
-
_paintLive: paintLive, _paintMeta: paintMeta, _onClick: onClick, _expire: expireLater, _kb: KB, _fitNow: applyFit
|
|
1468
|
+
_paintLive: paintLive, _paintMeta: paintMeta, _onClick: onClick, _expire: expireLater, _kb: KB, _fitNow: applyFit, _revive: revive
|
|
1302
1469
|
});
|
|
1303
1470
|
})();
|
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
.wk-filters { width: 100%; justify-content: flex-start; }
|
|
60
60
|
.wk-filters .cx-search { max-width: none; flex-basis: 100%; }
|
|
61
61
|
.wk-pick { flex: 1 1 140px; flex-direction: column; align-items: stretch; gap: 4px; font-size: 11.5px; }
|
|
62
|
-
.wk-pick select.field {
|
|
62
|
+
.wk-pick select.field { font-size: 15px; width: 100%; background-position: calc(100% - 16px) 17px, calc(100% - 11px) 17px; }
|
|
63
63
|
.wk-text-btn { min-height: 36px; font-size: 14px; }
|
|
64
64
|
}
|
|
65
65
|
|
|
@@ -313,3 +313,13 @@
|
|
|
313
313
|
.wk-pick-row { display: flex; align-items: center; gap: 10px; padding: 8px 2px; border-top: 1px solid var(--hairline); }
|
|
314
314
|
.wk-pick-row > div { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
|
|
315
315
|
.wk-pick-row strong { font-size: 13.5px; }
|
|
316
|
+
|
|
317
|
+
/* Duo tables: the divider between a card and its side panel (app.js splitKit, styled in app.css; work.js DIVIDER). From
|
|
318
|
+
900 px only, where both show. Its 22 px column is the old gap, so the default layout is unchanged; a dragged width
|
|
319
|
+
sizes the side panel, clamped so it keeps 320 px and the card 380 px. */
|
|
320
|
+
@media (min-width: 900px) {
|
|
321
|
+
.wk-detail.split-side { grid-template-columns: minmax(0, 1fr) 22px minmax(320px, 400px); column-gap: 0; }
|
|
322
|
+
.wk-detail.split-side.split-sized { grid-template-columns: minmax(0, 1fr) 22px clamp(320px, var(--split-side-share, 30%), calc(100% - 402px)); }
|
|
323
|
+
.wk-detail.split-side > .split-handle { display: flex; }
|
|
324
|
+
}
|
|
325
|
+
@media (min-width: 1300px) { .wk-detail.split-side { grid-template-columns: minmax(0, 1fr) 22px 452px; } }
|
|
@@ -1126,6 +1126,13 @@
|
|
|
1126
1126
|
(str(t.source) ? fact('Source', t.source) : '') + (str(t.runAt) ? fact('Scheduled', t.runAt) : '') +
|
|
1127
1127
|
'<p class="wk-fine">Task editing, scheduling, and completion stay attached to the original task. Agent output does not change its completion state.</p></section>';
|
|
1128
1128
|
}
|
|
1129
|
+
// Duo tables: the divider between a card and its side panel (app.js splitKit, 900 px and up). The side panel is the
|
|
1130
|
+
// column on the right, so it grows as the line moves left; it keeps 320 px and the card 380 px, past a 22 px divider
|
|
1131
|
+
// that is the old gap. A repaint in place keeps the same divider. (DIVIDER, since SPLIT is the Sort split.)
|
|
1132
|
+
// A page without the kit (a test's stand-in W) draws no divider and keeps working.
|
|
1133
|
+
var SPLIT_KIT = W.splitKit || { handle: function () { return ''; }, mount: function () { return null; } };
|
|
1134
|
+
var DIVIDER = { key: 'cos_web_work_split', label: 'Side panel', side: 'end', sized: '.wk-detail-side', cssVar: '--split-side-share', media: '(min-width: 900px)', listMin: 320, readerMin: 380, handleW: 22 };
|
|
1135
|
+
var SPLIT_HANDLE = SPLIT_KIT.handle({ id: 'wk-handle', controls: 'wk-part-side', label: DIVIDER.label });
|
|
1129
1136
|
function renderDetail(arg) {
|
|
1130
1137
|
var it = itemFor(arg);
|
|
1131
1138
|
S.viewWaiting = !it && (S.loading || !S.tasks);
|
|
@@ -1151,11 +1158,12 @@
|
|
|
1151
1158
|
} else {
|
|
1152
1159
|
S.chatDeferred = false;
|
|
1153
1160
|
pane.innerHTML = '<div class="cx wk" data-tint="work">' + backBar(backLabel()) + '<div id="wk-part-notices" class="wk-part">' + parts.notices + '</div>' +
|
|
1154
|
-
'<div class="wk-detail" data-wid="' + esc(it.wid) + '"><div class="wk-detail-main"><div id="wk-part-main" class="wk-part">' + parts.main + '</div>' + chatSectionHtml(it) + '</div>' +
|
|
1155
|
-
'<aside class="wk-detail-side" id="wk-part-side">' + parts.side + '</aside></div></div>';
|
|
1161
|
+
'<div class="wk-detail' + (SPLIT_HANDLE ? ' split-side' : '') + '" data-wid="' + esc(it.wid) + '"><div class="wk-detail-main"><div id="wk-part-main" class="wk-part">' + parts.main + '</div>' + chatSectionHtml(it) + '</div>' +
|
|
1162
|
+
SPLIT_HANDLE + '<aside class="wk-detail-side" id="wk-part-side">' + parts.side + '</aside></div></div>';
|
|
1156
1163
|
[['wk-part-notices', parts.notices], ['wk-part-main', parts.main], ['wk-part-side', parts.side]].forEach(function (p) { var el = $(p[0]); if (el) el.__html = p[1]; });
|
|
1157
1164
|
var hist = $('wk-part-chat'); if (hist) hist.__html = hist.innerHTML;
|
|
1158
1165
|
}
|
|
1166
|
+
SPLIT_KIT.mount(pane.querySelector('.wk-detail'), $('wk-handle'), DIVIDER);
|
|
1159
1167
|
chatSyncComposer(it);
|
|
1160
1168
|
if (!S.activity[it.wid] && !S.activityError[it.wid] && !S.activityLoading[it.wid]) {
|
|
1161
1169
|
S.activityLoading[it.wid] = true;
|
|
@@ -77,10 +77,13 @@
|
|
|
77
77
|
}
|
|
78
78
|
// About 16 reads: never two runs at once, never more than one a minute, two at a time through storyApi, and never
|
|
79
79
|
// ahead of the pane the person just opened (Safari keeps 6 connections to the Mac over HTTP/1.1).
|
|
80
|
-
//
|
|
81
|
-
// when the menu opens rather than costing the pane its connections on every visit.
|
|
80
|
+
// Below 600 px wide (the folded Duo) the rail is hidden and the stories show only inside the section menu, so they
|
|
81
|
+
// load when the menu opens rather than costing the pane its connections on every visit. This is the WIDTH test on
|
|
82
|
+
// purpose, not W.isPhone: from 7.0.2 a short touch screen at any width counts as a phone (the unfolded Duo, 951x539),
|
|
83
|
+
// and it still has the rail, its story lines and hot dots.
|
|
84
|
+
function railHidden() { return !window.matchMedia('(min-width: 600px)').matches; }
|
|
82
85
|
function loadStories(force) {
|
|
83
|
-
if (!force &&
|
|
86
|
+
if (!force && railHidden()) return;
|
|
84
87
|
if (Date.now() - storiesAt < 60000) return;
|
|
85
88
|
storiesAt = Date.now();
|
|
86
89
|
var now = new Date();
|
|
@@ -210,17 +213,17 @@
|
|
|
210
213
|
setStory('threads', active + ' active' + (list[0] && list[0].name ? ', newest ' + clip(list[0].name, 40) : ''), false);
|
|
211
214
|
});
|
|
212
215
|
|
|
213
|
-
// Memories (TodayHome.memoriesStory): n is the
|
|
214
|
-
//
|
|
215
|
-
//
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
var n =
|
|
216
|
+
// Memories (TodayHome.memoriesStory): n is the needs-review count (capped, from the bridge's
|
|
217
|
+
// status, which runs the same CLI Control's helper runs, judged by memory-review.js); total is
|
|
218
|
+
// every item that needs review by the same predicate, never the To review list's length (that
|
|
219
|
+
// list also holds suggestions and proposals with no decision waiting). "Next n of total" when
|
|
220
|
+
// more need review than are shown, else "n to review".
|
|
221
|
+
soft(storyApi('/api/web-bridge/memories', { method: 'POST', body: { id: 'rail-status', op: 'status', args: {} } })).then(function (r) {
|
|
222
|
+
var d = r && r.details ? r.details : {};
|
|
223
|
+
var n = d.learningToReview;
|
|
221
224
|
if (typeof n !== 'number') return setStory('memories', FALLBACK.memories, false);
|
|
222
225
|
if (n <= 0) return setStory('memories', 'Nothing to review', false);
|
|
223
|
-
var total =
|
|
226
|
+
var total = typeof d.learningToReviewTotal === 'number' ? d.learningToReviewTotal : null;
|
|
224
227
|
setStory('memories', total != null && total > n ? 'Next ' + n + ' of ' + total : n + ' to review', true);
|
|
225
228
|
});
|
|
226
229
|
// Dictation lives on the Mac (its history and suggestions are local files).
|
|
@@ -260,9 +263,9 @@
|
|
|
260
263
|
}
|
|
261
264
|
crumbs.innerHTML = parts.join('');
|
|
262
265
|
$('nav-back').disabled = onToday;
|
|
263
|
-
// The phone top bar names the detail too, so a reader says where it is.
|
|
266
|
+
// The phone top bar (below 600 px, the same width test) names the detail too, so a reader says where it is.
|
|
264
267
|
var label = $('section-name');
|
|
265
|
-
if (label && detail &&
|
|
268
|
+
if (label && detail && railHidden()) label.textContent = detail;
|
|
266
269
|
}
|
|
267
270
|
|
|
268
271
|
document.addEventListener('click', function (e) {
|
|
@@ -286,6 +289,9 @@
|
|
|
286
289
|
});
|
|
287
290
|
|
|
288
291
|
W.onRoute(paintNavbar);
|
|
292
|
+
// 6.99.30: the server line moves the moment a request fails to reach the Mac or reaches it again (app.js noteServer),
|
|
293
|
+
// not on the five-minute check. Back up, the health read confirms it and refreshes what rides on it.
|
|
294
|
+
if (W.onServer) W.onServer(function (up) { setServer(up); if (up) checkServer(); });
|
|
289
295
|
W.onShell(function () {
|
|
290
296
|
Object.keys(FALLBACK).forEach(function (k) { setStory(k, FALLBACK[k], false); });
|
|
291
297
|
setTimeout(function () { loadStories(); checkServer(); }, 2500);
|