feat(mcp): show tool retrieval diagnostics in clients
Adapted while landing on current master:
- Ported SSE forwarding + rendering to the refactored web layer
(channel/web/core/channel.py, channel/web/static/js/chat/*, core/i18n.js).
- Fallback (full-injection) is now log-only instead of a client-facing
diagnostic step; only successful retrieved decisions are surfaced.
Co-authored-by: junqi.zhong <3249888615@qq.com>
Co-authored-by: cowagent <cow@cowagent.ai>
The voice bridge sent a fixed whisper-1 for every ASR request, ignoring
the user's picked engine. Now it forwards the configured voice_to_text_model
(empty means let the gateway pick its default). Also expose default/doubao/
whisper-1/baidu as ASR options and fix the empty 'default' option rendering
blank in both the web console and the desktop app.
Co-authored-by: cowagent <cow@cowagent.ai>
Uploads are stored per agent under <workspace>/tmp, but /upload returned a
preview_url without the agent, so /uploads/<name> resolved against the default
agent's tmp. Under any non-default agent the attachment 404'd and rendered as a
broken thumbnail.
Multipart uploads also ignored the agent entirely and always wrote to the
default workspace: the client sends it in the query string (a field present in
both query and body arrives as a list and breaks handlers), while the handler
read the body alone via rawinput("post").
Both upload routes now resolve the agent and name it in the URL they hand back,
matching what the voice ASR and TTS paths already did.
Co-authored-by: cowagent <cow@cowagent.ai>
README had a Frontend layout section and nothing for the server side. Adds
the counterpart: what web_channel.py is now, what core/ and api/ hold, and
the two things that bite when moving code between them -- patch targets
follow the handler's module, and __file__-derived paths are relative to it.
The desktop build resolves channel.web.web_channel as a hidden import and
would have to follow the new modules through static analysis. These are
namespace packages, which collect_submodules does not reliably walk, so the
spec reads the list off the directory instead.
Co-authored-by: cowagent <cow@cowagent.ai>
Auth, the shell pages and their assets, the message routes, and everything
that moves bytes. web_channel.py is now 177 lines: the URL table, build_app,
and the imports that put every handler in the namespace build_app resolves
against.
Two latent NameErrors surfaced once the star import was no longer in the
way. VoiceTtsHandler read ReplyType, which 'from bridge.context import *'
never exported, so a TTS reply raised instead of being checked; it now
imports it from bridge.reply. WebChannel's local-file import read quote,
which stopped resolving when the class moved to core/channel.py earlier on
this branch.
AssetsHandler derives static/ from __file__ and sits a level deeper now,
so it walks up one more directory.
Co-authored-by: cowagent <cow@cowagent.ai>
Skills, memory, knowledge, logs and the updater. Small modules, but they
are what is left of the page-shaped groups.
test_doc_edit's request helpers take the handler class, so they now read
the module off the class instead of naming one: moving a handler between
api/ modules no longer sends them patching an empty namespace.
Co-authored-by: cowagent <cow@cowagent.ai>
Browsing, reading and writing workspace and project files. _is_path_allowed
and the roots it checks against go to core instead: the file-serving and
preview routes gate on them too.
Co-authored-by: cowagent <cow@cowagent.ai>
Listing and renaming sessions, replaying history, the per-session
permission mode and model, clearing and compacting context. The
step-rendering helpers come along: replaying history is the only thing
that reads stored steps back, since a live turn streams them as they
happen.
Co-authored-by: cowagent <cow@cowagent.ai>
The roster, core files and avatars, plus the two things that make editing
an Agent more than a config write: _reload_agent_runtime rebuilds the
snapshot the running services hold, _bind_channel_instance re-points the
IM channels the Agent answers on. cloud_client.py imports the first of
those and now asks the module that owns it.
Co-authored-by: cowagent <cow@cowagent.ai>
Eleven handlers, one per route as web.py wants, plus the task-store
accessor they all go through and the instance-to-agent lookup only the
create path uses.
Co-authored-by: cowagent <cow@cowagent.ai>
ChannelsHandler plus the two interactive sign-ins, the WeChat QR code and
Feishu's one-click registration. _live_channel_manager goes to core, not
with them: the agents code binds instances through it too.
The disconnect tests patched _require_auth and friends on web_channel,
which no longer reaches a handler reading them from its own module, and
the _channel_mgr source guard was reading a file the handler had left.
That one now reads web_backend_py(), so it guards the console's server
code rather than one file of it.
Co-authored-by: cowagent <cow@cowagent.ai>
ModelsHandler was 2.3k lines, a third of what was left in web_channel.py
and the largest handler in the console by a wide margin. Both are pure
moves; the modules take only the imports their code actually reaches for.
They are re-exported from web_channel.py because build_app() resolves the
URL table against that module's globals, which is also why the tests can
keep importing them from there. What could not stay is the patch targets:
patching channel.web.web_channel.conf no longer reaches a handler that
reads conf out of its own module, so those tests now name the module the
handler actually lives in.
Co-authored-by: cowagent <cow@cowagent.ai>
The two handlers reached into each other: ModelsHandler read the vendor
catalogue and the key-masking helper off ConfigHandler, ConfigHandler
called ModelsHandler._legacy_custom_in_use. That is a cycle as soon as
they live in separate modules, which is where they are headed.
All three shared pieces describe a provider's configured values, so they
move to core/providers.py next to the catalogue itself, and both handlers
read them from there. The ConfigHandler.PROVIDER_MODELS alias goes with
them; nothing needed the indirection any more.
Imported by name rather than as a module: 'providers' is a common local
variable in these handlers, and a local binding shadows the module for
the whole function, including the lines above the assignment.
Co-authored-by: cowagent <cow@cowagent.ai>
Renamed to openai_compat.py: it is the OpenAI-compatible chat completions
endpoint, and a module called openai.py sitting in an import path is a
trap worth avoiding.
Co-authored-by: cowagent <cow@cowagent.ai>
The split was dropping modules next to chat.html, static/ and templates/,
and the handler split still to come would put a dozen more there. Mirror
the frontend instead: core/ for the shared infrastructure, api/ for the
handlers, leaving web_channel.py as the only Python file at the top.
template.py and channel.py derive paths from __file__ and now sit one
level below the web root, so both walk up one more directory.
Co-authored-by: cowagent <cow@cowagent.ai>
The channel and the request handlers were in one module because they shared
helpers; with those in _common there was one thing left holding them together,
a WebChannel method reading ConfigHandler.PROVIDER_MODELS, and that table is
now in providers. WebChannel needs nothing else from the handler module.
The URL table moves with the handlers rather than with the channel, to module
scope as URLS next to the build_app() that feeds it to web.py -- the names in
it are resolved out of the namespace they are defined in, so the table has to
live where the handlers do. WebChannel now asks for the app instead of
assembling one, and imports build_app inside startup(): web_channel.py imports
this module, and by the time startup() runs it is always already imported,
since channel_factory only ever reaches the class through it.
web_channel.py keeps re-exporting WebChannel and SERVING, which app.py and
channel_factory resolve by name through this module.
Two tests imported helpers straight out of web_channel and now take them from
_common, where they live.
Co-authored-by: cowagent <cow@cowagent.ai>
Everything WebChannel reaches for, the handlers reach for too -- auth, the
addressed agent, upload and workspace paths, the small value types. There is
nothing only the channel needs. That shared surface was the reason the channel
and 76 handlers had to live in one module: a helper could only be shared by
being a neighbour.
_common knows about neither side, so both can import it without a cycle, which
is what makes the rest of the split possible. The names are imported back here
by name rather than as a module, so the handlers still resolve them out of this
module's globals -- the same place web.py resolves handler names from, and
where the tests already patch them.
Pure move: all 46 definitions are byte-identical to the originals.
Co-authored-by: cowagent <cow@cowagent.ai>
Seven test files read channel/web/web_channel.py as text and assert a route
or a helper is still wired up. Moving any of that code out of the file breaks
tests that have nothing to do with the move, which is what makes a large file
hard to split.
web_backend_py() is the backend counterpart of console_js(): it concatenates
every Python file under channel/web/, read off the directory so it cannot
fall behind a new one. The two tests that parse a specific structure -- the
URL table, RootHandler's body -- keep reading the single file they mean, so
they still fail loudly when that structure moves.
Co-authored-by: cowagent <cow@cowagent.ai>
PROVIDER_MODELS is data, but it lived as a class attribute on ConfigHandler
while ModelsHandler read it fifteen times and WebChannel once, so two of the
three readers had to reach into a handler to get at it. It now sits in
channel/web/providers.py; ConfigHandler.PROVIDER_MODELS stays as an alias so
no call site changes.
Pure move: the extracted block is byte-identical to the original.
Co-authored-by: cowagent <cow@cowagent.ai>
The _is_mention_bot fallback returned True when the bot identity was
unknown, so an app with the broad im:message scope (which receives every
group message) would treat any @-mention as directed at the bot. This let
the bot process and run tools on messages addressed to other people.
- Fail closed: return False (and warn) when neither bot open_id nor
feishu_bot_name is available, instead of assuming the bot was mentioned.
- Lazily retry the open_id lookup (rate-limited) so a transient startup
failure no longer leaves the bot blind for the whole process lifetime.
- Log non-200 responses in _fetch_bot_open_id and add a debug log when a
group message mentioning others is ignored.
Fixes#3168
Co-authored-by: cowagent <cow@cowagent.ai>
The split itself already landed on master; the branch was force-pushed
afterwards, so this records its final head and takes its English README,
re-applying the master-side changes on top: mtime asset versioning, the
address-bar routing section, and the removal of the -old scaffolding.
Co-authored-by: cowagent <cow@cowagent.ai>
The backend has called this feature scheduler all along (/api/scheduler/...)
and the view only reads as "tasks" because that is its element id. Routing it
at /tasks also spent the obvious name for a task on the thing that schedules
them, and a task is a concept this will want separately.
The view id, the element ids and the switcher stay as they are; only the
path segment moves, so /scheduler and /scheduler/records are what the address
bar shows and /tasks is free again.
Co-authored-by: cowagent <cow@cowagent.ai>
The top-left mark is the way back to the start of an app everywhere else on
the web, and here it was inert: the only route to the chat view was its
sidebar entry, which is two groups down and scrolls away.
The image is the target, not the header row it sits in -- the name beside it
is a heading, and a click target that wide is one the user hits by accident.
It goes through navigateTo like every other destination, so the address bar
follows, the unsaved-edit guard still applies and the mobile drawer closes.
Co-authored-by: cowagent <cow@cowagent.ai>
The usage line still pointed at /chat, the vendor notes still pointed at
the deleted console.js, and a comment in the shell still described the
router as listening for hashchange. Also documents the two route naming
rules: default tabs stay out of the path, and a tab whose element id reads
as an internal name is routed under the name the UI uses.
Co-authored-by: cowagent <cow@cowagent.ai>
Two module-level caches survive a test and are reachable from one, so the
console tests passed or failed on what the run happened to collect first.
template caches each fragment under its mtime, which no test disturbs, so
a read taken while test_web_chat_content_type had builtins.open patched was
held for the rest of the session and every later render() returned that
test's stand-in markup. Nine modules install a stub web module, none of
which carries the ctx that handlers driven straight from a test read.
Clear the cache around each test and give the stub an empty ctx, which is
the case those handlers are already written for.
Co-authored-by: cowagent <cow@cowagent.ai>
Every tabbed view wrote its default tab into the path, so opening the
scheduler landed on /tasks/tasks and settings on /settings/basic. A segment
is there to say which of several tabs is showing, so the one you get by
default has nothing to say: it is now left out, and only /tasks/records,
/settings/models and friends carry one.
A bare path consequently means the default tab rather than "leave the tab
alone", or Back out of /tasks/records would keep showing records and the
settle at the end of routeApply would push the path straight back.
The self-evolution tab routed as /memory/dreams, which is its element id,
not the Self-Evolution it is labelled everywhere the user can see it. Paths
say what the UI says now; ROUTE_TAB_PATHS maps the two, so the element id,
the switcher argument and ROUTE_TABS are untouched, and a hand-written
/memory/dreams still opens the tab and normalises to /memory/evolution.
Co-authored-by: cowagent <cow@cowagent.ai>
The -old flag served a git-checked-out snapshot of the pre-split console
so the two could be compared side by side. That was worth having while the
split was under review; merged, it is broken by its own success, since
snapshot_legacy.py reads console.js out of master and the split deletes it.
Removing the flag also takes a branch out of ChatHandler.
The URL table gained '/chat' -> RootHandler without losing the '/chat' ->
ChatHandler below it. web.py takes the first match, so the second entry
never ran.
Co-authored-by: cowagent <cow@cowagent.ai>
qwen3.8-max/flash were wrongly marked thinking_only, which forced the long
xhigh reasoning pass with no way to disable it — the cause of the reported
"first byte in 1.7s but output only after 90s". They are actually hybrid
thinking models, so honor the thinking toggle: enable_thinking=false now
truly disables thinking (and skips preserve_thinking/reasoning_effort).
Co-authored-by: cowagent <cow@cowagent.ai>
The self-evolution summary is both pushed live (random evolution_ request id) and persisted to history (no request id), so the existing request-id dedupe couldn't match them and the same learning showed as two identical bubbles after a history reload. Tag evolution bubbles with a content key and skip a persisted copy when the live one is already on screen.
Co-authored-by: cowagent <cow@cowagent.ai>
The backend's stdout was persisted to run.log by three writers at once:
Python's own FileHandler, python-manager mirroring stdout for the whole
run, and the console.* patch re-writing every emitted line with a
[MAIN][backend] prefix. Only mirror stdout before the backend is ready
(bootstrap-crash capture, which is why the mirror exists) and route the
per-line forward through process.stdout instead of the patched console,
so steady-state logs land in run.log exactly once.
Co-authored-by: cowagent <cow@cowagent.ai>
Make the create-agent form body scroll when it outgrows the viewport (small laptops, narrow mobile widths) so the create/cancel buttons in the footer stay reachable. The body still overflows visibly while a dropdown is open so its menu isn't clipped.
Co-authored-by: cowagent <cow@cowagent.ai>
net.fetch only exists in Electron 28+, so the Win7 legacy build (Electron 22)
failed tsc with "Property 'fetch' does not exist on type 'Net'". The runtime
guard and http fallback were already correct; access net.fetch through a
custom structural type so it no longer depends on Electron's Net typing (nor
the DOM Response type, absent from the main tsconfig).
Co-authored-by: cowagent <cow@cowagent.ai>
Adds a "Group chat" button next to "Create Agent" (shown when at least two
Agents exist) that navigates to the chat page with ?team=1, which pops the
group-chat picker over the conversation. The flag is cleared once consumed so
back/refresh doesn't reopen it.
Co-authored-by: cowagent <cow@cowagent.ai>
Group chats now list the host first as the "main Agent" (green badge, not
removable) so the owner is visible; solo chats drop the full switch roster and
show only the current Agent, since switching there silently forked a new
conversation. Renames the clashing "default" wording to "main Agent" and
removes the now-dead pickComposerAgent path on web.
Co-authored-by: cowagent <cow@cowagent.ai>
When a user addresses a teammate by name, routing overwrites the turn's
agent_id with the conversation host, so agent_delegate was running under the
host's identity and rejecting the guest handing work to the host as
"delegate to self". Resolve the delegation source from speaker_agent_id
(falling back to agent_id for nested turns) and include the host in the
delegable roster so it matches the prompt roster [host, *members].
Co-authored-by: cowagent <cow@cowagent.ai>
Agent replies embed workspace-relative image refs (e.g. images/x.png) that
only resolved for the default agent. Backend now rewrites relative media refs
to absolute /api/file URLs on the SSE/polling and history paths, and the
desktop markdown renderer recognizes those URLs and prefixes them with the
backend base. Also add a native right-click Save/Copy image menu on desktop.
Co-authored-by: cowagent <cow@cowagent.ai>
Use dynamic viewport height (dvh) instead of 100vh so the full-height layout
no longer overflows below the mobile URL bar, let the logs toolbar wrap on
narrow screens, add a small-screen horizontal-overflow guard, and hide the
redundant header logout icon on phones. Desktop layout is unchanged.
Co-authored-by: cowagent <cow@cowagent.ai>
Clarify the bash run_in_background param to be used only for long-lived processes, and tell the image-generation skill to run synchronously.
Co-authored-by: cowagent <cow@cowagent.ai>
Bring the desktop Models tab in line with the web console's catalog editor:
a collapsible advanced section in the vendor and custom-provider modals that
edits a provider's model list with the same overlay semantics — prefill the
effective list (presets − removals + overrides), diff against the presets on
save, and persist only overrides + hidden so untouched presets keep following
the code-side metadata.
- New ModelCatalogEditor component with the overlay helpers (toDraftRows,
normalizeRows, diffAgainstSeed); wired into both modals, widened to lg.
- types: ModelProvider gains hidden/effective, ModelsAction gains save_catalog.
- Save credentials then the catalog (custom providers use the returned id),
skipping the write when the draft matches the stored overlay.
- Both platforms: the catalog section now always opens collapsed, and the
redundant "no models yet" empty hint is removed.
Co-authored-by: cowagent <cow@cowagent.ai>
Rework the per-provider model catalog from a wholesale replacement into an
overlay stored at <workspace>/system/models.json, so it no longer bloats
config.json and untouched presets keep following the code-side metadata.
- Store only overrides + hidden tombstones; the editor prefills the full
effective list (presets - removals + overrides) so adding one model can no
longer wipe the rest, and only the diff from the presets is persisted.
- Migrate an early adopter's legacy config.json provider_model_catalog into
the overlay store once, then strip the old key.
- Share the family budget table via resolve_family_spec so the editor shows
the same context window / max output the runtime uses (e.g. gpt-6-astra).
- UI: widen the vendor modals, fix the vendor picker being clipped by the
modal's scroll body, go straight to the custom-provider modal once every
built-in is configured, and simplify the catalog hint copy.
Co-authored-by: cowagent <cow@cowagent.ai>
The default Agent face used the wide-margin square logo.png scaled to 72%,
so it looked small and, once cropped into the circle, read as an octagon.
Use the same round logo.jpg the login/chat screens and web console use,
filled edge-to-edge, so circle faces are clean discs and chat bubbles fill.
Co-authored-by: cowagent <cow@cowagent.ai>
Enable transport-level ping keepalive (ping_interval/ping_timeout) so a
half-open connection triggers the existing _on_close reconnect path, and
add a heartbeat-ACK watchdog that force-closes the socket when the gateway
stops acking. Extend the ws compat layer to strip ping_interval/ping_timeout
on older websocket-client versions so the keepalive knobs never break startup.
Co-authored-by: cowagent <cow@cowagent.ai>
The desktop team page could come up empty - not even the built-in CowAgent
card - so users thought their Agent was gone. The Agent was fine (its
persona lives in ~/cow/AGENT.md and it kept chatting); only the roster list
failed to render.
Two causes, both fixed:
1. Frontend (the actual trigger): on a cold start the local WSGI backend can
be momentarily busy (an SSE stream or a concurrent poll holding threads),
and a plain fetch then throws a bare TypeError. getAgents() went through
the no-retry request() path (postFormData already retries the same hiccup
for uploads), and agentStore.refresh() treated that failure as a valid
empty roster and cleared the list. Creating an Agent still worked because
that POST ran once the backend was idle and then refreshed - which is why
"create works but the list is empty" looked contradictory.
Now the roster fetch retries a few times on the transient network error
(a real HTTP status like 401/404 stays final), and the page is never
stranded on an empty roster: keep the last good roster, otherwise fall
back to the same synthesized default the backend uses (id=default,
name=CowAgent) so the built-in Agent - and its brand logo - is always
shown.
2. Backend hardening: a config.json saved with a UTF-8 BOM (e.g. edited with
Windows Notepad / PowerShell) made AgentAdminService._load() raise
"Unexpected UTF-8 BOM", which surfaced as a failed /api/agents snapshot.
commit a9dfc54c fixed the main config and web-console read paths but
missed the roster paths; read config.json/team.json with utf-8-sig at
agent/admin.py and agent/team.py (write-back stays plain utf-8 so the BOM
is stripped on the next save).
Single- and multi-Agent installs and legacy backends without /api/agents are
unaffected.
Co-authored-by: cowagent <cow@cowagent.ai>
PR #3141 dropped the sys/app_module definition in the disconnect path's
_do_stop while switching to _live_channel_manager(), but the next line
still reads _clear_singleton_cache off app_module. That raised NameError
(swallowed by the except), so the singleton cache was never cleared on
disconnect. Restore the app_module lookup so cleanup runs again.
Co-authored-by: cowagent <cow@cowagent.ai>
Read config.json with utf-8-sig so a BOM (e.g. saved by Windows Notepad)
no longer breaks config/model updates from the web console; write-back
stays plain utf-8 to strip the BOM on next save.
Gate the /poll and /api/scheduler/runs background pollers behind auth so
a password-protected console no longer spams "401 Unauthorized" before
the user logs in.
Co-authored-by: cowagent <cow@cowagent.ai>
The Team nav entry was only shown when the roster already had more than one
enabled Agent, but that entry is the only way to add a second Agent — a
chicken-and-egg trap that left single-Agent installs unable to opt into a
team. Always surface it.
Also render the CowAgent brand logo for the default Agent's avatar instead of
a bare initial disc, so the built-in Agent's face matches the app.
Co-authored-by: cowagent <cow@cowagent.ai>
Add deepseek-flash (DeepSeek V4.1 Flash, natively multimodal) and make it the
global default across config templates, provider recommended lists, startup
scripts and docs. Recognize it for thinking mode / reasoning_effort, the large
context+output window, and native vision; keep deepseek-v4-flash / -pro for
backward compatibility.
Co-authored-by: cowagent <cow@cowagent.ai>
Route gpt-6-astra tool calls through the Responses API without depending on
the openai SDK, translating requests/streaming back to the chat-completion
shape consumed downstream. Also surface it in the console dropdown, README
and docs, and reorder the recommended list (gpt group ahead of gemini).
Co-authored-by: cowagent <cow@cowagent.ai>
Disabling chat_fallback in the UI only rewrote config; a model that had already switched to the backup kept its engaged fallback state on the long-lived AgentLLMModel, so the backup model kept answering until a run boundary reset it (or a restart). Now clear the engaged fallback across all live agents right when the fallback is turned off.
Co-authored-by: cowagent <cow@cowagent.ai>
Some backends (e.g. Agnes) return an empty b64_json alongside a valid url in URL output mode, which decoded to a 0-byte file. Check value non-emptiness instead of key presence.
Co-authored-by: cowagent <cow@cowagent.ai>
The Win7 legacy pipeline pins Electron to 22, whose typings don't export
webUtils, so the static named import failed tsc with TS2305. Look up
webUtils off the runtime electron module instead, with an optional-chained
call and existing try/catch fallback.
Co-authored-by: cowagent <cow@cowagent.ai>
Solo chats can only @ files, so the composer placeholder now falls back to
the file-only hint and switches to the Agent-or-file hint when the session
has other members (web and desktop). Also relocalize the Agents workbench on
language switch and add the task-name placeholder i18n key.
Co-authored-by: cowagent <cow@cowagent.ai>
Add the v2.1.8 release notes in zh/en/ja, plus Agent team, agent_delegate
and subagent tool pages. Translate the multi-agent docs to en/ja to match
the restructured zh version, update navigation and the changelog overview,
and point the docs images to the cowagent-assets CDN.
Co-authored-by: cowagent <cow@cowagent.ai>
Skill cards now show a pencil icon that opens the skill directly in edit
mode (clicking the card body still opens the read-only view). The skill
viewer parses YAML frontmatter into a titled header block instead of
feeding it to the markdown renderer, which turned the header into an
oversized bold heading. Applies to both web and desktop.
Co-authored-by: cowagent <cow@cowagent.ai>
Drop the misleading "config will be preserved" wording and state the actual
effect: the channel stops receiving messages. Applied to both the web console
(zh / zh-Hant / en) and the desktop app (zh / en).
Co-authored-by: cowagent <cow@cowagent.ai>
In a group chat every Agent cached its own message list and only restored
it on first init, so a teammate missed later turns spoken by someone else
(and the host missed guest replies). Reload the shared transcript before
each turn in a team conversation.
Replay a colleague's past reply as a `Name(@id):` user turn instead of an
`[Name]` assistant prefix, so the model has nothing to copy into its own
reply, and strip any speaker label it still echoes. Tell it in the prompt
not to prefix its reply or expose teammate ids to the user.
Also refresh team.mdx with real screenshots.
Co-authored-by: cowagent <cow@cowagent.ai>
The desktop app disconnected a channel the instant the button was clicked,
with no confirmation — unlike the web console, which has always asked first.
Add a confirm dialog (via the shared askConfirm store) before disconnect, and
the channels_disconnect_confirm i18n string it needs.
Co-authored-by: cowagent <cow@cowagent.ai>
Disconnecting a channel had no effect on both the web console and the
desktop app. In multi-Agent mode a legacy channel (enabled via config.json
channel_type) is folded into channel_instances by bootstrap_legacy_instances,
which runs on every team.json write. Removing the instance record alone was
immediately undone: the next write re-materialized it straight from
channel_type, so the card came right back.
- Prune the type from channel_type when disconnecting its last instance, so
the bootstrap has nothing to recreate.
- Route disconnect/rename without an instance_id to the legacy per-type path
instead of rejecting it with "instance_id is required".
- Surface disconnect failures on both clients (web toast, desktop status)
and refresh the desktop list silently so it no longer jumps to the top.
- Show WeChat channels first in the connected list (stable sort).
- Add regression tests covering the dispatch routing and the bootstrap
re-materialization bug.
Co-authored-by: cowagent <cow@cowagent.ai>
Add keenable to the dedicated-key providers, generalize the anonymous
opt-in flow (anysearch + keenable) and add the missing i18n title/desc
so the desktop search modal no longer shows raw i18n keys.
Co-authored-by: cowagent <cow@cowagent.ai>
feat: add Keenable as an opt-in web_search provider
Thanks @ilya-bogin-keenable (#3122). Resolved conflicts against the
Tavily/SearXNG providers merged earlier; all nine backends coexist.
Co-authored-by: cowagent <cow@cowagent.ai>
Add Tavily and SearXNG to the v2.1.8 release notes and split the search
provider entry into one bullet per backend.
Co-authored-by: cowagent <cow@cowagent.ai>
Extend the desktop search credential UI to cover the two new providers,
matching the web console: Tavily uses the shared API-key modal, and SearXNG
gets an instance-URL field (prefilled from url_masked, no masking). Adds the
needs_url/url_masked types and the tavily/searxng i18n strings.
Co-authored-by: cowagent <cow@cowagent.ai>
Align the desktop client's search credential UI with the web console: show all
dedicated-key providers (bocha/anysearch/serply) as editable chips plus an add
entry, support AnySearch anonymous mode, and generalize the key modal.
Co-authored-by: cowagent <cow@cowagent.ai>
Scheduler execution history improvements across web console and desktop:
- Paginate the run history list with a 'load more' button (30 per page),
backed by an offset parameter on list_runs and the runs API.
- Delete a single run record via a trash button on each card with a
confirm dialog, backed by a new delete_run store method and API.
- Keep the runs row a light index: output_preview stays a 1000-char
snippet and now appends an ellipsis when truncated so a clipped preview
never reads as the whole message.
- Raise the session-copy cap (the detail view's full body source) from
2000 to 4000 chars, with an ellipsis on overflow.
- Render the run-detail body as markdown by scoping it to .msg-content so
paragraphs, headings and lists show correctly; add a preview/raw toggle
on desktop to match web.
Co-authored-by: cowagent <cow@cowagent.ai>
- Tasks/Records tabbed view with an execution-records list + detail modal;
run-detail resolves channel type/name and owning agent.
- Channel cards match the web console (instance friendly name as title with
inline rename, type · id as subtitle).
- ConfirmDialog renders above the modal that spawned it (elevated z-index).
- Scheduler notifications come from a single global runs poll
(useSchedulerNotifyPoll), deduped by run id, so a scheduled push into any
session surfaces exactly once.
Co-authored-by: cowagent <cow@cowagent.ai>
- Web is always a valid delivery target and always persists to conversation
history (an idle /poll client has no other surface after a restart), instead
of gating on the in-memory session_queues.
- Record instance_id on each run and raise the output_preview cap to 500 so the
history detail view has enough context.
- list_tasks orders enabled-first then newest-created, so a freshly added task
surfaces at the top.
Co-authored-by: cowagent <cow@cowagent.ai>
Web console additions and fixes for scheduled-task delivery:
- Execution-records tab + list + detail modal, backed by a new
/api/scheduler/runs(+/detail) endpoint over the global runs ledger
(list_runs gains agent_id/since filters; get_run_detail recovers the full
delivered body from the receiver session).
- Run-detail output renders Markdown by default with a preview/text toggle.
- type=web shows channel type 'web' / name '网页'.
- Notifications: pop exactly once per run across all browser tabs
(localStorage + BroadcastChannel dedup); route a manual run's notice back to
the triggering tab; on click, broadcast open-session so whichever tab the
browser foregrounds lands on the right conversation; pass agent_id so
cross-agent notification clicks switch correctly.
Co-authored-by: cowagent <cow@cowagent.ai>