@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
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
/* The preview engine's web viewer (phase 1, PLAN_preview_engine_2026-11-08 4d). Link cards where a reply links a local
|
|
2
|
+
* file, and one viewer for what a card or a sent file opens: Markdown rendered with the reply renderer, text and code
|
|
3
|
+
* with line numbers and jump-to-line, CSV and TSV as a table, HTML with scripts off (a per-open "Run scripts" runs them
|
|
4
|
+
* in an opaque origin with no network), a PDF inline on a desktop and in a new tab on a phone, pictures through the
|
|
5
|
+
* normalizer. Only opaque ids reach the server (GET /api/preview/...); a Mac path never reaches this page.
|
|
6
|
+
* The viewer shell (history, Back, focus, scroll restore, the 2-request budget) is panes/reader-media.js. */
|
|
7
|
+
(function () {
|
|
8
|
+
'use strict';
|
|
9
|
+
var W = window.COSWeb;
|
|
10
|
+
if (!W) return;
|
|
11
|
+
var LINK_ID = /^al_[a-f0-9]{64}$/, FILE_ID = /^af_[a-f0-9]{64}$/;
|
|
12
|
+
var KINDS = ['image', 'pdf', 'html', 'markdown', 'text', 'code', 'table', 'document', 'slides', 'sheet', 'audio', 'video', 'archive', 'folder', 'file'];
|
|
13
|
+
var OFF = 'Not available on this Mac';
|
|
14
|
+
var REASONS = { outside_workspace: OFF, missing: OFF, secret_file: 'Not shown: this file may hold secrets.', unavailable: 'Not available right now. Reopen the session to check again.' };
|
|
15
|
+
var NORMALIZED = /\.(png|jpe?g|webp|hei[cf]|avif)$/i;
|
|
16
|
+
var THUMBS = { pdf: 1, document: 1, slides: 1, sheet: 1 };
|
|
17
|
+
var style = document.createElement('style');
|
|
18
|
+
style.textContent = '.pv-chip{display:inline-flex;align-items:center;gap:8px;vertical-align:middle;max-width:100%;min-height:44px;margin:2px 0;padding:4px 10px 4px 4px;border:1px solid var(--panel-border);border-radius:10px;background:var(--panel);color:var(--ink);font:inherit;text-align:left;cursor:pointer;box-sizing:border-box}' +
|
|
19
|
+
'button.pv-chip:hover{border-color:var(--sub)}button.pv-chip:focus-visible{outline:2px solid var(--accent,currentColor);outline-offset:2px}' +
|
|
20
|
+
'.pv-badge{flex:none;display:inline-flex;align-items:center;justify-content:center;min-width:34px;height:34px;padding:0 4px;border-radius:7px;background:var(--bg);border:1px solid var(--panel-border);font:600 10.5px/1 var(--body);letter-spacing:.04em;color:var(--sub);box-sizing:border-box}' +
|
|
21
|
+
'.pv-chip-text{display:inline-flex;flex-direction:column;min-width:0;line-height:1.25}.pv-chip-label{font:500 14px/1.3 var(--body);overflow-wrap:anywhere}.pv-chip-meta{font:12px/1.3 var(--body);color:var(--sub);overflow-wrap:anywhere}' +
|
|
22
|
+
'.pv-chip.off{cursor:default;opacity:.8;border-style:dashed}.pv-chip.off .pv-chip-meta{color:var(--sub)}.pv-chip.blocked .pv-badge{color:var(--amber,var(--sub))}' +
|
|
23
|
+
'.pv-chip.has-thumb{padding:4px 12px 4px 4px}.pv-thumb{flex:none;display:inline-flex;align-items:center;justify-content:center;width:48px;height:62px;overflow:hidden;border-radius:6px;background:#fff;border:1px solid var(--panel-border);font:600 10.5px/1 var(--body);color:#6e675d}.pv-thumb img{width:100%;height:100%;object-fit:contain;display:block}' +
|
|
24
|
+
// The viewer sits on a dark overlay (reader-media.js .rm-view), so everything drawn on a panel names its own ink:
|
|
25
|
+
// inherited, it was the overlay's white on a light panel (7.0.5 render: Markdown, code and the line field were blank).
|
|
26
|
+
'.pv-scroll{flex:1;min-height:0;width:100%;overflow:auto;border:1px solid var(--panel-border);border-radius:10px;background:var(--panel);color:var(--ink)}' +
|
|
27
|
+
'.pv-md{padding:12px 16px;max-width:none}.pv-code{font:12.5px/1.55 ui-monospace,SFMono-Regular,Menlo,monospace;padding:6px 0;min-width:max-content}.pv-line{display:flex;scroll-margin:40px 0}.pv-line.hit{background:var(--gold-bg,rgba(185,138,62,.14))}' +
|
|
28
|
+
'.pv-n{flex:none;width:4.2em;padding-right:12px;text-align:right;color:var(--sub);user-select:none;-webkit-user-select:none}.pv-t{white-space:pre;padding-right:16px}' +
|
|
29
|
+
'.pv-bar{display:flex;flex-wrap:wrap;gap:8px;align-items:center;width:100%}.pv-bar .rm-status{flex:1;min-width:12em}.pv-jump{display:flex;gap:6px;align-items:center}.pv-jump input{width:6.5em;min-height:44px;padding:0 10px;border:1px solid var(--panel-border);border-radius:10px;background:var(--panel);color:var(--ink);font:16px var(--body);box-sizing:border-box}' +
|
|
30
|
+
// A table is as wide as its columns need (a cell wraps by words past 22em, never mid-word), and scrolls sideways
|
|
31
|
+
// inside its own box on a phone; the page itself never does (7.0.5 render: 458 px crushed "industry" to "ind ustr y").
|
|
32
|
+
'.pv-table{border-collapse:collapse;font:13px/1.4 var(--body);width:max-content;min-width:100%}.pv-table th,.pv-table td{padding:6px 10px;border-bottom:1px solid var(--panel-border);text-align:left;vertical-align:top;max-width:22em;overflow-wrap:break-word}.pv-table thead th{position:sticky;top:0;background:var(--panel);z-index:1;font-weight:600}.pv-table td.pv-rn,.pv-table th.pv-rn{color:var(--sub);text-align:right;width:1%;white-space:nowrap}' +
|
|
33
|
+
'.pv-frame{flex:1;min-height:0;width:100%;border:1px solid var(--panel-border);border-radius:10px;background:#fff}.pv-other{display:flex;flex-direction:column;align-items:center;gap:12px;text-align:center}.pv-other .pv-thumb{width:160px;height:206px}' +
|
|
34
|
+
'.rm-view-body.pv-body{align-items:stretch;justify-content:flex-start}';
|
|
35
|
+
document.head.appendChild(style);
|
|
36
|
+
|
|
37
|
+
function element(tag, cls, text) { var el = document.createElement(tag); if (cls) el.className = cls; if (text != null) el.textContent = text; return el; }
|
|
38
|
+
function button(text, action, cls) { var b = element('button', cls || 'cx-btn quiet', text); b.type = 'button'; b.addEventListener('click', action); return b; }
|
|
39
|
+
function kindOf(name) {
|
|
40
|
+
var n = String(name || '');
|
|
41
|
+
var map = [[NORMALIZED, 'image'], [/\.gif$/i, 'image'], [/\.pdf$/i, 'pdf'], [/\.html?$/i, 'html'], [/\.(md|markdown|mdx)$/i, 'markdown'],
|
|
42
|
+
[/\.(csv|tsv)$/i, 'table'], [/\.(txt|log|text)$/i, 'text'],
|
|
43
|
+
[/\.(json|jsonl|ya?ml|toml|xml|svg|tsx?|mts|cts|jsx?|mjs|cjs|py|swift|sh|zsh|bash|s?css|rb|go|rs|java|kt|c|h|cc|cpp|hpp|m|mm|sql|lua|php|plist|ini|conf|cfg|properties)$/i, 'code'],
|
|
44
|
+
[/\.(docx?|pages|rtf)$/i, 'document'], [/\.(xlsx?|numbers)$/i, 'sheet'], [/\.(pptx?|key)$/i, 'slides'],
|
|
45
|
+
[/\.(wav|mp3|m4a|aac|flac|aiff?)$/i, 'audio'], [/\.(mp4|mov|webm|m4v)$/i, 'video'], [/\.(zip|tar|gz|tgz|bz2|xz|7z|ehpk)$/i, 'archive']];
|
|
46
|
+
for (var i = 0; i < map.length; i++) if (map[i][0].test(n)) return map[i][1];
|
|
47
|
+
return 'file';
|
|
48
|
+
}
|
|
49
|
+
// The checked DTO. Anything else from the server is ignored; a card never trusts a field it did not expect.
|
|
50
|
+
function clean(dto) {
|
|
51
|
+
if (!dto || (!LINK_ID.test(dto.id || '') && !FILE_ID.test(dto.id || '')) || ['available', 'unavailable'].indexOf(dto.state) < 0) return null;
|
|
52
|
+
var name = String(dto.name || 'File').slice(0, 120);
|
|
53
|
+
return {
|
|
54
|
+
id: dto.id, name: name, label: String(dto.label || name).slice(0, 120),
|
|
55
|
+
kind: KINDS.indexOf(dto.kind) >= 0 ? dto.kind : kindOf(name), type: String(dto.type || 'File').slice(0, 40),
|
|
56
|
+
size: typeof dto.size === 'number' && dto.size >= 0 ? dto.size : null,
|
|
57
|
+
line: Math.max(0, Math.min(9999999, Math.floor(Number(dto.line)) || 0)) || null,
|
|
58
|
+
state: dto.state, reason: REASONS[dto.reason] ? dto.reason : (dto.state === 'available' ? null : 'unavailable')
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
function parts(f) {
|
|
62
|
+
if (f.kind === 'folder' || f.state !== 'available') return [];
|
|
63
|
+
var p = ['raw'];
|
|
64
|
+
if (f.kind === 'pdf' || (f.kind === 'image' && NORMALIZED.test(f.name))) p.push('inline');
|
|
65
|
+
if (['markdown', 'text', 'code', 'html', 'table'].indexOf(f.kind) >= 0) p.push('text');
|
|
66
|
+
if (f.kind === 'table') p.push('table');
|
|
67
|
+
if (f.kind === 'html') p.push('html', 'html-run');
|
|
68
|
+
if (THUMBS[f.kind] || f.kind === 'html' || (f.kind === 'image' && NORMALIZED.test(f.name))) p.push('thumb');
|
|
69
|
+
return p;
|
|
70
|
+
}
|
|
71
|
+
function url(scope, id, part, q) {
|
|
72
|
+
return '/api/preview/agent-sessions/' + encodeURIComponent(scope.provider) + '/' + encodeURIComponent(scope.id) + '/' + encodeURIComponent(id) + '/content?part=' + part + (q || '');
|
|
73
|
+
}
|
|
74
|
+
function bytesWords(n) {
|
|
75
|
+
if (typeof n !== 'number' || !(n >= 0)) return '';
|
|
76
|
+
if (n < 1024) return n + ' B';
|
|
77
|
+
if (n < 1024 * 1024) return Math.max(1, Math.round(n / 1024)) + ' KB';
|
|
78
|
+
return (n / 1024 / 1024).toFixed(n < 10 * 1024 * 1024 ? 1 : 0) + ' MB';
|
|
79
|
+
}
|
|
80
|
+
function badge(f) {
|
|
81
|
+
if (f.kind === 'folder') return 'DIR';
|
|
82
|
+
if (f.kind === 'markdown') return 'MD';
|
|
83
|
+
var ext = /\.([a-z0-9]{1,4})$/i.exec(f.name); return ext ? ext[1].toUpperCase() : 'FILE';
|
|
84
|
+
}
|
|
85
|
+
// 7.0.4 QA, kept in the shared viewer: 503 previews_busy means the Mac is already reading its limit of previews.
|
|
86
|
+
var BUSY_WORDS = 'Your Mac is busy with other previews. Try again in a moment.';
|
|
87
|
+
function isBusy(err) { return !!(err && err.status === 503 && err.code === 'previews_busy'); }
|
|
88
|
+
function failWords(err) {
|
|
89
|
+
if (isBusy(err)) return BUSY_WORDS;
|
|
90
|
+
if (err && err.status === 401) return 'Signed out. Connect this browser again to open the file.';
|
|
91
|
+
if (err && err.status === 403) return err.code === 'secret_file_denied' ? REASONS.secret_file : 'Previews are off. Turn them on in Settings, Privacy.';
|
|
92
|
+
if (err && err.status === 404) return 'This file is no longer in the recent turns. Reopen the session to refresh it.';
|
|
93
|
+
if (err && err.status === 413) return 'Too large to open here. Open it on your Mac.';
|
|
94
|
+
if (err && err.status === 415) return 'This file is not text. Download it instead.';
|
|
95
|
+
return 'The file could not load. Check that your Mac is awake, then retry.';
|
|
96
|
+
}
|
|
97
|
+
function metaWords(f) {
|
|
98
|
+
if (f.kind === 'folder') return 'Folder · Open it on your Mac';
|
|
99
|
+
if (f.state !== 'available') return f.type + ' · ' + (REASONS[f.reason] || OFF);
|
|
100
|
+
return [f.type, bytesWords(f.size), f.line ? 'line ' + f.line : ''].filter(Boolean).join(' · ');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// A card where the link sits. An unavailable, blocked or folder card is words, never a button.
|
|
104
|
+
function chip(scope, dto, words, onOpen) {
|
|
105
|
+
var f = clean(dto); if (!f) return null;
|
|
106
|
+
var open = f.state === 'available' && f.kind !== 'folder';
|
|
107
|
+
var el = element(open ? 'button' : 'span', 'pv-chip' + (open ? '' : ' off') + (f.reason === 'secret_file' ? ' blocked' : ''));
|
|
108
|
+
if (open) { el.type = 'button'; el.setAttribute('aria-label', 'Open ' + f.type + ': ' + f.name + (f.line ? ', line ' + f.line : '')); }
|
|
109
|
+
var lead = element('span', 'pv-badge', badge(f)); lead.setAttribute('aria-hidden', 'true');
|
|
110
|
+
var text = element('span', 'pv-chip-text');
|
|
111
|
+
text.appendChild(element('span', 'pv-chip-label', String(words || f.label).slice(0, 120)));
|
|
112
|
+
text.appendChild(element('span', 'pv-chip-meta', metaWords(f)));
|
|
113
|
+
var card = { el: el, f: f, url: '', generation: 0 };
|
|
114
|
+
if (open && f.kind === 'pdf' && parts(f).indexOf('thumb') >= 0) {
|
|
115
|
+
el.className += ' has-thumb'; lead = element('span', 'pv-thumb', 'PDF'); lead.setAttribute('aria-hidden', 'true'); card.thumb = lead;
|
|
116
|
+
}
|
|
117
|
+
el.appendChild(lead); el.appendChild(text);
|
|
118
|
+
if (open) el.addEventListener('click', function () { onOpen(f, el); });
|
|
119
|
+
return card;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// ── Renderers. `v` is the viewer shell from reader-media.js: { body, acts, alive(), request(url, foreground, type) } ──
|
|
123
|
+
function status(v, words) { v.body.textContent = ''; var p = element('p', 'rm-status', words); p.setAttribute('role', 'status'); v.body.appendChild(p); return p; }
|
|
124
|
+
// A busy Mac, or one that did not answer, gets a "Try again" that draws the same file again; a refusal (secret,
|
|
125
|
+
// previews off, gone, too large) does not, because trying again cannot change it.
|
|
126
|
+
function fail(v) {
|
|
127
|
+
return function (err) {
|
|
128
|
+
if (!v.alive()) return;
|
|
129
|
+
status(v, failWords(err));
|
|
130
|
+
var again = isBusy(err) || !(err && err.status) || err.status === 503;
|
|
131
|
+
if (again && typeof v.retry === 'function') v.body.appendChild(button('Try again', function () { v.retry(); }));
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
function textPart(v, scope, f, offset, limit) {
|
|
135
|
+
return v.request(url(scope, f.id, 'text', '&offset=' + (offset || 0) + (limit ? '&limit=' + limit : '')), true, 'json').then(function (d) {
|
|
136
|
+
if (!d || typeof d.text !== 'string') throw new Error('No text');
|
|
137
|
+
return d;
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
function moreButton(v, words, run) { var b = button(words, function () { b.disabled = true; b.textContent = 'Loading…'; run(b); }); return b; }
|
|
141
|
+
|
|
142
|
+
function renderMarkdown(v, scope, f) {
|
|
143
|
+
var all = '';
|
|
144
|
+
var box = element('div', 'pv-scroll'), md = element('div', 'sx-md pv-md'); box.appendChild(md);
|
|
145
|
+
function load(offset) {
|
|
146
|
+
return textPart(v, scope, f, offset).then(function (d) {
|
|
147
|
+
if (!v.alive()) return;
|
|
148
|
+
all += d.text;
|
|
149
|
+
// The reply renderer: everything escaped first, a small subset of Markdown, links only to https.
|
|
150
|
+
md.innerHTML = W.sessionKit && W.sessionKit.markdown ? W.sessionKit.markdown(all) : '';
|
|
151
|
+
if (!md.innerHTML) md.textContent = all;
|
|
152
|
+
if (box.parentNode !== v.body) { v.body.textContent = ''; v.body.appendChild(box); }
|
|
153
|
+
if (d.more) md.appendChild(moreButton(v, 'Show more', function () { load(d.end); }));
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
load(0).catch(fail(v));
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function renderCode(v, scope, f) {
|
|
160
|
+
var box = element('div', 'pv-scroll'), code = element('div', 'pv-code'), lines = [], next = 1, carry = '', loaded = false;
|
|
161
|
+
box.appendChild(code);
|
|
162
|
+
var bar = element('div', 'pv-bar'), note = element('span', 'rm-status');
|
|
163
|
+
var jump = element('form', 'pv-jump'), input = element('input'); input.type = 'number'; input.min = '1'; input.inputMode = 'numeric'; input.setAttribute('aria-label', 'Line number'); input.placeholder = 'Line';
|
|
164
|
+
jump.appendChild(input); jump.appendChild(button('Go to line', function () { go(Number(input.value)); }));
|
|
165
|
+
jump.addEventListener('submit', function (e) { e.preventDefault(); go(Number(input.value)); });
|
|
166
|
+
bar.appendChild(jump); bar.appendChild(note);
|
|
167
|
+
var more = null, end = 0;
|
|
168
|
+
function addLines(text, final) {
|
|
169
|
+
var parts = (carry + text).split('\n');
|
|
170
|
+
carry = final ? '' : parts.pop();
|
|
171
|
+
if (final && parts.length && parts[parts.length - 1] === '' ) parts.pop();
|
|
172
|
+
parts.forEach(function (t) {
|
|
173
|
+
var row = element('div', 'pv-line'); row.setAttribute('data-line', String(next));
|
|
174
|
+
row.appendChild(element('span', 'pv-n', String(next))); row.appendChild(element('span', 'pv-t', t || ' '));
|
|
175
|
+
code.appendChild(row); lines.push(row); next++;
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
function go(n) {
|
|
179
|
+
n = Math.floor(n);
|
|
180
|
+
if (!(n >= 1)) return;
|
|
181
|
+
if (n > lines.length) { note.textContent = more ? 'Line ' + n + ' is further down. Show more first.' : 'This file has ' + lines.length + ' lines.'; return; }
|
|
182
|
+
lines.forEach(function (r) { r.classList.remove('hit'); });
|
|
183
|
+
var row = lines[n - 1]; row.classList.add('hit');
|
|
184
|
+
try { row.scrollIntoView({ block: 'center' }); } catch (_e) { box.scrollTop = row.offsetTop - 40; }
|
|
185
|
+
note.textContent = 'Line ' + n;
|
|
186
|
+
}
|
|
187
|
+
function load(offset) {
|
|
188
|
+
return textPart(v, scope, f, offset).then(function (d) {
|
|
189
|
+
if (!v.alive()) return;
|
|
190
|
+
addLines(d.text, !d.more); end = d.end;
|
|
191
|
+
if (!loaded) { loaded = true; v.body.textContent = ''; v.body.appendChild(bar); v.body.appendChild(box); }
|
|
192
|
+
if (more) { more.remove(); more = null; }
|
|
193
|
+
if (d.more) { more = moreButton(v, 'Show more', function () { load(end); }); code.appendChild(more); }
|
|
194
|
+
if (f.line && lines.length >= f.line && !box.__jumped) { box.__jumped = true; input.value = String(f.line); go(f.line); }
|
|
195
|
+
else if (f.line && !d.more && !box.__jumped) { box.__jumped = true; go(f.line); }
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
load(0).catch(fail(v));
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function renderTable(v, scope, f) {
|
|
202
|
+
var box = element('div', 'pv-scroll'), table = element('table', 'pv-table'), head = element('thead'), body = element('tbody'), note = element('p', 'rm-status');
|
|
203
|
+
table.appendChild(head); table.appendChild(body); box.appendChild(table);
|
|
204
|
+
var more = null, shown = 0;
|
|
205
|
+
function load(offset) {
|
|
206
|
+
return v.request(url(scope, f.id, 'table', '&offset=' + offset), true, 'json').then(function (d) {
|
|
207
|
+
if (!v.alive()) return;
|
|
208
|
+
if (!d || !Array.isArray(d.rows) || !Array.isArray(d.header)) throw new Error('No table');
|
|
209
|
+
if (!shown && !head.firstChild) {
|
|
210
|
+
var tr = element('tr'); tr.appendChild(element('th', 'pv-rn', '#'));
|
|
211
|
+
d.header.forEach(function (c) { var th = element('th', '', String(c)); th.setAttribute('scope', 'col'); tr.appendChild(th); });
|
|
212
|
+
head.appendChild(tr);
|
|
213
|
+
v.body.textContent = ''; v.body.appendChild(box); v.body.appendChild(note);
|
|
214
|
+
}
|
|
215
|
+
d.rows.forEach(function (r) {
|
|
216
|
+
var tr = element('tr'); shown++; tr.appendChild(element('td', 'pv-rn', String(shown)));
|
|
217
|
+
for (var k = 0; k < d.header.length || k < r.length; k++) tr.appendChild(element('td', '', String(r[k] == null ? '' : r[k])));
|
|
218
|
+
body.appendChild(tr);
|
|
219
|
+
});
|
|
220
|
+
if (more) { more.remove(); more = null; }
|
|
221
|
+
var words = [shown + (shown === 1 ? ' row' : ' rows') + ' shown'];
|
|
222
|
+
if (d.capped && !d.more) words.push('The preview stops at 5,000 rows. Download it for the rest.');
|
|
223
|
+
if (d.columnsCut) words.push('Only the first 50 columns are shown.');
|
|
224
|
+
note.textContent = words.join('. ') + (words.length > 1 ? '' : '.');
|
|
225
|
+
if (d.more) { more = moreButton(v, 'Show 200 more rows', function () { load(d.next); }); v.body.appendChild(more); }
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
load(0).catch(fail(v));
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// HTML is served by the server under its own sandbox CSP (scripts off, no network), with the same policy injected as a
|
|
232
|
+
// <meta> at the top of the page. The frame is sandboxed too and never gets allow-same-origin. Full length.
|
|
233
|
+
function renderHtml(v, scope, f) {
|
|
234
|
+
var bar = element('div', 'pv-bar'), note = element('span', 'rm-status'), toggle = null, frame = null, on = false;
|
|
235
|
+
function show() {
|
|
236
|
+
if (frame) frame.remove();
|
|
237
|
+
frame = element('iframe', 'pv-frame');
|
|
238
|
+
frame.setAttribute('sandbox', on ? 'allow-scripts' : '');
|
|
239
|
+
frame.setAttribute('referrerpolicy', 'no-referrer');
|
|
240
|
+
frame.title = 'Preview of ' + f.name + (on ? ' (scripts on, no network)' : ' (scripts off)');
|
|
241
|
+
frame.src = url(scope, f.id, on ? 'html-run' : 'html');
|
|
242
|
+
v.body.appendChild(frame);
|
|
243
|
+
// Honest words: with scripts on, the page still cannot fetch anything or read COS, but a script can move its own
|
|
244
|
+
// frame to another address (no browser can stop that), so "cannot reach the internet" would overclaim.
|
|
245
|
+
note.textContent = on ? 'Scripts are on for this view only. They cannot fetch data or read COS.' : 'Scripts are off. Nothing on this page loads from the internet.';
|
|
246
|
+
toggle.textContent = on ? 'Turn scripts off' : 'Run scripts';
|
|
247
|
+
toggle.setAttribute('aria-pressed', String(on));
|
|
248
|
+
}
|
|
249
|
+
toggle = button('Run scripts', function () { on = !on; show(); });
|
|
250
|
+
bar.appendChild(toggle); bar.appendChild(note);
|
|
251
|
+
v.body.textContent = ''; v.body.appendChild(bar);
|
|
252
|
+
show();
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Decision 4a: a PDF opens inline on a desktop and in its own tab on a phone. "Phone" here is any touch-first screen,
|
|
256
|
+
// the unfolded Duo (703 wide) included: iOS Safari draws only part of a PDF inside a frame (plan canary 17, unrun).
|
|
257
|
+
function isPhone() {
|
|
258
|
+
try {
|
|
259
|
+
if (typeof W.isPhone === 'function' && W.isPhone()) return true;
|
|
260
|
+
return !!(window.matchMedia && window.matchMedia('(hover: none) and (pointer: coarse)').matches);
|
|
261
|
+
} catch (_e) { return false; }
|
|
262
|
+
}
|
|
263
|
+
function renderPdf(v, scope, f) {
|
|
264
|
+
v.body.textContent = '';
|
|
265
|
+
if (isPhone()) {
|
|
266
|
+
var p = element('p', 'rm-status', 'PDFs open in a new tab on a phone.'); v.body.appendChild(p);
|
|
267
|
+
var a = element('a', 'cx-btn', 'Open ' + f.name); a.href = url(scope, f.id, 'inline'); a.target = '_blank'; a.rel = 'noopener'; v.body.appendChild(a);
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
// Same origin, so the browser's own PDF viewer draws it; the server sends it as application/pdf, nosniff.
|
|
271
|
+
var frame = element('iframe', 'pv-frame'); frame.title = f.name; frame.src = url(scope, f.id, 'inline');
|
|
272
|
+
v.body.appendChild(frame);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function renderImage(v, scope, f) {
|
|
276
|
+
status(v, 'Loading image…');
|
|
277
|
+
var gif = !NORMALIZED.test(f.name);
|
|
278
|
+
v.request(url(scope, f.id, gif ? 'raw' : 'inline'), true, 'blob').then(function (blob) {
|
|
279
|
+
if (!v.alive()) return;
|
|
280
|
+
if (!blob || !blob.size) throw new Error('No image');
|
|
281
|
+
var typed = new Blob([blob], { type: gif ? 'image/gif' : 'image/jpeg' });
|
|
282
|
+
v.setUrl(URL.createObjectURL(typed));
|
|
283
|
+
v.body.textContent = ''; var img = element('img'); img.alt = f.name; img.src = v.url(); v.body.appendChild(img);
|
|
284
|
+
}).catch(fail(v));
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function renderOther(v, scope, f) {
|
|
288
|
+
v.body.textContent = '';
|
|
289
|
+
var box = element('div', 'pv-other');
|
|
290
|
+
if (parts(f).indexOf('thumb') >= 0) {
|
|
291
|
+
var t = element('span', 'pv-thumb', badge(f)); box.appendChild(t);
|
|
292
|
+
v.request(url(scope, f.id, 'thumb'), true, 'blob').then(function (blob) {
|
|
293
|
+
if (!v.alive() || !blob || !blob.size) return;
|
|
294
|
+
v.setUrl(URL.createObjectURL(new Blob([blob], { type: 'image/png' })));
|
|
295
|
+
t.textContent = ''; var img = element('img'); img.alt = ''; img.src = v.url(); t.appendChild(img);
|
|
296
|
+
}, function () { t.textContent = 'No preview image'; });
|
|
297
|
+
}
|
|
298
|
+
box.appendChild(element('p', 'rm-status', f.type + (f.size != null ? ' · ' + bytesWords(f.size) : '') + '. Download it, or open it on your Mac.'));
|
|
299
|
+
v.body.appendChild(box);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function render(v, scope, dto) {
|
|
303
|
+
var f = clean(dto); if (!f) return status(v, OFF);
|
|
304
|
+
if ((' ' + v.body.className + ' ').indexOf(' pv-body ') < 0) v.body.className += ' pv-body';
|
|
305
|
+
v.retry = function () { if (v.alive()) render(v, scope, dto); };
|
|
306
|
+
if (f.state !== 'available') return status(v, REASONS[f.reason] || OFF);
|
|
307
|
+
if (f.kind === 'markdown') return renderMarkdown(v, scope, f);
|
|
308
|
+
if (f.kind === 'text' || f.kind === 'code') return renderCode(v, scope, f);
|
|
309
|
+
if (f.kind === 'table') return renderTable(v, scope, f);
|
|
310
|
+
if (f.kind === 'html') return renderHtml(v, scope, f);
|
|
311
|
+
if (f.kind === 'pdf') return renderPdf(v, scope, f);
|
|
312
|
+
if (f.kind === 'image') return renderImage(v, scope, f);
|
|
313
|
+
return renderOther(v, scope, f);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
W.preview = { chip: chip, render: render, clean: clean, parts: parts, kindOf: kindOf, url: url, failWords: failWords, isBusy: isBusy, reasons: REASONS, isPhone: isPhone };
|
|
317
|
+
W.registerPane('preview', W.preview);
|
|
318
|
+
})();
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/* COS Control Web: session questions and approvals (panes/questions.js), server 6.99.12. */
|
|
1
|
+
/* COS Control Web: session questions and approvals (panes/questions.js), from server 6.99.12. */
|
|
2
2
|
.sq-host { display: grid; gap: 12px; margin: 0 0 16px; }
|
|
3
3
|
.sq-host[hidden] { display: none; }
|
|
4
4
|
.sq-card { background: var(--panel); border: 1px solid color-mix(in srgb, var(--hot) 45%, var(--panel-border)); border-radius: 16px; padding: 14px 16px 12px; min-width: 0; }
|
|
@@ -52,11 +52,27 @@
|
|
|
52
52
|
.sq-foot { position: sticky; bottom: var(--safe-bottom, 0px); z-index: 2; margin: 12px -16px -12px; padding: 10px 16px 12px; background: var(--panel);
|
|
53
53
|
border-top: 1px solid var(--row-border); border-radius: 0 0 16px 16px; }
|
|
54
54
|
.sq-foot .sq-acts { margin-top: 0; }
|
|
55
|
-
/* QA 6.99.27: on the page, the footer rides above the
|
|
56
|
-
|
|
57
|
-
bottom of the sheet with no gap. */
|
|
55
|
+
/* QA 6.99.27: on the page, the footer rides above whatever covers the bottom (questions.js sets --sq-kb from app.js
|
|
56
|
+
viewportFit: a keyboard, its strip, a browser's bottom bar). In Today's sheet the layer is itself the visible part of
|
|
57
|
+
the page (today-actions.js applyFit), so the footer sits at the bottom of the sheet with no gap. */
|
|
58
58
|
.sq-host .sq-foot { bottom: max(var(--safe-bottom, 0px), var(--sq-kb, 0px)); }
|
|
59
59
|
.ta-q .sq-foot { bottom: 0; }
|
|
60
|
+
/* 6.99.30 (canary): with a keyboard up on the page (questions.js sets sq-kb-up) the footer scrolls with the card, so
|
|
61
|
+
the options it would cover stay readable. */
|
|
62
|
+
.sq-host.sq-kb-up .sq-foot { position: static; }
|
|
63
|
+
/* The Mac not answering: Submit, Allow and Deny wait, and say why (questions.js offline). */
|
|
64
|
+
.sq-offline { margin: 8px 0 0; font-size: 13px; color: var(--sub); }
|
|
65
|
+
/* 6.99.30 (canary): folded, a waiting question is one line over the pane (it filled the first screen of every pane);
|
|
66
|
+
a tap opens the cards. From 600 px the cards show as before and the line does not. */
|
|
67
|
+
.sq-banner { display: none; }
|
|
68
|
+
@media (max-width: 599px) {
|
|
69
|
+
.sq-host .sq-banner { display: flex; align-items: center; gap: 10px; width: 100%; min-height: 44px; padding: 10px 14px; border-radius: 14px;
|
|
70
|
+
background: var(--panel); border: 1px solid color-mix(in srgb, var(--hot) 45%, var(--panel-border)); color: var(--ink); font: 600 15px/1.3 var(--body); text-align: left; }
|
|
71
|
+
.sq-host .sq-banner-text { flex: 1; min-width: 0; }
|
|
72
|
+
.sq-host .sq-banner-act { flex: none; color: var(--accent); }
|
|
73
|
+
.sq-host .sq-cards:not(.open) { display: none; }
|
|
74
|
+
}
|
|
75
|
+
.sq-cards { display: grid; gap: 12px; }
|
|
60
76
|
.sq-progress { margin: 0 0 8px; font: 500 13.5px/1.35 var(--body); color: var(--sub); }
|
|
61
77
|
.sq-progress[data-alert] { color: var(--hot); font-weight: 600; }
|
|
62
78
|
.sq-keys { display: none; margin-top: 10px; }
|