Commit Graph

570 Commits

Author SHA1 Message Date
dependabot[bot]
eb3c6fdc20
chore(deps): bump next-intl from 4.13.1 to 4.13.2
Bumps [next-intl](https://github.com/amannn/next-intl) from 4.13.1 to 4.13.2.
- [Release notes](https://github.com/amannn/next-intl/releases)
- [Changelog](https://github.com/amannn/next-intl/blob/main/CHANGELOG.md)
- [Commits](https://github.com/amannn/next-intl/compare/v4.13.1...v4.13.2)

---
updated-dependencies:
- dependency-name: next-intl
  dependency-version: 4.13.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-13 10:36:42 +00:00
Arnas Donauskas
b867760801
Merge pull request #364 from ArnasDon/fix/duplicate-conversations
fix: stop inbox creating duplicate chats for one contact (#363)
2026-07-10 16:27:12 +03:00
Arnas Donauskas
faf0f740a4
fix: stop inbound chats fragmenting into duplicate conversations (#363)
An inbound message could create a second conversation for a contact
under a race (Meta retries a delivery, or a batch fans out to concurrent
after() runs). There was no UNIQUE index and no unique-violation
backstop on conversations, unlike contacts (migration 022). Worse, once
two conversations existed the `.single()` lookup errored on every later
message and the webhook created yet another conversation each time,
snowballing into a wall of duplicate chats.

- findOrCreateConversation now resolves oldest-first with .limit(1)
  instead of .single(), so pre-existing duplicates converge, and catches
  23505 on insert to re-resolve the winning row.
- Same hardening applied to the public-API resolver (resolve-conversation).
- Migration 036 merges existing duplicate conversations into the oldest
  thread (re-pointing messages, reactions, deals, flow_runs,
  notifications, ai_usage_log — no data loss) then adds a
  UNIQUE (account_id, contact_id) index as the authoritative guarantee.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 12:27:46 +03:00
Arnas Donauskas
137760e8f8
Merge pull request #358 from ArnasDon/feat/whatsapp-interactive-messages
feat: WhatsApp interactive messages (reply buttons + lists)
2026-07-08 19:10:53 +03:00
Arnas Donauskas
78b42dfba9 feat: WhatsApp interactive messages (reply buttons + lists)
Add full support for WhatsApp Cloud API interactive messages across the
inbox, automations, and a new quick-replies feature. Extends the
interactive plumbing that previously existed only for the Flows engine.

Sending
- send-message core handles `interactive` (reply buttons ≤3, list ≤10
  rows) via the existing Meta senders; validated server-side before send.
- Inbox composer gains a "+" menu → builder dialog with live WhatsApp-style
  preview + Meta char limits (button 20, list row 24, body 1024).
- Automation "Send Buttons" / "Send List" steps in the builder.
- Public v1 /messages accepts type=interactive.

Storage / round-trip
- migration 035: messages.interactive_payload JSONB + quick_replies table.
- Outbound interactive persists the structured payload (composer,
  automations, and Flows) so the thread re-renders the buttons/rows.

Receiving
- Bubble renders outbound interactive via the shared preview; inbound taps
  (button_reply/list_reply) keep the "chosen option" affordance.

Automations
- New `interactive_reply` trigger matches on tapped button/list IDs
  (exact), enabling chained multi-step menus.

Quick replies
- New account-scoped quick_replies (text or interactive snippets), managed
  in Settings and insertable from the composer; "Save as quick reply" in
  the builder.

Shared core in src/lib/whatsapp/interactive.ts + src/components/interactive/.
Includes unit tests for the validator, send-param validation, the
interactive_reply trigger match, and the new automation step/trigger
validation. typecheck + lint clean; full suite green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 19:03:15 +03:00
Arnas Donauskas
3eff5b854e
Merge pull request #357 from ArnasDon/fix/mcp-server-json
Correct MCP registry namespace casing (ArnasDon), bump to 0.1.1
2026-07-08 18:06:03 +03:00
Arnas Donauskas
21506cb9d8 Correct registry namespace casing; bump to 0.1.1
The MCP registry namespace is case-sensitive and keyed to the GitHub
login: the owner is `ArnasDon`, not `arnasdon`. Publishing as
io.github.arnasdon/wacrm-mcp is rejected 403. Fix the name in
server.json and the matching mcpName in package.json to
io.github.ArnasDon/wacrm-mcp.

Bump to 0.1.1 because the published 0.1.0 baked in the lowercase
mcpName; the registry validates the published package's mcpName
against server.json, so a fresh npm version is required.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 17:58:20 +03:00
Arnas Donauskas
e23f954221
Merge pull request #356 from ArnasDon/fix/mcp-server-json
Fix MCP registry validation in server.json
2026-07-08 17:49:35 +03:00
Arnas Donauskas
18e4bf4d21 Fix MCP registry validation in server.json
Shorten description to the registry's 100-char limit and migrate the
$schema to the current 2025-12-11 version (2025-10-17 is deprecated).
`mcp-publisher validate` now passes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 17:46:21 +03:00
Arnas Donauskas
9441533458
Merge pull request #355 from ArnasDon/feature/mcp-server
Add MCP server for the public API
2026-07-08 17:34:03 +03:00
Arnas Donauskas
78f2f79d3b Add MCP server for the public API
Introduce mcp-server/ — a stdio Model Context Protocol server that
wraps the /api/v1 public API so MCP clients (Claude, Cursor, etc.) can
drive a self-hosted wacrm instance in natural language.

- Thin wrapper over the existing REST API; all auth, scoping, and rate
  limiting stay server-side. No new backend surface.
- Read-only by default: write and broadcast tools are only registered
  when WACRM_ENABLE_WRITES / WACRM_ENABLE_BROADCASTS are set, layered
  on top of the API key's own scopes. send_broadcast also requires an
  explicit confirm and is marked destructive.
- Registry-ready: server.json manifest, package.json mcpName linked to
  it (io.github.arnasdon/wacrm-mcp), and glama.json for discovery.
- Docs: docs/mcp.md + README bullet + mcp-server/README.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 17:30:24 +03:00
Arnas Donauskas
95f6052e34
Merge pull request #353 from ArnasDon/feat/ai-autoreply-polish
feat(ai): polish auto-reply — inbox controls, real handoff, usage logging
2026-07-08 16:39:17 +03:00
Arnas Donauskas
2311b7ea35 Merge origin/main; internationalize new AI auto-reply UI
Brings in the i18n PR (#348) that landed on main and localizes the new
strings this branch added so nothing regresses the new next-intl
convention:
- ai-config.tsx: resolved import conflict; handoff-target picker strings
  moved to Settings.aiConfig (handoffTo / handoffToDesc / handoffQueue).
- message-bubble.tsx: "AI" badge + tooltip → Inbox.bubble
  (aiBadge / aiBadgeTitle).
- ai-thread-banner.tsx: take-over/resume banner + toasts → new
  Inbox.aiBanner namespace.

The AI Agents section (/agents playground, setup tabs, usage card) is
still English on main, so those strings are left as-is for a future
section-wide i18n pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 16:35:27 +03:00
Arnas Donauskas
eefb6179c2 fix(db): resolve duplicate migration version 031 (rename profiles-RLS fix to 034)
The security PR (#352) shipped 031_fix_profiles_update_rls.sql while
031_ai_reply_slot_grant.sql (#345) already held version 031. Supabase
keys schema_migrations on the numeric prefix, so applying both to a fresh
DB fails with `duplicate key ... (version)=(031)` (SQLSTATE 23505) — which
is what broke the Supabase Preview check on this PR.

Rename the newer, colliding file to 034 to restore unique versions. It's
idempotent and independent of the AI tables, so re-sequencing after 033 is
safe; a prod DB that already applied 031 will apply this fresh as 034
without side effects.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 16:04:39 +03:00
Arnas Donauskas
077d203980
Merge pull request #348 from ivanmercedes/feat/i18n-dashboard-shell
feat(i18n): finalize localization for Dashboard, Inbox, Settings, and Automations
2026-07-08 15:39:18 +03:00
Arnas Donauskas
e132cc7a15 fix(ai): address code-review findings on auto-reply polish
Correctness:
- Resume AI now releases ANY assignment (not just the caller's own).
  A handoff-assigned thread previously stayed assigned after resume, so
  the eligibility gate silently kept the bot muted — Resume was a no-op.
- GET /api/ai/usage: guard days >= 1 (Number(null)===0 is finite, so the
  30-day default was unreachable and the window collapsed to 1 day).
- GET /api/ai/usage: align the query cutoff to the start of the oldest
  charted local day so rows in the oldest partial day no longer count in
  totals while missing from the daily series (totals now match the bars).
- ai-thread-banner: cache account AI-status per accountId and never cache
  a transient fetch failure — fixes cross-account leak and the banner
  staying hidden all session after one failed /api/ai/config call.
- /api/ai/draft: construct the service-role client inside a guard and
  fire-and-forget the usage log, so a missing service-role key (or any
  log failure) can't 500 an already-generated, already-billed draft.

Efficiency:
- Don't await usage logging on the reply hot path (auto-reply + draft);
  logAiUsage swallows its own errors, so fire-and-forget is safe and
  removes a DB round-trip from the customer-facing send.

Reuse / consistency:
- usage route reuses date-utils (daysAgoStart/lastNDayKeys/localDayKey)
  and now buckets by LOCAL day like every other dashboard chart (was UTC).
- extract formatCompactNumber (shared with formatCurrencyShort); drop the
  hand-rolled month table in favour of date-fns; extract
  fetchAccountMembers/memberLabel and use them in the AI settings picker.

Resume still resets the per-conversation reply count (a deliberate,
manual, rate-limited re-engagement — documented in the handler).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 15:35:41 +03:00
Ivan Mercedes
4690c4d929 refactor: address PR feedback for security and cleanups
- refactor: migrate dangerouslySetInnerHTML(t.raw) to t.rich() to prevent HTML injection vectors
- fix: restore per-status wording for flow activation toasts ("Flow activated.", "Archived.", "Saved as draft.")
- chore: remove unused `tComposer` in message-thread.tsx
- chore: remove unused `ROLE_DESCRIPTIONS` in invite-member-dialog.tsx
2026-07-08 07:58:33 -04:00
Arnas Donauskas
c736e7a43f feat(ai): add token-usage dashboard (AI Agents → Usage)
Surfaces the ai_usage_log data added in the previous commit:
- GET /api/ai/usage — admin-only aggregation (totals, per-mode,
  per-model, zero-filled daily series) over a 7/30/90-day window,
  mirroring the ai_usage_log SELECT policy
- AiUsageCard — stat tiles + daily token bar chart + by-model list,
  rendered under a new admin-only "Usage" tab on the AI Agents page

Counts only; no message content is stored or shown.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 14:31:36 +03:00
Arnas Donauskas
b0194dfc27 feat(ai): polish auto-reply — inbox controls, real handoff, usage logging
Makes the AI auto-reply bot visible and controllable from the inbox,
completes the handoff, and records token spend. Builds on the existing
lib/ai engine rather than replacing it.

Track A — user-facing
- messages.ai_generated flag → small "AI" badge on bot-generated bubbles
- "Take over" / "Resume AI" banner per conversation
  (POST /api/ai/autoreply/[id]) — take-over pauses + assigns to the agent
- handoff now assigns to a configurable target (agent or unassigned
  queue) and writes an internal note summarizing the exchange; the
  existing on_conversation_assigned trigger notifies the assignee
- Settings → AI: "Hand off to" target picker

Track B — observability + safety
- providers return normalized token usage; logged per run to ai_usage_log
  (admin-readable) from both auto-reply and draft
- account-wide auto-reply rate limit (30/min) on top of the
  per-conversation cap

Migration: 033_ai_reply_polish.sql (messages.ai_generated,
ai_configs.handoff_agent_id, conversations.ai_handoff_summary,
ai_usage_log). Tests + typecheck + lint pass (607 tests).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 14:03:33 +03:00
Arnas Donauskas
23838a9959
Merge pull request #352 from ArnasDon/fix/ghsa-8jqh-598v-rfxc-webhook-ssrf
fix(security): SSRF guard on automation send_webhook action (GHSA-8jqh-598v-rfxc)
2026-07-08 10:09:23 +03:00
Arnas Donauskas
7d1ddbfdb8 fix(security): SSRF guard on automation send_webhook action (GHSA-8jqh-598v-rfxc)
The send_webhook automation step fetched an account-controlled URL with
attacker-controlled headers and no SSRF protection, following redirects
by default — letting an agent+ reach cloud metadata (169.254.169.254),
loopback, and RFC1918 hosts from the server's network (status code
surfaces in automation logs). The separate webhook_endpoints delivery
path already guards this; the automation path did not.

Route send_webhook through the existing isDeliverableUrl guard before the
fetch, and mirror the deliver.ts hardening: redirect: 'manual' (a public
URL can't 3xx-bounce to an internal one) and a 10s timeout. Add an engine
regression test asserting a link-local target never reaches fetch.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 10:04:11 +03:00
Arnas Donauskas
03e851bea5
Merge pull request #351 from ArnasDon/fix/ghsa-34q7-fv77-625j-role-checks
fix(security): enforce agent role on service-role write routes (GHSA-34q7-fv77-625j)
2026-07-08 09:39:41 +03:00
Arnas Donauskas
1d7829e68a fix(security): enforce agent role on service-role write routes (GHSA-34q7-fv77-625j)
Flows and automations write routes verify only authentication/membership
(via the RLS client, whose *_select policies have no minimum role) and
then mutate with the service-role client, which bypasses the agent-gated
*_insert/update/delete RLS policies. A read-only `viewer` therefore had
full write power.

H3 (High): add requireRole('agent') to the flows write handlers —
PUT/DELETE (flows/[id]), activate (flows/[id]/activate), and create
(flows POST). GET/list stay open to viewers.

M2 (Medium): same gate on automations POST, the engine trigger
(automations/engine, which sends outbound WhatsApp), and the [id]
mutation handlers (PATCH/DELETE/duplicate). The cron route is unchanged —
it is gated by the x-cron-secret shared secret, not user auth.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 09:35:20 +03:00
Arnas Donauskas
e01f7ed371
Merge pull request #350 from ArnasDon/fix/ghsa-fg5p-2qc3-jmxr-db-authz
fix(security): patch DB-layer authz bypasses (GHSA-fg5p-2qc3-jmxr)
2026-07-08 09:13:48 +03:00
Arnas Donauskas
2b32308703 fix(security): patch DB-layer authz bypasses (GHSA-fg5p-2qc3-jmxr)
C1 (Critical): the profiles_update RLS policy gates only on user_id,
leaving account_role/account_id — the source of truth for
is_account_member() — freely writable by the row's own user. A
browser-client UPDATE could self-promote a viewer to owner or move
into another tenant. Add a BEFORE UPDATE trigger that rejects changes
to those columns when current_user = 'authenticated'; the SECURITY
DEFINER member/invitation RPCs (postgres) and the service_role backend
are unaffected.

H2 (High): match_ai_knowledge_fts/_semantic were SECURITY DEFINER,
filtered only on the caller-supplied p_account_id with no membership
check, and were granted to authenticated — letting any logged-in user
read another account's knowledge base via a foreign p_account_id.
Recreate both as SECURITY INVOKER so the existing
ai_knowledge_chunks_select RLS policy applies; the service_role
auto-reply bot still bypasses RLS.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 09:10:07 +03:00
Ivan Mercedes
de75715cff feat(i18n): localize Inbox components and update english translations 2026-07-07 21:24:54 -04:00
Ivan Mercedes
801969a361 feat(i18n): localize settings ai and meta sections
- Integrated next-intl into ai-config.tsx and ai-knowledge.tsx panels.
- Defined translation schemas for Settings.aiConfig and Settings.aiKnowledge in en.json.
- Migrated hardcoded role labels from role-meta.ts to use translation keys dynamically in settings-overview.tsx using the 'roles' namespace.
- All Typescript and Linter checks successfully passed without regressions.
2026-07-07 21:17:41 -04:00
Ivan Mercedes
9ee79638da feat(i18n): localize settings configuration panels
- Implemented next-intl across Appearance, Security, API Keys, and Deals settings panels.
- Defined translation schemas for Settings.appearance, Settings.security, Settings.apiKeys, and Settings.deals in en.json.
- Removed explicit 'any' types in components by utilizing local useTranslations hooks directly inside sub-components (ModeCard, ThemeCard, CreateKeyDialog).
- Eliminated all new typescript and eslint errors related to the localization migrations.
- Updated progress trackers to reflect completion.
2026-07-07 21:09:50 -04:00
Ivan Mercedes
c30e117552 refactor: update translation key types and simplify delete aria-label implementation 2026-07-07 21:00:33 -04:00
Ivan Mercedes
a6596de2c9 refactor: improve type safety for translations by replacing any types with specific parameters and node types 2026-07-07 20:55:21 -04:00
Ivan Mercedes
eeea478315 feat(i18n): localize settings templates and profile modul 2026-07-07 20:50:57 -04:00
Ivan Mercedes
a91b8a7ba7 feat(i18n): complete flows module internationalization 2026-07-07 20:12:38 -04:00
Ivan Mercedes
7cb019081b feat(i18n): translate settings page titles 2026-07-07 19:57:36 -04:00
Ivan Mercedes
9357fd9ec9 feat(i18n): complete settings module internationalization 2026-07-07 19:56:55 -04:00
Ivan Mercedes
caf0f3660e feat(i18n): complete internationalization of flow builder components 2026-07-07 19:43:26 -04:00
Ivan Mercedes
d518d54520 i18n: Localize AutomationBuilder component and fix typing 2026-07-07 17:51:09 -04:00
Ivan Mercedes
92e5789837 feat(i18n): localize Broadcasts module and wizard
- Add 'Broadcasts' namespace to en.json for all messaging flows
- Localize broadcast list and detail views (stats, recipient tables, funnel)
- Localize 4-step Broadcast creation wizard (Choose Template, Select Audience, Personalize, Schedule)
- Refactor status configuration to use translation keys instead of hardcoded labels
- Pass full typechecking across all wizard components
2026-07-07 17:29:45 -04:00
Ivan Mercedes
8b1d75456f feat(i18n): localize pipelines module and components
- Added Pipelines namespace to messages/en.json encompassing page, board, card, form, settings, and analytics.
- Migrated PipelinesPage empty states, dropdowns, and pipeline creation dialogues.
- Localized pipeline-board and deal-card UI strings and badges (Won/Lost).
- Translated deal-form inputs, dynamic tooltips, select options, and error toasts.
- Localized pipeline-settings including the sortable stage management rows and delete confirmations.
- Migrated pipeline-analytics metrics and detailed statistical tooltips.
- Resolved typecheck dependencies to ensure the t function is correctly passed to nested sortable components.
2026-07-07 17:14:48 -04:00
Ivan Mercedes
dcfd66792b feat: internationalize contact 2026-07-07 17:02:02 -04:00
Ivan Mercedes
57a914d547 feat(i18n): translate inbox module and components
- Add Inbox namespace to en.json dictionary
- Localize InboxPage and WhatsApp connection banners
- Translate ConversationList, search placeholders, and filters
- Localize MessageThread, session timers, and agent assignment dropdowns
- Translate MessageComposer, attachment menus, and AI draft hints
- Update MessageBubble, MessageActions, ReplyQuote, and TemplatePicker
- Verify strict typing for next-intl hooks across all inbox files
2026-07-07 15:09:24 -04:00
Ivan Mercedes
afa136245b feat(i18n): translate dashboard overview components
- Added `Dashboard` namespace to English dictionary.

- Translated metric cards, quick actions, and activity feed.

- Localized charts (conversations, pipeline donut, response time) and empty states.
2026-07-07 14:31:01 -04:00
Ivan Mercedes
738b0dde1f refactor: update translation key type assertions from any to string in header and sidebar components 2026-07-07 13:49:56 -04:00
Ivan Mercedes
07b20f42d4 feat(i18n): translate dashboard layout components
- Added `Sidebar`, `Header`, and `ModeToggle` namespaces to English dictionary.
- Refactored `sidebar.tsx` and `header.tsx` to use dynamic translations.
- Updated accessibility labels for navigation and theme switcher.
2026-07-07 13:43:06 -04:00
Arnas Donauskas
79565c1fbe
Merge pull request #328 from ivanmercedes/feat/i18n-support
feat: integrate next-intl for i18n support
2026-07-07 12:57:04 +03:00
Arnas Donauskas
4e2192d84f
Merge pull request #347 from ArnasDon/fix/flows-add-node-crash
fix(flows): wrap add-node menu groups in DropdownMenuGroup (#336)
2026-07-07 11:49:36 +03:00
Arnas Donauskas
f90563b35c fix(flows): wrap add-node menu groups in DropdownMenuGroup (#336)
Clicking "Add node" in the flow builder crashed the whole page to a
"This page couldn't load" error boundary, in both the list and canvas
views.

The add-node menus render a DropdownMenuLabel per category. That is
base-ui's Menu.GroupLabel, which calls useMenuGroupRootContext() during
render and THROWS ("MenuGroupContext is missing") when it has no
Menu.Group ancestor. Both menus wrapped each category in a plain <div>
instead of a DropdownMenuGroup, so the label had no group context. The
menu content only mounts when the menu opens, which is why initial
render was fine and the crash fired exactly on click.

Wrap each category's label + items in DropdownMenuGroup (base-ui
Menu.Group), which provides the context Menu.GroupLabel needs. The
between-group separator moves into a keyed Fragment so it stays a
sibling of the groups.

Adds a regression test pinning the contract: a bare DropdownMenuLabel
throws, a group-wrapped one does not.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 11:45:01 +03:00
Arnas Donauskas
2ebdc2ebf9
Merge pull request #346 from ArnasDon/fix/ai-autoreply-slot-grant
fix(ai): grant EXECUTE on claim_ai_reply_slot so auto-reply works (#345)
2026-07-07 10:46:08 +03:00
Arnas Donauskas
95faf58e75 fix(ai): grant EXECUTE on claim_ai_reply_slot so auto-reply works (#345)
Migration 029 created claim_ai_reply_slot(uuid, integer) as SECURITY
DEFINER but never GRANTed EXECUTE — the only function in the schema
missing its grant (007/012/017/018/019/025/030 all grant theirs).

SECURITY DEFINER sets the privileges a function runs *with*, not who may
*call* it: the caller still needs EXECUTE. On instances where the default
PUBLIC execute privilege on public-schema functions has been revoked
(hardened / self-hosted Supabase), the service_role that PostgREST uses
cannot invoke it. The AI auto-reply path runs entirely under the
service-role client (the inbound webhook has no auth.uid()), so
db.rpc('claim_ai_reply_slot') fails permission-denied, the dispatch bails
before sending, and the bot silently never answers ANY inbound message.
The Playground works because it never claims a slot — matching the report.

- 031_ai_reply_slot_grant.sql: new migration granting EXECUTE to
  service_role (029 is already applied on existing installs, so it needs
  its own migration to reach them). service_role only — the sole caller —
  matching the 007/012 increment-counter precedent.
- 029_ai_reply.sql: add the same grant next to the definition so fresh
  installs are correct.
- auto-reply.ts: log a real claim RPC error instead of returning
  silently, so this class of deploy failure is diagnosable.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 10:43:03 +03:00
Arnas Donauskas
274db1c7ce
Merge pull request #334 from ArnasDon/feat/ai-agents-sidebar
feat: AI Agents sidebar section (setup + playground)
2026-07-02 18:22:51 +03:00
Arnas Donauskas
10d31bcfc8 feat: AI Agents sidebar section (setup + playground)
Promotes the AI assistant out of Settings into a first-class "AI Agents"
sidebar area.

- Sidebar: new "AI Agents" item → /agents.
- /agents page with two tabs:
  - Playground — a test chat (POST /api/ai/playground) that runs the
    real auto-reply path (KB retrieval + provider, auto_reply prompt,
    multi-turn, shows handoff). loadAiConfig gains requireActive so you
    can test before enabling.
  - Setup — the existing provider/key + knowledge-base config, relocated.
- Removed the AI section from Settings (settings-sections + page); same
  account config, new home. No migration.

Tests +6 (query, loadAiConfig requireActive). 593 pass; typecheck, lint,
build green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 18:20:18 +03:00