# Changelog All notable changes to `@tuteliq/mcp` are documented here. This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). The MCP tool surface — tool names, input schemas, and `structuredContent` shapes — is the public API. Changes to the interactive widgets are user-visible but do not break programmatic callers. ## [Unreleased] ## [4.2.2] - 2026-10-12 ### Changed - **Annotations on 25 tools now say what the tools can do.** A tool annotation is a claim a host acts on: a read-only tool may be called without asking the user. - The 19 detection and analysis tools that take `external_id`, `customer_id`, `metadata` or `incident_moderation_enabled`, and `batch_analyze`, were marked read-only and closed-world. A detection can be stored as an incident and delivered to the account's webhooks, which is what those fields are for. They are now `readOnlyHint: false`, `openWorldHint: true`, `destructiveHint: false`. A host may ask the user to confirm before calling them. - `analyze_voice`, `analyze_image`, `analyze_video` and `analyze_document` can download their input from a `url`. They stay read-only and are now `openWorldHint: true`. The three synthetic-media tools that take a `url` are covered by the first group. - `create_verification_session` takes a `redirect_url`, an address the caller chooses to send the person to afterwards. It was already declared as writing and is now also `openWorldHint: true`. - No tool name, input or result changed. ### Added - `createServer(apiKey, { exclude: ['verification'] })` registers every tool except the three verification-session tools. The default is unchanged: all tools are registered. ## [4.2.1] — 2026-10-12 ### Fixed - **`get_usage_summary` failed on every call** with `Cannot read properties of undefined (reading 'toFixed')`. The tool read a billing-period shape (`messages_used`, `usage_percentage`, ...) that `GET /usage/summary` has never returned; the route returns one day's request counts and the daily and per-minute limits. The tool now renders what the API returns, prints `n/a` for a missing field and never throws, and its description says what it reports. Billing-period totals remain in `get_usage_monthly`. ### Dependencies - `@modelcontextprotocol/sdk` 1.30.1 → 1.32.0. The SDK's upgrade notes for 1.31.0 and 1.32.0 concern its HTTP client transports and OAuth client helpers, which this package does not use. - Lockfile: `proxy-addr` 2.0.8, `ip-address` 10.7.4 and `source-map-js` 1.2.2, clearing the advisories `npm audit` reported against them. ## [4.0.2] — 2026-09-22 ### Fixed - **`detect_grooming` text output now carries the per-message rows**, with two things the structured result does not say (customer report, 2026-09-22): when rows cover fewer messages than were sent, a line states how many and that a message without a row carried no per-message finding, not a zero; and a tactic the API withdrew from a row (`unsupported_flags`) is shown as withdrawn rather than dropped. Text only; `structuredContent` is unchanged. - **`get_usage_quota` says what the daily figure counts.** The per-minute figure counts requests as they start, the daily figure counts completed requests, so a quota read while calls are in flight shows them in the first and not yet in the second. The output line and the tool description now say so. ### Dependencies - zod 4.5.4 → 4.6.5; github/codeql-action 4.38.0 → 4.38.1. `@modelcontextprotocol/ext-apps` 2.0.0 was declined: it moves to the split MCP SDK 2.0 packages (`@modelcontextprotocol/client` and `server` replace `sdk@^1`), a migration of the whole server, not a bump. ## [4.0.1] — 2026-09-21 ### Fixed - **`get_usage_quota` printed `undefined/min`, `[object Object]` and `undefineds`** (customer report, 2026-09-21). The formatter read flat `rate_limit` / `remaining` / `reset_in_seconds` fields that `GET /usage/quota` never returned; the SDK type declared them, so nothing failed to compile. The tool now renders the wire shape: tier, per-minute limit, used and remaining this minute, seconds until the window resets, and remaining today where the tier has a daily limit. Any value the API omits renders as `n/a`, never `undefined`. Output text only; the tool's name, input schema and `structuredContent` are unchanged, hence a patch. **Requires `@tuteliq/sdk` 2.33.0+** (the corrected `UsageQuota` type). `resetsInSeconds` needs the API deployed on or after 2026-09-21 and shows `n/a` on older deployments. ## [4.0.0] — 2026-08-31 ### Removed — BREAKING - **`detect_emotional_distress` has been removed.** Use **`detect_distress_signals`** instead: identical behaviour, identical backing endpoint (`/safety/distress-signals`), and it accepts the multi-turn inputs the alias never did. The alias was deprecated two minor versions ago and its description has carried the removal notice since. It was flagged again in an external review of the connector, which is what prompted this. **This is a tool-surface change, which is the public API — hence the major version.** Only callers invoking the MCP tool by that name are affected; the underlying HTTP route is untouched and HTTP integrations need no change. **Migration:** rename the tool call. Everything else — inputs, `structuredContent` shape, results — is identical. ```diff - detect_emotional_distress({ content, context }) + detect_distress_signals({ content, context }) ``` **Migrating is an upgrade, not just a rename.** The alias could not accept `continuation_token` or `reset_conversation`, so an agent that selected it silently lost conversation-trajectory awareness across turns. `detect_distress_signals` carries it. ## [3.27.0] — 2026-08-28 ### Added - **`detect_unsafe` multi-turn support.** Never had any before — no `context.priorMessages` for a one-shot whole-conversation call, and no `continuation_token`/`reset_conversation` for one-message-at-a-time flows, despite the API's `/unsafe` route supporting `continuation_token` all along and now also `context.prior_messages` (Tuteliq/api#173). `detect_unsafe` now supports both, matching `detect_bullying`: the "Conversation risk"/"Conversation state" sections render the same way, and the tool description explains when to use which. - **`priorMessages` on every fraud tool's context, explicitly documented.** `detect_social_engineering`, `detect_app_fraud`, `detect_romance_scam`, `detect_mule_recruitment`, `detect_gambling_harm`, `detect_coercive_control`, `detect_vulnerability_exploitation`, `detect_radicalisation`, `detect_distress_signals`, `detect_tfgbv` already accepted it silently through an undocumented Zod `.passthrough()` — functionally fine, but undiscoverable: an LLM reading the tool schema had no way to know the field existed. Now named and described like every other parameter. **Requires `@tuteliq/sdk` 2.32.0+.** ### Fixed - **Context reference doc (`resources.ts`) was stale on two counts.** Still named the old `conversation_history` field (now `priorMessages`), and described the wrong shape entirely — `{ sender: string, content: string }` instead of the real `{ role: string, text: string, timestamp?: string }`. ## [3.26.0] — 2026-08-27 ### Added - **`flag_profanity`/`flag_risk_terms` on `detect_bullying`, `detect_unsafe`, and `analyze`.** The API's additive, deterministic word-list flags (never affect severity/risk_score/recommended_action) existed since PR #149, and the SDK gained typed support in `@tuteliq/sdk` 2.27–2.31, but no MCP tool ever exposed them — there was no way to override either flag per-call through MCP, and the resulting `profanity`/`risk_terms` fields, while always present in `structuredContent` when the account default was on, were never rendered in the text summary. Both are now visible in the response text and settable per-call on all three tools. - **`default_flag_profanity`/`default_flag_risk_terms` on `get_detection_settings`/`update_detection_settings`.** These account-level defaults existed on the API since PR #149 but were invisible and unsettable through MCP — `get_detection_settings` never displayed them and `update_detection_settings` had no field for them, so an MCP-only user had no way to turn on (or confirm) profanity/risk-term detection at all. Root cause of a customer report: reconnecting after toggling something in a dashboard did nothing, because MCP could never reach `default_flag_profanity` in the first place. **Requires `@tuteliq/sdk` 2.31.0+.** ## [3.25.2] — 2026-08-25 ### Fixed - **`detect_bullying`/`detect_unsafe` headers no longer contradict the verdict below them.** `is_bullying`/`unsafe` correctly clear for a reporter relaying someone else's real abuse (e.g. a child reporting a threat written on their locker), while `severity`/`recommended_action` correctly stay live — but the H2 header was built from the raw boolean alone, so that case rendered a green "No Bullying Detected"/"Content is Safe" heading directly above a `flag_for_review` verdict. `detect_bullying`, `detect_unsafe`, and the `analyze` multi-endpoint summary now render a third, amber "Flagged for Review" state whenever the content wasn't flagged against the speaker but `recommended_action` says otherwise. `detect_grooming` was already keying its header off the graded `grooming_risk` value rather than a boolean, so it needed no change. ## [3.25.1] — 2026-08-23 ### Fixed - **Harmonized annotation key order across admin tools.** `READ_ONLY`, `ADDITIVE`, and `DESTRUCTIVE` in `src/tools/admin.ts` now declare `readOnlyHint`/`openWorldHint`/`destructiveHint` in the same order as the inline annotation literals elsewhere in the codebase. Key order carries no protocol meaning, so this doesn't change behavior; it was the one objective discrepancy found while investigating an unexplained per-tool approval-gating report (no code in this stack implements approval gating, so that report was very likely client-side) and is harmonized since it's zero-risk. ## [3.25.0] — 2026-08-20 The second half of the connector-review finding. 3.24.0 made the `continuation_token` readable; this release makes the thing the token exists to carry readable too. ### Added - **A "Conversation risk" section in the text block.** The reviewer fed a six-turn bullying escalation and the scores came back 5, 10, 65, 5, 75, 5 — the final "see you tomorrow :)", sent immediately after two flagged messages, reported as a positive social interaction. Every one of those per-message scores was right. `risk_score` scores the message; nothing was scoring the conversation. The API now returns `trajectory_risk` (0-1 for the conversation, anchored on the worst turn seen and decaying only slowly), `trajectory` (`rising`/`stable`/`declining`/`none`) and `severity_series` (per-turn severity, oldest first). `detect_bullying`, `detect_grooming`, `detect_coercive_control`, `detect_vulnerability_exploitation` and `detect_distress_signals` now render all three in the text block an MCP host actually shows, not only in `structuredContent`. The section is built for the case that matters — `risk_score` low, `trajectory_risk` high — which is precisely the case a moderator misreads. It gets its own heading at its own severity, states both numbers side by side (`This message: 10%. This conversation: 74%.`), says outright not to stop at the low one, and draws the per-turn series as an aligned table with a sparkline so the 74% is explainable from the evidence rather than asserted. The sparkline is on an absolute 0-1 scale, never normalised to the series' own peak — normalising would draw a full-height bar for the worst turn of an entirely benign conversation, which is the same mistake in a different place. Nothing is rendered on the first turn of a fresh conversation, where the API omits the fields because they would only restate `risk_score`. The `Risk Score` line picks up a `_(this message only)_` scope note when, and only when, a conversation-level number follows it. - **Tool descriptions describe what the tools now return.** The five conversational tools listed the token and stopped there. They now name `trajectory_risk`, `trajectory` and `severity_series`, say that `risk_score` covers only the content in the current call, and say to act on the higher of the two. ### Fixed - **`---` under the last line of a result rendered as a heading, not a rule.** The `Conversation state` footer and the crisis-support block each began with a single newline followed by `---`. In Markdown that is a setext H2, so the line above — usually the `recommended_action` — was rendered as a large heading wherever either block was appended. Both now open with a blank line. Runs of three or more newlines left by an empty optional section are also collapsed, so a result with no `bullying_type` no longer opens with a gap. ## [3.24.0] — 2026-08-20 Connector-review fixes. Every item below was reproduced through the live connector before being changed. ### Fixed - **`batch_analyze` could not succeed with any input.** The tool sent items shaped `{ type, content, external_id }`; the API requires `{ id, type, data }`. `id` was not in the tool's input schema at all, so Zod stripped it even when a caller supplied one, and every call came back `body/items/0 must have required property 'id'`. `content` vs `data` was a second, independent mismatch. Items now take an optional `id` (generated as `item-N` when omitted) and are mapped to the API's shape by the SDK. `id` and `external_id` remain different things and are both shown in the result: `id` addresses the item inside this batch, `external_id` is your own record's identifier. - **`continuation_token` was returned but invisible.** It was present in `structuredContent` and nowhere in the text block, which is what an MCP host renders — so through a connector the token could not be read, could not be carried into the next call, and conversation-level detection could not be used at all despite being what the tool descriptions promised. Every tool that returns one now ends its text output with a `Conversation state` section carrying the token verbatim in a fenced block, its `state_source`, and its expiry. - **`verdict_only: true` rendered `Rationale: undefined`.** The mode suppresses rationale generation server-side, and the text block interpolated the field regardless. It now falls back to the endpoint's other free-text field (`action_detail`, which `verdict_only` does return) and otherwise says the rationale was not generated and why. - **Crisis helplines were not matched to the harm.** When the API cannot map a detection onto a helpline topic it falls back to returning the first five lines for the country, so a phishing verdict and a weapon threat both arrived carrying a domestic-violence line, a fraud line and a gambling line together. Topical lines that contradict the detected harm are now dropped at render time, in both the text block and the widget. The filter is conservative: only `domesticViolence`, `fraudPrevention` and `gambling` are ever removed, general and crisis lines are always kept, and the list is never emptied. The underlying fallback is an API-side fix. - **Support resources read as if they were local.** Helplines are correctly localised to `context.country`, but the resource links below them are a single global list per category that still leans US (StopBullying.gov, 988). The helpline section is now labelled with the country it belongs to and the resource section is labelled as general rather than country-specific. - **Widget and version paths only resolved from `dist/`.** `package.json` and `dist-ui` were located by hard-coded relative hops counted from the compiled layout, so running the same modules from source — `tsx`, or a test — read a manifest belonging to a different package. Both are now found by walking up to this package's own manifest. ### Added - **`continuation_token` and `reset_conversation` on `detect_coercive_control`, `detect_vulnerability_exploitation` and `detect_distress_signals`.** These endpoints have always issued a token; the tools never accepted one back, so their multi-turn support was unreachable. The inputs are deliberately not added to endpoints that issue no token. - **All twelve `batch_analyze` types.** Previously four (`bullying`, `unsafe`, `emotions`, `grooming`); now also `social_engineering`, `app_fraud`, `romance_scam`, `mule_recruitment`, `gambling_harm`, `coercive_control`, `vulnerability_exploitation` and `radicalisation` — the exact set the batch route accepts. Emotions items may now pass `messages` with a `sender`. - **`context.country` is advertised on the fraud and extended detection tools.** Their `context` was an unlabelled free-form record, so nothing told a caller that `country` existed even though it worked. `language`, `ageGroup`, `platform` and `country` are now named; other keys still pass through. - **A test suite.** `npm test` (vitest) covers the formatters, the helpline-relevance filter, and the registered tool contract — input schemas, descriptions and annotations — read back over an in-memory MCP transport. ### Changed - **`detect_emotional_distress` is retained but unmistakably deprecated.** Removing it would break integrations already calling it, so it stays listed; its title is now `DEPRECATED — use detect_distress_signals`, its description opens with `DEPRECATED ALIAS — DO NOT USE FOR NEW WORK`, and every result it returns is prefixed with a deprecation notice naming the replacement. It will be removed in the next major version. ### Requires - `@tuteliq/sdk` >= 2.24.0. The batch and continuation fixes are implemented there; the dependency range has been raised accordingly. ## [3.23.0] — 2026-08-18 ### Fixed - **Ambiguous file input was resolved silently instead of rejected.** Every file-taking tool accepts `file_path`, `url` and `base64` as three optional strings, of which exactly one is required. Nothing said so, and `resolveFile` returned on the first one it found. A caller supplying two had the others discarded with nothing thrown and nothing warned, so an agent hedging with both `url` and `base64` received a confident analysis of one file. Where the two pointed at different content, the answer described the wrong file. Supplying more than one source is now an error naming what was received. Empty strings count as absent, so a caller filling every field with `""` gets "no source" rather than "ambiguous". Supplying exactly one is unchanged. **Potentially breaking.** A caller that previously sent two sources received a successful response; it now receives an error. That response was describing a file the caller did not choose, so the previous behaviour was not safe to rely on, but the change is visible and is why this is a minor rather than a patch. ### Changed - **The exactly-one constraint is now stated in the schema descriptions.** 28 parameter descriptions and 7 tool descriptions across the media and synthetic tools. "Provide a file_path, url, or base64" read as a menu of things a caller may supply rather than a choice between them. - **`analyze` now says when not to use it.** Its description was "Quick comprehensive safety analysis that checks for both bullying and unsafe content", which gave an agent nothing to choose it by. It now states that `detect_bullying` and `detect_unsafe` return richer per-category detail when the harm is known, and that multi-turn conversations belong to `detect_grooming` or `analyse_multi` because this endpoint scores one message at a time and does not reason across a conversation. ### Notes - This package has no test runner. File-input behaviour is verified by a runnable script, `npm run verify:file-inputs`, rather than a test file that nothing would execute. ## [3.22.2] — 2026-08-13 ### Fixed - **`analyse_multi` accepted any string as an endpoint id.** The schema was `z.array(z.string())` with the description "Detection endpoints to run" and no list of valid values, so a caller had to guess them — and they are hyphenated and deliberately *not* the tool names (`social-engineering`, not `detect_social_engineering`). A wrong guess only surfaced as an API error. It is now a closed enum of the 11 documented endpoints, bounded to the 1-10 the endpoint itself enforces, with the values listed in the description. The tool description also points callers here from `analyze`, which only ever runs bullying and unsafe — analysing a financial scam with `analyze` returns categories from the child-safety taxonomy because no fraud detector was run, which reads as a misclassification and is not one. ## [3.22.1] — 2026-08-13 ### Fixed - **`analyze` showed a restatement of its own chips as the analysis summary.** The card read `rationale || summary`, but `analyze` has no top-level `rationale` — it fans out to the bullying and unsafe detectors and only those sub-calls produce free text, while its top-level `summary` is a terse derived line ("Unsafe content: sexual_exploitation, illegal_activity"). Every `analyze` result therefore displayed the categories back to the reader instead of the reasoning behind them. The summary now prefers real prose wherever it lives: top-level `rationale`, then the nested sub-detector rationales, and `summary` only as a last resort — which is also the correct answer for `verdict_only` responses, since those skip rationale generation entirely. Where more than one detector explains itself, each paragraph is attributed, because two unlabelled paragraphs leave the reader guessing which detector said what. ## [3.22.0] — 2026-08-13 ### Added - **A cleared state for the risk gauge.** At 0% the ring had nothing to draw, so it rendered an empty grey track that read as a component that had failed to load. A cleared result is not a small amount of risk, it is a different kind of answer, so it now gets its own mark: a tinted disc, a tick, and a `0% risk / CLEARED` label. The scored ring is untouched. - **A `DETECTED CATEGORIES` row on detection results**, above the analysis summary and separated by a rule. Chips are mono — these are literal API enum values a moderator may paste into a filter — with a leading dot for a scannable left edge, tinted by the result's severity. The section always renders. An absent section cannot be told apart from one that failed to load, and "none" is a finding worth stating, so a cleared result shows a teal `none` chip. ### Fixed - **`analyze` never showed its categories.** It has no top-level `categories` field — it nests a `bullying` and an `unsafe` result, each carrying its own list under a different name (`bullying_type`, `categories`). The card read only the top level, so the chip row was empty and the finding surfaced only buried in the summary prose. Categories are now collected from every known shape, including the nested ones, and de-duplicated. - **`server.json` had drifted to 3.21.2 while the package was at 3.21.7.** The MCP registry manifest and the published package now carry the same version. ### Changed - The card keeps a 4px brand rule along its top edge in place of the chrome bar, per the updated UI kit. Hosts draw their own header naming the server and tool, so the card no longer repeats it. - The moderation console's reasoning block is titled "Detection reasoning". No vendor or agent name appears anywhere in the widget surface. ## [3.21.7] — 2026-08-12 ### Added - **Per-call incident logging control** — `detect_bullying`, `detect_grooming`, `detect_unsafe` and `analyze_emotions` now accept an optional `incident_moderation_enabled`. It overrides your account-level incident-logging setting for that single call: `true` forces the incident to be persisted, `false` suppresses persistence, and omitting it defers to your account default (which itself defaults to enabled). Passed through to the API via `@tuteliq/sdk` 2.23.0. ## [3.21.1] — 2026-08-07 ### Fixed - **The chrome bar and footer had no left or right padding**, so the Tuteliq mark sat flush against the card's left edge and the `` glyph against the right. Introduced in 3.20.1 along with the responsive gutters: both elements set `padding: '14px 0'` inline, and that shorthand overrides the horizontal padding the gutter class supplies. The card body escaped it only because it uses `paddingTop`/`paddingBottom` longhand. Both now use longhand. Verified at 920px, 750px and 400px: the mark and the `` glyph are inset 23px at full width and 15px on narrow hosts, matching the body gutter. - **The risk gauge's centre text overlapped the ring.** Its overlay was pinned to the full square (`inset: 0`), so "RISK SCORE" ran the whole width of the gauge and crossed the coloured stroke on both sides. The overlay is now clamped to the ring's inner disc, the label has lost its letter-spacing, and both faces scale with the gauge. Measured rather than eyeballed — worst-corner distance from the centre versus the inner clear radius, since the label sits below centre where the circle has already narrowed. Every value from 0% to 100% now clears the ring, worst case 5.4px at "100%", and the label holds 6px of clearance even when the webfont is blocked and it falls back to a system face. - `WidgetShell` gained a `showChrome` prop (default on). MCP hosts draw their own header above the widget iframe naming the server and tool, so both are visible at once; set it to false where that duplication is unwanted. ## [3.21.0] — 2026-08-07 ### Added - **`moderation_queue`** — a moderator triage console, with a paired widget. Reads the unreviewed queue and the next item from the incident store, and renders the calling assistant's own analysis trace, reasoning and recommended decision alongside it for a human to sign off. The tool itself is read-only — it reads the queue and renders a recommendation. Applying a decision is always `review_incident`. The card keeps API-derived facts and assistant-supplied claims visually distinct, and labels the latter "Assistant reasoning" — a moderator signing an escalation needs to know which half is a measurement and which is an argument. `operator_name` is an optional parameter, never defaulted. There is no account- or organisation-name field anywhere in the Tuteliq SDK, so the name cannot be retrieved; supplying it is the caller's job, and omitting it yields an unbranded header rather than an invented one. Queue depth beyond one page is reported as a floor (`47+`) rather than an exact count, since the list endpoint returns a page, not a total. **The decision buttons act.** "Escalate" calls `review_incident` through the host — the moderator clicking the button *is* the human decision, and routing that through copy-and-paste would have added friction without adding oversight. It does not fire on a single click: `review_incident` persists an override and emits a signed Art 12 audit receipt, and it requires a `reason_code`. Defaulting that silently would write a value the moderator never chose into a document that is legal evidence, so the button opens a reason picker and a second click commits. "View full analysis" calls `get_incident`; "Skip to next" re-invokes `moderation_queue` at a higher offset via the new `skip` parameter, mutating nothing. ### Changed - The README now documents the incident and moderation tools, which had never appeared in the tool tables despite shipping since 3.15.3. ## [3.20.1] — 2026-08-07 Layout fixes found by running 3.20.0 in a real MCP host. Presentation only. ### Fixed - **The KPI row on the incidents overview wrapped to 4+1** instead of the five columns it is designed as. It used `auto-fit, minmax(150px, 1fr)`, which fits five at the 920px design width but silently drops to four below ~810px — and hosts render the card narrower than that. Now a fixed five-column grid that collapses only below 700px. - **"Last 24 hours" was painted as volume rather than needs-attention**, which made the card's own legend wrong. An earlier revision coloured the recency tiles by whether they were running above the 30-day average, but the legend declares three fixed meanings (volume, needs attention, recent window) and each tile belongs to exactly one of them. Tile accents are fixed again: | Tile | Accent | |---|---| | Total incidents | `#1D1D33` volume | | Needs review | `#C2543A` needs attention | | Last 24 hours | `#C2543A` needs attention | | Last 7 days | `#19B79A` recent window | | Last 30 days | `#19B79A` recent window | - The analysis panels below the KPI row are now an explicit two-column grid collapsing to one, rather than relying on a min-width that behaved inconsistently across host widths. ## [3.20.0] — 2026-08-06 Complete redesign of the interactive widgets against the Tuteliq MCP UI Kit. No tool schemas changed; every widget looks different. ### Added - **Unified widget shell.** Every widget now renders inside one frame: a dark chrome bar naming the MCP tool that produced the result, a white body, and a footer carrying the data-handling note and a Trust Center link. Previously the detection widgets and the dashboard widgets used two unrelated headers, so two cards in the same transcript looked like two products. - **Design token system** (`ui/src/theme.ts`) — ink/teal/severity palettes, a three-typeface scale (Poppins for headline numbers, Inter for prose, JetBrains Mono for IDs and counts), radii, shadow, and spacing. Replaces ad-hoc colour literals scattered across pages. - **Shared primitives** (`ui/src/components/primitives.tsx`) — `StatTile`, `MetricBar`, `Panel`, `CardHeader`, `SeverityChip`, `StatusPill`, `Tag`, `Callout`, `Legend`, `EmptyState`. - **Responsive layout.** Card gutters narrow from 36px to 18px below 560px, and unbroken tokens (incident IDs, cursors, envelope references) now wrap. Verified with no horizontal overflow at 400px, 600px, and 900px viewports. - **Loading state renders inside the real shell**, so the frame and footer are final from first paint and only the body swaps when the result arrives. - **`npm run preview:ui`** — a local harness that renders every widget against fixture data on the host backdrop. Output is gitignored. - **Relative timestamps** on incident rows ("2h ago") alongside the absolute time. - **Above-trend markers** on the incidents overview, comparing the 24-hour and 7-day counts against the 30-day daily average. ### Changed - **Severity ramp is now monotonic** across all five levels, so two chips can be ranked by colour alone: | Level | Colour | |---|---| | `critical` | `#9C3A29` | | `high` | `#C2543A` | | `medium` | `#D98A3D` | | `low` | `#B7C2D4` | | `safe` / `none` | `#19B79A` | `low` is a cool grey rather than the previous yellow: in a moderation queue, yellow reads as caution and competed with `medium` for attention. - **Status banners no longer animate.** The previous treatment used a full-bleed severity gradient, a translucent shimmer overlay, and an infinite pulse on high and critical results. These widgets report on grooming, self-harm and abuse; a pulsing red box the reader cannot dismiss, on content they may scroll past repeatedly, is the interface performing urgency at them. Severity is now carried by a rule and a glyph on a flat ink panel. A global `prefers-reduced-motion` rule was added alongside. - **Support card rebuilt.** Moved from blue to ink, and the emoji (💙 🚨 📞) were removed — they render inconsistently across hosts and screen readers announce their CLDR names mid-sentence. Phone numbers are now the highest-contrast targets on the card and copy in one click. - **Upsell banner is quieter** — no rocket, no gradient hero. It appears where the user expected an analysis result. - **Brand mark is inline SVG**, replacing an 8KB base64 PNG that was duplicated across two files. - Widgets report their real version to the MCP host. `appInfo.version` is now injected from `package.json` at build time; it had been hardcoded to `3.0.0` since the 3.0 release. ### Fixed - **Widget collapsed to content width** when a host rendered it as a flex or grid child. `margin: 0 auto` with `max-width` and no `width: 100%` makes the auto side margins consume the free space instead of stretching. - **Footer and helpline links now open through the MCP host** on every widget. Nine of the eleven entrypoints did not wrap their page in `AppProvider`, so `openLink` fell through to its clipboard fallback — the link appeared to do nothing when clicked. Previously this only affected two widgets, because only those two rendered any links. - **Trust Center links pointed at paths that don't exist** (`tuteliq.ai/trust/…`, carried over from a design mockup). All widgets now link to `https://trust.tuteliq.ai/`. - **Empty cards in the synthetic-media widgets.** `RationaleCard` and `ActionBadge` had no empty guards, so a tool returning no rationale or no recommended action produced a headed card with no body — indistinguishable from a failed analysis. The score panel likewise rendered a 0% grey gauge and an unlabelled severity pill when those fields were absent. - **Default button border leaked** through on the first Action Plan step. React removes a style declaration set to `undefined` rather than blanking it, so the user-agent's `2px outset` border won. - **`textTransform: capitalize`** on the incident detail banner rendered "Critical Severity". - **Duplicated trend statement** on the emotions widget, which printed "Trend: ↓ Worsening" directly beneath a banner reading "Worsening trend". - Bar charts scale against the largest row rather than the total. With incident distributions as skewed as these, scaling to the total left every row but the first as an unreadable stub. ### Removed - `ui/src/App.tsx` (`AppWrapper`), `ui/src/components/IncidentHeader.tsx`, and `ui/src/components/FooterLinks.tsx` — superseded by `WidgetShell`. ## [3.19.0] — 2026-08 - Updated to `@tuteliq/sdk` 2.21. - Surfaced `action_detail` on detection results. - Fixed the video formatter. - Documentation now leads with OAuth setup; static token and stdio demoted to secondary paths. ## [3.18.0] - Verdict-only fast mode on `detect_grooming` and `detect_bullying` via `verdict_only: true` — returns the verdict without the per-message breakdown. - Consumed `@tuteliq/sdk` 2.16.0. ## [3.17.0] - Policy automation rules, detection settings, and threat intelligence (13 new tools). ## [3.16.0] - Moderation-agent tooling: `batch_analyze`, policy configuration, usage quota. ## [3.15.8] - Bulk triage: `batch_review_incidents` plus a multi-select incidents widget. ## [3.15.7] - BYOK paste-in decryption using Web Crypto, browser-only. ## [3.15.6] - Trajectory curve hero on the incident detail widget. ## [3.15.4] - Interactive widgets for the read-only dashboard tools. ## [3.15.3] - Read-only dashboard tools; `bypass_cache` on the `synthetic_*` tools. ## [3.13.0] - Governance tools: E2E key management, audit receipts, moderator review.