bunnyquery 1.10.0 → 1.10.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bunnyquery",
3
- "version": "1.10.0",
3
+ "version": "1.10.2",
4
4
  "description": "Embeddable BunnyQuery AI chat widget + its framework-agnostic chat engine",
5
5
  "main": "bunnyquery.js",
6
6
  "exports": {
@@ -36,6 +36,17 @@
36
36
  * loaded (`mayHaveOlder`), and it is the same event that already re-derives
37
37
  * `runKey` — so the view treats it as a new row either way.
38
38
  *
39
+ * One row per FILE, not per run. A file indexed and later indexed again (a
40
+ * re-upload, an explicit Reindex, a retry after a failure) has several RUNS in
41
+ * the transcript, each opened by its own "A new file has just been uploaded"
42
+ * pass. They are attributed as separate groups (mixing their passes would let an
43
+ * old failure be painted over by a new success, or a new run inherit an old
44
+ * stop), but only the NEWEST run of a file is rendered once the older ones have
45
+ * settled. Two green "Indexed" rows for one file read as a duplicate, not as
46
+ * history, and a superseded run's verdict is not the file's state. An older run
47
+ * that is still working stays visible until it settles, so a double dispatch is
48
+ * never hidden while both chains are live and its Stop button stays reachable.
49
+ *
39
50
  * The group deliberately reports no authoritative pass TOTAL. History is paged
40
51
  * newest-first, so any total computed from loaded messages is a lower bound that
41
52
  * a later scroll-up would contradict. It reports STATE (indexing / indexed /
@@ -75,12 +86,13 @@ export type IndexingGroup = {
75
86
  * surprised out of. */
76
87
  key: string;
77
88
  /** Identity of this ROW: one indexing RUN of that file. A file indexed on
78
- * Monday and re-indexed on Wednesday is two runs, and collapsing them into
79
- * one row erased Monday's from Monday's place in the conversation, claimed
80
- * its passes for Wednesday, and let Monday's failure be overwritten by
81
- * Wednesday's success. Named after the run's FIRST loaded pass (see where it
82
- * is assigned below), so passes appended to the run and other runs appearing
83
- * on either side of it never rename a row already on screen.
89
+ * Monday and re-indexed on Wednesday is two runs, and merging them into one
90
+ * group claimed Monday's passes for Wednesday and let Monday's failure be
91
+ * overwritten by Wednesday's success. They stay separate groups; what the
92
+ * view gets is only the newest of them once the rest have settled (see the
93
+ * file docstring). Named after the run's FIRST loaded pass (see where it is
94
+ * assigned below), so passes appended to the run and other runs appearing on
95
+ * either side of it never rename a row already on screen.
84
96
  *
85
97
  * This is the RENDER key, and only that. It is renamed when the run's true
86
98
  * first pass finally loads — routine while a worker-driven chain is running,
@@ -395,6 +407,48 @@ function isHiddenPass(m: ChatMessage): boolean {
395
407
  * Messages that are not background-indexing pass through untouched, at their
396
408
  * original positions and with their original indices.
397
409
  */
410
+ /**
411
+ * A loaded group reads its state off its newest loaded pass, and for a run the WORKER
412
+ * ended (three failed retries, a stale grant, a cap-out) that pass looks settled: the
413
+ * model answered normally, the failure happened after. The run:: record is the only
414
+ * thing that knows, and until now it was consulted for record-only stubs alone, so
415
+ * the chat row said "Indexed", the files page took that as a verdict and painted the
416
+ * badge green over the record's "error". The record's terminal verdict is laid over
417
+ * the group when it describes THIS run (started no later than the newest loaded pass)
418
+ * and is newer than that pass; visible work and a live queue hit still outrank it.
419
+ */
420
+ function overlayRunRecordVerdict(
421
+ grp: IndexingGroup,
422
+ rec: RunStubInfo,
423
+ liveIndexKeys: { [key: string]: boolean },
424
+ ): void {
425
+ if (!grp.members.length || grp.status === 'active' || grp.cancelling) return;
426
+ if (rec.status === 'working' || typeof rec.started !== 'number') return;
427
+ if (liveIndexKeys[grp.key] || liveIndexKeys[canonIndexKey(grp.key)]) return;
428
+ var firstTs = grp.members[0].msg._ts;
429
+ var lastTs = grp.members[grp.members.length - 1].msg._ts;
430
+ if (typeof lastTs !== 'number' || typeof firstTs !== 'number') return;
431
+ // a record that started after the newest loaded pass is a LATER run, not this one
432
+ if (rec.started > lastTs) return;
433
+ var when = typeof rec.finished === 'number' ? rec.finished : undefined;
434
+ if (when === undefined) return;
435
+ // a record that ENDED before this run began describes a previous run
436
+ if (when < firstTs) return;
437
+ // the record's verdict must be at least as new as the pass it would override
438
+ // (a few seconds of slack: the worker closes the record right after the pass lands)
439
+ if (when < lastTs - 5000) return;
440
+ if (rec.status === 'error' || rec.status === 'cancelled') {
441
+ grp.status = rec.status;
442
+ } else if (rec.status === 'done') {
443
+ grp.status = 'done';
444
+ } else {
445
+ return;
446
+ }
447
+ grp.finished = true;
448
+ grp.resolving = false;
449
+ grp.resolvingReason = undefined;
450
+ }
451
+
398
452
  export function buildChatDisplayList(
399
453
  messages: ChatMessage[],
400
454
  opts?: BuildDisplayListOptions,
@@ -812,6 +866,28 @@ export function buildChatDisplayList(
812
866
  }
813
867
  }
814
868
 
869
+ // --- superseded runs -------------------------------------------------------
870
+ // Every run of a file but its newest is history the newest run replaced: the
871
+ // file's state is the newest run's, and a second "Indexed" row for the same
872
+ // file reads as a duplicate (reported as exactly that, for files re-indexed
873
+ // during testing). Such a run keeps its group (its passes stay attributed to
874
+ // it, never demoted to loose "Indexing: ..." bubbles) but draws no row.
875
+ //
876
+ // ONLY once it has settled. An older run that is still working, or still
877
+ // stopping, is live work the user can see the effects of and may want to stop,
878
+ // and hiding it would also hide a double dispatch (two chains reading one file)
879
+ // for as long as both ran. Runs open in walk order, and the walk is request
880
+ // creation order, so the last run of a key is its newest.
881
+ var superseded: { [runId: string]: boolean } = {};
882
+ for (var sk in runsOfKey) {
883
+ var srs = runsOfKey[sk];
884
+ for (var sri = 0; sri < srs.length - 1; sri++) {
885
+ var sgp = groups[srs[sri]];
886
+ if (!sgp || sgp.status === 'active' || sgp.cancelling) continue;
887
+ superseded[srs[sri]] = true;
888
+ }
889
+ }
890
+
815
891
  // --- durable run:: stubs ---------------------------------------------------
816
892
  // A run record whose passes are not among the loaded messages still deserves
817
893
  // a row: on a fresh open the bg history is deferred, and a run older than the
@@ -991,6 +1067,7 @@ export function buildChatDisplayList(
991
1067
  // record-only to loaded-passes keeps its DOM node and its position.
992
1068
  tg.runKey = 'run:' + (tg.path || tg.key) + '#' + trec.started;
993
1069
  stubList.push({ started: trec.started, group: tg });
1070
+ overlayRunRecordVerdict(tg, trec, liveIndexKeys);
994
1071
  }
995
1072
  }
996
1073
  stubList.sort(function (a, b) { return a.started - b.started; });
@@ -1015,7 +1092,9 @@ export function buildChatDisplayList(
1015
1092
  // Every other member of the run is represented by the row at the anchor —
1016
1093
  // unless the row is timestamp-anchored (incomplete run with a run::
1017
1094
  // record), in which case the flush above already emitted it.
1018
- if (groups[r].anchorIndex === j && !suppressAnchor[r]) {
1095
+ // ...or the run is superseded, in which case nothing is emitted for it at
1096
+ // all: its members are represented by the newer run's row.
1097
+ if (groups[r].anchorIndex === j && !suppressAnchor[r] && !superseded[r]) {
1019
1098
  out.push({ kind: 'indexing', group: groups[r], index: j });
1020
1099
  }
1021
1100
  }
@@ -77,8 +77,15 @@ export function renderInlineLinkHtml(link: RenderableInlineLink, opts?: InlineLi
77
77
  if (unavailable) cls.push('is-unavailable');
78
78
  if (preview) cls.push('is-image-preview');
79
79
 
80
- var labelText = (unavailable ? INLINE_LINK_UNAVAILABLE_GLYPH : INLINE_LINK_GLYPH) + ' ' + link.label
81
- + (unavailable ? INLINE_LINK_UNAVAILABLE_SUFFIX : refreshing ? ' (fetching...)' : '');
80
+ // The glyph is its own element so chat.css can make it an atomic inline: an
81
+ // underline propagates into every in-flow inline descendant of the anchor and
82
+ // nothing inside can cancel it, so the ↗ was underlined along with the label
83
+ // whenever the chip was. The spacing that used to be a literal space is a
84
+ // margin on the span for the same reason (a space is decorated too).
85
+ var glyphHtml = '<span class="bq-link-glyph" translate="no">'
86
+ + (unavailable ? INLINE_LINK_UNAVAILABLE_GLYPH : INLINE_LINK_GLYPH) + '</span>';
87
+ var labelHtml = glyphHtml + escapeInlineHtml(link.label
88
+ + (unavailable ? INLINE_LINK_UNAVAILABLE_SUFFIX : refreshing ? ' (fetching...)' : ''));
82
89
  var attrs = ['class="' + cls.join(' ') + '"'];
83
90
  // NO href when the file is unavailable. That is what disables the click:
84
91
  // an anchor with no href does not navigate, is not a tab stop and takes the
@@ -99,7 +106,7 @@ export function renderInlineLinkHtml(link: RenderableInlineLink, opts?: InlineLi
99
106
  if (link.remotePath) attrs.push('data-bq-remote-path="' + escapeInlineHtml(link.remotePath) + '"');
100
107
  if (link.fullLabel) attrs.push('data-bq-full-label="' + escapeInlineHtml(link.fullLabel) + '"');
101
108
 
102
- if (!preview) return '<a ' + attrs.join(' ') + '>' + escapeInlineHtml(labelText) + '</a>';
109
+ if (!preview) return '<a ' + attrs.join(' ') + '>' + labelHtml + '</a>';
103
110
 
104
111
  // NO src attribute. A stored file always classifies as the _expired_.url
105
112
  // placeholder until something mints a real url, so a src written here would be
@@ -127,6 +134,6 @@ export function renderInlineLinkHtml(link: RenderableInlineLink, opts?: InlineLi
127
134
  // dot trail, never the jumping bunny. CSS hides it the moment the <img>
128
135
  // gains a src, so no JS removes DOM and nothing re-parses.
129
136
  '<span class="bq-loader" data-bq-img-loader="1"></span>' +
130
- '<span class="bq-img-preview-caption" translate="no">' + escapeInlineHtml(labelText) + '</span>' +
137
+ '<span class="bq-img-preview-caption" translate="no">' + labelHtml + '</span>' +
131
138
  '</a>';
132
139
  }
package/styles/chat.css CHANGED
@@ -419,6 +419,26 @@
419
419
  text-decoration: none;
420
420
  cursor: default;
421
421
  }
422
+ /* An assistant bubble's chips are emitted by renderInlineLinkHtml INSIDE the
423
+ v-html `.bq-md` block, where `.bq-md a` below (underline at rest, none on
424
+ hover -- the prose-link convention) is (0,1,1) and outranks .bq-link-button
425
+ at (0,1,0). So those chips rendered the exact inverse of the rule above:
426
+ underlined at rest, underline gone under the pointer, while the same chip on
427
+ a user bubble (outside .bq-md) behaved. Restate the chip rule at a
428
+ specificity that wins inside .bq-md, the dead chip included. */
429
+ .bq-md a.bq-link-button { text-decoration: none; }
430
+ .bq-md a.bq-link-button:hover { text-decoration: underline; }
431
+ .bq-md a.bq-link-button.is-unavailable:hover { text-decoration: none; }
432
+ /* The ↗ / ✕ glyph is never underlined, at rest or on hover. Underlines propagate
433
+ into every in-flow inline descendant and a descendant cannot cancel them, so
434
+ `text-decoration: none` on the glyph alone does nothing; an atomic inline
435
+ (inline-block) is exempt, and the spacing is a margin so no decorated space
436
+ character carries the line up to the glyph. */
437
+ .bq-link-glyph {
438
+ display: inline-block;
439
+ text-decoration: none;
440
+ margin-right: 0.3em;
441
+ }
422
442
 
423
443
  /* ---- inline image previews ----------------------------------------------*/
424
444
  /* The anchor IS the preview: picture on top, the ordinary chip underneath as a