bunnyquery 1.8.3 → 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.
package/README.md CHANGED
@@ -1,28 +1,28 @@
1
1
  # BunnyQuery
2
2
 
3
3
  An embeddable, dependency-free AI chat widget for [Skapi](https://www.skapi.com)-powered
4
- projects. Drop it into any web page and your users get a full chat experience
4
+ projects. Drop it into any web page and your users get a full chat experience:
5
5
  account login/signup, conversation history, file & folder uploads, and a settings
6
- panel all talking to your project's **BunnyQuery** AI agent.
6
+ panel, all talking to your project's **BunnyQuery** AI agent.
7
7
 
8
8
  BunnyQuery is a standalone vanilla-JS port of the BunnyQuery (www.bunnyquery.com) agent
9
9
  chatbox. The **widget** ships as a single IIFE that exposes `window.BunnyQuery` plus one
10
- stylesheet drop it in via `<script>`, no build step or framework required.
10
+ stylesheet. Drop it in via `<script>`, no build step or framework required.
11
11
 
12
12
  The package also exports the **framework-agnostic chat engine** that powers it
13
- (`bunnyquery/engine`) the same DOM-free core the Skapi admin chatbox consumes so
13
+ (`bunnyquery/engine`), the same DOM-free core the Skapi admin chatbox consumes, so
14
14
  you can build your own chat UI on top of it. See
15
15
  [Importing the chat engine](#importing-the-chat-engine).
16
16
 
17
17
  ## Features
18
18
 
19
19
  - **AI chat** against your project's configured agent (Claude or OpenAI under the
20
- hood), with streaming-style "Thinking…" indicators and a background indexing queue.
21
- - **Authentication** email/password login, optional signup, password change,
20
+ hood), with animated pending indicators and a background indexing queue.
21
+ - **Authentication**: email/password login, optional signup, password change,
22
22
  email verification, account recovery, and "Sign in with Google".
23
- - **Conversation history** paginated, with "Fetching history…" indicators on
23
+ - **Conversation history**: paginated, with "Fetching history…" indicators on
24
24
  first load and on scroll-up.
25
- - **Attachments** drag-and-drop files and folders, per-file upload status
25
+ - **Attachments**: drag-and-drop files and folders, per-file upload status
26
26
  (uploading / failed / indexed), overflow collapsing for large batches, and a
27
27
  prompt when an upload hits a file that already exists (skip / reindex only /
28
28
  overwrite, with "apply to all remaining"). Images are read with vision/OCR,
@@ -33,12 +33,12 @@ you can build your own chat UI on top of it. See
33
33
  across as many passes as it takes. A file's passes collapse into a single
34
34
  status row in the chat that can be expanded, and stopped: "Stop" cancels every
35
35
  queued and running pass at once and ends the continuation chain.
36
- - **Attachment parser plugins** register a client-side parser so the widget
36
+ - **Attachment parser plugins**: register a client-side parser so the widget
37
37
  extracts text in the browser from formats the model can't otherwise read, and
38
38
  indexes it directly. See [Attachment parser plugins](#attachment-parser-plugins).
39
- - **Settings panel** in-place inside the chat: light/dark theme, account details,
39
+ - **Settings panel**, in place inside the chat: light/dark theme, account details,
40
40
  newsletter subscription, clear history, and remove account.
41
- - **Theming** light and dark modes via CSS custom properties; the choice is
41
+ - **Theming**: light and dark modes via CSS custom properties; the choice is
42
42
  remembered in `localStorage` and falls back to the OS preference.
43
43
 
44
44
  ## Requirements
@@ -82,7 +82,7 @@ call `BunnyQuery.init()`:
82
82
  </html>
83
83
  ```
84
84
 
85
- That's it BunnyQuery takes over the `#chatbox` element and renders the login or
85
+ That's it. BunnyQuery takes over the `#chatbox` element and renders the login or
86
86
  chat view depending on the user's session.
87
87
 
88
88
  ## What's in the package
@@ -91,11 +91,11 @@ chat view depending on the user's session.
91
91
  | ----------------------------- | -------------------------------------------------------------------------------- |
92
92
  | `bunnyquery.js` | The widget IIFE. Exposes the global `window.BunnyQuery`. CDN / `<script>` drop-in. |
93
93
  | `bunnyquery.css` | The widget's full stylesheet, scoped under `.bq-agent` / `[data-bq-theme]`. |
94
- | `bunnyquery/engine` | The framework-agnostic chat engine ships as ESM + CJS with TypeScript types. |
94
+ | `bunnyquery/engine` | The framework-agnostic chat engine. Ships as ESM + CJS with TypeScript types. |
95
95
  | `bunnyquery/styles/chat.css` | The shared chat-surface styles (bubbles, markdown, links) for an engine-built UI. |
96
96
 
97
97
  The two widget files can be hosted yourself (same origin recommended) or loaded from a
98
- CDN no npm needed. The `engine` / `styles` subpaths are for bundler consumers
98
+ CDN, no npm needed. The `engine` / `styles` subpaths are for bundler consumers
99
99
  (`npm install bunnyquery`); see [Importing the chat engine](#importing-the-chat-engine).
100
100
 
101
101
  ## API
@@ -123,6 +123,7 @@ Mounts the widget. Returns the `BunnyQuery` object.
123
123
  | `mcpBaseUrl` | `string` | `null` | Override the MCP OAuth server base URL entirely (advanced). |
124
124
  | `hostDomain` | `string` | `null` | db-CDN host for temporary file URLs. Defaults to `skapi.app` (dev) / `skapi.com` (prod). |
125
125
  | `attachmentParsers` | `array` | `null` | Client-side attachment parsers. See [Attachment parser plugins](#attachment-parser-plugins). |
126
+ | `windowedIndexing` | `boolean` | `true` | Server-driven windowed indexing for text and grid files (see [file types](#supported-file-types)). Pass `false` to fall back to agent-driven paging, which keeps the traversal inside the model's turn budget and the tab open. |
126
127
 
127
128
  ### Methods
128
129
 
@@ -142,9 +143,9 @@ BunnyQuery.toggleTheme();
142
143
  BunnyQuery.logout();
143
144
  ```
144
145
 
145
- > `init()` is idempotent calling it twice logs a warning and returns the existing
146
+ > `init()` is idempotent: calling it twice logs a warning and returns the existing
146
147
  > instance rather than re-mounting. On a successful mount it logs its version, e.g.
147
- > `[bunnyquery] v1.7.0`.
148
+ > `[bunnyquery] v1.8.3`.
148
149
 
149
150
  See [HISTORY.md](HISTORY.md) for the release-by-release changelog.
150
151
 
@@ -209,13 +210,13 @@ upload: a 5MB `.txt` indexed 4.0% of its content, a 4.8MB `.json` 4.2%, a
209
210
  because the agent received a plausible-looking document with no way to know most
210
211
  of it was missing.
211
212
 
212
- Two drivers exist for this loop. By default the agent pages the file itself with
213
- the `readFileContent` tool. With the engine's `windowedIndexing` option enabled,
213
+ Two drivers exist for this loop. With `windowedIndexing` (the widget's default)
214
214
  the **worker** reads a window per request and continues from the reader's own
215
- cursor, so the traversal no longer has to fit inside the model's turn budget.
216
- That option is off by default and must only be turned on against a deployed
217
- worker (see [Importing the chat engine](#importing-the-chat-engine)); the widget
218
- does not enable it.
215
+ cursor, so the traversal neither has to fit inside the model's turn budget nor
216
+ depends on the tab staying open. Without it the agent pages the file itself with
217
+ the `readFileContent` tool. Pass `windowedIndexing: false` to `init()` to opt
218
+ out; engine consumers opt in via `configureChatEngine` (see
219
+ [Importing the chat engine](#importing-the-chat-engine)).
219
220
 
220
221
  ### 4. Everything else extractable: inlined as text server-side
221
222
 
@@ -223,8 +224,10 @@ The skapi proxy downloads the file, extracts its text **server-side**, and
223
224
  inlines that text into the request, so the model reads it directly with no
224
225
  fetching. This keeps indexing consistent across model providers.
225
226
 
226
- **Office & e-book** (binary/zip, parsed):
227
- `.docx` · `.xlsx` · `.pptx` · `.hwp` · `.hwpx` · `.ods` · `.odt` · `.odp` · `.epub`
227
+ **Office & e-book** (binary/zip, parsed; includes legacy binary `.doc`/`.xls`/`.ppt`
228
+ and the macro-enabled `.docm`/`.xlsm`/`.pptm`):
229
+ `.doc` · `.docx` · `.docm` · `.xls` · `.xlsx` · `.xlsm` · `.ppt` · `.pptx` · `.pptm`
230
+ · `.hwp` · `.hwpx` · `.ods` · `.odt` · `.odp` · `.epub`
228
231
 
229
232
  **Text, data, markup & source code** (decoded as text; `.html`/`.htm` have their
230
233
  tags stripped):
@@ -261,13 +264,9 @@ via `web_search` (external web access is enabled).
261
264
 
262
265
  ### Caveats
263
266
 
264
- - **Legacy / macro Office** `.doc` `.xls` `.ppt` (legacy binary) and `.docm`
265
- `.xlsm` `.pptm` (macro-enabled) have no reliable server-side reader. They
266
- upload fine but are indexed from **metadata only**; re-save as
267
- `.docx` / `.xlsx` / `.pptx` (or PDF) to capture their contents.
268
- - **Anything else** — a format covered by none of the above is indexed from its
267
+ - **Anything else**: a format covered by none of the above is indexed from its
269
268
  metadata. To support it, register your own
270
- [Attachment parser plugin](#attachment-parser-plugins) it runs in the browser
269
+ [Attachment parser plugin](#attachment-parser-plugins), which runs in the browser
271
270
  and feeds parsed text straight into indexing.
272
271
 
273
272
  ### Re-indexing an existing file
@@ -311,7 +310,7 @@ interface AttachmentParser {
311
310
  ```
312
311
 
313
312
  The first parser whose `match` returns `true` wins. A parser that throws or
314
- returns nothing is ignored the file falls back to its normal path. Output is
313
+ returns nothing is ignored, and the file falls back to its normal path. Output is
315
314
  capped (~200k chars) before it is inlined.
316
315
 
317
316
  ### Example
@@ -370,13 +369,13 @@ The active theme is saved to `localStorage`, so a returning user keeps their cho
370
369
  ## Importing the chat engine
371
370
 
372
371
  `bunnyquery.js` is the ready-made widget. Under it sits a **framework-agnostic,
373
- DOM-free chat engine** the same core that powers both this widget and the Skapi
372
+ DOM-free chat engine**, the same core that powers both this widget and the Skapi
374
373
  admin chatbox. Import it from `bunnyquery/engine` when you want to build your own chat
375
374
  UI (React, Vue, Svelte, vanilla…) while reusing the engine's message/queue/typewriter/
376
375
  cache state machine, request builders, markdown-message composition, and prompts.
377
376
 
378
- Install the package, plus the `skapi-js` SDK (for the transport) and if you don't
379
- already have one a markdown renderer such as `marked`:
377
+ Install the package, plus the `skapi-js` SDK (for the transport) and, if you don't
378
+ already have one, a markdown renderer such as `marked`:
380
379
 
381
380
  ```bash
382
381
  npm install bunnyquery skapi-js marked
@@ -412,12 +411,12 @@ session.dispatchComposedMessage('Hello!'); // send a message
412
411
 
413
412
  The engine owns chat **state and logic** and calls back into your view through the
414
413
  `ChatHost` interface (render, scroll, identity, cancel/refresh). It has **no bundled
415
- runtime dependencies** you inject the skapi transport via `configureChatEngine()` and
414
+ runtime dependencies**: you inject the skapi transport via `configureChatEngine()` and
416
415
  render markdown yourself (e.g. with `marked`). Everything is fully typed: `ChatSession`,
417
416
  `ChatHost`, `ChatMessage`, `ChatIdentity`, `ChatState`, `composeUserMessage`, the request
418
417
  builders (`callClaudeWithPublicMcp` / `callOpenAIWithPublicMcp`, `getChatHistory`,
419
418
  `notifyAgentSaveAttachment`), the prompt builders, and the token-budget / link / history
420
- helpers see the `.d.ts` shipped with `bunnyquery/engine`.
419
+ helpers. See the `.d.ts` shipped with `bunnyquery/engine`.
421
420
 
422
421
  `configureChatEngine` options:
423
422
 
@@ -426,9 +425,9 @@ helpers — see the `.d.ts` shipped with `bunnyquery/engine`.
426
425
  | `clientSecretRequest` | `function` | `skapi.clientSecretRequest`, bound to your Skapi instance. **Required.** |
427
426
  | `clientSecretRequestHistory` | `function` | `skapi.clientSecretRequestHistory`, bound to your Skapi instance. **Required.** |
428
427
  | `mcpBaseUrl` | `string` | MCP server base URL (you resolve prod vs dev). **Required.** |
429
- | `poll` | `number?` | Value attached as `poll` on every request. Omit it if your `clientSecretRequest` already resolves with the final body; pass `0` for the deployed `skapi-js@latest` (needed for the early ack + a manual `.poll()` handle that powers queued-send cancel the widget's case). |
428
+ | `poll` | `number?` | Value attached as `poll` on every request. Omit it if your `clientSecretRequest` already resolves with the final body; pass `0` for the deployed `skapi-js@latest` (needed for the early ack + a manual `.poll()` handle that powers queued-send cancel, the widget's case). |
430
429
  | `attachmentParsers` | `array?` | Client-side attachment parsers, registered at configure time. More can be added later with `registerAttachmentParser()`. See [Attachment parser plugins](#attachment-parser-plugins). |
431
- | `windowedIndexing` | `boolean?` | Opt in to **server-driven** windowed indexing for text and grid files (see [file types](#supported-file-types)). Off by default and must stay off until the worker that strips the `_skapi_window` directive is deployed: against an older worker the directive reaches the provider as an unknown body field and the call fails terminally with no retry. |
430
+ | `windowedIndexing` | `boolean?` | Opt in to **server-driven** windowed indexing for text and grid files (see [file types](#supported-file-types)). Off by default in the engine; the widget passes it as `true`. The deployed skapi workers support it; only leave it off against a self-hosted worker that does not yet strip the `_skapi_window` directive, where it would reach the provider as an unknown body field and fail the call terminally with no retry. |
432
431
 
433
432
  ### Display and paging helpers
434
433
 
@@ -464,7 +463,7 @@ never progress.
464
463
  BunnyQuery connects to your AI agent through an MCP OAuth server
465
464
  (`mcp.broadwayinc.computer` in production, `mcp-dev.broadwayinc.computer` when
466
465
  `dev: true`). After authorization, the OAuth server redirects back to **the current
467
- host page** BunnyQuery reads the `?code=…&state=…` parameters, completes the
466
+ host page**: BunnyQuery reads the `?code=…&state=…` parameters, completes the
468
467
  exchange, and cleans them from the URL automatically. No dedicated callback page is
469
468
  needed; just make sure the page that hosts the widget is a stable, reachable URL.
470
469
 
package/bunnyquery.css CHANGED
@@ -1119,7 +1119,17 @@
1119
1119
  border: 1px solid var(--bq-line);
1120
1120
  background: rgba(127, 127, 127, 0.05);
1121
1121
  }
1122
+ /* Three states, three colours, and the row is readable at a glance without
1123
+ opening it:
1124
+ yellow working on it now (.is-active)
1125
+ green indexed, confirmed (.is-indexed)
1126
+ grey not known yet (.is-resolving, and the base look)
1127
+ Grey is the DEFAULT rather than a colour of its own, so anything that has not
1128
+ earned green or yellow reads as "no claim" — which is also what a cancelled
1129
+ row should look like, and what an unrecognised future state would fall back
1130
+ to. Red stays reserved for a failure. */
1122
1131
  .bq-index-group.is-active { border-color: var(--bq-warning-border); background: var(--bq-warning-bg); }
1132
+ .bq-index-group.is-indexed { border-color: var(--bq-success-border); background: var(--bq-success-bg); }
1123
1133
  .bq-index-group.is-error { border-color: var(--bq-danger); background: var(--bq-danger-bg); }
1124
1134
 
1125
1135
  .bq-index-head {
@@ -1142,7 +1152,12 @@
1142
1152
  }
1143
1153
  .bq-index-head:hover { background: rgba(127, 127, 127, 0.08); color: var(--bq-muted); }
1144
1154
  .bq-index-group.is-active .bq-index-head { color: var(--bq-warning); }
1155
+ .bq-index-group.is-indexed .bq-index-head { color: var(--bq-success); }
1145
1156
  .bq-index-group.is-error .bq-index-head { color: var(--bq-danger); }
1157
+ /* Stated rather than inherited: a waiting row keeping the muted default is a
1158
+ DECISION (it is not claiming a state), not an oversight, and writing it here
1159
+ means adding a colour to the base rule cannot silently give it one. */
1160
+ .bq-index-group.is-resolving .bq-index-head { color: var(--bq-muted); }
1146
1161
 
1147
1162
  /* Status glyph. The active one is a circular-arrow SVG the consumer inlines
1148
1163
  (never an icon font: agent.vue's Material Symbols build gates glyphs behind
@@ -1158,8 +1173,17 @@
1158
1173
  .bq-index-icon svg { width: 100%; height: 100%; display: block; }
1159
1174
  .bq-index-group.is-active .bq-index-icon svg { animation: bq-index-spin 1.1s linear infinite; }
1160
1175
  @keyframes bq-index-spin { to { transform: rotate(360deg); } }
1176
+ /* Waiting to find out (a run whose earlier passes are still being paged in, a
1177
+ file whose queue state has not been answered). Deliberately NOT the active
1178
+ look: no warning colour and no spin, because a spinning glyph on a chat row is
1179
+ read as "this file is being worked on right now" and that is the one thing this
1180
+ state does not know. A slow fade says "pending" without claiming progress. */
1181
+ .bq-index-group.is-resolving .bq-index-icon svg { animation: bq-index-fade 1.6s ease-in-out infinite; }
1182
+ @keyframes bq-index-fade { 0%, 100% { opacity: 1; } 50% { opacity: 0.35; } }
1161
1183
  @media (prefers-reduced-motion: reduce) {
1162
1184
  .bq-index-group.is-active .bq-index-icon svg { animation-duration: 3s; }
1185
+ /* A fade has no end state to settle on, so it stops rather than slows. */
1186
+ .bq-index-group.is-resolving .bq-index-icon svg { animation: none; opacity: 0.7; }
1163
1187
  }
1164
1188
 
1165
1189
  .bq-index-label {
@@ -1218,8 +1242,28 @@
1218
1242
  border-left: 2px solid var(--bq-line);
1219
1243
  }
1220
1244
  .bq-message.bq-index-pass .bq-bubble { font-size: 0.78rem; }
1221
- /* Close the run: the next ordinary message needs its normal breathing room. */
1222
- .bq-message.bq-index-pass + :not(.bq-index-pass) { margin-top: 0.55rem; }
1245
+ /* Close the run: the next ordinary message needs its normal breathing room.
1246
+ NOT scoped to .bq-message: the trailing loader below is a run member too, and
1247
+ requiring .bq-message would silently drop the gap whenever it is the last one. */
1248
+ .bq-index-pass + :not(.bq-index-pass) { margin-top: 0.55rem; }
1249
+ /* Trailing loader: an open row keeps a "still working" mark of its own for as long
1250
+ as the file's indexing is not confirmed over. It sits on the same rail as the
1251
+ passes so it reads as the run continuing rather than as a message of its own —
1252
+ which is why the rail is repeated here instead of reusing the rule above, whose
1253
+ selector requires .bq-message. */
1254
+ .bq-index-tail {
1255
+ margin-bottom: 0.3rem;
1256
+ margin-left: 0.7rem;
1257
+ padding: 0.1rem 0 0.1rem 0.7rem;
1258
+ border-left: 2px solid var(--bq-line);
1259
+ font-size: 0.78rem;
1260
+ color: var(--bq-muted);
1261
+ }
1262
+ /* An open row animates for as long as the file is indexing, which can be minutes.
1263
+ Readers who ask for less motion get the static end state. */
1264
+ @media (prefers-reduced-motion: reduce) {
1265
+ .bq-index-tail .bq-loader::after { animation: none; content: '...'; }
1266
+ }
1223
1267
  .bq-index-note {
1224
1268
  padding: 0 0.6rem 0.45rem;
1225
1269
  font-size: 0.7rem;
@@ -1253,6 +1297,68 @@
1253
1297
  /* While the temporary URL is being re-resolved the click is swallowed — show a
1254
1298
  busy cursor and dim it so it reads as "working, don't click again". */
1255
1299
  .bq-link-button.is-refreshing { cursor: progress; opacity: 0.6; }
1300
+ /* The client asked for a url for this file and did not get one, so the chip is
1301
+ emitted with NO href (it cannot navigate) and with ✕ in place of ↗. Paint it
1302
+ as the dead text it now is: muted, no underline, default cursor, and no hover
1303
+ response, since the base rule above still fires on hover and would otherwise
1304
+ promising a link. */
1305
+ .bq-link-button.is-unavailable,
1306
+ .bq-link-button.is-unavailable:hover {
1307
+ color: var(--bq-muted);
1308
+ text-decoration: none;
1309
+ cursor: default;
1310
+ }
1311
+
1312
+ /* ---- inline image previews ----------------------------------------------*/
1313
+ /* The anchor IS the preview: picture on top, the ordinary chip underneath as a
1314
+ caption. The caption is the FALLBACK, not decoration: when the url dies or the
1315
+ file is gone the <img> hides itself and what is left is exactly the text chip
1316
+ this feature replaced, still clickable. */
1317
+ .bq-link-button.is-image-preview {
1318
+ display: inline-block;
1319
+ max-width: 100%;
1320
+ white-space: normal;
1321
+ overflow: visible;
1322
+ text-overflow: clip;
1323
+ vertical-align: top;
1324
+ margin: 0.25em 0;
1325
+ }
1326
+ /* Qualified with .bq-md because `.bq-md img` below already sets max-width and
1327
+ would otherwise out-specify a bare .bq-img-preview. */
1328
+ .bq-md img.bq-img-preview {
1329
+ display: block;
1330
+ /* Bounded on BOTH axes so a 12000px panorama and a 40x9000 sliver each land
1331
+ inside the bubble; width/height auto keep the aspect ratio. The bubble cap
1332
+ differs per client, so the preview carries its own. */
1333
+ max-width: min(100%, 320px);
1334
+ max-height: 320px;
1335
+ width: auto;
1336
+ height: auto;
1337
+ border-radius: 4px;
1338
+ border: 1px solid var(--bq-line);
1339
+ /* A transparent PNG is invisible on paper without this. */
1340
+ background: rgba(127, 127, 127, 0.08);
1341
+ }
1342
+ /* Keyed on the MISSING src, not on a state class: the element is emitted with no
1343
+ src at all (the url has to be minted first) and a src-less <img> would
1344
+ otherwise paint the browser's broken-image glyph. */
1345
+ .bq-md img.bq-img-preview:not([src]),
1346
+ .bq-md img.bq-img-preview[data-bq-img-state="error"] { display: none; }
1347
+ /* The dot trail covers the mint round trip, then gets out of the way. */
1348
+ .bq-md img.bq-img-preview[src] ~ [data-bq-img-loader],
1349
+ .bq-md img.bq-img-preview[data-bq-img-state="error"] ~ [data-bq-img-loader] { display: none; }
1350
+ .bq-img-preview-caption {
1351
+ display: inline-block;
1352
+ max-width: 100%;
1353
+ overflow: hidden;
1354
+ text-overflow: ellipsis;
1355
+ white-space: nowrap;
1356
+ vertical-align: bottom;
1357
+ }
1358
+ /* A collapsed indexing row is a single line and its label is the indexed file's
1359
+ own path, which is frequently an image. Never grow that row into a picture. */
1360
+ .bq-index-label .bq-md img.bq-img-preview,
1361
+ .bq-index-label [data-bq-img-loader] { display: none; }
1256
1362
 
1257
1363
  /* ---- rendered-markdown body ----------------------------------------------*/
1258
1364
  /* Overrides the bubble's `white-space: pre-wrap` since marked already produces