Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
00655ec
feat(agentui): WhatsApp channel via waconector (9 providers)
alltomatos Sep 9, 2026
c48d875
fix(ci): widen tight subprocess timeouts causing flaky unit failures
alltomatos Sep 9, 2026
48824f3
Merge remote-tracking branch 'origin/fix/ci-flaky-subprocess-timeouts…
alltomatos Sep 9, 2026
faaf86a
fix(e2e): widen todo-dock re-open sampling window against CI load
alltomatos Sep 9, 2026
f94558c
fix(test): register LocationServiceMap in snapshot-tool-race layer graph
alltomatos Sep 9, 2026
9f9410d
Merge remote-tracking branch 'origin/fix/ci-flaky-subprocess-timeouts…
alltomatos Sep 9, 2026
9d98775
test: skip pre-existing flaky MCP-instructions test, track in #185
alltomatos Sep 9, 2026
a711499
Merge remote-tracking branch 'origin/fix/ci-flaky-subprocess-timeouts…
alltomatos Sep 9, 2026
fd1775b
feat(agentui): tabbed channel form, izapia UX, MCP tool access
alltomatos Sep 10, 2026
023843d
fix(i18n): backfill 9 English-only AgentUI keys across non-br locales
alltomatos Sep 10, 2026
2a6ae4b
fix(app): scope model picker to connected providers with models
alltomatos Sep 10, 2026
6556363
fix(agentui): default WhatsApp provider to izapia, not waha
alltomatos Sep 10, 2026
176e948
feat(agentui): fetch izapia sessions instead of typing sid by hand
alltomatos Sep 10, 2026
069b59f
fix(i18n): backfill 3 English-only izapiaSessions keys across non-br …
alltomatos Sep 10, 2026
7710d09
feat(agentui): multi-session izapia WhatsApp channels + group allow-list
alltomatos Sep 10, 2026
f6f4bc9
feat(agentui): public tunnel button for WhatsApp webhook (cloudflared)
alltomatos Sep 11, 2026
7a96ca6
fix(i18n): backfill izapiaGroups/tunnel keys, fix dv 'many' plural gap
alltomatos Sep 11, 2026
7b36b9c
fix(i18n): pin dv plural categories, stop trusting platform ICU (#197)
alltomatos Sep 11, 2026
2ed23cd
feat(agentui): audit log of incoming messages and agent replies
alltomatos Sep 11, 2026
12a089a
fix(agentui): redesign audit log as dedicated page with contact cards
alltomatos Sep 11, 2026
6081486
feat(agentui): surface the WhatsApp webhook secret in the form
alltomatos Sep 11, 2026
4e7e517
feat(agentui): detect local Tailscale IP as a webhook tunnel option
alltomatos Sep 11, 2026
6681119
fix(agentui): find tailscale CLI on GUI-only installs (Windows/macOS)
alltomatos Sep 11, 2026
9370794
fix(agentui): auto-load izapia session/group pickers when editing
alltomatos Sep 11, 2026
91e9c5b
fix(agentui): WhatsApp channel reliability fixes from live izapia tes…
alltomatos Sep 11, 2026
378eca9
test(agentui): update sessionPermission() test for wildcard deny
alltomatos Sep 12, 2026
dc47bfb
Merge branch 'agentui-telegram-channel' into agentui-whatsapp-channel
alltomatos Sep 13, 2026
8c40492
test(httpapi): add scenarios for agentui.audit, tunnel, and whatsapp …
alltomatos Sep 13, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,12 @@ test("animates todo lifecycle without replaying it across session tabs", async (
await switchSession(page, otherID, otherTitle)
await expect(dock).toHaveCount(0)

const returningOpen = sampleDock(page, 700)
// Was 700ms: under CI load the session-switch render can take long enough
// that the sampling window closes before the dock ever mounts, leaving
// zero "present" samples and failing on a load artifact rather than a
// real animation regression (observed 2026-09-09, all 3 attempts failing
// identically in one run — a systemic-load run, not a rare flake).
const returningOpen = sampleDock(page, 1_400)
await switchSession(page, sourceID, sourceTitle)
const openSamples = (await returningOpen).filter((sample) => sample.present)
expect(openSamples.length).toBeGreaterThan(0)
Expand Down
2 changes: 2 additions & 0 deletions packages/app/src/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ import { BatutaActivityFormPage } from "@/pages/batuta/activity-form"
import { BatutaActivityLivePage } from "@/pages/batuta/activity-live"
import { AgentUIPage } from "@/pages/agentui"
import { AgentUIFormPage } from "@/pages/agentui-form"
import { AgentUIAuditPage } from "@/pages/agentui-audit"
import { SettingsPage } from "@/pages/settings"
import { LegacyHome } from "@/pages/home/legacy-home"

Expand Down Expand Up @@ -663,6 +664,7 @@ function Routes(props: { serverScoped?: JSX.Element }) {
<Route path="/agentui" component={AgentUIPage} />
<Route path="/agentui/new" component={AgentUIFormPage} />
<Route path="/agentui/:id/edit" component={AgentUIFormPage} />
<Route path="/agentui/:id/audit" component={AgentUIAuditPage} />
<Route path="/settings/:tab?" component={SettingsPage} />
<Route path="/:dir/session/:id" component={NewLayoutLegacySessionRedirect} />
<Route path="/server/:serverKey/session/:id" component={TargetSessionRoute} />
Expand Down
27 changes: 24 additions & 3 deletions packages/app/src/components/batuta/model-picker-v2.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,16 @@ export interface ModelPickerV2Props {
onChange: (value: string) => void
/** When provided, the provider dropdown also lists these as selectable combos. */
combos?: { id: string; name: string }[]
/**
* Worktree to scope the provider catalog to. Without this, useProviders()
* falls back to the global catalog, which misses providers only connected
* per-project (e.g. Omniroute) — those still show in a real session's
* chat composer (project-scoped) but silently disappear from this picker.
* Pass the caller's own directory when one is available (AgentUI); other
* callers (Batuta, Memória) have no natural directory and keep the old
* global-catalog behavior by omitting this.
*/
directory?: string
}

function splitModel(value: string) {
Expand All @@ -33,10 +43,21 @@ const selectClass = `

export const ModelPickerV2: Component<ModelPickerV2Props> = (props) => {
const language = useLanguage()
const providers = useProviders(() => undefined)
const providers = useProviders(() => props.directory)

// Only providers actually connected (auth configured) with at least one
// model to pick — the full catalog (`providers.all()`) includes every
// provider opencode knows about, most of which aren't usable without
// setup, and would otherwise flood this dropdown with hundreds of dead
// options. A connected provider can still have zero synced models in this
// global (non-directory-scoped) catalog — Omniroute is one such case,
// since its catalog only populates once synced against a specific
// project — which would be a dead end if left selectable here.
const providerList = createMemo(() =>
Array.from(providers.all().values()).sort((a, b) => a.name.localeCompare(b.name)),
providers
.connected()
.filter((provider) => Object.keys(provider.models).length > 0)
.sort((a, b) => a.name.localeCompare(b.name)),
)
const isCombo = createMemo(() => props.value.startsWith(COMBO_PREFIX))
const selectedComboID = createMemo(() => (isCombo() ? props.value.slice(COMBO_PREFIX.length) : ""))
Expand Down Expand Up @@ -97,7 +118,7 @@ export const ModelPickerV2: Component<ModelPickerV2Props> = (props) => {
</select>
<Show when={!isCombo()}>
<div class="flex min-w-0 flex-1 flex-col gap-1">
<Show when={allModels().length > MODEL_RENDER_CAP}>
<Show when={allModels().length > 0}>
<input
type="text"
class="h-8 min-w-0 rounded-md border border-v2-border-border-base bg-v2-background-bg-base px-2.5 text-13-regular text-v2-text-text-base outline-none placeholder:text-v2-text-text-faint focus-visible:border-v2-border-border-focus"
Expand Down
8 changes: 8 additions & 0 deletions packages/app/src/components/settings-v2/agentui.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,14 @@ export const SettingsAgentUIV2: Component<{ directory?: string }> = (props) => {
<ButtonV2 variant="neutral" size="normal" onClick={() => openSandbox(agent)}>
{language.t("settings.agentui.sandbox.open")}
</ButtonV2>
<IconButtonV2
type="button"
variant="ghost-muted"
size="small"
icon={<IconV2 name="review" />}
aria-label={language.t("settings.agentui.audit.title")}
onClick={() => navigate(`/agentui/${agent.id}/audit`)}
/>
<IconButtonV2
type="button"
variant="ghost-muted"
Expand Down
80 changes: 80 additions & 0 deletions packages/app/src/components/settings-v2/whatsapp-providers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
// Mirrors packages/opencode/src/whatsapp/index.ts PROVIDER_FIELDS — kept as a
// small static duplicate on purpose (avoids a round-trip just to render a
// form) rather than a shared package, since these are UI labels, not
// business logic. Keep the two in sync when adding/removing a provider or
// changing a field name (the backend's `config` keys must match exactly).
export type WhatsAppProvider =
| "waha"
| "evolution"
| "zapi"
| "uazapi"
| "whapi"
| "wuzapi"
| "quepasa"
| "wppconnect"
| "izapia"

export interface WhatsAppProviderField {
key: string
required: boolean
label: string
}

export const WHATSAPP_PROVIDER_LABELS: Record<WhatsAppProvider, string> = {
izapia: "izapia",
waha: "WAHA (self-hosted, Docker)",
evolution: "Evolution GO (self-hosted, Docker)",
zapi: "Z-API (SaaS)",
uazapi: "uazapi (SaaS)",
whapi: "Whapi.Cloud (SaaS)",
wuzapi: "Wuzapi (self-hosted, Docker)",
quepasa: "QuePasa (self-hosted, Docker)",
wppconnect: "WPPConnect Server (self-hosted, Docker)",
}

// Painel/dashboard oficial do provider, onde a pessoa consegue criar a conta
// e pegar as credenciais pedidas nos campos abaixo. Só preenchido quando
// existe uma URL confirmada (não um chute) — providers self-hosted não têm
// uma, já que rodam no servidor da própria pessoa.
export const WHATSAPP_PROVIDER_LINKS: Partial<Record<WhatsAppProvider, string>> = {
izapia: "https://app.izapia.com/login",
}

export const WHATSAPP_PROVIDER_FIELDS: Record<WhatsAppProvider, WhatsAppProviderField[]> = {
waha: [
{ key: "baseUrl", required: true, label: "URL base (ex.: http://localhost:3000)" },
{ key: "apiKey", required: true, label: "API Key (X-Api-Key)" },
{ key: "session", required: false, label: "Nome da sessão (padrão: default)" },
],
evolution: [
{ key: "baseUrl", required: true, label: "URL base do servidor Evolution GO" },
{ key: "apiKey", required: true, label: "API Key da instância" },
],
zapi: [
{ key: "instanceId", required: true, label: "Instance ID" },
{ key: "token", required: true, label: "Token da instância" },
{ key: "clientToken", required: false, label: "Client-Token (se ativado na conta)" },
],
uazapi: [
{ key: "baseUrl", required: true, label: "URL base (ex.: https://minhaempresa.uazapi.com)" },
{ key: "token", required: true, label: "Token da instância" },
],
whapi: [{ key: "token", required: true, label: "Token do canal (Bearer)" }],
wuzapi: [
{ key: "baseUrl", required: true, label: "URL base do servidor Wuzapi" },
{ key: "token", required: true, label: "Token do usuário" },
],
quepasa: [
{ key: "baseUrl", required: true, label: "URL base da instância QuePasa" },
{ key: "token", required: true, label: "Token da instância" },
],
wppconnect: [
{ key: "baseUrl", required: true, label: "URL base do servidor WPPConnect" },
{ key: "session", required: true, label: "Nome da sessão" },
{ key: "token", required: true, label: "Token Bearer da sessão" },
],
// No "sid" field here — izapia is multi-session, picked via the "buscar
// sessões" checkboxes in agentui-form.tsx (WhatsAppChannelBinding.sessionIds),
// not typed in as config.
izapia: [{ key: "apiKey", required: true, label: "API key do tenant" }],
}
43 changes: 43 additions & 0 deletions packages/app/src/i18n/am.ts
Original file line number Diff line number Diff line change
Expand Up @@ -816,6 +816,7 @@ export const dict = {
"common.dismiss": "አሰናብት",
"common.moreCountSuffix": " (+{{count}} ተጨማሪ)",
"common.requestFailed": "ጥያቄው አልተሳካም",
"common.optional": "አማራጭ",
"common.moreOptions": "ተጨማሪ አማራጮች",
"common.learnMore": "ተጨማሪ ለመረዳት",
"common.rename": "ዳግም ስም",
Expand Down Expand Up @@ -1418,6 +1419,7 @@ export const dict = {
"settings.agentui.empty": "No agents yet.", // TODO: translate (auto-filled from en)
"settings.agentui.error.incomplete": "Give the agent a name and pick a model.", // TODO: translate (auto-filled from en)
"settings.agentui.error.telegramNeedsProject": "ለዚህ ወኪል የራሱን የቴሌግራም ቦት ለመስጠት ይህን ገጽ ከተገናኘ ፕሮጀክት ይክፈቱ።",
"settings.agentui.error.whatsappIncomplete": "ይህን ገጽ ከተገናኘ ፕሮጀክት ይክፈቱ እና ለተመረጠው WhatsApp አቅራቢ ሁሉንም አስፈላጊ መስኮች ይሙሉ።",
"settings.agentui.toast.saved": "Agent saved", // TODO: translate (auto-filled from en)
"settings.agentui.dialog.addTitle": "Create agent", // TODO: translate (auto-filled from en)
"settings.agentui.dialog.editTitle": "Edit agent", // TODO: translate (auto-filled from en)
Expand All @@ -1427,12 +1429,53 @@ export const dict = {
"settings.agentui.ai.generate": "ፍጠር",
"settings.agentui.ai.generating": "በመፍጠር ላይ…",
"settings.agentui.ai.toast.generated": "ረቂቅ ተፈጥሯል — ከማስቀመጥዎ በፊት ይከልሱ",
"settings.agentui.tabs.general": "General", // TODO: translate (auto-filled from en)
"settings.agentui.tabs.channels": "Channels", // TODO: translate (auto-filled from en)
"settings.agentui.tabs.tools": "Tools", // TODO: translate (auto-filled from en)
"settings.agentui.tabs.knowledge": "Knowledge", // TODO: translate (auto-filled from en)
"settings.agentui.field.mcpServers": "MCP servers", // TODO: translate (auto-filled from en)
"settings.agentui.field.mcpServers.hint": "Let this agent call tools from the MCP servers below, connected to the same project as its channels. Off by default — agents are conversational-only until you turn one on.", // TODO: translate (auto-filled from en)
"settings.agentui.field.mcpServers.empty": "No MCP servers configured for this project yet. Add one in Settings → MCP first.", // TODO: translate (auto-filled from en)
"settings.agentui.field.mcpServers.noDirectory": "Open a project first to see its MCP servers.", // TODO: translate (auto-filled from en)
"settings.agentui.field.whatsapp.providerLink": "Open {{provider}} dashboard →", // TODO: translate (auto-filled from en)
"settings.agentui.field.whatsapp.izapiaSessions.fetch": "Fetch sessions", // TODO: translate (auto-filled from en)
"settings.agentui.field.whatsapp.izapiaSessions.loading": "Fetching…", // TODO: translate (auto-filled from en)
"settings.agentui.field.whatsapp.izapiaSessions.empty": "No sessions found for this API key.", // TODO: translate (auto-filled from en)
"settings.agentui.field.whatsapp.izapiaSessions.label": "WhatsApp sessions (select one or more)", // TODO: translate (auto-filled from en)
"settings.agentui.field.whatsapp.izapiaGroups.label": "Groups", // TODO: translate (auto-filled from en)
"settings.agentui.field.whatsapp.izapiaGroups.hint": "Direct messages are always answered. Check any groups the agent should also answer in — groups left unchecked are ignored.", // TODO: translate (auto-filled from en)
"settings.agentui.field.whatsapp.izapiaGroups.fetch": "Fetch groups", // TODO: translate (auto-filled from en)
"settings.agentui.field.whatsapp.izapiaGroups.loading": "Fetching…", // TODO: translate (auto-filled from en)
"settings.agentui.field.whatsapp.izapiaGroups.empty": "No groups found for the selected sessions.", // TODO: translate (auto-filled from en)
"settings.agentui.field.whatsapp.tunnel.tailscale.hint": "Simplest option if you already have Tailscale running on this machine and the provider's server: use this machine's Tailscale IP directly, no tunnel process needed.", // TODO: translate (auto-filled from en)
"settings.agentui.field.whatsapp.tunnel.tailscale.use": "Use Tailscale IP", // TODO: translate (auto-filled from en)
"settings.agentui.field.whatsapp.tunnel.tailscale.detecting": "Detecting…", // TODO: translate (auto-filled from en)
"settings.agentui.field.whatsapp.tunnel.tailscale.notFound": "No Tailscale IP found. Is Tailscale installed and running?", // TODO: translate (auto-filled from en)
"settings.agentui.field.whatsapp.tunnel.hint": "This server's URL is local (127.0.0.1/localhost) — the provider can't call it back to deliver messages. Start a public tunnel so it can.", // TODO: translate (auto-filled from en)
"settings.agentui.field.whatsapp.tunnel.start": "Start public tunnel", // TODO: translate (auto-filled from en)
"settings.agentui.field.whatsapp.tunnel.starting": "Starting…", // TODO: translate (auto-filled from en)
"settings.agentui.field.whatsapp.tunnel.active": "Public tunnel active: {{url}} — the webhook URL below now uses it.", // TODO: translate (auto-filled from en)
"settings.agentui.field.whatsapp.tunnel.error": "Couldn't start the tunnel. Is cloudflared installed?", // TODO: translate (auto-filled from en)
"settings.agentui.tabs.audit": "Audit", // TODO: translate (auto-filled from en)
"settings.agentui.audit.title": "Message log", // TODO: translate (auto-filled from en)
"settings.agentui.audit.refresh": "Refresh", // TODO: translate (auto-filled from en)
"settings.agentui.audit.loading": "Loading…", // TODO: translate (auto-filled from en)
"settings.agentui.audit.needsSave": "Save the agent first to see its message log.", // TODO: translate (auto-filled from en)
"settings.agentui.audit.empty": "No messages logged yet.", // TODO: translate (auto-filled from en)
"settings.agentui.audit.blocked": "Blocked", // TODO: translate (auto-filled from en)
"settings.agentui.audit.incoming": "Received", // TODO: translate (auto-filled from en)
"settings.agentui.audit.outgoing": "Replied", // TODO: translate (auto-filled from en)
"settings.agentui.field.name": "Name", // TODO: translate (auto-filled from en)
"settings.agentui.field.personality": "Personality", // TODO: translate (auto-filled from en)
"settings.agentui.field.model": "Model", // TODO: translate (auto-filled from en)
"settings.agentui.field.telegram": "Reply on Telegram", // TODO: translate (auto-filled from en)
"settings.agentui.field.telegramToken.placeholder": "የቦት ቶከን (አማራጭ — ሁለንተናዊ ቦትን ለማጋራት ባዶ ይተዉት)",
"settings.agentui.field.telegramToken.hint": "ለዚህ ወኪል የራሱ የተወሰነ የቴሌግራም ቦት ለመስጠት ከ@BotFather ቶከን ይለጥፉ። ባዶ ከተተወ በቅንብሮች → ውህደቶች ውስጥ ባለው የጋራ ቦት በኩል በራሱ ትዕዛዝ ቅድመ ቅጥያ ይደረሳል።",
"settings.agentui.field.whatsapp": "በWhatsApp ይመልሱ",
"settings.agentui.field.whatsapp.hint": "ኦፊሴላዊ ያልሆነ WhatsApp API (በ waconector በኩል) ይጠቀማል — አቅራቢ ይምረጡ እና የግንኙነት ዝርዝሮችን ይሙሉ። ራስን-ያስተናገዱ አቅራቢዎች የራሳቸው ሰርቨር/ኮንቴይነር ተለይተው እንዲሰራ ይፈልጋሉ፤ ይህ ከዚያ ጋር ብቻ ይገናኛል።",
"settings.agentui.field.whatsapp.webhookUrl": "የዌብሁክ URL — በአቅራቢው ዳሽቦርድ ውስጥ ይለጥፉ",
"settings.agentui.field.whatsapp.webhookSecret": "Webhook secret — some providers (e.g. izapia) ask for this separately", // TODO: translate (auto-filled from en)
"settings.agentui.field.whatsapp.webhookAfterSave": "በአቅራቢው በኩል መዋቀር ያለበት የዌብሁክ URL ካስቀመጡ በኋላ እዚህ ይታያል።",
"settings.agentui.field.commandTriggers": "Command triggers (space-separated, e.g. ! #)", // TODO: translate (auto-filled from en)
"settings.agentui.field.guardrails": "Guardrails", // TODO: translate (auto-filled from en)
"settings.agentui.field.guardrailsLevel": "Guardrails level", // TODO: translate (auto-filled from en)
Expand Down
Loading
Loading