---
title: "Egenverk Babbla — Ändringslogg"
description: "Ändringslogg för Egenverk Babbla. Vad som ändrats i varje tillägg, senaste först."
canonical: https://egenverk.se/plugins/babbla/changelog
lang: sv
updated: 2026-09-29
alternate: https://egenverk.se/en/plugins/babbla/changelog.md
---
# Ändringslogg för Egenverk Babbla

Vad som ändrats i varje tillägg, senaste först.

Ändringsloggens text är på engelska, som i tillägget.

## 0.46.1 — 2026-09-29

**Åtgärdat**

- With the chat closed, the unread badge could miss a message for up to two minutes when two arrived within a few seconds of each other. Tabs of one browser share the unread count for eight seconds (0.18.0), and a tab that noticed the second message could take the count another tab had fetched just before it. A shared count is now used only when it is at least as new as the change the tab noticed; otherwise the tab asks the server.

## 0.46.0 — 2026-09-29

**Tillagt**

- Settings → Babbla → Diagnostics can turn on **lean chat reads**. Every time a chat checks for new messages — the conversation list, a conversation's messages, the unread count and the combined check the open chat makes — WordPress normally starts with every plugin and the theme, WooCommerce included, although none of them is needed to answer. With lean reads on, those requests load only Babbla and no theme, so each one holds a PHP worker for a fraction of the time. Sending messages, the assistant and everything else on the site are unchanged. It installs a small must-use plugin, `wp-content/mu-plugins/egenverk-babbla-lean.php`, and is off until an admin turns it on: a plugin that grants chat access through its own capability rule is not loaded for those requests either, and its users would then be refused — turn lean reads off again in that case. Deactivating Babbla removes the file (on a multisite network only a network-wide deactivation does, since the file serves every site). `wp egvb lean on|off|status` does the same from WP-CLI. When a Babbla update changes the file, Diagnostics offers to update it.

## 0.45.0 — 2026-09-29

**Tillagt**

- Diagnostics now has a Server load card showing Babbla REST requests by route, request rate and response time. It needs a persistent object cache such as Redis; without one it counts nothing. Measurements remain for at most two hours.

## 0.44.0 — 2026-09-29

**Ändrat**

- A new message or an assistant answer now wakes only the chats of the people who can see it, instead of every open wp-admin tab on the site. Each user has their own change signal file: an assistant conversation wakes only its owner, a direct message its two participants, a private room its members. A public room still wakes everyone. Before, every write — including each step of an assistant answer ("Searching the chat…", "Calculating…") in someone's private assistant chat — made every open tab of every user ask WordPress, so one question could cost more than a hundred WordPress starts across the site. The file still says only that something changed; its name is derived from the user id, a random key and the site's secret salt, so no one can work out a colleague's file.
- Settings → Babbla → Diagnostics tests the admin's own signal file, so opening the tab no longer wakes every other open chat.
- A wp-admin tab left open across the update reads the old shared file, which is deleted once the first user file is created: it gets three 404s and asks the server on a schedule until it is reloaded. Uninstall (with purge on) removes every user file.

## 0.41.0 — 2026-09-24

**Tillagt**

- Settings → Babbla → Diagnostics has a "Live updates" row. When the tab opens it rewrites the change signal file and fetches it back over HTTP the way a browser does: it says the signal works, that the file is cached or unreachable (with the HTTP status, or "stale content" when an old token comes back), that the test could not run from the server (a blocked loopback, which says nothing about the browser), or that uploads is not writable. The result is kept for five minutes, so reloading the tab does not rewrite the file each time.

**Ändrat**

- The change signal file moved from the uploads root into its own directory, `wp-content/uploads/egenverk-babbla-signal/`, which carries an `.htaccess` that sends `Cache-Control: no-store, private` (Apache with mod_headers) and an empty `index.php`. The directory and both files are created on activation, on the first write, and again whenever one is missing. The old file in the uploads root is deleted once the new one exists; uninstall (with purge on) removes the directory as well. A wp-admin tab left open across the update still reads the old address: it gets three 404s and falls back to asking the server on a schedule until it is reloaded.
- A cache that freezes the signal file no longer delays new messages by up to a minute for the rest of the page. When the server has reported a token for more than 30 seconds that the file still has not shown, the page stops reading the file, gives up its place as the browser's reader (0.40.0), falls back to the schedule it uses without a signal (0.38.0/0.39.0) and writes one warning to the browser console naming a cache in front of uploads as the likely cause. A file that lags less than 30 seconds, or a burst of writes, does not trigger it.

## 0.40.0 — 2026-09-24

**Ändrat**

- With several wp-admin tabs open, only one of them reads the change signal file: the visible tab that holds a browser lock (Web Locks) reads it at the pace the fastest visible tab needs and passes every token to the other tabs (BroadcastChannel). Each tab still asks WordPress itself when the token changed, so a new message costs one request per tab as before, and every tab keeps its own safety net and the tab-return rule. A hidden tab never leads; a tab that hears no token for three of its read steps plus two seconds (a frozen or throttled leader) reads the file again itself. Measured in the browser tests: one hour of three visible tabs (one open idle conversation, two closed chats) is 1 826 file reads instead of 3 207, and the same 120 requests to WordPress (60 + 30 + 30). A browser without Web Locks or BroadcastChannel behaves exactly as in 0.39.0.

## 0.39.0 — 2026-09-24

**Ändrat**

- An open chat asks the server about once a minute instead of every 4–16 seconds while nothing happens; new messages still appear within one to two seconds. The open conversation and the conversation list read the small change file every second for a minute after something happens (a message arrives or is sent, the user types, a conversation is opened) and every two seconds otherwise, and ask WordPress only when the file changed, when the tab comes back after more than 30 seconds, or on a one-minute safety net. Measured in the browser tests: one hour of an open conversation with nothing new is 60 requests to WordPress and 1 767 file reads, against 226 requests in 0.38.0. The list is refreshed with every change and on the safety net instead of on every fourth request.
- The assistant's progress no longer costs a request every 1.5 seconds: while it works the chat reads the file every second, and the assistant's status and answer arrive with the change that carries them.
- The open chat follows the server's `poll_interval` and `signal_token` from `GET /poll` too (see 0.38.0); a busy site's `egvb_poll_interval` now also stretches the open chat's safety net. Without the file (uploads not writable, or three failed reads) the open chat keeps the 0.38.0 schedule unchanged.

## 0.38.0 — 2026-09-24

**Tillagt**

- `GET /unread` and `GET /poll` return `poll_interval` (seconds) and `signal_token` (the change signal's current token, read before the answer is built). A new filter, `egvb_poll_interval` (default 0, clamped to 0–600 seconds), lets a busy site make every closed chat wait longer between its scheduled requests without a release.

**Ändrat**

- An idle wp-admin tab with the chat closed asks the server about once every two minutes instead of every ten seconds; the unread badge still updates within about five seconds of a new message. The closed chat now checks the same small file as the open chat, every five seconds, and asks WordPress for the unread count only when that file changed (at most once per five seconds), when the tab comes back after more than 30 seconds, or on a two-minute safety net. Measured in the browser tests: one hour of a closed chat with nothing new is 30 requests to WordPress and 720 file reads, against 360 requests before.
- A hidden tab asks every five minutes instead of every 30 seconds, and does not read the file. Without the file (uploads not writable, or three failed reads) the closed chat keeps the 10- and 30-second schedule of 0.37.1. An open conversation and the conversation list are unchanged.

## 0.37.1 — 2026-09-24

**Ändrat**

- Database schema 8: updating converts that one column in place (`ALTER TABLE … MODIFY request_id`). Existing messages and ids are kept; if the conversion fails, the plugin retries it on the next request.

**Åtgärdat**

- A message containing å, ä, ö or any other non-ASCII character was never saved: the chat showed "Send status unknown" and a retry failed the same way. The same fault could stop the assistant's answers and chat searches that contained such characters. Since 0.17.0 the column holding each message's request id used the ascii character set; WordPress then treats the whole messages table as ascii and refuses any query on it that carries other characters. The column now uses the table's own character set, still compared case-sensitively.

## 0.37.0 — 2026-09-24

**Tillagt**

- A new route, `GET /babbla/v1/poll`, answers in one request what an open conversation used to ask for in two: its new messages and, when due, the conversation list (and, on request, the unread count). Each part is exactly what its own route returns. The existing routes are unchanged.

**Ändrat**

- A new message in an open conversation now costs the server one request instead of two: the chat fetches the message and the refreshed conversation list together, so WordPress starts once per update instead of twice. A page loaded before the update keeps working: when the server does not know the new route, the chat goes back to the separate requests.

## 0.36.0 — 2026-09-23

**Tillagt**

- Messages from colleagues appear within about two seconds instead of up to sixteen. The open chat checks a small file in the uploads folder every two seconds (three on the conversation list) and only asks the server for messages when that file says something changed. The file holds a timestamp and nothing else: it reveals that something changed in the chat, never what, where or by whom. It is removed on uninstall when "Purge data on uninstall" is on.
- The assistant's answer, and its "Searching the chat…"-style status, show sooner: while it works the conversation is checked every one and a half seconds, for up to two minutes; after that the small file wakes the chat when the answer is ready.

**Ändrat**

- Less load on the server: an open chat with nothing new asks WordPress for messages every 16 seconds in a conversation and every 20 seconds on the list, instead of every 4 to 16 seconds. A new message elsewhere also refreshes the conversation list right away, so its unread count shows without waiting.
- When the uploads folder is not writable, or reading the file fails three times in a row (an error page or something that is not the file; a dropped network connection only skips that check), the chat falls back to exactly the checking schedule of 0.35.2. A CDN that caches the file only slows the chat to the 16- and 20-second checks. A hidden tab or a closed chat is unchanged.

## 0.35.2 — 2026-09-23

**Åtgärdat**

- The thin loading stripe at the top of the chat no longer blinks every few seconds on its own. Checking for new messages, unread counts, read markers and the colleague list now run quietly in the background; the stripe shows only while something you did (send, create, search, open) is waiting for the server.

## 0.35.1 — 2026-09-23

**Åtgärdat**

- On the full-page chat, the mention picker and the other panels (new message, new room, room info) cover only the conversation, not the whole admin screen.
- The full-page chat's conversation list is wider (300 px), so room and person names are no longer cut to a few letters.

## 0.35.0 — 2026-09-23

**Ändrat**

- Access, Usage and Diagnostics follow the Egenverk plugin shell like the Settings tab: each section is a card with its heading, tables use the Egenverk table style, and confirmations ("Access saved.", "Babbla ownership claimed.", "The log files were deleted.") are green notices.
- Access: claiming ownership, finding a user and choosing a user's Babbla role are each a card of rows, label and help on the left and the field and its button on the right.
- Usage: the period's figures are a row of tiles (questions, failed, tokens in, tokens out, and the estimated cost when prices are set) instead of one sentence. The daily bars use the Babbla colour, and a day over the budget is red.
- Diagnostics: each status row names its state in a chip ("OK", "Warning", "Error") instead of a coloured dot alone. The log file picker has a visible label, and the log scrolls inside its own box. "Delete all log files" is a red button that shows a loader while it works.
- On a phone the rows stack and the tables scroll sideways inside their card.
- Form fields, what each form sends and who may use it are unchanged.

## 0.34.0 — 2026-09-23

**Ändrat**

- Settings → Babbla follows the Egenverk plugin shell. The version sits beside the Babbla name in the header, and each tab's content fades in on load.
- The Settings tab is one page of cards: Overview, Provider, Behaviour, Limits and cost, and Data. The left section menu is gone. Each setting is a row with its label and help on the left and the field on the right. On a phone the rows stack.
- The floating bubble, "Purge data on uninstall" and each integration are now on/off switches.
- Provider is one choice at the top of the Provider card. Only the chosen provider's key and model fields show. The "In use" badge and the second "Use … for the assistant" radio are gone. "Save everything and test the connection" is a secondary button with its description on its own line, and it tests the chosen provider.
- Saving works the Egenverk way. A save bar slides up from the bottom only when something has changed, and a "Settings saved." toast confirms the save. The always-visible save bar at the top and the second Save button at the bottom are gone. Pressing Enter in a field saves, and it does not run the connection test.
- Option names, stored values and what is saved are unchanged.

## 0.33.4 — 2026-09-23

**Åtgärdat**

- The assistant can be mentioned from the picker again. In a room (public or private) it is the first row of "Mention a colleague or assistant", with its avatar and "Answers once in this room", and it filters with the search like everyone else; the near-invisible "@Agent" button above the list is gone. Direct messages still do not offer it, since the assistant never answers there.
- The picker's search field is visible in the dark theme: it has a fill, a border and a search icon, and a clear (×) button empties it.
- Every overlay (mention picker, new DM, new room, room info) has a close (×) button in its header that returns to where you were, like Back and Esc.
- Selected mentions above the composer are pills with the name and a separate × to remove them.

## 0.33.3 — 2026-09-23

**Åtgärdat**

- Picking a person under New direct message no longer turns the row into "PAPatrik" with a spinner. The busy state flattened the row to text, so the avatar's initials ran into the name and the avatar never came back after a failed request. The spinner now takes the avatar's place and the avatar returns when the request fails.

## 0.33.2 — 2026-09-23

**Åtgärdat**

- Plugin Check reports no code errors. The access compare-and-swap passes `$wpdb->prepare()` straight to `$wpdb->query()`; the analytics queries that build their SQL from fixed fragments and placeholder lists say so where they run; calculator error messages escape the function names they quote; uninstall deletes log files with `wp_delete_file()`; the "Selected user" string has a translators comment.
- `Tested up to` is 6.9, the version the acceptance runs used.

## 0.33.1 — 2026-09-23

**Ändrat**

- The Egenverk mark is centred on its grid (kit sync from `_commercial/brand/egenverk-ui/`): the symbol and the Babbla glyph in `assets/egenverk-ui/`, the "by egenverk" signature on Settings → Babbla, and the wordpress.org banner and icons, now rendered from sources kept in `.wordpress-org/src/`.

## 0.33.0 — 2026-09-23

**Ändrat**

- **One menu entry: Settings → Babbla.** The top-level Babbla menu is gone. Settings, Access, Usage and Diagnostics are tabs of one page (`options-general.php?page=egenverk-babbla&tab=…`), each shown only to users who may open it; a chat user without Babbla administration sees Access only. The Plugins screen gets a Settings link to it.
- The full-page chat has no menu entry. The admin-bar chat opens it, and the drawer's ⋯ menu has "Open as full page". Its url is `admin.php?page=egenverk-babbla-chat`.
- Settings → Babbla is built with the Egenverk UI kit 1.1.0 (`assets/egenverk-ui/`, copied unchanged, loaded only on that page): header with the Babbla glyph and the egenverk signature, lead, tabs and footer. Fields and behaviour are unchanged; each tab's own heading replaces its old page title, and the settings sidebar no longer repeats links to Access and Usage.
- wordpress.org banner (1544×500, 772×250) and icon (128, 256) in `.wordpress-org/`, kept out of the release zip.
- The settings help texts point at the Usage and Diagnostics tabs instead of the old "Babbla → …" menu path, and only the end of the page keeps room for the floating chat bubble (every form used to reserve it, leaving a gap under the usage period selector).
- The admin urls of 0.31.0 (`admin.php?page=bbl-*`) and 0.32.0 (`admin.php?page=egvb-*`) redirect to their tab or to the full-page chat for one version, keeping their other query args. This replaces 0.32.0's `bbl-*` → `egvb-*` redirect.

## 0.32.0 — 2026-09-23

**Ändrat**

- **The plugin is now Egenverk Babbla** (slug, folder and text domain `egenverk-babbla`, main file `egenverk-babbla.php`, Author Egenverk). WordPress sees the new folder as a new plugin: activating it deactivates the old `babbla/babbla.php` copy, so the two never answer the same question twice.
- Every PHP, CSS and JavaScript identifier moves from `BBL_`/`bbl_`/`bbl-` to `EGVB_`/`egvb_`/`egvb-`: classes, constants, options, capabilities (`egvb_chat`, `egvb_manage`), transients, admin-post actions, the log cron, admin page slugs, CSS classes and the `egvbConfig` client global. The WP-CLI command is `wp egvb job`. wordpress.org requires a prefix of at least four characters for global symbols.
- Filters are renamed to `egvb_*` (`egvb_agent_tools`, `egvb_integrations`, `egvb_capability`, `egvb_admin_capability`, `egvb_tool_capability`, `egvb_tool_status_label`, `egvb_redacted_keys`, `egvb_rate_limit`, `egvb_anthropic_request`, `egvb_openai_request`).
- The API key constants are `EGVB_ANTHROPIC_KEY` and `EGVB_OPENAI_KEY`.
- A `readme.txt` for wordpress.org, with the external services the assistant uses.
- The `bbl_*` filter names still run for this version, through `apply_filters_deprecated()`. So do `BBL_ANTHROPIC_KEY`/`BBL_OPENAI_KEY` in `wp-config.php`, the `wp bbl job` command, and agent jobs Action Scheduler queued under `bbl_run_job` before the update.
- `EGVB_Install::migrate_from_bbl()` runs on activation and on the first request after the update, after the 0.8.0 `wctc_` move: `bbl_settings`, `bbl_access`, `bbl_db_version`, `bbl_log_dir` and `bbl_purge_on_uninstall` are copied to their `egvb_` names (an existing `egvb_` value wins) and deleted; a stored `agent_capability` of `bbl_chat`/`bbl_manage` and any role granted those capabilities move to the new names; the `bbl_log_cleanup` event is cleared. It runs once and a second run changes nothing. Deleting the old keys also disarms the old copy's `uninstall.php`, which would otherwise read its purge flag and drop the tables this version uses. Transients are left to expire. `uninstall.php` cleans the old and the new keys.

## 0.31.0 — 2026-09-22

**Ändrat**

- The SQLite double used by the tests can be told to fail a chosen statement (`fail_on`), the way a timed-out query fails in production: no rows plus `last_error`, never an exception. Every fix above is pinned by a test that fails without it.

**Åtgärdat**

- A chat search that the statement guard stops no longer reads as "nothing found". `get_results()` answers a failed query with `false`, the array cast turned that into zero rows, and the agent reported — with every appearance of certainty — that nothing in the chat mentioned what was asked about. `BBL_Repository::search_messages()` returns null when the query itself failed, and `search_chat` refuses with an error that tells the model to say so instead of answering from it.
- The channel pre-scan that orders a search now runs inside the same statement guard, and a failure there is an error rather than an empty channel list — which reached the model as "nothing matched" by another route. `list_conversations` refuses in the same way instead of printing every conversation as empty and never touched.
- `POST /channels/{id}/read` answers 500 when the marker was not stored. `mark_read()` ignored both write results and read back a missing row as 0, so the client cleared its badge on a marker the server had never saved and the unread count returned on the next poll.
- `finish_job()` reports whether a row actually moved. `BBL_Agent_Runner`'s `finally` branch treats `status = 'running'` as unfinished, so a silently failed UPDATE could fail an answered job and post "The agent could not answer" underneath the answer it had just given. A job whose answer reached the channel is now closed as done by that branch, and the failure notice is posted only by whoever actually closed the row.

## 0.30.0 — 2026-09-22

**Ändrat**

- The settings sections work the way Customer Hub's do: the server renders the section the url names and the nav links are real urls, so a section is reachable, bookmarkable and shareable with no JavaScript at all. `assets/js/bbl-settings.js` only upgrades the click to an in-place swap, so switching costs no request and keeps unsaved edits in the fields. A url with an unknown or missing section opens Overview, and a connection test still opens Provider, where its result renders.
- The back button walks the sections that were opened instead of leaving the page.

**Åtgärdat**

- Clicking a settings section no longer jumps the page down a step. The switcher assigned `window.location.hash`, and the browser answers that by scrolling the named element to the top of the viewport; every click pushed the screen further down. The section now lives in the url as `?section=<slug>` and the client swaps the card with `history.pushState`, which does not scroll.

## 0.29.0 — 2026-09-22

**Tillagt**

- The floating launcher bubble can be hidden entirely and scaled in three sizes: small (40px), medium (56px, today's size, unchanged) and large (72px). The site sets a default (`BBL_Settings::get( 'launcher_visible' | 'launcher_size' )`, new fields on the settings page); a signed-in user's own choice, made from the bubble's own menu, is stored in `uiPrefs` in `localStorage` exactly like theme and drawer/window are, and overrides the site default until the user changes it back — a later site-default change does not silently undo it. Hidden means not rendered at all, not `display: none` on an element that still exists, so no keyboard trap is left behind.
- The admin-bar Babbla icon carries a second badge, `@N` (`@99+` above 99), for unread mentions, next to the existing total-unread badge. With the bubble hidden it is the only place `@`-mentions surface outside the panel itself.

## 0.28.0 — 2026-09-22

**Ändrat**

- The composer no longer waits for `GET /channels`. The cache paints the conversation on the first frame, and the textarea, the mention button and Send used to stay disabled until the channel list answered — on a host that takes seconds per request, that wait was the whole delay made visible in the one place the user came to type. A channel the cache still has but the server does not is answered with 403/404 and cleared, as before.
- The cached (or linked) conversation's messages are requested in parallel with the channel list instead of after it. The list only confirms that the conversation still exists, so running the two in series cost a second round trip for nothing; when the list lands on the same conversation it no longer spends a second request on it.
- An unanswered first load shows placeholder bubbles rather than "No messages yet". An empty pane under a request in flight is a claim the client cannot make, and in the assistant conversation it also offered the starter chips over a history the user had not seen yet.
- Asking the assistant shows it working immediately instead of at the next poll, and that poll is brought forward to a second after the send is confirmed. The indicator is optimistic: a failed send clears it, and the next `agent_busy` corrects it either way.
- Opening the panel, and picking a conversation, puts the cursor in the composer.

## 0.27.0 — 2026-09-22

**Tillagt**

- A conversation menu (⋯) in the chat header: Room info, Open as drawer/window, Copy link to conversation, and Clear conversation. The padlock that used to sit in the header is gone — it opened Room info, which no padlock has ever meant.
- `DELETE /channels/{id}/messages` empties a conversation, and only the caller's own assistant conversation: a DM or a room holds what colleagues wrote, and no menu item deletes that. Refused with 409 while a job for that channel is queued or running, since the answer would land in a conversation the user just emptied. Mention rows and read markers go with the messages, so the next answer is not born already read.

**Ändrat**

- The surface toggle moved from its own header icon into the menu, where its label says which surface it switches to.

## 0.26.0 — 2026-09-22

**Ändrat**

- The name picker paints the people it already knows, filtered locally, and refreshes behind that instead of showing "Loading…" until the server answers. The list is fetched once when the panel opens and kept in the same per-user cache entry as the conversations — names only, never anything about who may see what.
- Picking someone you already have a direct message with opens it immediately, with no request at all. Only a genuinely new conversation waits for the server.
- The picker fills the panel instead of a 220px window inside it.
- A click outside the drawer closes it, the way other wp-admin overlays behave. Page mode is a screen, not an overlay, so it is exempt, and the launcher is excluded because it toggles the panel itself.

## 0.25.1 — 2026-09-22

**Ändrat**

- The plugin header names Viktor Borg as the author, with `https://github.com/vigg3` as the author URI. Regenerated catalogs carry the same name.

## 0.25.0 — 2026-09-22

**Ändrat**

- The settings screen follows the same shape as Customer Hub's: a sticky bar that always carries Save and says whether there are unsaved changes, a sectioned left column, and one card at a time instead of four headings down a long page. Form rows are label/control pairs in a 220px grid rather than a WordPress table.
- A new Overview section opens first and answers what an administrator checks before reading any field: whether a provider key is stored (and whether it comes from `wp-config.php`), which model is entered, and what the daily budget is. "Getting started" moved into it.
- Section links switch cards instead of jumping down the page, and the open section is kept in the URL hash so a bookmark or a browser back step returns to it. With no hash the server decides which card opens: the Provider card after a connection test, so its result is on screen rather than one click away, and Overview otherwise.

## 0.24.0 — 2026-09-22

**Ändrat**

- The "What is sent" help tab says what tool results now contain, and that the conversation itself is still sent as colleagues wrote it.

**Säkerhet**

- No tool result carries personal data to the provider any more. `BBL_Tools::run()` scrubs every result through the new `BBL_Redact`, before the transient is filled, so a cached entry is clean too and an integration cannot forget the step. Removed keys cover names, e-mail addresses, phone numbers, street addresses, postcodes, cities, personal numbers and IP addresses, at any depth and whatever their case; an e-mail address written inside free text is masked in place. Filter `bbl_redacted_keys` lets an integration add keys of its own.
- `order_lookup` no longer reads the customer's name or e-mail at all. It returns whether the order is a guest order; staff have the customer on screen in wp-admin next to the answer.

## 0.23.0 — 2026-09-22

**Tillagt**

- Integration registry: a plugin that adds agent tools through `bbl_agent_tools` declares itself through the new `bbl_integrations` filter with a name, version, the tools it registers and a note about what they read. Settings → Assistant → Integrations lists them with a checkbox each, so an administrator can see what a plugin offers the assistant and switch it off without deactivating the plugin.
- Setting `enabled_integrations`. A newly installed integration starts **off**: activating a plugin must not change what leaves the store for the provider until someone has seen what it does and said yes.
- Tools that arrive through the filter without a declaration are grouped under "Tools from an undeclared plugin" — visible and switchable rather than silently trusted or silently dropped.

**Ändrat**

- `BBL_Tools::resolve_tools()` drops a filter-registered tool whose integration is not enabled, so the model is never offered it and `run()` refuses it as an unknown tool. Tools registered in core are unaffected.
- The "What is sent" help tab says that an enabled integration's results go to the provider like every other tool result.

## 0.22.0 — 2026-09-22

**Ändrat**

- Schema 7: `bbl_jobs.message_id` is unique, so two workers retrying the same question cannot both create a job and answer twice. `BBL_Install::DB_VERSION` is 7; `maybe_upgrade()` adds the index through dbDelta.
- The SQLite `wpdb` double returns an empty result and sets `last_error` when a select fails, the way `wpdb` does, instead of throwing. A missing table used to throw in tests while production returned null, which let a test prove something production does not do.

## 0.21.0 — 2026-09-22

**Tillagt**

- The channel says what the agent is doing while it works: `GET /channels/{id}/messages` returns `agent_status` with the tool now running and a label already translated for display ("Searching the chat…"). The label is built server-side from the tool name and its validated arguments, never from model text, and is always null when `agent_busy` is false, so a status left behind by a crashed job is never shown under a finished answer. The value lives in a two-minute transient rather than a `bbl_jobs` column: it is worthless once the job ends. Filter `bbl_tool_status_label` lets an integration label its own tools.
- An answer records what it looked at: `meta.sources` holds up to twelve items a tool actually returned, each with its `source_id`, a label and a URL built server-side. The assistant is asked to cite a claim with `[[s:<id>]]`. The client renders those markers in the next release, and only for ids present in `meta.sources` — a reference the model invents has nothing to render.

**Ändrat**

- The default system prompt carries the citation instruction. A customized prompt is untouched; an untouched one is not stored and follows the new wording.
- `tests/test-i18n.php` no longer freezes the sv_SE default prompt to the 0.5.0 wording. That invariant kept a Swedish site on the text it read before the prompt became translatable, but it also meant no new instruction could ever reach one. It is replaced by the requirement that the translation keeps `calculate`, `analytics_report`, `source_id` and the `[[s:` marker syntax untranslated — a translated marker would make every citation unrenderable.

## 0.20.0 — 2026-09-22

**Tillagt**

- The agent can read the store's own published documents through two read-only tools: `list_documents` (what is readable) and `read_document` (one page as plain text). The readable set is WooCommerce's terms page, the site's privacy policy page and the pages an administrator picks under Settings → Assistant → Readable pages — nothing else. Drafts, password-protected pages, posts and unknown slugs are refused with one wording that does not reveal whether the page exists.
- Setting `context_pages`: up to 20 published pages the assistant may quote, such as shipping, returns and warranty. Ids are validated when saved and again on every read, so a page that is unpublished or protected afterwards stops being quotable at once.
- Each read carries a `source_id` (`doc:<slug>`) and the page's permalink, built server-side — the anchor the reference chips in a later release attach to.

**Ändrat**

- Document text comes from the stored content with shortcodes and markup stripped; shortcodes and dynamic blocks are never executed, since that would run third-party callbacks inside an agent job outside any page request. A page whose text lives only in a dynamic block therefore reads as empty rather than appearing complete.

## 0.19.0 — 2026-09-22

**Tillagt**

- The agent can read the chat itself when a question calls for it, through three read-only tools: `list_conversations` (the conversations you can see), `search_chat` (text search, default the last 90 days) and `read_conversation` (a window of messages, paged with `before`). Every read is scoped to the person asking — `BBL_Repository::visible_channels()` and `channel_for_user()` decide, so the agent never returns a private room, a DM or another user's assistant conversation the asker could not already open. A channel id the asker cannot see is refused with the same wording as one that does not exist, so the tools cannot be used to probe which conversations exist.
- Every match carries a `source_id` (`msg:<channel_id>:<message_id>`), the anchor the reference chips in a later release attach to.

**Ändrat**

- A tool callback now receives the asking user id as its second argument: `fn( array $args, int $user_id )`. Existing callbacks that only read shop data ignore it. A user-scoped tool must take it from there and never call `get_current_user_id()`, because the agent job runs in a background request with no user set.

## 0.18.1 — 2026-09-21

**Tillagt**

- A WP-CLI migration rehearsal (`tests/migration-rehearsal.php`) that exercises the schema 4 to 6 upgrade, the request-id constraint and the legacy rename against a real WordPress and MySQL, on an isolated table prefix. Every option the migration reads or deletes is snapshotted and put back, since option names carry no table prefix.

**Ändrat**

- Settings screen: help text and the system prompt are capped at a readable line length instead of spanning the full admin width; the section links read as navigation; "Getting started" collapses once a provider key is stored; the provider panel no longer repeats the card's numbered steps or the "in use" status the tab badge already shows; both price fields share one note that says the provider quotes US dollars while the labels carry the store currency; and the test button says it saves everything.

**Åtgärdat**

- The trailing read marker posts the newest message on screen instead of deferring itself indefinitely, so the last message of a burst no longer stays unread.
- The first message page is authoritative for the ids it covers: a locally cached message the server no longer returns is dropped instead of staying on screen. Older loaded history is kept, and a send confirmed while the request was in flight is never dropped. The poll cursor is the newest id the response covers: it moves down when a stale cached id is dropped, and it does not jump to a send confirmed mid-request, so nothing another author wrote below that send is skipped.
- After a dropped request an open conversation retries after eight seconds instead of sixteen.
- Inbox and divider dates follow the site language instead of the browser's: the client now receives the locale the interface is translated against and passes it to `toLocaleDateString`. A Swedish site in an English browser showed `9/18/2026`. A WordPress locale that is not a valid BCP 47 tag, such as `pt_PT_ao90`, falls back to the browser's format instead of throwing inside the inbox render.
- Swedish: `assistantsvar` corrected to `assistentsvar` in the new-group screen and the shared-answer label.
- The unread count on the floating launcher has its red background again. The launcher sits outside the themed chat root, where the colour variable it referenced is not defined, so the number was white on transparent.
- Browser test cases destroy their apps and remove their fixtures even when they fail, so one failure no longer makes later cases fail for the wrong reason.

## 0.18.0 — 2026-09-21

**Tillagt**

- The floating launcher shows a separate unread-mention count, alongside ordinary unread messages, with accessible labels. Confirmed reads clear both immediately.
- Background tabs share short-lived unread counts through a site/user-scoped lease; no conversation content is shared. Closed visible chat checks notifications every ten seconds and on startup. Older in-flight snapshots cannot replace newer shared counts.

**Ändrat**

- Conversation aggregates reuse persistent object cache when available. Fresh visibility checks, live message ids and read markers determine each cache key; role and membership checks remain uncached. Without persistent object cache, the existing database path is retained.

## 0.17.0 — 2026-09-21

**Tillagt**

- Session-scoped chat recovery after admin-page navigation, including drafts, selected mentions, reading position and explicit retry of uncertain sends. Recovery retains the most recently used conversations, including revisited chats after the 20-conversation limit.
- Durable per-user/per-channel request identity prevents duplicate sends after the former 15-minute retry window. Schema version 6 adds a nullable request id and a unique index without changing existing messages.
- Backward message pagination for loading older conversation history.

**Ändrat**

- Recently visited conversations render from a bounded memory cache before server revalidation.
- Conversation-list requests are coordinated and unchanged rows avoid unnecessary redraws. Idle polling backs off without overlapping requests. Opening the chat or returning to the tab during an active request queues an immediate refresh.
- Conversation previews fetch bounded text instead of complete message bodies and metadata.

## 0.16.0 — 2026-09-21

**Tillagt**

- Colleague and assistant mention picker, personal unread-mention badges, and explicit private-group creation from a personal assistant conversation. Only the composed message and an explicitly selected answer are shared; existing private history is never merged.
- Delegated access remains effective when capability filters use custom names, while external WordPress and WooCommerce capabilities are never granted.
- Site-scoped Babbla ownership and delegation. The installer owns a fresh single-site installation; existing or unattended installations use explicit owner setup. Owners assign administrators and chat users. Administrators manage settings and usage without delegation rights or additional private-conversation access.
- Navigable settings sections and an access page with user search. Delegation preserves existing grants by default, and colleague search includes directly granted chat roles. Store-data tools retain separate permissions when chat access is delegated.

**Ändrat**

- Chat lists and headers state their audience. Shared rooms keep a visible warning about shared assistant answers and requester-authorized store data. Nonmember administrators can no longer invite each other into private rooms.
- Mobile chat uses one pane with a back action. User pickers search the server, show retry actions and use accessible multi-selection controls. Assistant activity is shown in shared rooms too.
- Mention recipients and room creation are stored transactionally; room creation retries reuse the same room. Schema version 5 adds an indexed mention table while retaining existing conversations.

**Åtgärdat**

- Drafts and send responses remain scoped to their channel. Initial snapshots and concurrent messages merge in order without hiding replies or showing them in another chat. Failed read acknowledgements remain retryable.
- Queued assistant jobs recheck the original actor's access and use history ending at the triggering question. Later questions cannot replace the queued request's context.
- Private member lists require current membership even for administrators. User search accepts partial WordPress query rows without hiding eligible colleagues.
- Membership database failures return an error and roll back partial changes. Private-channel listing fetches memberships once instead of once per room.

## 0.15.0 — 2026-09-21

**Tillagt**

- **Swedish.** The plugin now loads its text domain and ships a complete `sv_SE` catalog: the chat client, the settings, usage and diagnostics screens, the system messages the assistant writes into a conversation, and the provider errors it reports. A site running any other locale keeps the English source strings.
- `languages/babbla.pot` for translators, plus `bin/make-pot.sh`, which regenerates it, merges the new strings into every shipped `.po`, and compiles the `.mo` and `.l10n.php` catalogs WordPress reads. Header updates normalize wrapped fields and only touch the catalog header; malformed headers and invalid printf translations stop generation.
- A test over the translation surface: every client string the JavaScript asks for is localized in PHP with the same English text (a missing key would silently show the fallback), every source string reaches `babbla.pot` (including echoing helpers, contexts and plurals), and every Swedish entry and plural form is translated and present in both compiled catalogs. Client translation calls require literal arguments, enforced by lint; `composer test` also runs the catalog-generator regression.

**Ändrat**

- Persisted assistant notices and errors use the configured site language during both enqueue and job execution, with the caller language restored afterward. Background dispatch no longer changes the language stored in a conversation.
- **The default system prompt is translatable**, and asks the assistant to answer in the language of the site instead of always in Swedish. On a Swedish site the prompt is the same text as before; on an English one the assistant now answers in English rather than Swedish under an English interface. The configured site locale (the `WPLANG` option, with WordPress fallbacks) takes precedence over the administrator's profile language, and saving an unchanged default keeps it following future locale and wording changes.
- An installation still running the 0.5.0–0.14.0 default prompt untouched picks up the new one on upgrade, the same rule that has applied to the 0.3.0 default since 0.5.0. A prompt the admin edited is never touched.
- Provider errors that reach the user ("Could not reach the OpenAI API.") are translatable. Two kinds stay English on purpose: a tool's argument errors, which only the model reads, and the provider's own HTTP error text, which the connection test shows verbatim because it comes from the API.
- A cleared system prompt falls back to the shipped one when the settings are saved. Empty prompts stored by older versions also fall back immediately on read, without requiring another save. An empty prompt still reached the provider, only with none of the instructions that keep an answer to tool data — a worse state than the default, and one nothing on the page announced.
- The release zip no longer carries the `.po`/`.pot` sources — WordPress loads the compiled catalogs, so those are development files.

## 0.14.0 — 2026-09-21

**Tillagt**

- **The assistant can answer customer questions.** `analytics_report` gains three metrics — `customers` (distinct customers with a paid order in the period), `new_customers` and `returning_customers` (split by WooCommerce's own `returning_customer` flag, so the numbers match its Analytics screens) — and a `customer` breakdown that returns one row per customer with their id and name, for "who buys the most".
- New and returning are decided per customer, not per order: someone who first orders during the period is new once, even if they order again within it, and returning is everyone else — so new plus returning always equals customers, including rows whose flag WooCommerce never set.
- An order with no customer record at all is left out of the customer counts and out of a per-customer breakdown, rather than becoming a customer named nobody.
- The tool's description lists the customer metrics among the order-level ones that cannot be split by product or category, so "how many customers bought product X" is not a question the model is steered into asking and having refused.
- The tool's note now states what a customer count includes: guests count, because WooCommerce keeps a customer record per guest email, so one person who ordered under two emails counts twice.

**Ändrat**

- Customer names come from `wc_customer_lookup`, the table Analytics maintains — no `wp_users`, no `usermeta`, no free-form SQL, and the same 50-row cap and paid-status predicate as every other breakdown. A customer breakdown is order-level, so it is refused together with a product or category split for the same reason the other order totals are.

## 0.13.0 — 2026-09-21

**Tillagt**

- **The assistant's answers are rendered.** It replies in Markdown, and the chat showed the markers verbatim ("**4 st**"). Headings, bullet and numbered lists, paragraphs, line breaks, bold, italic and inline code are now rendered as elements. Only the assistant's own messages are parsed — a colleague who types `3*4*5` means `3*4*5`.
- **A last-view cache** in `localStorage`, per user: the conversation you left is on screen at the first paint instead of the inbox → an empty conversation → the messages, which is three states for two round trips. The network result then replaces it in place.

**Ändrat**

- The view follows the agent while it is still working: the typing indicator now scrolls the conversation the way an arriving message does, instead of catching up only once the answer lands.
- The message list, the channel list and the overlay panels have a thin, dimmed scrollbar of their own instead of the platform slab, in both themes.
- An underscore only opens emphasis between word boundaries, so `net_total` and `wc_order_stats` survive the renderer intact, and a body over 20 000 characters is shown as it came rather than parsed.
- A message the server has confirmed is cached as soon as it is stored, not at the next poll, so a reload in between cannot come back without it.
- A conversation restored from the cache keeps its own poll cursor, so a refresh that fails does not make the next poll re-fetch the history and append it under what is already on screen. A failed refresh over cached messages says so, rather than letting them read as current.

**Säkerhet**

- Nothing is cached when the client has no user id: a key that cannot name its owner would be shared between whoever uses that browser.
- The Markdown renderer builds DOM nodes and never assigns HTML, so a message body cannot introduce markup. A tag typed into a message stays visible text, exactly as before.
- The cache key names the site as well as the user: a subdirectory multisite serves every site from one browser origin and a network user keeps the same id across them, so the key alone decided whether site A's conversation was painted over site B. Pruning leaves another site's entry alone.
- The cache is bounded on every axis: one key per user and site (other users' and older versions' keys are removed on boot), 24 hours, the 30 most recent messages of one conversation, and a 48 KB ceiling above which the messages are dropped and then the whole entry. Leaving a conversation clears its messages from the cache, and a conversation that no longer exists falls back to the inbox.

## 0.12.0 — 2026-09-21

**Tillagt**

- **Babbla → Diagnostics** (chat administrators only): a status panel that answers why a question went unanswered — whether a key is set and where it comes from, which of the three routes runs a job on this host, how many jobs have been waiting over five minutes, the last failure with its job id and error, and whether the log is on — plus a reader for the log itself and a button that deletes every log file.
- **A "Save and test connection" button** in each provider panel on the settings screen. It saves the form, then asks that provider one throwaway question with the key and model just entered, and reports either the model that answered and how long it took, or the provider's own error text verbatim. The test sends no tools, so it isolates "can this site reach this provider at all" from what the assistant does with tools.
- **A diagnostic log** (`BBL_Log`): the job's whole path — queued, dispatched (with the route), started, the provider request and its outcome, the answer or the failure — plus refusals (no key, missing capability, rate limit, budget) and settings changes. Level is `Off / Errors only / Errors and warnings / Everything the agent does / Debug` on the settings page, default *Errors only*.
- Log files are day-rotated under `wp-content/uploads/babbla-logs-<random>/`, closed off with an `.htaccess` and an `index.html` and named unguessably, deleted after 14 days by a daily cron event, and removed with the rest of the data when **Purge data on uninstall** is ticked.
- `BBL_Repository::stuck_jobs()` and `BBL_Agent_Runner::dispatch_method()`, both read by the status panel.

**Ändrat**

- Neither adapter sends an empty `tools` array any more: with no tools the key comes off the request entirely, which is what the connection test needs and what both APIs expect.

**Åtgärdat**

- The log reader read the whole day file into memory before showing 200 lines of it; at debug level on a busy site that is the file most likely to exceed an admin request's memory limit. It reads backwards from the end in 8 KB steps, capped at 1 MB.
- `stuck_jobs()` aged a running job from when it was asked rather than from when it was claimed, so a question that queued for a while and is being answered right now was reported as stuck. A running job is now judged on `started_at`, a queued one on `created_at`.
- Testing the provider that is not in use left its result inside a hidden tab: the redirect after a test opens the tested provider's panel (`view=`), without changing which provider the assistant uses.

**Säkerhet**

- Nothing from a conversation reaches the log: messages, tool results and channel names are never logged, a context value that is not a scalar is written as its size instead of its content, and anything key-shaped (an `sk-` key, a `Bearer` token, the configured key itself — whether it is stored in the database or defined in `wp-config.php`) is redacted from both the message and the context before the line is written.

## 0.11.0 — 2026-09-21

**Tillagt**

- **Setup help on the settings screen.** A "Getting started" card states the four steps from an empty install to a working assistant, each provider tab carries the link to that provider's API key page, the prefix its keys start with, and example model ids, and the price fields say where the two numbers come from.
- **A Help panel** (WordPress's own, top right of the settings screen) with four tabs: API keys (including the `wp-config.php` constants, which never reach the database), Models (the field takes any id the provider documents, so a model released after this build works), Cost and limits (what the budget, rate limit, prices and cache TTL actually do), and What is sent (which data leaves the site, and that staff-to-staff chat never does).

**Åtgärdat**

- **The assistant could not answer at all on a reasoning model.** OpenAI's `/v1/chat/completions` refuses to combine function tools with the reasoning effort such a model applies by default, and the error reached the channel as "Function tools with reasoning_effort are not supported for …". The adapter now retries that one error once with `reasoning_effort: none`, which is the API's own advice; the parameter is never sent up front, because a model that does not know it would reject the request.
- The provider is no longer decided by which tab is open: the tabs are navigation (`bbl_view`, ignored when saving) and each panel carries an explicit "Use <provider> for the assistant" radio, with an *In use* badge on the provider actually in use. Looking at the other provider's key no longer switches the assistant over on save, and a form that arrives without the field keeps the stored provider instead of resetting to Anthropic.
- Number and price fields were `small-text` (50px), which clipped a seven-digit token budget and a four-decimal price; they now have a width that fits the values they accept.
- The provider tabs' focus ring was a `box-shadow`, which forced-colors mode drops — a keyboard user in Windows High Contrast saw no focus at all. There is an `outline` for that mode, and the active tab's bottom border works there too.
- The radios are wrapped in a `fieldset` with a screen-reader `legend`, so the two options are announced as the Provider group rather than as two loose radios.
- The active tab carries `nav-tab-active` in the markup, not only through a CSS sibling rule.
- The active tab's `border-bottom-color` was a no-op: WordPress sets `border-bottom: none` on `.nav-tab`, so the colour applied to a zero-width border.

## 0.10.0 — 2026-09-21

**Ändrat**

- The settings screen is grouped into **Provider**, **Assistant**, **Limits and cost** and **Data** instead of one twelve-row table.
- The provider is picked with **tabs** (Anthropic / OpenAI) instead of a dropdown, and each tab's panel holds that provider's API key and model, so only the provider in use is on screen. The tabs are two radio inputs named `provider` styled as WordPress nav tabs, with the panels revealed by a CSS sibling rule — the visible tab *is* the stored setting, it works without JavaScript, and nothing has to be kept in sync.
- Number and price fields use WordPress's `small-text` width, price labels carry the store's currency code, and the system prompt is a taller monospace field.

**Åtgärdat**

- The top-level menu no longer shows a duplicate **Babbla** entry above Chat: `add_menu_page()` mirrors the root as a submenu item, and with no page behind the root that entry was dead.
- The "Remove key" checkbox only appears when a key is actually stored, and the key field's state line ("Not set." / "Saved (…abcd)") is tied to the field with `aria-describedby`.

## 0.9.0 — 2026-09-21

**Tillagt**

- **Agent usage page** (`Babbla → Usage`, `BBL_Usage`, chat-admin capability only): a period selector (1/7/30 days), a summary line (questions, tokens, failures, estimated cost), a per-user table (questions, failures, tokens in/out, average answer time, cost), a 30-day tokens-per-day bar graph marking days at or over `daily_token_budget`, and the 50 most recent jobs with status, tokens and error text. Job metadata only: the page never reads a message body or a channel name, so the chat admin still cannot see other people's conversations.
- `BBL_Repository::usage_by_user()`, `usage_by_day()`, `usage_totals()` and `recent_jobs()`. All four filter on `finished_at` (the indexed column, `KEY finished`), so an unfinished job is never counted; per-user rows are capped at 50, days at 400 and recent jobs at 50. The summary has its own total query rather than summing the capped list.
- Two settings, `price_in` and `price_out` — the price per one million tokens in the store's currency. Both default to 0, which hides every cost figure. A comma is accepted as the decimal separator.

**Ändrat**

- Babbla is its own top-level admin menu (`dashicons-format-chat`, just below WooCommerce) instead of three entries under WooCommerce: **Babbla → Chat / Usage / Settings**. The root has no page of its own, so WordPress opens the first submenu the user may see, and it is registered with whichever tier the user holds — a chat admin without chat access still reaches Usage and Settings. Page slugs (`bbl-chat`, `bbl-usage`, `bbl-settings`) are unchanged, so existing links still work.
- `tests/support/sqlite-wpdb.php` rewrites `TIMESTAMPDIFF(SECOND, …)` into SQLite's `strftime` arithmetic, and the rewrite now runs for reads as well as writes.

**Åtgärdat**

- `usage_by_day()` kept the oldest days when its LIMIT bit, so today's bar read zero on a full 30-day window; it now takes the newest days and hands them back oldest-first.
- The per-user average answer time counted failed jobs, letting one timeout dominate the column; it averages answered jobs only.
- The price field's `step` allowed two decimals while the setting stores four, so a sub-cent price could not be submitted.
- The estimated cost follows `woocommerce_currency_pos` instead of always suffixing the symbol.

## 0.8.0 — 2026-09-21

**Tillagt**

- `BBL_Install::migrate_from_wctc()` (`DB_VERSION` 4): an existing 0.7.0 install is moved in place — `RENAME TABLE wp_wctc_* TO wp_bbl_*` for all five tables, then `wctc_settings`, `wctc_purge_on_uninstall` and `wctc_db_version` copied to their `bbl_` keys and deleted. It runs from both entry points: `activate()` (the rename changes the plugin's file and folder, so WordPress installs Babbla as a new plugin and fires activation before `maybe_upgrade()` ever runs) and `maybe_upgrade()`. An empty table already sitting under the new name is dropped so the rows can still move; two populated tables are left untouched for a human to resolve. A failed `RENAME TABLE` or such a conflict makes the migration report failure: the schema is still created so the plugin runs, but `DB_VERSION` is left unrecorded and the next request retries instead of declaring the site migrated. A value already stored under the new option key is not overwritten, and `wctc_t_*`/`wctc_rl_*` transients are left to expire. Public, so a failed upgrade can be re-run with `wp eval 'BBL_Install::migrate_from_wctc();'`. Table existence is compared case-insensitively, since MySQL with `lower_case_table_names=1` answers in lower case whatever case `$table_prefix` uses.
- `uninstall.php` drops the legacy `wctc_` tables and options too, and reads the purge flag from `wctc_settings`/`wctc_purge_on_uninstall` when the site was uninstalled before the upgrade ran.

**Ändrat**

- **The plugin is renamed to Babbla** ("Babbla for WooCommerce"). Every prefix moves with it: classes `WCTC_*` → `BBL_*`, functions, hooks, options and table names `wctc_*` → `bbl_*`, constants `WCTC_VERSION`/`WCTC_ANTHROPIC_KEY`/`WCTC_OPENAI_KEY` → `BBL_*`, text domain `wc-team-chat` → `babbla`, main file `wc-team-chat.php` → `babbla.php`, `includes/class-wctc-*.php` → `class-bbl-*.php`, `assets/{js,css}/wctc-chat.*` → `bbl-chat.*`, CSS classes `.wctc-*` → `.bbl-*`, the JS globals `wctcConfig`/`window.WCTC` → `bblConfig`/`window.BBL`, and the per-user UI preference key `wctc.ui.<id>` → `bbl.ui.<id>` (a user's remembered surface and theme reset once).
- The REST namespace is `babbla/v1` (was `wctc/v1`) and every error code is `bbl_*` (was `wctc_*`); `docs/rest-contract.md` is bumped to v0.3. No route, payload or field changed.
- No `wctc_` alias is kept: the old hooks, options and namespace are gone, since nothing outside this workspace consumes them before 1.0.

## 0.7.0 — 2026-09-21

**Tillagt**

- New chat UI (`assets/js/wctc-chat.js`, `assets/css/wctc-chat.css`): a floating launcher bottom-left opens a compact chat window (inbox → conversation); the admin bar node opens a two-pane drawer that overlays the page without moving it; the Team chat page keeps a two-pane layout. Messenger-style grouped bubbles (same author within 5 minutes, avatar on the last bubble), time dividers after 15 minutes, sending/sent/failed/too-long states, an agent typing indicator, starter suggestions in the empty agent channel, a light/dark/system theme scoped to the chat root, and room creation and management (rename, members, leave, archive/restore).

**Ändrat**

- The dock/undock drawer mode is removed; the drawer no longer adds a body class or pushes page content.

**Åtgärdat**

- The window surface always opens on the inbox, and the drawer/page auto-selects the channel the inbox actually lists first (sorted by `last_message`), not the first channel in fetch order.
- A background poll no longer marks the open channel read unless the conversation is the view on screen: the window surface can sit on the inbox, and any surface can cover the conversation with an overlay (room info, new room, new DM).
- Esc now dismisses an open menu first, then an open overlay (new DM, new room, room info), and only then the surface. An overlay opened on top of another overlay keeps the view underneath as the way back, so Esc and Back can no longer bounce between two overlays.
- Room mutations (create, rename, add/remove member, leave, archive) report the server's error: an invalid member, a bad name, a forbidden action or a 429, instead of failing silently.
- Room info fills the member list and its actions in place once membership resolves, so "Leave room" reflects real membership on first open without discarding a half-typed room name, and a failing members request shows an error instead of re-rendering the panel in a loop. "Add people" is only offered to a caller who can manage the room. A failed member refetch after a successful add/remove reports the error and keeps the list it had, instead of blanking it. Leaving a room clears the button and closes the panel.
- The user picker in new DM / new room / add people has a name filter; the members subtitle uses a singular string at one member. `wctc_bad_room` only claims the name is wrong where there is a name field — the REST API also returns it for a bad audience and an empty archive payload.
- The open inbox with no channel selected (the window surface's default) polls the channel list every 8 seconds instead of falling through to the badge-only 30-second `/unread` poll, so a new room or message shows up without selecting a channel first.
- The closed floating surface marks only its own root `inert`; it no longer sets `inert` on the parent node (the page body in window/drawer mode).
- `destroy()` now also removes the document-level menu click handler and the `matchMedia` theme/reduced-motion listeners, and the global `* { box-sizing }` rule is scoped to the chat root so the chat stylesheet cannot restyle wp-admin.

## 0.6.0 — 2026-09-18

**Tillagt**

- Chat rooms (`WCTC_Install::DB_VERSION = '3'`): a new `wctc_members` table and `wctc_channels.archived_at`; `uninstall.php` drops `wctc_members` too. Any user with chat access can create a room (`POST /rooms`) with audience `everyone` (dynamic, stored as `type: public`) or `members` (`type: private`, explicit membership). Manage (rename, add/remove members, archive/unarchive) is the room's creator or the chat admin; any member can leave. The chat admin can manage a private room's membership without being able to read its messages, same rule as a DM/agent channel, and is never auto-added. `general` and the `dm`/`agent` types are never manageable through the room routes.
- A caller who can manage a room but not see it (chat admin who is not a member, or a creator who left) gets `last_message: null`, `unread: 0` and `last_id: 0` in every Channel response. `PATCH /rooms/{id}` without `name` or `archived` returns 400 `wctc_bad_room`; a value equal to the current one writes nothing and posts no system message. `last_message.author_name` is `""` for `system` messages.
- `PATCH /rooms/{id}`, `GET/POST /rooms/{id}/members`, `DELETE /rooms/{id}/members/{user_id}`, and `GET /rooms` (chat-admin-only room list, no message text). Every manage action posts a system message ("created", "renamed", "archived"/"restored", "added", "left"/"removed").
- `POST /channels/{id}/messages` into an archived room now returns 409 `wctc_archived`; reads still work. An archived room drops out of `GET /channels` but stays reachable by id.
- `deleted_user` now removes that user's room memberships (`WCTC_Repository::delete_user_memberships()`).
- The Channel object gains `last_message` (author, truncated body, timestamp, `mine`, `kind`, or `null`), `created_by`, `archived` and `can_manage`; `last_message` is fetched in one extra query across every visible channel, never per channel.

**Ändrat**

- `docs/rest-contract.md` bumped to v0.2: rooms, the archived-room 409, and the Channel object's new fields, documented for every route that returns a Channel.
- `@<agent name>` mentions target the agent in `public` and `private` rooms (`WCTC_Agent_Runner::targets_agent()`).
- `PATCH /rooms/{id}`, `POST /rooms/{id}/members` and `DELETE /rooms/{id}/members/{user_id}` share the existing per-user rate limit with posting a message and creating a room.

## 0.5.0 — 2026-09-18

**Tillagt**

- `calculate` tool (`WCTC_Calculator`): a whitelisted expression evaluator with its own tokenizer and recursive-descent parser — never `eval()`/`create_function()`, no identifier outside `sum`/`avg`/`min`/`max`/`round`/`abs`/`pct_change`/`pct`. Numbers, `+ - * / %`, parentheses and unary minus; errors (division by zero, an unknown token/function/bare name, unbalanced parentheses, a malformed or oversized number literal, an over-length expression, a >200-token expression, a non-finite intermediate or final result) are returned as tool errors, never thrown out of the loop. A number token is strictly `\d+(\.\d+)?` (a leading, trailing or repeated `.` is rejected) and `%` scales decimal operands to an exact integer remainder instead of `fmod()`'s binary-float noise.
- `analytics_report` tool (`WCTC_Report_Tool`): a generic, whitelisted sales/refund breakdown over WooCommerce Analytics — 1-4 metrics from `net_sales`, `gross_sales`, `orders`, `items_sold`, `avg_order`, `refunds`, `product_net_revenue`, `product_qty`, optionally grouped by `day`/`week`/`month`/`product`/`category`/`customer_country`/`payment_method` (HPOS only), with `product_ids`/`category_ids`/`customer_country` filters. No free SQL: every fragment is keyed by a fixed whitelisted name and every value goes through `$wpdb->prepare`. An order-level metric (`net_sales`, `gross_sales`, `orders`, `items_sold`, `avg_order`, `refunds`) is always rejected with a product/category breakdown or filter, to avoid double counting; `refunds` is rejected with any filter and only supports no grouping or a date grouping, and (unlike the other metrics) runs its own unbounded `s.parent_id > 0` query, merged back in by group and re-sorted in PHP so a `limit` never drops the group the merge needed. A `category_ids` filter matches via `EXISTS` (never a row-multiplying join) and expands to descendant categories through `wc_category_lookup`; `group_by category` keeps the join instead, so a product in two categories counts in both, same as WooCommerce Analytics.
- `WCTC_Agent`: the tool-calling loop now allows up to 8 rounds (was 5); `WCTC_Agent_Runner` gives it a 90s deadline (was 60s) under a 120s `set_time_limit()` (was 90s), so a question needing several `analytics_report`/`calculate` round trips has room to finish.

**Ändrat**

- The default system prompt now tells the assistant to use `calculate` for every calculation and state which numbers it used, and to reach for `analytics_report` for any question about a period, a breakdown or a comparison — replacing the older "never guess, never work out a number yourself" line from `docs/brief-2026-09-18.md`. An install that never touched the previous default (kept as `WCTC_Settings::LEGACY_DEFAULT_PROMPT`) picks up the new default on upgrade; any prompt actually edited is left as stored.

**Åtgärdat**

- `analytics_report`: the SQL `ORDER BY`/`LIMIT` used to rank by the wrong column whenever `avg_order` was the primary metric (its own alias doesn't exist in SQL — the literal average expression does now) or whenever `refunds` was combined with another metric over a `limit` (each query's own top-N could disagree, silently zeroing out a shown period's refund total or returning the earliest days instead of the largest). `order_by: group_asc` with `group_by: product` is now rejected instead of actually sorting by product id. `order_by: group_asc` with `group_by: category` no longer re-sorts rows by name in PHP after the SQL query already ordered them by `t.name ASC`, which could scramble non-ASCII names under PHP's byte-wise comparison.
- `WCTC_Tools::validate_value()` now enforces a schema property's `maxLength`/`minLength` for every tool, not just its `type`/`enum`/`minimum`/`maximum`.
- `calculate`'s `%` on decimals falls back to `fmod()` (rounded to 6 dp) whenever the integer-scaled exact-remainder path would lose precision — either scaled operand at or above 2^53, or a divisor with more decimals than the 6 dp cap keeps, whose own scaled value rounds to 0 without the divisor being zero (previously misreported as "Division by zero."). A number literal's significant-digit count now ignores leading zeros, so `0.000000000000001` is accepted while a 16-significant-digit literal is still rejected.

## 0.4.0 — 2026-09-18

**Tillagt**

- `WCTC_Tools`: schema-validated, transient-cached tool registry (`register`/`all`/`run`/`resolve_tools`), a `guarded()` server-side statement time limit (MariaDB `max_statement_time` / MySQL `MAX_EXECUTION_TIME`), `analytics_ready()` and `date_range()` (400-day cap, real-calendar-date validated). `run()` also refuses per-call when the asking user doesn't hold the tool's capability, and resolves both core and `wctc_agent_tools` filter tools from the same merged registry as `all()`.
- Core tools (`WCTC_Core_Tools`): `shop_info`, `sales_summary`, `top_products`, `order_lookup` against WooCommerce Analytics (`wc_order_stats`, `wc_order_product_lookup`) and `wc_get_order`, read-only, indexed predicates only, always the paid statuses (no `status_set`/`all_non_cancelled`). `order_lookup` is never cached.
- `WCTC_Agent`: the provider-agnostic tool-calling loop (max 5 rounds, one shared deadline), history → conversation mapping, token/tool accounting. The tool set is scoped to `$config['user_id']` (the asking user from the job row), not inferred from the last human author in history; a tool name outside that set is refused as an `is_error` tool_result without ever running its callback.
- Provider adapters `WCTC_Provider_Anthropic` (Messages API, thinking-safe verbatim replay, byte-faithful `"input":{}` echo) and `WCTC_Provider_OpenAI` (Chat Completions), both `disable_parallel_tool_use`/`parallel_tool_calls: false`. Both allowlist the provider's stop/finish reason so a `max_tokens`/`length` cutoff, a `content_filter`/`refusal` decline, or anything else unrecognised (e.g. `pause_turn`, an empty final text) always errors instead of returning a partial answer; provider error text is scrubbed of API-key-shaped fragments before it's returned.

## 0.3.0 — 2026-09-18

**Tillagt**

- `wctc_jobs` table (`WCTC_Install::DB_VERSION = '2'`), one row per agent question; `uninstall.php` drops it.
- `WCTC_Settings`: the `wctc_settings` option, provider/model/agent-name/system-prompt/capability/rate-limit/budget/cache-TTL settings, `wp-config.php` API key constants taking precedence over the option, and the WooCommerce > Team chat settings admin page.
- `WCTC_Agent_Runner`: targets agent channels and `@<agent name>` mentions in public channels, gates on agent-enabled/capability/rate-limit, queues a job and dispatches it (`fastcgi_finish_request()`, else Action Scheduler, else a signed loopback `POST /jobs/{id}/run`), with a single-UPDATE claim lock (stale after 5 min, max 3 attempts), a daily token budget check, and try/catch/finally so a PHP-level error never leaves a job stuck `running`. Calls `WCTC_Agent` (provided by a separate PR) only through `class_exists()`.
- `wp wctc job run <id>` and `wp wctc job list [--status=<s>]` WP-CLI commands.
- `GET /channels/{id}/messages` now also returns `agent_busy`.

**Ändrat**

- The agent's display name now reads from `WCTC_Settings::get( 'agent_name' )`; the old `wctc_agent_name` option is no longer used.

**Åtgärdat**

- The settings page shows a purge opt-in made through the legacy `wctc_purge_on_uninstall` option as ticked, so saving the page no longer cancels it.
- `uninstall.php` reads the purge flag from `wctc_settings['purge_on_uninstall']` (falling back to the legacy `wctc_purge_on_uninstall` option), instead of an option `WCTC_Settings` never writes.
- The channel never sees a raw Throwable message (which may contain paths or SQL) — only a generic notice with the job id; the raw text still lands in the job row's `error` column. A `WP_Error` from the engine, already user-readable, is still shown as-is.
- A blank agent name falls back to the default "Agent"; the `@<name>` mention regex no longer matches a bare `@` when the stored name is empty.
- `maybe_enqueue()` leaves a system message and queues no job when `insert_job()` fails, instead of dispatching job id `0`.
- `wp wctc job run` now validates its `<id>` argument and errors on a missing or non-numeric one, instead of silently running job `0`.

## 0.2.0 — 2026-09-18

**Tillagt**

- Chat client (`assets/js/wctc-chat.js`, vanilla JS, no jQuery): drawer opened from the admin bar and a full WooCommerce → Chat page, channel list, composer with optimistic send, polling with backoff, unread badge, dock mode, reduced-motion support.
- `WCTC_Admin`: admin bar node, `WooCommerce → Chat` submenu page, and asset/config loading for capable users.
- Browser test `tests/browser/chat-flow.html` driving the client end to end.

## 0.1.0 — 2026-09-18

**Tillagt**

- Plugin skeleton: activation/upgrade hooks, HPOS compatibility declaration, WooCommerce presence check.
- Storage: `wctc_channels`, `wctc_messages`, `wctc_reads` tables (`WCTC_Install`), seeded with a public `general` channel.
- `WCTC_Repository`: channel visibility (public, private per-user agent, DM), message pages, read markers, unread counts, lazy DM and agent channel creation.
- REST API `wctc/v1`: `GET /channels`, `GET /channels/{id}/messages`, `POST /channels/{id}/messages` (idempotent, rate-limited), `POST /channels/{id}/read`, `GET /unread`, `POST /channels/dm`, `GET /users`. Contract documented in `docs/rest-contract.md`.
- `uninstall.php` purge, opt-in via the `wctc_purge_on_uninstall` option.
- `README.md` for GitHub: status, features and roadmap, requirements, access model, privacy, hooks, development gates.
- `composer php:compat` gate (PHPCompatibility, `testVersion 7.4-`) next to PHPStan's PHP 7.4 target.

**Ändrat**

- Message bodies are stored as plain text (valid UTF-8, normalised line breaks, control characters removed) instead of through `sanitize_textarea_field`, which HTML-escaped a lone `<` and dropped `%xx` sequences.
- Read markers are written atomically (`INSERT IGNORE` + conditional `UPDATE`) and the stored value is returned, so concurrent `POST /read` calls no longer collide on the primary key and the marker never moves backwards.
- Idempotency keys are scoped per user and channel; a failed insert returns `500 wctc_store_failed` instead of a `201` with an empty message.
- DM and agent channel lookups read before inserting, so `GET /channels` no longer consumes an `AUTO_INCREMENT` value on every call.

- [Produktsida](https://egenverk.se/plugins/babbla)
- [Dokumentation](https://egenverk.se/plugins/babbla/docs)
- [Atom-flöde](https://egenverk.se/feeds/changelog/babbla.xml)
