bunnyquery 1.8.2 → 1.8.4

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.
@@ -151,10 +151,51 @@ export async function fillHistoryViewport(opts: FillHistoryViewportOptions): Pro
151
151
  * come true are dropped as it goes, so the cost stays flat.
152
152
  */
153
153
  export function createHistoryFiller(
154
- base: Omit<FillHistoryViewportOptions, 'isSatisfied'>,
154
+ base: Omit<FillHistoryViewportOptions, 'isSatisfied'> & {
155
+ /** Fired when the loop starts FETCHING and when it stops, and only on a real
156
+ * change.
157
+ *
158
+ * This — not the caller's own per-request `isLoading` — is what "older
159
+ * history is still coming in" means to a view. A fill is many pages, and
160
+ * `isLoading` drops to false between every one of them, so anything
161
+ * rendered off it flickers once per page for the whole loop. A collapsed
162
+ * indexing row whose run begins above the loaded window renders exactly
163
+ * that ("still loading this run" vs a status it cannot know yet), which is
164
+ * why the loop has to publish its own span.
165
+ *
166
+ * Fetching, NOT requested. Most fills fetch nothing: they are fired on every
167
+ * window resize, every row a user collapses, and every first-page load, and
168
+ * the overwhelmingly common outcome is `isSatisfied` returning true on the
169
+ * first look. Announcing at request time published a true/false pair for
170
+ * each of those, and the widget's own satisfied-check spans two animation
171
+ * frames — long enough for the browser to PAINT the intermediate state. Every
172
+ * collapsed row strobed through "loading" on every resize tick. So the span
173
+ * opens at the first actual page request, which is also the first moment the
174
+ * claim is true. */
175
+ onRunningChange?: (running: boolean) => void;
176
+ },
155
177
  ): { fill: (isSatisfied: () => boolean | Promise<boolean>) => Promise<void>; isRunning: () => boolean } {
156
178
  var pending: Array<() => boolean | Promise<boolean>> = [];
179
+ // One loop at a time. Set the instant a fill is REQUESTED, whether or not that
180
+ // fill goes on to fetch anything.
157
181
  var running = false;
182
+ // What the view is told: a page is actually being fetched. Deliberately a
183
+ // different fact from `running` — see onRunningChange.
184
+ var fetching = false;
185
+
186
+ // The callback can NEVER break the loop, and the swallow is the point. It is a
187
+ // view render (the widget rebuilds its whole message list from here), so it is
188
+ // the least trustworthy code this module touches, and it is invoked from two
189
+ // places that must both survive it: inside the fetch wrapper, and inside `done`
190
+ // — where a throw would reject the promise `fill()` returns and leave the
191
+ // closing edge unsent, pinning every row that renders off this at "loading"
192
+ // forever. A view that cannot paint is the view's problem, not the pager's.
193
+ function announce(next: boolean): void {
194
+ if (fetching === next) return;
195
+ fetching = next;
196
+ if (!base.onRunningChange) return;
197
+ try { base.onRunningChange(next); } catch (e) { /* never break the pager */ }
198
+ }
158
199
 
159
200
  async function allSatisfied(): Promise<boolean> {
160
201
  var next: Array<() => boolean | Promise<boolean>> = [];
@@ -166,18 +207,24 @@ export function createHistoryFiller(
166
207
  }
167
208
 
168
209
  return {
169
- isRunning: function () { return running; },
210
+ // The published fact, so a view and `isRunning()` can never disagree about
211
+ // what they are showing. A fill that never fetches is not something anyone
212
+ // outside this module has any use for knowing about.
213
+ isRunning: function () { return fetching; },
170
214
  fill: function (isSatisfied) {
171
215
  pending.push(isSatisfied);
172
216
  if (running) return Promise.resolve();
173
217
  running = true;
174
- var done = function () { running = false; pending = []; };
218
+ var done = function () { pending = []; running = false; announce(false); };
175
219
  return fillHistoryViewport({
176
220
  isSatisfied: allSatisfied,
177
221
  isEndOfList: base.isEndOfList,
178
222
  isLoading: base.isLoading,
179
223
  messageCount: base.messageCount,
180
- fetchOlder: base.fetchOlder,
224
+ // The span opens HERE, at the first real page request: past
225
+ // isEndOfList, past isStale, past isSatisfied. Everything before this
226
+ // point is a fill that concluded there was nothing to do.
227
+ fetchOlder: function () { announce(true); return base.fetchOlder(); },
181
228
  isStale: base.isStale,
182
229
  maxPages: base.maxPages,
183
230
  }).then(done, done);
package/styles/chat.css CHANGED
@@ -123,7 +123,17 @@
123
123
  border: 1px solid var(--bq-line);
124
124
  background: rgba(127, 127, 127, 0.05);
125
125
  }
126
+ /* Three states, three colours, and the row is readable at a glance without
127
+ opening it:
128
+ yellow working on it now (.is-active)
129
+ green indexed, confirmed (.is-indexed)
130
+ grey not known yet (.is-resolving, and the base look)
131
+ Grey is the DEFAULT rather than a colour of its own, so anything that has not
132
+ earned green or yellow reads as "no claim" — which is also what a cancelled
133
+ row should look like, and what an unrecognised future state would fall back
134
+ to. Red stays reserved for a failure. */
126
135
  .bq-index-group.is-active { border-color: var(--bq-warning-border); background: var(--bq-warning-bg); }
136
+ .bq-index-group.is-indexed { border-color: var(--bq-success-border); background: var(--bq-success-bg); }
127
137
  .bq-index-group.is-error { border-color: var(--bq-danger); background: var(--bq-danger-bg); }
128
138
 
129
139
  .bq-index-head {
@@ -146,7 +156,12 @@
146
156
  }
147
157
  .bq-index-head:hover { background: rgba(127, 127, 127, 0.08); color: var(--bq-muted); }
148
158
  .bq-index-group.is-active .bq-index-head { color: var(--bq-warning); }
159
+ .bq-index-group.is-indexed .bq-index-head { color: var(--bq-success); }
149
160
  .bq-index-group.is-error .bq-index-head { color: var(--bq-danger); }
161
+ /* Stated rather than inherited: a waiting row keeping the muted default is a
162
+ DECISION (it is not claiming a state), not an oversight, and writing it here
163
+ means adding a colour to the base rule cannot silently give it one. */
164
+ .bq-index-group.is-resolving .bq-index-head { color: var(--bq-muted); }
150
165
 
151
166
  /* Status glyph. The active one is a circular-arrow SVG the consumer inlines
152
167
  (never an icon font: agent.vue's Material Symbols build gates glyphs behind
@@ -162,8 +177,17 @@
162
177
  .bq-index-icon svg { width: 100%; height: 100%; display: block; }
163
178
  .bq-index-group.is-active .bq-index-icon svg { animation: bq-index-spin 1.1s linear infinite; }
164
179
  @keyframes bq-index-spin { to { transform: rotate(360deg); } }
180
+ /* Waiting to find out (a run whose earlier passes are still being paged in, a
181
+ file whose queue state has not been answered). Deliberately NOT the active
182
+ look: no warning colour and no spin, because a spinning glyph on a chat row is
183
+ read as "this file is being worked on right now" and that is the one thing this
184
+ state does not know. A slow fade says "pending" without claiming progress. */
185
+ .bq-index-group.is-resolving .bq-index-icon svg { animation: bq-index-fade 1.6s ease-in-out infinite; }
186
+ @keyframes bq-index-fade { 0%, 100% { opacity: 1; } 50% { opacity: 0.35; } }
165
187
  @media (prefers-reduced-motion: reduce) {
166
188
  .bq-index-group.is-active .bq-index-icon svg { animation-duration: 3s; }
189
+ /* A fade has no end state to settle on, so it stops rather than slows. */
190
+ .bq-index-group.is-resolving .bq-index-icon svg { animation: none; opacity: 0.7; }
167
191
  }
168
192
 
169
193
  .bq-index-label {
@@ -222,8 +246,28 @@
222
246
  border-left: 2px solid var(--bq-line);
223
247
  }
224
248
  .bq-message.bq-index-pass .bq-bubble { font-size: 0.78rem; }
225
- /* Close the run: the next ordinary message needs its normal breathing room. */
226
- .bq-message.bq-index-pass + :not(.bq-index-pass) { margin-top: 0.55rem; }
249
+ /* Close the run: the next ordinary message needs its normal breathing room.
250
+ NOT scoped to .bq-message: the trailing loader below is a run member too, and
251
+ requiring .bq-message would silently drop the gap whenever it is the last one. */
252
+ .bq-index-pass + :not(.bq-index-pass) { margin-top: 0.55rem; }
253
+ /* Trailing loader: an open row keeps a "still working" mark of its own for as long
254
+ as the file's indexing is not confirmed over. It sits on the same rail as the
255
+ passes so it reads as the run continuing rather than as a message of its own —
256
+ which is why the rail is repeated here instead of reusing the rule above, whose
257
+ selector requires .bq-message. */
258
+ .bq-index-tail {
259
+ margin-bottom: 0.3rem;
260
+ margin-left: 0.7rem;
261
+ padding: 0.1rem 0 0.1rem 0.7rem;
262
+ border-left: 2px solid var(--bq-line);
263
+ font-size: 0.78rem;
264
+ color: var(--bq-muted);
265
+ }
266
+ /* An open row animates for as long as the file is indexing, which can be minutes.
267
+ Readers who ask for less motion get the static end state. */
268
+ @media (prefers-reduced-motion: reduce) {
269
+ .bq-index-tail .bq-loader::after { animation: none; content: '...'; }
270
+ }
227
271
  .bq-index-note {
228
272
  padding: 0 0.6rem 0.45rem;
229
273
  font-size: 0.7rem;
@@ -257,6 +301,68 @@
257
301
  /* While the temporary URL is being re-resolved the click is swallowed — show a
258
302
  busy cursor and dim it so it reads as "working, don't click again". */
259
303
  .bq-link-button.is-refreshing { cursor: progress; opacity: 0.6; }
304
+ /* The client asked for a url for this file and did not get one, so the chip is
305
+ emitted with NO href (it cannot navigate) and with ✕ in place of ↗. Paint it
306
+ as the dead text it now is: muted, no underline, default cursor, and no hover
307
+ response, since the base rule above still fires on hover and would otherwise
308
+ promising a link. */
309
+ .bq-link-button.is-unavailable,
310
+ .bq-link-button.is-unavailable:hover {
311
+ color: var(--bq-muted);
312
+ text-decoration: none;
313
+ cursor: default;
314
+ }
315
+
316
+ /* ---- inline image previews ----------------------------------------------*/
317
+ /* The anchor IS the preview: picture on top, the ordinary chip underneath as a
318
+ caption. The caption is the FALLBACK, not decoration: when the url dies or the
319
+ file is gone the <img> hides itself and what is left is exactly the text chip
320
+ this feature replaced, still clickable. */
321
+ .bq-link-button.is-image-preview {
322
+ display: inline-block;
323
+ max-width: 100%;
324
+ white-space: normal;
325
+ overflow: visible;
326
+ text-overflow: clip;
327
+ vertical-align: top;
328
+ margin: 0.25em 0;
329
+ }
330
+ /* Qualified with .bq-md because `.bq-md img` below already sets max-width and
331
+ would otherwise out-specify a bare .bq-img-preview. */
332
+ .bq-md img.bq-img-preview {
333
+ display: block;
334
+ /* Bounded on BOTH axes so a 12000px panorama and a 40x9000 sliver each land
335
+ inside the bubble; width/height auto keep the aspect ratio. The bubble cap
336
+ differs per client, so the preview carries its own. */
337
+ max-width: min(100%, 320px);
338
+ max-height: 320px;
339
+ width: auto;
340
+ height: auto;
341
+ border-radius: 4px;
342
+ border: 1px solid var(--bq-line);
343
+ /* A transparent PNG is invisible on paper without this. */
344
+ background: rgba(127, 127, 127, 0.08);
345
+ }
346
+ /* Keyed on the MISSING src, not on a state class: the element is emitted with no
347
+ src at all (the url has to be minted first) and a src-less <img> would
348
+ otherwise paint the browser's broken-image glyph. */
349
+ .bq-md img.bq-img-preview:not([src]),
350
+ .bq-md img.bq-img-preview[data-bq-img-state="error"] { display: none; }
351
+ /* The dot trail covers the mint round trip, then gets out of the way. */
352
+ .bq-md img.bq-img-preview[src] ~ [data-bq-img-loader],
353
+ .bq-md img.bq-img-preview[data-bq-img-state="error"] ~ [data-bq-img-loader] { display: none; }
354
+ .bq-img-preview-caption {
355
+ display: inline-block;
356
+ max-width: 100%;
357
+ overflow: hidden;
358
+ text-overflow: ellipsis;
359
+ white-space: nowrap;
360
+ vertical-align: bottom;
361
+ }
362
+ /* A collapsed indexing row is a single line and its label is the indexed file's
363
+ own path, which is frequently an image. Never grow that row into a picture. */
364
+ .bq-index-label .bq-md img.bq-img-preview,
365
+ .bq-index-label [data-bq-img-loader] { display: none; }
260
366
 
261
367
  /* ---- rendered-markdown body ----------------------------------------------*/
262
368
  /* Overrides the bubble's `white-space: pre-wrap` since marked already produces