loopengine 0.1.50 → 0.1.52
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/dist/web/playground.js +95 -6
- package/package.json +1 -1
package/dist/web/playground.js
CHANGED
|
@@ -200,7 +200,14 @@ export const playgroundHtml = `<!doctype html>
|
|
|
200
200
|
image's real visible edge instead of floating in empty space beside
|
|
201
201
|
a narrower image inside a full-width box. */
|
|
202
202
|
.msg-preview-wrap { position: relative; display: inline-block; max-width: 100%; margin: 6px 0; }
|
|
203
|
-
.msg-
|
|
203
|
+
/* .msg-plain-img (no download link — see renderMessageMarkdown's own
|
|
204
|
+
doc comment) gets the same capped size and click-to-zoom as the
|
|
205
|
+
merged .msg-preview-img, just without the corner download icon and
|
|
206
|
+
without a wrap element of its own (nothing needs to be positioned
|
|
207
|
+
relative to it) — see the chatPane click handler below and
|
|
208
|
+
openImageLightbox's own handling of a missing downloadHref. */
|
|
209
|
+
.msg-preview-img, .msg-plain-img { display: block; max-width: 100%; max-height: 260px; width: auto; height: auto; border-radius: 8px; cursor: zoom-in; }
|
|
210
|
+
.msg-plain-img { margin: 6px 0; }
|
|
204
211
|
.msg-download-icon {
|
|
205
212
|
position: absolute;
|
|
206
213
|
top: 8px;
|
|
@@ -405,6 +412,42 @@ export const playgroundHtml = `<!doctype html>
|
|
|
405
412
|
// target for one that happens to reuse an id namespace.
|
|
406
413
|
var toolCallCardsById = {};
|
|
407
414
|
|
|
415
|
+
// Keyed by a signed URL's own base (everything before its "?") —
|
|
416
|
+
// populated from every 'toollane:result' event's own 'summary' (see
|
|
417
|
+
// core/loop-events.ts's own ToolLaneResultEvent doc comment: the raw,
|
|
418
|
+
// unparsed tool return value, straight from execute() with nothing
|
|
419
|
+
// reshaped in between). A long signed-URL query string (a GCS
|
|
420
|
+
// Signature param can run ~300 opaque base64 characters) is exactly
|
|
421
|
+
// the kind of thing an LLM can flip one character of while generating
|
|
422
|
+
// its own reply text from the same value earlier in its context — see
|
|
423
|
+
// correctKnownUrls below, which snaps a written URL back to this
|
|
424
|
+
// known-good one whenever the base (the object path — descriptive,
|
|
425
|
+
// not random, so far less likely to get mistyped) matches but the
|
|
426
|
+
// full string doesn't. Cleared on every conversation switch, same as
|
|
427
|
+
// toolCallCardsById above — a base from an unrelated earlier session
|
|
428
|
+
// is never a valid correction target (and its signed URL has likely
|
|
429
|
+
// expired anyway).
|
|
430
|
+
var knownUrlsByBase = {};
|
|
431
|
+
|
|
432
|
+
// Recursively walks an arbitrary JSON value (a tool's own summary,
|
|
433
|
+
// parsed) collecting every string that looks like a signed URL — has
|
|
434
|
+
// a "?" (nothing to correct in a bare URL with no query to corrupt).
|
|
435
|
+
// Last one seen for a given base wins, which is fine: a re-signed URL
|
|
436
|
+
// for the same object is just as valid a correction target as the
|
|
437
|
+
// first one seen.
|
|
438
|
+
function recordKnownUrls(value) {
|
|
439
|
+
if (typeof value === 'string') {
|
|
440
|
+
if (value.indexOf('https://') === 0 || value.indexOf('http://') === 0) {
|
|
441
|
+
var qIndex = value.indexOf('?');
|
|
442
|
+
if (qIndex !== -1) knownUrlsByBase[value.slice(0, qIndex)] = value;
|
|
443
|
+
}
|
|
444
|
+
} else if (Array.isArray(value)) {
|
|
445
|
+
for (var i = 0; i < value.length; i++) recordKnownUrls(value[i]);
|
|
446
|
+
} else if (value && typeof value === 'object') {
|
|
447
|
+
for (var key in value) recordKnownUrls(value[key]);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
408
451
|
// A page refresh only ever loses *this tab's* JS state — the actual
|
|
409
452
|
// conversation is durably stored server-side (see session-store.ts).
|
|
410
453
|
// What's missing after a refresh is just "which session ids did I
|
|
@@ -456,8 +499,30 @@ export const playgroundHtml = `<!doctype html>
|
|
|
456
499
|
// attachment — a real HTTP response header, so a plain click already
|
|
457
500
|
// downloads correctly regardless of origin, no JS fetch/blob trick or
|
|
458
501
|
// same-origin requirement needed.
|
|
502
|
+
//
|
|
503
|
+
// Before any of that: correctKnownUrls (see knownUrlsByBase's own doc
|
|
504
|
+
// comment) snaps any signed URL the model wrote back to the exact
|
|
505
|
+
// known-good one whenever they share a base but differ — recovers a
|
|
506
|
+
// broken image/download link from a transcription slip without
|
|
507
|
+
// needing the model to get a ~300-character signature exactly right.
|
|
508
|
+
//
|
|
509
|
+
// Built from a runtime backslash character, never a literal
|
|
510
|
+
// backslash-letter sequence in this file's own source — same gotcha
|
|
511
|
+
// renderMessageMarkdown's own patterns below document in full (this
|
|
512
|
+
// page is one big outer template literal whose own output is itself
|
|
513
|
+
// parsed as JS a second time).
|
|
514
|
+
var urlBs = String.fromCharCode(92);
|
|
515
|
+
var urlPattern = new RegExp('https?:' + urlBs + '/' + urlBs + '/[^' + urlBs + 's)' + urlBs + ']]+', 'g');
|
|
516
|
+
function correctKnownUrls(md) {
|
|
517
|
+
return md.replace(urlPattern, function (url) {
|
|
518
|
+
var qIndex = url.indexOf('?');
|
|
519
|
+
if (qIndex === -1) return url;
|
|
520
|
+
var known = knownUrlsByBase[url.slice(0, qIndex)];
|
|
521
|
+
return known || url;
|
|
522
|
+
});
|
|
523
|
+
}
|
|
459
524
|
function renderMessageMarkdown(md) {
|
|
460
|
-
var lines = escapeHtml(md).split('\\n');
|
|
525
|
+
var lines = escapeHtml(correctKnownUrls(md)).split('\\n');
|
|
461
526
|
var html = '';
|
|
462
527
|
var inCode = false;
|
|
463
528
|
var codeBuffer = '';
|
|
@@ -488,7 +553,7 @@ export const playgroundHtml = `<!doctype html>
|
|
|
488
553
|
.replace(inlineCodePattern, '<code>$1</code>')
|
|
489
554
|
.replace(boldPattern, '<strong>$1</strong>')
|
|
490
555
|
.replace(italicPattern, '<em>$1</em>')
|
|
491
|
-
.replace(imagePattern, '<img src="$2" alt="$1" class="msg-plain-img">')
|
|
556
|
+
.replace(imagePattern, '<img src="$2" alt="$1" class="msg-plain-img" data-full-src="$2">')
|
|
492
557
|
.replace(linkPattern, '<a href="$2" target="_blank" rel="noopener noreferrer">$1</a>');
|
|
493
558
|
}
|
|
494
559
|
function flushParagraph() {
|
|
@@ -571,7 +636,7 @@ export const playgroundHtml = `<!doctype html>
|
|
|
571
636
|
// isn't a recognized string escape character either. A plain string
|
|
572
637
|
// needs no such escaping for "/" at all.
|
|
573
638
|
var previewMergePattern = new RegExp(
|
|
574
|
-
'<img src="([^"]*)" alt="([^"]*)" class="msg-plain-img">' + bs + 's*<a href="([^"]*)"[^>]*>[^<]*</a>',
|
|
639
|
+
'<img src="([^"]*)" alt="([^"]*)" class="msg-plain-img" data-full-src="[^"]*">' + bs + 's*<a href="([^"]*)"[^>]*>[^<]*</a>',
|
|
575
640
|
'g',
|
|
576
641
|
);
|
|
577
642
|
html = html.replace(previewMergePattern, function (match, src, alt, href) {
|
|
@@ -591,7 +656,16 @@ export const playgroundHtml = `<!doctype html>
|
|
|
591
656
|
|
|
592
657
|
function openImageLightbox(src, downloadHref) {
|
|
593
658
|
lightboxImage.src = src;
|
|
594
|
-
|
|
659
|
+
// A plain image (no download link — see renderMessageMarkdown's own
|
|
660
|
+
// doc comment on .msg-plain-img) has no downloadHref at all; hiding
|
|
661
|
+
// the button beats pointing it at '' (a dead link that would just
|
|
662
|
+
// reload the page) or at src (opens inline, not an actual download).
|
|
663
|
+
if (downloadHref) {
|
|
664
|
+
lightboxDownload.href = downloadHref;
|
|
665
|
+
lightboxDownload.style.display = '';
|
|
666
|
+
} else {
|
|
667
|
+
lightboxDownload.style.display = 'none';
|
|
668
|
+
}
|
|
595
669
|
lightboxOverlay.classList.add('open');
|
|
596
670
|
}
|
|
597
671
|
function closeImageLightbox() {
|
|
@@ -616,7 +690,7 @@ export const playgroundHtml = `<!doctype html>
|
|
|
616
690
|
// rewired after each new message.
|
|
617
691
|
chatPane.addEventListener('click', function (ev) {
|
|
618
692
|
var target = ev.target;
|
|
619
|
-
if (target && target.classList && target.classList.contains('msg-preview-img')) {
|
|
693
|
+
if (target && target.classList && (target.classList.contains('msg-preview-img') || target.classList.contains('msg-plain-img'))) {
|
|
620
694
|
openImageLightbox(target.getAttribute('data-full-src'), target.getAttribute('data-download-href'));
|
|
621
695
|
}
|
|
622
696
|
});
|
|
@@ -1168,6 +1242,7 @@ export const playgroundHtml = `<!doctype html>
|
|
|
1168
1242
|
sessionLabel.classList.add('copyable');
|
|
1169
1243
|
chatPane.textContent = '';
|
|
1170
1244
|
toolCallCardsById = {};
|
|
1245
|
+
knownUrlsByBase = {};
|
|
1171
1246
|
setEmptyHint(timelinePane, 'Loop events (tool calls, permission checks, budget checks) will appear here as the agent runs.');
|
|
1172
1247
|
renderSessionsPane();
|
|
1173
1248
|
|
|
@@ -1318,6 +1393,7 @@ export const playgroundHtml = `<!doctype html>
|
|
|
1318
1393
|
function resetConversation() {
|
|
1319
1394
|
sessionId = null;
|
|
1320
1395
|
toolCallCardsById = {};
|
|
1396
|
+
knownUrlsByBase = {};
|
|
1321
1397
|
removeThinking();
|
|
1322
1398
|
sessionLabel.textContent = 'session: (new)';
|
|
1323
1399
|
sessionLabel.classList.remove('copyable');
|
|
@@ -1451,6 +1527,19 @@ export const playgroundHtml = `<!doctype html>
|
|
|
1451
1527
|
removeThinking();
|
|
1452
1528
|
appendQuestionCard(data);
|
|
1453
1529
|
appendTimelineEntry(eventName, data);
|
|
1530
|
+
} else if (eventName === 'toollane:result') {
|
|
1531
|
+
// data.summary is a tool's raw, unreshaped return value (see
|
|
1532
|
+
// knownUrlsByBase's own doc comment above) — a JSON string for
|
|
1533
|
+
// this ability, but tools generally can return plain text too, so
|
|
1534
|
+
// a parse failure here just means nothing to record, not an error.
|
|
1535
|
+
if (typeof data.summary === 'string') {
|
|
1536
|
+
try {
|
|
1537
|
+
recordKnownUrls(JSON.parse(data.summary));
|
|
1538
|
+
} catch (err) {
|
|
1539
|
+
// Not JSON — nothing to correct against.
|
|
1540
|
+
}
|
|
1541
|
+
}
|
|
1542
|
+
appendTimelineEntry(eventName, data);
|
|
1454
1543
|
} else {
|
|
1455
1544
|
appendTimelineEntry(eventName, data);
|
|
1456
1545
|
}
|