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>
CONTRIBUTING asks for English in code comments and documentation so the
project stays accessible to a global community; this file was the one piece
of the console split written in Chinese.
The per-file line counts, which went stale with every edit, are dropped from
the tables in favour of a responsibility column only.
Co-authored-by: cowagent <cow@cowagent.ai>
Nine upstream commits landed after the previous merge; the only file they
touched that this branch also touches is console.js, which the split deleted.
Its three hunks -- the self-evolution bubble dedupe on history reload (an
evolutionContentKey helper, the data-evolution-key tag in createBotMessageEl,
and the skip in loadHistory) -- were routed into chat/render.js, the slice
that owns both functions.
Verified the same way as before: render.js gains exactly the 27 lines
console.js did upstream, every new symbol is defined once in the tree, and the
suite fails exactly the same 57 tests that already fail on upstream master in
this environment.
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>
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>
Nine upstream commits landed after the previous merge; the only file they
touched that this branch also touches is console.js, which the split deleted.
Its three hunks -- the self-evolution bubble dedupe on history reload (an
evolutionContentKey helper, the data-evolution-key tag in createBotMessageEl,
and the skip in loadHistory) -- were routed into chat/render.js, the slice
that owns both functions.
Verified the same way as before: render.js gains exactly the 27 lines
console.js did upstream, every new symbol is defined once in the tree, and the
suite fails exactly the same 57 tests that already fail on upstream master in
this environment.
Co-authored-by: cowagent <cow@cowagent.ai>
Routing landed on the hash a commit ago because the page referenced every
asset relatively, which a path route would resolve against the routed path and
404. That was the wrong thing to work around: the console reads as an app, so
its address bar should too. Make the references absolute and the constraint
goes away.
The console now answers at / rather than redirecting to /chat, and each view is
a path under it: /agents, /settings, /settings/models. /chat stays as a
redirect, since it is what older bookmarks and the startup banner point at.
The settings view is /settings, not /config: /config is already the config API,
which both this console and the desktop client call. The view routes sit last
in the URL table so web.py's first-match rule keeps any view name from
shadowing an endpoint, and a test pins that collision so it cannot return
quietly. Unknown paths still 404 rather than being swallowed by the console.
This costs no reverse-proxy subpath mount, because there was never one to
lose: every API call the console makes is already an absolute /api/... path, so
it could not be served from under a prefix either way. Supporting one means
prefixing all of them, not keeping asset references relative.
Co-authored-by: cowagent <cow@cowagent.ai>
The console kept its whole location in memory, so a reload always dropped the
user back on the chat view, a link to the settings page could not be shared,
and Back left the console entirely instead of returning to the previous view.
Mirror the view and its tab into location.hash: #config, #config/models. The
hash rather than the path, because every asset in the page is referenced
relatively -- a path route like /chat/config would resolve those against
/chat/ and 404 the entire console, and would need the backend route and any
reverse proxy to change with it. This way nothing outside the frontend moves.
navigateTo stays the single funnel it already was; it now takes the tab the
route named and reports whether the unsaved-edit guard refused, which is what
lets the router put the address bar back after a Back it could not honour.
Tab switches replace the current history entry rather than stacking one, so
Back leaves a view instead of walking every tab visited inside it.
Routing depth stops at the tab. Which session is open and which file the
editor holds stay out of the URL: they are already restored from localStorage,
and putting them in would rewrite the address bar on every click in the list.
tools/check-router.mjs drives the router against a stubbed location/history and
asserts what lands on the history stack -- it already caught re-entering the
current view stacking an entry per click, which made Back look broken.
Co-authored-by: cowagent <cow@cowagent.ai>
The asset stamp was the wall clock, so all 46 first-party scripts and
stylesheets changed URL on every request and the browser refetched ~1MB on
every reload. The vendored bundles, fonts and logos were worse off: the handler
sent no validator at all, so a browser had nothing to cache them on either.
Stamp each asset with its own mtime instead. A URL then changes only when the
file behind it does, which is what makes it content-addressed, so the handler
can hand those out under a long immutable cache without ever stranding a
browser on stale code. Everything unstamped gets an ETag and answers
If-None-Match with 304: still asked for on every reload, but a changed vendor
bundle arrives immediately and an unchanged one costs a header, not the file.
Measured against the handler over HTTP: a warm reload drops from 1011KB of
bodies to zero, and a stale stamp still serves the file rather than 404ing, so
any page a browser is already holding keeps working.
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 split deleted console.js and console.css and cut chat.html down to a
shell, so the 92 upstream commits that changed those three files had nothing
left to merge against. Every upstream hunk was routed into the slice that owns
its region instead:
- 69 console.js hunks into core/, chat/ and views/ (i18n, models, agents,
skills, channels, sessions, tasks and the chat composer).
- The one-click updater landed next to the version badge in core/version.js,
and the auth gate for the background pollers next to the rest of auth in
core/auth.js, rather than where they happened to sit in the single file.
- 7 console.css hunks into the per-area stylesheets; the update menu's styles
sit in components.css with the other shared popovers.
- 13 chat.html hunks into the templates/ fragments.
- test_web_console_update.py, added upstream, now asserts against the rendered
page and the concatenated scripts like the other console tests do.
Verified four ways: the split totals still track the pre-split originals line
for line, all 147 symbols upstream added are defined somewhere in the tree,
check-load-order.mjs reports no new ordering problems, and the suite fails
exactly the same 57 tests that already fail on upstream master in this
environment.
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>
Reuse the cow update git/pip path from the sidebar version panel, check
GitHub only on click, back up user data first, and restart through a
detached worker so a failed import never kills the running service.
Fixes#3148
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>
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>
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 backend has carried a per-provider model catalog for a while — entries
with capability tags, a context window and a max output budget — and the
models API already returns both what is saved (catalog) and the vendor's
presets typed with their real capabilities (seed). Nothing rendered them, so
the only way to use the feature was hand-editing config.json.
Add an editor to both provider modals, collapsed under an "Advanced" heading
since it is opt-in: a catalog REPLACES a vendor's preset model list, so the
section stays shut until the user has a reason to open it.
- rows for each entry: name, capability chips, context window, max output
- "restore presets" seeds the draft from the vendor's preset models, so
editing an override starts from the real values instead of a blank row
- embedding / image / asr / tts entries have no text budget, so their window
and output inputs are hidden — and cleared if the tags change — rather than
inviting numbers that mean nothing
- the row list scrolls on its own and each modal is height-capped, so a long
catalog cannot push the save button off screen
- saved as a separate request, and only when the draft differs from what is
stored: writing it on every credentials save would silently wipe a list the
user never opened
Tests: 21 covering the seed/catalog fields the editor reads, the save
handler it posts to, and the normalization rules it relies on.
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>
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>
The console could not start a channel configured from the UI: saving a
channel whose credentials had just been filled in (e.g. a QQ bot with a
valid AppID/AppSecret) returned success, then logged
[WebChannel] ChannelManager not available, cannot start 'qq'
and never started it, so the platform showed the bot as disconnected.
commit 87706bee (the fix for #3120) moved the manager out of app.py's
module globals into common.channel_registry, because `python app.py`
makes `__main__` a distinct module object from a later `import app`, and
reading `_channel_mgr` off `__main__` could silently yield None. That
migration missed channel/web/web_channel.py, which still looked the
manager up with
getattr(sys.modules['__main__'], '_channel_mgr', None)
`_channel_mgr` no longer exists anywhere, so every console-side lookup
returned None: connect, disconnect, restart-on-save, the WeChat login
status badge, and channel-instance binding were all affected.
Resolve the manager through common.channel_registry at every one of those
call sites, via a small `_live_channel_manager()` helper, and add
regression tests that fail against the previous lookup.
Add the two new OpenAI image models and make gpt-image-2.5-flare the
recommended default for OpenAI and LinkAI.
- generate.py: list both ids in the provider docstring, promote the
`gpt-image-2.5` prefix in _MODEL_PREFERRED_PROVIDER so it keeps routing
to OpenAI, and switch OpenAIProvider/LinkAIProvider DEFAULT_MODEL from
gpt-image-2 to gpt-image-2.5-flare.
- web_channel.py: expose both ids in the OpenAI and LinkAI model lists and
update _IMAGE_AUTO_ORDER so the predicted default matches the runtime.
- SKILL.md and docs (en/zh/ja): document the new models and the new default.
- tests: add regression coverage for prefix routing and the default model.
Flare is OpenAI's recommended model for mainstream use (same quality with
up to 50% lower latency); Sunburst targets high-precision editing.
gpt-image-2 / gpt-image-1 remain fully supported.
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>
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>
The frontend split replaced three files with 47, and there was no way to put
the two versions side by side against real data.
\python app.py -old\ serves the old page instead, sharing the backend and so
the same sessions and history. It reproduces the old ChatHandler rather than
reusing the current renderer, including its hardcoded asset list: the point of
the flag is to serve that page as it actually was.
The snapshot is checked out of git by channel/web/tools/snapshot_legacy.py and
is gitignored. Committing a second copy of a superseded 1MB frontend would
only rot, and git history already holds the one that matters. Starting with
-old and no snapshot renders instructions rather than a blank page.
The flag is read per request and fails closed, so it cannot swap the console
out of a normal install.
Co-authored-by: cowagent <cow@cowagent.ai>
The console.js split reordered the scripts into core, chat and views. That
put views/agents.js after chat/state.js, and chat/state.js resolves sessionId
while it runs: activeSessionStorageKey() compares activeAgentId against
defaultAgentId, a \let\ declared in views/agents.js.
Reading it that early hits its temporal dead zone, so chat/state.js threw
partway through. Everything below the throw never ran, which is sessionId and
every composer element reference, and those bindings then stay uninitialised
for the life of the page: no restored conversation, no sending, no
attachments. The comparison short-circuits on a falsy activeAgentId, so it
only broke for people who had selected an Agent.
core/auth.js moves back to the end of the core layer too. It wraps
window.fetch for 401 handling and chat/state.js wraps it to append agent_id;
installing the 401 wrapper second keeps it outermost, as it was before.
The split check only looked at identifiers a top-level statement named
directly, which is why neither of these showed up. Adds
channel/web/tools/check-load-order.mjs, which follows calls transitively -
the reference here is three frames down - and reports scripts that wrap the
same property. Verified it fails on the reintroduced bug.
Co-authored-by: cowagent <cow@cowagent.ai>
navigateTo was declared, then reassigned 11800 lines later to a wrapper that
added the unsaved-edit guard and the per-view lazy loading. Which version a
caller got depended on whether it ran before or after that line, and nothing
would have reported it if the split had moved the two apart.
The wrapper is now the declaration and the original body is _switchToView.
This was checked to be behaviour-preserving: the only top-level code that read
navigateTo while running was the wrapper installation itself, so no load-time
caller ever saw the unwrapped version.
That was the last top-level assignment to an existing global in the frontend,
so the hazard class is now empty rather than merely accounted for.
Co-authored-by: cowagent <cow@cowagent.ai>
composer.js came out of the console.js split at 2767 lines, still holding
per-session settings, drag-and-drop, slash commands, sending, SSE streaming
and message rendering.
Split along the seams already visible in the file into seven scripts. As
before, nothing was rewritten: the 89 top-level declarations are unchanged
and no part reaches a declaration that moved into a later file.
Also adds tests for the wiring the split depends on. There is no bundler
here, so a script left off the page or listed in the wrong order fails only
in a browser: the tests pin that every script on disk is loaded exactly once,
that core/ precedes views/, that boot.js runs last but ahead of workspace.js,
and that no two scripts declare the same global.
Co-authored-by: cowagent <cow@cowagent.ai>
console.js was 16149 lines and 757KB: everything from the translation table
to the knowledge graph lived in it, so locating any feature meant a search
and every frontend change collided in the same file.
It is now 30 files under core/, chat/ and views/, following the dividers the
file already had. Nothing was rewritten, only moved.
Splitting classic scripts has two failure modes and both were checked
mechanically before writing anything out:
- A top-level call can reach a function declared further down in one file,
because declarations hoist; across scripts the later file has not run yet.
There was exactly one such call, /config + startPolling at L4412, and it
moved to boot.js which loads last.
- All 709 top-level declarations are implicit globals, which generated markup
reaches through inline onclick. The set is unchanged.
boot.js sits before workspace.js, where console.js used to be: applyI18n()
probes for relocalizeWorkspacePanel with a typeof guard and has always run
before workspace.js defined it.
Tests that read console.js now use a conftest helper that concatenates the
scripts in the order the page lists them, so it cannot fall behind the split.
Co-authored-by: cowagent <cow@cowagent.ai>
chat.html was 2118 lines holding nine views, twelve modals and the whole
layout, so any markup change meant scrolling to find the right region and
every frontend commit touched the same file.
The shell is now 157 lines of page skeleton plus include markers; the views,
layout regions and modals live in templates/ as 22 fragments. Assembly is
server-side and the assembled page is byte-identical to the previous file.
Tests that asserted on chat.html now assert on the assembled page, which is
what the browser actually receives.
Co-authored-by: cowagent <cow@cowagent.ai>
console.css had grown to 3908 lines covering everything from scrollbars to
the agent roster, so every style change had to be located by scrolling.
Split by area, keeping every rule verbatim. The load order in the shell is
load-bearing and was chosen so that no two rules which can style the same
element change relative position: sessions.css has to precede components.css
because both size .agent-avatar at equal specificity.
Dark mode here is .dark ancestor selectors interleaved with the light rules
rather than a separate layer, so paired rules moved together.
Co-authored-by: cowagent <cow@cowagent.ai>
Two console bugs found while configuring a chain by hand.
The fallback card built its model lists straight from PROVIDER_MODELS while
the primary card ran them through _apply_catalog, so the two pickers drifted
apart: a vendor whose models came from a catalog offered a dropdown on the
primary card but only "custom" + a typed name on the fallback one. Both cards
now share one path — the vendors' presets as the base, a catalog overriding
them — which is what _chat_provider_models' own docstring claimed already
happened. The presets have to stay as the base: the primary card is driven by
its separate `providers` list, but the fallback has no such layer, so starting
from an empty map emptied the picker for every vendor without a catalog.
Separately, Save disabled itself only when the enable toggle was flipped, so
its state went stale after any edit to the chain: enabling the fallback with
no links left it disabled, and adding a link never re-enabled it. The refresh
now runs at the end of every re-render, including the empty-chain branch that
returns early — removing the last link is exactly when it must flip back.
The console page is a single 2k-line chat.html and every frontend change
lands in it, so the next step is to split it into fragments. Add the
server-side include mechanism that split needs, plus two fixes it depends on:
- Cache-busting matched a hardcoded list of four asset paths, so any new
script or stylesheet would have been served without a version query and
could be answered from a stale browser cache. Match assets/js and
assets/css by pattern instead, leaving pinned assets/vendor alone.
- WebChannel.chat_page() had no callers and read chat.html directly, which
after the split would return markup with unexpanded include markers.
Output is byte-identical to the previous handler.
Co-authored-by: cowagent <cow@cowagent.ai>
The chat fallback could only ever hold a single backup model: once
fallback_available() saw a fallback engaged it returned False for the rest of
the turn, so max_switches was never actually reachable and a second backup
could not be configured at all.
Replace the single {provider, model, max_switches} entry with an ordered chain
of links. A failing link advances to the next one rather than ending the turn,
and the walk wraps around for a second pass so a transient failure (a rate
limit that has since cleared) does not cost the user the whole turn.
- config: chat_fallback is now {enabled, chain: [{provider, model}, ...]};
_migrate_chat_fallback() folds a pre-chain entry into a one-link chain at
load time, so existing configs keep working and max_switches is dropped
- model: fallback_available()/use_fallback() walk the chain by index;
links duplicating the primary or an earlier link are skipped
- executor: a link gets one attempt rather than the primary's full retry
budget, so a long chain cannot sleep past the web channel's SSE idle
timeout; when the chain is spent the error names every model that was
tried instead of only the last one
- a rate limit switches immediately when a backup is configured, and keeps
waiting it out when there is none
- web console + desktop: edit the chain as a list with add/remove/reorder
Tests: 68 covering config migration, chain walking, the rate-limit fast path,
and the nested-run scope that keeps a sub agent from rewinding its parent.
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>
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>
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>