@songsid/agend 2.3.3 → 2.3.4-beta.1
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/README.md +3 -3
- package/README.zh-TW.md +3 -3
- package/dist/agent-cli-instructions.md +1 -1
- package/dist/auth-api.d.ts +16 -3
- package/dist/auth-api.js +22 -6
- package/dist/auth-api.js.map +1 -1
- package/dist/authority/web-authority.d.ts +15 -1
- package/dist/authority/web-authority.js +27 -10
- package/dist/authority/web-authority.js.map +1 -1
- package/dist/backend/claude-code.js +11 -1
- package/dist/backend/claude-code.js.map +1 -1
- package/dist/backend/codex.js +12 -0
- package/dist/backend/codex.js.map +1 -1
- package/dist/backend/types.d.ts +7 -0
- package/dist/backend/types.js.map +1 -1
- package/dist/config-validator.js +18 -1
- package/dist/config-validator.js.map +1 -1
- package/dist/daemon.js +8 -1
- package/dist/daemon.js.map +1 -1
- package/dist/env-file.d.ts +2 -0
- package/dist/env-file.js +13 -8
- package/dist/env-file.js.map +1 -1
- package/dist/fleet-manager.d.ts +25 -1
- package/dist/fleet-manager.js +150 -21
- package/dist/fleet-manager.js.map +1 -1
- package/dist/general-knowledge/skills/cross-instance-messaging/SKILL.md +1 -1
- package/dist/general-knowledge/skills/web-dashboard/SKILL.md +4 -0
- package/dist/general-knowledge/skills/worker-collaboration/SKILL.md +1 -1
- package/dist/instance-config-impact.js +1 -0
- package/dist/instance-config-impact.js.map +1 -1
- package/dist/instance-lifecycle.d.ts +2 -0
- package/dist/instance-lifecycle.js +23 -7
- package/dist/instance-lifecycle.js.map +1 -1
- package/dist/instructions.d.ts +1 -1
- package/dist/instructions.js +1 -1
- package/dist/instructions.js.map +1 -1
- package/dist/interaction-observation.d.ts +1 -0
- package/dist/interaction-observation.js +1 -1
- package/dist/interaction-observation.js.map +1 -1
- package/dist/locale.js +48 -0
- package/dist/locale.js.map +1 -1
- package/dist/next-steps.d.ts +12 -2
- package/dist/next-steps.js +19 -3
- package/dist/next-steps.js.map +1 -1
- package/dist/office-share-service.d.ts +57 -0
- package/dist/office-share-service.js +76 -11
- package/dist/office-share-service.js.map +1 -1
- package/dist/office-share.d.ts +8 -0
- package/dist/office-share.js +34 -0
- package/dist/office-share.js.map +1 -1
- package/dist/outbound-schemas.js +1 -1
- package/dist/outbound-schemas.js.map +1 -1
- package/dist/page-share.d.ts +122 -0
- package/dist/page-share.js +199 -0
- package/dist/page-share.js.map +1 -0
- package/dist/public-preview.d.ts +4 -0
- package/dist/public-preview.js +5 -4
- package/dist/public-preview.js.map +1 -1
- package/dist/public-web-gateway.js +5 -0
- package/dist/public-web-gateway.js.map +1 -1
- package/dist/quickstart-api.d.ts +5 -0
- package/dist/quickstart-api.js +1 -1
- package/dist/quickstart-api.js.map +1 -1
- package/dist/quickstart.d.ts +40 -0
- package/dist/quickstart.js +174 -28
- package/dist/quickstart.js.map +1 -1
- package/dist/remote-card-answer.js +2 -0
- package/dist/remote-card-answer.js.map +1 -1
- package/dist/remote-card-prompt.js +4 -2
- package/dist/remote-card-prompt.js.map +1 -1
- package/dist/selection-card.d.ts +21 -0
- package/dist/selection-card.js +129 -0
- package/dist/selection-card.js.map +1 -0
- package/dist/settings-api.js +4 -0
- package/dist/settings-api.js.map +1 -1
- package/dist/settings-change.js +5 -0
- package/dist/settings-change.js.map +1 -1
- package/dist/settings-effect.js +2 -0
- package/dist/settings-effect.js.map +1 -1
- package/dist/setup-host.js +2 -2
- package/dist/setup-host.js.map +1 -1
- package/dist/setup-wizard.js +2 -2
- package/dist/setup-wizard.js.map +1 -1
- package/dist/types.d.ts +10 -0
- package/dist/ui/chat-store.js +8 -1
- package/dist/ui/chat-strings.js +36 -0
- package/dist/ui/page-share-dialog.js +191 -0
- package/dist/ui/panel-chat.js +14 -0
- package/dist/ui/panel-notices.js +98 -0
- package/dist/ui/panel-settings.js +9 -2
- package/dist/ui/preview-pages.js +41 -0
- package/dist/ui/preview-panel-actions.js +161 -0
- package/dist/ui/preview-panel.js +209 -28
- package/dist/ui/preview-versions.js +155 -0
- package/dist/ui/preview.js +88 -3
- package/dist/ui/settings-registry.js +1 -0
- package/dist/ui/settings-strings.js +4 -0
- package/dist/ui/shared/app-i18n.js +2 -2
- package/dist/ui/shared/app-route.js +4 -0
- package/dist/ui/shared/app-shell.js +5 -3
- package/dist/ui/shared/app-store.js +71 -1
- package/dist/ui/shared/app-stream.js +1 -1
- package/dist/ui/shared/app.css +43 -1
- package/dist/ui/shared/app.js +22 -1
- package/dist/ui/shared/signin.js +12 -1
- package/dist/ui/shared/ui-icons.js +3 -0
- package/dist/ui/shared/web-authorize.js +3 -1
- package/dist/web-api.d.ts +26 -0
- package/dist/web-api.js +180 -1
- package/dist/web-api.js.map +1 -1
- package/dist/web-auth.js +7 -1
- package/dist/web-auth.js.map +1 -1
- package/dist/web-login.d.ts +2 -0
- package/dist/web-login.js +3 -1
- package/dist/web-login.js.map +1 -1
- package/dist/web-notices.d.ts +51 -0
- package/dist/web-notices.js +118 -0
- package/dist/web-notices.js.map +1 -0
- package/dist/web-only-fleet.d.ts +22 -0
- package/dist/web-only-fleet.js +13 -0
- package/dist/web-only-fleet.js.map +1 -0
- package/dist/web-preview.d.ts +32 -3
- package/dist/web-preview.js +52 -19
- package/dist/web-preview.js.map +1 -1
- package/dist/web-session.d.ts +7 -0
- package/dist/web-session.js +2 -0
- package/dist/web-session.js.map +1 -1
- package/dist/web-settings-patch.d.ts +3 -1
- package/dist/web-settings-patch.js +4 -1
- package/dist/web-settings-patch.js.map +1 -1
- package/dist/web-shell-routes.d.ts +2 -0
- package/dist/web-shell-routes.js +3 -0
- package/dist/web-shell-routes.js.map +1 -1
- package/package.json +1 -1
package/dist/ui/preview-panel.js
CHANGED
|
@@ -17,6 +17,11 @@ import "./chat-render.js";
|
|
|
17
17
|
import "./preview.js";
|
|
18
18
|
import { loadHtmlAttachment } from "./html-attachment.js";
|
|
19
19
|
import { chatTime, onDayChange } from "./chat-time.js";
|
|
20
|
+
import { PanelActions } from "./preview-panel-actions.js";
|
|
21
|
+
import { htmlTitle, versionsOf, lineDiff, diffHunks, DIFF_LIMITS } from "./preview-versions.js";
|
|
22
|
+
import { pagesOf, pageIdOf } from "./preview-pages.js";
|
|
23
|
+
|
|
24
|
+
export { htmlTitle };
|
|
20
25
|
|
|
21
26
|
const R = () => globalThis.AgendChatRender;
|
|
22
27
|
const P = () => globalThis.AgendPreview;
|
|
@@ -60,12 +65,6 @@ export function closePanel() {
|
|
|
60
65
|
/** The card key the panel shows, or null. */
|
|
61
66
|
export const shownKey = () => (state.open ? state.open.key : null);
|
|
62
67
|
|
|
63
|
-
/** The text of a block's <title>, or "" — what says two blocks are versions of one thing. */
|
|
64
|
-
export function htmlTitle(code) {
|
|
65
|
-
const m = /<title[^>]*>([\s\S]*?)<\/title>/i.exec(String(code || ""));
|
|
66
|
-
return m ? m[1].replace(/\s+/g, " ").trim() : "";
|
|
67
|
-
}
|
|
68
|
-
|
|
69
68
|
/**
|
|
70
69
|
* A newer version of the shown block: the last complete ```html block in an agent reply after the one it came from.
|
|
71
70
|
* When the shown block has a <title>, only a block with the same title counts (another page is not a new version);
|
|
@@ -118,6 +117,30 @@ export function clampWidth(w, total) {
|
|
|
118
117
|
return Math.round(Math.min(max, Math.max(MIN_PANEL, w)));
|
|
119
118
|
}
|
|
120
119
|
|
|
120
|
+
// ── #2911 (7): a CDN library the preview blocked (web.preview_cdn off) — the panel says where to allow it ──
|
|
121
|
+
|
|
122
|
+
/** web-preview.ts PREVIEW_CDN_SOURCES (a test keeps the two equal): what web.preview_cdn allows. */
|
|
123
|
+
export const CDN_PREFIXES = ["https://cdnjs.cloudflare.com/ajax/libs/", "https://cdn.jsdelivr.net/npm/"];
|
|
124
|
+
/** The CDN host of the first blocked library in a frame's error log that the setting would allow, or null. */
|
|
125
|
+
export function blockedCdnHost(log) {
|
|
126
|
+
for (const x of (log && log.items) || []) {
|
|
127
|
+
if (x.kind !== "blocked") continue;
|
|
128
|
+
const p = CDN_PREFIXES.find(pre => String(x.message).startsWith(pre));
|
|
129
|
+
if (p) return new URL(p).host;
|
|
130
|
+
}
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// ── #2911 (2): the page's width in the panel (a phone, a tablet, or the panel's own), this device's choice ──
|
|
135
|
+
|
|
136
|
+
const DEVICE_KEY = "agend_preview_panel_device";
|
|
137
|
+
/** The widths a preview can be shown at: CSS px, or null for the panel's full width. */
|
|
138
|
+
export const DEVICE_WIDTHS = { full: null, tablet: 768, phone: 390 };
|
|
139
|
+
export function storedDevice() { try { const v = localStorage.getItem(DEVICE_KEY); return Object.hasOwn(DEVICE_WIDTHS, v) ? v : "full"; } catch { return "full"; } }
|
|
140
|
+
function storeDevice(v) { try { localStorage.setItem(DEVICE_KEY, v); } catch { /* this page only */ } }
|
|
141
|
+
/** The element the page has fullscreen (standard, else webkit's), or null. */
|
|
142
|
+
const fullscreenEl = () => { const d = globalThis.document; return (d && (d.fullscreenElement || d.webkitFullscreenElement)) || null; };
|
|
143
|
+
|
|
121
144
|
/** #1770: as the thread shows it — the fleet's zone, with the day when it is not today. */
|
|
122
145
|
const timeOf = (ts) => (ts ? chatTime(ts, { yesterday: (hm) => t("chat.yesterdayAt", hm) }) : "");
|
|
123
146
|
|
|
@@ -133,10 +156,53 @@ export function PreviewPanel({ name, split, msgs, subscribe, keyOf, reveal, down
|
|
|
133
156
|
const [run, setRun] = useState({ state: "idle", reason: "" });
|
|
134
157
|
const [newer, setNewer] = useState(null);
|
|
135
158
|
const [newerNote, setNewerNote] = useState("");
|
|
159
|
+
const [vers, setVers] = useState({ list: [], index: -1 }); // #2911 (1): the shown block's versions in the thread
|
|
160
|
+
const [pages, setPages] = useState([]); // #2911 (6): every page in the thread, newest first
|
|
161
|
+
const [cdnHost, setCdnHost] = useState(null); // #2911 (7): a CDN library this page's run was refused
|
|
162
|
+
const [listing, setListing] = useState(false); // the page list is on screen (instead of the page)
|
|
163
|
+
const [changes, setChanges] = useState(null); // the Changes view: { state, from, hunks, shown, total }
|
|
164
|
+
const keepView = useRef(false); // a version step keeps Code / Changes on screen
|
|
165
|
+
const loaded = useRef(new Map()); // an attached version's HTML, read once (key → code)
|
|
136
166
|
const reading = useRef(null); // the newer version whose file is being read (one at a time)
|
|
137
167
|
const pending = useRef(0); // #1704: the card's click, waiting for this device's Allow (the st.run it came with)
|
|
138
168
|
const ownAllow = useRef(0); // while this panel's own Allow commits its choice: the click it answers (not cancelled by it)
|
|
139
169
|
const holder = useRef(null), divider = useRef(null), panel = useRef(null);
|
|
170
|
+
const [device, setDeviceState] = useState(storedDevice); // #2911 (2): the page's width
|
|
171
|
+
const setDevice = (v) => { setDeviceState(v); storeDevice(v); };
|
|
172
|
+
// Full screen: the Fullscreen API on the panel (the dashboard's document — the frame's own policy still denies it),
|
|
173
|
+
// or, where the page may not, the panel over the whole window ("immersive"). Esc leaves it before it closes the panel.
|
|
174
|
+
const [fs, setFs] = useState("");
|
|
175
|
+
const fsTarget = useRef(null); // the element asked to go full screen (the panel may be gone when it is left)
|
|
176
|
+
// #2918 review: each request has a generation; leaving (Esc, the button, a close, the chat unmounting) moves it, so a
|
|
177
|
+
// request still pending answers nobody — a late refusal does not make a later open immersive, and a late success
|
|
178
|
+
// only takes back the full screen it made.
|
|
179
|
+
const fsGen = useRef(0);
|
|
180
|
+
const exitOwn = (el) => {
|
|
181
|
+
const d = globalThis.document;
|
|
182
|
+
if (!d || !el || fullscreenEl() !== el) return;
|
|
183
|
+
const ex = d.exitFullscreen || d.webkitExitFullscreen;
|
|
184
|
+
try { const r = typeof ex === "function" ? ex.call(d) : null; if (r && typeof r.catch === "function") r.catch(() => {}); } catch { /* already out */ }
|
|
185
|
+
};
|
|
186
|
+
const enterFs = () => {
|
|
187
|
+
const el = panel.current, d = globalThis.document; if (!el || fs) return;
|
|
188
|
+
const gen = ++fsGen.current;
|
|
189
|
+
const req = el.requestFullscreen || el.webkitRequestFullscreen;
|
|
190
|
+
const enabled = d ? (d.fullscreenEnabled ?? d.webkitFullscreenEnabled) : false;
|
|
191
|
+
if (typeof req !== "function" || enabled === false) { setFs("immersive"); return; }
|
|
192
|
+
let r;
|
|
193
|
+
fsTarget.current = el;
|
|
194
|
+
try { r = req.call(el); } catch { fsTarget.current = null; setFs("immersive"); return; }
|
|
195
|
+
if (r && typeof r.then === "function") {
|
|
196
|
+
r.then(() => { if (fsGen.current === gen) { if (fullscreenEl() === el) setFs("native"); } else exitOwn(el); },
|
|
197
|
+
() => { if (fsGen.current === gen) { fsTarget.current = null; setFs("immersive"); } });
|
|
198
|
+
} else setFs("native");
|
|
199
|
+
};
|
|
200
|
+
function leaveFs() {
|
|
201
|
+
fsGen.current++;
|
|
202
|
+
const el = fsTarget.current; fsTarget.current = null;
|
|
203
|
+
exitOwn(el);
|
|
204
|
+
setFs("");
|
|
205
|
+
}
|
|
140
206
|
const open = st.open?.instance === name ? st.open : null;
|
|
141
207
|
|
|
142
208
|
useEffect(() => panelStore.subscribe(setSt), []);
|
|
@@ -147,6 +213,15 @@ export function PreviewPanel({ name, split, msgs, subscribe, keyOf, reveal, down
|
|
|
147
213
|
useEffect(() => (P() ? P().onChange(() => { if (pending.current && ownAllow.current !== pending.current) pending.current = 0; choice(n => n + 1); }) : undefined), []);
|
|
148
214
|
// Leaving the chat (another instance, another panel) closes the panel: its frame stops with the thread's.
|
|
149
215
|
useEffect(() => () => closePanel(), []);
|
|
216
|
+
// Leaving the chat: a pending request answers nobody, and our own full screen is taken back.
|
|
217
|
+
useEffect(() => () => { fsGen.current++; exitOwn(fsTarget.current); fsTarget.current = null; }, []);
|
|
218
|
+
// Full screen ends with the browser's own Esc or its exit control too: follow it.
|
|
219
|
+
useEffect(() => {
|
|
220
|
+
const d = globalThis.document; if (!d || !d.addEventListener) return undefined;
|
|
221
|
+
const sync = () => setFs(f => (f === "native" && fullscreenEl() !== fsTarget.current ? "" : f));
|
|
222
|
+
d.addEventListener("fullscreenchange", sync); d.addEventListener("webkitfullscreenchange", sync);
|
|
223
|
+
return () => { d.removeEventListener("fullscreenchange", sync); d.removeEventListener("webkitfullscreenchange", sync); };
|
|
224
|
+
}, []);
|
|
150
225
|
// #1770: the source and newer-version labels say the day as the thread does — drawn again when the fleet's day
|
|
151
226
|
// changes, while a block is open. A render: the frame and the run are left as they are.
|
|
152
227
|
const [, dayTick] = useState(0);
|
|
@@ -166,7 +241,9 @@ export function PreviewPanel({ name, split, msgs, subscribe, keyOf, reveal, down
|
|
|
166
241
|
// change of this device's choice cancels it.
|
|
167
242
|
useEffect(() => {
|
|
168
243
|
setRun({ state: "idle", reason: "" });
|
|
169
|
-
setView("preview");
|
|
244
|
+
if (!keepView.current) setView("preview");
|
|
245
|
+
setListing(false); // #2920 review: a new block on screen leaves the page list
|
|
246
|
+
keepView.current = false;
|
|
170
247
|
if (open && st.run) {
|
|
171
248
|
if (P().availability().ok) start();
|
|
172
249
|
else pending.current = st.run;
|
|
@@ -174,11 +251,21 @@ export function PreviewPanel({ name, split, msgs, subscribe, keyOf, reveal, down
|
|
|
174
251
|
// Focus goes to the panel's close control that is on screen (Back on a phone, × beside the thread).
|
|
175
252
|
if (panel.current) { const f = [...panel.current.querySelectorAll(".pv-panel-close")].find(b => b.offsetParent !== null); if (f) f.focus(); }
|
|
176
253
|
}, [open && open.key, st.run]);
|
|
254
|
+
// The panel closing, or showing another instance's block: out of full screen.
|
|
255
|
+
useEffect(() => { if (!open) leaveFs(); }, [!!open]);
|
|
256
|
+
// #2911 (7): the running page's blocked CDN library, read from its error log (preview.js), kept as it changes.
|
|
257
|
+
useEffect(() => {
|
|
258
|
+
const key = open ? frameKey(open.key) : null;
|
|
259
|
+
const read = () => setCdnHost(key && P() && typeof P().errors === "function" ? blockedCdnHost(P().errors(key)) : null);
|
|
260
|
+
read();
|
|
261
|
+
if (!key || !P() || typeof P().onErrors !== "function") return undefined;
|
|
262
|
+
return P().onErrors((k) => { if (k === key) read(); });
|
|
263
|
+
}, [open && open.key]);
|
|
177
264
|
// A later reply with a newer version of this block: offered, not swapped in.
|
|
178
265
|
useEffect(() => {
|
|
179
266
|
setNewerNote(""); reading.current = null;
|
|
180
267
|
if (!open) { setNewer(null); return undefined; }
|
|
181
|
-
const check = () => setNewer(newerVersion(open,
|
|
268
|
+
const check = () => { const all = msgs() || []; setNewer(newerVersion(open, all, keyOf)); setVers(versionsOf(open, all, keyOf)); setPages(pagesOf(all, keyOf, name)); };
|
|
182
269
|
check();
|
|
183
270
|
return subscribe((instance, kind) => { if (instance === name && kind === "msgs") check(); });
|
|
184
271
|
}, [open && open.key, name]);
|
|
@@ -219,24 +306,70 @@ export function PreviewPanel({ name, split, msgs, subscribe, keyOf, reveal, down
|
|
|
219
306
|
el.addEventListener("pointerup", up);
|
|
220
307
|
el.addEventListener("pointercancel", up);
|
|
221
308
|
};
|
|
222
|
-
// Show the newer
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
309
|
+
// Show another version (the newer one offered, or a step of ‹ ›): an attached file's HTML is read first. A read the
|
|
310
|
+
// panel moved on from is dropped. It is shown as Show it shows one — waiting for Preview, never started by the step.
|
|
311
|
+
const attReason = (r) => t(r.reason === "over" ? "chat.pvAttOver" : r.reason === "gone" ? "chat.pvAttGone" : "chat.pvAttFailed");
|
|
312
|
+
// #2920 review: a newer choice supersedes a read still waiting (it opens, or claims, and so moves the generation the
|
|
313
|
+
// waiting read checks) — it is never ignored because another read is pending.
|
|
314
|
+
const show = async (spec, { keep = false } = {}) => {
|
|
315
|
+
if (!spec) return;
|
|
316
|
+
const cached = spec.code == null ? loaded.current.get(spec.key) : undefined;
|
|
317
|
+
if (spec.code != null || cached != null) { keepView.current = keep; openPanel(cached != null ? { ...spec, code: cached } : spec); return; }
|
|
318
|
+
// #2915 review: the panel's generation, not just the key — a close and reopen of the same block while this read
|
|
319
|
+
// waits moves it, and the read is then dropped (claimPanel also drops any earlier click still reading).
|
|
320
|
+
const gen = claimPanel(), mine = reading.current = spec;
|
|
227
321
|
setNewerNote(t("chat.pvLoading"));
|
|
228
|
-
// A click on
|
|
229
|
-
const r = await loadHtmlAttachment(
|
|
230
|
-
if (reading.current
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
322
|
+
// A click on a version reads it whatever its size (up to the 50 MB ceiling); over 8 MB the panel then asks.
|
|
323
|
+
const r = await loadHtmlAttachment(spec.att, undefined, { anyway: true });
|
|
324
|
+
if (reading.current === mine) reading.current = null;
|
|
325
|
+
if (panelVersion() !== gen) return;
|
|
326
|
+
if (!r.ok) { setNewerNote(attReason(r)); return; }
|
|
327
|
+
setNewerNote("");
|
|
328
|
+
loaded.current.set(spec.key, r.code);
|
|
329
|
+
keepView.current = keep;
|
|
330
|
+
openPanel({ ...spec, code: r.code });
|
|
236
331
|
};
|
|
332
|
+
const showNewer = () => show(newer);
|
|
333
|
+
/** A view button: that view on screen — the page list, if open, gives way to it (#2920 review). */
|
|
334
|
+
const pickView = (v) => { setListing(false); setView(v); };
|
|
335
|
+
const step = (by) => { const v = vers.list[vers.index + by]; if (vers.index >= 0 && v) show(v, { keep: true }); };
|
|
336
|
+
// Changes: the previous version against this one, a line diff (bounded: DIFF_LIMITS). An attached previous version
|
|
337
|
+
// is read for it, once.
|
|
338
|
+
const prev = vers.index > 0 ? vers.list[vers.index - 1] : null;
|
|
339
|
+
// A step to the first version from Changes: nothing to compare it with, so the page is shown (Changes is kept as the
|
|
340
|
+
// choice, and comes back with the next step that has a previous version).
|
|
341
|
+
const shown = view === "changes" && !prev ? "preview" : view;
|
|
342
|
+
useEffect(() => {
|
|
343
|
+
if (shown !== "changes" || !open) { setChanges(null); return undefined; }
|
|
344
|
+
let live = true;
|
|
345
|
+
const from = vers.index; // "Changes from v{from}" (1-based: the previous one)
|
|
346
|
+
const diff = (before) => {
|
|
347
|
+
const ops = lineDiff(before, open.code);
|
|
348
|
+
if (!ops) return { state: "big", from };
|
|
349
|
+
const hunks = diffHunks(ops);
|
|
350
|
+
let total = 0; for (const h of hunks) total += h.lines.length + 1;
|
|
351
|
+
return { state: "ready", from, hunks, total };
|
|
352
|
+
};
|
|
353
|
+
const known = prev.code != null ? prev.code : loaded.current.get(prev.key);
|
|
354
|
+
if (known != null) setChanges(diff(known));
|
|
355
|
+
else {
|
|
356
|
+
setChanges({ state: "loading", from });
|
|
357
|
+
loadHtmlAttachment(prev.att, undefined, { anyway: true }).then((r) => {
|
|
358
|
+
if (!live) return;
|
|
359
|
+
if (!r.ok) { setChanges({ state: "failed", from, reason: attReason(r) }); return; }
|
|
360
|
+
loaded.current.set(prev.key, r.code);
|
|
361
|
+
setChanges(diff(r.code));
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
return () => { live = false; };
|
|
365
|
+
}, [shown, open && open.key, prev && prev.key]);
|
|
237
366
|
const fileName = open && open.att ? open.att.name : undefined;
|
|
238
367
|
// Esc in the panel closes it (and is not the chat's "stop the reply").
|
|
239
|
-
const onPanelKey = (e) => {
|
|
368
|
+
const onPanelKey = (e) => {
|
|
369
|
+
if (e.key !== "Escape" || e.isComposing) return;
|
|
370
|
+
e.preventDefault();
|
|
371
|
+
if (fs) leaveFs(); else closePanel();
|
|
372
|
+
};
|
|
240
373
|
|
|
241
374
|
if (!open) return null;
|
|
242
375
|
const going = run.state === "starting" || run.state === "running";
|
|
@@ -259,33 +392,81 @@ export function PreviewPanel({ name, split, msgs, subscribe, keyOf, reveal, down
|
|
|
259
392
|
const source = t("chat.pvSource", open.sender || name, timeOf(open.ts));
|
|
260
393
|
return html`<div data-trust="preview" class="pv-divider" ref=${divider} role="separator" aria-orientation="vertical" tabindex="0" aria-label=${t("chat.pvResize")}
|
|
261
394
|
aria-valuemin=${String(MIN_PANEL)} onKeyDown=${onKey} onPointerDown=${onDown}></div>
|
|
262
|
-
<aside data-trust="preview" class
|
|
395
|
+
<aside data-trust="preview" class=${`pv-panel${fs ? ` pv-fs pv-fs-${fs}` : ""}`} ref=${panel} aria-label=${t("chat.pvPanel")} onKeyDown=${onPanelKey}>
|
|
263
396
|
<div class="pv-panel-head">
|
|
264
397
|
<button type="button" class="icon-btn only-narrow pv-panel-close" title=${t("chat.pvBack")} aria-label=${t("chat.pvBack")} onClick=${closePanel}><${Icon} name="back" size=${18} /></button>
|
|
265
398
|
<div class="pv-panel-title"><strong>${t("chat.pvPanel")}</strong>
|
|
266
399
|
<button type="button" class="pv-source" title=${source} onClick=${() => { if (window.matchMedia && window.matchMedia("(max-width: 899px)").matches) closePanel(); reveal(open.msgKey); }}>${source}</button></div>
|
|
400
|
+
${pages.length > 1 ? html`<button type="button" class=${`btn btn-sm pv-pages-toggle${listing ? " on" : ""}`} aria-pressed=${listing ? "true" : "false"} title=${t("chat.pvPagesTitle")}
|
|
401
|
+
onClick=${() => setListing(v => !v)}><${Icon} name="file" size=${14} />${t("chat.pvPagesCount", pages.length)}</button>` : null}
|
|
267
402
|
<button type="button" class="icon-btn only-wide pv-panel-close" title=${t("chat.pvClose")} aria-label=${t("chat.pvClose")} onClick=${closePanel}><${Icon} name="close" size=${18} /></button>
|
|
268
403
|
</div>
|
|
269
404
|
<div class="pv-panel-bar">
|
|
405
|
+
${vers.list.length > 1 && vers.index >= 0 ? html`<div class="pv-ver" role="group" aria-label=${t("chat.pvVersions")}>
|
|
406
|
+
<button type="button" class="icon-btn pv-ver-prev" title=${t("chat.pvVerPrev")} aria-label=${t("chat.pvVerPrev")} disabled=${vers.index === 0} onClick=${() => step(-1)}><${Icon} name="prev" size=${16} /></button>
|
|
407
|
+
<span class="pv-ver-at" aria-live="polite" title=${t("chat.pvVerLong", vers.index + 1, vers.list.length)} aria-label=${t("chat.pvVerLong", vers.index + 1, vers.list.length)}>${t("chat.pvVer", vers.index + 1, vers.list.length)}</span>
|
|
408
|
+
<button type="button" class="icon-btn pv-ver-next" title=${t("chat.pvVerNext")} aria-label=${t("chat.pvVerNext")} disabled=${vers.index === vers.list.length - 1} onClick=${() => step(1)}><${Icon} name="next" size=${16} /></button>
|
|
409
|
+
</div>` : null}
|
|
270
410
|
<div class="seg-inline" role="group" aria-label=${t("chat.pvPanel")}>
|
|
271
|
-
<button type="button" class=${`btn btn-sm${
|
|
272
|
-
<button type="button" class=${`btn btn-sm${
|
|
411
|
+
<button type="button" class=${`btn btn-sm${shown === "preview" ? " on" : ""}`} aria-pressed=${shown === "preview" ? "true" : "false"} title=${t("chat.pvPage")} onClick=${() => pickView("preview")}><${Icon} name="eye" size=${14} /><span class="lbl">${t("chat.pvPage")}</span></button>
|
|
412
|
+
<button type="button" class=${`btn btn-sm${shown === "code" ? " on" : ""}`} aria-pressed=${shown === "code" ? "true" : "false"} title=${t("chat.pvCode")} onClick=${() => pickView("code")}><${Icon} name="code" size=${14} /><span class="lbl">${t("chat.pvCode")}</span></button>
|
|
413
|
+
${prev ? html`<button type="button" class=${`btn btn-sm pv-changes${shown === "changes" ? " on" : ""}`} aria-pressed=${shown === "changes" ? "true" : "false"} title=${t("chat.pvChanges")} onClick=${() => pickView("changes")}><${Icon} name="diff" size=${14} /><span class="lbl">${t("chat.pvChanges")}</span></button>` : null}
|
|
273
414
|
</div>
|
|
274
415
|
<span class="grow"></span>
|
|
416
|
+
${shown === "preview" ? html`<select class="pv-width" aria-label=${t("chat.pvWidth")} title=${t("chat.pvWidth")} value=${device} onChange=${(e) => setDevice(e.currentTarget.value)}>
|
|
417
|
+
<option value="full">${t("chat.pvWidthFull")}</option><option value="tablet">${t("chat.pvWidthTablet")}</option><option value="phone">${t("chat.pvWidthPhone")}</option></select>` : null}
|
|
275
418
|
${going ? html`<button type="button" class="btn btn-sm pv-panel-reload" title=${t("chat.pvReload")} onClick=${() => { stop(); start(true); }}><${Icon} name="restart" size=${14} /><span class="lbl">${t("chat.pvReload")}</span></button>
|
|
276
419
|
<button type="button" class="btn btn-sm pv-panel-stop" title=${t("chat.pvStop")} onClick=${stop}><${Icon} name="stop" size=${14} /><span class="lbl">${t("chat.pvStop")}</span></button>`
|
|
277
420
|
: run.state === "large" ? html`<button type="button" class="btn btn-sm btn-primary pv-panel-anyway" title=${t("chat.pvAnyway")} disabled=${!a.ok} onClick=${() => start(true)}><${Icon} name="play" size=${14} /><span class="lbl">${t("chat.pvAnyway")}</span></button>`
|
|
278
421
|
: html`<button type="button" class="btn btn-sm btn-primary pv-panel-run" title=${t("chat.pvPreview")} disabled=${!a.ok} onClick=${() => start()}><${Icon} name="play" size=${14} /><span class="lbl">${t("chat.pvPreview")}</span></button>`}
|
|
422
|
+
<button type="button" class=${`btn btn-sm only-wide pv-panel-fs${fs ? " on" : ""}`} aria-pressed=${fs ? "true" : "false"} title=${t(fs ? "chat.pvFsExit" : "chat.pvFs")} aria-label=${t(fs ? "chat.pvFsExit" : "chat.pvFs")}
|
|
423
|
+
onClick=${() => (fs ? leaveFs() : enterFs())}><${Icon} name=${fs ? "fullscreenexit" : "fullscreen"} size=${14} /></button>
|
|
424
|
+
<${PanelActions} open=${open} name=${name} run=${run} view=${view} />
|
|
279
425
|
<button type="button" class="btn btn-sm pv-panel-dl" title=${t("chat.pvDownload")} onClick=${() => download(open.code, fileName)}><${Icon} name="download" size=${14} /><span class="lbl">${t("chat.pvDownload")}</span></button>
|
|
280
426
|
</div>
|
|
281
427
|
${newer && newer.key !== open.key ? html`<div class="pv-newer" role="status"><span>${t("chat.pvNewer", timeOf(newer.ts))}</span>
|
|
282
428
|
<button type="button" class="btn btn-sm pv-newer-show" onClick=${showNewer}>${t("chat.pvNewerShow")}</button>${newerNote ? html`<span class="pv-newer-note">${newerNote}</span>` : null}</div>` : null}
|
|
429
|
+
${newerNote && !(newer && newer.key !== open.key) ? html`<div class="pv-note pv-ver-note" role="status">${newerNote}</div>` : null}
|
|
283
430
|
${going && !P().bannerAcked() ? html`<div class="pv-banner"><span class="pv-banner-text">${P().banner()}</span>
|
|
284
431
|
<button type="button" class="btn btn-sm btn-ghost pv-ack" title=${P().bannerAckTitle()} onClick=${() => P().ackBanner()}>${P().bannerAckLabel()}</button></div>` : null}
|
|
285
|
-
${
|
|
286
|
-
|
|
432
|
+
${cdnHost && shown === "preview" ? html`<div class="pv-note pv-cdn-note" role="status">${t("chat.pvCdnNote", cdnHost)}</div>` : null}
|
|
433
|
+
${note && shown === "preview" ? html`<div class="pv-note">${note}${askOptIn ? html` <button type="button" class="btn btn-sm pv-panel-allow" onClick=${allowHere}>${t("chat.pvAllowButton")}</button>` : null}</div>` : null}
|
|
434
|
+
<div class=${`pv-panel-body${listing || shown !== "preview" ? " show-code" : ` w-${device}`}`}>
|
|
287
435
|
<div class="pv-holder" ref=${holder}></div>
|
|
288
|
-
${
|
|
436
|
+
${listing ? html`<${PageList} pages=${pages} current=${pageIdOf(open)} pick=${(p) => { setListing(false); if (p.id !== pageIdOf(open)) show(p.latest); }} />` : null}
|
|
437
|
+
${!listing && shown === "code" ? html`<pre class="pv-code"><code>${open.code}</code></pre>` : null}
|
|
438
|
+
${!listing && shown === "changes" ? html`<${Changes} changes=${changes} />` : null}
|
|
289
439
|
</div>
|
|
290
440
|
</aside>`;
|
|
291
441
|
}
|
|
442
|
+
|
|
443
|
+
/** The Changes view: hunks of the line diff as a unified diff shows them, drawn as text (never as HTML). */
|
|
444
|
+
function Changes({ changes }) {
|
|
445
|
+
if (!changes || changes.state === "loading") return html`<div class="pv-note pv-diff-note">${t("chat.pvLoading")}</div>`;
|
|
446
|
+
if (changes.state === "failed") return html`<div class="pv-note pv-diff-note">${changes.reason}</div>`;
|
|
447
|
+
if (changes.state === "big") return html`<div class="pv-note pv-diff-note">${t("chat.pvChangesBig")}</div>`;
|
|
448
|
+
if (!changes.hunks.length) return html`<div class="pv-note pv-diff-note">${t("chat.pvChangesNone", changes.from)}</div>`;
|
|
449
|
+
const rows = [];
|
|
450
|
+
let left = DIFF_LIMITS.maxLines;
|
|
451
|
+
for (const h of changes.hunks) {
|
|
452
|
+
if (left <= 0) break;
|
|
453
|
+
rows.push(html`<span class="d-hunk">${`@@ -${h.oldStart},${h.oldLines} +${h.newStart},${h.newLines} @@\n`}</span>`);
|
|
454
|
+
left--;
|
|
455
|
+
for (const l of h.lines) {
|
|
456
|
+
if (left-- <= 0) break;
|
|
457
|
+
rows.push(html`<span class=${l.t === "+" ? "d-add" : l.t === "-" ? "d-del" : "d-ctx"}>${`${l.t} ${l.text}\n`}</span>`);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
return html`<div class="pv-diff-wrap"><div class="pv-diff-head">${t("chat.pvChangesFrom", changes.from)}${changes.total > DIFF_LIMITS.maxLines ? html` · <span class="pv-diff-cut">${t("chat.pvChangesCut", DIFF_LIMITS.maxLines)}</span>` : null}</div>
|
|
461
|
+
<pre class="pv-code pv-diff"><code>${rows}</code></pre></div>`;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/** #2911 (6): the thread's pages, newest first — one row each, its newest version opened by a click (waiting for Preview). */
|
|
465
|
+
function PageList({ pages, current, pick }) {
|
|
466
|
+
return html`<ul class="pv-pages" aria-label=${t("chat.pvPagesTitle")}>${pages.map(p => html`<li key=${p.id}>
|
|
467
|
+
<button type="button" class=${`pv-page${p.id === current ? " on" : ""}`} aria-current=${p.id === current ? "true" : undefined} onClick=${() => pick(p)}>
|
|
468
|
+
<${Icon} name=${p.kind === "file" ? "attach" : "code"} size=${14} />
|
|
469
|
+
<span class="pv-page-name">${p.label || t("chat.pvPagesUntitled")}</span>
|
|
470
|
+
<span class="pv-page-meta">${p.versions > 1 ? t("chat.pvPagesVersions", p.versions) : ""}${p.versions > 1 ? " · " : ""}${timeOf(p.latest.ts)}</span>
|
|
471
|
+
</button></li>`)}</ul>`;
|
|
472
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
// #2911 (1): the versions of a page in the preview panel, and the changes from one to the next. Pure functions: the
|
|
2
|
+
// panel (preview-panel.js) draws them. Nothing here runs a preview — a version is shown as any block is, waiting for
|
|
3
|
+
// Preview (#1481 §14).
|
|
4
|
+
import "./chat-render.js";
|
|
5
|
+
|
|
6
|
+
const R = () => globalThis.AgendChatRender;
|
|
7
|
+
|
|
8
|
+
/** The text of a block's <title>, or "" — what says two blocks are versions of one thing. */
|
|
9
|
+
export function htmlTitle(code) {
|
|
10
|
+
const m = /<title[^>]*>([\s\S]*?)<\/title>/i.exec(String(code || ""));
|
|
11
|
+
return m ? m[1].replace(/\s+/g, " ").trim() : "";
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Every version of the shown block in the loaded thread, oldest first, and where the shown one is in it.
|
|
16
|
+
* - An attached file: the agent's attachments of the same name (case-insensitive). Their HTML is read when picked (code: null).
|
|
17
|
+
* - A titled block: the agent's complete ```html blocks with the same <title>.
|
|
18
|
+
* - An untitled block: the agent's complete untitled blocks — a titled one is another page.
|
|
19
|
+
* Only agent replies count, and only complete blocks, as for a card. `index` is -1 (and `list` is just the shown block)
|
|
20
|
+
* when the block is no longer in the thread.
|
|
21
|
+
*/
|
|
22
|
+
export function versionsOf(open, msgs, keyOf) {
|
|
23
|
+
if (!open) return { list: [], index: -1 };
|
|
24
|
+
const list = [];
|
|
25
|
+
const all = Array.isArray(msgs) ? msgs : [];
|
|
26
|
+
if (open.att) {
|
|
27
|
+
const name = String(open.att.name || "").trim().toLowerCase();
|
|
28
|
+
for (const x of all) {
|
|
29
|
+
if (x.role !== "agent" || !Array.isArray(x.attachments)) continue;
|
|
30
|
+
const msgKey = keyOf(x);
|
|
31
|
+
for (const a of x.attachments) {
|
|
32
|
+
if (!R().isHtmlAttachment(a) || a.name.trim().toLowerCase() !== name) continue;
|
|
33
|
+
list.push({ key: `${msgKey}:a${a.id}`, instance: open.instance, msgKey, att: { id: a.id, name: a.name, size: a.size }, code: null, sender: x.sender, ts: x.ts });
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
} else {
|
|
37
|
+
const title = htmlTitle(open.code);
|
|
38
|
+
for (const x of all) {
|
|
39
|
+
if (x.role !== "agent") continue;
|
|
40
|
+
const msgKey = keyOf(x);
|
|
41
|
+
R().htmlFences(x.text).forEach((f, n) => {
|
|
42
|
+
if (!f.terminated || htmlTitle(f.code) !== title) return;
|
|
43
|
+
list.push({ key: `${msgKey}:f${n}`, instance: open.instance, msgKey, n, code: f.code, sender: x.sender, ts: x.ts });
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
const index = list.findIndex(v => v.key === open.key);
|
|
48
|
+
return index < 0 ? { list: [open], index: -1 } : { list, index };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ── Changes: a line diff (Myers), bounded ──
|
|
52
|
+
|
|
53
|
+
/** Past these, "Changes" says the versions are too large or too different to compare here. */
|
|
54
|
+
export const DIFF_LIMITS = { maxChars: 5 * 1024 * 1024, maxInputLines: 50000, maxEdits: 2000, context: 3, maxLines: 3000 };
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* How many lines `lines()` makes of `s`, counted without splitting it (#2915 review: a budget before anything is
|
|
58
|
+
* built) — LF, CRLF and a lone CR each end a line, and a last line without one counts; it stops counting past `max`
|
|
59
|
+
* (anything over `max` is then reported as max + 1).
|
|
60
|
+
*/
|
|
61
|
+
export function lineCount(s, max = Infinity) {
|
|
62
|
+
if (!s) return 0;
|
|
63
|
+
let breaks = 0;
|
|
64
|
+
for (let i = 0; i < s.length; i++) {
|
|
65
|
+
const c = s.charCodeAt(i);
|
|
66
|
+
if (c === 10 || c === 13) {
|
|
67
|
+
if (c === 13 && s.charCodeAt(i + 1) === 10) i++;
|
|
68
|
+
if (++breaks > max) return max + 1;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
const last = s.charCodeAt(s.length - 1);
|
|
72
|
+
return Math.min(breaks + (last === 10 || last === 13 ? 0 : 1), max + 1);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const lines = (s) => { if (!s) return []; const v = String(s).replace(/\r\n?/g, "\n").split("\n"); if (v.length > 1 && v[v.length - 1] === "") v.pop(); return v; };
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* The line edits from `a` to `b`: [{ t: " " | "-" | "+", text }] in order, or null when it cannot be done within
|
|
79
|
+
* DIFF_LIMITS: either side over maxChars or maxInputLines (checked before anything is split or built: an op per line
|
|
80
|
+
* is made only under that budget), or more than maxEdits lines differ after the common head and tail.
|
|
81
|
+
*/
|
|
82
|
+
export function lineDiff(a, b, limits = DIFF_LIMITS) {
|
|
83
|
+
const sa = String(a ?? ""), sb = String(b ?? "");
|
|
84
|
+
if (sa.length > limits.maxChars || sb.length > limits.maxChars) return null;
|
|
85
|
+
if (lineCount(sa, limits.maxInputLines) > limits.maxInputLines || lineCount(sb, limits.maxInputLines) > limits.maxInputLines) return null;
|
|
86
|
+
a = sa; b = sb;
|
|
87
|
+
const A = lines(a), B = lines(b);
|
|
88
|
+
let head = 0;
|
|
89
|
+
while (head < A.length && head < B.length && A[head] === B[head]) head++;
|
|
90
|
+
let tail = 0;
|
|
91
|
+
while (tail < A.length - head && tail < B.length - head && A[A.length - 1 - tail] === B[B.length - 1 - tail]) tail++;
|
|
92
|
+
const x = A.slice(head, A.length - tail), y = B.slice(head, B.length - tail);
|
|
93
|
+
const mid = myers(x, y, limits.maxEdits);
|
|
94
|
+
if (!mid) return null;
|
|
95
|
+
return [...A.slice(0, head).map(text => ({ t: " ", text })), ...mid, ...A.slice(A.length - tail).map(text => ({ t: " ", text }))];
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Myers' O((N+M)·D) shortest edit script, with D capped (the trace is kept for the walk back). */
|
|
99
|
+
function myers(a, b, maxD) {
|
|
100
|
+
const n = a.length, m = b.length, max = n + m;
|
|
101
|
+
if (max === 0) return [];
|
|
102
|
+
const off = max + 1, v = new Int32Array(2 * max + 3), trace = [];
|
|
103
|
+
for (let d = 0; d <= Math.min(max, maxD); d++) {
|
|
104
|
+
trace.push(v.slice(off - d - 1, off + d + 2));
|
|
105
|
+
for (let k = -d; k <= d; k += 2) {
|
|
106
|
+
let i = k === -d || (k !== d && v[off + k - 1] < v[off + k + 1]) ? v[off + k + 1] : v[off + k - 1] + 1;
|
|
107
|
+
let j = i - k;
|
|
108
|
+
while (i < n && j < m && a[i] === b[j]) { i++; j++; }
|
|
109
|
+
v[off + k] = i;
|
|
110
|
+
if (i >= n && j >= m) return walkBack(a, b, trace, d);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function walkBack(a, b, trace, dEnd) {
|
|
117
|
+
const out = [];
|
|
118
|
+
let i = a.length, j = b.length;
|
|
119
|
+
for (let d = dEnd; d >= 0; d--) {
|
|
120
|
+
const vd = trace[d], at = (k) => vd[k + d + 1]; // trace[d] holds v[-d-1 .. d+1] before step d
|
|
121
|
+
const k = i - j;
|
|
122
|
+
if (d === 0) { while (i > 0 && j > 0) { out.push({ t: " ", text: a[--i] }); j--; } break; }
|
|
123
|
+
const down = k === -d || (k !== d && at(k - 1) < at(k + 1));
|
|
124
|
+
const pk = down ? k + 1 : k - 1, pi = at(pk), pj = pi - pk;
|
|
125
|
+
while (i > pi && j > pj) { out.push({ t: " ", text: a[--i] }); j--; }
|
|
126
|
+
if (down) out.push({ t: "+", text: b[--j] }); else out.push({ t: "-", text: a[--i] });
|
|
127
|
+
}
|
|
128
|
+
return out.reverse();
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* The edits as hunks with `context` lines around each change, as a unified diff shows them:
|
|
133
|
+
* [{ oldStart, oldLines, newStart, newLines, lines: [{ t, text }] }] (1-based starts). [] when nothing changed.
|
|
134
|
+
*/
|
|
135
|
+
export function diffHunks(ops, context = DIFF_LIMITS.context) {
|
|
136
|
+
// Each op's line numbers on either side (before it), then the changed spans widened by `context` and merged.
|
|
137
|
+
const oldAt = [], newAt = [];
|
|
138
|
+
let o = 1, n = 1;
|
|
139
|
+
for (const op of ops) { oldAt.push(o); newAt.push(n); if (op.t !== "+") o++; if (op.t !== "-") n++; }
|
|
140
|
+
const ranges = [];
|
|
141
|
+
ops.forEach((op, idx) => {
|
|
142
|
+
if (op.t === " ") return;
|
|
143
|
+
const from = Math.max(0, idx - context), to = Math.min(ops.length - 1, idx + context);
|
|
144
|
+
const last = ranges[ranges.length - 1];
|
|
145
|
+
if (last && from <= last.to + 1) last.to = Math.max(last.to, to); else ranges.push({ from, to });
|
|
146
|
+
});
|
|
147
|
+
return ranges.map(({ from, to }) => {
|
|
148
|
+
const part = ops.slice(from, to + 1);
|
|
149
|
+
return {
|
|
150
|
+
oldStart: oldAt[from], newStart: newAt[from],
|
|
151
|
+
oldLines: part.filter(x => x.t !== "+").length, newLines: part.filter(x => x.t !== "-").length,
|
|
152
|
+
lines: part,
|
|
153
|
+
};
|
|
154
|
+
});
|
|
155
|
+
}
|
package/dist/ui/preview.js
CHANGED
|
@@ -249,6 +249,7 @@
|
|
|
249
249
|
if (bytes > LIMITS.askBytes && !(opts && opts.anyway)) { ui.state("large", say("pvLarge", "This file is large ({0}). Preview anyway?", sizeLabel(bytes))); return false; }
|
|
250
250
|
stopAll("another");
|
|
251
251
|
starts++;
|
|
252
|
+
resetErrors(key);
|
|
252
253
|
var doc = holder.ownerDocument;
|
|
253
254
|
var iframe = mountPreview(doc);
|
|
254
255
|
var card = {
|
|
@@ -285,6 +286,7 @@
|
|
|
285
286
|
var card = live.get(key);
|
|
286
287
|
if (!card) return false;
|
|
287
288
|
live.delete(key);
|
|
289
|
+
if (card.picking) { card.picking = false; tellPick(key, null, false); }
|
|
288
290
|
if (card.readyTimer) root.clearTimeout(card.readyTimer);
|
|
289
291
|
if (card.watchdogTimer) root.clearTimeout(card.watchdogTimer);
|
|
290
292
|
if (card.iframe.parentNode) card.iframe.parentNode.removeChild(card.iframe);
|
|
@@ -310,13 +312,93 @@
|
|
|
310
312
|
|
|
311
313
|
// ── Messages from frames (design §4.2) ──────────────────
|
|
312
314
|
|
|
313
|
-
var KEYS = { ready: "boot,ch,type,v", resize: "ch,height,type,v", heartbeat: "ch,type,v"
|
|
315
|
+
var KEYS = { ready: "boot,ch,type,v", resize: "ch,height,type,v", heartbeat: "ch,type,v", error: "ch,col,kind,line,message,type,v",
|
|
316
|
+
pick: "ch,h,selector,text,type,v,w,x,y", pickend: "ch,type,v" };
|
|
317
|
+
|
|
318
|
+
// ── #2911: the errors a running preview reported (design §4.1 amendment) ──
|
|
319
|
+
// Recorded only: a count, and the first few distinct ones as one line of plain text each. Nothing here passes them on;
|
|
320
|
+
// a person's click in the panel (preview-panel-actions.js) puts them in the composer, quoted, for them to edit.
|
|
321
|
+
var ERRORS = { maxCount: 999, keep: 5, messageMax: 200, rawMax: 1000, notifyMs: 200 };
|
|
322
|
+
var ERROR_KINDS = { error: 1, rejection: 1, console: 1, blocked: 1 }; // blocked: #2911 (7), a CSP-blocked script/style/font URL
|
|
323
|
+
var errorLog = new Map(); // frame key → { count, items: [{ kind, message, line, col }] }
|
|
324
|
+
var errorListeners = new Set();
|
|
325
|
+
var errorNotify = new Map(); // frame key → a pending notification's timer
|
|
326
|
+
function oneLine(v, max) {
|
|
327
|
+
// Format characters (\p{Cf}: bidi overrides and isolates, zero-widths, U+FEFF…) go first: a quoted line must read
|
|
328
|
+
// as its bytes say (#2914 review — the Trojan Source reordering, text hidden between zero-widths).
|
|
329
|
+
var t = String(v).replace(/\p{Cf}+/gu, "").replace(/[\s\u0000-\u001f\u007f-\u009f\u2028\u2029]+/g, " ").trim();
|
|
330
|
+
var chars = Array.from(t);
|
|
331
|
+
return chars.length > max ? chars.slice(0, max - 1).join("") + "\u2026" : t;
|
|
332
|
+
}
|
|
333
|
+
function tellErrors(key) {
|
|
334
|
+
if (errorNotify.has(key)) return;
|
|
335
|
+
errorNotify.set(key, root.setTimeout(function () {
|
|
336
|
+
errorNotify.delete(key);
|
|
337
|
+
errorListeners.forEach(function (fn) { try { fn(key); } catch (e) { /* one listener's error stops no other */ } });
|
|
338
|
+
}, ERRORS.notifyMs));
|
|
339
|
+
}
|
|
340
|
+
// A new run: its chip goes at once (not after the notification delay), and a notification still pending is dropped.
|
|
341
|
+
function resetErrors(key) {
|
|
342
|
+
if (!errorLog.delete(key)) return;
|
|
343
|
+
if (errorNotify.has(key)) { root.clearTimeout(errorNotify.get(key)); errorNotify.delete(key); }
|
|
344
|
+
errorListeners.forEach(function (fn) { try { fn(key); } catch (e) { /* one listener's error stops no other */ } });
|
|
345
|
+
}
|
|
346
|
+
function recordError(card, d) {
|
|
347
|
+
if (!Object.prototype.hasOwnProperty.call(ERROR_KINDS, d.kind) || typeof d.message !== "string" || d.message.length > ERRORS.rawMax) return;
|
|
348
|
+
var pos = function (n) { return typeof n === "number" && isFinite(n) && Math.floor(n) === n && n >= 0 && n <= 1e7; };
|
|
349
|
+
if (!pos(d.line) || !pos(d.col)) return;
|
|
350
|
+
var log = errorLog.get(card.key);
|
|
351
|
+
if (!log) { log = { count: 0, items: [] }; errorLog.set(card.key, log); }
|
|
352
|
+
if (log.count >= ERRORS.maxCount) return;
|
|
353
|
+
log.count++;
|
|
354
|
+
var item = { kind: d.kind, message: oneLine(d.message, ERRORS.messageMax) || "(no message)", line: d.line, col: d.col };
|
|
355
|
+
if (log.items.length < ERRORS.keep && !log.items.some(function (x) { return x.kind === item.kind && x.message === item.message && x.line === item.line && x.col === item.col; })) log.items.push(item);
|
|
356
|
+
tellErrors(card.key);
|
|
357
|
+
}
|
|
358
|
+
/** What the preview under `key` reported since it started: { count, items } (copies), or null when nothing. */
|
|
359
|
+
function errors(key) {
|
|
360
|
+
var log = errorLog.get(key);
|
|
361
|
+
return log ? { count: log.count, items: log.items.map(function (x) { return { kind: x.kind, message: x.message, line: x.line, col: x.col }; }) } : null;
|
|
362
|
+
}
|
|
363
|
+
// ── #2911: pick mode — the person points at an element of a running preview (design §4.1 amendment) ──
|
|
364
|
+
// Only on a person's request (pick(key, true)), and only one answer per request: a `pick` from a frame that was not
|
|
365
|
+
// asked is dropped. The answer is the page's text — a selector it built and the element's words — kept one line each
|
|
366
|
+
// and handed to the listeners; the panel puts it in the composer for the person to edit. Nothing here sends it.
|
|
367
|
+
var PICK = { selectorMax: 300, textMax: 200 };
|
|
368
|
+
var pickListeners = new Set();
|
|
369
|
+
function tellPick(key, result, on) { pickListeners.forEach(function (fn) { try { fn(key, result, on); } catch (e) { /* one listener's error stops no other */ } }); }
|
|
370
|
+
/** Turn pick mode on or off for the running preview `key`; false when it is not running (rendered). */
|
|
371
|
+
function pick(key, on) {
|
|
372
|
+
var card = live.get(key);
|
|
373
|
+
if (!card || !card.rendered) return false;
|
|
374
|
+
card.picking = !!on;
|
|
375
|
+
try { card.iframe.contentWindow.postMessage({ v: 1, type: "pick", ch: card.ch, on: !!on }, "*"); } catch (e) { /* the frame went: its stop says so */ }
|
|
376
|
+
tellPick(key, null, card.picking);
|
|
377
|
+
return true;
|
|
378
|
+
}
|
|
379
|
+
function picking(key) { var c = live.get(key); return !!(c && c.picking); }
|
|
380
|
+
/** fn(key, result | null, on): a picked element ({ selector, text, x, y, w, h }), or the mode changing; returns the unsubscribe. */
|
|
381
|
+
function onPick(fn) { pickListeners.add(fn); return function () { pickListeners.delete(fn); }; }
|
|
382
|
+
function recordPick(card, d) {
|
|
383
|
+
if (!card.picking) return;
|
|
384
|
+
if (d.type === "pickend") { card.picking = false; tellPick(card.key, null, false); return; }
|
|
385
|
+
var box = function (n) { return typeof n === "number" && isFinite(n) && Math.floor(n) === n && n >= 0 && n < 1e6; };
|
|
386
|
+
if (typeof d.selector !== "string" || d.selector.length > 1000 || typeof d.text !== "string" || d.text.length > 1000) return;
|
|
387
|
+
if (!box(d.x) || !box(d.y) || !box(d.w) || !box(d.h)) return;
|
|
388
|
+
card.picking = false;
|
|
389
|
+
tellPick(card.key, { selector: oneLine(d.selector, PICK.selectorMax), text: oneLine(d.text, PICK.textMax), x: d.x, y: d.y, w: d.w, h: d.h }, false);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/** fn(key) after a preview's errors changed (at most every 200 ms per preview); returns the unsubscribe. */
|
|
393
|
+
function onErrors(fn) { errorListeners.add(fn); return function () { errorListeners.delete(fn); }; }
|
|
314
394
|
|
|
315
395
|
/**
|
|
316
396
|
* The only window message listener. A message counts only from one of OUR frames (event.source is that frame's
|
|
317
|
-
* window, and its origin is the opaque "null"), with one of
|
|
397
|
+
* window, and its origin is the opaque "null"), with one of six exact shapes; each has one effect, on that frame
|
|
318
398
|
* only: ready → send the HTML once (boot checked, opt-in checked again), resize → its height, heartbeat → its
|
|
319
|
-
* watchdog
|
|
399
|
+
* watchdog, error (#2911) → its error log (recorded, never passed on), pick / pickend (#2911) → the answer to the
|
|
400
|
+
* person's own pick request, handed to the pick listeners (dropped when none was asked). It has no reference to the chat, the
|
|
401
|
+
* composer, the fleet's API, navigation or storage.
|
|
320
402
|
*/
|
|
321
403
|
function onMessage(event) {
|
|
322
404
|
var card = null;
|
|
@@ -344,6 +426,8 @@
|
|
|
344
426
|
armWatchdog(card);
|
|
345
427
|
return;
|
|
346
428
|
}
|
|
429
|
+
if (d.type === "error") { recordError(card, d); return; }
|
|
430
|
+
if (d.type === "pick" || d.type === "pickend") { recordPick(card, d); return; }
|
|
347
431
|
if (typeof d.height !== "number" || !isFinite(d.height) || Math.floor(d.height) !== d.height) return;
|
|
348
432
|
requestHeight(card, d.height);
|
|
349
433
|
}
|
|
@@ -377,6 +461,7 @@
|
|
|
377
461
|
|
|
378
462
|
return {
|
|
379
463
|
init: init, availability: availability, optedIn: optedIn, setOptIn: setOptIn, never: never, setNever: setNever, onChange: onChange, listeners: listeners, onStorage: onStorage,
|
|
464
|
+
errors: errors, onErrors: onErrors, ERRORS: ERRORS, pick: pick, picking: picking, onPick: onPick, PICK: PICK,
|
|
380
465
|
start: start, startCount: startCount, stop: stop, stopAll: stopAll, stopIn: stopIn, running: running, liveCount: liveCount, liveFrame: liveFrame,
|
|
381
466
|
mountPreview: mountPreview, onMessage: onMessage, BANNER: BANNER, LIMITS: LIMITS,
|
|
382
467
|
/** The banner every running preview carries, in the page's language (#1554); BANNER is its English. */
|