@gotcos/glasses-server 6.99.33 → 7.0.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +37 -0
- package/CHANGELOG.md +587 -0
- package/README.md +3 -1
- package/docs/web-key-verifier-contract.md +378 -0
- package/managed-runtime-contract.json +12 -1
- package/package.json +10 -3
- package/server/bootstrap.ts +10 -0
- package/server/index.ts +155 -30
- package/server/lib/agent-session-output-images.ts +627 -49
- package/server/lib/agent-session-recent.ts +321 -0
- package/server/lib/agent-session-store.ts +27 -14
- package/server/lib/agent-session-tool-images.ts +224 -0
- package/server/lib/agent-session-turns.ts +19 -2
- package/server/lib/api-auth.ts +43 -1
- package/server/lib/ask-work-plan.ts +3 -1
- package/server/lib/calendar-meetings.ts +70 -0
- package/server/lib/calendar-source.ts +536 -0
- package/server/lib/claude-hooks-installer.ts +111 -12
- package/server/lib/codex-bridge.ts +4 -1
- package/server/lib/codex-run-ledger.ts +29 -0
- package/server/lib/context-builder.ts +2 -1
- package/server/lib/conversation.ts +56 -28
- package/server/lib/cors-reject-log.ts +51 -0
- package/server/lib/disk-io-meter.ts +202 -0
- package/server/lib/drop-p2-runtime.ts +1 -1
- package/server/lib/drop-service.ts +47 -12
- package/server/lib/drop-store.ts +31 -5
- package/server/lib/event-loop-stall.ts +62 -0
- package/server/lib/full-hold-engine.ts +114 -0
- package/server/lib/full-hold.ts +124 -0
- package/server/lib/g2-turn-provenance.ts +128 -9
- package/server/lib/learning-needs-you.ts +24 -7
- package/server/lib/listener-startup.ts +18 -0
- package/server/lib/live-events.ts +533 -0
- package/server/lib/local-path-redact.ts +272 -0
- package/server/lib/meeting-ask-orientation.ts +40 -6
- package/server/lib/meeting-ask-runner.ts +30 -10
- package/server/lib/meeting-batch-transcribe.ts +32 -5
- package/server/lib/morning-brief-runtime.ts +4 -2
- package/server/lib/native-mac-caller.ts +29 -0
- package/server/lib/permission-broker.ts +443 -11
- package/server/lib/ping-engine.ts +1 -1
- package/server/lib/ping-sources.ts +11 -6
- package/server/lib/preview-content.ts +172 -0
- package/server/lib/preview-error-code.ts +10 -0
- package/server/lib/preview-folders-setting.ts +133 -0
- package/server/lib/preview-log-throttle.ts +18 -0
- package/server/lib/preview-thumbs.ts +139 -0
- package/server/lib/secret-path-guard.ts +83 -0
- package/server/lib/session-cache-writer.ts +8 -1
- package/server/lib/session-files-setting.ts +88 -0
- package/server/lib/session-hook-events.ts +23 -1
- package/server/lib/session-hooks-runtime.ts +34 -0
- package/server/lib/session-links.ts +280 -0
- package/server/lib/session-log.ts +313 -4
- package/server/lib/session-state-derive.ts +36 -0
- package/server/lib/session-stream-events.ts +25 -1
- package/server/lib/thread-turn-queue.ts +10 -0
- package/server/lib/today-brief.ts +640 -85
- package/server/lib/turn-end-ask.ts +320 -0
- package/server/lib/turn-footer.ts +3 -3
- package/server/lib/web-control-integrity.ts +160 -0
- package/server/lib/web-control-offnet-page.ts +58 -0
- package/server/lib/web-control-static.ts +53 -9
- package/server/lib/web-key-authority.ts +372 -0
- package/server/lib/web-key-envelope.ts +98 -0
- package/server/lib/web-key-handoff.ts +135 -0
- package/server/lib/web-key-origin.ts +21 -0
- package/server/lib/web-key-passkey.ts +155 -0
- package/server/lib/web-key-registry.ts +825 -0
- package/server/lib/web-key-signer-app.ts +152 -0
- package/server/lib/web-memories-bridge.ts +3 -1
- package/server/lib/web-session-auth.ts +133 -15
- package/server/lib/whisper-local.ts +93 -7
- package/server/lib/whisper-preview.ts +5 -3
- package/server/lib/work-activity.ts +56 -6
- package/server/lib/work-orchestrator/snapshot.ts +17 -3
- package/server/lib/work-review-runtime.ts +17 -4
- package/server/lib/work-session-activity.ts +260 -0
- package/server/lib/your-move-count.ts +9 -3
- package/server/routes/agent-session-bindings.ts +29 -1
- package/server/routes/agent-session-stream.ts +23 -0
- package/server/routes/agent-sessions.ts +146 -11
- package/server/routes/ask-work-plan.ts +4 -1
- package/server/routes/claude-sessions.ts +5 -1
- package/server/routes/drops.ts +7 -2
- package/server/routes/g2-turns.ts +35 -0
- package/server/routes/health.ts +46 -1
- package/server/routes/live-events.ts +334 -0
- package/server/routes/permission-broker.ts +95 -5
- package/server/routes/query-jobs.ts +3 -0
- package/server/routes/session-preview.ts +200 -0
- package/server/routes/sessions.ts +15 -24
- package/server/routes/thread-turn-queue.ts +223 -12
- package/server/routes/today-brief.ts +134 -44
- package/server/routes/web-control.ts +37 -4
- package/server/routes/web-key.ts +284 -0
- package/server/routes/welcome-context.ts +28 -4
- package/server/routes/work-board.ts +9 -2
- package/server/routes/work-orchestrator.ts +12 -7
- package/server/scripts/write-web-control-manifest.mjs +43 -0
- package/server/web-control/.manifest.json +44 -0
- package/server/web-control/app.css +76 -1
- package/server/web-control/app.js +2247 -98
- package/server/web-control/apple-touch-icon.png +0 -0
- package/server/web-control/control.css +25 -0
- package/server/web-control/index.html +21 -3
- package/server/web-control/memory-review.js +100 -0
- package/server/web-control/panes/cos-chat.css +94 -9
- package/server/web-control/panes/cos-chat.js +628 -44
- package/server/web-control/panes/meetings.css +18 -2
- package/server/web-control/panes/meetings.js +24 -8
- package/server/web-control/panes/memories.css +17 -2
- package/server/web-control/panes/memories.js +127 -42
- package/server/web-control/panes/messages.js +21 -2
- package/server/web-control/panes/preview.js +318 -0
- package/server/web-control/panes/questions.js +162 -21
- package/server/web-control/panes/reader-media.js +409 -23
- package/server/web-control/panes/sessions.css +32 -3
- package/server/web-control/panes/settings.js +97 -6
- package/server/web-control/panes/today-actions.css +46 -0
- package/server/web-control/panes/today-actions.js +119 -17
- package/server/web-control/panes/work.css +13 -0
- package/server/web-control/panes/work.js +188 -37
- package/server/web-control/shell.js +17 -14
- package/server/web-signer/.manifest.json +11 -0
- package/server/web-signer/index.html +74 -0
- package/server/web-signer/signer-proof.js +348 -0
- package/server/web-signer/signer-store.js +39 -0
- package/server/web-signer/signer.css +23 -0
- package/server/web-signer/signer.js +341 -0
- package/shared/web-approval-packets.golden.json +124 -0
- package/shared/web-turn-envelope.ts +271 -0
|
@@ -161,9 +161,12 @@
|
|
|
161
161
|
.md-check { display: inline-block; width: 12px; height: 12px; border: 1.5px solid var(--sub); border-radius: 3px; margin-right: 7px; vertical-align: -1px; }
|
|
162
162
|
.md-check.on { background: var(--green); border-color: var(--green); }
|
|
163
163
|
.md-table-wrap { overflow-x: auto; margin: 0 0 1em; -webkit-overflow-scrolling: touch; }
|
|
164
|
-
|
|
164
|
+
/* Duo tables: separated borders so the held first column (control.css table.tf) keeps its line; the same look. */
|
|
165
|
+
.mt-md { --tf-bg: var(--page); }
|
|
166
|
+
.mt-answer .mt-md { --tf-bg: var(--qopen); }
|
|
167
|
+
.md-table { border-collapse: separate; border-spacing: 0; font-size: 14px; border: 1px solid var(--panel-border); border-radius: 6px; min-width: 60%; }
|
|
165
168
|
.md-table th, .md-table td { padding: 6px 12px; text-align: left; vertical-align: top; border-bottom: 1px solid var(--hairline); }
|
|
166
|
-
.md-table th { font-weight: 600; background: var(--qopen); }
|
|
169
|
+
.md-table th, .md-table.tf thead tr > :first-child { font-weight: 600; background: var(--qopen); }
|
|
167
170
|
.md-table tr:last-child td { border-bottom: 0; }
|
|
168
171
|
.md-table .md-key { color: var(--sub); white-space: nowrap; }
|
|
169
172
|
.md-code { overflow-x: auto; font: 400 12.5px/1.45 var(--mono); background: var(--qopen); border: 1px solid var(--panel-border); padding: 10px; border-radius: 8px; margin: 0 0 1em; }
|
|
@@ -358,3 +361,16 @@
|
|
|
358
361
|
.mt-lgrid { grid-template-columns: minmax(0, 1fr) minmax(280px, min(380px, 38%)); align-items: start; column-gap: 24px; }
|
|
359
362
|
.mt-lgrid > .mt-side-panel { position: sticky; top: calc(62px + var(--safe-top, 0px)); max-height: calc(100vh - 80px - var(--safe-top, 0px)); max-height: calc(100dvh - 80px - var(--safe-top, 0px)); overflow: auto; }
|
|
360
363
|
}
|
|
364
|
+
|
|
365
|
+
/* Duo tables: the divider between the notes or live transcript and the Ask panel (app.js splitKit, styled in app.css;
|
|
366
|
+
meetings.js SPLIT). From 860 px only, where both show. Its 24 px column is the old gap, so the default layout is
|
|
367
|
+
unchanged; a dragged width sizes the Ask panel, clamped so it keeps 280 px and the notes 380 px. */
|
|
368
|
+
@media (min-width: 860px) {
|
|
369
|
+
.mt-detail .mt-dbody.split-side { grid-template-columns: minmax(0, 1fr) 24px minmax(280px, min(380px, 38%)); column-gap: 0; }
|
|
370
|
+
.mt-detail .mt-dbody.split-side.split-sized { grid-template-columns: minmax(0, 1fr) 24px clamp(280px, var(--split-side-share, 30%), calc(100% - 404px)); }
|
|
371
|
+
.mt-detail .mt-dbody.split-side > .split-handle { display: flex; grid-column: 2; grid-row: 1 / span 20; top: calc(62px + var(--safe-top, 0px)); height: calc(100dvh - 80px - var(--safe-top, 0px)); }
|
|
372
|
+
.mt-detail .mt-dbody.split-side > :not(.mt-doc):not(.split-handle) { grid-column: 3; }
|
|
373
|
+
.mt-lgrid.split-side { grid-template-columns: minmax(0, 1fr) 24px minmax(280px, min(380px, 38%)); column-gap: 0; }
|
|
374
|
+
.mt-lgrid.split-side.split-sized { grid-template-columns: minmax(0, 1fr) 24px clamp(280px, var(--split-side-share, 30%), calc(100% - 404px)); }
|
|
375
|
+
.mt-lgrid.split-side > .split-handle { display: flex; top: calc(62px + var(--safe-top, 0px)); height: calc(100dvh - 80px - var(--safe-top, 0px)); }
|
|
376
|
+
}
|
|
@@ -391,13 +391,18 @@
|
|
|
391
391
|
case 'table': {
|
|
392
392
|
var kv = b.header.length === 2 && b.rows.length && b.rows.every(function (r) { return r.length === 2 && r[0].indexOf('**') === 0; });
|
|
393
393
|
var cols = Math.max(b.header.length, b.rows.reduce(function (m, r) { return Math.max(m, r.length); }, 0));
|
|
394
|
-
|
|
394
|
+
// Duo tables: the shared column floors (app.js tableFit), measured on the rows that show.
|
|
395
|
+
var fit = W.tableFit(kv ? b.rows : [b.header].concat(b.rows));
|
|
396
|
+
function row(r, tag, key, first) {
|
|
395
397
|
var h = '<tr>';
|
|
396
|
-
for (var c = 0; c < cols; c++)
|
|
398
|
+
for (var c = 0; c < cols; c++) {
|
|
399
|
+
var w = fit.width(c, first);
|
|
400
|
+
h += '<' + tag + (key && c === 0 ? ' class="md-key"' : '') + (w ? ' style="' + w + '"' : '') + '>' + fit.cell(c, inline(r[c] || ''), first) + '</' + tag + '>';
|
|
401
|
+
}
|
|
397
402
|
return h + '</tr>';
|
|
398
403
|
}
|
|
399
|
-
return '<div class="md-table-wrap"><table class="md-table' + (kv ? ' kv' : '') + '">' + (kv ? '' : '<thead>' + row(b.header, 'th') + '</thead>') +
|
|
400
|
-
'<tbody>' + b.rows.map(function (r) { return row(r, 'td', kv); }).join('') + '</tbody></table></div>';
|
|
404
|
+
return '<div class="md-table-wrap tf-wrap"><table class="md-table ' + fit.tableClass + (kv ? ' kv' : '') + '">' + (kv ? '' : '<thead>' + row(b.header, 'th', false, true) + '</thead>') +
|
|
405
|
+
'<tbody>' + b.rows.map(function (r, n) { return row(r, 'td', kv, kv && n === 0); }).join('') + '</tbody></table></div>';
|
|
401
406
|
}
|
|
402
407
|
case 'details': {
|
|
403
408
|
var sp = b.blocks.filter(function (x) { return x.t === 'speakers'; })[0];
|
|
@@ -1245,7 +1250,9 @@
|
|
|
1245
1250
|
else if (!d) body = '<p class="cx-note mt-wait">Loading meeting…</p>' + W.skeletonRows(5);
|
|
1246
1251
|
else {
|
|
1247
1252
|
var sessions = detailSessions();
|
|
1248
|
-
|
|
1253
|
+
// Duo tables: the line between the notes and the Ask panel drags where both show (860 px and up).
|
|
1254
|
+
var divider = sessions.length ? SPLIT_KIT.handle({ id: 'mt-dhandle', controls: 'mt-ask-' + safeId(sessions[0]), label: SPLIT.label }) : '';
|
|
1255
|
+
body = detailBarHtml(row, d) + '<div class="mt-dbody' + (divider ? ' split-side' : '') + '" id="mt-dbody">' + divider +
|
|
1249
1256
|
(row.sourceSessionId && sessions.length ? '<p class="mt-note-line">Ask history covers the whole original capture, including its other split pieces.</p>' : '') +
|
|
1250
1257
|
sessions.map(function (sid, i) {
|
|
1251
1258
|
A(sid).ctx = { saved: true, meeting: row, captureLabel: 'Original recording ' + (i + 1) + '. Questions use its saved cutoff; each recording keeps its own history.' };
|
|
@@ -1255,7 +1262,14 @@
|
|
|
1255
1262
|
'<article class="mt-doc">' + documentHtml(d) + '</article></div>';
|
|
1256
1263
|
}
|
|
1257
1264
|
pane.innerHTML = '<div class="cx mt mt-detail" data-tint="meetings">' + head + body + '</div>';
|
|
1258
|
-
|
|
1265
|
+
SPLIT_KIT.mount($('mt-dbody'), $('mt-dhandle'), SPLIT);
|
|
1266
|
+
}
|
|
1267
|
+
// Duo tables: the divider between a meeting's notes or live transcript and its Ask panel (app.js splitKit, 860 px and
|
|
1268
|
+
// up), one width for both views. The Ask panel is the column on the right, so it grows as the line moves left; it keeps
|
|
1269
|
+
// 280 px and the notes 380 px, past a 24 px divider that is the old gap.
|
|
1270
|
+
// A page without the kit (a test's stand-in W) draws no divider and keeps working.
|
|
1271
|
+
var SPLIT_KIT = W.splitKit || { handle: function () { return ''; }, mount: function () { return null; } };
|
|
1272
|
+
var SPLIT = { key: 'cos_web_meetings_split', label: 'Ask panel', side: 'end', sized: '.mt-side-panel', cssVar: '--split-side-share', media: '(min-width: 860px)', listMin: 280, readerMin: 380, handleW: 24 };
|
|
1259
1273
|
// ── Prep in a saved meeting (6.99.19) ─────────────────
|
|
1260
1274
|
// GET /api/prep/by-session/:sessionId for each of the meeting's recordings: what the prep focused on and its files,
|
|
1261
1275
|
// each opening from the Prep store. Nothing shows when there is no prep, or on a server without Prep.
|
|
@@ -2001,12 +2015,14 @@
|
|
|
2001
2015
|
// 6.99.14: the transcript scrolls inside its own box, as Control's does, so a new line never moves the page: the
|
|
2002
2016
|
// title, the recording bar, the files and the Ask panel stay where the reader left them. Everything that changes
|
|
2003
2017
|
// height while it records (the stop line, the files, the confirm card) sits under the box, never above it.
|
|
2004
|
-
|
|
2018
|
+
var divider = SPLIT_KIT.handle({ id: 'mt-lhandle', controls: 'mt-ask-' + safeId(sid), label: SPLIT.label });
|
|
2019
|
+
pane.innerHTML = '<div class="cx mt mt-livev" data-tint="meetings"><div id="mt-lhead"></div><div class="mt-lgrid' + (divider ? ' split-side' : '') + '" id="mt-lgrid"><div class="mt-lmain" id="mt-lmain">' +
|
|
2005
2020
|
'<div class="mt-tbox"><div id="mt-turns" class="mt-turns" tabindex="0" role="log" aria-live="polite" aria-label="Live transcript"></div>' +
|
|
2006
2021
|
'<button type="button" class="cx-btn quiet mt-jump" id="mt-jump" data-mt="jump" hidden>Jump to live</button></div>' +
|
|
2007
2022
|
'<div id="mt-lbar" class="mt-lbarslot"></div><div id="mt-lfiles"></div><div id="mt-lprep"></div><div id="mt-lfoot"></div>' +
|
|
2008
|
-
'<input type="file" id="mt-lpick" class="mt-pick" multiple accept="' + esc(LIVE_ACCEPT) + '" tabindex="-1" aria-hidden="true"></div>' +
|
|
2023
|
+
'<input type="file" id="mt-lpick" class="mt-pick" multiple accept="' + esc(LIVE_ACCEPT) + '" tabindex="-1" aria-hidden="true"></div>' + divider +
|
|
2009
2024
|
'<aside class="mt-side-panel" id="mt-ask-' + safeId(sid) + '" data-sid="' + esc(sid) + '">' + sidePanelHtml(sid) + '</aside></div></div>';
|
|
2025
|
+
SPLIT_KIT.mount($('mt-lgrid'), $('mt-lhandle'), SPLIT);
|
|
2010
2026
|
S.headHtml = S.footHtml = S.barHtml = S.filesHtml = S.prepHtml = null;
|
|
2011
2027
|
paintLiveHead(); paintTurns(true); paintLiveFoot();
|
|
2012
2028
|
clearInterval(S.liveTimer); clearInterval(S.clockTimer);
|
|
@@ -28,6 +28,18 @@
|
|
|
28
28
|
.mm-warn { color: var(--amber); font-size: 13px; }
|
|
29
29
|
.mm-split { margin-top: 2px; }
|
|
30
30
|
.mm-split, .mm-list, .mm-reader { min-width: 0; }
|
|
31
|
+
/* Duo tables: the divider between the list and the reader (app.js splitKit, styled in app.css), from 760 px only. Its
|
|
32
|
+
18 px column replaces the split's 18 px gap, so the default layout is unchanged; a dragged width is a share of the
|
|
33
|
+
split, clamped so the list keeps 240 px and the reader 380 px (memories.js SPLIT), like Sessions. */
|
|
34
|
+
@media (min-width: 760px) {
|
|
35
|
+
.split.mm-split { grid-template-columns: minmax(260px, 340px) 18px minmax(0, 1fr); column-gap: 0; }
|
|
36
|
+
.split.mm-split.split-sized { grid-template-columns: clamp(240px, var(--split-list-share, 30%), calc(100% - 398px)) 18px minmax(0, 1fr); }
|
|
37
|
+
.split.mm-split > .split-handle { display: flex; }
|
|
38
|
+
.mm-reader .split-full-btn { display: inline-flex; }
|
|
39
|
+
/* Full width: the reader takes the split; the list and the divider come back with Show list. */
|
|
40
|
+
.split.mm-split.split-full.mm-fullable:not(.solo) { grid-template-columns: minmax(0, 1fr); }
|
|
41
|
+
.split.mm-split.split-full.mm-fullable:not(.solo) > .split-list, .split.mm-split.split-full.mm-fullable:not(.solo) > .split-handle { display: none; }
|
|
42
|
+
}
|
|
31
43
|
|
|
32
44
|
/* A reader on a narrow screen is a pushed page: the header, views and counts step aside. */
|
|
33
45
|
@media (max-width: 759px) {
|
|
@@ -134,8 +146,10 @@
|
|
|
134
146
|
.mm-answer ul { margin: 0 0 8px; padding-left: 1.2em; }
|
|
135
147
|
.mm-answer code { font: 400 13px/1.4 var(--mono); background: var(--panel); padding: 0 3px; border-radius: 4px; }
|
|
136
148
|
.mm-table { overflow-x: auto; max-width: 100%; }
|
|
137
|
-
.
|
|
138
|
-
.mm-answer
|
|
149
|
+
/* Duo tables: grid lines and the held first column are control.css (table.tf-grid), in this pane's border colour. */
|
|
150
|
+
.mm-answer { --tf-bg: var(--qopen); --tf-line: var(--panel-border); }
|
|
151
|
+
.mm-answer table { font-size: 13px; }
|
|
152
|
+
.mm-answer th, .mm-answer td { padding: 4px 8px; text-align: left; vertical-align: top; }
|
|
139
153
|
.mm-refs ol { margin: 0; padding-left: 1.4em; font-size: 13px; line-height: 1.45; }
|
|
140
154
|
.mm-refs li { margin-bottom: 4px; overflow-wrap: anywhere; }
|
|
141
155
|
.mm-ref { color: var(--accent); font-variant-numeric: tabular-nums; }
|
|
@@ -259,6 +273,7 @@ body.mm-modal-open { overflow: hidden; }
|
|
|
259
273
|
.split.mm-split.solo { display: block; }
|
|
260
274
|
.split.mm-split.solo .split-list { position: static; max-height: none; overflow: visible; max-width: 820px; }
|
|
261
275
|
.split.mm-split.solo .reader { display: none; }
|
|
276
|
+
.split.mm-split.solo > .split-handle { display: none; }
|
|
262
277
|
.mm-cue-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; margin: 6px 2px 12px; }
|
|
263
278
|
.mm-cue-head p { margin: 3px 0 0; }
|
|
264
279
|
.mm-cue-title { margin: 0; font: 500 24px/1.15 var(--display); color: var(--ink); letter-spacing: -0.01em; }
|
|
@@ -19,9 +19,19 @@
|
|
|
19
19
|
(function () {
|
|
20
20
|
'use strict';
|
|
21
21
|
var W = window.COSWeb;
|
|
22
|
+
// What counts as Needs review: one predicate, shared with the server's count (memory-review.js, loaded by index.html).
|
|
23
|
+
var R = window.COSMemoryReview || null;
|
|
22
24
|
var esc = W.esc;
|
|
23
25
|
var pane = W.$('pane-memories');
|
|
24
26
|
if (!pane) return;
|
|
27
|
+
// 7.0.4 QA: a tab opened before the update has the old index.html, which never loaded memory-review.js, and fetches
|
|
28
|
+
// this pane fresh. Load the predicate here; until it arrives, review rows and counts read as not yet known.
|
|
29
|
+
if (!R) {
|
|
30
|
+
var reviewScript = document.createElement('script');
|
|
31
|
+
reviewScript.src = '/control/memory-review.js';
|
|
32
|
+
reviewScript.onload = function () { R = window.COSMemoryReview || null; if (R && !pane.classList.contains('hidden')) { try { paint(); } catch (_e) { /* the next paint draws it */ } } };
|
|
33
|
+
document.head.appendChild(reviewScript);
|
|
34
|
+
}
|
|
25
35
|
|
|
26
36
|
// ── vocabulary (plain words for the store and event names Control's page shows) ──
|
|
27
37
|
var KIND = { captured: 'Captured', proposed: 'Proposed', promotable: 'Suggested rule', saved: 'Saved', retrieved: 'Recalled',
|
|
@@ -31,7 +41,8 @@
|
|
|
31
41
|
review_ledger: 'Your review decisions', git_versions: 'Skill versions', eval_scores: 'Run checks', reflect_log: 'Daily reflection',
|
|
32
42
|
capture_ledger: 'Capture record', memory_trace: 'Recall record', retrieval_log: 'Recall record', used: 'Use record',
|
|
33
43
|
memory: 'Saved memory', occurrence: 'When it happened', assistant_turn: 'An answer' };
|
|
34
|
-
var GATE = { attribution: 'Who said it is unclear',
|
|
44
|
+
var GATE = { attribution: 'Who said it is unclear', rule_ready: 'Ready to become a rule', low_confidence: 'Ready to become a rule', prune_ready: 'Ready to quarantine',
|
|
45
|
+
contradiction: 'Contradicts another memory', scope: 'No area set', candidate: 'Waiting for your acceptance' };
|
|
35
46
|
var VIEWS = [['recent', 'Recent learning'], ['applied', 'Applied this week'], ['memories', 'All memories'], ['review', 'To review'], ['knowledge', 'Knowledge']];
|
|
36
47
|
// Control's cue review (MeetingAsk.swift CueReviewPane): shown beside the views once the server's cue history answers.
|
|
37
48
|
var CUE_VIEWS = [['cues', 'Cues to rate'], ['rated', 'Rated cues']];
|
|
@@ -132,7 +143,7 @@
|
|
|
132
143
|
graph: null, graphError: '', setup: null, setupError: '', setupLoading: false, setupBusy: '', progress: null,
|
|
133
144
|
ingest: null, ingestLimit: 10, building: '', folderDraft: '',
|
|
134
145
|
detail: {}, memDetail: {}, passages: {}, loading: {},
|
|
135
|
-
decisions: {}, decisionTs: {}, decided: {},
|
|
146
|
+
decisions: {}, decisionTs: {}, decided: {}, history: {},
|
|
136
147
|
armed: '', reviewBusy: '', reviewIntent: null, bulk: null,
|
|
137
148
|
ask: { q: '', busy: false, answer: null, error: '', cards: [], cardsBusy: false, startedAt: 0, message: '', pending: null },
|
|
138
149
|
focus: null, focusChosen: false, focusResolved: false, knowledgeFocus: null, neighbors: [], graphVisible: null,
|
|
@@ -142,7 +153,7 @@
|
|
|
142
153
|
owner: null, ownerError: '', ownerBusy: false,
|
|
143
154
|
policy: null, policyError: '', policyBusy: false, policyIntent: null, policyLoading: false,
|
|
144
155
|
guard: null, guardError: '', guardBusy: '', guardRun: null, guardLines: 0, guardLoading: false,
|
|
145
|
-
steward: { tab: 'rules', rows: [], cursor: null, busy: false, error: '', intent: null, outcomes: null, serial: 0 }
|
|
156
|
+
steward: { tab: 'rules', rows: [], cursor: null, busy: false, error: '', intent: null, outcomes: null, serial: 0, loaded: false }
|
|
146
157
|
};
|
|
147
158
|
function persist() { W.writeStore(STORE_KEY, { view: S.view, ktab: S.ktab }); }
|
|
148
159
|
var serials = {};
|
|
@@ -385,7 +396,16 @@
|
|
|
385
396
|
var o = outcomeText(e);
|
|
386
397
|
if (o) { var r = typeof e.outcome === 'object' ? e.outcome.result : e.outcome; return { text: 'Checked: ' + o, tone: /^(pass|passed|success)$/.test(r) ? 'ok' : /^(fail|failed)$/.test(r) ? 'bad' : '' }; }
|
|
387
398
|
if (e.needs) return { text: (GATE[e.needs.gate] || 'Needs you') + (e.needs.reason ? '. ' + e.needs.reason : ''), tone: 'warn' };
|
|
388
|
-
if (
|
|
399
|
+
if (e.learning) return { text: 'Learning. ' + (e.learning.reason || 'Not a rule yet'), tone: '' };
|
|
400
|
+
if (isProposal(e) || isPattern(e)) {
|
|
401
|
+
// Needs review only when the predicate says so (a suggested rule at its bar, in the needs list); a suggestion
|
|
402
|
+
// with no next step but Dismiss is listed calmly.
|
|
403
|
+
if (closedHere(e)) return { text: 'Dismissed', tone: '' };
|
|
404
|
+
if (needsLesson(e.lesson_id)) return { text: 'Needs your review', tone: 'warn' };
|
|
405
|
+
var lr = learningFor(e.lesson_id);
|
|
406
|
+
if (lr) return { text: 'Learning. ' + lr.learning.reason, tone: '' };
|
|
407
|
+
return { text: isProposal(e) ? 'Suggested by a scheduled task' : 'Watching. Not a rule yet', tone: '' };
|
|
408
|
+
}
|
|
389
409
|
if (e.event_type === 'dismissed') return { text: 'Dismissed', tone: '' };
|
|
390
410
|
if (e.event_type === 'retrieved') return { text: 'Recalled. Not yet shown to be used.', tone: '' };
|
|
391
411
|
if (e.event_type === 'included') return { text: 'Included in context. Not yet shown to be used.', tone: '' };
|
|
@@ -410,38 +430,65 @@
|
|
|
410
430
|
function plainReason(r) {
|
|
411
431
|
var t = String(r || ''), m;
|
|
412
432
|
if ((m = /^promotable at confidence ([\d.]+) \(< ([\d.]+)\)$/.exec(t))) return 'Confidence ' + m[1] + ', under the ' + m[2] + ' bar for a rule';
|
|
433
|
+
if ((m = /^promotable at confidence ([\d.]+) \(>= ([\d.]+)\)$/.exec(t))) return 'Confidence ' + m[1] + ', at or over the ' + m[2] + ' bar for a rule';
|
|
434
|
+
if (/^promotable at confidence unknown/.test(t)) return 'Confidence not known yet, so it cannot become a rule';
|
|
413
435
|
if ((m = /^names (.+?) near '(.+?)' with no attribution field$/.exec(t))) return 'Mentions ' + m[1].replace(/\b\w/g, function (c) { return c.toUpperCase(); }) + ' near "' + m[2] + '" without saying who said it';
|
|
414
436
|
return t;
|
|
415
437
|
}
|
|
416
|
-
function
|
|
438
|
+
function needsBlock() {
|
|
417
439
|
var ny = (S.recent.info && S.recent.info.needs_you) || (S.status && S.status.needs_you) || null;
|
|
418
|
-
return ny && Array.isArray(ny.items) ? ny
|
|
440
|
+
return R && ny && Array.isArray(ny.items) ? R.normalizeBlock(ny) : null;
|
|
441
|
+
}
|
|
442
|
+
function needsItems() { return R ? R.openItems(needsBlock(), S.decisions) : []; }
|
|
443
|
+
function needsLesson(id) { return !!id && needsItems().some(function (it) { return it.lesson_id === id; }); }
|
|
444
|
+
// An item's latest decision closes it (accepted, dismissed, pruned): it leaves To review for history.
|
|
445
|
+
function closedHere(e) { return !!R && R.reviewState(null, null, decisionFor(e)) === R.HISTORY; }
|
|
446
|
+
function itemRow(it, extra) {
|
|
447
|
+
var fromList = S.review.rows.filter(function (r) { return r.event_id === it.event_id; })[0] ||
|
|
448
|
+
S.recent.rows.filter(function (r) { return r.event_id === it.event_id; })[0];
|
|
449
|
+
var mem = /^mem_/.test(str(it.lesson_id));
|
|
450
|
+
var base = fromList || { event_id: it.event_id, lesson_id: it.lesson_id, ts: it.ts, title: it.title, category: it.category,
|
|
451
|
+
store: it.store || (mem ? 'bot_memory' : null), event_type: mem ? 'captured' : 'promotable', scope: 'unknown', engine: 'unknown',
|
|
452
|
+
target: { kind: mem ? 'memory' : 'pattern', id: it.lesson_id }, source_refs: [], applies_to: [] };
|
|
453
|
+
return Object.assign({}, base, extra);
|
|
454
|
+
}
|
|
455
|
+
// Below-bar suggestions: listed under Learning with their confidence, never counted, no warning tone.
|
|
456
|
+
function learningRows() {
|
|
457
|
+
if (!R) return [];
|
|
458
|
+
return R.learningItems(needsBlock(), S.decisions).filter(function (it) { return it && it.event_id; }).map(function (it) {
|
|
459
|
+
return itemRow(it, { learning: { reason: plainReason(it.reason), confidence: it.confidence, bar: it.bar } });
|
|
460
|
+
});
|
|
419
461
|
}
|
|
462
|
+
function learningFor(id) { return id ? learningRows().filter(function (r) { return r.lesson_id === id; })[0] || null : null; }
|
|
420
463
|
function needsRows() {
|
|
421
|
-
//
|
|
464
|
+
// Only what still needs review: a row decided here leaves the list for history (its reader stays open on it).
|
|
422
465
|
return needsItems().map(function (it) {
|
|
423
|
-
|
|
424
|
-
S.recent.rows.filter(function (r) { return r.event_id === it.event_id; })[0];
|
|
425
|
-
var mem = /^mem_/.test(str(it.lesson_id));
|
|
426
|
-
var base = fromList || { event_id: it.event_id, lesson_id: it.lesson_id, ts: it.ts, title: it.title, category: it.category,
|
|
427
|
-
store: it.store || (mem ? 'bot_memory' : null), event_type: mem ? 'captured' : 'promotable', scope: 'unknown', engine: 'unknown',
|
|
428
|
-
target: { kind: mem ? 'memory' : 'pattern', id: it.lesson_id }, source_refs: [], applies_to: [] };
|
|
429
|
-
return Object.assign({}, base, { needs: { gate: it.gate, reason: plainReason(it.reason) } });
|
|
466
|
+
return itemRow(it, { needs: { gate: it.gate, reason: plainReason(it.reason) } });
|
|
430
467
|
});
|
|
431
468
|
}
|
|
469
|
+
// The same count the server hands Today and the rail (memory-review.js count), less what was decided on this page.
|
|
432
470
|
function needsCount() {
|
|
433
471
|
var s = S.status || {}, ny = (S.recent.info && S.recent.info.needs_you) || s.needs_you;
|
|
434
|
-
|
|
435
|
-
if (
|
|
472
|
+
var n = ny && R ? R.count(R.normalizeBlock(ny), S.decisions) : null;
|
|
473
|
+
if (n != null) return n;
|
|
436
474
|
if (typeof s.learningToReview === 'number') return s.learningToReview;
|
|
437
475
|
return null;
|
|
438
476
|
}
|
|
477
|
+
// To review's other rows: proposed changes still open, and suggested rules only once the predicate counts them.
|
|
478
|
+
function reviewRestRows(seen) {
|
|
479
|
+
var learn = {};
|
|
480
|
+
learningRows().forEach(function (r) { learn[r.lesson_id] = 1; });
|
|
481
|
+
return S.review.rows.filter(function (e) { return !seen[e.event_id] && !closedHere(e) && !(isPattern(e) && learn[e.lesson_id]); });
|
|
482
|
+
}
|
|
439
483
|
function findRow(id) {
|
|
440
|
-
var all = needsRows().concat(S.review.rows, S.recent.rows, S.applied.rows);
|
|
484
|
+
var all = needsRows().concat(learningRows(), S.review.rows, S.recent.rows, S.applied.rows);
|
|
441
485
|
for (var i = 0; i < all.length; i++) if (all[i].event_id === id) return all[i];
|
|
486
|
+
if (S.history[id]) return S.history[id];
|
|
442
487
|
var d = S.detail[id];
|
|
443
488
|
return d && !d._error && d.event_id ? d : null;
|
|
444
489
|
}
|
|
490
|
+
// A row decided here leaves the lists; its reader keeps it so Restore stays one tap away.
|
|
491
|
+
function keepForReader(e) { if (e && e.event_id) S.history[e.event_id] = e; }
|
|
445
492
|
function fullRow(e) { var d = S.detail[e.event_id]; return Object.assign({}, e, d && !d._error ? d : {}); }
|
|
446
493
|
function rewardOn() { return !!(S.status && S.status.learningRewardEnabled === true); }
|
|
447
494
|
function rankingSentence() { return rewardOn() ? '<p>This lesson now ranks ahead of unused memories like it in later recall.</p>' : ''; }
|
|
@@ -801,17 +848,20 @@
|
|
|
801
848
|
(L.cursor ? moreBtn('more-recent', 'Load more', L.loading) : '') + coverageNote(L.coverage);
|
|
802
849
|
}
|
|
803
850
|
function reviewListHtml() {
|
|
804
|
-
var L = S.review, sel = selectedId(), needs = needsRows(), seen = {};
|
|
851
|
+
var L = S.review, sel = selectedId(), needs = needsRows(), learning = learningRows(), seen = {};
|
|
805
852
|
needs.forEach(function (e) { seen[e.event_id] = 1; });
|
|
806
|
-
|
|
853
|
+
learning.forEach(function (e) { seen[e.event_id] = 1; });
|
|
854
|
+
var rest = reviewRestRows(seen);
|
|
807
855
|
var other = rest.filter(function (e) { return !isPruneReady(e); }), ready = rest.filter(isPruneReady);
|
|
808
856
|
var out = '';
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
if (!
|
|
814
|
-
if (
|
|
857
|
+
if (needs.length) out += groupHead('Needs you now', fmt(needsCount() || needs.length) + ' open') + '<div class="cx-list">' + needs.map(function (e) { return rowHtml(e, e.event_id === sel); }).join('') + '</div>';
|
|
858
|
+
var learnHtml = learning.length ? groupHead('Learning', fmt(learning.length) + ' ' + plural(learning.length, 'suggestion', 'suggestions')) +
|
|
859
|
+
'<p class="mm-group-note">Suggested rules still gathering evidence. They cannot become a rule until their confidence reaches the bar, and move to Needs you now on their own when it does.</p>' +
|
|
860
|
+
'<div class="cx-list">' + learning.map(function (e) { return rowHtml(e, e.event_id === sel); }).join('') + '</div>' : '';
|
|
861
|
+
if (!L.loaded && L.loading) return out + groupHead('Proposed changes') + loadingBox('Reading the changes waiting for you') + learnHtml;
|
|
862
|
+
if (L.error && !L.rows.length) return out + groupHead('Proposed changes') + stateBox('The review list did not load.', L.error, 'refresh') + learnHtml;
|
|
863
|
+
if (!needs.length && !rest.length) return groupHead('To review') + stateBox('You are up to date.', 'Nothing is waiting for your decision.') + learnHtml;
|
|
864
|
+
if (other.length) out += groupHead('Proposed changes', fmt(other.length) + ' open') + '<div class="cx-list">' + other.map(function (e) { return rowHtml(e, e.event_id === sel); }).join('') + '</div>';
|
|
815
865
|
if (ready.length) {
|
|
816
866
|
var open = ready.filter(function (e) { return !decisionFor(e); }).length, b = S.bulk;
|
|
817
867
|
out += groupHead('Ready to quarantine', open ? fmt(open) + ' open' : 'All decided') +
|
|
@@ -821,7 +871,7 @@
|
|
|
821
871
|
'<div class="cx-list">' + ready.map(function (e) { return rowHtml(e, e.event_id === sel); }).join('') + '</div>';
|
|
822
872
|
}
|
|
823
873
|
if (L.cursor) out += moreBtn('more-review', 'Load more to review', L.loading);
|
|
824
|
-
return out;
|
|
874
|
+
return out + learnHtml;
|
|
825
875
|
}
|
|
826
876
|
function memSearchHtml() {
|
|
827
877
|
return '<form class="cx-search mm-search" data-mm-form="mem-search" role="search"><input class="field" type="search" id="mm-mem-q" placeholder="Search topics, ideas…" value="' + esc(S.mem.q) + '" autocomplete="off" enterkeyhint="search" aria-label="Search memories">' +
|
|
@@ -864,7 +914,7 @@
|
|
|
864
914
|
if (!isWide() || S.view === 'cues' || S.view === 'rated') return '';
|
|
865
915
|
if (S.view === 'knowledge') return S.ktab;
|
|
866
916
|
if (S.view === 'memories') { var m = S.mem.hits != null ? S.mem.hits : S.mem.rows; return m[0] ? m[0].id : ''; }
|
|
867
|
-
var rows = S.view === 'applied' ? S.applied.rows : S.view === 'review' ? needsRows().concat(
|
|
917
|
+
var rows = S.view === 'applied' ? S.applied.rows : S.view === 'review' ? needsRows().concat(reviewRestRows({}), learningRows()) : S.recent.rows;
|
|
868
918
|
return rows[0] ? rows[0].event_id : '';
|
|
869
919
|
}
|
|
870
920
|
|
|
@@ -874,8 +924,9 @@
|
|
|
874
924
|
}
|
|
875
925
|
function quote(text) { return '<blockquote class="mm-quote">' + esc(text) + '</blockquote>'; }
|
|
876
926
|
function readerHead(title, facts, actions) {
|
|
927
|
+
var full = SPLIT_KIT.fullButton(SPLIT.key, SPLIT.controls);
|
|
877
928
|
return '<div class="mm-reader-head"><h2 class="mm-title">' + esc(title) + '</h2>' + (facts ? '<p class="mm-sub">' + facts + '</p>' : '') +
|
|
878
|
-
(actions ? '<div class="mm-actions">' + actions + '</div>' : '') + '</div>';
|
|
929
|
+
(actions || full ? '<div class="mm-actions">' + (actions || '') + full + '</div>' : '') + '</div>';
|
|
879
930
|
}
|
|
880
931
|
function eventReader(id) {
|
|
881
932
|
var base = findRow(id);
|
|
@@ -894,7 +945,8 @@
|
|
|
894
945
|
var facts = ['<span class="mm-kind">' + esc(o ? 'Checked: ' + o : kindLabel(e)) + '</span>', esc(storeName(e.store)), esc(known(e.scope)), esc(known(e.engine) !== known(e.scope) ? known(e.engine) : ''), esc(when(e.ts))].filter(function (x) { return x && x !== '<span class="mm-kind"></span>'; }).join(' · ');
|
|
895
946
|
var actions = btn('open-sources', 'Open sources', { id: e.event_id }) + btn('explore', 'Explore in graph', { id: e.event_id }) + btn('copy-open', 'Copy context', { id: e.event_id, cls: 'primary' });
|
|
896
947
|
var note = '';
|
|
897
|
-
if (e.needs) note += '<div class="cx-notice warn">' + esc((GATE[e.needs.gate] || 'Needs you') + (e.needs.reason ? ': ' + e.needs.reason : '') + '.') + '</div>';
|
|
948
|
+
if (e.needs && !closedHere(e)) note += '<div class="cx-notice warn">' + esc((GATE[e.needs.gate] || 'Needs you') + (e.needs.reason ? ': ' + e.needs.reason : '') + '.') + '</div>';
|
|
949
|
+
else if (e.learning || learningFor(e.lesson_id)) note += '<div class="cx-notice">' + esc('Learning: ' + ((e.learning || learningFor(e.lesson_id).learning).reason || 'not a rule yet') + '. It moves to Needs you now on its own once its confidence reaches the bar.') + '</div>';
|
|
898
950
|
if (full && full._error && !(e.store === 'bot_memory')) note += '<div class="cx-notice">' + esc(full._error) + '</div>';
|
|
899
951
|
else if (!full && !(e.store === 'bot_memory' && e.lesson_id) && S.loading[e.event_id]) note += '<p class="cx-note">Reading the full record…</p>';
|
|
900
952
|
if (e.detail && e.detail.truncated) note += '<div class="cx-notice">The record was shortened to fit. Your Mac holds more than shown here.</div>';
|
|
@@ -1069,7 +1121,15 @@
|
|
|
1069
1121
|
function renderMarkdown(md) {
|
|
1070
1122
|
var lines = esc(md || '').split('\n'), out = [], ul = null, table = null;
|
|
1071
1123
|
function inline(s) { return s.replace(/`([^`]+)`/g, '<code>$1</code>').replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>'); }
|
|
1072
|
-
function flush() { if (ul) { out.push('<ul>' + ul.join('') + '</ul>'); ul = null; } if (table) { out.push(
|
|
1124
|
+
function flush() { if (ul) { out.push('<ul>' + ul.join('') + '</ul>'); ul = null; } if (table) { out.push(tableHtml(table)); table = null; } }
|
|
1125
|
+
// Duo tables: the first row is the header; the shared column floors (app.js tableFit) set how far each column shrinks.
|
|
1126
|
+
function tableHtml(rows) {
|
|
1127
|
+
var fit = W.tableFit(rows);
|
|
1128
|
+
return '<div class="mm-table tf-wrap"><table class="' + fit.tableClass + ' tf-grid">' + rows.map(function (cells, n) {
|
|
1129
|
+
var tag = n ? 'td' : 'th';
|
|
1130
|
+
return '<tr>' + cells.map(function (c, k) { var w = fit.width(k, !n); return '<' + tag + (w ? ' style="' + w + '"' : '') + '>' + fit.cell(k, inline(c), !n) + '</' + tag + '>'; }).join('') + '</tr>';
|
|
1131
|
+
}).join('') + '</table></div>';
|
|
1132
|
+
}
|
|
1073
1133
|
lines.forEach(function (raw) {
|
|
1074
1134
|
var line = raw.replace(/\s+$/, '');
|
|
1075
1135
|
if (!line.trim()) { flush(); return; }
|
|
@@ -1080,9 +1140,8 @@
|
|
|
1080
1140
|
if (/^\s*\|.*\|\s*$/.test(line)) {
|
|
1081
1141
|
if (/^\s*\|[\s:|-]+\|\s*$/.test(line)) return;
|
|
1082
1142
|
if (ul) flush();
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
table.push('<tr>' + cells.map(function (c) { return '<' + tag + '>' + c + '</' + tag + '>'; }).join('') + '</tr>'); return;
|
|
1143
|
+
table = table || [];
|
|
1144
|
+
table.push(line.trim().replace(/^\||\|$/g, '').split('|').map(function (c) { return c.trim(); })); return;
|
|
1086
1145
|
}
|
|
1087
1146
|
flush(); out.push('<p>' + inline(line) + '</p>');
|
|
1088
1147
|
});
|
|
@@ -1324,7 +1383,7 @@
|
|
|
1324
1383
|
function closeModal() {
|
|
1325
1384
|
var m = S.modal; if (!m) return;
|
|
1326
1385
|
if (m.kind === 'merge' && S.merge && (S.merge.phase === 'preview' || S.merge.phase === 'loading')) { var pend = S.merge; S.merge = null; if (pend.resolve) pend.resolve(false); W.toast('Nothing was changed.'); }
|
|
1327
|
-
if (m.kind === 'steward') S.steward.serial++;
|
|
1386
|
+
if (m.kind === 'steward') S.steward.serial++; // supersedes the read in flight; the next open starts its own (stewardLoad)
|
|
1328
1387
|
S.modal = null; paintModal(); if (m.opener && m.opener.focus) { try { m.opener.focus({ preventScroll: true }); } catch (_e) {} }
|
|
1329
1388
|
}
|
|
1330
1389
|
function modalHtml() {
|
|
@@ -1430,16 +1489,28 @@
|
|
|
1430
1489
|
'<ul class="mm-log">' + flagged.concat(review).slice(0, 60).map(function (x) { return '<li><b class="' + (x.verdict === 'prune' ? 'mm-bad' : '') + '">' + (x.verdict === 'prune' ? (x.applied ? 'Quarantined' : 'Would quarantine') : 'For you') + '</b> <span>' + esc(clip(x.excerpt || x.id, 160)) + '</span><span class="mm-muted">' + esc((x.reasons || []).join('; ')) + (x.by === 'model' ? ' (model)' : '') + '</span></li>'; }).join('') + '</ul>' : '') + '</section>';
|
|
1431
1490
|
}
|
|
1432
1491
|
// Stewardship (memories/memory-stewardship.js, ported): standing rules, current sources and outcome evidence.
|
|
1492
|
+
// A read never waits on an older one: each takes a new serial and only the newest may answer. Closing the sheet or
|
|
1493
|
+
// switching tabs supersedes the read in flight, and every read ends (an answer, an error, or STEWARD_READ_MS) so
|
|
1494
|
+
// "Working…" can never outlive it. 7.0.2 kept `busy` set when a read was superseded, and the next open or tab
|
|
1495
|
+
// switch returned early on it: Outcome evidence read "Working…" for good.
|
|
1496
|
+
var STEWARD_READ_MS = 30000;
|
|
1433
1497
|
function stewardLoad(more) {
|
|
1434
|
-
var T = S.steward; if (T.
|
|
1435
|
-
T.busy = true; T.error = ''; var serial = ++T.serial;
|
|
1498
|
+
var T = S.steward; if (T.intent) return;
|
|
1499
|
+
T.busy = true; T.error = ''; T.loaded = false; var serial = ++T.serial;
|
|
1436
1500
|
var action = T.tab === 'rules' ? 'rule_page' : T.tab === 'sources' ? 'source_status' : 'trace_summary', body = { action: action };
|
|
1437
1501
|
if (action === 'rule_page') { body.limit = 25; body.cursor = more ? T.cursor : null; }
|
|
1438
1502
|
paintModal();
|
|
1439
|
-
|
|
1503
|
+
var timer = null;
|
|
1504
|
+
var late = new Promise(function (_ok, fail) { timer = setTimeout(function () { fail(new Error('Your Mac did not answer in ' + (STEWARD_READ_MS / 1000) + ' seconds. Reload to try again.')); }, STEWARD_READ_MS); });
|
|
1505
|
+
Promise.race([call('workspace.request', body), late]).then(function (d) {
|
|
1440
1506
|
if (serial !== T.serial) return;
|
|
1507
|
+
d = d || {};
|
|
1441
1508
|
if (action === 'trace_summary') T.outcomes = d; else { T.rows = more ? T.rows.concat(d.items || []) : d.items || []; T.cursor = d.next_cursor || null; }
|
|
1442
|
-
}, function (e) { if (serial !== T.serial) return; T.error = e.message; }).then(function () {
|
|
1509
|
+
}, function (e) { if (serial !== T.serial) return; T.error = e.message; }).then(function () {
|
|
1510
|
+
clearTimeout(timer);
|
|
1511
|
+
if (serial !== T.serial) return;
|
|
1512
|
+
T.busy = false; T.loaded = true; paintModal();
|
|
1513
|
+
});
|
|
1443
1514
|
}
|
|
1444
1515
|
function stewardMutate(body) {
|
|
1445
1516
|
var T = S.steward; if (T.busy) return;
|
|
@@ -1479,8 +1550,9 @@
|
|
|
1479
1550
|
body += T.rows.length ? T.rows.map(function (r) { return '<section class="cx-card mm-card"><h4>' + esc(r.title || r.id) + '</h4><p class="mm-muted">Revision ' + esc(r.revision) + ' · ' + esc(when(r.captured_at)) + ' · ' + esc(r.projection) + '</p>' + sbtn('steward-refresh', 'Refresh this document', r.id) + '</section>'; }).join('') :
|
|
1480
1551
|
(!T.busy ? '<p>No granted documents right now. The owner grants documents on the Mac.</p>' : '');
|
|
1481
1552
|
} else {
|
|
1482
|
-
var d = T.outcomes;
|
|
1483
|
-
|
|
1553
|
+
var d = T.outcomes, none = d && !(Number(d.unique_runs) > 0) && !(Number(d.checked_eligible_runs) > 0) && !(Number(d.inconclusive_runs) > 0);
|
|
1554
|
+
if (!T.busy && !T.error && T.loaded && (!d || none)) body += '<p>No outcome evidence yet.</p>';
|
|
1555
|
+
body += d && !none ? '<dl class="mm-grid"><dt>Observed runs</dt><dd>' + fmt(d.unique_runs || 0) + '</dd><dt>Checked runs that count</dt><dd>' + fmt(d.checked_eligible_runs || 0) + '</dd><dt>Not conclusive</dt><dd>' + fmt(d.inconclusive_runs || 0) + '</dd><dt>Days observed</dt><dd>' + fmt(d.observation_days || 0) + '</dd><dt>Evidence bar</dt><dd>' + (d.evidence_gate_met ? 'Met' : 'Not met yet') + (d.rate_limit_reason ? '. ' + esc(d.rate_limit_reason) : '') + '</dd></dl>' +
|
|
1484
1556
|
'<details><summary>Show the raw numbers</summary><pre class="mm-pre">' + esc(JSON.stringify(d, null, 2)) + '</pre></details>' : '';
|
|
1485
1557
|
body += '<p class="mm-muted">Being recalled, being included in context, a model saying it applied something and an independently checked result are four different things. Too little evidence does not mean things improved.</p>';
|
|
1486
1558
|
}
|
|
@@ -1489,7 +1561,7 @@
|
|
|
1489
1561
|
function stewardAct(act, id) {
|
|
1490
1562
|
var T = S.steward, dr = S.drafts;
|
|
1491
1563
|
if (act === 'steward-retry') return stewardMutate(T.intent);
|
|
1492
|
-
if (act === 'steward-tab') { if (T.intent) return; T.tab = id; T.rows = []; T.cursor = null; T.outcomes = null; return stewardLoad(false); }
|
|
1564
|
+
if (act === 'steward-tab') { if (T.intent) return; T.tab = id; T.rows = []; T.cursor = null; T.outcomes = null; T.loaded = false; return stewardLoad(false); }
|
|
1493
1565
|
if (act === 'steward-reload') return stewardLoad(false);
|
|
1494
1566
|
if (act === 'steward-more') return stewardLoad(true);
|
|
1495
1567
|
if (act === 'steward-propose') {
|
|
@@ -1801,6 +1873,7 @@
|
|
|
1801
1873
|
// Restore is Accept on the wire: the bridge returns a quarantined memory to recall before marking it accepted.
|
|
1802
1874
|
call('memory.review', { id: id, decision: decision === 'restore' ? 'accept' : decision }).then(function (d) {
|
|
1803
1875
|
var row = d.decision || {};
|
|
1876
|
+
S.review.rows.concat(needsRows(), S.recent.rows).forEach(function (r) { if (r.lesson_id === id) keepForReader(r); });
|
|
1804
1877
|
S.decisions[id] = row.decision || (decision === 'prune' ? 'pruned' : 'accepted'); S.decisionTs[id] = new Date().toISOString(); S.decided[id] = 1;
|
|
1805
1878
|
if (decision === 'prune') { S.mem.rows = S.mem.rows.filter(function (m) { return m.id !== id; }); }
|
|
1806
1879
|
W.toast(decision === 'prune' ? 'Memory quarantined.' : decision === 'restore' ? 'Memory restored to recall.' : 'Memory accepted.');
|
|
@@ -1829,6 +1902,7 @@
|
|
|
1829
1902
|
function decide(eventId, decision) {
|
|
1830
1903
|
var e = findRow(eventId); if (!e) return;
|
|
1831
1904
|
call('learning.decide', { id: e.lesson_id, decision: decision }).then(function () {
|
|
1905
|
+
keepForReader(e);
|
|
1832
1906
|
S.decisions[e.lesson_id] = decision; if (decision === 'dismissed') S.decided[e.lesson_id] = 1; else delete S.decided[e.lesson_id];
|
|
1833
1907
|
W.toast(decision === 'dismissed' ? 'Suggestion dismissed.' : 'Suggestion restored.');
|
|
1834
1908
|
loadReview(false); afterWrite();
|
|
@@ -1972,8 +2046,15 @@
|
|
|
1972
2046
|
}
|
|
1973
2047
|
|
|
1974
2048
|
// ── painting ─────────────────────────────────────────────
|
|
2049
|
+
// Duo tables: the divider between the list and the reader from 760 px (app.js splitKit; the same limits as Sessions),
|
|
2050
|
+
// with its own remembered width, and Full width in a reader's header.
|
|
2051
|
+
var SPLIT = { key: 'cos_web_memories_split', label: 'Memories list', controls: 'mm-list-col', media: '(min-width: 760px)', listMin: 240, readerMin: 380, handleW: 18 };
|
|
2052
|
+
// A page without the kit (a test's stand-in W) draws no divider and keeps working.
|
|
2053
|
+
var SPLIT_KIT = W.splitKit || { handle: function () { return ''; }, mount: function () { return null; }, fullButton: function () { return ''; } };
|
|
2054
|
+
var SPLIT_HANDLE = SPLIT_KIT.handle({ id: 'mm-handle', controls: SPLIT.controls, label: SPLIT.label });
|
|
1975
2055
|
var SKELETON = '<div class="cx mm" data-tint="memories" id="mm-root"><div id="mm-hero"></div><div class="mm-controls" id="mm-views"></div><p class="mm-counts" id="mm-counts"></p>' +
|
|
1976
|
-
'<div class="split mm-split" id="mm-split"><div class="split-list mm-list" id="mm-list-col"><div id="mm-tools"></div><div id="mm-list"></div></div
|
|
2056
|
+
'<div class="split mm-split" id="mm-split"><div class="split-list mm-list" id="mm-list-col"><div id="mm-tools"></div><div id="mm-list"></div></div>' +
|
|
2057
|
+
SPLIT_HANDLE + '<div class="reader mm-reader" id="mm-reader"></div></div>' +
|
|
1977
2058
|
'<p class="mm-foot" id="mm-foot"></p></div><div id="mm-modal-host"></div>';
|
|
1978
2059
|
function write(node, html) { if (!node || node.__mm === html) return false; node.innerHTML = html; node.__mm = html; return true; }
|
|
1979
2060
|
var paintTimer = null;
|
|
@@ -1982,6 +2063,7 @@
|
|
|
1982
2063
|
var r = W.parseRoute(); if (r.pane !== 'memories') return;
|
|
1983
2064
|
if (!el('mm-root')) pane.innerHTML = SKELETON;
|
|
1984
2065
|
var reading = readerArgs(r).length > 0, split = el('mm-split');
|
|
2066
|
+
SPLIT_KIT.mount(split, el('mm-handle'), SPLIT);
|
|
1985
2067
|
pane.classList.toggle('reading', reading); if (split) split.classList.toggle('reading', reading);
|
|
1986
2068
|
// The cue views are one column at every width, like Control's cue review (a card, not a list beside a reader).
|
|
1987
2069
|
if (split) split.classList.toggle('solo', !reading && (S.view === 'cues' || S.view === 'rated'));
|
|
@@ -2007,6 +2089,9 @@
|
|
|
2007
2089
|
var html = readerHtml(r);
|
|
2008
2090
|
if (readerArgs(r).length && W.crumb && W.crumb() !== S.crumbText) W.setCrumb(S.crumbText);
|
|
2009
2091
|
if (write(node, html) && el('mm-graph-host')) mountGraph();
|
|
2092
|
+
// Full width hides the list only while a reader with the button is open, so the list is never hidden behind a
|
|
2093
|
+
// placeholder with no way back.
|
|
2094
|
+
var split = el('mm-split'); if (split) split.classList.toggle('mm-fullable', !!(node.querySelector && node.querySelector('[data-split-full]')));
|
|
2010
2095
|
document.body.classList.toggle('mm-graph-open', S.graphFull && !!el('mm-graph-host'));
|
|
2011
2096
|
}
|
|
2012
2097
|
function crumb(t) { S.crumbText = String(t || ''); }
|
|
@@ -401,15 +401,34 @@
|
|
|
401
401
|
}).join('') + '</div>';
|
|
402
402
|
}
|
|
403
403
|
// Drop it on: the list column and the chat column. Built once; each column then paints only itself.
|
|
404
|
+
// Duo tables: the divider between the two columns from 860 px (app.js splitKit), with its own remembered width. Its
|
|
405
|
+
// 24 px column replaces the split's 24 px gap, so the default layout is unchanged. The list keeps 240 px like Sessions;
|
|
406
|
+
// the chat keeps 440, not 380, because its composer (text box, camera, file, Ask, Drop) squeezes the text box to about
|
|
407
|
+
// 90 px at 380 (headless render at 1000x703).
|
|
408
|
+
// A page without the kit (a test's stand-in W) draws no divider and keeps working.
|
|
409
|
+
var SPLIT_KIT = W.splitKit || { handle: function () { return ''; }, mount: function () { return null; } };
|
|
410
|
+
var SPLIT = { key: 'cos_web_messages_split', label: 'Message list', controls: 'msg-left', sized: '.msg-left', media: '(min-width: 860px)', listMin: 240, readerMin: 440, handleW: 24 };
|
|
404
411
|
function splitHost() {
|
|
405
412
|
var split = pane.querySelector('.msg-split');
|
|
406
413
|
if (!split) {
|
|
407
|
-
pane.innerHTML = '<div class="msg-split"><div class="msg-left" id="msg-left"></div
|
|
414
|
+
pane.innerHTML = '<div class="msg-split"><div class="msg-left" id="msg-left"></div>' +
|
|
415
|
+
SPLIT_KIT.handle({ id: 'msg-handle', controls: SPLIT.controls, label: SPLIT.label }) + '<div class="msg-right" id="msg-right"></div></div>';
|
|
408
416
|
split = pane.querySelector('.msg-split');
|
|
409
417
|
}
|
|
418
|
+
SPLIT_KIT.mount(split, $('msg-handle'), SPLIT);
|
|
410
419
|
split.setAttribute('data-show', current[0] === 'cos' ? 'chat' : 'list');
|
|
420
|
+
chatRoute(current[0] === 'cos');
|
|
411
421
|
return split;
|
|
412
422
|
}
|
|
423
|
+
// 7.0.2 (A2, the Duo audit): the workspace's bottom padding (app.css, +96 px folded and +40 px from 600 px) is room for
|
|
424
|
+
// a bottom bar that no longer exists. Under the chat's sticky composer it was blank page: focusing the box scrolled to
|
|
425
|
+
// it, and after the keyboard closed it stayed as a band under Model and Effort. On the COS chat route the workspace
|
|
426
|
+
// carries ws-chat, and app.css drops that padding. Any other route takes the class off.
|
|
427
|
+
function chatRoute(on) {
|
|
428
|
+
var ws = $('workspace');
|
|
429
|
+
if (ws && ws.classList) ws.classList.toggle('ws-chat', !!on);
|
|
430
|
+
}
|
|
431
|
+
if (W.onRoute) W.onRoute(function (r) { if (!r || r.pane !== 'messages' || r.args[0] !== 'cos') chatRoute(false); });
|
|
413
432
|
function renderList() {
|
|
414
433
|
var focus = document.activeElement && document.activeElement.id === 'msg-q';
|
|
415
434
|
var caret = focus ? document.activeElement.selectionStart : null;
|
|
@@ -452,7 +471,7 @@
|
|
|
452
471
|
var a = atts(t);
|
|
453
472
|
return '<div class="cx-card you"><div class="cx-card-label">YOU</div><div class="cx-card-body" data-hl>' + esc(String(t.query || '').trim() || '(empty query)').replace(/\n/g, '<br>') + '</div></div>' +
|
|
454
473
|
stripHtml(a.filter(isUserAtt), true, msgKey) +
|
|
455
|
-
'<div class="cx-card"><div class="cx-card-label">COS</div><div class="cx-card-body md" data-hl>' + W.md(String(t.text || '')) + '</div></div>' +
|
|
474
|
+
'<div class="cx-card"><div class="cx-card-label">COS</div><div class="cx-card-body md" data-hl>' + W.md(W.structureLensAnswer(String(t.text || ''))) + '</div></div>' +
|
|
456
475
|
stripHtml(a.filter(function (x) { return !isUserAtt(x); }), false, msgKey);
|
|
457
476
|
}
|
|
458
477
|
function copyTurnText(t) {
|